11#!/usr/bin/env bun
22
33import { spawn } from "child_process" ;
4+ import { existsSync , readFileSync } from "fs" ;
45import packageJson from "../../package.json" with { type : "json" } ;
56import { getWebHost , getWebPort } from "@/config" ;
67import { runDaemon } from "@/core/daemon/manager" ;
@@ -37,6 +38,7 @@ function printHelp(): void {
3738 "Usage:" ,
3839 " ode [--foreground]" ,
3940 " ode status" ,
41+ " ode log [--info|--error] [--tail [N]]" ,
4042 " ode restart" ,
4143 " ode stop" ,
4244 " ode onboard" ,
@@ -48,6 +50,8 @@ function printHelp(): void {
4850 "Examples:" ,
4951 " ode" ,
5052 " ode status" ,
53+ " ode log --error" ,
54+ " ode log --tail 200" ,
5155 " ode restart" ,
5256 " ode stop" ,
5357 " ode onboard" ,
@@ -144,7 +148,7 @@ async function waitForStopped(timeoutMs: number): Promise<boolean> {
144148async function startBackground ( ) : Promise < void > {
145149 const state = daemonState ( ) ;
146150 if ( state . status === "ready" && state . readyMessage && managerRunning ( state ) ) {
147- console . log ( state . readyMessage ) ;
151+ console . log ( fallbackReadyMessage ( ) ) ;
148152 return ;
149153 }
150154 ensureDaemonRunning ( ) ;
@@ -161,6 +165,83 @@ function formatTimestamp(value: number | null): string {
161165 return new Date ( value ) . toLocaleString ( ) ;
162166}
163167
168+ type LogFilterLevel = "all" | "info" | "error" ;
169+
170+ function parseLogFilterLevel ( commandArgs : string [ ] ) : LogFilterLevel {
171+ if ( commandArgs . includes ( "--error" ) ) return "error" ;
172+ if ( commandArgs . includes ( "--info" ) ) return "info" ;
173+ return "all" ;
174+ }
175+
176+ function parseLogTailLimit ( commandArgs : string [ ] ) : number | null {
177+ const tailWithValue = commandArgs . find ( ( arg ) => arg . startsWith ( "--tail=" ) ) ;
178+ if ( tailWithValue ) {
179+ const rawValue = tailWithValue . slice ( "--tail=" . length ) . trim ( ) ;
180+ const parsed = Number ( rawValue ) ;
181+ return Number . isFinite ( parsed ) && parsed > 0 ? Math . floor ( parsed ) : 200 ;
182+ }
183+
184+ const tailIndex = commandArgs . indexOf ( "--tail" ) ;
185+ if ( tailIndex < 0 ) return null ;
186+
187+ const nextArg = commandArgs [ tailIndex + 1 ] ;
188+ if ( ! nextArg || nextArg . startsWith ( "--" ) ) return 200 ;
189+
190+ const parsed = Number ( nextArg ) ;
191+ return Number . isFinite ( parsed ) && parsed > 0 ? Math . floor ( parsed ) : 200 ;
192+ }
193+
194+ function lineMatchesLogLevel ( line : string , level : LogFilterLevel ) : boolean {
195+ if ( level === "all" ) return true ;
196+
197+ if ( line . startsWith ( "{" ) ) {
198+ try {
199+ const parsed = JSON . parse ( line ) as { level ?: unknown } ;
200+ if ( typeof parsed . level === "number" ) {
201+ if ( level === "error" ) return parsed . level >= 50 ;
202+ return parsed . level >= 30 ;
203+ }
204+ } catch {
205+ // Ignore malformed JSON and use fallback matching.
206+ }
207+ }
208+
209+ if ( level === "error" ) {
210+ return line . toLowerCase ( ) . includes ( "error" )
211+ || line . includes ( "Unhandled rejection" )
212+ || line . includes ( "Uncaught exception" ) ;
213+ }
214+
215+ return true ;
216+ }
217+
218+ function showLogs ( commandArgs : string [ ] ) : void {
219+ const logPath = getDaemonLogPath ( ) ;
220+ if ( ! existsSync ( logPath ) ) {
221+ console . log ( `No daemon logs found yet at ${ logPath } ` ) ;
222+ return ;
223+ }
224+
225+ const filterLevel = parseLogFilterLevel ( commandArgs ) ;
226+ const tailLimit = parseLogTailLimit ( commandArgs ) ;
227+ const content = readFileSync ( logPath , "utf8" ) ;
228+ if ( content . length === 0 ) {
229+ console . log ( `Daemon log is empty at ${ logPath } ` ) ;
230+ return ;
231+ }
232+
233+ const lines = content . split ( / \r ? \n / ) . filter ( ( line ) => line . length > 0 ) ;
234+ const filtered = lines . filter ( ( line ) => lineMatchesLogLevel ( line , filterLevel ) ) ;
235+ const output = tailLimit === null ? filtered : filtered . slice ( - tailLimit ) ;
236+
237+ if ( output . length === 0 ) {
238+ console . log ( `No ${ filterLevel } logs found in ${ logPath } ` ) ;
239+ return ;
240+ }
241+
242+ console . log ( output . join ( "\n" ) ) ;
243+ }
244+
164245async function showStatus ( ) : Promise < void > {
165246 const state = daemonState ( ) ;
166247 const daemonIsRunning = managerRunning ( state ) ;
@@ -173,7 +254,7 @@ async function showStatus(): Promise<void> {
173254 console . log ( "Upgrade: none pending" ) ;
174255 }
175256 if ( daemonIsRunning ) {
176- console . log ( " ode is running, setting UI is running on localhost:9293..." ) ;
257+ console . log ( ` ode is running, setting UI is accessible at ${ getLocalSettingsUrl ( ) } ` ) ;
177258 return ;
178259 }
179260 console . log ( "ode is installed but not running, can run it with ode" ) ;
@@ -271,6 +352,11 @@ if (command === "status") {
271352 process . exit ( 0 ) ;
272353}
273354
355+ if ( command === "log" ) {
356+ showLogs ( args . slice ( 1 ) ) ;
357+ process . exit ( 0 ) ;
358+ }
359+
274360if ( command === "restart" ) {
275361 await restartDaemonCommand ( ) ;
276362 process . exit ( 0 ) ;
0 commit comments