From 350751f3769cd32e3f1577f31bb0ae3a623245c2 Mon Sep 17 00:00:00 2001 From: Josephat-S Date: Mon, 25 May 2026 19:51:26 +0200 Subject: [PATCH 01/22] add commander dependency and add cli flag support --- index.js | 320 ++++++++++++++++++++++++++++++++++++---------- package-lock.json | 10 ++ package.json | 3 +- src/generate.js | 24 ++-- 4 files changed, 275 insertions(+), 82 deletions(-) diff --git a/index.js b/index.js index 667bcfc..3a49579 100755 --- a/index.js +++ b/index.js @@ -1,66 +1,173 @@ #!/usr/bin/env node +import { Command } from 'commander'; import inquirer from 'inquirer'; import chalk from 'chalk'; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; import { generateProject } from './src/generate.js'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8')); + const TEMPLATES = { portfolio: { - label: "Portfolio Website", - variants: ["Minimal Dark", "Creative Agency", "Developer Folio", "Photography", "Resume Style"], - sidebarOpts: false + label: 'Portfolio Website', + variants: ['Minimal Dark', 'Creative Agency', 'Developer Folio', 'Photography', 'Resume Style'], + sidebarOpts: false, }, ecommerce: { - label: "E-Commerce Store", - variants: ["Fashion Store", "Electronics Shop", "Food & Grocery", "Digital Products", "Multi-Vendor"], - sidebarOpts: true + label: 'E-Commerce Store', + variants: ['Fashion Store', 'Electronics Shop', 'Food & Grocery', 'Digital Products', 'Multi-Vendor'], + sidebarOpts: true, }, school: { - label: "School Management", - variants: ["University Portal", "K-12 System", "Online Academy", "Training Platform", "LMS Dashboard"], - sidebarOpts: true + label: 'School Management', + variants: ['University Portal', 'K-12 System', 'Online Academy', 'Training Platform', 'LMS Dashboard'], + sidebarOpts: true, }, saas: { - label: "SaaS Dashboard", - variants: ["Analytics Tool", "CRM System", "Project Manager", "Finance Tracker", "HR Platform"], - sidebarOpts: true + label: 'SaaS Dashboard', + variants: ['Analytics Tool', 'CRM System', 'Project Manager', 'Finance Tracker', 'HR Platform'], + sidebarOpts: true, }, blog: { - label: "Blog / Magazine", - variants: ["Tech Blog", "Lifestyle Mag", "News Portal", "Personal Journal", "Tutorial Site"], - sidebarOpts: true - } + label: 'Blog / Magazine', + variants: ['Tech Blog', 'Lifestyle Mag', 'News Portal', 'Personal Journal', 'Tutorial Site'], + sidebarOpts: true, + }, }; +const VALID_TEMPLATES = Object.keys(TEMPLATES); +const VALID_ARCHS = ['nextjs-monolith', 'vite-react', 'nextjs-turborepo']; +const ARCH_ALIASES = { nextjs: 'nextjs-monolith', vite: 'vite-react', turborepo: 'nextjs-turborepo' }; const DESIGNS = [ - "Minimal Clean", "Dark Terminal", "Glassmorphism", "Brutalist", - "Soft Pastel", "Corporate Blue", "Neon Cyberpunk", "Earth Tones" + 'Minimal Clean', 'Dark Terminal', 'Glassmorphism', 'Brutalist', + 'Soft Pastel', 'Corporate Blue', 'Neon Cyberpunk', 'Earth Tones', ]; -async function runOpusifyWizard() { +function resolveArch(value) { + if (!value) return undefined; + const lower = value.toLowerCase(); + if (ARCH_ALIASES[lower]) return ARCH_ALIASES[lower]; + if (VALID_ARCHS.includes(lower)) return lower; + return null; +} + +function resolveDesign(value) { + if (!value) return undefined; + const lower = value.toLowerCase(); + const match = DESIGNS.find((d) => d.toLowerCase() === lower); + return match || null; +} + +function printBanner() { console.clear(); console.log(chalk.green('❯ npx opusify create')); console.log(chalk.green(' ██████╗ ██████╗ ██╗ ██╗███████╗██╗███████╗██╗ ██╗')); - console.log(chalk.blue(` Welcome to ${chalk.magenta('Opusify')} — The Full-Stack Scaffold Engine v1.0.0`)); + console.log(chalk.blue(` Welcome to ${chalk.magenta('Opusify')} — The Full-Stack Scaffold Engine v${pkg.version}`)); console.log(chalk.gray(' Generate production-ready apps with one command.\n')); +} - let config; - try { - config = await inquirer.prompt([ - // NEW STEP: Project Name - { +async function createAction(projectName, options) { + printBanner(); + + // Validate flags early + if (options.template && !VALID_TEMPLATES.includes(options.template)) { + console.log(chalk.red(`\n✖ Invalid template: "${options.template}"`)); + console.log(chalk.gray(` Valid options: ${VALID_TEMPLATES.join(', ')}`)); + process.exit(1); + } + + if (options.arch) { + const resolved = resolveArch(options.arch); + if (resolved === null) { + console.log(chalk.red(`\n✖ Invalid architecture: "${options.arch}"`)); + console.log(chalk.gray(` Valid options: nextjs, vite, turborepo (or nextjs-monolith, vite-react, nextjs-turborepo)`)); + process.exit(1); + } + options.arch = resolved; + } + + if (options.design) { + const resolved = resolveDesign(options.design); + if (resolved === null) { + console.log(chalk.red(`\n✖ Invalid design: "${options.design}"`)); + console.log(chalk.gray(` Valid options: ${DESIGNS.join(', ')}`)); + process.exit(1); + } + options.design = resolved; + } + + if (options.nav !== undefined) { + const nav = parseInt(options.nav, 10); + if (isNaN(nav) || nav < 3 || nav > 9) { + console.log(chalk.red('\n✖ Invalid nav count. Must be a number between 3 and 9.')); + process.exit(1); + } + options.nav = nav; + } + + // Build the list of prompts, skipping any that were provided via flags + const prompts = []; + const defaults = { + projectName: projectName || 'my-opusify-app', + template: 'portfolio', + variant: null, // resolved after template is known + architecture: 'nextjs-monolith', + design: 'Minimal Clean', + navCount: 5, + includeSidebar: false, + initGit: true, + enableSecurity: true, + }; + + // If --yes, use all defaults + any provided flags + if (options.yes) { + const template = options.template || defaults.template; + const config = { + projectName: projectName || defaults.projectName, + template, + variant: options.variant || TEMPLATES[template].variants[0], + architecture: options.arch || defaults.architecture, + design: options.design || defaults.design, + navCount: options.nav || defaults.navCount, + includeSidebar: options.sidebar || defaults.includeSidebar, + initGit: options.git !== false, + enableSecurity: defaults.enableSecurity, + noInstall: options.install === false, + }; + + console.log(chalk.green('✔ Using defaults (--yes mode)')); + console.log(chalk.gray(` Project: ${config.projectName}`)); + console.log(chalk.gray(` Template: ${config.template} / ${config.variant}`)); + console.log(chalk.gray(` Architecture: ${config.architecture}`)); + console.log(chalk.gray(` Design: ${config.design}`)); + console.log(''); + + await generateProject(config); + return; + } + + // Interactive prompts — skip those already provided via flags + if (!projectName) { + prompts.push({ type: 'input', name: 'projectName', message: chalk.magenta.bold('What is your project name?'), - default: 'my-opusify-app', + default: defaults.projectName, validate: (input) => { if (!/^[a-z0-9-]+$/.test(input)) { return 'Please use only lowercase letters, numbers, and hyphens (e.g., my-awesome-app)'; } return true; - } - }, - // Step 1: Template - { + }, + }); + } + + if (!options.template) { + prompts.push({ type: 'rawlist', name: 'template', message: chalk.magenta.bold('Select a project template:'), @@ -69,71 +176,95 @@ async function runOpusifyWizard() { { name: 'E-Commerce Store', value: 'ecommerce' }, { name: 'School Management', value: 'school' }, { name: 'SaaS Dashboard', value: 'saas' }, - { name: 'Blog / Magazine', value: 'blog' } - ] - }, - // Step 2: Variant - { + { name: 'Blog / Magazine', value: 'blog' }, + ], + }); + } + + // Variant — skip if provided via flag + if (!options.variant) { + prompts.push({ type: 'rawlist', name: 'variant', message: chalk.magenta.bold('Choose a variant style:'), choices: (answers) => { - if (!TEMPLATES[answers.template]) return ["Default"]; - return TEMPLATES[answers.template].variants; - } - }, - // Step 3: Architecture - { + const tmpl = options.template || answers.template; + if (!TEMPLATES[tmpl]) return ['Default']; + return TEMPLATES[tmpl].variants; + }, + }); + } + + if (!options.arch) { + prompts.push({ type: 'rawlist', name: 'architecture', message: chalk.magenta.bold('Choose architecture:'), choices: [ { name: 'Next.js 14 — App Router (Recommended)', value: 'nextjs-monolith' }, { name: 'Vite + React 18 — SPA', value: 'vite-react' }, - { name: 'Turborepo — Monorepo (Enterprise)', value: 'nextjs-turborepo' } - ] - }, - // Step 4: Design System - { + { name: 'Turborepo — Monorepo (Enterprise)', value: 'nextjs-turborepo' }, + ], + }); + } + + if (!options.design) { + prompts.push({ type: 'rawlist', name: 'design', message: chalk.magenta.bold('Choose design system:'), - choices: DESIGNS - }, - // Step 5: Navigation Config - { + choices: DESIGNS, + }); + } + + if (options.nav === undefined) { + prompts.push({ type: 'number', name: 'navCount', message: chalk.cyan.bold('How many navigation links? (3-9)'), - default: 5, - validate: (input) => input >= 3 && input <= 9 ? true : 'Please enter a number between 3 and 9' - }, - // Step 6: Sidebar Config - { + default: defaults.navCount, + validate: (input) => (input >= 3 && input <= 9 ? true : 'Please enter a number between 3 and 9'), + }); + } + + // Sidebar prompt — only if template supports it and flag not provided + if (options.sidebar === undefined) { + prompts.push({ type: 'confirm', name: 'includeSidebar', message: chalk.cyan.bold('Include a sidebar layout?'), default: false, when: (answers) => { - if (!TEMPLATES[answers.template]) return false; - return TEMPLATES[answers.template].sidebarOpts; - } - }, - // Step 7: Git Init Config - { + const tmpl = options.template || answers.template; + if (!TEMPLATES[tmpl]) return false; + return TEMPLATES[tmpl].sidebarOpts; + }, + }); + } + + if (options.git !== false) { + prompts.push({ type: 'confirm', name: 'initGit', message: chalk.cyan.bold('Initialize a new Git repository?'), - default: true - }, - // Step 8: Security Config (NEW) - { + default: true, + }); + } + + // Only ask security prompt in interactive mode (when not all flags provided) + const allFlagsProvided = options.template && options.variant && options.arch && options.design && options.nav !== undefined; + if (!allFlagsProvided) { + prompts.push({ type: 'confirm', name: 'enableSecurity', message: chalk.red.bold('Enable Enterprise Security Hardening (Zod env validation & CSP headers)?'), - default: true - } - ]); + default: true, + }); + } + + let answers; + try { + answers = await inquirer.prompt(prompts); } catch (error) { if (error.name === 'ExitPromptError' || error.message?.includes('User force closed')) { console.log(chalk.yellow('\nScaffold cancelled. Goodbye!')); @@ -142,10 +273,57 @@ async function runOpusifyWizard() { throw error; } + // Merge flags with interactive answers + const config = { + projectName: projectName || answers.projectName, + template: options.template || answers.template, + variant: options.variant || answers.variant, + architecture: options.arch || answers.architecture, + design: options.design || answers.design, + navCount: options.nav || answers.navCount, + includeSidebar: options.sidebar !== undefined ? options.sidebar : (answers.includeSidebar || false), + initGit: options.git === false ? false : (answers.initGit !== undefined ? answers.initGit : true), + enableSecurity: answers.enableSecurity !== undefined ? answers.enableSecurity : true, + noInstall: options.install === false, + }; + console.log('\n' + chalk.green('✔ Configuration collected successfully!')); - - // Hand off the config to the generation engine! await generateProject(config); } -runOpusifyWizard(); \ No newline at end of file +// CLI setup +const program = new Command(); + +program + .name('opusify') + .description('The Full-Stack Scaffold Engine — Generate production-ready apps with one command.') + .version(pkg.version); + +program + .command('create') + .description('Scaffold a new project') + .argument('[project-name]', 'Name of the project to create') + .option('-t, --template