@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.
- package/CHANGELOG.md +50 -0
- package/package.json +3 -1
- 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 +17 -70
- package/src/render/theme.ts +18 -0
- 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/theme-write.ts +17 -3
- package/src/tools/wait.ts +1 -0
|
@@ -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
|
+
});
|
package/src/cli/commands/stop.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// cesium stop — kill the running cesium server cross-process via PID file.
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
26
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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:
|
|
100
|
-
timeoutMs:
|
|
42
|
+
force: args.force,
|
|
43
|
+
timeoutMs: args.timeoutMs,
|
|
101
44
|
};
|
|
102
|
-
if (
|
|
103
|
-
|
|
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
|
-
|
|
53
|
+
ctx.stdout.write("no cesium server running\n");
|
|
117
54
|
return 0;
|
|
118
55
|
case "stale":
|
|
119
|
-
|
|
56
|
+
ctx.stdout.write("server not running (stale PID file removed)\n");
|
|
120
57
|
return 0;
|
|
121
58
|
case "stopped":
|
|
122
|
-
|
|
59
|
+
ctx.stdout.write(`stopped cesium server (pid ${outcome.pid}, port ${outcome.port})\n`);
|
|
123
60
|
return 0;
|
|
124
61
|
case "permission-denied":
|
|
125
|
-
|
|
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
|
+
});
|