Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();

48 changes: 29 additions & 19 deletions bin/lib/scaffold.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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') {
Expand All @@ -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)) {
Expand All @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
84 changes: 84 additions & 0 deletions bin/lib/ui.js
Original file line number Diff line number Diff line change
@@ -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));
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Loading