@cfbender/cesium 0.3.6 → 0.5.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 +53 -0
- package/README.md +28 -14
- package/assets/styleguide.html +149 -0
- package/package.json +1 -1
- package/src/cli/commands/serve.ts +3 -0
- package/src/index.ts +4 -1
- package/src/prompt/field-reference.ts +94 -0
- package/src/prompt/system-fragment.md +56 -65
- package/src/render/blocks/catalog.ts +39 -0
- package/src/render/blocks/escape.ts +27 -0
- package/src/render/blocks/index.ts +6 -0
- package/src/render/blocks/markdown.ts +217 -0
- package/src/render/blocks/render.ts +96 -0
- package/src/render/blocks/renderers/callout.ts +38 -0
- package/src/render/blocks/renderers/code.ts +44 -0
- package/src/render/blocks/renderers/compare-table.ts +56 -0
- package/src/render/blocks/renderers/diagram.ts +48 -0
- package/src/render/blocks/renderers/divider.ts +31 -0
- package/src/render/blocks/renderers/hero.ts +66 -0
- package/src/render/blocks/renderers/kv.ts +45 -0
- package/src/render/blocks/renderers/list.ts +51 -0
- package/src/render/blocks/renderers/pill-row.ts +45 -0
- package/src/render/blocks/renderers/prose.ts +29 -0
- package/src/render/blocks/renderers/raw-html.ts +32 -0
- package/src/render/blocks/renderers/risk-table.ts +76 -0
- package/src/render/blocks/renderers/section.ts +95 -0
- package/src/render/blocks/renderers/timeline.ts +58 -0
- package/src/render/blocks/renderers/tldr.ts +30 -0
- package/src/render/blocks/types.ts +127 -0
- package/src/render/blocks/validate-block.ts +202 -0
- package/src/render/critique.ts +410 -10
- package/src/render/fallback.ts +18 -0
- package/src/render/theme.ts +235 -0
- package/src/render/validate.ts +282 -17
- package/src/render/wrap.ts +7 -7
- package/src/server/lifecycle.ts +7 -1
- package/src/storage/assets.ts +66 -0
- package/src/storage/index-cache.ts +1 -0
- package/src/storage/index-gen.ts +13 -14
- package/src/tools/ask.ts +5 -3
- package/src/tools/critique.ts +41 -6
- package/src/tools/publish.ts +39 -12
- package/src/tools/styleguide.ts +109 -9
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Materializes /theme.css in the state directory, atomically and idempotently.
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
frameworkRulesCss,
|
|
7
|
+
themeTokensCss,
|
|
8
|
+
defaultTheme,
|
|
9
|
+
type ThemeTokens,
|
|
10
|
+
} from "../render/theme.ts";
|
|
11
|
+
import { atomicWrite } from "./write.ts";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
|
|
14
|
+
/** Per-theme CSS cache: built CSS string keyed by theme content hash. */
|
|
15
|
+
const cssCache = new Map<string, string>();
|
|
16
|
+
|
|
17
|
+
/** Returns a stable cache key for a theme (hash of its JSON representation). */
|
|
18
|
+
function themeKey(theme: ThemeTokens): string {
|
|
19
|
+
return createHash("sha256").update(JSON.stringify(theme)).digest("hex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Build the full theme.css string for a given theme (tokens + framework rules). */
|
|
23
|
+
function buildCss(theme: ThemeTokens): string {
|
|
24
|
+
const key = themeKey(theme);
|
|
25
|
+
const cached = cssCache.get(key);
|
|
26
|
+
if (cached !== undefined) return cached;
|
|
27
|
+
const css = themeTokensCss(theme) + "\n" + frameworkRulesCss();
|
|
28
|
+
cssCache.set(key, css);
|
|
29
|
+
return css;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Returns the absolute path to theme.css in stateDir. */
|
|
33
|
+
export function themeCssAssetPath(stateDir: string): string {
|
|
34
|
+
return join(stateDir, "theme.css");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Writes <stateDir>/theme.css with the full framework CSS (tokens + rules)
|
|
39
|
+
* for the given theme, iff the on-disk file is missing or its content hash
|
|
40
|
+
* differs from the expected content. Idempotent and self-healing on plugin
|
|
41
|
+
* upgrade or theme change.
|
|
42
|
+
*
|
|
43
|
+
* When called without a theme argument, falls back to defaultTheme() so
|
|
44
|
+
* existing call sites remain valid.
|
|
45
|
+
*/
|
|
46
|
+
export async function ensureThemeCss(
|
|
47
|
+
stateDir: string,
|
|
48
|
+
theme: ThemeTokens = defaultTheme(),
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const dest = themeCssAssetPath(stateDir);
|
|
51
|
+
const bundledCss = buildCss(theme);
|
|
52
|
+
const bundledHash = createHash("sha256").update(bundledCss).digest("hex");
|
|
53
|
+
|
|
54
|
+
// Fast path: compare hash of existing file to expected hash.
|
|
55
|
+
try {
|
|
56
|
+
const existing = await readFile(dest, "utf8");
|
|
57
|
+
const existingHash = createHash("sha256").update(existing).digest("hex");
|
|
58
|
+
if (existingHash === bundledHash) {
|
|
59
|
+
return; // already up-to-date
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// ENOENT or unreadable — fall through to write
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await atomicWrite(dest, bundledCss);
|
|
66
|
+
}
|
package/src/storage/index-gen.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import type { IndexEntry } from "./index-cache.ts";
|
|
4
4
|
import type { ThemeTokens } from "../render/theme.ts";
|
|
5
|
-
import { frameworkRulesCss, themeTokensCss } from "../render/theme.ts";
|
|
6
5
|
import { faviconLinkTag, faviconEmblemSvg } from "../render/favicon.ts";
|
|
6
|
+
import { fallbackCss } from "../render/fallback.ts";
|
|
7
7
|
|
|
8
8
|
export interface RenderProjectIndexArgs {
|
|
9
9
|
projectSlug: string;
|
|
@@ -236,6 +236,9 @@ function indexJs(): string {
|
|
|
236
236
|
function renderEntryCard(entry: IndexEntry): string {
|
|
237
237
|
const isSuperseded = entry.supersededBy !== null ? "1" : "0";
|
|
238
238
|
const kindPill = `<span class="pill">${esc(entry.kind)}</span>`;
|
|
239
|
+
const inputModeBadge = entry.inputMode !== undefined
|
|
240
|
+
? ` <span class="tag">${esc(entry.inputMode)}</span>`
|
|
241
|
+
: "";
|
|
239
242
|
const dateStr = `<span class="card-date">${esc(formatDate(entry.createdAt))}</span>`;
|
|
240
243
|
const supersededBadge =
|
|
241
244
|
entry.supersedes !== null
|
|
@@ -255,7 +258,7 @@ function renderEntryCard(entry: IndexEntry): string {
|
|
|
255
258
|
: "";
|
|
256
259
|
|
|
257
260
|
return `<div class="entry-card" data-card data-kind="${esc(entry.kind)}" data-title-lower="${esc(entry.title.toLowerCase())}" data-body-text="${esc(entry.bodyText.toLowerCase())}" data-superseded="${isSuperseded}">
|
|
258
|
-
<div class="card-top">${kindPill}${supersededBadge}${supersededByBadge}${dateStr}</div>
|
|
261
|
+
<div class="card-top">${kindPill}${inputModeBadge}${supersededBadge}${supersededByBadge}${dateStr}</div>
|
|
259
262
|
<div class="card-title"><a href="artifacts/${esc(entry.filename)}">${esc(entry.title)}</a></div>
|
|
260
263
|
${summaryHtml}${tagsHtml}
|
|
261
264
|
<div class="card-footer"><a class="open-link" href="artifacts/${esc(entry.filename)}">Open →</a></div>
|
|
@@ -265,7 +268,7 @@ function renderEntryCard(entry: IndexEntry): string {
|
|
|
265
268
|
// ─── renderProjectIndex ──────────────────────────────────────────────────────
|
|
266
269
|
|
|
267
270
|
export function renderProjectIndex(args: RenderProjectIndexArgs): string {
|
|
268
|
-
const { projectSlug, projectName, entries
|
|
271
|
+
const { projectSlug, projectName, entries } = args;
|
|
269
272
|
const href =
|
|
270
273
|
args.themeCssHref === undefined
|
|
271
274
|
? "../../theme.css"
|
|
@@ -274,8 +277,7 @@ export function renderProjectIndex(args: RenderProjectIndexArgs): string {
|
|
|
274
277
|
: args.themeCssHref;
|
|
275
278
|
const suppressLink = args.themeCssHref === null;
|
|
276
279
|
|
|
277
|
-
const
|
|
278
|
-
const tokens = themeTokensCss(theme);
|
|
280
|
+
const fallback = fallbackCss();
|
|
279
281
|
const iCss = indexCss();
|
|
280
282
|
const iJs = indexJs();
|
|
281
283
|
|
|
@@ -358,9 +360,8 @@ ${cardsHtml}
|
|
|
358
360
|
<meta charset="utf-8">
|
|
359
361
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
360
362
|
<title>${esc(projectName)} · cesium</title>
|
|
361
|
-
<style
|
|
362
|
-
|
|
363
|
-
${tokens}${iCss}</style>${linkTag}${faviconTag}
|
|
363
|
+
<style>/* fallback — standalone-readable; full styles served from /theme.css */
|
|
364
|
+
${fallback}${iCss}</style>${linkTag}${faviconTag}
|
|
364
365
|
</head>
|
|
365
366
|
<body>
|
|
366
367
|
<div class="page">
|
|
@@ -381,7 +382,7 @@ ${tokens}${iCss}</style>${linkTag}${faviconTag}
|
|
|
381
382
|
// ─── renderGlobalIndex ───────────────────────────────────────────────────────
|
|
382
383
|
|
|
383
384
|
export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
|
|
384
|
-
const { projects
|
|
385
|
+
const { projects } = args;
|
|
385
386
|
const href =
|
|
386
387
|
args.themeCssHref === undefined
|
|
387
388
|
? "theme.css"
|
|
@@ -390,8 +391,7 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
|
|
|
390
391
|
: args.themeCssHref;
|
|
391
392
|
const suppressLink = args.themeCssHref === null;
|
|
392
393
|
|
|
393
|
-
const
|
|
394
|
-
const tokens = themeTokensCss(theme);
|
|
394
|
+
const fallback = fallbackCss();
|
|
395
395
|
const iCss = indexCss();
|
|
396
396
|
|
|
397
397
|
const linkTag = suppressLink ? "" : `\n <link rel="stylesheet" href="${href}">`;
|
|
@@ -448,9 +448,8 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
|
|
|
448
448
|
<meta charset="utf-8">
|
|
449
449
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
450
450
|
<title>All projects · cesium</title>
|
|
451
|
-
<style
|
|
452
|
-
|
|
453
|
-
${tokens}${iCss}</style>${linkTag}${faviconTag}
|
|
451
|
+
<style>/* fallback — standalone-readable; full styles served from /theme.css */
|
|
452
|
+
${fallback}${iCss}</style>${linkTag}${faviconTag}
|
|
454
453
|
</head>
|
|
455
454
|
<body>
|
|
456
455
|
<div class="page">
|
package/src/tools/ask.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { wrapDocument, type ArtifactMeta } from "../render/wrap.ts";
|
|
|
13
13
|
import type { InteractiveData } from "../render/validate.ts";
|
|
14
14
|
import { deriveProjectIdentity, artifactFilename, pathsFor } from "../storage/paths.ts";
|
|
15
15
|
import { atomicWrite } from "../storage/write.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { ensureThemeCss } from "../storage/assets.ts";
|
|
17
17
|
import { writeFaviconSvg } from "../storage/favicon-write.ts";
|
|
18
18
|
import { loadIndex, writeIndex, appendEntry, type IndexEntry } from "../storage/index-cache.ts";
|
|
19
19
|
import { withLock } from "../storage/lock.ts";
|
|
@@ -204,6 +204,7 @@ export function createAskTool(
|
|
|
204
204
|
supersedes: null,
|
|
205
205
|
supersededBy: null,
|
|
206
206
|
contentSha256,
|
|
207
|
+
inputMode: "html",
|
|
207
208
|
};
|
|
208
209
|
|
|
209
210
|
// 12. Build interactive data
|
|
@@ -228,8 +229,8 @@ export function createAskTool(
|
|
|
228
229
|
// 14. Build theme + wrap document
|
|
229
230
|
const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
|
|
230
231
|
|
|
231
|
-
// 14a.
|
|
232
|
-
await
|
|
232
|
+
// 14a. Ensure theme.css + favicon.svg (idempotent, outside index lock — separate files)
|
|
233
|
+
await ensureThemeCss(config.stateDir, theme);
|
|
233
234
|
await writeFaviconSvg(config.stateDir);
|
|
234
235
|
|
|
235
236
|
const fullHtml = wrapDocument({
|
|
@@ -309,6 +310,7 @@ export function createAskTool(
|
|
|
309
310
|
portMax: config.portMax,
|
|
310
311
|
idleTimeoutMs: config.idleTimeoutMs,
|
|
311
312
|
hostname: config.hostname,
|
|
313
|
+
theme,
|
|
312
314
|
});
|
|
313
315
|
if (maybeInfo !== null) {
|
|
314
316
|
serverInfo = maybeInfo;
|
package/src/tools/critique.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
// Tool handler for cesium_critique —
|
|
1
|
+
// Tool handler for cesium_critique — mode-aware body analyzer.
|
|
2
|
+
// Accepts either { html: string } (html mode) or { blocks: Block[] } (blocks mode). Exactly one required.
|
|
2
3
|
|
|
3
4
|
import { tool } from "@opencode-ai/plugin";
|
|
4
5
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
critiqueHtml,
|
|
8
|
+
critiqueBlocks,
|
|
9
|
+
type CritiqueResult,
|
|
10
|
+
type CritiqueSeverity,
|
|
11
|
+
} from "../render/critique.ts";
|
|
12
|
+
import type { Block } from "../render/blocks/types.ts";
|
|
6
13
|
|
|
7
14
|
const TOOL_DESCRIPTION = `Analyze a draft HTML body for adherence to the cesium design
|
|
8
15
|
system before publishing. Returns a 0-100 score and findings (warn/suggest/info).
|
|
@@ -18,6 +25,7 @@ HTML only, no <!doctype>/<html>/<head>/<body> wrappers.`;
|
|
|
18
25
|
* Format a CritiqueResult into a concise human-readable string the agent can parse.
|
|
19
26
|
* Format:
|
|
20
27
|
* score: 87/100
|
|
28
|
+
* mode: html
|
|
21
29
|
*
|
|
22
30
|
* warn:
|
|
23
31
|
* - [external-resource] External resource will be stripped...
|
|
@@ -29,7 +37,7 @@ HTML only, no <!doctype>/<html>/<head>/<body> wrappers.`;
|
|
|
29
37
|
* - [code-without-highlights] Code blocks render without...
|
|
30
38
|
*/
|
|
31
39
|
export function formatCritiqueForAgent(result: CritiqueResult): string {
|
|
32
|
-
const lines: string[] = [`score: ${result.score}/100`];
|
|
40
|
+
const lines: string[] = [`score: ${result.score}/100`, `mode: ${result.mode}`];
|
|
33
41
|
|
|
34
42
|
const bySeverity: Record<CritiqueSeverity, typeof result.findings> = {
|
|
35
43
|
warn: [],
|
|
@@ -47,7 +55,8 @@ export function formatCritiqueForAgent(result: CritiqueResult): string {
|
|
|
47
55
|
lines.push("");
|
|
48
56
|
lines.push(`${sev}:`);
|
|
49
57
|
for (const f of group) {
|
|
50
|
-
|
|
58
|
+
const pathSuffix = f.path !== undefined ? ` (${f.path})` : "";
|
|
59
|
+
lines.push(`- [${f.code}] ${f.message}${pathSuffix}`);
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
|
|
@@ -57,9 +66,35 @@ export function formatCritiqueForAgent(result: CritiqueResult): string {
|
|
|
57
66
|
export function createCritiqueTool(_ctx: PluginInput): ReturnType<typeof tool> {
|
|
58
67
|
return tool({
|
|
59
68
|
description: TOOL_DESCRIPTION,
|
|
60
|
-
args: {
|
|
69
|
+
args: {
|
|
70
|
+
html: tool.schema.string().optional(),
|
|
71
|
+
blocks: tool.schema.any().optional(),
|
|
72
|
+
},
|
|
61
73
|
async execute(args) {
|
|
62
|
-
const
|
|
74
|
+
const hasHtml = args.html !== undefined && args.html !== null;
|
|
75
|
+
const hasBlocks = args.blocks !== undefined && args.blocks !== null;
|
|
76
|
+
|
|
77
|
+
if (hasHtml && hasBlocks) {
|
|
78
|
+
return "error: provide exactly one of html or blocks, not both";
|
|
79
|
+
}
|
|
80
|
+
if (!hasHtml && !hasBlocks) {
|
|
81
|
+
return "error: provide exactly one of html or blocks";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let result: CritiqueResult;
|
|
85
|
+
|
|
86
|
+
if (hasHtml) {
|
|
87
|
+
if (typeof args.html !== "string") {
|
|
88
|
+
return "error: html must be a string";
|
|
89
|
+
}
|
|
90
|
+
result = critiqueHtml(args.html);
|
|
91
|
+
} else {
|
|
92
|
+
if (!Array.isArray(args.blocks)) {
|
|
93
|
+
return "error: blocks must be an array";
|
|
94
|
+
}
|
|
95
|
+
result = critiqueBlocks(args.blocks as Block[]);
|
|
96
|
+
}
|
|
97
|
+
|
|
63
98
|
return formatCritiqueForAgent(result);
|
|
64
99
|
},
|
|
65
100
|
});
|
package/src/tools/publish.ts
CHANGED
|
@@ -10,10 +10,11 @@ import { scrub } from "../render/scrub.ts";
|
|
|
10
10
|
import { extractTextContent } from "../render/extract.ts";
|
|
11
11
|
import { themeFromPreset, mergeTheme } from "../render/theme.ts";
|
|
12
12
|
import { validatePublishInput, htmlBodyWarnings, PUBLISH_KINDS } from "../render/validate.ts";
|
|
13
|
+
import { renderBlocks } from "../render/blocks/render.ts";
|
|
13
14
|
import { wrapDocument, type ArtifactMeta } from "../render/wrap.ts";
|
|
14
15
|
import { deriveProjectIdentity, artifactFilename, pathsFor } from "../storage/paths.ts";
|
|
15
16
|
import { atomicWrite, patchEmbeddedMetadata } from "../storage/write.ts";
|
|
16
|
-
import {
|
|
17
|
+
import { ensureThemeCss } from "../storage/assets.ts";
|
|
17
18
|
import { writeFaviconSvg } from "../storage/favicon-write.ts";
|
|
18
19
|
import {
|
|
19
20
|
loadIndex,
|
|
@@ -112,7 +113,18 @@ export function createPublishTool(
|
|
|
112
113
|
args: {
|
|
113
114
|
title: tool.schema.string(),
|
|
114
115
|
kind: tool.schema.enum([...PUBLISH_KINDS] as [string, ...string[]]),
|
|
115
|
-
html: tool.schema
|
|
116
|
+
html: tool.schema
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe(
|
|
120
|
+
"Body HTML — escape valve / legacy mode. Provide exactly one of html or blocks.",
|
|
121
|
+
),
|
|
122
|
+
blocks: tool.schema
|
|
123
|
+
.array(tool.schema.any())
|
|
124
|
+
.optional()
|
|
125
|
+
.describe(
|
|
126
|
+
"Structured block array — preferred for token efficiency. Provide exactly one of html or blocks.",
|
|
127
|
+
),
|
|
116
128
|
summary: tool.schema.string().optional(),
|
|
117
129
|
tags: tool.schema.array(tool.schema.string()).optional(),
|
|
118
130
|
supersedes: tool.schema.string().optional(),
|
|
@@ -174,11 +186,23 @@ export function createPublishTool(
|
|
|
174
186
|
// 6. Timestamps
|
|
175
187
|
const createdAt = now();
|
|
176
188
|
|
|
177
|
-
// 7.
|
|
178
|
-
|
|
189
|
+
// 7. Render body (blocks path or html path)
|
|
190
|
+
let bodyHtml: string;
|
|
191
|
+
let scrubRemovedCount = 0;
|
|
192
|
+
const inputMode: "html" | "blocks" = input.blocks !== undefined ? "blocks" : "html";
|
|
193
|
+
|
|
194
|
+
if (input.blocks !== undefined) {
|
|
195
|
+
// Blocks path: render structured blocks → trusted HTML
|
|
196
|
+
bodyHtml = renderBlocks(input.blocks);
|
|
197
|
+
} else {
|
|
198
|
+
// HTML path: scrub agent-supplied HTML
|
|
199
|
+
const scrubbed = scrub(input.html);
|
|
200
|
+
bodyHtml = scrubbed.html;
|
|
201
|
+
scrubRemovedCount = scrubbed.removed.length;
|
|
202
|
+
}
|
|
179
203
|
|
|
180
204
|
// 7a. Extract body text for full-text search
|
|
181
|
-
const bodyText = extractTextContent(
|
|
205
|
+
const bodyText = extractTextContent(bodyHtml);
|
|
182
206
|
|
|
183
207
|
// 8. Compute filename + paths
|
|
184
208
|
const filename = artifactFilename({ title: input.title, id, createdAt });
|
|
@@ -189,7 +213,7 @@ export function createPublishTool(
|
|
|
189
213
|
});
|
|
190
214
|
|
|
191
215
|
// 9. Content SHA-256
|
|
192
|
-
const contentSha256 = createHash("sha256").update(
|
|
216
|
+
const contentSha256 = createHash("sha256").update(bodyHtml).digest("hex");
|
|
193
217
|
|
|
194
218
|
// 10. Build ArtifactMeta
|
|
195
219
|
const meta: ArtifactMeta = {
|
|
@@ -210,14 +234,15 @@ export function createPublishTool(
|
|
|
210
234
|
supersedes: input.supersedes ?? null,
|
|
211
235
|
supersededBy: null,
|
|
212
236
|
contentSha256,
|
|
237
|
+
inputMode,
|
|
213
238
|
};
|
|
214
239
|
|
|
215
240
|
// 11. Build warnings
|
|
216
241
|
const warnings: string[] = [];
|
|
217
|
-
if (
|
|
218
|
-
warnings.push(`Removed ${
|
|
242
|
+
if (scrubRemovedCount > 0) {
|
|
243
|
+
warnings.push(`Removed ${scrubRemovedCount} external resource(s) during scrub.`);
|
|
219
244
|
}
|
|
220
|
-
const bodyWarnings = htmlBodyWarnings(
|
|
245
|
+
const bodyWarnings = input.html !== undefined ? htmlBodyWarnings(bodyHtml) : [];
|
|
221
246
|
for (const w of bodyWarnings) {
|
|
222
247
|
warnings.push(w);
|
|
223
248
|
}
|
|
@@ -225,12 +250,12 @@ export function createPublishTool(
|
|
|
225
250
|
// 12. Build theme + wrap document
|
|
226
251
|
const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
|
|
227
252
|
|
|
228
|
-
// 12a.
|
|
229
|
-
await
|
|
253
|
+
// 12a. Ensure theme.css + favicon.svg (idempotent, outside index lock — separate files)
|
|
254
|
+
await ensureThemeCss(config.stateDir, theme);
|
|
230
255
|
await writeFaviconSvg(config.stateDir);
|
|
231
256
|
|
|
232
257
|
const fullHtml = wrapDocument({
|
|
233
|
-
body:
|
|
258
|
+
body: bodyHtml,
|
|
234
259
|
meta,
|
|
235
260
|
theme,
|
|
236
261
|
warnings,
|
|
@@ -257,6 +282,7 @@ export function createPublishTool(
|
|
|
257
282
|
projectSlug: identity.slug,
|
|
258
283
|
projectName: identity.name,
|
|
259
284
|
bodyText,
|
|
285
|
+
inputMode,
|
|
260
286
|
};
|
|
261
287
|
|
|
262
288
|
const lockPath = join(config.stateDir, ".index.lock");
|
|
@@ -336,6 +362,7 @@ export function createPublishTool(
|
|
|
336
362
|
portMax: config.portMax,
|
|
337
363
|
idleTimeoutMs: config.idleTimeoutMs,
|
|
338
364
|
hostname: config.hostname,
|
|
365
|
+
theme,
|
|
339
366
|
});
|
|
340
367
|
if (maybeInfo !== null) {
|
|
341
368
|
serverInfo = maybeInfo;
|
package/src/tools/styleguide.ts
CHANGED
|
@@ -1,15 +1,115 @@
|
|
|
1
|
-
// Tool handler for cesium_styleguide — returns
|
|
1
|
+
// Tool handler for cesium_styleguide — returns a markdown reference generated from the block catalog.
|
|
2
2
|
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
3
|
import { tool } from "@opencode-ai/plugin";
|
|
7
4
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
5
|
+
import { blockCatalog, blockTypes } from "../render/blocks/catalog.ts";
|
|
6
|
+
import { renderBlock } from "../render/blocks/render.ts";
|
|
7
|
+
import type { RenderCtx, SectionCounter } from "../render/blocks/render.ts";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
9
|
+
function makeCtx(): RenderCtx {
|
|
10
|
+
const counter: SectionCounter = { value: 1 };
|
|
11
|
+
return { sectionCounter: counter, depth: 0, path: "blocks[0]" };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Escape a string for safe insertion inside a markdown fenced code block. */
|
|
15
|
+
function escapeForCodeFence(s: string): string {
|
|
16
|
+
// Prevent accidental fence closing — replace ``` with ` `` ` (rare in practice)
|
|
17
|
+
return s.replace(/```/g, "` `` `");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Generate the full markdown reference from the catalog. Deterministic — same catalog → same output. */
|
|
21
|
+
export function generateStyleguideMarkdown(): string {
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
|
|
24
|
+
lines.push("# Cesium publishing reference");
|
|
25
|
+
lines.push("");
|
|
26
|
+
lines.push("## Two input modes");
|
|
27
|
+
lines.push("");
|
|
28
|
+
lines.push(
|
|
29
|
+
"`cesium_publish` accepts either `blocks: Block[]` (preferred) or `html: string`" +
|
|
30
|
+
" (escape valve). Provide exactly one.",
|
|
31
|
+
);
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push(
|
|
34
|
+
"Prefer `blocks` for plans, reviews, reports, explainers, comparisons, audits, design docs." +
|
|
35
|
+
" Use `html` only for whole-document bespoke layouts (custom hero, non-standard grid," +
|
|
36
|
+
" experimental visual essay). For isolated bespoke regions, stay in `blocks` and use" +
|
|
37
|
+
" `raw_html` or `diagram`.",
|
|
38
|
+
);
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push("## Block reference");
|
|
41
|
+
lines.push("");
|
|
42
|
+
|
|
43
|
+
for (const blockType of blockTypes) {
|
|
44
|
+
const entry = blockCatalog[blockType];
|
|
45
|
+
|
|
46
|
+
lines.push(`### \`${entry.type}\``);
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push(entry.description);
|
|
49
|
+
lines.push("");
|
|
50
|
+
|
|
51
|
+
// Schema as JSON
|
|
52
|
+
lines.push("```json");
|
|
53
|
+
lines.push(escapeForCodeFence(JSON.stringify(entry.schema, null, 2)));
|
|
54
|
+
lines.push("```");
|
|
55
|
+
lines.push("");
|
|
56
|
+
|
|
57
|
+
// Canonical example
|
|
58
|
+
lines.push("Example:");
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push("```json");
|
|
61
|
+
lines.push(escapeForCodeFence(JSON.stringify(entry.example, null, 2)));
|
|
62
|
+
lines.push("```");
|
|
63
|
+
lines.push("");
|
|
64
|
+
|
|
65
|
+
// Rendered HTML
|
|
66
|
+
const rendered = entry.renderedExample ?? (() => {
|
|
67
|
+
try {
|
|
68
|
+
return renderBlock(entry.example, makeCtx());
|
|
69
|
+
} catch {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
if (rendered !== "") {
|
|
75
|
+
lines.push("Renders to:");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("```html");
|
|
78
|
+
lines.push(escapeForCodeFence(rendered));
|
|
79
|
+
lines.push("```");
|
|
80
|
+
lines.push("");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lines.push(
|
|
85
|
+
"## Markdown subset (inside `prose`, `tldr`, `callout.markdown`, list items, table cells)",
|
|
86
|
+
);
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push(
|
|
89
|
+
"- Block: paragraph, `-` lists, `1.` lists, `>` blockquote, `---` rule, hard break (two-space EOL).",
|
|
90
|
+
);
|
|
91
|
+
lines.push(
|
|
92
|
+
"- Inline: `**bold**`, `*italic*`, `` `code` ``, `[text](href)` (relative or anchor only).",
|
|
93
|
+
);
|
|
94
|
+
lines.push(
|
|
95
|
+
"- HTML safelist: `<kbd>`, `<span class=\"pill\">`, `<span class=\"tag\">`. Anything else is escaped.",
|
|
96
|
+
);
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push("## When to reach for raw_html / diagram");
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(
|
|
101
|
+
"- `diagram` — inline SVG visualizations or bespoke composed HTML diagrams.",
|
|
102
|
+
);
|
|
103
|
+
lines.push(
|
|
104
|
+
"- `raw_html` — anything genuinely creative that doesn't fit a known block type." +
|
|
105
|
+
" Include a `purpose` string describing what you're building.",
|
|
106
|
+
);
|
|
107
|
+
lines.push(
|
|
108
|
+
"- Critique flags raw_html overuse (>2 blocks or >30% of body characters).",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
13
113
|
|
|
14
114
|
export function createStyleguideTool(_ctx: PluginInput): ReturnType<typeof tool> {
|
|
15
115
|
return tool({
|
|
@@ -17,7 +117,7 @@ export function createStyleguideTool(_ctx: PluginInput): ReturnType<typeof tool>
|
|
|
17
117
|
"Returns the cesium HTML design system reference page (CSS classes with example usage). Call this once at the start of writing a complex artifact to internalize the available components.",
|
|
18
118
|
args: {},
|
|
19
119
|
async execute(_args, _context) {
|
|
20
|
-
return
|
|
120
|
+
return generateStyleguideMarkdown();
|
|
21
121
|
},
|
|
22
122
|
});
|
|
23
123
|
}
|