11#!/usr/bin/env bun
22
3+ import { spawn } from "child_process" ;
4+ import { closeSync , openSync , readSync , statSync } from "fs" ;
35import packageJson from "../../package.json" with { type : "json" } ;
4- import { isInstalledBinary , performUpgrade } from "./upgrade" ;
5- import { runOnboarding } from "./onboarding" ;
6+ import { getWebHost , getWebPort } from "@/config" ;
7+ import { runDaemon } from "@/core/daemon/manager" ;
8+ import { getDaemonLogPath } from "@/core/daemon/paths" ;
9+ import { isProcessAlive , readDaemonState , type DaemonState } from "@/core/daemon/state" ;
10+ import { runOnboarding } from "@/core/onboarding" ;
11+ import { isInstalledBinary , performUpgrade } from "@/core/upgrade" ;
612
7- const args = process . argv . slice ( 2 ) ;
13+ const rawArgs = process . argv . slice ( 2 ) ;
814const CURRENT_VERSION = packageJson . version ?? "0.0.0" ;
15+ const CLI_ENTRY = new URL ( import . meta. url ) . pathname ;
16+ const BUN_EXECUTABLE : string = process . argv [ 0 ] ?? process . execPath ;
17+ const READY_WAIT_MS = 2 * 60 * 1000 ;
18+ const READY_POLL_MS = 500 ;
19+ const LOG_TAIL_BYTES = 200_000 ;
20+ const LOG_TAIL_LINES = 40 ;
21+
22+ const foregroundRequested = rawArgs . includes ( "--foreground" ) ;
23+ const args = foregroundRequested
24+ ? rawArgs . filter ( ( arg ) => arg !== "--foreground" )
25+ : rawArgs ;
26+ const command = args [ 0 ] ;
927
1028function printHelp ( ) : void {
11- // Keep this minimal; runtime.ts runs local mode by default.
1229 console . log (
1330 [
1431 "ode - OpenCode Slack bot" ,
1532 "" ,
1633 "Usage:" ,
17- " ode [--local]" ,
34+ " ode [--foreground]" ,
35+ " ode status" ,
36+ " ode restart" ,
1837 " ode onboarding" ,
1938 " ode upgrade" ,
2039 " ode --version" ,
2140 "" ,
2241 "Examples:" ,
23- " ode --local" ,
24- " ode onboarding" ,
25- " ode upgrade" ,
26- ] . join ( "\n" )
42+ " ode" ,
43+ " ode status" ,
44+ " ode restart" ,
45+ " ode --foreground" ,
46+ ] . join ( "\n" ) ,
2747 ) ;
2848}
2949
@@ -42,24 +62,195 @@ async function upgrade(): Promise<void> {
4262 console . log ( "ode upgraded." ) ;
4363}
4464
65+ function getLocalSettingsUrl ( ) : string {
66+ const host = getWebHost ( ) ;
67+ const port = getWebPort ( ) ;
68+ return `http://${ host } :${ port } /local-setting` ;
69+ }
70+
71+ function fallbackReadyMessage ( ) : string {
72+ return `Ode is ready! Waiting for messages, setting UI is accessible at ${ getLocalSettingsUrl ( ) } ` ;
73+ }
74+
75+ function delay ( ms : number ) : Promise < void > {
76+ return new Promise ( ( resolve ) => {
77+ setTimeout ( resolve , ms ) ;
78+ } ) ;
79+ }
80+
81+ function daemonState ( ) : DaemonState {
82+ return readDaemonState ( ) ;
83+ }
84+
85+ function managerRunning ( state : DaemonState = daemonState ( ) ) : boolean {
86+ return isProcessAlive ( state . managerPid ) ;
87+ }
88+
89+ function runtimeRunning ( state : DaemonState = daemonState ( ) ) : boolean {
90+ return isProcessAlive ( state . runtimePid ) ;
91+ }
92+
93+ function ensureDaemonRunning ( ) : void {
94+ const state = daemonState ( ) ;
95+ if ( managerRunning ( state ) ) return ;
96+ const child = spawn ( BUN_EXECUTABLE , [ CLI_ENTRY , "daemon" ] , {
97+ detached : true ,
98+ stdio : "ignore" ,
99+ } ) ;
100+ child . unref ( ) ;
101+ }
102+
103+ async function waitForReadyMessage ( timeoutMs : number ) : Promise < string | null > {
104+ const startedAt = Date . now ( ) ;
105+ while ( Date . now ( ) - startedAt < timeoutMs ) {
106+ const state = daemonState ( ) ;
107+ if ( state . status === "ready" && typeof state . readyMessage === "string" && state . readyMessage . length > 0 && managerRunning ( state ) ) {
108+ return state . readyMessage ;
109+ }
110+ if ( ! managerRunning ( state ) ) {
111+ ensureDaemonRunning ( ) ;
112+ }
113+ await delay ( READY_POLL_MS ) ;
114+ }
115+ return null ;
116+ }
117+
118+ async function startBackground ( ) : Promise < void > {
119+ const state = daemonState ( ) ;
120+ if ( state . status === "ready" && state . readyMessage && managerRunning ( state ) ) {
121+ console . log ( state . readyMessage ) ;
122+ return ;
123+ }
124+ ensureDaemonRunning ( ) ;
125+ const readyMessage = await waitForReadyMessage ( READY_WAIT_MS ) ;
126+ if ( readyMessage ) {
127+ console . log ( readyMessage ) ;
128+ return ;
129+ }
130+ console . log ( `Ode daemon is still starting. Follow logs at ${ getDaemonLogPath ( ) } ` ) ;
131+ }
132+
133+ function tailLogs ( maxLines : number ) : string [ ] {
134+ const logPath = getDaemonLogPath ( ) ;
135+ try {
136+ const stats = statSync ( logPath ) ;
137+ if ( stats . size === 0 ) return [ ] ;
138+ const bytes = Math . min ( LOG_TAIL_BYTES , stats . size ) ;
139+ const buffer = Buffer . alloc ( Number ( bytes ) ) ;
140+ const fd = openSync ( logPath , "r" ) ;
141+ try {
142+ readSync ( fd , buffer , 0 , Number ( bytes ) , stats . size - bytes ) ;
143+ } finally {
144+ closeSync ( fd ) ;
145+ }
146+ const content = buffer . toString ( "utf-8" ) ;
147+ const lines = content . split ( / \r ? \n / ) . filter ( ( line ) => line . trim ( ) . length > 0 ) ;
148+ return lines . slice ( - maxLines ) ;
149+ } catch {
150+ return [ ] ;
151+ }
152+ }
153+
154+ function formatTimestamp ( value : number | null ) : string {
155+ if ( ! value ) return "n/a" ;
156+ return new Date ( value ) . toLocaleString ( ) ;
157+ }
158+
159+ async function showStatus ( ) : Promise < void > {
160+ const state = daemonState ( ) ;
161+ const daemonStatus = managerRunning ( state ) ? `running (pid ${ state . managerPid } )` : "stopped" ;
162+ const runtimeStatus = runtimeRunning ( state ) ? `running (pid ${ state . runtimePid } )` : "stopped" ;
163+ console . log ( `Daemon: ${ daemonStatus } ` ) ;
164+ console . log ( `Runtime: ${ runtimeStatus } ` ) ;
165+ console . log ( `Last start: ${ formatTimestamp ( state . lastStartAt ) } ` ) ;
166+ console . log ( `Last ready: ${ formatTimestamp ( state . lastReadyAt ) } ` ) ;
167+ if ( state . pendingUpgradeRestart ) {
168+ console . log (
169+ `Pending upgrade restart since ${ formatTimestamp ( state . pendingUpgradeRestart . scheduledAt ) } (${ state . pendingUpgradeRestart . reason } )` ,
170+ ) ;
171+ }
172+ const logs = tailLogs ( LOG_TAIL_LINES ) ;
173+ if ( logs . length === 0 ) {
174+ console . log ( `No logs yet. Log file: ${ getDaemonLogPath ( ) } ` ) ;
175+ return ;
176+ }
177+ console . log ( `Recent logs (${ getDaemonLogPath ( ) } ):` ) ;
178+ console . log ( logs . join ( "\n" ) ) ;
179+ }
180+
181+ async function restartDaemonCommand ( ) : Promise < void > {
182+ const state = daemonState ( ) ;
183+ if ( ! managerRunning ( state ) ) {
184+ console . log ( "Daemon not running. Starting a new daemon instance..." ) ;
185+ ensureDaemonRunning ( ) ;
186+ const ready = await waitForReadyMessage ( READY_WAIT_MS ) ;
187+ console . log ( ready ?? `Restart requested. Follow logs at ${ getDaemonLogPath ( ) } ` ) ;
188+ return ;
189+ }
190+
191+ if ( runtimeRunning ( state ) && state . runtimePid ) {
192+ try {
193+ process . kill ( state . runtimePid , "SIGTERM" ) ;
194+ console . log ( `Sent shutdown signal to runtime (pid ${ state . runtimePid } ).` ) ;
195+ } catch ( error ) {
196+ console . warn ( `Failed to signal runtime (pid ${ state . runtimePid } ): ${ String ( error ) } ` ) ;
197+ }
198+ } else {
199+ console . log ( "Runtime is not currently running; waiting for daemon to restart." ) ;
200+ }
201+
202+ const ready = await waitForReadyMessage ( READY_WAIT_MS ) ;
203+ console . log ( ready ?? `Restart requested. Follow logs at ${ getDaemonLogPath ( ) } ` ) ;
204+ }
205+
45206if ( args . includes ( "--help" ) || args . includes ( "-h" ) ) {
46207 printHelp ( ) ;
47208 process . exit ( 0 ) ;
48209}
49210
50- if ( args . includes ( "--version" ) || args [ 0 ] === "version" ) {
211+ if ( command === "__runtime" ) {
212+ await import ( "./index" ) ;
213+ process . exit ( 0 ) ;
214+ }
215+
216+ if ( command === "daemon" ) {
217+ await runDaemon ( ) ;
218+ process . exit ( 0 ) ;
219+ }
220+
221+ if ( args . includes ( "--version" ) || command === "version" ) {
51222 console . log ( CURRENT_VERSION ) ;
52223 process . exit ( 0 ) ;
53224}
54225
55- if ( args [ 0 ] === "upgrade" ) {
226+ if ( command === "upgrade" ) {
56227 await upgrade ( ) ;
57228 process . exit ( 0 ) ;
58229}
59230
60- if ( args [ 0 ] === "onboarding" ) {
231+ if ( command === "onboarding" ) {
61232 await runOnboarding ( { force : true } ) ;
62233 process . exit ( 0 ) ;
63234}
64235
65- await import ( "./index" ) ;
236+ if ( command === "status" ) {
237+ await showStatus ( ) ;
238+ process . exit ( 0 ) ;
239+ }
240+
241+ if ( command === "restart" ) {
242+ await restartDaemonCommand ( ) ;
243+ process . exit ( 0 ) ;
244+ }
245+
246+ if ( command === "start" ) {
247+ await startBackground ( ) ;
248+ process . exit ( 0 ) ;
249+ }
250+
251+ if ( foregroundRequested ) {
252+ await import ( "./index" ) ;
253+ process . exit ( 0 ) ;
254+ }
255+
256+ await startBackground ( ) ;
0 commit comments