Skip to content

Commit 5574bb4

Browse files
authored
Merge pull request #61 from kaifcoder/bug/service_all_run
feat: enhance service management by adding support for Python service…
2 parents 42467ed + 28d4e15 commit 5574bb4

File tree

2 files changed

+238
-43
lines changed

2 files changed

+238
-43
lines changed

bin/lib/dev.js

Lines changed: 179 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -118,57 +118,193 @@ if (fs.existsSync(servicesDir)) {
118118
const svcPath = path.join(cwd, svc.path);
119119

120120
if (!fs.existsSync(svcPath)) continue;
121-
// Only auto-run node & frontend services (others require language runtime dev tasks)
122-
if (!['node','frontend'].includes(svc.type)) continue;
123121

124-
const pkgPath = path.join(svcPath, 'package.json');
125-
if (!fs.existsSync(pkgPath)) {
126-
console.log(`Skipping ${svc.name} (no package.json)`);
127-
continue;
128-
}
122+
const color = colorFor(svc.name);
123+
let child = null;
129124

130-
let pkg;
131-
try {
132-
pkg = JSON.parse(fs.readFileSync(pkgPath,'utf-8'));
133-
} catch {
134-
console.log(chalk.yellow(`Skipping ${svc.name} (invalid package.json)`));
135-
continue;
136-
}
125+
// Handle different service types
126+
switch (svc.type) {
127+
case 'node':
128+
case 'frontend': {
129+
const pkgPath = path.join(svcPath, 'package.json');
130+
if (!fs.existsSync(pkgPath)) {
131+
console.log(chalk.yellow(`Skipping ${svc.name} (no package.json)`));
132+
continue;
133+
}
137134

138-
// Determine which script to run
139-
const useScript = pkg.scripts?.dev ? 'dev' : pkg.scripts?.start ? 'start' : null;
140-
if (!useScript) {
141-
console.log(chalk.yellow(`Skipping ${svc.name} (no "dev" or "start" script)`));
142-
continue;
143-
}
144-
if (useScript === 'start') {
145-
console.log(`running start instead of dev for ${svc.name}`);
146-
}
135+
let pkg;
136+
try {
137+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
138+
} catch {
139+
console.log(chalk.yellow(`Skipping ${svc.name} (invalid package.json)`));
140+
continue;
141+
}
147142

148-
const color = colorFor(svc.name);
149-
const pm = detectPM(svcPath);
150-
const cmd = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun' : 'npm';
151-
const args = ['run', useScript];
152-
153-
const child = spawn(cmd, args, { cwd: svcPath, env: { ...process.env, PORT: String(svc.port) }, shell: true });
154-
procs.push(child);
155-
child.stdout.on('data', d => process.stdout.write(color(`[${svc.name}] `) + d.toString()));
156-
child.stderr.on('data', d => process.stderr.write(color(`[${svc.name}] `) + d.toString()));
157-
child.on('exit', code => {
158-
process.stdout.write(color(`[${svc.name}] exited with code ${code}\n`));
159-
});
160-
// health check
161-
const healthUrl = `http://localhost:${svc.port}/health`;
162-
const hp = waitForHealth(healthUrl,30000).then(ok => {
163-
const msg = ok ? chalk.green(`✔ health OK ${svc.name} ${healthUrl}`) : chalk.yellow(`⚠ health timeout ${svc.name} ${healthUrl}`);
164-
console.log(msg);
165-
});
166-
healthPromises.push(hp);
143+
// Determine which script to run
144+
const useScript = pkg.scripts?.dev ? 'dev' : pkg.scripts?.start ? 'start' : null;
145+
if (!useScript) {
146+
console.log(chalk.yellow(`Skipping ${svc.name} (no "dev" or "start" script)`));
147+
continue;
148+
}
149+
if (useScript === 'start') {
150+
console.log(color(`[${svc.name}] running start instead of dev`));
151+
}
152+
153+
const pm = detectPM(svcPath);
154+
const cmd = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun' : 'npm';
155+
const args = ['run', useScript];
156+
157+
child = spawn(cmd, args, { cwd: svcPath, env: { ...process.env, PORT: String(svc.port) }, shell: true });
158+
break;
159+
}
160+
161+
case 'python': {
162+
const venvPath = path.join(svcPath, 'venv');
163+
const venvBin = path.join(venvPath, 'bin');
164+
const venvPython = path.join(venvBin, 'python');
165+
const venvPip = path.join(venvBin, 'pip');
166+
const venvUvicorn = path.join(venvBin, 'uvicorn');
167+
168+
// Create virtual environment if it doesn't exist
169+
if (!fs.existsSync(venvPath)) {
170+
console.log(color(`[${svc.name}] Creating Python virtual environment...`));
171+
const venvCreate = spawn('python3', ['-m', 'venv', 'venv'], { cwd: svcPath, stdio: 'pipe' });
172+
venvCreate.stdout.on('data', d => process.stdout.write(color(`[${svc.name}:venv] `) + d.toString()));
173+
venvCreate.stderr.on('data', d => process.stderr.write(color(`[${svc.name}:venv] `) + d.toString()));
174+
await new Promise(resolve => venvCreate.on('close', resolve));
175+
}
176+
177+
// Check for requirements.txt and install dependencies in venv
178+
const reqPath = path.join(svcPath, 'requirements.txt');
179+
if (fs.existsSync(reqPath)) {
180+
console.log(color(`[${svc.name}] Installing Python dependencies in virtual environment...`));
181+
const pipInstall = spawn(venvPip, ['install', '-r', 'requirements.txt'], { cwd: svcPath, stdio: 'pipe' });
182+
pipInstall.stdout.on('data', d => process.stdout.write(color(`[${svc.name}:setup] `) + d.toString()));
183+
pipInstall.stderr.on('data', d => process.stderr.write(color(`[${svc.name}:setup] `) + d.toString()));
184+
await new Promise(resolve => pipInstall.on('close', resolve));
185+
}
186+
187+
// Look for main.py in app/ or root
188+
const mainPath = fs.existsSync(path.join(svcPath, 'app', 'main.py'))
189+
? path.join('app', 'main.py')
190+
: 'main.py';
191+
192+
if (!fs.existsSync(path.join(svcPath, mainPath))) {
193+
console.log(chalk.yellow(`Skipping ${svc.name} (no ${mainPath} found)`));
194+
continue;
195+
}
196+
197+
console.log(color(`[${svc.name}] Starting Python service with uvicorn...`));
198+
const module = mainPath.replace(/\//g, '.').replace('.py', '');
199+
200+
// Use venv's uvicorn if it exists, otherwise try to install it
201+
let uvicornCmd = venvUvicorn;
202+
if (!fs.existsSync(venvUvicorn)) {
203+
console.log(color(`[${svc.name}] Installing uvicorn in virtual environment...`));
204+
const uvicornInstall = spawn(venvPip, ['install', 'uvicorn[standard]', 'fastapi'], { cwd: svcPath, stdio: 'pipe' });
205+
uvicornInstall.stdout.on('data', d => process.stdout.write(color(`[${svc.name}:setup] `) + d.toString()));
206+
uvicornInstall.stderr.on('data', d => process.stderr.write(color(`[${svc.name}:setup] `) + d.toString()));
207+
await new Promise(resolve => uvicornInstall.on('close', resolve));
208+
}
209+
210+
child = spawn(uvicornCmd, [module + ':app', '--reload', '--host', '0.0.0.0', '--port', String(svc.port)], {
211+
cwd: svcPath,
212+
env: { ...process.env, PORT: String(svc.port) },
213+
shell: false
214+
});
215+
break;
216+
}
217+
218+
case 'go': {
219+
// Check for go.mod
220+
const goModPath = path.join(svcPath, 'go.mod');
221+
if (fs.existsSync(goModPath)) {
222+
console.log(color(`[${svc.name}] Installing Go dependencies...`));
223+
const goGet = spawn('go', ['mod', 'download'], { cwd: svcPath, stdio: 'pipe' });
224+
goGet.stdout.on('data', d => process.stdout.write(color(`[${svc.name}:setup] `) + d.toString()));
225+
goGet.stderr.on('data', d => process.stderr.write(color(`[${svc.name}:setup] `) + d.toString()));
226+
await new Promise(resolve => goGet.on('close', resolve));
227+
}
228+
229+
// Check for main.go
230+
if (!fs.existsSync(path.join(svcPath, 'main.go'))) {
231+
console.log(chalk.yellow(`Skipping ${svc.name} (no main.go found)`));
232+
continue;
233+
}
234+
235+
console.log(color(`[${svc.name}] Starting Go service...`));
236+
child = spawn('go', ['run', 'main.go'], {
237+
cwd: svcPath,
238+
env: { ...process.env, PORT: String(svc.port) },
239+
shell: true
240+
});
241+
break;
242+
}
243+
244+
case 'java': {
245+
// Check for pom.xml (Maven) or build.gradle (Gradle)
246+
const pomPath = path.join(svcPath, 'pom.xml');
247+
const gradlePath = path.join(svcPath, 'build.gradle');
248+
249+
if (fs.existsSync(pomPath)) {
250+
console.log(color(`[${svc.name}] Building Java service with Maven...`));
251+
const mvnPackage = spawn('mvn', ['clean', 'package', '-DskipTests'], { cwd: svcPath, stdio: 'pipe' });
252+
mvnPackage.stdout.on('data', d => process.stdout.write(color(`[${svc.name}:build] `) + d.toString()));
253+
mvnPackage.stderr.on('data', d => process.stderr.write(color(`[${svc.name}:build] `) + d.toString()));
254+
await new Promise(resolve => mvnPackage.on('close', resolve));
255+
256+
console.log(color(`[${svc.name}] Starting Java service with Spring Boot...`));
257+
child = spawn('mvn', ['spring-boot:run'], {
258+
cwd: svcPath,
259+
env: { ...process.env, SERVER_PORT: String(svc.port) },
260+
shell: true
261+
});
262+
} else if (fs.existsSync(gradlePath)) {
263+
console.log(color(`[${svc.name}] Building Java service with Gradle...`));
264+
const gradleBuild = spawn('./gradlew', ['build', '-x', 'test'], { cwd: svcPath, stdio: 'pipe' });
265+
gradleBuild.stdout.on('data', d => process.stdout.write(color(`[${svc.name}:build] `) + d.toString()));
266+
gradleBuild.stderr.on('data', d => process.stderr.write(color(`[${svc.name}:build] `) + d.toString()));
267+
await new Promise(resolve => gradleBuild.on('close', resolve));
268+
269+
console.log(color(`[${svc.name}] Starting Java service with Gradle...`));
270+
child = spawn('./gradlew', ['bootRun'], {
271+
cwd: svcPath,
272+
env: { ...process.env, SERVER_PORT: String(svc.port) },
273+
shell: true
274+
});
275+
} else {
276+
console.log(chalk.yellow(`Skipping ${svc.name} (no pom.xml or build.gradle found)`));
277+
continue;
278+
}
279+
break;
280+
}
281+
282+
default:
283+
console.log(chalk.yellow(`Skipping ${svc.name} (unsupported service type: ${svc.type})`));
284+
continue;
285+
}
286+
287+
if (child) {
288+
procs.push(child);
289+
child.stdout.on('data', d => process.stdout.write(color(`[${svc.name}] `) + d.toString()));
290+
child.stderr.on('data', d => process.stderr.write(color(`[${svc.name}] `) + d.toString()));
291+
child.on('exit', code => {
292+
process.stdout.write(color(`[${svc.name}] exited with code ${code}\n`));
293+
});
294+
295+
// Health check
296+
const healthUrl = `http://localhost:${svc.port}/health`;
297+
const hp = waitForHealth(healthUrl, 30000).then(ok => {
298+
const msg = ok ? chalk.green(`✔ health OK ${svc.name} ${healthUrl}`) : chalk.yellow(`⚠ health timeout ${svc.name} ${healthUrl}`);
299+
console.log(msg);
300+
});
301+
healthPromises.push(hp);
302+
}
167303
}
168304
}
169305

170306
if (!procs.length) {
171-
console.log(chalk.yellow('No auto-runnable Node/Frontend services found. Use --docker to start all via compose.'));
307+
console.log(chalk.yellow('No services found to start. Use --docker to start all via compose.'));
172308
// ✅ FIXED: Exit cleanly when running in CI/test mode
173309
if (process.env.CI === 'true') {
174310
process.exit(0);

templates/python/.gitignore

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Python virtual environment
2+
venv/
3+
env/
4+
ENV/
5+
6+
# Python cache
7+
__pycache__/
8+
*.py[cod]
9+
*$py.class
10+
*.so
11+
12+
# Distribution / packaging
13+
.Python
14+
build/
15+
develop-eggs/
16+
dist/
17+
downloads/
18+
eggs/
19+
.eggs/
20+
lib/
21+
lib64/
22+
parts/
23+
sdist/
24+
var/
25+
wheels/
26+
*.egg-info/
27+
.installed.cfg
28+
*.egg
29+
30+
# PyInstaller
31+
*.manifest
32+
*.spec
33+
34+
# Unit test / coverage
35+
htmlcov/
36+
.tox/
37+
.coverage
38+
.coverage.*
39+
.cache
40+
nosetests.xml
41+
coverage.xml
42+
*.cover
43+
.hypothesis/
44+
.pytest_cache/
45+
46+
# Logs
47+
*.log
48+
.logs/
49+
50+
# IDE
51+
.vscode/
52+
.idea/
53+
*.swp
54+
*.swo
55+
*~
56+
57+
# OS
58+
.DS_Store
59+
Thumbs.db

0 commit comments

Comments
 (0)