@cfbender/cesium 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/package.json +3 -1
- package/src/cli/commands/ls.ts +62 -65
- package/src/cli/commands/open.ts +47 -62
- package/src/cli/commands/prune.ts +59 -71
- package/src/cli/commands/restart.ts +100 -12
- package/src/cli/commands/serve.ts +118 -114
- package/src/cli/commands/stop.ts +51 -84
- package/src/cli/commands/theme.ts +54 -92
- package/src/cli/index.ts +17 -70
- package/src/render/theme.ts +18 -0
- package/src/server/api.ts +112 -124
- package/src/server/favicon.ts +8 -16
- package/src/server/http.ts +101 -106
- package/src/server/lifecycle.ts +7 -5
- package/src/storage/assets.ts +8 -10
- package/src/storage/theme-write.ts +17 -3
- package/src/tools/wait.ts +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cesium theme — show or apply the configured theme.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { loadConfig, type CesiumConfig } from "../../config.ts";
|
|
6
6
|
import {
|
|
@@ -9,9 +9,8 @@ import {
|
|
|
9
9
|
type ThemeTokens,
|
|
10
10
|
type ThemePalette,
|
|
11
11
|
} from "../../render/theme.ts";
|
|
12
|
-
import { writeThemeCss, themeCssPath } from "../../storage/theme-write.ts";
|
|
12
|
+
import { writeThemeCss, themeCssPath, buildThemeCss } from "../../storage/theme-write.ts";
|
|
13
13
|
import { writeFaviconSvg } from "../../storage/favicon-write.ts";
|
|
14
|
-
import { themeTokensCss } from "../../render/theme.ts";
|
|
15
14
|
import { atomicWrite } from "../../storage/write.ts";
|
|
16
15
|
import { readdir } from "node:fs/promises";
|
|
17
16
|
import { join } from "node:path";
|
|
@@ -75,7 +74,7 @@ function printTokenTable(
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
async function isWriteNeeded(cssPath: string, theme: ThemeTokens): Promise<boolean> {
|
|
78
|
-
const expected =
|
|
77
|
+
const expected = buildThemeCss(theme);
|
|
79
78
|
try {
|
|
80
79
|
const existing = await readFile(cssPath, "utf8");
|
|
81
80
|
return existing !== expected;
|
|
@@ -205,61 +204,12 @@ async function retrofitAll(
|
|
|
205
204
|
|
|
206
205
|
// ─── Command ──────────────────────────────────────────────────────────────────
|
|
207
206
|
|
|
208
|
-
export
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const subcommand = argv[0];
|
|
212
|
-
const rest = argv.slice(1);
|
|
213
|
-
|
|
214
|
-
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
215
|
-
resolved.stdout.write(
|
|
216
|
-
[
|
|
217
|
-
"Usage: cesium theme <subcommand> [options]",
|
|
218
|
-
"",
|
|
219
|
-
"Subcommands:",
|
|
220
|
-
" show Print resolved theme tokens",
|
|
221
|
-
" apply [--rewrite-artifacts] Write theme.css from current config",
|
|
222
|
-
"",
|
|
223
|
-
"Options:",
|
|
224
|
-
" --help, -h Show this help message",
|
|
225
|
-
"",
|
|
226
|
-
].join("\n"),
|
|
227
|
-
);
|
|
228
|
-
return subcommand ? 0 : 1;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (subcommand === "show") {
|
|
232
|
-
return themeShowCommand(rest, resolved);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (subcommand === "apply") {
|
|
236
|
-
return themeApplyCommand(rest, resolved);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
resolved.stderr.write(`cesium theme: unknown subcommand: ${subcommand}\n`);
|
|
240
|
-
return 1;
|
|
207
|
+
export interface ThemeApplyArgs {
|
|
208
|
+
rewriteArtifacts: boolean;
|
|
241
209
|
}
|
|
242
210
|
|
|
243
|
-
async function
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
const parsed = parseArgs({
|
|
247
|
-
args: argv,
|
|
248
|
-
options: { help: { type: "boolean", short: "h", default: false } },
|
|
249
|
-
allowPositionals: false,
|
|
250
|
-
strict: true,
|
|
251
|
-
});
|
|
252
|
-
values = parsed.values as typeof values;
|
|
253
|
-
} catch (err) {
|
|
254
|
-
const e = err as Error;
|
|
255
|
-
ctx.stderr.write(`cesium theme show: ${e.message}\n`);
|
|
256
|
-
return 1;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (values.help) {
|
|
260
|
-
ctx.stdout.write("Usage: cesium theme show\n\nPrint resolved theme tokens.\n\n");
|
|
261
|
-
return 0;
|
|
262
|
-
}
|
|
211
|
+
export async function runThemeShow(ctxOverride?: Partial<ThemeContext>): Promise<number> {
|
|
212
|
+
const ctx: ThemeContext = { ...defaultCtx(), ...ctxOverride };
|
|
263
213
|
|
|
264
214
|
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
265
215
|
const { theme, presetLabel } = resolveTheme(cfg);
|
|
@@ -270,47 +220,18 @@ async function themeShowCommand(argv: string[], ctx: ThemeContext): Promise<numb
|
|
|
270
220
|
return 0;
|
|
271
221
|
}
|
|
272
222
|
|
|
273
|
-
async function
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
options: {
|
|
279
|
-
"rewrite-artifacts": { type: "boolean", default: false },
|
|
280
|
-
help: { type: "boolean", short: "h", default: false },
|
|
281
|
-
},
|
|
282
|
-
allowPositionals: false,
|
|
283
|
-
strict: true,
|
|
284
|
-
});
|
|
285
|
-
values = parsed.values as typeof values;
|
|
286
|
-
} catch (err) {
|
|
287
|
-
const e = err as Error;
|
|
288
|
-
ctx.stderr.write(`cesium theme apply: ${e.message}\n`);
|
|
289
|
-
return 1;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (values.help) {
|
|
293
|
-
ctx.stdout.write(
|
|
294
|
-
[
|
|
295
|
-
"Usage: cesium theme apply [--rewrite-artifacts]",
|
|
296
|
-
"",
|
|
297
|
-
"Write theme.css from the current config.",
|
|
298
|
-
"",
|
|
299
|
-
"Options:",
|
|
300
|
-
" --rewrite-artifacts Retrofit existing artifacts and index pages with the theme link",
|
|
301
|
-
" --help, -h Show this help message",
|
|
302
|
-
"",
|
|
303
|
-
].join("\n"),
|
|
304
|
-
);
|
|
305
|
-
return 0;
|
|
306
|
-
}
|
|
223
|
+
export async function runThemeApply(
|
|
224
|
+
args: ThemeApplyArgs,
|
|
225
|
+
ctxOverride?: Partial<ThemeContext>,
|
|
226
|
+
): Promise<number> {
|
|
227
|
+
const ctx: ThemeContext = { ...defaultCtx(), ...ctxOverride };
|
|
307
228
|
|
|
308
229
|
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
309
230
|
const { theme, presetLabel } = resolveTheme(cfg);
|
|
310
231
|
const cssPath = await writeThemeCss(cfg.stateDir, theme);
|
|
311
232
|
await writeFaviconSvg(cfg.stateDir);
|
|
312
233
|
|
|
313
|
-
if (
|
|
234
|
+
if (args.rewriteArtifacts) {
|
|
314
235
|
const { artifacts, indexes } = await retrofitAll(cfg.stateDir, ctx.stdout);
|
|
315
236
|
ctx.stdout.write(
|
|
316
237
|
[
|
|
@@ -333,3 +254,44 @@ async function themeApplyCommand(argv: string[], ctx: ThemeContext): Promise<num
|
|
|
333
254
|
|
|
334
255
|
return 0;
|
|
335
256
|
}
|
|
257
|
+
|
|
258
|
+
const themeShowCmd = defineCommand({
|
|
259
|
+
meta: {
|
|
260
|
+
name: "show",
|
|
261
|
+
description: "Print resolved theme tokens.",
|
|
262
|
+
},
|
|
263
|
+
args: {},
|
|
264
|
+
async run() {
|
|
265
|
+
const code = await runThemeShow();
|
|
266
|
+
if (code !== 0) process.exit(code);
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const themeApplyCmd = defineCommand({
|
|
271
|
+
meta: {
|
|
272
|
+
name: "apply",
|
|
273
|
+
description: "Write theme.css from the current config.",
|
|
274
|
+
},
|
|
275
|
+
args: {
|
|
276
|
+
"rewrite-artifacts": {
|
|
277
|
+
type: "boolean",
|
|
278
|
+
default: false,
|
|
279
|
+
description: "Retrofit existing artifacts and index pages with the theme link",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
async run({ args }) {
|
|
283
|
+
const code = await runThemeApply({ rewriteArtifacts: args["rewrite-artifacts"] });
|
|
284
|
+
if (code !== 0) process.exit(code);
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
export const themeCmd = defineCommand({
|
|
289
|
+
meta: {
|
|
290
|
+
name: "theme",
|
|
291
|
+
description: "Show or apply the configured theme.",
|
|
292
|
+
},
|
|
293
|
+
subCommands: {
|
|
294
|
+
show: themeShowCmd,
|
|
295
|
+
apply: themeApplyCmd,
|
|
296
|
+
},
|
|
297
|
+
});
|
package/src/cli/index.ts
CHANGED
|
@@ -1,78 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { defineCommand, runMain } from "citty";
|
|
4
4
|
import pkg from "../../package.json" with { type: "json" };
|
|
5
|
-
import { lsCommand } from "./commands/ls.ts";
|
|
6
|
-
import { openCommand } from "./commands/open.ts";
|
|
7
|
-
import { serveCommand } from "./commands/serve.ts";
|
|
8
|
-
import { stopCommand } from "./commands/stop.ts";
|
|
9
|
-
import { restartCommand } from "./commands/restart.ts";
|
|
10
|
-
import { pruneCommand } from "./commands/prune.ts";
|
|
11
|
-
import { themeCommand } from "./commands/theme.ts";
|
|
12
|
-
|
|
13
|
-
const subcommand = process.argv[2];
|
|
14
|
-
const rest = process.argv.slice(3);
|
|
15
5
|
|
|
16
6
|
export const CESIUM_VERSION: string = pkg.version;
|
|
17
7
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
restart: restartCommand,
|
|
24
|
-
prune: pruneCommand,
|
|
25
|
-
theme: themeCommand,
|
|
26
|
-
version: async () => {
|
|
27
|
-
process.stdout.write(`cesium ${CESIUM_VERSION}\n`);
|
|
28
|
-
return 0;
|
|
8
|
+
const main = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "cesium",
|
|
11
|
+
version: pkg.version,
|
|
12
|
+
description: "artifact manager for opencode sessions",
|
|
29
13
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
" ls List artifacts in the current project (or all with --all)",
|
|
41
|
-
" open Open an artifact by id prefix in the browser",
|
|
42
|
-
" serve Start the local HTTP server in the foreground",
|
|
43
|
-
" stop Stop the running cesium server",
|
|
44
|
-
" restart Stop and re-start the cesium server",
|
|
45
|
-
" prune Delete artifacts older than a given duration",
|
|
46
|
-
" theme Show or apply the configured theme",
|
|
47
|
-
" version Print the cesium version",
|
|
48
|
-
"",
|
|
49
|
-
"Options:",
|
|
50
|
-
" --help, -h Show this help message",
|
|
51
|
-
" --version, -v Print the cesium version",
|
|
52
|
-
"",
|
|
53
|
-
"Run 'cesium <command> --help' for command-specific options.",
|
|
54
|
-
"",
|
|
55
|
-
].join("\n"),
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function main(): Promise<void> {
|
|
60
|
-
if (subcommand === "--version" || subcommand === "-v") {
|
|
61
|
-
process.stdout.write(`cesium ${CESIUM_VERSION}\n`);
|
|
62
|
-
process.exit(0);
|
|
63
|
-
}
|
|
64
|
-
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
65
|
-
printHelp();
|
|
66
|
-
process.exit(subcommand ? 0 : 1);
|
|
67
|
-
}
|
|
68
|
-
const fn = COMMANDS[subcommand];
|
|
69
|
-
if (!fn) {
|
|
70
|
-
process.stderr.write(`cesium: unknown command: ${subcommand}\n`);
|
|
71
|
-
printHelp();
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
const code = await fn(rest);
|
|
75
|
-
process.exit(code);
|
|
76
|
-
}
|
|
14
|
+
subCommands: {
|
|
15
|
+
ls: () => import("./commands/ls.ts").then((m) => m.lsCmd),
|
|
16
|
+
open: () => import("./commands/open.ts").then((m) => m.openCmd),
|
|
17
|
+
serve: () => import("./commands/serve.ts").then((m) => m.serveCmd),
|
|
18
|
+
stop: () => import("./commands/stop.ts").then((m) => m.stopCmd),
|
|
19
|
+
restart: () => import("./commands/restart.ts").then((m) => m.restartCmd),
|
|
20
|
+
prune: () => import("./commands/prune.ts").then((m) => m.pruneCmd),
|
|
21
|
+
theme: () => import("./commands/theme.ts").then((m) => m.themeCmd),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
77
24
|
|
|
78
|
-
await main
|
|
25
|
+
await runMain(main);
|
package/src/render/theme.ts
CHANGED
|
@@ -318,6 +318,13 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
318
318
|
border-radius: 12px;
|
|
319
319
|
padding: 18px 22px;
|
|
320
320
|
margin-bottom: 1.5em;
|
|
321
|
+
/* contain wide children (tables, long URLs, code) inside the card.
|
|
322
|
+
* min-width:0 lets the card shrink in grid/flex contexts (.cards-grid)
|
|
323
|
+
* so it actually obeys its track instead of growing to its widest child.
|
|
324
|
+
* overflow-x:auto then scrolls any content that's STILL too wide
|
|
325
|
+
* (e.g. a many-column table) rather than bursting the card border. */
|
|
326
|
+
min-width: 0;
|
|
327
|
+
overflow-x: auto;
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
/* tldr */
|
|
@@ -329,6 +336,8 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
329
336
|
margin-bottom: 1.5em;
|
|
330
337
|
font-size: 1.05rem;
|
|
331
338
|
color: var(--ink-soft);
|
|
339
|
+
min-width: 0;
|
|
340
|
+
overflow-x: auto;
|
|
332
341
|
}
|
|
333
342
|
|
|
334
343
|
/* callout */
|
|
@@ -340,6 +349,8 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
340
349
|
background: var(--surface-2);
|
|
341
350
|
color: var(--ink-soft);
|
|
342
351
|
font-size: 0.95rem;
|
|
352
|
+
min-width: 0;
|
|
353
|
+
overflow-x: auto;
|
|
343
354
|
}
|
|
344
355
|
.callout.note { border-color: var(--olive); background: color-mix(in srgb, var(--olive) 10%, var(--surface)); }
|
|
345
356
|
.callout.warn { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
|
|
@@ -573,6 +584,11 @@ figure.code figcaption {
|
|
|
573
584
|
padding: 10px 14px;
|
|
574
585
|
text-align: left;
|
|
575
586
|
vertical-align: top;
|
|
587
|
+
/* let long URLs / identifiers / paths wrap inside the cell instead of
|
|
588
|
+
* pushing the table beyond its container. Many-column tables that are
|
|
589
|
+
* still wider than the card fall through to the card's overflow-x. */
|
|
590
|
+
overflow-wrap: anywhere;
|
|
591
|
+
word-break: break-word;
|
|
576
592
|
}
|
|
577
593
|
.compare-table th {
|
|
578
594
|
background: var(--surface-2);
|
|
@@ -594,6 +610,8 @@ figure.code figcaption {
|
|
|
594
610
|
padding: 10px 14px;
|
|
595
611
|
text-align: left;
|
|
596
612
|
vertical-align: top;
|
|
613
|
+
overflow-wrap: anywhere;
|
|
614
|
+
word-break: break-word;
|
|
597
615
|
}
|
|
598
616
|
.risk-table th {
|
|
599
617
|
background: var(--surface-2);
|
package/src/server/api.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
// API
|
|
1
|
+
// API routes for interactive artifact submissions and state queries, exposed
|
|
2
|
+
// as a Hono sub-app. Mounted by lifecycle.ts via `handle.app.route("/", apiApp)`.
|
|
2
3
|
//
|
|
3
4
|
// Routes:
|
|
4
5
|
// POST /api/sessions/:projectSlug/:filename/answers/:questionId
|
|
5
6
|
// GET /api/sessions/:projectSlug/:filename/state
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
+
// Any other /api/* path returns a JSON 404 (rather than falling through to the
|
|
9
|
+
// static file handler, which would return the HTML 404 page).
|
|
8
10
|
|
|
9
11
|
import { join, resolve, relative } from "node:path";
|
|
12
|
+
import { Hono } from "hono";
|
|
10
13
|
import { submitAnswer, getState } from "../storage/mutate.ts";
|
|
11
14
|
import type { AnswerValue } from "../render/validate.ts";
|
|
12
15
|
|
|
@@ -19,148 +22,133 @@ export interface ApiHandlerOptions {
|
|
|
19
22
|
const FILENAME_RE = /^[^/\\]+\.html$/;
|
|
20
23
|
const DANGEROUS_RE = /[/\\]|\.\./;
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
interface ResolvedArtifact {
|
|
26
|
+
/** Absolute path to the artifact file. */
|
|
27
|
+
artifactPath: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate the slug/filename pair and resolve the artifact's absolute path,
|
|
32
|
+
* enforcing containment under <stateDir>/projects/<slug>/artifacts/. Returns
|
|
33
|
+
* a Hono `Response` on validation failure, or the resolved path on success.
|
|
34
|
+
*/
|
|
35
|
+
function resolveArtifact(
|
|
36
|
+
stateDir: string,
|
|
37
|
+
projectSlug: string,
|
|
38
|
+
filename: string,
|
|
39
|
+
): ResolvedArtifact | Response {
|
|
40
|
+
if (DANGEROUS_RE.test(projectSlug) || DANGEROUS_RE.test(filename)) {
|
|
41
|
+
return Response.json({ ok: false, error: "invalid path component" }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
if (!FILENAME_RE.test(filename)) {
|
|
44
|
+
return Response.json({ ok: false, error: "filename must end with .html" }, { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const artifactsDir = join(stateDir, "projects", projectSlug, "artifacts");
|
|
48
|
+
const artifactPath = join(artifactsDir, filename);
|
|
49
|
+
const resolvedArtifactsDir = resolve(artifactsDir);
|
|
50
|
+
const resolvedArtifact = resolve(artifactPath);
|
|
51
|
+
const rel = relative(resolvedArtifactsDir, resolvedArtifact);
|
|
52
|
+
if (rel.startsWith("..") || rel.includes("/")) {
|
|
53
|
+
return Response.json({ ok: false, error: "invalid path" }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
return { artifactPath: resolvedArtifact };
|
|
30
56
|
}
|
|
31
57
|
|
|
32
|
-
export function
|
|
33
|
-
options: ApiHandlerOptions,
|
|
34
|
-
): (req: Request) => Promise<Response | undefined> {
|
|
58
|
+
export function createApiApp(options: ApiHandlerOptions): Hono {
|
|
35
59
|
const { stateDir } = options;
|
|
60
|
+
const app = new Hono();
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
// All API responses are dynamic — never let intermediaries cache them.
|
|
63
|
+
app.use("/api/*", async (c, next) => {
|
|
64
|
+
await next();
|
|
65
|
+
c.header("Cache-Control", "no-store");
|
|
66
|
+
});
|
|
40
67
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
68
|
+
// POST /api/sessions/:projectSlug/:filename/answers/:questionId
|
|
69
|
+
app.post("/api/sessions/:projectSlug/:filename/answers/:questionId", async (c) => {
|
|
70
|
+
const { projectSlug, filename, questionId } = c.req.param();
|
|
45
71
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const answerMatch = /^\/api\/sessions\/([^/]+)\/([^/]+)\/answers\/([^/]+)$/.exec(pathname);
|
|
49
|
-
// GET /api/sessions/:projectSlug/:filename/state
|
|
50
|
-
const stateMatch = /^\/api\/sessions\/([^/]+)\/([^/]+)\/state$/.exec(pathname);
|
|
72
|
+
const resolved = resolveArtifact(stateDir, projectSlug, filename);
|
|
73
|
+
if (resolved instanceof Response) return resolved;
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
let body: unknown;
|
|
76
|
+
try {
|
|
77
|
+
body = await c.req.json();
|
|
78
|
+
} catch {
|
|
79
|
+
return c.json({ ok: false, error: "invalid JSON body" }, 400);
|
|
55
80
|
}
|
|
56
81
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
82
|
+
if (
|
|
83
|
+
body === null ||
|
|
84
|
+
typeof body !== "object" ||
|
|
85
|
+
Array.isArray(body) ||
|
|
86
|
+
!("value" in (body as Record<string, unknown>))
|
|
87
|
+
) {
|
|
88
|
+
return c.json({ ok: false, error: 'body must contain a "value" field' }, 400);
|
|
64
89
|
}
|
|
65
90
|
|
|
66
|
-
|
|
67
|
-
return jsonResponse({ ok: false, error: "filename must end with .html" }, 400);
|
|
68
|
-
}
|
|
91
|
+
const value = (body as Record<string, unknown>)["value"] as AnswerValue;
|
|
69
92
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
const outcome = await submitAnswer({
|
|
94
|
+
artifactPath: resolved.artifactPath,
|
|
95
|
+
questionId,
|
|
96
|
+
value,
|
|
97
|
+
});
|
|
73
98
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
if (outcome.ok) {
|
|
100
|
+
return c.json(
|
|
101
|
+
{
|
|
102
|
+
ok: true,
|
|
103
|
+
status: outcome.status,
|
|
104
|
+
remaining: outcome.remaining,
|
|
105
|
+
replacementHtml: outcome.replacementHtml,
|
|
106
|
+
},
|
|
107
|
+
200,
|
|
108
|
+
);
|
|
79
109
|
}
|
|
80
110
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let body: unknown;
|
|
93
|
-
try {
|
|
94
|
-
body = await req.json();
|
|
95
|
-
} catch {
|
|
96
|
-
return jsonResponse({ ok: false, error: "invalid JSON body" }, 400);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
body === null ||
|
|
101
|
-
typeof body !== "object" ||
|
|
102
|
-
Array.isArray(body) ||
|
|
103
|
-
!("value" in (body as Record<string, unknown>))
|
|
104
|
-
) {
|
|
105
|
-
return jsonResponse({ ok: false, error: 'body must contain a "value" field' }, 400);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const value = (body as Record<string, unknown>)["value"] as AnswerValue;
|
|
109
|
-
|
|
110
|
-
const outcome = await submitAnswer({ artifactPath: resolvedArtifact, questionId, value });
|
|
111
|
-
|
|
112
|
-
if (outcome.ok) {
|
|
113
|
-
return jsonResponse(
|
|
114
|
-
{
|
|
115
|
-
ok: true,
|
|
116
|
-
status: outcome.status,
|
|
117
|
-
remaining: outcome.remaining,
|
|
118
|
-
replacementHtml: outcome.replacementHtml,
|
|
119
|
-
},
|
|
120
|
-
200,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
switch (outcome.reason) {
|
|
125
|
-
case "not-found":
|
|
126
|
-
case "not-interactive":
|
|
127
|
-
case "unknown-question":
|
|
128
|
-
return jsonResponse({ ok: false, reason: outcome.reason }, 404);
|
|
129
|
-
case "session-ended":
|
|
130
|
-
return jsonResponse({ ok: false, status: outcome.status }, 410);
|
|
131
|
-
case "expired":
|
|
132
|
-
return jsonResponse({ ok: false, status: "expired" }, 410);
|
|
133
|
-
case "invalid-value":
|
|
134
|
-
return jsonResponse({ ok: false, message: outcome.message }, 422);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Fallback (should not reach)
|
|
138
|
-
return jsonResponse({ ok: false, error: "internal error" }, 500);
|
|
111
|
+
switch (outcome.reason) {
|
|
112
|
+
case "not-found":
|
|
113
|
+
case "not-interactive":
|
|
114
|
+
case "unknown-question":
|
|
115
|
+
return c.json({ ok: false, reason: outcome.reason }, 404);
|
|
116
|
+
case "session-ended":
|
|
117
|
+
return c.json({ ok: false, status: outcome.status }, 410);
|
|
118
|
+
case "expired":
|
|
119
|
+
return c.json({ ok: false, status: "expired" }, 410);
|
|
120
|
+
case "invalid-value":
|
|
121
|
+
return c.json({ ok: false, message: outcome.message }, 422);
|
|
139
122
|
}
|
|
140
123
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (req.method !== "GET") {
|
|
144
|
-
return jsonResponse({ ok: false, error: "method not allowed" }, 404);
|
|
145
|
-
}
|
|
124
|
+
return c.json({ ok: false, error: "internal error" }, 500);
|
|
125
|
+
});
|
|
146
126
|
|
|
147
|
-
|
|
127
|
+
// GET /api/sessions/:projectSlug/:filename/state
|
|
128
|
+
app.get("/api/sessions/:projectSlug/:filename/state", async (c) => {
|
|
129
|
+
const { projectSlug, filename } = c.req.param();
|
|
148
130
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
131
|
+
const resolved = resolveArtifact(stateDir, projectSlug, filename);
|
|
132
|
+
if (resolved instanceof Response) return resolved;
|
|
152
133
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
answers: outcome.answers,
|
|
157
|
-
remaining: outcome.remaining,
|
|
158
|
-
},
|
|
159
|
-
200,
|
|
160
|
-
);
|
|
134
|
+
const outcome = await getState(resolved.artifactPath);
|
|
135
|
+
if (!outcome.ok) {
|
|
136
|
+
return c.json({ ok: false, reason: outcome.reason }, 404);
|
|
161
137
|
}
|
|
162
138
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
139
|
+
return c.json(
|
|
140
|
+
{
|
|
141
|
+
status: outcome.status,
|
|
142
|
+
answers: outcome.answers,
|
|
143
|
+
remaining: outcome.remaining,
|
|
144
|
+
},
|
|
145
|
+
200,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Catch-all under /api/* — keeps unmatched API paths as JSON 404 instead of
|
|
150
|
+
// falling through to the static file handler.
|
|
151
|
+
app.all("/api/*", (c) => c.json({ ok: false, error: "not found" }, 404));
|
|
152
|
+
|
|
153
|
+
return app;
|
|
166
154
|
}
|
package/src/server/favicon.ts
CHANGED
|
@@ -7,22 +7,14 @@
|
|
|
7
7
|
// (written by writeFaviconSvg on every publish). This shim covers the .ico
|
|
8
8
|
// fallback so users don't see a 404 in DevTools.
|
|
9
9
|
|
|
10
|
+
import { Hono } from "hono";
|
|
10
11
|
import { FAVICON_SVG } from "../render/favicon.ts";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
19
|
-
const url = new URL(req.url);
|
|
20
|
-
if (url.pathname !== "/favicon.ico") {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
return new Response(FAVICON_SVG, { status: 200, headers: SVG_RESPONSE_HEADERS });
|
|
27
|
-
};
|
|
13
|
+
export function createFaviconApp(): Hono {
|
|
14
|
+
const app = new Hono();
|
|
15
|
+
app.on(["GET", "HEAD"], "/favicon.ico", (c) => {
|
|
16
|
+
c.header("Cache-Control", "public, max-age=86400");
|
|
17
|
+
return c.body(FAVICON_SVG, 200, { "Content-Type": "image/svg+xml; charset=utf-8" });
|
|
18
|
+
});
|
|
19
|
+
return app;
|
|
28
20
|
}
|