@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.6.1 — 2026-05-13
|
|
4
|
+
|
|
5
|
+
Fixes a regression introduced by the v0.4 blocks refactor: the README's
|
|
6
|
+
"self-contained HTML file" claim had quietly become false. Block-based
|
|
7
|
+
artifacts (cards, callouts, compare-tables, timelines, code) rendered as
|
|
8
|
+
broken-looking plain HTML when opened via `file://` because the inline
|
|
9
|
+
fallback CSS only covered basic typography — all the block styling lived
|
|
10
|
+
in the server-side `theme.css`. Sharing an artifact required either the
|
|
11
|
+
running cesium server or a recipient who happened to have it.
|
|
12
|
+
|
|
13
|
+
- **feat:** Artifacts now embed the full `theme.css` (~21KB) in an inline
|
|
14
|
+
`<style>` block at generation time. Opening any artifact via `file://`
|
|
15
|
+
or on any other machine now renders correctly with zero external
|
|
16
|
+
dependencies — typography, blocks, tables, controls, diff renderer, all
|
|
17
|
+
of it. The `<link rel="stylesheet" href=".../theme.css">` is preserved
|
|
18
|
+
so served artifacts still pick up live theme changes (the link rules
|
|
19
|
+
override the earlier inline `<style>` in cascade order).
|
|
20
|
+
- **feat:** New `cesium export <id-prefix>` command emits an artifact's
|
|
21
|
+
self-contained HTML to stdout, or to `--out file.html`. Resolves the id
|
|
22
|
+
prefix against the global index the same way `cesium open` does. With
|
|
23
|
+
the baked-in CSS, export is now a near-trivial file copy — pipe it
|
|
24
|
+
anywhere, attach it to email, drop it in a chat, commit it to a repo.
|
|
25
|
+
- **refactor:** Removed `src/render/fallback.ts` and the matching test
|
|
26
|
+
file. The 8-line fallback CSS it produced is no longer needed; the
|
|
27
|
+
inline `<style>` carries the full framework directly.
|
|
28
|
+
- **chore:** README "Share an artifact" section now leads with
|
|
29
|
+
`cesium export` and explains the bake-and-override model.
|
|
30
|
+
|
|
31
|
+
Old artifacts on disk are not retroactively re-baked — they keep their
|
|
32
|
+
generation-time fallback + link, which means they continue to render
|
|
33
|
+
fine when served by cesium but look plain when opened standalone.
|
|
34
|
+
Historical fidelity is preserved. New artifacts written from v0.6.1
|
|
35
|
+
onward are genuinely portable.
|
|
36
|
+
|
|
37
|
+
## v0.6.0 — 2026-05-13
|
|
38
|
+
|
|
39
|
+
Internal cleanup release. Two hand-rolled server and CLI layers swapped
|
|
40
|
+
for small, well-fit libraries (Hono and Citty) that delete the regex and
|
|
41
|
+
dispatcher boilerplate without changing user-visible behavior. No new
|
|
42
|
+
features and no protocol changes — artifacts written by older versions
|
|
43
|
+
continue rendering unchanged.
|
|
44
|
+
|
|
45
|
+
- **refactor:** Migrate the cesium HTTP server from a hand-rolled
|
|
46
|
+
`Bun.serve` fetch handler with a custom `preHandlers` middleware chain
|
|
47
|
+
to a Hono app fronted by Bun.serve. Routes for `/api/sessions/*` and
|
|
48
|
+
`/favicon.ico` now use Hono's typed `:param` syntax instead of regex
|
|
49
|
+
match + manual narrowing. The static file handler is preserved verbatim
|
|
50
|
+
and mounted as `app.notFound` so all 11 existing static-serve tests
|
|
51
|
+
pass unchanged. `ServerHandle.app: Hono` replaces `addHandler` for
|
|
52
|
+
callers that need to register additional routes.
|
|
53
|
+
- **refactor:** Migrate the `cesium` CLI from `node:util parseArgs` +
|
|
54
|
+
a hand-rolled subcommand dispatcher to Citty. Each command file now
|
|
55
|
+
exports a typed `runX(args, ctx)` inner function (kept directly
|
|
56
|
+
testable with injected `{stdout, stderr, loadConfig}`) plus an outer
|
|
57
|
+
`xxxCmd = defineCommand(...)` that Citty consumes. Subcommands load
|
|
58
|
+
lazily via dynamic `import()` so cold-start cost is paid only for the
|
|
59
|
+
command the user actually invoked. `cesium theme show|apply` is now
|
|
60
|
+
expressed as native Citty sub-subcommands rather than the previous
|
|
61
|
+
manual routing.
|
|
62
|
+
- **fix:** `cesium restart` no longer fails on serve-only flags. The
|
|
63
|
+
previous implementation passed argv through to both `stopCommand` and
|
|
64
|
+
`serveCommand` under `strict: true`, so `cesium restart --port 4000`
|
|
65
|
+
would have errored. Restart now defines its own arg schema covering
|
|
66
|
+
both stop and serve options.
|
|
67
|
+
- **chore:** Clean up all 25 oxlint warnings:
|
|
68
|
+
- Restructure `src/server/api.ts` regex matching to remove four
|
|
69
|
+
`!` non-null assertions.
|
|
70
|
+
- Replace `match![1]!` patterns in tests with a local `unwrap(value,
|
|
71
|
+
name)` helper for proper type narrowing.
|
|
72
|
+
- Replace `handle!.url` in tests with an explicit null check.
|
|
73
|
+
- Add targeted `eslint-disable-next-line no-await-in-loop` comments
|
|
74
|
+
(with `--` reason annotations matching the existing repo convention)
|
|
75
|
+
in places where sequential execution is required: middleware chain,
|
|
76
|
+
poll-with-backoff retry, short-circuit search.
|
|
77
|
+
- **chore:** Add `hono@4.12.18` and `citty@0.2.2` to dependencies.
|
|
78
|
+
Removes a meaningful amount of hand-rolled routing / arg-parsing code
|
|
79
|
+
for ~50KB total install footprint.
|
|
80
|
+
- **chore:** `cesium --version` output format changes from
|
|
81
|
+
`cesium 0.6.0` to plain `0.6.0` (Citty's default). The `cesium version`
|
|
82
|
+
subcommand is removed — use `cesium --version` or `cesium -v`. Auto-
|
|
83
|
+
generated help text for each command also follows Citty's formatting
|
|
84
|
+
(uppercase USAGE/OPTIONS, color codes, default annotations) rather
|
|
85
|
+
than the previous hand-aligned `Usage: cesium ls` strings.
|
|
86
|
+
|
|
3
87
|
## v0.5.2 — 2026-05-12
|
|
4
88
|
|
|
5
89
|
A 16th block type — `diff` — that renders a beautiful side-by-side
|
package/README.md
CHANGED
|
@@ -48,8 +48,8 @@ unreleased changes).
|
|
|
48
48
|
### CLI
|
|
49
49
|
|
|
50
50
|
The CLI puts a `cesium` binary on your `PATH` for browsing, opening, and
|
|
51
|
-
managing artifacts (`cesium ls`, `cesium open`, `cesium
|
|
52
|
-
`cesium theme`).
|
|
51
|
+
managing artifacts (`cesium ls`, `cesium open`, `cesium export`, `cesium serve`,
|
|
52
|
+
`cesium prune`, `cesium theme`).
|
|
53
53
|
|
|
54
54
|
**Recommended: install with [mise](https://mise.jdx.dev/)** so cesium is pinned
|
|
55
55
|
in your config and tracks with the rest of your toolchain. Add to your
|
|
@@ -292,15 +292,24 @@ cesium open a7K9 --print # just print the URL
|
|
|
292
292
|
|
|
293
293
|
### Share an artifact
|
|
294
294
|
|
|
295
|
-
Each artifact is a single self-contained `.html` file —
|
|
296
|
-
|
|
295
|
+
Each artifact is a single self-contained `.html` file — the full theme CSS is
|
|
296
|
+
baked into a `<style>` tag at generation time, so it renders correctly when
|
|
297
|
+
opened anywhere. Four ways to share:
|
|
297
298
|
|
|
299
|
+
- **Pipe it anywhere** — `cesium export <id-prefix>` dumps the file to stdout;
|
|
300
|
+
`cesium export <id-prefix> --out plan.html` writes it to disk. The output
|
|
301
|
+
is a portable HTML file you can attach to email, drop in a chat, or commit
|
|
302
|
+
to a repo. Opens correctly with no server running.
|
|
298
303
|
- **Same machine** — copy or attach the `file://` path printed in the terminal.
|
|
299
304
|
- **Over SSH** — forward the port with `ssh -L 3030:localhost:3030 your-host`,
|
|
300
305
|
then send the `http://localhost:3030/...` URL.
|
|
301
306
|
- **On a trusted LAN** — set `"hostname": "0.0.0.0"` in `cesium.json` and share
|
|
302
307
|
the LAN URL. Only do this on networks you trust.
|
|
303
308
|
|
|
309
|
+
When an artifact is served by the cesium HTTP server, a `<link>` to the live
|
|
310
|
+
`theme.css` overrides the baked-in `<style>` so theme changes apply retroactively
|
|
311
|
+
to served pages. Standalone copies keep their generation-time look forever.
|
|
312
|
+
|
|
304
313
|
### Clean up old artifacts
|
|
305
314
|
|
|
306
315
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfbender/cesium",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// cesium export — emit an artifact's HTML to stdout (or a file).
|
|
2
|
+
//
|
|
3
|
+
// Because cesium artifacts are written with the full theme CSS baked into a
|
|
4
|
+
// <style> tag at generation time, an artifact file on disk is already a
|
|
5
|
+
// fully self-contained HTML document. Export is therefore a thin wrapper:
|
|
6
|
+
// resolve by id-prefix, read the file, write to stdout or --out.
|
|
7
|
+
//
|
|
8
|
+
// (When served by the cesium HTTP server, the <link rel="stylesheet"
|
|
9
|
+
// href=".../theme.css"> in the same artifact still loads and overrides the
|
|
10
|
+
// baked CSS in cascade order, so live theme changes apply server-side. The
|
|
11
|
+
// baked CSS only acts as the standalone fallback.)
|
|
12
|
+
|
|
13
|
+
import { defineCommand } from "citty";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { loadConfig, type CesiumConfig } from "../../config.ts";
|
|
17
|
+
import { loadIndex } from "../../storage/index-cache.ts";
|
|
18
|
+
import { pathsFor } from "../../storage/paths.ts";
|
|
19
|
+
import { atomicWrite } from "../../storage/write.ts";
|
|
20
|
+
|
|
21
|
+
export interface ExportArgs {
|
|
22
|
+
idPrefix: string;
|
|
23
|
+
out: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ExportContext {
|
|
27
|
+
stdout: { write: (s: string) => void };
|
|
28
|
+
stderr: { write: (s: string) => void };
|
|
29
|
+
loadConfig?: () => CesiumConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function defaultCtx(): ExportContext {
|
|
33
|
+
return {
|
|
34
|
+
stdout: process.stdout,
|
|
35
|
+
stderr: process.stderr,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runExport(
|
|
40
|
+
args: ExportArgs,
|
|
41
|
+
ctxOverride?: Partial<ExportContext>,
|
|
42
|
+
): Promise<number> {
|
|
43
|
+
const ctx: ExportContext = { ...defaultCtx(), ...ctxOverride };
|
|
44
|
+
|
|
45
|
+
if (args.idPrefix.length === 0) {
|
|
46
|
+
ctx.stderr.write(`cesium export: missing required argument <id-prefix>\n`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const prefixLower = args.idPrefix.toLowerCase();
|
|
51
|
+
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
52
|
+
|
|
53
|
+
// Resolve artifact via global index (same matching as `open`)
|
|
54
|
+
const globalJsonPath = join(cfg.stateDir, "index.json");
|
|
55
|
+
let allEntries;
|
|
56
|
+
try {
|
|
57
|
+
allEntries = await loadIndex(globalJsonPath);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const e = err as Error;
|
|
60
|
+
ctx.stderr.write(`cesium export: failed to read index: ${e.message}\n`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const matches = allEntries.filter((e) => e.id.toLowerCase().startsWith(prefixLower));
|
|
65
|
+
|
|
66
|
+
if (matches.length === 0) {
|
|
67
|
+
ctx.stderr.write(`cesium export: no artifact found with id prefix "${args.idPrefix}"\n`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (matches.length > 1) {
|
|
72
|
+
ctx.stderr.write(
|
|
73
|
+
`cesium export: ambiguous prefix "${args.idPrefix}" — ${matches.length} matches:\n`,
|
|
74
|
+
);
|
|
75
|
+
for (const m of matches) {
|
|
76
|
+
ctx.stderr.write(` ${m.id} ${m.title} (${m.kind})\n`);
|
|
77
|
+
}
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const entry = matches[0];
|
|
82
|
+
if (entry === undefined) {
|
|
83
|
+
// Unreachable; satisfies type checker
|
|
84
|
+
ctx.stderr.write(`cesium export: internal error — no match\n`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const paths = pathsFor({
|
|
89
|
+
stateDir: cfg.stateDir,
|
|
90
|
+
projectSlug: entry.projectSlug,
|
|
91
|
+
filename: entry.filename,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let html: string;
|
|
95
|
+
try {
|
|
96
|
+
html = await readFile(paths.artifactPath, "utf8");
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const e = err as Error;
|
|
99
|
+
ctx.stderr.write(`cesium export: failed to read artifact: ${e.message}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (args.out !== null) {
|
|
104
|
+
try {
|
|
105
|
+
await atomicWrite(args.out, html);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const e = err as Error;
|
|
108
|
+
ctx.stderr.write(`cesium export: failed to write ${args.out}: ${e.message}\n`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
ctx.stderr.write(`Wrote ${args.out}\n`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ctx.stdout.write(html);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const exportCmd = defineCommand({
|
|
120
|
+
meta: {
|
|
121
|
+
name: "export",
|
|
122
|
+
description: "Emit an artifact's self-contained HTML to stdout (or --out file).",
|
|
123
|
+
},
|
|
124
|
+
args: {
|
|
125
|
+
idPrefix: {
|
|
126
|
+
type: "positional",
|
|
127
|
+
description: "Artifact id prefix (any unique substring of the id)",
|
|
128
|
+
required: true,
|
|
129
|
+
},
|
|
130
|
+
out: {
|
|
131
|
+
type: "string",
|
|
132
|
+
alias: "o",
|
|
133
|
+
description: "Write to this file path instead of stdout",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
async run({ args }) {
|
|
137
|
+
const code = await runExport({
|
|
138
|
+
idPrefix: args.idPrefix,
|
|
139
|
+
out: args.out ?? null,
|
|
140
|
+
});
|
|
141
|
+
if (code !== 0) process.exit(code);
|
|
142
|
+
},
|
|
143
|
+
});
|
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
|
+
});
|