@shetty4l/core 0.1.28 → 0.1.30

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.ts +157 -0
  3. package/src/http.ts +15 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shetty4l/core",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Shared infrastructure primitives for Bun/TypeScript services",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  * Each service defines its own commands and help text.
6
6
  */
7
7
 
8
+ import type { DaemonManager } from "./daemon";
9
+
8
10
  // --- Arg parsing ---
9
11
 
10
12
  export interface ParsedArgs {
@@ -184,3 +186,158 @@ export async function runCli(opts: RunCliOpts): Promise<void> {
184
186
  process.exit(result);
185
187
  }
186
188
  }
189
+
190
+ // --- Daemon command factories ---
191
+
192
+ export interface DaemonCommandsOpts {
193
+ /** Service name (e.g. "synapse"). Used in output messages. */
194
+ name: string;
195
+ /** Factory that returns a DaemonManager. Called lazily per command. */
196
+ getDaemon: () => DaemonManager;
197
+ }
198
+
199
+ /**
200
+ * Create standard daemon lifecycle commands: start, stop, status, restart.
201
+ *
202
+ * Returns four CommandHandler functions ready to register with runCli.
203
+ * Output format is standardized across all services.
204
+ */
205
+ export function createDaemonCommands(opts: DaemonCommandsOpts): {
206
+ start: CommandHandler;
207
+ stop: CommandHandler;
208
+ status: CommandHandler;
209
+ restart: CommandHandler;
210
+ } {
211
+ const { name, getDaemon } = opts;
212
+
213
+ const start: CommandHandler = async () => {
214
+ const result = await getDaemon().start();
215
+ if (!result.ok) {
216
+ console.error(result.error);
217
+ return 1;
218
+ }
219
+ const { pid, port } = result.value;
220
+ const portStr = port != null ? `, port: ${port}` : "";
221
+ console.log(`${name} daemon started (PID: ${pid}${portStr})`);
222
+ return 0;
223
+ };
224
+
225
+ const stop: CommandHandler = async () => {
226
+ const result = await getDaemon().stop();
227
+ if (!result.ok) {
228
+ if (result.error.includes("not running")) {
229
+ console.log(`${name} daemon is not running`);
230
+ } else {
231
+ console.error(result.error);
232
+ }
233
+ return 1;
234
+ }
235
+ console.log(`${name} daemon stopped (was PID: ${result.value.pid})`);
236
+ return 0;
237
+ };
238
+
239
+ const status: CommandHandler = async (_args, json) => {
240
+ const s = await getDaemon().status();
241
+
242
+ if (json) {
243
+ console.log(JSON.stringify(s, null, 2));
244
+ return s.running ? 0 : 1;
245
+ }
246
+
247
+ if (!s.running) {
248
+ console.log(`${name} is not running`);
249
+ return 1;
250
+ }
251
+
252
+ const uptimeStr = s.uptime != null ? formatUptime(s.uptime) : "unknown";
253
+ const portStr = s.port != null ? `, port: ${s.port}` : "";
254
+ console.log(
255
+ `${name} is running (PID: ${s.pid}${portStr}, uptime: ${uptimeStr})`,
256
+ );
257
+ return 0;
258
+ };
259
+
260
+ const restart: CommandHandler = async () => {
261
+ const result = await getDaemon().restart();
262
+ if (!result.ok) {
263
+ console.error(result.error);
264
+ return 1;
265
+ }
266
+ const { pid, port } = result.value;
267
+ const portStr = port != null ? `, port: ${port}` : "";
268
+ console.log(`${name} daemon restarted (PID: ${pid}${portStr})`);
269
+ return 0;
270
+ };
271
+
272
+ return { start, stop, status, restart };
273
+ }
274
+
275
+ // --- Health command factory ---
276
+
277
+ export interface HealthCommandOpts {
278
+ /** Service name (e.g. "synapse"). Used in output messages. */
279
+ name: string;
280
+ /** Returns the health endpoint URL. Called lazily per invocation. */
281
+ getHealthUrl: () => string;
282
+ /**
283
+ * Optional callback to print service-specific health data after the
284
+ * standard Status/Version/Uptime fields (e.g. provider health table).
285
+ */
286
+ formatExtra?: (data: Record<string, unknown>) => void;
287
+ }
288
+
289
+ /**
290
+ * Create a standard `health` CLI command that fetches /health and displays
291
+ * the result. Supports --json for machine-readable output.
292
+ */
293
+ export function createHealthCommand(opts: HealthCommandOpts): CommandHandler {
294
+ const { name, getHealthUrl, formatExtra } = opts;
295
+
296
+ return async (_args, json) => {
297
+ const healthUrl = getHealthUrl();
298
+ const port = safeParsePort(healthUrl);
299
+
300
+ let response: Response;
301
+ try {
302
+ response = await fetch(healthUrl);
303
+ } catch {
304
+ if (json) {
305
+ console.log(JSON.stringify({ error: "Server not reachable", port }));
306
+ } else {
307
+ console.error(`${name} is not running on port ${port}`);
308
+ }
309
+ return 1;
310
+ }
311
+
312
+ const data = (await response.json()) as Record<string, unknown>;
313
+
314
+ if (json) {
315
+ console.log(JSON.stringify(data, null, 2));
316
+ return data.status === "healthy" ? 0 : 1;
317
+ }
318
+
319
+ console.log(
320
+ `\nStatus: ${data.status === "healthy" ? "healthy" : "degraded"}`,
321
+ );
322
+ console.log(`Version: ${data.version}`);
323
+ if (typeof data.uptime === "number") {
324
+ console.log(`Uptime: ${formatUptime(data.uptime)}`);
325
+ }
326
+
327
+ if (formatExtra) {
328
+ formatExtra(data);
329
+ }
330
+
331
+ console.log();
332
+ return data.status === "healthy" ? 0 : 1;
333
+ };
334
+ }
335
+
336
+ /** Extract port from a URL string, returning "unknown" on failure. */
337
+ function safeParsePort(url: string): string {
338
+ try {
339
+ return new URL(url).port || "unknown";
340
+ } catch {
341
+ return "unknown";
342
+ }
343
+ }
package/src/http.ts CHANGED
@@ -81,6 +81,17 @@ export interface ServerOpts {
81
81
  req: Request,
82
82
  url: URL,
83
83
  ) => Response | Promise<Response> | null | Promise<Response | null>;
84
+ /**
85
+ * Optional custom health endpoint handler.
86
+ * When provided, called instead of the default healthResponse().
87
+ * Return a full Response to control both body and HTTP status
88
+ * (e.g. 503 for degraded state). Use healthResponse() inside
89
+ * the handler for the standard healthy case.
90
+ */
91
+ onHealth?: (
92
+ version: string,
93
+ startTime: number,
94
+ ) => Response | Promise<Response>;
84
95
  }
85
96
 
86
97
  export interface HttpServer {
@@ -100,7 +111,7 @@ export interface HttpServer {
100
111
  * 4. If onRequest returns null -> 404
101
112
  */
102
113
  export function createServer(opts: ServerOpts): HttpServer {
103
- const { port, host = "127.0.0.1", version, onRequest, name } = opts;
114
+ const { port, host = "127.0.0.1", version, onRequest, onHealth, name } = opts;
104
115
  const startTime = Date.now();
105
116
  const prefix = name ? `${name}: ` : "";
106
117
 
@@ -115,7 +126,9 @@ export function createServer(opts: ServerOpts): HttpServer {
115
126
  }
116
127
 
117
128
  if (url.pathname === "/health" && req.method === "GET") {
118
- return healthResponse(version, startTime);
129
+ return onHealth
130
+ ? onHealth(version, startTime)
131
+ : healthResponse(version, startTime);
119
132
  }
120
133
 
121
134
  try {