diff --git a/README.md b/README.md index 23cbadd..1536098 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,28 @@ Add a new service later: create-polyglot add service payments --type node --port 4100 ``` +### New Interactive Flow (Dynamic Service Count) +If you omit `--services`, the CLI now asks: +1. "How many services do you want to create?" (enter a number) +2. For each service: choose a type (Node, Python, Go, Java, Frontend) +3. Optionally enter a custom name (blank keeps the default type name) +4. Optionally override the suggested port (blank keeps default) + +Example (interactive): +```bash +create-polyglot init my-org +# > How many services? 3 +# > Service #1 type: Node.js (Express) +# > Name for node service: api +# > Port for api (node) (default 3001): 4001 +# > Service #2 type: Python (FastAPI) +# > Name for python service: (enter) # keeps 'python' +# > Port for python (python) (default 3004): (enter) +# > Service #3 type: Frontend (Next.js) +# ... +``` +Non-interactive (`--yes`) still scaffolds exactly one `node` service by default for speed. + ## Installation Global (recommended for repeated use): ```bash diff --git a/bin/lib/scaffold.js b/bin/lib/scaffold.js index ec99b90..558dfe5 100644 --- a/bin/lib/scaffold.js +++ b/bin/lib/scaffold.js @@ -173,16 +173,58 @@ export async function scaffoldMonorepo(projectNameArg, options) { services.push({ type, name, port }); } } else { - const answer = await prompts({ - type: 'multiselect', - name: 'serviceTypes', - message: 'Select services to include:', - choices: allServiceChoices, - instructions: false, - min: 1 + // New dynamic interactive flow: ask how many services, then collect each. + const countAns = await prompts({ + type: 'number', + name: 'svcCount', + message: 'How many services do you want to create?', + initial: 1, + min: 1, + validate: v => Number.isInteger(v) && v > 0 && v <= 50 ? true : 'Enter a positive integer (max 50)' }); - const selected = answer.serviceTypes || []; - for (const type of selected) services.push({ type, name: type, port: defaultPorts[type] }); + const svcCount = countAns.svcCount || 1; + for (let i = 0; i < svcCount; i++) { + const typeAns = await prompts({ + type: 'select', + name: 'svcType', + message: `Service #${i+1} type:`, + choices: allServiceChoices.map(c => ({ title: c.title, value: c.value })), + initial: 0 + }); + const svcType = typeAns.svcType; + if (!svcType) { + console.log(chalk.red('No type selected; aborting.')); + process.exit(1); + } + const nameAns = await prompts({ + type: 'text', + name: 'svcName', + message: `Name for ${svcType} service (leave blank for default '${svcType}'):`, + validate: v => !v || (/^[a-zA-Z0-9._-]+$/.test(v) ? true : 'Use alphanumerics, dash, underscore, dot') + }); + let svcName = nameAns.svcName && nameAns.svcName.trim() ? nameAns.svcName.trim() : svcType; + if (reservedNames.has(svcName) || services.find(s => s.name === svcName)) { + console.log(chalk.red(`Name '${svcName}' is reserved or already used. Using '${svcType}'.`)); + svcName = svcType; + } + const portDefault = defaultPorts[svcType]; + const portAns = await prompts({ + type: 'text', + name: 'svcPort', + message: `Port for ${svcName} (${svcType}) (default ${portDefault}):`, + validate: v => !v || (/^\d+$/.test(v) && +v > 0 && +v <= 65535) ? true : 'Enter a valid port 1-65535' + }); + let svcPort = portDefault; + if (portAns.svcPort) { + const parsed = Number(portAns.svcPort); + if (services.find(s => s.port === parsed)) { + console.log(chalk.red(`Port ${parsed} already used; keeping ${portDefault}.`)); + } else if (parsed >=1 && parsed <= 65535) { + svcPort = parsed; + } + } + services.push({ type: svcType, name: svcName, port: svcPort }); + } } // Always allow customization of name & port in interactive mode (not nonInteractive) diff --git a/package-lock.json b/package-lock.json index 34bd17d..a3e5f5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-polyglot", - "version": "1.6.1", + "version": "1.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-polyglot", - "version": "1.6.1", + "version": "1.6.2", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index c19fc77..96ce098 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-polyglot", - "version": "1.6.1", + "version": "1.6.2", "description": "Scaffold polyglot microservice monorepos with built-in templates for Node, Python, Go, and more.", "main": "bin/index.js", "scripts": { diff --git a/tests/smoke.test.js b/tests/smoke.test.js index 868efad..781b1bb 100644 --- a/tests/smoke.test.js +++ b/tests/smoke.test.js @@ -20,7 +20,7 @@ describe('create-polyglot CLI smoke', () => { it('scaffolds a project with a node service', async () => { const repoRoot = process.cwd(); const cliPath = path.join(repoRoot, 'bin/index.js'); - await execa('node', [cliPath, projName, '--services', 'node', '--no-install', '--yes'], { cwd: tmpDir }); + await execa('node', [cliPath, projName, '--services', 'node', '--no-install', '--yes'], { cwd: tmpDir }); const projectPath = path.join(tmpDir, projName); expect(fs.existsSync(path.join(projectPath, 'services/node'))).toBe(true); expect(fs.existsSync(path.join(projectPath, 'package.json'))).toBe(true);