11#!/usr/bin/env bun
22import { render } from "@opentui/solid"
33import { watch } from "fs"
4- import { loadConfig } from "./config/loader"
4+ import { loadConfig , findConfigFile } from "./config/loader"
55import { ProcessManager } from "./process/manager"
66import { App } from "./app"
77
@@ -19,6 +19,8 @@ Commands:
1919 down Stop all services
2020 restart Restart all services
2121 status Show service status (non-interactive)
22+ init Create a new devproc.yaml config file
23+ validate Validate the config file without starting services
2224
2325Options:
2426 -c, --config <file> Path to config file (default: devproc.yaml)
@@ -29,11 +31,189 @@ Options:
2931Examples:
3032 devproc Start all services with TUI
3133 devproc up Start all services with TUI
34+ devproc init Create a starter config file
35+ devproc validate Check config for errors
3236 devproc -c dev.yaml Use custom config file
3337 devproc -w Auto-reload on config changes
3438` )
3539}
3640
41+ /**
42+ * Generate a starter devproc.yaml config file
43+ */
44+ async function initConfig ( ) : Promise < void > {
45+ const configPath = "devproc.yaml"
46+ const file = Bun . file ( configPath )
47+
48+ if ( await file . exists ( ) ) {
49+ console . error ( `Error: ${ configPath } already exists` )
50+ console . log ( "Use a different directory or remove the existing file." )
51+ process . exit ( 1 )
52+ }
53+
54+ // Try to detect project type from package.json
55+ let projectName = "my-project"
56+ let suggestedServices = ""
57+
58+ const pkgFile = Bun . file ( "package.json" )
59+ if ( await pkgFile . exists ( ) ) {
60+ try {
61+ const pkg = await pkgFile . json ( )
62+ projectName = pkg . name || projectName
63+
64+ // Suggest services based on scripts
65+ const scripts = pkg . scripts || { }
66+ const suggestions : string [ ] = [ ]
67+
68+ if ( scripts . dev ) {
69+ suggestions . push ( ` # Frontend dev server
70+ web:
71+ cmd: ${ pkg . packageManager ?. startsWith ( "bun" ) ? "bun" : "npm" } run dev
72+ color: cyan` )
73+ }
74+
75+ if ( scripts . start ) {
76+ suggestions . push ( ` # Main application
77+ app:
78+ cmd: ${ pkg . packageManager ?. startsWith ( "bun" ) ? "bun" : "npm" } run start
79+ color: green` )
80+ }
81+
82+ if ( scripts [ "start:dev" ] || scripts [ "dev:server" ] ) {
83+ const script = scripts [ "start:dev" ] ? "start:dev" : "dev:server"
84+ suggestions . push ( ` # Dev server
85+ server:
86+ cmd: ${ pkg . packageManager ?. startsWith ( "bun" ) ? "bun" : "npm" } run ${ script }
87+ color: green` )
88+ }
89+
90+ if ( suggestions . length > 0 ) {
91+ suggestedServices = suggestions . join ( "\n\n" )
92+ }
93+ } catch {
94+ // Ignore JSON parse errors
95+ }
96+ }
97+
98+ // Default services if we couldn't detect any
99+ if ( ! suggestedServices ) {
100+ suggestedServices = ` # Example: Web server
101+ # web:
102+ # cmd: npm run dev
103+ # color: cyan
104+
105+ # Example: API server
106+ # api:
107+ # cmd: go run ./cmd/api
108+ # healthcheck:
109+ # cmd: curl -f http://localhost:8080/health
110+ # interval: 2s
111+ # retries: 30
112+ # color: green
113+
114+ # Example: Worker process
115+ # worker:
116+ # cmd: npm run worker
117+ # depends_on:
118+ # - api
119+ # restart: on-failure
120+
121+ # Example: Docker database
122+ # postgres:
123+ # cmd: docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16
124+ # healthcheck: pg_isready -h localhost -p 5432
125+
126+ # Placeholder service (remove this)
127+ echo:
128+ cmd: "bash -c 'while true; do echo Hello from devproc; sleep 5; done'"
129+ color: cyan`
130+ }
131+
132+ const configContent = `# DevProc Configuration
133+ # Documentation: https://github.com/captjt/devproc
134+
135+ name: ${ projectName }
136+
137+ # Global environment variables (applied to all services)
138+ # env:
139+ # NODE_ENV: development
140+
141+ # Load environment from .env file
142+ # dotenv: .env
143+
144+ # Organize services into groups (optional)
145+ # groups:
146+ # backend:
147+ # - api
148+ # - worker
149+ # frontend:
150+ # - web
151+
152+ services:
153+ ${ suggestedServices }
154+ `
155+
156+ await Bun . write ( configPath , configContent )
157+ console . log ( `Created ${ configPath } ` )
158+ console . log ( "" )
159+ console . log ( "Next steps:" )
160+ console . log ( " 1. Edit devproc.yaml to configure your services" )
161+ console . log ( " 2. Run 'devproc' to start all services" )
162+ console . log ( " 3. Press '?' for keyboard shortcuts" )
163+ }
164+
165+ /**
166+ * Validate the config file and report any errors
167+ */
168+ async function validateConfig ( configPath ?: string ) : Promise < void > {
169+ console . log ( "Validating configuration..." )
170+ console . log ( "" )
171+
172+ try {
173+ const config = await loadConfig ( configPath )
174+
175+ console . log ( `✓ Config file: ${ config . configPath } ` )
176+ console . log ( `✓ Project name: ${ config . name } ` )
177+ console . log ( `✓ Services: ${ config . services . size } ` )
178+
179+ // List services with their dependencies
180+ console . log ( "" )
181+ console . log ( "Services:" )
182+ for ( const [ name , service ] of config . services ) {
183+ const deps = Array . from ( service . dependsOn . keys ( ) )
184+ const depStr = deps . length > 0 ? ` (depends on: ${ deps . join ( ", " ) } )` : ""
185+ const healthStr = service . healthcheck ? " [healthcheck]" : ""
186+ const groupStr = service . group ? ` [group: ${ service . group } ]` : ""
187+ console . log ( ` • ${ name } ${ depStr } ${ healthStr } ${ groupStr } ` )
188+ }
189+
190+ // List groups
191+ if ( config . groups . size > 0 ) {
192+ console . log ( "" )
193+ console . log ( "Groups:" )
194+ for ( const [ name , group ] of config . groups ) {
195+ console . log ( ` • ${ name } : ${ group . services . join ( ", " ) } ` )
196+ }
197+ }
198+
199+ console . log ( "" )
200+ console . log ( "✓ Configuration is valid!" )
201+ } catch ( error ) {
202+ console . error ( "✗ Configuration error:" )
203+ console . error ( "" )
204+ if ( error instanceof Error ) {
205+ // Format the error message nicely
206+ const lines = error . message . split ( "\n" )
207+ for ( const line of lines ) {
208+ console . error ( ` ${ line } ` )
209+ }
210+ } else {
211+ console . error ( ` ${ error } ` )
212+ }
213+ process . exit ( 1 )
214+ }
215+ }
216+
37217async function main ( ) {
38218 const args = process . argv . slice ( 2 )
39219 let configPath : string | undefined
@@ -65,7 +245,7 @@ async function main() {
65245 }
66246
67247 // Commands
68- if ( [ "up" , "down" , "restart" , "status" ] . includes ( arg ! ) ) {
248+ if ( [ "up" , "down" , "restart" , "status" , "init" , "validate" ] . includes ( arg ! ) ) {
69249 command = arg !
70250 continue
71251 }
@@ -75,6 +255,18 @@ async function main() {
75255 process . exit ( 1 )
76256 }
77257
258+ // Handle init command (doesn't need existing config)
259+ if ( command === "init" ) {
260+ await initConfig ( )
261+ process . exit ( 0 )
262+ }
263+
264+ // Handle validate command
265+ if ( command === "validate" ) {
266+ await validateConfig ( configPath )
267+ process . exit ( 0 )
268+ }
269+
78270 try {
79271 // Load configuration
80272 const config = await loadConfig ( configPath )
0 commit comments