@shetty4l/core 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.ts +68 -0
- package/src/daemon.ts +49 -5
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -40,6 +40,74 @@ export function formatUptime(seconds: number): string {
|
|
|
40
40
|
return `${hours}h ${mins}m`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// --- Log file tail ---
|
|
44
|
+
|
|
45
|
+
export interface LogsCommandOpts {
|
|
46
|
+
/** Absolute path to the log file. */
|
|
47
|
+
logFile: string;
|
|
48
|
+
/** Default number of lines when no count is specified. */
|
|
49
|
+
defaultCount?: number;
|
|
50
|
+
/** Message to display when the log file does not exist or is empty. */
|
|
51
|
+
emptyMessage?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a `logs` command handler that tails a log file.
|
|
56
|
+
*
|
|
57
|
+
* Supports a positional count argument (`logs 50`) and the standard `--json` flag.
|
|
58
|
+
* Returns exit code 0 on success, 1 on error.
|
|
59
|
+
*/
|
|
60
|
+
export function createLogsCommand(opts: LogsCommandOpts): CommandHandler {
|
|
61
|
+
const {
|
|
62
|
+
logFile,
|
|
63
|
+
defaultCount = 20,
|
|
64
|
+
emptyMessage = "No log entries yet.",
|
|
65
|
+
} = opts;
|
|
66
|
+
|
|
67
|
+
return async (args: string[], json: boolean): Promise<number> => {
|
|
68
|
+
const count = args.length > 0 ? Number.parseInt(args[0], 10) : defaultCount;
|
|
69
|
+
if (Number.isNaN(count) || count <= 0) {
|
|
70
|
+
console.error(`Invalid count: ${args[0]}`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const file = Bun.file(logFile);
|
|
75
|
+
if (!(await file.exists())) {
|
|
76
|
+
if (json) {
|
|
77
|
+
console.log(JSON.stringify({ lines: [], file: logFile }));
|
|
78
|
+
} else {
|
|
79
|
+
console.log(emptyMessage);
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const text = await file.text();
|
|
85
|
+
const allLines = text.split("\n").filter((l) => l.length > 0);
|
|
86
|
+
|
|
87
|
+
if (allLines.length === 0) {
|
|
88
|
+
if (json) {
|
|
89
|
+
console.log(JSON.stringify({ lines: [], file: logFile }));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(emptyMessage);
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lines = allLines.slice(-count);
|
|
97
|
+
|
|
98
|
+
if (json) {
|
|
99
|
+
console.log(
|
|
100
|
+
JSON.stringify({ lines, file: logFile, total: allLines.length }),
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
console.log(line);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
43
111
|
// --- Command dispatch ---
|
|
44
112
|
|
|
45
113
|
export type CommandHandler = (
|
package/src/daemon.ts
CHANGED
|
@@ -25,8 +25,10 @@ export interface DaemonManagerOpts {
|
|
|
25
25
|
serveCommand?: string;
|
|
26
26
|
/** Health endpoint URL for verifying the daemon is responsive. */
|
|
27
27
|
healthUrl?: string;
|
|
28
|
-
/** Milliseconds
|
|
29
|
-
|
|
28
|
+
/** Milliseconds between health poll attempts during startup. Defaults to 500. */
|
|
29
|
+
startupPollMs?: number;
|
|
30
|
+
/** Total milliseconds to wait for health endpoint after spawn. Defaults to 10000. */
|
|
31
|
+
healthTimeoutMs?: number;
|
|
30
32
|
/** Milliseconds to wait for graceful stop before SIGKILL. Defaults to 5000. */
|
|
31
33
|
stopTimeoutMs?: number;
|
|
32
34
|
}
|
|
@@ -94,7 +96,8 @@ export function createDaemonManager(opts: DaemonManagerOpts): DaemonManager {
|
|
|
94
96
|
cliPath,
|
|
95
97
|
serveCommand = "serve",
|
|
96
98
|
healthUrl,
|
|
97
|
-
|
|
99
|
+
startupPollMs = 500,
|
|
100
|
+
healthTimeoutMs = 10_000,
|
|
98
101
|
stopTimeoutMs = 5000,
|
|
99
102
|
} = opts;
|
|
100
103
|
|
|
@@ -157,8 +160,43 @@ export function createDaemonManager(opts: DaemonManagerOpts): DaemonManager {
|
|
|
157
160
|
});
|
|
158
161
|
|
|
159
162
|
writePid(pidFile, proc.pid);
|
|
160
|
-
await new Promise((resolve) => setTimeout(resolve, startupWaitMs));
|
|
161
163
|
|
|
164
|
+
if (healthUrl) {
|
|
165
|
+
// Poll health endpoint until responsive or timeout
|
|
166
|
+
let waited = 0;
|
|
167
|
+
while (waited < healthTimeoutMs) {
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, startupPollMs));
|
|
169
|
+
waited += startupPollMs;
|
|
170
|
+
|
|
171
|
+
if (!isProcessRunning(proc.pid)) {
|
|
172
|
+
removePidFile(pidFile);
|
|
173
|
+
return err(
|
|
174
|
+
`${name}: process exited during startup (check ${logFile})`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const health = await checkHealth();
|
|
179
|
+
if (health.healthy) {
|
|
180
|
+
return ok({
|
|
181
|
+
running: true,
|
|
182
|
+
pid: proc.pid,
|
|
183
|
+
uptime: health.uptime,
|
|
184
|
+
port: health.port,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Timed out — kill the unresponsive process
|
|
190
|
+
process.kill(proc.pid, "SIGKILL");
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
192
|
+
removePidFile(pidFile);
|
|
193
|
+
return err(
|
|
194
|
+
`${name}: health check timed out after ${healthTimeoutMs}ms (check ${logFile})`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// No health URL — fall back to PID check after one poll interval
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, startupPollMs));
|
|
162
200
|
const status = await manager.status();
|
|
163
201
|
if (status.running) {
|
|
164
202
|
return ok(status);
|
|
@@ -195,7 +233,13 @@ export function createDaemonManager(opts: DaemonManagerOpts): DaemonManager {
|
|
|
195
233
|
},
|
|
196
234
|
|
|
197
235
|
async restart(): Promise<Result<DaemonStatus>> {
|
|
198
|
-
await manager.stop();
|
|
236
|
+
const stopResult = await manager.stop();
|
|
237
|
+
// Propagate stop errors unless the service simply wasn't running
|
|
238
|
+
if (!stopResult.ok && !stopResult.error.includes("not running")) {
|
|
239
|
+
return stopResult;
|
|
240
|
+
}
|
|
241
|
+
// Brief delay to let the OS release the port
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, startupPollMs));
|
|
199
243
|
return manager.start();
|
|
200
244
|
},
|
|
201
245
|
};
|