-
Notifications
You must be signed in to change notification settings - Fork 37
feat(dev-cli): add Bun-based local development CLI #1142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
68ea7f5
feat(dev-cli): initialize @kilocode/dev-cli package with bun-types an…
evanjacobson 338a1eb
feat(dev-cli): add service registry with 21 services and tests
evanjacobson 24bedc1
feat(dev-cli): add topological dependency resolver with cycle detection
evanjacobson bf27f66
feat(dev-cli): add commands, utilities, and infrastructure helpers (T…
evanjacobson 564a520
feat(dev-cli): register CLI in workspace and document in DEVELOPMENT.md
evanjacobson eeb5bae
feat(dev-cli): add kiloclaw project commands (setup, push-dev)
evanjacobson cee9624
feat(dev-cli): add app-builder project commands (tmux session)
evanjacobson b23bf9d
feat(dev-cli): add code-review and auto-fix project commands
evanjacobson 6034633
feat(dev-cli): add project routing and service-specific subcommands
evanjacobson ca8ffd3
fix(dev-cli): fix bugs found in code review
evanjacobson 051a304
style(dev-cli): format with prettier
evanjacobson 8c95e61
fix(dev-cli): handle non-compose infra services in logs command
evan-claw 7936682
fix(dev-cli): include prereq checks in envCheck allGood flag
evan-claw e151f78
fix(dev-cli): include prereq checks in envCheck allGood flag (#1303)
evanjacobson 79ad70f
fix(dev-cli): handle non-compose infra services in logs command (#1301)
evanjacobson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "@kilocode/dev-cli", | ||
| "version": "0.0.1", | ||
| "private": true, | ||
| "type": "module", | ||
| "devDependencies": { | ||
| "bun-types": "^1.3.10" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import * as docker from '../infra/docker'; | ||
| import * as ui from '../utils/ui'; | ||
|
|
||
| export async function down(root: string) { | ||
| ui.header('Stopping services'); | ||
| await docker.stopAll(root); | ||
| ui.success('Docker services stopped'); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { services } from '../services/registry'; | ||
| import { parseEnvFile, findMissingVars } from '../utils/env'; | ||
| import * as ui from '../utils/ui'; | ||
| import { join } from 'path'; | ||
|
|
||
| export async function envCheck(root: string) { | ||
| ui.header('Environment Variable Check'); | ||
|
|
||
| const envLocalPath = join(root, '.env.local'); | ||
| const envLocalExists = await Bun.file(envLocalPath).exists(); | ||
| if (envLocalExists) { | ||
| ui.success('.env.local exists'); | ||
| } else { | ||
| ui.error('.env.local missing — run: vercel env pull'); | ||
| } | ||
|
|
||
| const vercelProjectPath = join(root, '.vercel', 'project.json'); | ||
| const vercelLinked = await Bun.file(vercelProjectPath).exists(); | ||
| if (vercelLinked) { | ||
| ui.success('Vercel project linked'); | ||
| } else { | ||
| ui.warn('Vercel project not linked — run: vercel link --project kilocode-app'); | ||
| } | ||
|
|
||
| let allGood = envLocalExists && vercelLinked; | ||
|
|
||
| const servicesWithEnv = services.filter(s => s.envFile); | ||
|
|
||
| for (const svc of servicesWithEnv) { | ||
| const examplePath = join(root, svc.dir, svc.envFile!); | ||
| const actualPath = join(root, svc.dir, '.dev.vars'); | ||
|
|
||
| const exampleExists = await Bun.file(examplePath).exists(); | ||
| const actualExists = await Bun.file(actualPath).exists(); | ||
|
|
||
| if (!actualExists) { | ||
| ui.warn(`${svc.name}: .dev.vars missing (copy from ${svc.envFile})`); | ||
| allGood = false; | ||
| continue; | ||
| } | ||
|
|
||
| if (exampleExists) { | ||
| const exampleContent = await Bun.file(examplePath).text(); | ||
| const actualContent = await Bun.file(actualPath).text(); | ||
| const example = parseEnvFile(exampleContent); | ||
| const actual = parseEnvFile(actualContent); | ||
| const missing = findMissingVars(example, actual); | ||
|
|
||
| if (missing.length > 0) { | ||
| ui.warn(`${svc.name}: placeholder values: ${missing.join(', ')}`); | ||
| allGood = false; | ||
| } else { | ||
| ui.success(`${svc.name}: .dev.vars OK`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (allGood) { | ||
| console.log(`\n ${ui.green('All environment checks passed!')}\n`); | ||
| } else { | ||
| console.log(`\n ${ui.yellow('Some checks need attention (see above)')}\n`); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { services } from '../services/registry'; | ||
| import * as ui from '../utils/ui'; | ||
|
|
||
| export async function logs(args: string[], root: string) { | ||
| if (args.length === 0) { | ||
| ui.header('Available services'); | ||
| for (const svc of services) { | ||
| const portInfo = svc.port ? ` (port ${svc.port})` : ''; | ||
| console.log(` ${svc.name.padEnd(20)} ${ui.dim(svc.description)}${portInfo}`); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const name = args[0]; | ||
| const svc = services.find(s => s.name === name); | ||
| if (!svc) { | ||
| ui.error(`Unknown service: "${name}"`); | ||
| return; | ||
| } | ||
|
|
||
| if (svc.type === 'infra' && svc.devCommand?.startsWith('docker compose')) { | ||
| const proc = Bun.spawn( | ||
| ['docker', 'compose', '-f', 'dev/docker-compose.yml', 'logs', '-f', svc.name], | ||
| { stdout: 'inherit', stderr: 'inherit', cwd: root } | ||
| ); | ||
| await proc.exited; | ||
| } else if (svc.type === 'infra') { | ||
| ui.warn( | ||
| `"${svc.name}" is not a Docker Compose service (runs: ${svc.devCommand ?? 'n/a'}).\n It does not produce persistent logs that can be tailed.` | ||
| ); | ||
| } else { | ||
| ui.warn( | ||
| `Log tailing for running dev servers is not yet supported.\n Start the service with 'pnpm kilo dev up ${name}' to see its output.` | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { services } from '../services/registry'; | ||
| import * as docker from '../infra/docker'; | ||
| import * as ui from '../utils/ui'; | ||
|
|
||
| export async function status(root: string) { | ||
| ui.header('Service Status'); | ||
|
|
||
| const pgHealthy = await docker.isHealthy(root, 'postgres'); | ||
| const redisHealthy = await docker.isHealthy(root, 'redis'); | ||
|
|
||
| console.log( | ||
| ` ${pgHealthy ? ui.green('●') : ui.red('●')} postgres ${pgHealthy ? 'running' : 'stopped'}` | ||
| ); | ||
| console.log( | ||
| ` ${redisHealthy ? ui.green('●') : ui.red('●')} redis ${redisHealthy ? 'running' : 'stopped'}` | ||
| ); | ||
|
|
||
| const portServices = services.filter(s => s.port && s.type !== 'infra'); | ||
| for (const svc of portServices) { | ||
| const listening = await isPortListening(svc.port!); | ||
| console.log( | ||
| ` ${listening ? ui.green('●') : ui.dim('○')} ${svc.name.padEnd(12)} ${listening ? `port ${svc.port}` : ui.dim('not running')}` | ||
| ); | ||
| } | ||
|
|
||
| console.log(); | ||
| } | ||
|
|
||
| async function isPortListening(port: number): Promise<boolean> { | ||
| try { | ||
| const socket = await Bun.connect({ | ||
| hostname: '127.0.0.1', | ||
| port, | ||
| socket: { | ||
| data() {}, | ||
| open(s) { | ||
| s.end(); | ||
| }, | ||
| error() {}, | ||
| }, | ||
| }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { startQuickTunnel, startNamedTunnel, updateDevVarsUrl } from '../infra/tunnel'; | ||
| import * as ui from '../utils/ui'; | ||
| import { join } from 'path'; | ||
|
|
||
| export async function tunnel(args: string[], root: string) { | ||
| const nameIdx = args.indexOf('--name'); | ||
| const tunnelName = nameIdx !== -1 ? args[nameIdx + 1] : undefined; | ||
| const port = 3000; | ||
|
|
||
| if (tunnelName) { | ||
| ui.header(`Starting named tunnel: ${tunnelName}`); | ||
| startNamedTunnel(tunnelName); | ||
| } else { | ||
| ui.header('Starting quick tunnel'); | ||
| const result = await startQuickTunnel(port); | ||
| if (result.url) { | ||
| ui.success(`Tunnel URL: ${result.url}`); | ||
| const devVarsPath = join(root, 'kiloclaw', '.dev.vars'); | ||
| if (await Bun.file(devVarsPath).exists()) { | ||
| await updateDevVarsUrl(devVarsPath, result.url); | ||
| } | ||
| } else { | ||
| ui.warn('Could not capture tunnel URL within 30s'); | ||
| ui.warn('Check cloudflared output and manually update .dev.vars'); | ||
| } | ||
| } | ||
|
|
||
| await new Promise(() => {}); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { resolve } from '../services/resolver'; | ||
| import { getServiceNames, type ServiceDef } from '../services/registry'; | ||
| import * as docker from '../infra/docker'; | ||
| import { spawnService, run } from '../utils/process'; | ||
| import * as ui from '../utils/ui'; | ||
| import { join } from 'path'; | ||
|
|
||
| export async function up(args: string[], root: string) { | ||
| const targets = args.length > 0 ? args : ['nextjs']; | ||
|
|
||
| const validNames = getServiceNames(); | ||
| for (const name of targets) { | ||
| if (!validNames.includes(name)) { | ||
| ui.error(`Unknown service: "${name}"`); | ||
| console.log(`\nAvailable services: ${validNames.join(', ')}`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| const plan = resolve(targets); | ||
|
|
||
| ui.header('Starting services'); | ||
| console.log(` ${plan.map(s => s.name).join(' → ')}\n`); | ||
|
|
||
| const infraServices = plan.filter(s => s.type === 'infra'); | ||
| const appServices = plan.filter(s => s.type !== 'infra'); | ||
|
|
||
| for (const svc of infraServices) { | ||
| await startInfraService(svc, root); | ||
| } | ||
|
|
||
| if (appServices.length === 0) { | ||
| ui.success('Infrastructure is ready. No app services to start.'); | ||
| return; | ||
| } | ||
|
|
||
| ui.header('Starting dev servers'); | ||
|
|
||
| for (const svc of appServices) { | ||
| if (!svc.devCommand) continue; | ||
| const cwd = join(root, svc.dir); | ||
| const portInfo = svc.port ? ` (port ${svc.port})` : ''; | ||
| console.log(` Starting ${ui.bold(svc.name)}${portInfo}...`); | ||
| spawnService({ name: svc.name, command: svc.devCommand, cwd }); | ||
| } | ||
|
|
||
| console.log(`\n ${ui.dim('Press Ctrl+C to stop all services')}\n`); | ||
| await new Promise(() => {}); | ||
| } | ||
|
|
||
| async function startInfraService(svc: ServiceDef, root: string) { | ||
| ui.header(`Starting ${svc.name}`); | ||
|
|
||
| if (!svc.devCommand) return; | ||
|
|
||
| const ok = await run({ | ||
| command: svc.devCommand, | ||
| cwd: join(root, svc.dir), | ||
| label: svc.devCommand, | ||
| }); | ||
|
|
||
| if (!ok) { | ||
| ui.error(`Failed to start ${svc.name}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (svc.name === 'postgres' || svc.name === 'redis') { | ||
| console.log(` Waiting for ${svc.name} to be healthy...`); | ||
| const healthy = await docker.waitForHealthy(root, svc.name); | ||
| if (healthy) { | ||
| ui.success(`${svc.name} is ready`); | ||
| } else { | ||
| ui.warn(`${svc.name} health check timed out — continuing anyway`); | ||
| } | ||
| } else { | ||
| ui.success(`${svc.name} complete`); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: Port-only checks misreport services that share a port
This status view assumes one service per port, but the new registry assigns
8792to bothauto-fixanddb-proxy, and8795to bothkiloclawandgit-token. If either process is running, this loop marks the sibling service as running too, sopnpm kilo statusproduces false positives.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — confirmed. Port-only checks do misreport when multiple services share a port. Fix in #1302: status now resolves by process name (or PID) rather than relying solely on the port, so co-tenanted ports no longer cause false positives.