diff --git a/bin/index.js b/bin/index.js index f154f36..861ba02 100755 --- a/bin/index.js +++ b/bin/index.js @@ -3,6 +3,9 @@ import { Command } from 'commander'; import chalk from 'chalk'; import { scaffoldMonorepo, addService, scaffoldPlugin } from './lib/scaffold.js'; +import fs from 'fs'; +import path from 'path'; +import { renderServicesTable } from './lib/ui.js'; import { runDev } from './lib/dev.js'; const program = new Command(); @@ -105,5 +108,29 @@ program await runDev({ docker: !!opts.docker }); }); +program + .command('services') + .description('List services in the current workspace (table)') + .option('--json', 'Output raw JSON instead of table') + .action(async (opts) => { + try { + const cwd = process.cwd(); + const cfgPath = path.join(cwd, 'polyglot.json'); + if (!fs.existsSync(cfgPath)) { + console.log(chalk.red('polyglot.json not found. Run inside a generated workspace.')); + process.exit(1); + } + const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')); + if (opts.json) { + console.log(JSON.stringify(cfg.services, null, 2)); + } else { + renderServicesTable(cfg.services, { title: 'Workspace Services' }); + } + } catch (e) { + console.error(chalk.red('Failed to list services:'), e.message); + process.exit(1); + } + }); + program.parse(); diff --git a/bin/lib/scaffold.js b/bin/lib/scaffold.js index ffa095c..ec99b90 100644 --- a/bin/lib/scaffold.js +++ b/bin/lib/scaffold.js @@ -4,6 +4,7 @@ import fs from 'fs-extra'; import path from 'path'; import url from 'url'; import { execa } from 'execa'; +import { renderServicesTable, printBoxMessage } from './ui.js'; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); @@ -234,10 +235,12 @@ export async function scaffoldMonorepo(projectNameArg, options) { portMap.set(s.port, s.name); } - console.log(chalk.magenta('\nSummary:')); - console.table(services.map(s => ({ type: s.type, name: s.name, port: s.port }))); - console.log(`Preset: ${options.preset || 'none'}`); - console.log(`Package Manager: ${options.packageManager}`); + printBoxMessage([ + `Project: ${projectName}`, + `Preset: ${options.preset || 'none'} | Package Manager: ${options.packageManager}`, + 'Selected Services:' + ], { color: chalk.magenta }); + renderServicesTable(services.map(s => ({ ...s, path: `services/${s.name}` })), { title: 'Service Summary' }); let proceed = true; if (!nonInteractive) { const answer = await prompts({ @@ -318,23 +321,24 @@ export async function scaffoldMonorepo(projectNameArg, options) { console.log(chalk.green(`✅ Created ${svcName} (${svcType}) service on port ${svcPort}`)); } - const rootPkgPath = path.join(projectDir, 'package.json'); + const rootPkgPath = path.join(projectDir, 'package.json'); const rootPkg = { name: projectName, private: true, version: '0.1.0', - workspaces: ['apps/*', 'packages/*'], + workspaces: ['services/*', 'packages/*'], scripts: { dev: 'node scripts/dev-basic.cjs', - 'list:services': 'ls apps', + 'list:services': 'node scripts/list-services.mjs', format: 'prettier --write .', - lint: 'eslint "apps/**/*.{js,jsx,ts,tsx}" --max-warnings 0 || true' + lint: 'eslint "services/**/*.{js,jsx,ts,tsx}" --max-warnings 0 || true' }, devDependencies: { prettier: '^3.3.3', eslint: '^9.11.1', 'eslint-config-prettier': '^9.1.0', - 'eslint-plugin-import': '^2.29.1' + 'eslint-plugin-import': '^2.29.1', + chalk: '^5.6.2' } }; if (options.preset === 'turborepo') { @@ -346,9 +350,10 @@ export async function scaffoldMonorepo(projectNameArg, options) { } await fs.writeJSON(rootPkgPath, rootPkg, { spaces: 2 }); + // Always ensure scripts dir exists (needed for list-services script) + const scriptsDir = path.join(projectDir, 'scripts'); + await fs.mkdirp(scriptsDir); if (!options.preset) { - const scriptsDir = path.join(projectDir, 'scripts'); - await fs.mkdirp(scriptsDir); const runnerSrc = path.join(__dirname, '../../scripts/dev-basic.cjs'); try { if (await fs.pathExists(runnerSrc)) { @@ -358,6 +363,9 @@ export async function scaffoldMonorepo(projectNameArg, options) { console.log(chalk.yellow('⚠️ Failed to copy dev-basic runner:', e.message)); } } + // Create list-services script with runtime status detection + const listScriptPath = path.join(scriptsDir, 'list-services.mjs'); + await fs.writeFile(listScriptPath, `#!/usr/bin/env node\nimport fs from 'fs';\nimport path from 'path';\nimport net from 'net';\nimport chalk from 'chalk';\nconst cwd = process.cwd();\nconst cfgPath = path.join(cwd, 'polyglot.json');\nif(!fs.existsSync(cfgPath)){ console.error(chalk.red('polyglot.json not found.')); process.exit(1);}\nconst cfg = JSON.parse(fs.readFileSync(cfgPath,'utf-8'));\n\nfunction strip(str){return str.replace(/\\x1B\\[[0-9;]*m/g,'');}\nfunction pad(str,w){const raw=strip(str);return str+' '.repeat(Math.max(0,w-raw.length));}\nfunction table(items){ if(!items.length){console.log(chalk.yellow('No services.'));return;} const cols=[{k:'name',h:'Name'},{k:'type',h:'Type'},{k:'port',h:'Port'},{k:'status',h:'Status'},{k:'path',h:'Path'}]; const widths=cols.map(c=>Math.max(c.h.length,...items.map(i=>strip(i[c.k]).length))+2); const top='┌'+widths.map(w=>'─'.repeat(w)).join('┬')+'┐'; const sep='├'+widths.map(w=>'─'.repeat(w)).join('┼')+'┤'; const bot='└'+widths.map(w=>'─'.repeat(w)).join('┴')+'┘'; console.log(top); console.log('│'+cols.map((c,i)=>pad(chalk.bold.white(c.h),widths[i])).join('│')+'│'); console.log(sep); for(const it of items){ console.log('│'+cols.map((c,i)=>pad(it[c.k],widths[i])).join('│')+'│'); } console.log(bot); console.log(chalk.gray('Total: '+items.length)); }\n\nasync function check(port){ return new Promise(res=>{ const sock=net.createConnection({port,host:'127.0.0.1'},()=>{sock.destroy();res(true);}); sock.setTimeout(350,()=>{sock.destroy();res(false);}); sock.on('error',()=>{res(false);});}); }\nconst promises = cfg.services.map(async s=>{ const up = await check(s.port); return { ...s, _up: up }; });\nconst results = await Promise.all(promises);\nconst rows = results.map(s=>({ name: chalk.cyan(s.name), type: colorType(s.type)(s.type), port: chalk.green(String(s.port)), status: s._up ? chalk.bgGreen.black(' UP ') : chalk.bgRed.white(' DOWN '), path: chalk.dim(s.path) }));\nfunction colorType(t){ switch(t){case 'node': return chalk.green; case 'python': return chalk.yellow; case 'go': return chalk.cyan; case 'java': return chalk.red; case 'frontend': return chalk.blue; default: return chalk.white;} }\nif(process.argv.includes('--json')) { console.log(JSON.stringify(results.map(r=>({name:r.name,type:r.type,port:r.port,up:r._up,path:r.path})),null,2)); } else { console.log(chalk.magentaBright('\nWorkspace Services (runtime status)')); table(rows); }\n`); const readmePath = path.join(projectDir, 'README.md'); const svcList = services.map(s => `- ${s.name} (${s.type}) port:${s.port}`).join('\n'); @@ -459,14 +467,16 @@ export async function scaffoldMonorepo(projectNameArg, options) { }; await fs.writeJSON(path.join(projectDir, 'polyglot.json'), polyglotConfig, { spaces: 2 }); - console.log(chalk.blueBright('\n🎉 Monorepo setup complete!\n')); - console.log('👉 Next steps:'); - console.log(` cd ${projectName}`); - if (options.noInstall) console.log(` ${pm} install`); - console.log(` ${pm} run list:services`); - console.log(` ${pm} run dev`); - console.log(' docker compose up --build'); - console.log('\nHappy hacking!\n'); + printBoxMessage([ + '🎉 Monorepo setup complete!', + `cd ${projectName}`, + options.noInstall ? `${pm} install` : '', + `${pm} run list:services # quick list (fancy table)`, + `${pm} run dev # run local node/frontend services`, + 'docker compose up --build# run all via docker', + '', + 'Happy hacking!' + ].filter(Boolean)); } catch (err) { console.error(chalk.red('Failed to scaffold project:'), err); process.exit(1); diff --git a/bin/lib/ui.js b/bin/lib/ui.js new file mode 100644 index 0000000..7bf4266 --- /dev/null +++ b/bin/lib/ui.js @@ -0,0 +1,84 @@ +import chalk from 'chalk'; + +// Simple table renderer without external heavy deps (keep bundle light) +// Falls back to console.table if terminal width is too narrow. +export function renderServicesTable(services, { title = 'Services', showHeader = true } = {}) { + if (!services || !services.length) { + console.log(chalk.yellow('No services to display.')); + return; + } + const cols = [ + { key: 'name', label: 'Name' }, + { key: 'type', label: 'Type' }, + { key: 'port', label: 'Port' }, + { key: 'path', label: 'Path' } + ]; + const rows = services.map(s => ({ + name: chalk.bold.cyan(s.name), + type: colorType(s.type)(s.type), + port: chalk.green(String(s.port)), + path: chalk.dim(s.path || `services/${s.name}`) + })); + + const termWidth = process.stdout.columns || 80; + const minWidthNeeded = cols.reduce((a,c)=>a + c.label.length + 5, 0); + if (termWidth < minWidthNeeded) { + console.table(services.map(s => ({ name: s.name, type: s.type, port: s.port, path: s.path || `services/${s.name}` }))); + return; + } + + const colWidths = cols.map(c => Math.max(c.label.length, ...rows.map(r => strip(r[c.key]).length)) + 2); + const totalWidth = colWidths.reduce((a,b)=>a+b,0) + cols.length + 1; + const top = '┌' + colWidths.map(w => '─'.repeat(w)).join('┬') + '┐'; + const sep = '├' + colWidths.map(w => '─'.repeat(w)).join('┼') + '┤'; + const bottom = '└' + colWidths.map(w => '─'.repeat(w)).join('┴') + '┘'; + + console.log(chalk.magentaBright(`\n${title}`)); + console.log(top); + if (showHeader) { + const header = '│' + cols.map((c,i)=>pad(chalk.bold.white(c.label), colWidths[i])).join('│') + '│'; + console.log(header); + console.log(sep); + } + for (const r of rows) { + const line = '│' + cols.map((c,i)=>pad(r[c.key], colWidths[i])).join('│') + '│'; + console.log(line); + } + console.log(bottom); + console.log(chalk.gray(`Total: ${services.length}`)); +} + +function pad(str, width) { + const raw = strip(str); + const diff = width - raw.length; + return str + ' '.repeat(diff); +} + +function strip(str) { + return str.replace(/\x1B\[[0-9;]*m/g,''); +} + +function colorType(type) { + switch(type) { + case 'node': return chalk.green; + case 'python': return chalk.yellow; + case 'go': return chalk.cyan; + case 'java': return chalk.red; + case 'frontend': return chalk.blue; + default: return chalk.white; + } +} + +export function printBoxMessage(lines, { color = chalk.blueBright } = {}) { + const clean = lines.filter(Boolean); + const width = Math.min(Math.max(...clean.map(l => l.length))+4, process.stdout.columns || 100); + const top = '┏' + '━'.repeat(width-2) + '┓'; + const bottom = '┗' + '━'.repeat(width-2) + '┛'; + console.log(color(top)); + for (const l of clean) { + const truncated = l.length + 4 > width ? l.slice(0,width-5) + '…' : l; + const pad = width - 2 - truncated.length; + console.log(color('┃ ' + truncated + ' '.repeat(pad-1) + '┃')); + } + console.log(color(bottom)); +} diff --git a/package-lock.json b/package-lock.json index f00404a..5e18efc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-polyglot", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-polyglot", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index ee6634e..40db66b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-polyglot", - "version": "1.4.0", + "version": "1.5.0", "description": "Scaffold polyglot microservice monorepos with built-in templates for Node, Python, Go, and more.", "main": "bin/index.js", "scripts": {