@cfbender/cesium 0.6.0 → 0.6.2

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 CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.6.2 — 2026-05-13
4
+
5
+ Prompt fix. The `diff` block was missing from the agent-facing quick reference
6
+ in `system-fragment.md` — only fifteen of the sixteen catalog blocks were
7
+ surfaced — so agents would reach for `code` blocks with hand-rolled `+`/`-`
8
+ lines when asked to walk through a diff or release. The `diff` block is now
9
+ listed alongside `code`/`timeline`, and a short trigger note pairs "release
10
+ walkthrough / version diff / before-after / refactor proposal" with the `diff`
11
+ block explicitly.
12
+
13
+ - **fix:** Add `diff` to the prompt's quick block reference.
14
+ - **fix:** Add "When showing code changes" trigger paragraph.
15
+ - **chore:** Bump the block-type enumeration tests from 15 → 16 to keep the
16
+ catalog and the human-curated reference in lockstep.
17
+
18
+ No runtime or rendering changes; the `diff` block itself was already wired
19
+ end-to-end since v0.4.
20
+
21
+ ## v0.6.1 — 2026-05-13
22
+
23
+ Fixes a regression introduced by the v0.4 blocks refactor: the README's
24
+ "self-contained HTML file" claim had quietly become false. Block-based
25
+ artifacts (cards, callouts, compare-tables, timelines, code) rendered as
26
+ broken-looking plain HTML when opened via `file://` because the inline
27
+ fallback CSS only covered basic typography — all the block styling lived
28
+ in the server-side `theme.css`. Sharing an artifact required either the
29
+ running cesium server or a recipient who happened to have it.
30
+
31
+ - **feat:** Artifacts now embed the full `theme.css` (~21KB) in an inline
32
+ `<style>` block at generation time. Opening any artifact via `file://`
33
+ or on any other machine now renders correctly with zero external
34
+ dependencies — typography, blocks, tables, controls, diff renderer, all
35
+ of it. The `<link rel="stylesheet" href=".../theme.css">` is preserved
36
+ so served artifacts still pick up live theme changes (the link rules
37
+ override the earlier inline `<style>` in cascade order).
38
+ - **feat:** New `cesium export <id-prefix>` command emits an artifact's
39
+ self-contained HTML to stdout, or to `--out file.html`. Resolves the id
40
+ prefix against the global index the same way `cesium open` does. With
41
+ the baked-in CSS, export is now a near-trivial file copy — pipe it
42
+ anywhere, attach it to email, drop it in a chat, commit it to a repo.
43
+ - **refactor:** Removed `src/render/fallback.ts` and the matching test
44
+ file. The 8-line fallback CSS it produced is no longer needed; the
45
+ inline `<style>` carries the full framework directly.
46
+ - **chore:** README "Share an artifact" section now leads with
47
+ `cesium export` and explains the bake-and-override model.
48
+
49
+ Old artifacts on disk are not retroactively re-baked — they keep their
50
+ generation-time fallback + link, which means they continue to render
51
+ fine when served by cesium but look plain when opened standalone.
52
+ Historical fidelity is preserved. New artifacts written from v0.6.1
53
+ onward are genuinely portable.
54
+
3
55
  ## v0.6.0 — 2026-05-13
4
56
 
5
57
  Internal cleanup release. Two hand-rolled server and CLI layers swapped
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 serve`, `cesium prune`,
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 — no external resources.
296
- Three ways to share:
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.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Beautiful self-contained HTML artifacts from your opencode agent.",
5
5
  "keywords": [
6
6
  "agent",
@@ -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/index.ts CHANGED
@@ -14,6 +14,7 @@ const main = defineCommand({
14
14
  subCommands: {
15
15
  ls: () => import("./commands/ls.ts").then((m) => m.lsCmd),
16
16
  open: () => import("./commands/open.ts").then((m) => m.openCmd),
17
+ export: () => import("./commands/export.ts").then((m) => m.exportCmd),
17
18
  serve: () => import("./commands/serve.ts").then((m) => m.serveCmd),
18
19
  stop: () => import("./commands/stop.ts").then((m) => m.stopCmd),
19
20
  restart: () => import("./commands/restart.ts").then((m) => m.restartCmd),
@@ -78,11 +78,16 @@ Call `cesium_styleguide` for full schemas and rendered examples.
78
78
  - `prose` — free-form markdown; `list` — bullet/numbered/checklist
79
79
  - `callout` — aside with variant: note/warn/risk; `divider` — rule
80
80
  - `code` — fenced code with lang; `timeline` — milestone list
81
+ - `diff` — side-by-side before/after code with bezier connectors
81
82
  - `compare_table` — comparison grid; `risk_table` — risk grid
82
83
  - `kv` — key-value pairs; `pill_row` — pill/tag chips
83
84
  - `diagram` — SVG/HTML visual (scrubbed)
84
85
  - `raw_html` — custom HTML escape hatch (scrubbed; add `purpose`)
85
86
 
87
+ ## When showing code changes
88
+
89
+ Showing what changed — release walkthroughs, version diffs, refactor proposals, before/after — use the `diff` block, not a `code` block with hand-rolled `+`/`-` lines. Pass either `patch` (unified diff) or both `before` and `after`. One `diff` block per file or hunk.
90
+
86
91
  {{BLOCK_FIELD_REFERENCE}}
87
92
 
88
93
  ## When to use raw_html / diagram
@@ -1,7 +1,7 @@
1
1
  // Assembles the full <!doctype html> document from a body fragment + metadata.
2
2
 
3
3
  import { type ThemeTokens } from "./theme.ts";
4
- import { fallbackCss } from "./fallback.ts";
4
+ import { buildThemeCss } from "../storage/theme-write.ts";
5
5
  import { renderControl, renderAnswered } from "./controls.ts";
6
6
  import { getClientJs } from "./client-js.ts";
7
7
  import { faviconLinkTag } from "./favicon.ts";
@@ -131,7 +131,7 @@ function renderFooter(meta: ArtifactMeta): string {
131
131
  }
132
132
 
133
133
  export function wrapDocument(opts: WrapOptions): string {
134
- const { body, meta, warnings = [], interactive } = opts;
134
+ const { body, meta, theme, warnings = [], interactive } = opts;
135
135
  // Default href: artifact context (three levels deep from stateDir)
136
136
  const href =
137
137
  opts.themeCssHref === undefined
@@ -142,7 +142,12 @@ export function wrapDocument(opts: WrapOptions): string {
142
142
  // Suppress <link> when null is explicitly passed
143
143
  const suppressLink = opts.themeCssHref === null;
144
144
 
145
- const fallback = fallbackCss();
145
+ // Bake the full theme CSS into every artifact so it's genuinely
146
+ // self-contained when opened standalone. When served by the cesium HTTP
147
+ // server, the <link> below still loads and overrides the inline rules in
148
+ // cascade order — so theme upgrades retroactively apply to served artifacts
149
+ // while standalone copies retain their generation-time look.
150
+ const themeCss = buildThemeCss(theme);
146
151
  // Embed interactive into the cesium-meta JSON block when present
147
152
  const metaPayload: Record<string, unknown> = { ...meta };
148
153
  if (interactive !== undefined) {
@@ -172,8 +177,7 @@ export function wrapDocument(opts: WrapOptions): string {
172
177
  <meta charset="utf-8">
173
178
  <meta name="viewport" content="width=device-width, initial-scale=1">
174
179
  <title>${titleEsc} · cesium</title>
175
- <style>/* fallback — standalone-readable; full styles served from /theme.css */
176
- ${fallback}</style>${linkTag}${faviconTag}
180
+ <style>${themeCss}</style>${linkTag}${faviconTag}
177
181
  <script type="application/json" id="cesium-meta">${metaJson}</script>
178
182
  </head>
179
183
  <body>
@@ -3,7 +3,7 @@
3
3
  import type { IndexEntry } from "./index-cache.ts";
4
4
  import type { ThemeTokens } from "../render/theme.ts";
5
5
  import { faviconLinkTag, faviconEmblemSvg } from "../render/favicon.ts";
6
- import { fallbackCss } from "../render/fallback.ts";
6
+ import { buildThemeCss } from "./theme-write.ts";
7
7
 
8
8
  export interface RenderProjectIndexArgs {
9
9
  projectSlug: string;
@@ -267,7 +267,7 @@ function renderEntryCard(entry: IndexEntry): string {
267
267
  // ─── renderProjectIndex ──────────────────────────────────────────────────────
268
268
 
269
269
  export function renderProjectIndex(args: RenderProjectIndexArgs): string {
270
- const { projectSlug, projectName, entries } = args;
270
+ const { projectSlug, projectName, entries, theme } = args;
271
271
  const href =
272
272
  args.themeCssHref === undefined
273
273
  ? "../../theme.css"
@@ -276,7 +276,9 @@ export function renderProjectIndex(args: RenderProjectIndexArgs): string {
276
276
  : args.themeCssHref;
277
277
  const suppressLink = args.themeCssHref === null;
278
278
 
279
- const fallback = fallbackCss();
279
+ // Bake the full theme CSS into the index so it's self-contained when opened
280
+ // standalone; the <link> below still loads and overrides when served.
281
+ const themeCss = buildThemeCss(theme);
280
282
  const iCss = indexCss();
281
283
  const iJs = indexJs();
282
284
 
@@ -359,8 +361,7 @@ ${cardsHtml}
359
361
  <meta charset="utf-8">
360
362
  <meta name="viewport" content="width=device-width, initial-scale=1">
361
363
  <title>${esc(projectName)} · cesium</title>
362
- <style>/* fallback — standalone-readable; full styles served from /theme.css */
363
- ${fallback}${iCss}</style>${linkTag}${faviconTag}
364
+ <style>${themeCss}${iCss}</style>${linkTag}${faviconTag}
364
365
  </head>
365
366
  <body>
366
367
  <div class="page">
@@ -381,7 +382,7 @@ ${fallback}${iCss}</style>${linkTag}${faviconTag}
381
382
  // ─── renderGlobalIndex ───────────────────────────────────────────────────────
382
383
 
383
384
  export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
384
- const { projects } = args;
385
+ const { projects, theme } = args;
385
386
  const href =
386
387
  args.themeCssHref === undefined
387
388
  ? "theme.css"
@@ -390,7 +391,9 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
390
391
  : args.themeCssHref;
391
392
  const suppressLink = args.themeCssHref === null;
392
393
 
393
- const fallback = fallbackCss();
394
+ // Bake the full theme CSS into the index so it's self-contained when opened
395
+ // standalone; the <link> below still loads and overrides when served.
396
+ const themeCss = buildThemeCss(theme);
394
397
  const iCss = indexCss();
395
398
 
396
399
  const linkTag = suppressLink ? "" : `\n <link rel="stylesheet" href="${href}">`;
@@ -447,8 +450,7 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
447
450
  <meta charset="utf-8">
448
451
  <meta name="viewport" content="width=device-width, initial-scale=1">
449
452
  <title>All projects · cesium</title>
450
- <style>/* fallback — standalone-readable; full styles served from /theme.css */
451
- ${fallback}${iCss}</style>${linkTag}${faviconTag}
453
+ <style>${themeCss}${iCss}</style>${linkTag}${faviconTag}
452
454
  </head>
453
455
  <body>
454
456
  <div class="page">
@@ -1,18 +0,0 @@
1
- // Minimal inline fallback CSS — ~8 lines, ≤500 bytes minified.
2
- // Goal: standalone .html files opened via file:// look "plain but readable,"
3
- // not broken. Full styling comes from the served /theme.css.
4
-
5
- /** Returns a compact CSS block covering typography, color tokens, and basic
6
- * component borders. Used as the inline <style> fallback in every artifact.
7
- * Budget: ≤500 bytes minified. */
8
- export function fallbackCss(): string {
9
- return (
10
- ":root{font-family:system-ui,sans-serif;line-height:1.6;}" +
11
- "body{max-width:900px;margin:0 auto;padding:24px;}" +
12
- "@media(prefers-color-scheme:dark){body{background:#180810;color:#ddd;}}" +
13
- "pre,code{font-family:ui-monospace,monospace;font-size:.875em;}" +
14
- ".card,.tldr{border:1.5px solid #ccc;border-radius:8px;padding:14px 18px;}" +
15
- ".callout{border:1.5px solid #ccc;border-radius:6px;padding:12px 16px;}" +
16
- "table{border-collapse:collapse;width:100%;}th,td{border:1px solid #ccc;padding:8px;}"
17
- );
18
- }