@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.6.0 — 2026-05-13
|
|
4
|
+
|
|
5
|
+
Internal cleanup release. Two hand-rolled server and CLI layers swapped
|
|
6
|
+
for small, well-fit libraries (Hono and Citty) that delete the regex and
|
|
7
|
+
dispatcher boilerplate without changing user-visible behavior. No new
|
|
8
|
+
features and no protocol changes — artifacts written by older versions
|
|
9
|
+
continue rendering unchanged.
|
|
10
|
+
|
|
11
|
+
- **refactor:** Migrate the cesium HTTP server from a hand-rolled
|
|
12
|
+
`Bun.serve` fetch handler with a custom `preHandlers` middleware chain
|
|
13
|
+
to a Hono app fronted by Bun.serve. Routes for `/api/sessions/*` and
|
|
14
|
+
`/favicon.ico` now use Hono's typed `:param` syntax instead of regex
|
|
15
|
+
match + manual narrowing. The static file handler is preserved verbatim
|
|
16
|
+
and mounted as `app.notFound` so all 11 existing static-serve tests
|
|
17
|
+
pass unchanged. `ServerHandle.app: Hono` replaces `addHandler` for
|
|
18
|
+
callers that need to register additional routes.
|
|
19
|
+
- **refactor:** Migrate the `cesium` CLI from `node:util parseArgs` +
|
|
20
|
+
a hand-rolled subcommand dispatcher to Citty. Each command file now
|
|
21
|
+
exports a typed `runX(args, ctx)` inner function (kept directly
|
|
22
|
+
testable with injected `{stdout, stderr, loadConfig}`) plus an outer
|
|
23
|
+
`xxxCmd = defineCommand(...)` that Citty consumes. Subcommands load
|
|
24
|
+
lazily via dynamic `import()` so cold-start cost is paid only for the
|
|
25
|
+
command the user actually invoked. `cesium theme show|apply` is now
|
|
26
|
+
expressed as native Citty sub-subcommands rather than the previous
|
|
27
|
+
manual routing.
|
|
28
|
+
- **fix:** `cesium restart` no longer fails on serve-only flags. The
|
|
29
|
+
previous implementation passed argv through to both `stopCommand` and
|
|
30
|
+
`serveCommand` under `strict: true`, so `cesium restart --port 4000`
|
|
31
|
+
would have errored. Restart now defines its own arg schema covering
|
|
32
|
+
both stop and serve options.
|
|
33
|
+
- **chore:** Clean up all 25 oxlint warnings:
|
|
34
|
+
- Restructure `src/server/api.ts` regex matching to remove four
|
|
35
|
+
`!` non-null assertions.
|
|
36
|
+
- Replace `match![1]!` patterns in tests with a local `unwrap(value,
|
|
37
|
+
name)` helper for proper type narrowing.
|
|
38
|
+
- Replace `handle!.url` in tests with an explicit null check.
|
|
39
|
+
- Add targeted `eslint-disable-next-line no-await-in-loop` comments
|
|
40
|
+
(with `--` reason annotations matching the existing repo convention)
|
|
41
|
+
in places where sequential execution is required: middleware chain,
|
|
42
|
+
poll-with-backoff retry, short-circuit search.
|
|
43
|
+
- **chore:** Add `hono@4.12.18` and `citty@0.2.2` to dependencies.
|
|
44
|
+
Removes a meaningful amount of hand-rolled routing / arg-parsing code
|
|
45
|
+
for ~50KB total install footprint.
|
|
46
|
+
- **chore:** `cesium --version` output format changes from
|
|
47
|
+
`cesium 0.6.0` to plain `0.6.0` (Citty's default). The `cesium version`
|
|
48
|
+
subcommand is removed — use `cesium --version` or `cesium -v`. Auto-
|
|
49
|
+
generated help text for each command also follows Citty's formatting
|
|
50
|
+
(uppercase USAGE/OPTIONS, color codes, default annotations) rather
|
|
51
|
+
than the previous hand-aligned `Usage: cesium ls` strings.
|
|
52
|
+
|
|
3
53
|
## v0.5.2 — 2026-05-12
|
|
4
54
|
|
|
5
55
|
A 16th block type — `diff` — that renders a beautiful side-by-side
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfbender/cesium",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Beautiful self-contained HTML artifacts from your opencode agent.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@opencode-ai/plugin": "latest",
|
|
49
|
+
"citty": "^0.2.2",
|
|
50
|
+
"hono": "^4.12.18",
|
|
49
51
|
"nanoid": "^5.0.0",
|
|
50
52
|
"parse5": "^7.1.0",
|
|
51
53
|
"shiki": "^4.0.2"
|
package/src/cli/commands/ls.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
// cesium ls — list artifacts for current project (or all).
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
6
|
import { loadConfig, type CesiumConfig } from "../../config.ts";
|
|
7
7
|
import { loadIndex, type IndexEntry } from "../../storage/index-cache.ts";
|
|
8
8
|
import { deriveProjectIdentity } from "../../storage/paths.ts";
|
|
9
9
|
|
|
10
|
+
export interface LsArgs {
|
|
11
|
+
all: boolean;
|
|
12
|
+
json: boolean;
|
|
13
|
+
limit: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
export interface LsContext {
|
|
11
17
|
stdout: { write: (s: string) => void };
|
|
12
18
|
stderr: { write: (s: string) => void };
|
|
@@ -58,69 +64,27 @@ function getGitRemote(cwd: string): string | null {
|
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
limit: string | undefined;
|
|
68
|
-
help: boolean;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const parsed = parseArgs({
|
|
73
|
-
args: argv,
|
|
74
|
-
options: {
|
|
75
|
-
all: { type: "boolean", short: "a", default: false },
|
|
76
|
-
json: { type: "boolean", default: false },
|
|
77
|
-
limit: { type: "string", short: "n" },
|
|
78
|
-
help: { type: "boolean", short: "h", default: false },
|
|
79
|
-
},
|
|
80
|
-
allowPositionals: false,
|
|
81
|
-
strict: true,
|
|
82
|
-
});
|
|
83
|
-
values = parsed.values as typeof values;
|
|
84
|
-
} catch (err) {
|
|
85
|
-
const e = err as Error;
|
|
86
|
-
resolved.stderr.write(`cesium ls: ${e.message}\n`);
|
|
87
|
-
resolved.stderr.write(`Usage: cesium ls [--all] [--json] [--limit N]\n`);
|
|
88
|
-
return 1;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (values.help) {
|
|
92
|
-
resolved.stdout.write(
|
|
93
|
-
[
|
|
94
|
-
"Usage: cesium ls [options]",
|
|
95
|
-
"",
|
|
96
|
-
"Options:",
|
|
97
|
-
" --all, -a Show artifacts for all projects (default: current project only)",
|
|
98
|
-
" --json Output as JSON array",
|
|
99
|
-
" --limit, -n N Show at most N most recent artifacts (default: 50)",
|
|
100
|
-
" --help, -h Show this help message",
|
|
101
|
-
"",
|
|
102
|
-
].join("\n"),
|
|
103
|
-
);
|
|
104
|
-
return 0;
|
|
105
|
-
}
|
|
67
|
+
/**
|
|
68
|
+
* Inner command logic. Tests call this directly with typed args + injected
|
|
69
|
+
* context for fast feedback; the Citty wrapper handles argv parsing.
|
|
70
|
+
*/
|
|
71
|
+
export async function runLs(args: LsArgs, ctxOverride?: Partial<LsContext>): Promise<number> {
|
|
72
|
+
const ctx: LsContext = { ...defaultCtx(), ...ctxOverride };
|
|
106
73
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
resolved.stderr.write(`cesium ls: --limit must be a positive integer\n`);
|
|
74
|
+
if (args.limit < 1) {
|
|
75
|
+
ctx.stderr.write(`cesium ls: --limit must be a positive integer\n`);
|
|
110
76
|
return 1;
|
|
111
77
|
}
|
|
112
|
-
const limit = limitRaw;
|
|
113
78
|
|
|
114
|
-
|
|
115
|
-
const cfg = (resolved.loadConfig ?? loadConfig)();
|
|
79
|
+
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
116
80
|
|
|
117
81
|
// Determine which index.json to read
|
|
118
82
|
let jsonPath: string;
|
|
119
|
-
if (
|
|
83
|
+
if (args.all) {
|
|
120
84
|
jsonPath = join(cfg.stateDir, "index.json");
|
|
121
85
|
} else {
|
|
122
|
-
const gitRemote = getGitRemote(
|
|
123
|
-
const identity = deriveProjectIdentity({ cwd:
|
|
86
|
+
const gitRemote = getGitRemote(ctx.cwd);
|
|
87
|
+
const identity = deriveProjectIdentity({ cwd: ctx.cwd, gitRemote });
|
|
124
88
|
jsonPath = join(cfg.stateDir, "projects", identity.slug, "index.json");
|
|
125
89
|
}
|
|
126
90
|
|
|
@@ -129,21 +93,19 @@ export async function lsCommand(argv: string[], ctx?: Partial<LsContext>): Promi
|
|
|
129
93
|
entries = await loadIndex(jsonPath);
|
|
130
94
|
} catch (err) {
|
|
131
95
|
const e = err as Error;
|
|
132
|
-
|
|
96
|
+
ctx.stderr.write(`cesium ls: failed to read index: ${e.message}\n`);
|
|
133
97
|
return 1;
|
|
134
98
|
}
|
|
135
99
|
|
|
136
|
-
|
|
137
|
-
const limited = entries.slice(0, limit);
|
|
100
|
+
const limited = entries.slice(0, args.limit);
|
|
138
101
|
|
|
139
|
-
if (
|
|
140
|
-
|
|
102
|
+
if (args.json) {
|
|
103
|
+
ctx.stdout.write(JSON.stringify(limited, null, 2) + "\n");
|
|
141
104
|
return 0;
|
|
142
105
|
}
|
|
143
106
|
|
|
144
|
-
// Table output
|
|
145
107
|
if (limited.length === 0) {
|
|
146
|
-
|
|
108
|
+
ctx.stdout.write("No artifacts found.\n");
|
|
147
109
|
return 0;
|
|
148
110
|
}
|
|
149
111
|
|
|
@@ -165,8 +127,8 @@ export async function lsCommand(argv: string[], ctx?: Partial<LsContext>): Promi
|
|
|
165
127
|
|
|
166
128
|
const sep = "─".repeat(header.length);
|
|
167
129
|
|
|
168
|
-
|
|
169
|
-
|
|
130
|
+
ctx.stdout.write(header + "\n");
|
|
131
|
+
ctx.stdout.write(sep + "\n");
|
|
170
132
|
|
|
171
133
|
for (const e of limited) {
|
|
172
134
|
const row =
|
|
@@ -179,8 +141,43 @@ export async function lsCommand(argv: string[], ctx?: Partial<LsContext>): Promi
|
|
|
179
141
|
fmtDate(e.createdAt).padEnd(COL_DATE) +
|
|
180
142
|
" " +
|
|
181
143
|
superCol(e);
|
|
182
|
-
|
|
144
|
+
ctx.stdout.write(row + "\n");
|
|
183
145
|
}
|
|
184
146
|
|
|
185
147
|
return 0;
|
|
186
148
|
}
|
|
149
|
+
|
|
150
|
+
export const lsCmd = defineCommand({
|
|
151
|
+
meta: {
|
|
152
|
+
name: "ls",
|
|
153
|
+
description: "List artifacts in the current project (or all with --all).",
|
|
154
|
+
},
|
|
155
|
+
args: {
|
|
156
|
+
all: {
|
|
157
|
+
type: "boolean",
|
|
158
|
+
alias: "a",
|
|
159
|
+
default: false,
|
|
160
|
+
description: "Show artifacts for all projects (default: current project only)",
|
|
161
|
+
},
|
|
162
|
+
json: {
|
|
163
|
+
type: "boolean",
|
|
164
|
+
default: false,
|
|
165
|
+
description: "Output as JSON array",
|
|
166
|
+
},
|
|
167
|
+
limit: {
|
|
168
|
+
type: "string",
|
|
169
|
+
alias: "n",
|
|
170
|
+
default: "50",
|
|
171
|
+
description: "Show at most N most recent artifacts",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
async run({ args }) {
|
|
175
|
+
const limit = parseInt(args.limit, 10);
|
|
176
|
+
if (isNaN(limit) || limit < 1) {
|
|
177
|
+
process.stderr.write(`cesium ls: --limit must be a positive integer\n`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
const code = await runLs({ all: args.all, json: args.json, limit });
|
|
181
|
+
if (code !== 0) process.exit(code);
|
|
182
|
+
},
|
|
183
|
+
});
|
package/src/cli/commands/open.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cesium open — find an artifact by id prefix and open it in the browser.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { platform } from "node:os";
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
} from "../../server/lifecycle.ts";
|
|
16
16
|
import { resolveDisplayHost } from "../../tools/publish.ts";
|
|
17
17
|
|
|
18
|
+
export interface OpenArgs {
|
|
19
|
+
idPrefix: string;
|
|
20
|
+
print: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export interface OpenContext {
|
|
19
24
|
stdout: { write: (s: string) => void };
|
|
20
25
|
stderr: { write: (s: string) => void };
|
|
@@ -90,59 +95,16 @@ async function tryGetHttpUrl(
|
|
|
90
95
|
return null;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
export async function
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
let values: { print: boolean; help: boolean };
|
|
97
|
-
let positionals: string[];
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const parsed = parseArgs({
|
|
101
|
-
args: argv,
|
|
102
|
-
options: {
|
|
103
|
-
print: { type: "boolean", default: false },
|
|
104
|
-
help: { type: "boolean", short: "h", default: false },
|
|
105
|
-
},
|
|
106
|
-
allowPositionals: true,
|
|
107
|
-
strict: true,
|
|
108
|
-
});
|
|
109
|
-
values = parsed.values as typeof values;
|
|
110
|
-
positionals = parsed.positionals;
|
|
111
|
-
} catch (err) {
|
|
112
|
-
const e = err as Error;
|
|
113
|
-
resolved.stderr.write(`cesium open: ${e.message}\n`);
|
|
114
|
-
resolved.stderr.write(`Usage: cesium open <id-prefix> [--print]\n`);
|
|
115
|
-
return 1;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (values.help) {
|
|
119
|
-
resolved.stdout.write(
|
|
120
|
-
[
|
|
121
|
-
"Usage: cesium open <id-prefix> [options]",
|
|
122
|
-
"",
|
|
123
|
-
"Options:",
|
|
124
|
-
" --print Print the URL instead of opening in the browser",
|
|
125
|
-
" --help, -h Show this help message",
|
|
126
|
-
"",
|
|
127
|
-
"Notes:",
|
|
128
|
-
" Browser launch is supported on macOS (open) and Linux (xdg-open).",
|
|
129
|
-
" On Windows, use --print to get the URL.",
|
|
130
|
-
"",
|
|
131
|
-
].join("\n"),
|
|
132
|
-
);
|
|
133
|
-
return 0;
|
|
134
|
-
}
|
|
98
|
+
export async function runOpen(args: OpenArgs, ctxOverride?: Partial<OpenContext>): Promise<number> {
|
|
99
|
+
const ctx: OpenContext = { ...defaultCtx(), ...ctxOverride };
|
|
135
100
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
resolved.stderr.write(`cesium open: missing required argument <id-prefix>\n`);
|
|
139
|
-
resolved.stderr.write(`Usage: cesium open <id-prefix> [--print]\n`);
|
|
101
|
+
if (args.idPrefix.length === 0) {
|
|
102
|
+
ctx.stderr.write(`cesium open: missing required argument <id-prefix>\n`);
|
|
140
103
|
return 1;
|
|
141
104
|
}
|
|
142
105
|
|
|
143
|
-
const prefixLower = idPrefix.toLowerCase();
|
|
144
|
-
|
|
145
|
-
const cfg = (resolved.loadConfig ?? loadConfig)();
|
|
106
|
+
const prefixLower = args.idPrefix.toLowerCase();
|
|
107
|
+
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
146
108
|
|
|
147
109
|
// Search global index for matches
|
|
148
110
|
const globalJsonPath = join(cfg.stateDir, "index.json");
|
|
@@ -151,23 +113,23 @@ export async function openCommand(argv: string[], ctx?: Partial<OpenContext>): P
|
|
|
151
113
|
allEntries = await loadIndex(globalJsonPath);
|
|
152
114
|
} catch (err) {
|
|
153
115
|
const e = err as Error;
|
|
154
|
-
|
|
116
|
+
ctx.stderr.write(`cesium open: failed to read index: ${e.message}\n`);
|
|
155
117
|
return 1;
|
|
156
118
|
}
|
|
157
119
|
|
|
158
120
|
const matches = allEntries.filter((e) => e.id.toLowerCase().startsWith(prefixLower));
|
|
159
121
|
|
|
160
122
|
if (matches.length === 0) {
|
|
161
|
-
|
|
123
|
+
ctx.stderr.write(`cesium open: no artifact found with id prefix "${args.idPrefix}"\n`);
|
|
162
124
|
return 1;
|
|
163
125
|
}
|
|
164
126
|
|
|
165
127
|
if (matches.length > 1) {
|
|
166
|
-
|
|
167
|
-
`cesium open: ambiguous prefix "${idPrefix}" — ${matches.length} matches:\n`,
|
|
128
|
+
ctx.stderr.write(
|
|
129
|
+
`cesium open: ambiguous prefix "${args.idPrefix}" — ${matches.length} matches:\n`,
|
|
168
130
|
);
|
|
169
131
|
for (const m of matches) {
|
|
170
|
-
|
|
132
|
+
ctx.stderr.write(` ${m.id} ${m.title} (${m.kind})\n`);
|
|
171
133
|
}
|
|
172
134
|
return 2;
|
|
173
135
|
}
|
|
@@ -175,7 +137,7 @@ export async function openCommand(argv: string[], ctx?: Partial<OpenContext>): P
|
|
|
175
137
|
const entry = matches[0];
|
|
176
138
|
if (entry === undefined) {
|
|
177
139
|
// Unreachable: guarded by matches.length === 1, but satisfies the type checker
|
|
178
|
-
|
|
140
|
+
ctx.stderr.write(`cesium open: internal error — no match\n`);
|
|
179
141
|
return 1;
|
|
180
142
|
}
|
|
181
143
|
const paths = pathsFor({
|
|
@@ -185,24 +147,47 @@ export async function openCommand(argv: string[], ctx?: Partial<OpenContext>): P
|
|
|
185
147
|
});
|
|
186
148
|
|
|
187
149
|
// Resolve URL
|
|
188
|
-
const runEnsureRunning =
|
|
150
|
+
const runEnsureRunning = ctx.ensureRunning ?? ensureRunning;
|
|
189
151
|
const httpUrl = await tryGetHttpUrl(cfg, paths.serverPath, runEnsureRunning);
|
|
190
152
|
const url = httpUrl ?? paths.fileUrl;
|
|
191
153
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
154
|
+
if (args.print) {
|
|
155
|
+
ctx.stdout.write(url + "\n");
|
|
194
156
|
return 0;
|
|
195
157
|
}
|
|
196
158
|
|
|
197
|
-
const open =
|
|
159
|
+
const open = ctx.opener ?? defaultOpener;
|
|
198
160
|
try {
|
|
199
161
|
await open(url);
|
|
200
162
|
} catch (err) {
|
|
201
163
|
const e = err as Error;
|
|
202
|
-
|
|
203
|
-
|
|
164
|
+
ctx.stderr.write(`cesium open: ${e.message}\n`);
|
|
165
|
+
ctx.stdout.write(`URL: ${url}\n`);
|
|
204
166
|
return 1;
|
|
205
167
|
}
|
|
206
168
|
|
|
207
169
|
return 0;
|
|
208
170
|
}
|
|
171
|
+
|
|
172
|
+
export const openCmd = defineCommand({
|
|
173
|
+
meta: {
|
|
174
|
+
name: "open",
|
|
175
|
+
description: "Open an artifact by id prefix in the browser.",
|
|
176
|
+
},
|
|
177
|
+
args: {
|
|
178
|
+
idPrefix: {
|
|
179
|
+
type: "positional",
|
|
180
|
+
description: "Artifact id prefix (any unique substring of the id)",
|
|
181
|
+
required: true,
|
|
182
|
+
},
|
|
183
|
+
print: {
|
|
184
|
+
type: "boolean",
|
|
185
|
+
default: false,
|
|
186
|
+
description: "Print the URL instead of opening in the browser",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
async run({ args }) {
|
|
190
|
+
const code = await runOpen({ idPrefix: args.idPrefix, print: args.print });
|
|
191
|
+
if (code !== 0) process.exit(code);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
@@ -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
|
+
});
|