Skip to content

Commit f142c4d

Browse files
committed
feat: add 'dev' command to run services locally or via Docker Compose with health checks
Signed-off-by: kaifcoder <kaifmohd2014@gmail.com>
1 parent a39b5bc commit f142c4d

File tree

5 files changed

+167
-0
lines changed

5 files changed

+167
-0
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,24 @@ Add a plugin scaffold:
3131
create-polyglot add plugin postgres
3232
```
3333

34+
Run all services in dev mode (Node & frontend locally; others manual unless using docker):
35+
```bash
36+
create-polyglot dev
37+
```
38+
39+
Run everything via Docker Compose:
40+
```bash
41+
create-polyglot dev --docker
42+
```
43+
3444
## Commands
3545

3646
| Command | Description |
3747
|---------|-------------|
3848
| `create-polyglot init <name>` | Scaffold a new workspace (root invocation without `init` is deprecated). |
3949
| `create-polyglot add service <name>` | Add a service after init (`--type`, `--port`, `--yes`). |
4050
| `create-polyglot add plugin <name>` | Create plugin skeleton under `plugins/<name>`. |
51+
| `create-polyglot dev [--docker]` | Run Node & frontend services locally or all via compose. |
4152

4253
## Init Options
4354

@@ -92,6 +103,11 @@ Non-Node services start manually or via compose:
92103
cd services/python && uvicorn app.main:app --reload
93104
```
94105

106+
### polyglot dev Command
107+
`create-polyglot dev` reads `polyglot.json`, launches Node & frontend services that expose a `dev` script, assigns each a colorized log prefix, and probes `http://localhost:<port>/health` until ready (15s timeout). Pass `--docker` to instead delegate to `docker compose up --build` for all services.
108+
109+
If a service lacks a `dev` script it is skipped with no error. Non-Node services (python/go/java) are not auto-started yet unless you choose `--docker`.
110+
95111
### Docker & Compose
96112
For each selected service a Dockerfile is generated. A `compose.yaml` includes:
97113
- Service definitions with build contexts

bin/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Command } from 'commander';
33
import chalk from 'chalk';
44
import { scaffoldMonorepo, addService, scaffoldPlugin } from './lib/scaffold.js';
5+
import { runDev } from './lib/dev.js';
56

67
const program = new Command();
78

@@ -94,5 +95,13 @@ program
9495
}
9596
});
9697

98+
program
99+
.command('dev')
100+
.description('Run services locally (Node & frontend) or use --docker for compose')
101+
.option('--docker', 'Use docker compose up --build to start all services')
102+
.action(async (opts) => {
103+
await runDev({ docker: !!opts.docker });
104+
});
105+
97106
program.parse();
98107

bin/lib/dev.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import chalk from 'chalk';
4+
import { spawn } from 'node:child_process';
5+
import http from 'http';
6+
7+
function colorFor(name) {
8+
const colors = [chalk.cyan, chalk.magenta, chalk.green, chalk.blue, chalk.yellow, chalk.redBright];
9+
let sum = 0; for (let i=0;i<name.length;i++) sum += name.charCodeAt(i);
10+
return colors[sum % colors.length];
11+
}
12+
13+
async function waitForHealth(url, timeoutMs=15000, interval=500) {
14+
const start = Date.now();
15+
return new Promise(resolve => {
16+
const check = () => {
17+
const req = http.get(url, res => {
18+
if (res.statusCode && res.statusCode < 500) {
19+
resolve(true); req.destroy(); return;
20+
}
21+
res.resume();
22+
if (Date.now() - start > timeoutMs) return resolve(false);
23+
setTimeout(check, interval);
24+
});
25+
req.on('error', () => {
26+
if (Date.now() - start > timeoutMs) return resolve(false);
27+
setTimeout(check, interval);
28+
});
29+
};
30+
check();
31+
});
32+
}
33+
34+
export async function runDev({ docker=false } = {}) {
35+
const cwd = process.cwd();
36+
const configPath = path.join(cwd, 'polyglot.json');
37+
if (!fs.existsSync(configPath)) {
38+
console.error(chalk.red('polyglot.json not found. Run inside a generated workspace.'));
39+
process.exit(1);
40+
}
41+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
42+
const servicesDir = path.join(cwd, 'services');
43+
if (!fs.existsSync(servicesDir)) {
44+
console.error(chalk.red('services/ directory not found.'));
45+
process.exit(1);
46+
}
47+
if (docker) {
48+
console.log(chalk.cyan('🛳 Starting via docker compose...'));
49+
const compose = spawn('docker', ['compose', 'up', '--build'], { stdio: 'inherit' });
50+
compose.on('exit', code => process.exit(code || 0));
51+
return;
52+
}
53+
console.log(chalk.cyan('🚀 Starting services locally (best effort)...'));
54+
const procs = [];
55+
const healthPromises = [];
56+
for (const svc of cfg.services) {
57+
const svcPath = path.join(cwd, svc.path);
58+
if (!fs.existsSync(svcPath)) continue;
59+
// Only auto-run node & frontend services (others require language runtime dev tasks)
60+
if (!['node','frontend'].includes(svc.type)) continue;
61+
const pkgPath = path.join(svcPath, 'package.json');
62+
if (!fs.existsSync(pkgPath)) continue;
63+
let pkg;
64+
try { pkg = JSON.parse(fs.readFileSync(pkgPath,'utf-8')); } catch { continue; }
65+
if (!pkg.scripts || !pkg.scripts.dev) continue;
66+
const color = colorFor(svc.name);
67+
const pm = detectPM(cwd);
68+
const cmd = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun' : 'npm';
69+
const args = pm === 'yarn' ? ['run','dev'] : ['run','dev'];
70+
const child = spawn(cmd, args, { cwd: svcPath, env: { ...process.env, PORT: String(svc.port) }, shell: true });
71+
procs.push(child);
72+
child.stdout.on('data', d => process.stdout.write(color(`[${svc.name}] `) + d.toString()));
73+
child.stderr.on('data', d => process.stderr.write(color(`[${svc.name}] `) + d.toString()));
74+
child.on('exit', code => {
75+
process.stdout.write(color(`[${svc.name}] exited with code ${code}\n`));
76+
});
77+
// health check
78+
const healthUrl = `http://localhost:${svc.port}/health`;
79+
const hp = waitForHealth(healthUrl).then(ok => {
80+
const msg = ok ? chalk.green(`✔ health OK ${svc.name} ${healthUrl}`) : chalk.yellow(`⚠ health timeout ${svc.name} ${healthUrl}`);
81+
console.log(msg);
82+
});
83+
healthPromises.push(hp);
84+
}
85+
if (!procs.length) {
86+
console.log(chalk.yellow('No auto-runnable Node/Frontend services found. Use --docker to start all via compose.'));
87+
}
88+
await Promise.all(healthPromises);
89+
console.log(chalk.blue('Watching services. Press Ctrl+C to exit.'));
90+
process.on('SIGINT', () => { procs.forEach(p => p.kill('SIGINT')); process.exit(0); });
91+
}
92+
93+
function detectPM(root) {
94+
if (fs.existsSync(path.join(root,'pnpm-lock.yaml'))) return 'pnpm';
95+
if (fs.existsSync(path.join(root,'yarn.lock'))) return 'yarn';
96+
if (fs.existsSync(path.join(root,'bun.lockb'))) return 'bun';
97+
return 'npm';
98+
}

docs/cli/index.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,15 @@ Add a plugin:
1515
```bash
1616
create-polyglot add plugin kafka
1717
```
18+
19+
Run dev (local processes):
20+
```bash
21+
create-polyglot dev
22+
```
23+
24+
Run dev via Docker Compose:
25+
```bash
26+
create-polyglot dev --docker
27+
```
28+
29+
Health endpoints are polled at `/health` on each service port with a 15s timeout.

tests/dev-command.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { execa } from 'execa';
2+
import { describe, it, expect, afterAll } from 'vitest';
3+
import fs from 'fs';
4+
import path from 'path';
5+
import os from 'os';
6+
7+
describe('polyglot dev command (non-docker)', () => {
8+
const tmpParent = fs.mkdtempSync(path.join(os.tmpdir(), 'polyglot-dev-'));
9+
const tmpDir = path.join(tmpParent, 'workspace');
10+
fs.mkdirSync(tmpDir);
11+
const projName = 'dev-proj';
12+
13+
afterAll(() => { try { fs.rmSync(tmpParent, { recursive: true, force: true }); } catch {} });
14+
15+
it('starts node service and performs health check', async () => {
16+
const repoRoot = process.cwd();
17+
const cliPath = path.join(repoRoot, 'bin/index.js');
18+
await execa('node', [cliPath, 'init', projName, '--services', 'node', '--no-install', '--yes'], { cwd: tmpDir, env: { ...process.env, CI: 'true' } });
19+
const projectPath = path.join(tmpDir, projName);
20+
// Write a minimal dev script to the node service if missing
21+
const nodePkgPath = path.join(projectPath, 'services/node/package.json');
22+
const nodePkg = JSON.parse(fs.readFileSync(nodePkgPath,'utf-8'));
23+
nodePkg.scripts = nodePkg.scripts || {}; nodePkg.scripts.dev = 'node src/index.js';
24+
fs.writeFileSync(nodePkgPath, JSON.stringify(nodePkg, null, 2));
25+
const proc = execa('node', [cliPath, 'dev'], { cwd: projectPath, env: { ...process.env, CI: 'true' } });
26+
// wait briefly then kill
27+
await new Promise(r => setTimeout(r, 3500));
28+
proc.kill('SIGINT');
29+
await proc.catch(()=>{}); // ignore exit errors due to SIGINT
30+
expect(fs.existsSync(path.join(projectPath,'polyglot.json'))).toBe(true);
31+
}, 20000);
32+
});

0 commit comments

Comments
 (0)