@cfbender/cesium 0.5.2 → 0.6.1
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/CHANGELOG.md +84 -0
- package/README.md +13 -4
- package/package.json +3 -1
- package/src/cli/commands/export.ts +143 -0
- package/src/cli/commands/ls.ts +62 -65
- package/src/cli/commands/open.ts +47 -62
- package/src/cli/commands/prune.ts +59 -71
- package/src/cli/commands/restart.ts +100 -12
- package/src/cli/commands/serve.ts +118 -114
- package/src/cli/commands/stop.ts +51 -84
- package/src/cli/commands/theme.ts +54 -92
- package/src/cli/index.ts +18 -70
- package/src/render/theme.ts +18 -0
- package/src/render/wrap.ts +9 -5
- package/src/server/api.ts +112 -124
- package/src/server/favicon.ts +8 -16
- package/src/server/http.ts +101 -106
- package/src/server/lifecycle.ts +7 -5
- package/src/storage/assets.ts +8 -10
- package/src/storage/index-gen.ts +11 -9
- package/src/storage/theme-write.ts +17 -3
- package/src/tools/wait.ts +1 -0
- package/src/render/fallback.ts +0 -18
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cesium prune — delete artifacts older than a given duration.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { readdir, unlink as fsUnlink, stat } from "node:fs/promises";
|
|
6
6
|
import { loadConfig, type CesiumConfig } from "../../config.ts";
|
|
@@ -15,6 +15,11 @@ import { themeFromPreset, mergeTheme } from "../../render/theme.ts";
|
|
|
15
15
|
import { readEmbeddedMetadata } from "../../storage/write.ts";
|
|
16
16
|
import { readFile } from "node:fs/promises";
|
|
17
17
|
|
|
18
|
+
export interface PruneArgs {
|
|
19
|
+
olderThan: string;
|
|
20
|
+
yes: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export interface PruneContext {
|
|
19
24
|
stdout: { write: (s: string) => void };
|
|
20
25
|
stderr: { write: (s: string) => void };
|
|
@@ -164,75 +169,28 @@ function formatTable(candidates: ArtifactCandidate[]): string {
|
|
|
164
169
|
return [header, sep, ...rows].join("\n");
|
|
165
170
|
}
|
|
166
171
|
|
|
167
|
-
export async function
|
|
168
|
-
|
|
172
|
+
export async function runPrune(
|
|
173
|
+
args: PruneArgs,
|
|
174
|
+
ctxOverride?: Partial<PruneContext>,
|
|
175
|
+
): Promise<number> {
|
|
176
|
+
const ctx: PruneContext = { ...defaultCtx(), ...ctxOverride };
|
|
169
177
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"dry-run": boolean;
|
|
173
|
-
yes: boolean;
|
|
174
|
-
help: boolean;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
const parsed = parseArgs({
|
|
179
|
-
args: argv,
|
|
180
|
-
options: {
|
|
181
|
-
"older-than": { type: "string" },
|
|
182
|
-
"dry-run": { type: "boolean", default: false },
|
|
183
|
-
yes: { type: "boolean", short: "y", default: false },
|
|
184
|
-
help: { type: "boolean", short: "h", default: false },
|
|
185
|
-
},
|
|
186
|
-
allowPositionals: false,
|
|
187
|
-
strict: true,
|
|
188
|
-
});
|
|
189
|
-
values = parsed.values as typeof values;
|
|
190
|
-
} catch (err) {
|
|
191
|
-
const e = err as Error;
|
|
192
|
-
resolved.stderr.write(`cesium prune: ${e.message}\n`);
|
|
193
|
-
resolved.stderr.write(`Usage: cesium prune --older-than <duration> [--yes]\n`);
|
|
178
|
+
if (args.olderThan.length === 0) {
|
|
179
|
+
ctx.stderr.write(`cesium prune: --older-than is required\n`);
|
|
194
180
|
return 1;
|
|
195
181
|
}
|
|
196
182
|
|
|
197
|
-
|
|
198
|
-
resolved.stdout.write(
|
|
199
|
-
[
|
|
200
|
-
"Usage: cesium prune --older-than <duration> [options]",
|
|
201
|
-
"",
|
|
202
|
-
"Options:",
|
|
203
|
-
" --older-than <dur> Delete artifacts older than this duration (e.g. 90d, 2w, 12h, 30m)",
|
|
204
|
-
" --yes, -y Actually delete (default is dry-run)",
|
|
205
|
-
" --dry-run Explicit dry-run (same as omitting --yes)",
|
|
206
|
-
" --help, -h Show this help message",
|
|
207
|
-
"",
|
|
208
|
-
"Duration format: <N><unit> where unit is d (days), w (weeks), h (hours), m (minutes).",
|
|
209
|
-
"",
|
|
210
|
-
"Note: prune deletes by age only. It does not check revision chains.",
|
|
211
|
-
" Deleting an early version in a supersedes chain does not affect the newer version.",
|
|
212
|
-
"",
|
|
213
|
-
].join("\n"),
|
|
214
|
-
);
|
|
215
|
-
return 0;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const durationStr = values["older-than"];
|
|
219
|
-
if (durationStr === undefined || durationStr.length === 0) {
|
|
220
|
-
resolved.stderr.write(`cesium prune: --older-than is required\n`);
|
|
221
|
-
resolved.stderr.write(`Usage: cesium prune --older-than <duration> [--yes]\n`);
|
|
222
|
-
return 1;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const durationMs = parseDuration(durationStr);
|
|
183
|
+
const durationMs = parseDuration(args.olderThan);
|
|
226
184
|
if (durationMs === null) {
|
|
227
|
-
|
|
228
|
-
`cesium prune: invalid duration "${
|
|
185
|
+
ctx.stderr.write(
|
|
186
|
+
`cesium prune: invalid duration "${args.olderThan}". Use format like 90d, 2w, 12h, 30m\n`,
|
|
229
187
|
);
|
|
230
188
|
return 1;
|
|
231
189
|
}
|
|
232
190
|
|
|
233
|
-
const isDryRun = !
|
|
234
|
-
const cfg = (
|
|
235
|
-
const now = (
|
|
191
|
+
const isDryRun = !args.yes;
|
|
192
|
+
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
193
|
+
const now = (ctx.now ?? (() => new Date()))();
|
|
236
194
|
const cutoff = now.getTime() - durationMs;
|
|
237
195
|
|
|
238
196
|
// Collect all artifacts
|
|
@@ -241,7 +199,7 @@ export async function pruneCommand(argv: string[], ctx?: Partial<PruneContext>):
|
|
|
241
199
|
allArtifacts = await collectAllArtifacts(cfg.stateDir);
|
|
242
200
|
} catch (err) {
|
|
243
201
|
const e = err as Error;
|
|
244
|
-
|
|
202
|
+
ctx.stderr.write(`cesium prune: failed to scan artifacts: ${e.message}\n`);
|
|
245
203
|
return 1;
|
|
246
204
|
}
|
|
247
205
|
|
|
@@ -252,16 +210,16 @@ export async function pruneCommand(argv: string[], ctx?: Partial<PruneContext>):
|
|
|
252
210
|
});
|
|
253
211
|
|
|
254
212
|
if (toDelete.length === 0) {
|
|
255
|
-
|
|
213
|
+
ctx.stdout.write(`No artifacts older than ${args.olderThan} found.\n`);
|
|
256
214
|
return 0;
|
|
257
215
|
}
|
|
258
216
|
|
|
259
217
|
if (isDryRun) {
|
|
260
|
-
|
|
261
|
-
`Would delete ${toDelete.length} artifact${toDelete.length !== 1 ? "s" : ""} older than ${
|
|
218
|
+
ctx.stdout.write(
|
|
219
|
+
`Would delete ${toDelete.length} artifact${toDelete.length !== 1 ? "s" : ""} older than ${args.olderThan}:\n`,
|
|
262
220
|
);
|
|
263
|
-
|
|
264
|
-
|
|
221
|
+
ctx.stdout.write(formatTable(toDelete) + "\n\n");
|
|
222
|
+
ctx.stdout.write(`Re-run with --yes to delete.\n`);
|
|
265
223
|
return 0;
|
|
266
224
|
}
|
|
267
225
|
|
|
@@ -274,9 +232,7 @@ export async function pruneCommand(argv: string[], ctx?: Partial<PruneContext>):
|
|
|
274
232
|
} catch (err) {
|
|
275
233
|
const e = err as NodeJS.ErrnoException;
|
|
276
234
|
if (e.code !== "ENOENT") {
|
|
277
|
-
|
|
278
|
-
`cesium prune: failed to delete ${candidate.filePath}: ${e.message}\n`,
|
|
279
|
-
);
|
|
235
|
+
ctx.stderr.write(`cesium prune: failed to delete ${candidate.filePath}: ${e.message}\n`);
|
|
280
236
|
}
|
|
281
237
|
return false;
|
|
282
238
|
}
|
|
@@ -341,8 +297,40 @@ export async function pruneCommand(argv: string[], ctx?: Partial<PruneContext>):
|
|
|
341
297
|
// best-effort
|
|
342
298
|
}
|
|
343
299
|
|
|
344
|
-
|
|
300
|
+
ctx.stdout.write(
|
|
345
301
|
`Deleted ${deletedCount} artifact${deletedCount !== 1 ? "s" : ""}. Indexes regenerated.\n`,
|
|
346
302
|
);
|
|
347
303
|
return 0;
|
|
348
304
|
}
|
|
305
|
+
|
|
306
|
+
export const pruneCmd = defineCommand({
|
|
307
|
+
meta: {
|
|
308
|
+
name: "prune",
|
|
309
|
+
description: "Delete artifacts older than a given duration.",
|
|
310
|
+
},
|
|
311
|
+
args: {
|
|
312
|
+
"older-than": {
|
|
313
|
+
type: "string",
|
|
314
|
+
required: true,
|
|
315
|
+
description: "Delete artifacts older than this duration (e.g. 90d, 2w, 12h, 30m)",
|
|
316
|
+
},
|
|
317
|
+
yes: {
|
|
318
|
+
type: "boolean",
|
|
319
|
+
alias: "y",
|
|
320
|
+
default: false,
|
|
321
|
+
description: "Actually delete (default is dry-run)",
|
|
322
|
+
},
|
|
323
|
+
"dry-run": {
|
|
324
|
+
type: "boolean",
|
|
325
|
+
default: false,
|
|
326
|
+
description: "Explicit dry-run (same as omitting --yes)",
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
async run({ args }) {
|
|
330
|
+
const code = await runPrune({
|
|
331
|
+
olderThan: args["older-than"],
|
|
332
|
+
yes: args.yes,
|
|
333
|
+
});
|
|
334
|
+
if (code !== 0) process.exit(code);
|
|
335
|
+
},
|
|
336
|
+
});
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
// cesium restart — stop the running server and start a new one in the foreground.
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { type
|
|
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
|
|
8
|
-
serveImpl?: (
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
export async function runRestart(
|
|
19
|
+
args: RestartArgs,
|
|
20
|
+
ctxOverride?: Partial<RestartContext>,
|
|
18
21
|
): Promise<number> {
|
|
19
|
-
const sleepFn =
|
|
20
|
-
const serveFn =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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 (
|
|
111
|
-
|
|
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
|
-
|
|
67
|
+
out.port = args.port;
|
|
117
68
|
}
|
|
118
69
|
|
|
119
|
-
if (
|
|
120
|
-
if (
|
|
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
|
-
|
|
75
|
+
out.hostname = args.hostname;
|
|
125
76
|
}
|
|
126
77
|
|
|
127
|
-
if (
|
|
128
|
-
if (
|
|
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
|
-
|
|
83
|
+
out.stateDir = args.stateDir;
|
|
133
84
|
}
|
|
134
85
|
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
91
|
+
out.idleTimeoutMs = args.idleTimeoutMs;
|
|
144
92
|
}
|
|
145
93
|
|
|
146
|
-
return
|
|
94
|
+
return out;
|
|
147
95
|
}
|
|
148
96
|
|
|
149
|
-
export async function
|
|
150
|
-
|
|
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
|
|
153
|
-
if (
|
|
154
|
-
if (parseResult === "help") return 0;
|
|
103
|
+
const validated = validateServeArgs(args, ctx);
|
|
104
|
+
if (validated === null) return 1;
|
|
155
105
|
|
|
156
|
-
const
|
|
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 =
|
|
112
|
+
const effectiveIdleTimeoutMs = validated.idleTimeoutMs ?? 0;
|
|
164
113
|
|
|
165
114
|
// Apply overrides from CLI flags
|
|
166
115
|
const effectiveCfg = {
|
|
167
116
|
...cfg,
|
|
168
|
-
...(
|
|
169
|
-
...(
|
|
170
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
147
|
+
ctx.stdout.write(`cesium serve · ${displayUrl}\n`);
|
|
148
|
+
ctx.stdout.write(` serving ${stateDirDisplay}\n`);
|
|
200
149
|
if (effectiveIdleTimeoutMs <= 0) {
|
|
201
|
-
|
|
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
|
-
|
|
153
|
+
ctx.stdout.write(
|
|
205
154
|
` idle timeout: ${minutes >= 1 ? `${minutes}m` : `${effectiveIdleTimeoutMs}ms`} of inactivity\n`,
|
|
206
155
|
);
|
|
207
156
|
}
|
|
208
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
});
|