@cfbender/cesium 0.5.2 → 0.6.0

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.
@@ -1,26 +1,31 @@
1
1
  // cesium restart — stop the running server and start a new one in the foreground.
2
2
 
3
- import { type StopContext, stopCommand } from "./stop.ts";
4
- import { type ServeContext, serveCommand as defaultServeCommand } from "./serve.ts";
3
+ import { defineCommand } from "citty";
4
+ import { type StopContext, type StopArgs, runStop } from "./stop.ts";
5
+ import { type ServeContext, type ServeArgs, runServe, parseDuration } from "./serve.ts";
5
6
 
6
7
  export interface RestartContext extends StopContext, ServeContext {
7
- /** Test injection: replace serveCommand with a mock so tests don't block. */
8
- serveImpl?: (argv: string[], ctx?: Partial<RestartContext>) => Promise<number>;
8
+ /** Test injection: replace runServe with a mock so tests don't block. */
9
+ serveImpl?: (args: ServeArgs, ctx?: Partial<RestartContext>) => Promise<number>;
9
10
  }
10
11
 
12
+ export interface RestartArgs extends StopArgs, ServeArgs {}
13
+
11
14
  function defaultSleep(ms: number): Promise<void> {
12
15
  return new Promise((r) => setTimeout(r, ms));
13
16
  }
14
17
 
15
- export async function restartCommand(
16
- argv: string[],
17
- ctx?: Partial<RestartContext>,
18
+ export async function runRestart(
19
+ args: RestartArgs,
20
+ ctxOverride?: Partial<RestartContext>,
18
21
  ): Promise<number> {
19
- const sleepFn = ctx?.sleep ?? defaultSleep;
20
- const serveFn = ctx?.serveImpl ?? defaultServeCommand;
22
+ const sleepFn = ctxOverride?.sleep ?? defaultSleep;
23
+ const serveFn = ctxOverride?.serveImpl ?? runServe;
24
+
25
+ const stopArgs: StopArgs = { force: args.force, timeoutMs: args.timeoutMs };
21
26
 
22
27
  // 1. Stop any running server
23
- const stopCode = await stopCommand(argv, ctx);
28
+ const stopCode = await runStop(stopArgs, ctxOverride);
24
29
  if (stopCode !== 0) {
25
30
  // e.g. EPERM — bail, pass through exit code
26
31
  return stopCode;
@@ -30,9 +35,92 @@ export async function restartCommand(
30
35
  await sleepFn(200);
31
36
 
32
37
  // 3. Announce restart
33
- const stdout = ctx?.stdout ?? process.stdout;
38
+ const stdout = ctxOverride?.stdout ?? process.stdout;
34
39
  stdout.write("starting new cesium server...\n");
35
40
 
36
41
  // 4. Start the new server in foreground (blocks until Ctrl-C)
37
- return serveFn(argv, ctx);
42
+ const serveArgs: ServeArgs = {};
43
+ if (args.port !== undefined) serveArgs.port = args.port;
44
+ if (args.hostname !== undefined) serveArgs.hostname = args.hostname;
45
+ if (args.stateDir !== undefined) serveArgs.stateDir = args.stateDir;
46
+ if (args.idleTimeoutMs !== undefined) serveArgs.idleTimeoutMs = args.idleTimeoutMs;
47
+
48
+ return serveFn(serveArgs, ctxOverride);
38
49
  }
50
+
51
+ export const restartCmd = defineCommand({
52
+ meta: {
53
+ name: "restart",
54
+ description: "Stop the running cesium server and start a new one in the foreground.",
55
+ },
56
+ args: {
57
+ // Stop args
58
+ force: {
59
+ type: "boolean",
60
+ alias: "f",
61
+ default: false,
62
+ description: "SIGKILL immediately — skip the SIGTERM grace period",
63
+ },
64
+ timeout: {
65
+ type: "string",
66
+ default: "3000",
67
+ description: "Grace period in ms before SIGKILL",
68
+ },
69
+ // Serve args (mirrored)
70
+ port: {
71
+ type: "string",
72
+ alias: "p",
73
+ description: "Override configured port (default: 3030)",
74
+ },
75
+ hostname: {
76
+ type: "string",
77
+ alias: "H",
78
+ description: "Override configured bind address",
79
+ },
80
+ "state-dir": {
81
+ type: "string",
82
+ description: "Override the cesium state directory",
83
+ },
84
+ "idle-timeout": {
85
+ type: "string",
86
+ description: 'Auto-shutdown after DUR of inactivity (e.g. "30m"). Default: never.',
87
+ },
88
+ },
89
+ async run({ args }) {
90
+ const timeoutMs = parseInt(args.timeout, 10);
91
+ if (isNaN(timeoutMs) || timeoutMs < 0) {
92
+ process.stderr.write(`cesium restart: --timeout must be a non-negative integer\n`);
93
+ process.exit(1);
94
+ }
95
+
96
+ const restartArgs: RestartArgs = {
97
+ force: args.force,
98
+ timeoutMs,
99
+ };
100
+
101
+ if (args.port !== undefined) {
102
+ const p = parseInt(args.port, 10);
103
+ if (isNaN(p) || p < 1 || p > 65535) {
104
+ process.stderr.write(`cesium restart: --port must be a number between 1 and 65535\n`);
105
+ process.exit(1);
106
+ }
107
+ restartArgs.port = p;
108
+ }
109
+ if (args.hostname !== undefined) restartArgs.hostname = args.hostname;
110
+ if (args["state-dir"] !== undefined) restartArgs.stateDir = args["state-dir"];
111
+
112
+ if (args["idle-timeout"] !== undefined) {
113
+ const ms = parseDuration(args["idle-timeout"]);
114
+ if (ms === null) {
115
+ process.stderr.write(
116
+ `cesium restart: --idle-timeout must be a duration like "30m", "2h", "90s", or "0"/"never" to disable\n`,
117
+ );
118
+ process.exit(1);
119
+ }
120
+ restartArgs.idleTimeoutMs = ms;
121
+ }
122
+
123
+ const code = await runRestart(restartArgs);
124
+ if (code !== 0) process.exit(code);
125
+ },
126
+ });
@@ -1,11 +1,24 @@
1
1
  // cesium serve — start the local HTTP server in the foreground.
2
2
 
3
- import { parseArgs } from "node:util";
3
+ import { defineCommand } from "citty";
4
4
  import { loadConfig, type CesiumConfig } from "../../config.ts";
5
5
  import { runServerForeground, stopRunning } from "../../server/lifecycle.ts";
6
6
  import { resolveDisplayHost } from "../../tools/publish.ts";
7
7
  import { themeFromPreset, mergeTheme } from "../../render/theme.ts";
8
8
 
9
+ export interface ServeArgs {
10
+ port?: number;
11
+ hostname?: string;
12
+ stateDir?: string;
13
+ /**
14
+ * Idle timeout in milliseconds. 0 (the default for `cesium serve`) means the
15
+ * server runs forever until SIGINT/SIGTERM. Override with --idle-timeout to
16
+ * opt back into auto-shutdown — useful for long-lived dev sessions that
17
+ * should still recycle eventually.
18
+ */
19
+ idleTimeoutMs?: number;
20
+ }
21
+
9
22
  export interface ServeContext {
10
23
  stdout: { write: (s: string) => void };
11
24
  stderr: { write: (s: string) => void };
@@ -19,21 +32,8 @@ function defaultCtx(): ServeContext {
19
32
  };
20
33
  }
21
34
 
22
- export interface ServeOptions {
23
- port?: number;
24
- hostname?: string;
25
- stateDir?: string;
26
- /**
27
- * Idle timeout in milliseconds. 0 (the default for `cesium serve`) means the
28
- * server runs forever until SIGINT/SIGTERM. Override with --idle-timeout to
29
- * opt back into auto-shutdown — useful for long-lived dev sessions that
30
- * should still recycle eventually.
31
- */
32
- idleTimeoutMs?: number;
33
- }
34
-
35
35
  /** Parse a duration string like "30m", "2h", "90s", "0", "never". Returns ms or null. */
36
- function parseDuration(input: string): number | null {
36
+ export function parseDuration(input: string): number | null {
37
37
  const trimmed = input.trim().toLowerCase();
38
38
  if (trimmed === "0" || trimmed === "never" || trimmed === "off") return 0;
39
39
  const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(trimmed);
@@ -45,129 +45,78 @@ function parseDuration(input: string): number | null {
45
45
  return Math.floor(n * mul);
46
46
  }
47
47
 
48
- /** Parse the argv for serve. Returns null on error (already written to stderr). */
49
- export function parseServeArgs(
50
- argv: string[],
51
- ctx: Pick<ServeContext, "stdout" | "stderr">,
52
- ): ServeOptions | null | "help" {
53
- let values: {
54
- port: string | undefined;
55
- hostname: string | undefined;
56
- "idle-timeout": string | undefined;
57
- "state-dir": string | undefined;
58
- help: boolean;
59
- };
60
-
61
- try {
62
- const parsed = parseArgs({
63
- args: argv,
64
- options: {
65
- port: { type: "string", short: "p" },
66
- hostname: { type: "string", short: "H" },
67
- "idle-timeout": { type: "string" },
68
- "state-dir": { type: "string" },
69
- help: { type: "boolean", short: "h", default: false },
70
- },
71
- allowPositionals: false,
72
- strict: true,
73
- });
74
- values = parsed.values as typeof values;
75
- } catch (err) {
76
- const e = err as Error;
77
- ctx.stderr.write(`cesium serve: ${e.message}\n`);
78
- ctx.stderr.write(`Usage: cesium serve [--port N] [--hostname H] [--idle-timeout DUR]\n`);
79
- return null;
80
- }
81
-
82
- if (values.help) {
83
- ctx.stdout.write(
84
- [
85
- "Usage: cesium serve [options]",
86
- "",
87
- "Options:",
88
- " --port, -p N Override configured port (default: 3030)",
89
- " --hostname, -H H Override configured bind address (default: 127.0.0.1)",
90
- " --state-dir DIR Override the cesium state directory",
91
- " --idle-timeout DUR Auto-shutdown after DUR of inactivity. Accepts plain",
92
- " milliseconds or a suffixed value (90s, 30m, 2h).",
93
- " Use 0 / never / off to disable. Default: 0 (never).",
94
- " --help, -h Show this help message",
95
- "",
96
- "Starts the cesium HTTP server in the foreground. Press Ctrl-C to stop.",
97
- "Uses the same config as the opencode plugin (~/.config/opencode/cesium.json).",
98
- "",
99
- "Note: foreground `cesium serve` ignores the configured idleTimeoutMs by",
100
- "default — the timeout exists for the plugin's lazy-started server, not",
101
- "for a server you launched explicitly.",
102
- "",
103
- ].join("\n"),
104
- );
105
- return "help";
106
- }
48
+ interface ValidatedServeArgs {
49
+ port?: number;
50
+ hostname?: string;
51
+ stateDir?: string;
52
+ idleTimeoutMs?: number;
53
+ }
107
54
 
108
- const opts: ServeOptions = {};
55
+ /**
56
+ * Validate ServeArgs and return a ValidatedServeArgs object, or null on error.
57
+ * Writes the error message to ctx.stderr.
58
+ */
59
+ export function validateServeArgs(args: ServeArgs, ctx: ServeContext): ValidatedServeArgs | null {
60
+ const out: ValidatedServeArgs = {};
109
61
 
110
- if (values.port !== undefined) {
111
- const p = parseInt(values.port, 10);
112
- if (isNaN(p) || p < 1 || p > 65535) {
62
+ if (args.port !== undefined) {
63
+ if (!Number.isInteger(args.port) || args.port < 1 || args.port > 65535) {
113
64
  ctx.stderr.write(`cesium serve: --port must be a number between 1 and 65535\n`);
114
65
  return null;
115
66
  }
116
- opts.port = p;
67
+ out.port = args.port;
117
68
  }
118
69
 
119
- if (values.hostname !== undefined) {
120
- if (values.hostname.length === 0) {
70
+ if (args.hostname !== undefined) {
71
+ if (args.hostname.length === 0) {
121
72
  ctx.stderr.write(`cesium serve: --hostname must not be empty\n`);
122
73
  return null;
123
74
  }
124
- opts.hostname = values.hostname;
75
+ out.hostname = args.hostname;
125
76
  }
126
77
 
127
- if (values["state-dir"] !== undefined) {
128
- if (values["state-dir"].length === 0) {
78
+ if (args.stateDir !== undefined) {
79
+ if (args.stateDir.length === 0) {
129
80
  ctx.stderr.write(`cesium serve: --state-dir must not be empty\n`);
130
81
  return null;
131
82
  }
132
- opts.stateDir = values["state-dir"];
83
+ out.stateDir = args.stateDir;
133
84
  }
134
85
 
135
- if (values["idle-timeout"] !== undefined) {
136
- const ms = parseDuration(values["idle-timeout"]);
137
- if (ms === null) {
138
- ctx.stderr.write(
139
- `cesium serve: --idle-timeout must be a duration like "30m", "2h", "90s", or "0"/"never" to disable\n`,
140
- );
86
+ if (args.idleTimeoutMs !== undefined) {
87
+ if (!Number.isFinite(args.idleTimeoutMs) || args.idleTimeoutMs < 0) {
88
+ ctx.stderr.write(`cesium serve: --idle-timeout must be a non-negative duration\n`);
141
89
  return null;
142
90
  }
143
- opts.idleTimeoutMs = ms;
91
+ out.idleTimeoutMs = args.idleTimeoutMs;
144
92
  }
145
93
 
146
- return opts;
94
+ return out;
147
95
  }
148
96
 
149
- export async function serveCommand(argv: string[], ctx?: Partial<ServeContext>): Promise<number> {
150
- const resolved: ServeContext = { ...defaultCtx(), ...ctx };
97
+ export async function runServe(
98
+ args: ServeArgs,
99
+ ctxOverride?: Partial<ServeContext>,
100
+ ): Promise<number> {
101
+ const ctx: ServeContext = { ...defaultCtx(), ...ctxOverride };
151
102
 
152
- const parseResult = parseServeArgs(argv, resolved);
153
- if (parseResult === null) return 1;
154
- if (parseResult === "help") return 0;
103
+ const validated = validateServeArgs(args, ctx);
104
+ if (validated === null) return 1;
155
105
 
156
- const opts = parseResult;
157
- const cfg = (resolved.loadConfig ?? loadConfig)();
106
+ const cfg = (ctx.loadConfig ?? loadConfig)();
158
107
 
159
108
  // Foreground `cesium serve` defaults to NO idle timeout — when the user
160
109
  // launches the server explicitly, they want it to live until they Ctrl-C.
161
110
  // The configured idleTimeoutMs only applies to the plugin's lazy-started
162
111
  // server. --idle-timeout opts back into auto-shutdown.
163
- const effectiveIdleTimeoutMs = opts.idleTimeoutMs ?? 0;
112
+ const effectiveIdleTimeoutMs = validated.idleTimeoutMs ?? 0;
164
113
 
165
114
  // Apply overrides from CLI flags
166
115
  const effectiveCfg = {
167
116
  ...cfg,
168
- ...(opts.stateDir !== undefined ? { stateDir: opts.stateDir } : {}),
169
- ...(opts.port !== undefined ? { port: opts.port, portMax: opts.port } : {}),
170
- ...(opts.hostname !== undefined ? { hostname: opts.hostname } : {}),
117
+ ...(validated.stateDir !== undefined ? { stateDir: validated.stateDir } : {}),
118
+ ...(validated.port !== undefined ? { port: validated.port, portMax: validated.port } : {}),
119
+ ...(validated.hostname !== undefined ? { hostname: validated.hostname } : {}),
171
120
  idleTimeoutMs: effectiveIdleTimeoutMs,
172
121
  };
173
122
 
@@ -184,7 +133,7 @@ export async function serveCommand(argv: string[], ctx?: Partial<ServeContext>):
184
133
  });
185
134
  } catch (err) {
186
135
  const e = err as Error;
187
- resolved.stderr.write(`cesium serve: failed to start server: ${e.message}\n`);
136
+ ctx.stderr.write(`cesium serve: failed to start server: ${e.message}\n`);
188
137
  return 1;
189
138
  }
190
139
 
@@ -195,26 +144,24 @@ export async function serveCommand(argv: string[], ctx?: Partial<ServeContext>):
195
144
  const home = process.env["HOME"] ?? "";
196
145
  const stateDirDisplay = home ? effectiveCfg.stateDir.replace(home, "~") : effectiveCfg.stateDir;
197
146
 
198
- resolved.stdout.write(`cesium serve · ${displayUrl}\n`);
199
- resolved.stdout.write(` serving ${stateDirDisplay}\n`);
147
+ ctx.stdout.write(`cesium serve · ${displayUrl}\n`);
148
+ ctx.stdout.write(` serving ${stateDirDisplay}\n`);
200
149
  if (effectiveIdleTimeoutMs <= 0) {
201
- resolved.stdout.write(` no idle timeout — runs until Ctrl-C\n`);
150
+ ctx.stdout.write(` no idle timeout — runs until Ctrl-C\n`);
202
151
  } else {
203
152
  const minutes = Math.round(effectiveIdleTimeoutMs / 60_000);
204
- resolved.stdout.write(
153
+ ctx.stdout.write(
205
154
  ` idle timeout: ${minutes >= 1 ? `${minutes}m` : `${effectiveIdleTimeoutMs}ms`} of inactivity\n`,
206
155
  );
207
156
  }
208
- resolved.stdout.write(` Ctrl-C to stop\n`);
157
+ ctx.stdout.write(` Ctrl-C to stop\n`);
209
158
 
210
159
  // If binding on all interfaces, also print the LAN URL
211
160
  if (effectiveCfg.hostname === "0.0.0.0" || effectiveCfg.hostname === "::") {
212
- // resolveDisplayHost already returns the LAN IP for 0.0.0.0
213
- resolved.stdout.write(` LAN: ${displayUrl}\n`);
161
+ ctx.stdout.write(` LAN: ${displayUrl}\n`);
214
162
  }
215
163
 
216
- // Run in the foreground until SIGINT/SIGTERM (handled by lifecycle module's signal handlers)
217
- // Keep the process alive by returning a promise that never resolves (until signal fires)
164
+ // Run in the foreground until SIGINT/SIGTERM
218
165
  await new Promise<void>((resolve) => {
219
166
  const cleanup = () => {
220
167
  void stopRunning(effectiveCfg.stateDir).finally(() => {
@@ -227,3 +174,60 @@ export async function serveCommand(argv: string[], ctx?: Partial<ServeContext>):
227
174
 
228
175
  return 0;
229
176
  }
177
+
178
+ export const serveCmd = defineCommand({
179
+ meta: {
180
+ name: "serve",
181
+ description: "Start the cesium HTTP server in the foreground. Press Ctrl-C to stop.",
182
+ },
183
+ args: {
184
+ port: {
185
+ type: "string",
186
+ alias: "p",
187
+ description: "Override configured port (default: 3030)",
188
+ },
189
+ hostname: {
190
+ type: "string",
191
+ alias: "H",
192
+ description: "Override configured bind address (default: 127.0.0.1)",
193
+ },
194
+ "state-dir": {
195
+ type: "string",
196
+ description: "Override the cesium state directory",
197
+ },
198
+ "idle-timeout": {
199
+ type: "string",
200
+ description:
201
+ 'Auto-shutdown after DUR of inactivity (e.g. "30m", "2h", "90s"). Use 0/never/off to disable. Default: never.',
202
+ },
203
+ },
204
+ async run({ args }) {
205
+ const serveArgs: ServeArgs = {};
206
+
207
+ if (args.port !== undefined) {
208
+ const p = parseInt(args.port, 10);
209
+ if (isNaN(p) || p < 1 || p > 65535) {
210
+ process.stderr.write(`cesium serve: --port must be a number between 1 and 65535\n`);
211
+ process.exit(1);
212
+ }
213
+ serveArgs.port = p;
214
+ }
215
+
216
+ if (args.hostname !== undefined) serveArgs.hostname = args.hostname;
217
+ if (args["state-dir"] !== undefined) serveArgs.stateDir = args["state-dir"];
218
+
219
+ if (args["idle-timeout"] !== undefined) {
220
+ const ms = parseDuration(args["idle-timeout"]);
221
+ if (ms === null) {
222
+ process.stderr.write(
223
+ `cesium serve: --idle-timeout must be a duration like "30m", "2h", "90s", or "0"/"never" to disable\n`,
224
+ );
225
+ process.exit(1);
226
+ }
227
+ serveArgs.idleTimeoutMs = ms;
228
+ }
229
+
230
+ const code = await runServe(serveArgs);
231
+ if (code !== 0) process.exit(code);
232
+ },
233
+ });
@@ -1,10 +1,15 @@
1
1
  // cesium stop — kill the running cesium server cross-process via PID file.
2
2
 
3
- import { parseArgs } from "node:util";
3
+ import { defineCommand } from "citty";
4
4
  import { loadConfig, type CesiumConfig } from "../../config.ts";
5
5
  import { stopServer } from "../../server/stop.ts";
6
6
  import type { StopServerArgs } from "../../server/stop.ts";
7
7
 
8
+ export interface StopArgs {
9
+ force: boolean;
10
+ timeoutMs: number;
11
+ }
12
+
8
13
  export interface StopContext {
9
14
  stdout: { write: (s: string) => void };
10
15
  stderr: { write: (s: string) => void };
@@ -22,109 +27,71 @@ function defaultCtx(): StopContext {
22
27
  };
23
28
  }
24
29
 
25
- export interface StopOptions {
26
- force: boolean;
27
- timeout: number;
28
- }
29
-
30
- /** Parse stop-command argv. Returns null on parse error. */
31
- export function parseStopArgs(
32
- argv: string[],
33
- ctx: Pick<StopContext, "stdout" | "stderr">,
34
- ): StopOptions | null | "help" {
35
- let values: { force: boolean; timeout: string | undefined; help: boolean };
30
+ export async function runStop(args: StopArgs, ctxOverride?: Partial<StopContext>): Promise<number> {
31
+ const ctx: StopContext = { ...defaultCtx(), ...ctxOverride };
36
32
 
37
- try {
38
- const parsed = parseArgs({
39
- args: argv,
40
- options: {
41
- force: { type: "boolean", short: "f", default: false },
42
- timeout: { type: "string" },
43
- help: { type: "boolean", short: "h", default: false },
44
- },
45
- allowPositionals: false,
46
- strict: true,
47
- });
48
- values = parsed.values as typeof values;
49
- } catch (err) {
50
- const e = err as Error;
51
- ctx.stderr.write(`cesium stop: ${e.message}\n`);
52
- ctx.stderr.write(`Usage: cesium stop [--force] [--timeout <ms>]\n`);
53
- return null;
54
- }
55
-
56
- if (values.help) {
57
- ctx.stdout.write(
58
- [
59
- "Usage: cesium stop [options]",
60
- "",
61
- "Options:",
62
- " --force, -f SIGKILL immediately — skip the SIGTERM grace period",
63
- " --timeout <ms> Grace period in ms before SIGKILL (default: 3000)",
64
- " --help, -h Show this help message",
65
- "",
66
- "Stops the running cesium server via its PID file. Idempotent when no",
67
- "server is running.",
68
- "",
69
- ].join("\n"),
70
- );
71
- return "help";
72
- }
73
-
74
- let timeout = 3000;
75
- if (values.timeout !== undefined) {
76
- const t = parseInt(values.timeout, 10);
77
- if (isNaN(t) || t < 0) {
78
- ctx.stderr.write(`cesium stop: --timeout must be a non-negative integer\n`);
79
- return null;
80
- }
81
- timeout = t;
33
+ if (!Number.isInteger(args.timeoutMs) || args.timeoutMs < 0) {
34
+ ctx.stderr.write(`cesium stop: --timeout must be a non-negative integer\n`);
35
+ return 1;
82
36
  }
83
37
 
84
- return { force: values.force, timeout };
85
- }
86
-
87
- export async function stopCommand(argv: string[], ctx?: Partial<StopContext>): Promise<number> {
88
- const resolved: StopContext = { ...defaultCtx(), ...ctx };
89
-
90
- const parseResult = parseStopArgs(argv, resolved);
91
- if (parseResult === null) return 1;
92
- if (parseResult === "help") return 0;
93
-
94
- const opts = parseResult;
95
- const cfg = (resolved.loadConfig ?? loadConfig)();
38
+ const cfg = (ctx.loadConfig ?? loadConfig)();
96
39
 
97
40
  const stopArgs: StopServerArgs = {
98
41
  stateDir: cfg.stateDir,
99
- force: opts.force,
100
- timeoutMs: opts.timeout,
42
+ force: args.force,
43
+ timeoutMs: args.timeoutMs,
101
44
  };
102
- if (resolved.isAlive !== undefined) {
103
- stopArgs.isAlive = resolved.isAlive;
104
- }
105
- if (resolved.killProcess !== undefined) {
106
- stopArgs.killProcess = resolved.killProcess;
107
- }
108
- if (resolved.sleep !== undefined) {
109
- stopArgs.sleep = resolved.sleep;
110
- }
45
+ if (ctx.isAlive !== undefined) stopArgs.isAlive = ctx.isAlive;
46
+ if (ctx.killProcess !== undefined) stopArgs.killProcess = ctx.killProcess;
47
+ if (ctx.sleep !== undefined) stopArgs.sleep = ctx.sleep;
111
48
 
112
49
  const outcome = await stopServer(stopArgs);
113
50
 
114
51
  switch (outcome.kind) {
115
52
  case "not-running":
116
- resolved.stdout.write("no cesium server running\n");
53
+ ctx.stdout.write("no cesium server running\n");
117
54
  return 0;
118
55
  case "stale":
119
- resolved.stdout.write("server not running (stale PID file removed)\n");
56
+ ctx.stdout.write("server not running (stale PID file removed)\n");
120
57
  return 0;
121
58
  case "stopped":
122
- resolved.stdout.write(`stopped cesium server (pid ${outcome.pid}, port ${outcome.port})\n`);
59
+ ctx.stdout.write(`stopped cesium server (pid ${outcome.pid}, port ${outcome.port})\n`);
123
60
  return 0;
124
61
  case "permission-denied":
125
- resolved.stderr.write(
62
+ ctx.stderr.write(
126
63
  `cesium stop: permission denied — process ${outcome.pid} is owned by another user\n`,
127
64
  );
128
65
  return 2;
129
66
  }
130
67
  }
68
+
69
+ export const stopCmd = defineCommand({
70
+ meta: {
71
+ name: "stop",
72
+ description:
73
+ "Stop the running cesium server via its PID file. Idempotent when no server is running.",
74
+ },
75
+ args: {
76
+ force: {
77
+ type: "boolean",
78
+ alias: "f",
79
+ default: false,
80
+ description: "SIGKILL immediately — skip the SIGTERM grace period",
81
+ },
82
+ timeout: {
83
+ type: "string",
84
+ default: "3000",
85
+ description: "Grace period in ms before SIGKILL",
86
+ },
87
+ },
88
+ async run({ args }) {
89
+ const t = parseInt(args.timeout, 10);
90
+ if (isNaN(t) || t < 0) {
91
+ process.stderr.write(`cesium stop: --timeout must be a non-negative integer\n`);
92
+ process.exit(1);
93
+ }
94
+ const code = await runStop({ force: args.force, timeoutMs: t });
95
+ if (code !== 0) process.exit(code);
96
+ },
97
+ });