fundus 0.0.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.
Files changed (61) hide show
  1. package/dist/build-CZYFLLvl.js +191 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +1420 -0
  4. package/dist/config.d.ts +2 -0
  5. package/dist/config.js +2 -0
  6. package/dist/core/image-Dqmz8FXS.js +54 -0
  7. package/dist/core/index.d.ts +499 -0
  8. package/dist/core/index.js +962 -0
  9. package/dist/core/reference-graph-CjuHbLDw.d.ts +280 -0
  10. package/dist/core/testing.d.ts +27 -0
  11. package/dist/core/testing.js +80 -0
  12. package/dist/editor/200.html +37 -0
  13. package/dist/editor/_app/immutable/assets/0.DLhIsYA_.css +1 -0
  14. package/dist/editor/_app/immutable/assets/2.DIbbW7U1.css +1 -0
  15. package/dist/editor/_app/immutable/assets/3.C-9vFQ9A.css +1 -0
  16. package/dist/editor/_app/immutable/assets/4.BS8ElpOV.css +1 -0
  17. package/dist/editor/_app/immutable/assets/ConfirmDialog.CHEISE-L.css +1 -0
  18. package/dist/editor/_app/immutable/assets/LibraryContextMenu.CDjCmdV-.css +1 -0
  19. package/dist/editor/_app/immutable/chunks/BfPE5wdt.js +1 -0
  20. package/dist/editor/_app/immutable/chunks/Bjy-W4x2.js +81 -0
  21. package/dist/editor/_app/immutable/chunks/CNRz3sSw.js +3 -0
  22. package/dist/editor/_app/immutable/chunks/CO4RBJfa.js +1 -0
  23. package/dist/editor/_app/immutable/chunks/Cx6qtxJg.js +1 -0
  24. package/dist/editor/_app/immutable/chunks/DjdrdRzT.js +1 -0
  25. package/dist/editor/_app/immutable/chunks/DjvOiDmq.js +1 -0
  26. package/dist/editor/_app/immutable/chunks/n7mvYc42.js +5 -0
  27. package/dist/editor/_app/immutable/chunks/xihTtKlq.js +1 -0
  28. package/dist/editor/_app/immutable/entry/app.BkqbfI8n.js +2 -0
  29. package/dist/editor/_app/immutable/entry/start.C8HHOLly.js +1 -0
  30. package/dist/editor/_app/immutable/nodes/0.xdMI5GEU.js +2 -0
  31. package/dist/editor/_app/immutable/nodes/1.D9p2tfoJ.js +1 -0
  32. package/dist/editor/_app/immutable/nodes/2.BrA0Hyzu.js +3 -0
  33. package/dist/editor/_app/immutable/nodes/3.WO7tZH_d.js +104 -0
  34. package/dist/editor/_app/immutable/nodes/4.DQtx17aW.js +2 -0
  35. package/dist/editor/_app/version.json +1 -0
  36. package/dist/index.d.ts +370 -0
  37. package/dist/index.js +4 -0
  38. package/dist/operations-DYM-5KVl.js +1797 -0
  39. package/dist/runtime/chroma-key-gl.d.ts +23 -0
  40. package/dist/runtime/chroma-key-gl.js +290 -0
  41. package/dist/runtime/components/ChromaKeyVideo.svelte +225 -0
  42. package/dist/runtime/components/Image.svelte +18 -0
  43. package/dist/runtime/components/Slice.svelte +35 -0
  44. package/dist/runtime/components/SliceCanvas.svelte +162 -0
  45. package/dist/runtime/components/SliceDom.svelte +104 -0
  46. package/dist/runtime/components/Video.svelte +60 -0
  47. package/dist/runtime/geometry.d.ts +75 -0
  48. package/dist/runtime/geometry.js +209 -0
  49. package/dist/runtime/index.d.ts +9 -0
  50. package/dist/runtime/index.js +9 -0
  51. package/dist/runtime/kind-handlers.d.ts +46 -0
  52. package/dist/runtime/kind-handlers.js +86 -0
  53. package/dist/runtime/manifest.d.ts +60 -0
  54. package/dist/runtime/manifest.js +117 -0
  55. package/dist/runtime/observe-border-box-size.d.ts +5 -0
  56. package/dist/runtime/observe-border-box-size.js +12 -0
  57. package/dist/runtime/preload-registry.d.ts +26 -0
  58. package/dist/runtime/preload-registry.js +68 -0
  59. package/dist/start-Bhnqt9Dc.js +279 -0
  60. package/dist/start-D2IXOrni.js +2 -0
  61. package/package.json +65 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1420 @@
1
+ #!/usr/bin/env node
2
+ import { b as serverPluginForType, t as FundusOperations, w as loadConfig, y as pluginsByType } from "./operations-DYM-5KVl.js";
3
+ import { n as formatCheckFinding, r as runCheck, t as runBuild } from "./build-CZYFLLvl.js";
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+ import { basename, join, resolve } from "node:path";
6
+ import { MANIFEST_COLORS, MANIFEST_EMOJIS, compareCodeUnits, folderNameOf, manifestEmojiError, parentFolderPath, proposeAssetId, resolveManifestColor, typesForFileName } from "#fundus/core";
7
+ //#region src/cli-io.ts
8
+ /**
9
+ * Terminal I/O for the fundus CLI (M7): hand-rolled flag parsing, the JSON
10
+ * output envelope, and the expected-error type — no CLI framework. The output
11
+ * contract agents rely on: with `--json`, stdout carries exactly one JSON
12
+ * document ({"ok": true, ...} on success, {"error": {"message": ...}} on an
13
+ * expected failure); without it, errors go to stderr. Exit codes: 0 success,
14
+ * 1 expected error, 2 unexpected crash (see cli.ts).
15
+ */
16
+ /**
17
+ * A wrong invocation (unknown flag/subcommand, missing positional, malformed
18
+ * flag value): an expected error whose message points at the right `--help`.
19
+ */
20
+ var CliUsageError = class extends Error {};
21
+ /**
22
+ * Parse a command's argument list against its flag definitions. Supports
23
+ * `--flag value` and `--flag=value`; anything not starting with `--` is a
24
+ * positional. Unknown flags and missing values throw a `CliUsageError` that
25
+ * names the command's `--help`.
26
+ */
27
+ function parseArguments(argumentList, flagDefinitions, commandPath) {
28
+ const positionals = [];
29
+ const flags = {};
30
+ for (let index = 0; index < argumentList.length; index++) {
31
+ const argument = argumentList[index];
32
+ if (!argument.startsWith("--")) {
33
+ positionals.push(argument);
34
+ continue;
35
+ }
36
+ const equalsIndex = argument.indexOf("=");
37
+ const name = equalsIndex === -1 ? argument.slice(2) : argument.slice(2, equalsIndex);
38
+ const definition = flagDefinitions[name];
39
+ if (!definition) throw new CliUsageError(`Unknown flag "--${name}" for \`${commandPath}\` — run \`${commandPath} --help\`.`);
40
+ if (!definition.takesValue) {
41
+ if (equalsIndex !== -1) throw new CliUsageError(`Flag "--${name}" takes no value.`);
42
+ flags[name] = true;
43
+ continue;
44
+ }
45
+ if (equalsIndex !== -1) {
46
+ flags[name] = argument.slice(equalsIndex + 1);
47
+ continue;
48
+ }
49
+ const value = argumentList[index + 1];
50
+ if (value === void 0) throw new CliUsageError(`Flag "--${name}" needs a value — run \`${commandPath} --help\`.`);
51
+ flags[name] = value;
52
+ index++;
53
+ }
54
+ return {
55
+ positionals,
56
+ flags
57
+ };
58
+ }
59
+ /**
60
+ * Enforce a command's exact positional shape. `names` are the placeholders
61
+ * from the synopsis (e.g. ["<id>", "<newId>"]); the mismatch message repeats
62
+ * the expected usage.
63
+ */
64
+ function requirePositionals(parsed, names, commandPath) {
65
+ if (parsed.positionals.length !== names.length) {
66
+ const usage = names.length === 0 ? commandPath : `${commandPath} ${names.join(" ")}`;
67
+ throw new CliUsageError(`\`${commandPath}\` expects ${names.length} argument(s) (usage: ${usage}) — got ${parsed.positionals.length}. Run \`${commandPath} --help\`.`);
68
+ }
69
+ return parsed.positionals;
70
+ }
71
+ /** The single JSON document a `--json` invocation writes to stdout. */
72
+ function printJsonDocument(document) {
73
+ console.log(JSON.stringify(document, null, " "));
74
+ }
75
+ /** Parse a JSON-valued flag (`--parameters`), mapping bad JSON to a usage error. */
76
+ function parseJsonFlag(name, value) {
77
+ try {
78
+ return JSON.parse(value);
79
+ } catch (cause) {
80
+ throw new CliUsageError(`Flag "--${name}" is not valid JSON (${cause instanceof Error ? cause.message : String(cause)}). Pass a single JSON value, e.g. --${name} '{"preset": "default"}'.`);
81
+ }
82
+ }
83
+ /**
84
+ * Parse a `--manifests a,b` list: comma-separated names, whitespace-tolerant.
85
+ * The empty string is the explicit "no manifests" — it clears the membership.
86
+ */
87
+ function parseManifestList(value) {
88
+ return value.split(",").map((name) => name.trim()).filter((name) => name.length > 0);
89
+ }
90
+ /** Human-readable byte count for terminal output (1024-based, one decimal). */
91
+ function formatBytes(bytes) {
92
+ if (bytes < 1024) return `${bytes} B`;
93
+ const units = [
94
+ "KiB",
95
+ "MiB",
96
+ "GiB"
97
+ ];
98
+ let value = bytes;
99
+ let unitIndex = -1;
100
+ while (value >= 1024 && unitIndex < units.length - 1) {
101
+ value /= 1024;
102
+ unitIndex++;
103
+ }
104
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
105
+ }
106
+ //#endregion
107
+ //#region src/cli-init.ts
108
+ const FUNDUS_CONFIG_TEMPLATE = `import { defineConfig } from 'fundus/config';
109
+
110
+ export default defineConfig({
111
+ rawDir: './assets/raw',
112
+ proxyDir: './static/fundus',
113
+ proxyBasePath: '/fundus/',
114
+ library: './assets/fundus.library.json',
115
+ manifestsDir: './src/lib/fundus'
116
+ });
117
+ `;
118
+ const INIT_HELP = `fundus init — create a Fundus configuration file
119
+
120
+ Usage: fundus init [--json]
121
+
122
+ Creates a template fundus.config.ts in the current directory. The command
123
+ refuses to overwrite an existing configuration file.
124
+
125
+ Flags:
126
+ --json print {"ok": true, "path": "fundus.config.ts"} on stdout
127
+ --help this help
128
+
129
+ Example:
130
+ cd my-host-app && npx fundus init
131
+ `;
132
+ async function runInitCommand(argumentList) {
133
+ if (argumentList.includes("--help")) {
134
+ console.log(INIT_HELP);
135
+ return;
136
+ }
137
+ const parsed = parseArguments(argumentList, {
138
+ json: { takesValue: false },
139
+ help: { takesValue: false }
140
+ }, "fundus init");
141
+ requirePositionals(parsed, [], "fundus init");
142
+ const configPath = resolve(process.cwd(), "fundus.config.ts");
143
+ try {
144
+ await writeFile(configPath, FUNDUS_CONFIG_TEMPLATE, {
145
+ encoding: "utf8",
146
+ flag: "wx"
147
+ });
148
+ } catch (cause) {
149
+ if (cause.code === "EEXIST") throw new Error(`fundus.config.ts already exists in ${process.cwd()}.`);
150
+ throw new Error(`Cannot create fundus.config.ts: ${cause instanceof Error ? cause.message : String(cause)}`);
151
+ }
152
+ if (parsed.flags.json) {
153
+ printJsonDocument({
154
+ ok: true,
155
+ path: "fundus.config.ts"
156
+ });
157
+ return;
158
+ }
159
+ console.log("Created fundus.config.ts.");
160
+ }
161
+ //#endregion
162
+ //#region src/cli-commands.ts
163
+ /**
164
+ * The state/asset/folder/manifest command groups of the fundus CLI (M7): thin
165
+ * terminal adapters over the one operations layer (operations.ts) — the same
166
+ * class the REST server exposes to the editor, so every mutation runs the
167
+ * editor's autosave chain (write library → process affected assets →
168
+ * regenerate manifest modules). Every command supports `--json` (a single
169
+ * JSON document on stdout, nothing else there) and `--help`; expected
170
+ * failures throw and cli.ts maps them to exit code 1.
171
+ */
172
+ const COMMON_FLAGS = {
173
+ json: { takesValue: false },
174
+ help: { takesValue: false }
175
+ };
176
+ function hasHelpFlag(argumentList) {
177
+ return argumentList.includes("--help");
178
+ }
179
+ async function createOperations() {
180
+ return new FundusOperations(await loadConfig(process.cwd()));
181
+ }
182
+ function manifestRows(state) {
183
+ return Object.keys(state.library.manifests).sort(compareCodeUnits).map((name) => ({
184
+ name,
185
+ color: state.library.manifests[name]?.color ?? null,
186
+ emoji: state.library.manifests[name]?.emoji ?? null,
187
+ explicitMemberIds: state.manifestMemberships[name]?.explicitMemberIds ?? [],
188
+ passiveMembers: state.manifestMemberships[name]?.passiveMembers ?? [],
189
+ deliveredBytes: state.manifestDeliveredBytes[name] ?? 0
190
+ }));
191
+ }
192
+ /** A manifest's `emoji name` prefix for human output (emoji only when set). */
193
+ function manifestLabel(row) {
194
+ return row.emoji ? `${row.emoji} ${row.name}` : row.name;
195
+ }
196
+ function assetById(state, id) {
197
+ const asset = state.assets.find((candidate) => candidate.id === id);
198
+ if (!asset) throw new Error(`Unknown asset "${id}".`);
199
+ return asset;
200
+ }
201
+ function membershipSummary(asset) {
202
+ const parts = [];
203
+ if (asset.manifests.length > 0) parts.push(asset.manifests.join(","));
204
+ if (asset.passiveManifests.length > 0) parts.push(`passive: ${asset.passiveManifests.map((membership) => membership.manifest).join(",")}`);
205
+ return parts.length > 0 ? parts.join(" + ") : "(no manifest)";
206
+ }
207
+ function printAssetLines(assets, indent = "") {
208
+ const idWidth = Math.max(0, ...assets.map((asset) => asset.id.length));
209
+ const typeWidth = Math.max(0, ...assets.map((asset) => asset.type.length));
210
+ for (const asset of assets) {
211
+ const warningSuffix = asset.warnings.length > 0 ? ` ⚠ ${asset.warnings.length} warning(s)` : "";
212
+ console.log(`${indent}${asset.id.padEnd(idWidth)} ${asset.type.padEnd(typeWidth)} ${membershipSummary(asset)}${warningSuffix}`);
213
+ }
214
+ }
215
+ function printIssues(issues) {
216
+ if (issues.length === 0) return;
217
+ console.log("Issues:");
218
+ for (const issue of issues) console.log(` - ${issue.assetId ? `[${issue.assetId}] ` : ""}${issue.message}`);
219
+ }
220
+ function printHumanState(state) {
221
+ const rows = manifestRows(state);
222
+ console.log(`${state.assets.length} asset(s), ${state.library.folders.length} folder(s), ${rows.length} manifest(s)`);
223
+ if (rows.length > 0) {
224
+ console.log("");
225
+ console.log("Manifests:");
226
+ const labels = rows.map(manifestLabel);
227
+ const nameWidth = Math.max(0, ...labels.map((label) => label.length));
228
+ rows.forEach((row, index) => {
229
+ console.log(` ${labels[index].padEnd(nameWidth)} ${row.explicitMemberIds.length} explicit + ${row.passiveMembers.length} passive member(s), ${formatBytes(row.deliveredBytes)} delivered`);
230
+ });
231
+ }
232
+ if (state.assets.length > 0) {
233
+ console.log("");
234
+ console.log("Assets:");
235
+ printAssetLines(state.assets, " ");
236
+ }
237
+ const warningTotal = state.assets.reduce((sum, asset) => sum + asset.warnings.length, 0);
238
+ if (warningTotal > 0) {
239
+ console.log("");
240
+ console.log(`⚠ ${warningTotal} warning(s) — see \`fundus asset list --warnings-only\`.`);
241
+ }
242
+ if (state.issues.length > 0) {
243
+ console.log("");
244
+ printIssues(state.issues);
245
+ }
246
+ }
247
+ function printHumanAssetDetail(asset) {
248
+ console.log(`${asset.id} (${asset.type})`);
249
+ console.log(` raw: ${asset.raw}`);
250
+ console.log(` folder: ${asset.folder || "root"}`);
251
+ console.log(` rawHash: ${asset.rawHash}`);
252
+ console.log(` manifests: ${asset.manifests.length > 0 ? asset.manifests.join(", ") : "(none)"}`);
253
+ for (const membership of asset.passiveManifests) console.log(` passive: ${membership.manifest} (via ${membership.via.join(", ")})`);
254
+ if (asset.proxy) {
255
+ const dimensions = asset.proxy.width !== void 0 && asset.proxy.height !== void 0 ? `, ${asset.proxy.width}×${asset.proxy.height}` : "";
256
+ const duration = asset.proxy.durationMs !== void 0 ? `, ${asset.proxy.durationMs} ms` : "";
257
+ console.log(` proxy: ${asset.proxy.relativePath} (${formatBytes(asset.proxy.fileSize)}${dimensions}${duration})`);
258
+ } else console.log(" proxy: (none — not delivered or not processed yet)");
259
+ for (const reference of asset.references.outgoing) console.log(` reference: ${reference.field} → ${reference.assetId}`);
260
+ for (const reference of asset.references.incoming) console.log(` referenced: ← ${reference.assetId} (${reference.field})`);
261
+ console.log(" parameters:");
262
+ const parametersJson = JSON.stringify(asset.parameters, null, " ");
263
+ for (const line of parametersJson.split("\n")) console.log(` ${line}`);
264
+ for (const warning of asset.warnings) console.log(` ⚠ ${warning}`);
265
+ }
266
+ /** Human output of a successful mutation: headline, asset warnings, issues. */
267
+ function printMutationResult(headline, asset, state) {
268
+ console.log(headline);
269
+ if (asset) for (const warning of asset.warnings) console.log(` ⚠ ${warning}`);
270
+ printIssues(state.issues);
271
+ }
272
+ const STATE_HELP = `fundus state — the full project state in one document
273
+
274
+ Usage: fundus state [--json]
275
+
276
+ The agent's one-stop overview: every asset (type, raw file, parameters,
277
+ explicit + passive manifest memberships, proxy status, reference edges,
278
+ warnings), every manifest (members, delivered bytes), library-level
279
+ validation issues, and the available preset names per asset type.
280
+
281
+ Flags:
282
+ --json print {"ok": true, "state": <FundusStateDto>} on stdout
283
+ --help this help
284
+
285
+ Read-only: nothing is processed or written.
286
+
287
+ Example:
288
+ fundus state --json | jq '.state.assets[].id'
289
+ `;
290
+ async function runStateCommand(argumentList) {
291
+ if (hasHelpFlag(argumentList)) {
292
+ console.log(STATE_HELP);
293
+ return;
294
+ }
295
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus state");
296
+ requirePositionals(parsed, [], "fundus state");
297
+ const state = await (await createOperations()).state();
298
+ if (parsed.flags.json) {
299
+ printJsonDocument({
300
+ ok: true,
301
+ state
302
+ });
303
+ return;
304
+ }
305
+ printHumanState(state);
306
+ }
307
+ const ASSET_HELP = `fundus asset — inspect and mutate assets
308
+
309
+ Usage: fundus asset <subcommand> …
310
+
311
+ Every mutation runs the full editor autosave chain: write library →
312
+ process affected assets → regenerate manifest modules. There is no
313
+ separate apply/sync step. All subcommands accept --json (a single JSON
314
+ document on stdout) and --help.
315
+
316
+ Subcommands:
317
+ list list assets (--manifest, --type, --warnings-only)
318
+ show one asset in detail
319
+ ingest copy a file into the raw dir as a typed asset
320
+ set merge parameters and/or replace manifest membership
321
+ rename rename an asset id
322
+ move move an asset to a folder or root
323
+ delete delete an asset (record, raw file, proxies)
324
+
325
+ Run \`fundus asset <subcommand> --help\` for the focused page (flags,
326
+ semantics, an example) — e.g. \`fundus asset set --help\` documents the
327
+ --parameters merge rules.
328
+ `;
329
+ const ASSET_SUBCOMMAND_HELP = {
330
+ list: `fundus asset list — list assets, optionally filtered
331
+
332
+ Usage: fundus asset list [--manifest <name>] [--type <type>] [--warnings-only] [--json]
333
+
334
+ One line per asset: id, type, manifest memberships (explicit +
335
+ passive), warning count.
336
+
337
+ Flags:
338
+ --manifest <name> only members of this manifest (explicit or passive)
339
+ --type <type> only this asset type (${Object.keys(pluginsByType).sort().join(", ")})
340
+ --warnings-only only assets with at least one warning
341
+ --json print {"ok": true, "assets": [<AssetStateDto>, …]}
342
+ --help this help
343
+
344
+ Read-only: nothing is processed or written.
345
+
346
+ Example:
347
+ fundus asset list --manifest mainCatalog --json
348
+ `,
349
+ show: `fundus asset show — one asset in detail
350
+
351
+ Usage: fundus asset show <id> [--json]
352
+
353
+ Prints the asset's raw file, content hash, parameters, explicit +
354
+ passive manifest memberships, proxy status, reference edges (outgoing
355
+ and incoming), and warnings.
356
+
357
+ Flags:
358
+ --json print {"ok": true, "asset": <AssetStateDto>}
359
+ --help this help
360
+
361
+ Read-only: nothing is processed or written.
362
+
363
+ Example:
364
+ fundus asset show navigationPanel --json
365
+ `,
366
+ ingest: `fundus asset ingest — copy a file into the raw dir as a typed asset
367
+
368
+ Usage: fundus asset ingest <file> [--type <type>] [--id <id>] [--folder <path|root>]
369
+ [--manifests <a,b>] [--parameters <json>] [--json]
370
+
371
+ Behaves like an editor upload: the file is copied into the raw
372
+ directory (sanitized, non-colliding name) and content-hashed, and the
373
+ record starts with the owning plugin's default parameters — a slice
374
+ additionally seeds insets from the image dimensions and a pixel ratio
375
+ from a @2x-style file-name suffix. Runs the full autosave chain
376
+ (write library → process → regenerate manifest modules).
377
+
378
+ Flags:
379
+ --type <type> required when the file extension is ambiguous
380
+ (.png could be image or slice, .mp4 video or
381
+ chroma-key-video); optional when exactly one
382
+ type accepts it (.wav → audio); known types:
383
+ ${Object.keys(pluginsByType).sort().join(", ")}
384
+ --id <id> asset id (camelCase identifier); default:
385
+ proposed from the file name
386
+ (panel_button@2x.png → sidebarPanel)
387
+ --folder <path|root> destination folder; default: root
388
+ --manifests <a,b> explicit manifest membership, applied in the
389
+ same operation (comma-separated; every name
390
+ must exist)
391
+ --parameters <json> parameter patch merged onto the plugin defaults
392
+ — same shallow-merge semantics as \`asset set\`
393
+ (see \`fundus asset set --help\`)
394
+ --json print {"ok": true, "asset": <AssetStateDto>, "issues": […]}
395
+ --help this help
396
+
397
+ A failed ingest (invalid patch, unknown manifest, …) leaves nothing
398
+ behind: the raw file and record are rolled back.
399
+
400
+ Example:
401
+ fundus asset ingest ./exports/navigation_panel@2x.png --type slice \\
402
+ --manifests mainCatalog --json
403
+ `,
404
+ set: `fundus asset set — update an asset's parameters and/or memberships
405
+
406
+ Usage: fundus asset set <id> [--parameters <json>] [--manifests <a,b>] [--json]
407
+
408
+ At least one of --parameters/--manifests is required. Runs the full
409
+ autosave chain (write library → process → regenerate manifest modules).
410
+
411
+ Flags:
412
+ --parameters <json> SHALLOW MERGE at the top level onto the current
413
+ parameters, then plugin validation:
414
+ {"preset": "webp-hq"} sets one field,
415
+ keeps the others
416
+ {"chromaKey": {…}} replaces the whole
417
+ chromaKey object
418
+ {"mask": null} clears an optional
419
+ field
420
+ {"mask": {"$asset": "someImage"}}
421
+ sets an asset
422
+ reference
423
+ --manifests <a,b> REPLACES the explicit membership set (declarative,
424
+ idempotent); --manifests "" clears it
425
+ --json print {"ok": true, "asset": <AssetStateDto>, "issues": […]}
426
+ --help this help
427
+
428
+ The mutation is validated first and rejected (exit 1, nothing written)
429
+ if it would leave the library invalid — e.g. a reference to a missing
430
+ asset, a reference cycle, or an unknown manifest name. Ingest reference
431
+ targets before pointing at them.
432
+
433
+ Example:
434
+ fundus asset set characterIdle \\
435
+ --parameters '{"mask": {"$asset": "characterIdleMask"}}' --json
436
+ `,
437
+ rename: `fundus asset rename — rename an asset id
438
+
439
+ Usage: fundus asset rename <id> <newId> [--json]
440
+
441
+ Renames the asset's id (the raw file keeps its name — ids are the
442
+ stable key) and re-hashes its proxy under the new id via the autosave
443
+ chain. Fails if another asset references <id> — update the referrers
444
+ first — or if <newId> is taken or invalid.
445
+
446
+ Flags:
447
+ --json print {"ok": true, "asset": <AssetStateDto>, "issues": […]}
448
+ --help this help
449
+
450
+ Example:
451
+ fundus asset rename sidebarPanel navigationPanel --json
452
+ `,
453
+ move: `fundus asset move — move an asset between folders
454
+
455
+ Usage: fundus asset move <id> <folder|root> [--json]
456
+
457
+ Moves the raw file and its current proxy without changing the stable asset id,
458
+ then runs the autosave chain. The destination folder must already exist; use
459
+ the literal "root" for the implicit root.
460
+
461
+ Flags:
462
+ --json print {"ok": true, "asset": <AssetStateDto>, "issues": […]}
463
+ --help this help
464
+
465
+ Example:
466
+ fundus asset move navigationPanel "UI/Buttons" --json
467
+ `,
468
+ delete: `fundus asset delete — delete an asset
469
+
470
+ Usage: fundus asset delete <id> [--json]
471
+
472
+ Deletes the library record, its raw file, and its proxies, then
473
+ regenerates the affected manifest modules. Fails if another asset
474
+ references it — remove the references first.
475
+
476
+ Flags:
477
+ --json print {"ok": true, "deletedAssetId": "<id>", "issues": […]}
478
+ --help this help
479
+
480
+ Example:
481
+ fundus asset delete obsoleteBanner --json
482
+ `
483
+ };
484
+ async function runAssetList(argumentList) {
485
+ const parsed = parseArguments(argumentList, {
486
+ ...COMMON_FLAGS,
487
+ manifest: { takesValue: true },
488
+ type: { takesValue: true },
489
+ "warnings-only": { takesValue: false }
490
+ }, "fundus asset list");
491
+ requirePositionals(parsed, [], "fundus asset list");
492
+ const typeFilter = parsed.flags.type;
493
+ if (typeof typeFilter === "string") serverPluginForType(typeFilter);
494
+ const state = await (await createOperations()).state();
495
+ const manifestFilter = parsed.flags.manifest;
496
+ if (typeof manifestFilter === "string" && !Object.hasOwn(state.library.manifests, manifestFilter)) throw new Error(`Unknown manifest "${manifestFilter}".`);
497
+ const assets = state.assets.filter((asset) => {
498
+ if (typeof typeFilter === "string" && asset.type !== typeFilter) return false;
499
+ if (typeof manifestFilter === "string" && !asset.manifests.includes(manifestFilter) && !asset.passiveManifests.some((membership) => membership.manifest === manifestFilter)) return false;
500
+ if (parsed.flags["warnings-only"] && asset.warnings.length === 0) return false;
501
+ return true;
502
+ });
503
+ if (parsed.flags.json) {
504
+ printJsonDocument({
505
+ ok: true,
506
+ assets
507
+ });
508
+ return;
509
+ }
510
+ if (assets.length === 0) {
511
+ console.log("No matching assets.");
512
+ return;
513
+ }
514
+ printAssetLines(assets);
515
+ }
516
+ async function runAssetShow(argumentList) {
517
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus asset show");
518
+ const [id] = requirePositionals(parsed, ["<id>"], "fundus asset show");
519
+ const asset = assetById(await (await createOperations()).state(), id);
520
+ if (parsed.flags.json) {
521
+ printJsonDocument({
522
+ ok: true,
523
+ asset
524
+ });
525
+ return;
526
+ }
527
+ printHumanAssetDetail(asset);
528
+ }
529
+ /**
530
+ * Infer the asset type from the file extension: unambiguous → that type;
531
+ * ambiguous (png: image/slice, mp4: video/chroma-key-video) → `--type` is
532
+ * required; unknown → list what the plugins accept.
533
+ */
534
+ function inferTypeForFileName(fileName) {
535
+ const candidates = typesForFileName(fileName);
536
+ if (candidates.length === 1) return candidates[0];
537
+ const extension = fileName.slice(fileName.lastIndexOf(".") + 1).toLowerCase();
538
+ if (candidates.length === 0) {
539
+ const knownExtensions = [...new Set(Object.values(pluginsByType).flatMap((plugin) => plugin.accepts))].sort().join(", ");
540
+ throw new Error(`No asset type accepts ".${extension}" files (accepted extensions: ${knownExtensions}).`);
541
+ }
542
+ throw new Error(`The ".${extension}" extension is ambiguous — pass --type <${candidates.join("|")}>.`);
543
+ }
544
+ async function runAssetIngest(argumentList) {
545
+ const parsed = parseArguments(argumentList, {
546
+ ...COMMON_FLAGS,
547
+ type: { takesValue: true },
548
+ id: { takesValue: true },
549
+ manifests: { takesValue: true },
550
+ parameters: { takesValue: true },
551
+ folder: { takesValue: true }
552
+ }, "fundus asset ingest");
553
+ const [filePath] = requirePositionals(parsed, ["<file>"], "fundus asset ingest");
554
+ const absolutePath = resolve(process.cwd(), filePath);
555
+ let bytes;
556
+ try {
557
+ bytes = new Uint8Array(await readFile(absolutePath));
558
+ } catch {
559
+ throw new Error(`Cannot read file "${filePath}".`);
560
+ }
561
+ const fileName = basename(absolutePath);
562
+ const type = typeof parsed.flags.type === "string" ? parsed.flags.type : inferTypeForFileName(fileName);
563
+ const id = typeof parsed.flags.id === "string" ? parsed.flags.id : proposeAssetId(fileName);
564
+ const parametersPatch = typeof parsed.flags.parameters === "string" ? parseJsonFlag("parameters", parsed.flags.parameters) : void 0;
565
+ const manifests = typeof parsed.flags.manifests === "string" ? parseManifestList(parsed.flags.manifests) : void 0;
566
+ const folder = typeof parsed.flags.folder === "string" ? parseFolderToken(parsed.flags.folder) : "";
567
+ const state = await (await createOperations()).ingest({
568
+ fileName,
569
+ bytes,
570
+ type,
571
+ id,
572
+ folder,
573
+ parametersPatch,
574
+ manifests
575
+ });
576
+ const asset = assetById(state, id);
577
+ if (parsed.flags.json) {
578
+ printJsonDocument({
579
+ ok: true,
580
+ asset,
581
+ issues: state.issues
582
+ });
583
+ return;
584
+ }
585
+ printMutationResult(`Ingested "${fileName}" as ${type} asset "${id}".`, asset, state);
586
+ }
587
+ async function runAssetSet(argumentList) {
588
+ const parsed = parseArguments(argumentList, {
589
+ ...COMMON_FLAGS,
590
+ parameters: { takesValue: true },
591
+ manifests: { takesValue: true }
592
+ }, "fundus asset set");
593
+ const [id] = requirePositionals(parsed, ["<id>"], "fundus asset set");
594
+ if (parsed.flags.parameters === void 0 && parsed.flags.manifests === void 0) throw new CliUsageError("`fundus asset set` needs --parameters and/or --manifests — run `fundus asset set --help`.");
595
+ const parametersPatch = typeof parsed.flags.parameters === "string" ? parseJsonFlag("parameters", parsed.flags.parameters) : void 0;
596
+ const manifests = typeof parsed.flags.manifests === "string" ? parseManifestList(parsed.flags.manifests) : void 0;
597
+ const state = await (await createOperations()).setAsset(id, {
598
+ parametersPatch,
599
+ manifests
600
+ });
601
+ const asset = assetById(state, id);
602
+ if (parsed.flags.json) {
603
+ printJsonDocument({
604
+ ok: true,
605
+ asset,
606
+ issues: state.issues
607
+ });
608
+ return;
609
+ }
610
+ printMutationResult(`Updated asset "${id}".`, asset, state);
611
+ }
612
+ async function runAssetRename(argumentList) {
613
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus asset rename");
614
+ const [id, newId] = requirePositionals(parsed, ["<id>", "<newId>"], "fundus asset rename");
615
+ const state = await (await createOperations()).renameAsset(id, newId);
616
+ const asset = assetById(state, newId);
617
+ if (parsed.flags.json) {
618
+ printJsonDocument({
619
+ ok: true,
620
+ asset,
621
+ issues: state.issues
622
+ });
623
+ return;
624
+ }
625
+ printMutationResult(`Renamed asset "${id}" to "${newId}".`, asset, state);
626
+ }
627
+ function parseFolderToken(token) {
628
+ return token.toLocaleLowerCase("en-US") === "root" ? "" : token;
629
+ }
630
+ async function runAssetMove(argumentList) {
631
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus asset move");
632
+ const [id, folderToken] = requirePositionals(parsed, ["<id>", "<folder|root>"], "fundus asset move");
633
+ const state = await (await createOperations()).moveAsset(id, parseFolderToken(folderToken));
634
+ const asset = assetById(state, id);
635
+ if (parsed.flags.json) {
636
+ printJsonDocument({
637
+ ok: true,
638
+ asset,
639
+ issues: state.issues
640
+ });
641
+ return;
642
+ }
643
+ printMutationResult(`Moved asset "${id}" to "${asset.folder || "root"}".`, asset, state);
644
+ }
645
+ async function runAssetDelete(argumentList) {
646
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus asset delete");
647
+ const [id] = requirePositionals(parsed, ["<id>"], "fundus asset delete");
648
+ const state = await (await createOperations()).deleteAsset(id, { failOnIntroducedIssues: true });
649
+ if (parsed.flags.json) {
650
+ printJsonDocument({
651
+ ok: true,
652
+ deletedAssetId: id,
653
+ issues: state.issues
654
+ });
655
+ return;
656
+ }
657
+ printMutationResult(`Deleted asset "${id}" (record, raw file, proxies).`, null, state);
658
+ }
659
+ async function runAssetCommand(argumentList) {
660
+ const [subcommand, ...rest] = argumentList;
661
+ if (subcommand === void 0 || subcommand === "--help") {
662
+ console.log(ASSET_HELP);
663
+ if (subcommand === void 0) throw new CliUsageError("`fundus asset` needs a subcommand: list, show, ingest, set, rename, move, delete.");
664
+ return;
665
+ }
666
+ if (hasHelpFlag(rest)) {
667
+ console.log(ASSET_SUBCOMMAND_HELP[subcommand] ?? ASSET_HELP);
668
+ return;
669
+ }
670
+ switch (subcommand) {
671
+ case "list": return runAssetList(rest);
672
+ case "show": return runAssetShow(rest);
673
+ case "ingest": return runAssetIngest(rest);
674
+ case "set": return runAssetSet(rest);
675
+ case "rename": return runAssetRename(rest);
676
+ case "move": return runAssetMove(rest);
677
+ case "delete": return runAssetDelete(rest);
678
+ default: throw new CliUsageError(`Unknown subcommand "asset ${subcommand}" — run \`fundus asset --help\`.`);
679
+ }
680
+ }
681
+ function folderRows(state) {
682
+ return [...state.library.folders].sort(compareCodeUnits).map((path) => ({
683
+ path,
684
+ name: folderNameOf(path),
685
+ parent: parentFolderPath(path),
686
+ directAssetIds: state.assets.filter((asset) => asset.folder === path).map((asset) => asset.id).sort(compareCodeUnits)
687
+ }));
688
+ }
689
+ const FOLDER_HELP = `fundus folder — manage the mirrored raw/proxy folder tree
690
+
691
+ Usage: fundus folder <subcommand> …
692
+
693
+ Subcommands:
694
+ list list registered folders and their direct assets
695
+ create create a persisted folder (fundus folder create <path>)
696
+ rename rename one folder segment and its subtree
697
+ move move a complete folder subtree below another folder
698
+ delete recursively delete a folder subtree and its assets
699
+
700
+ All mutations update rawDir and proxyDir together, maintain .gitkeep files,
701
+ and run the full autosave chain. All subcommands accept --json and --help.
702
+ `;
703
+ const FOLDER_SUBCOMMAND_HELP = {
704
+ list: `fundus folder list — list registered folders
705
+
706
+ Usage: fundus folder list [--json]
707
+
708
+ Prints each folder path, parent, and direct assets. Root is implicit and is not
709
+ included. Read-only: nothing is processed or written.
710
+
711
+ Flags:
712
+ --json print {"ok": true, "folders": […]}
713
+ --help this help
714
+
715
+ Example:
716
+ fundus folder list --json
717
+ `,
718
+ create: `fundus folder create — create a persisted folder
719
+
720
+ Usage: fundus folder create <path> [--json]
721
+
722
+ The parent must already exist (or be the implicit root). Creates matching raw
723
+ and proxy directories with managed .gitkeep files.
724
+
725
+ Flags:
726
+ --json print the created folder row and issues
727
+ --help this help
728
+
729
+ Example:
730
+ fundus folder create "UI/Buttons" --json
731
+ `,
732
+ rename: `fundus folder rename — rename a folder
733
+
734
+ Usage: fundus folder rename <path> <newName> [--json]
735
+
736
+ Renames only the final path segment, preserving the parent. Descendant folders,
737
+ raw paths, proxy paths, and generated URLs move with it.
738
+
739
+ Flags:
740
+ --json print the renamed folder row and issues
741
+ --help this help
742
+
743
+ Example:
744
+ fundus folder rename "UI/Buttons" Controls --json
745
+ `,
746
+ move: `fundus folder move — move a folder subtree
747
+
748
+ Usage: fundus folder move <path> <parent|root> [--json]
749
+
750
+ Moves the folder, every descendant folder, and all contained raw/proxy files
751
+ below an existing parent. The folder keeps its name and cannot move into its
752
+ own subtree. Use the literal "root" for the implicit root.
753
+
754
+ Flags:
755
+ --json print the moved folder row and issues
756
+ --help this help
757
+
758
+ Example:
759
+ fundus folder move "UI/Buttons" Shared --json
760
+ `,
761
+ delete: `fundus folder delete — recursively delete a folder
762
+
763
+ Usage: fundus folder delete <path> --recursive [--json]
764
+
765
+ Deletes the complete subtree and all assets in it. --recursive is required to
766
+ make this destructive operation explicit. External references to deleted assets
767
+ are reported as validation issues after deletion.
768
+
769
+ Flags:
770
+ --recursive required destructive-operation acknowledgement
771
+ --json print the deleted path and resulting issues
772
+ --help this help
773
+
774
+ Example:
775
+ fundus folder delete "UI/Obsolete" --recursive --json
776
+ `
777
+ };
778
+ async function runFolderList(argumentList) {
779
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus folder list");
780
+ requirePositionals(parsed, [], "fundus folder list");
781
+ const folders = folderRows(await (await createOperations()).state());
782
+ if (parsed.flags.json) {
783
+ printJsonDocument({
784
+ ok: true,
785
+ folders
786
+ });
787
+ return;
788
+ }
789
+ if (folders.length === 0) {
790
+ console.log("No folders.");
791
+ return;
792
+ }
793
+ for (const folder of folders) console.log(`${folder.path} ${folder.directAssetIds.join(", ") || "(empty)"}`);
794
+ }
795
+ async function runFolderCreate(argumentList) {
796
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus folder create");
797
+ const [path] = requirePositionals(parsed, ["<path>"], "fundus folder create");
798
+ const state = await (await createOperations()).createFolder(path);
799
+ const folder = folderRows(state).find((row) => row.path === path.normalize("NFC"));
800
+ if (parsed.flags.json) {
801
+ printJsonDocument({
802
+ ok: true,
803
+ folder,
804
+ issues: state.issues
805
+ });
806
+ return;
807
+ }
808
+ printMutationResult(`Created folder "${folder?.path ?? path}".`, null, state);
809
+ }
810
+ async function runFolderRename(argumentList) {
811
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus folder rename");
812
+ const [path, newName] = requirePositionals(parsed, ["<path>", "<newName>"], "fundus folder rename");
813
+ const state = await (await createOperations()).renameFolder(path, newName);
814
+ const nextPath = parentFolderPath(path) === "" ? newName : `${parentFolderPath(path)}/${newName}`;
815
+ const folder = folderRows(state).find((row) => row.path === nextPath.normalize("NFC"));
816
+ if (parsed.flags.json) {
817
+ printJsonDocument({
818
+ ok: true,
819
+ folder,
820
+ issues: state.issues
821
+ });
822
+ return;
823
+ }
824
+ printMutationResult(`Renamed folder "${path}" to "${folder?.path ?? nextPath}".`, null, state);
825
+ }
826
+ async function runFolderMove(argumentList) {
827
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus folder move");
828
+ const [path, parentToken] = requirePositionals(parsed, ["<path>", "<parent|root>"], "fundus folder move");
829
+ const parent = parseFolderToken(parentToken);
830
+ const state = await (await createOperations()).moveFolder(path, parent);
831
+ const nextPath = parent === "" ? folderNameOf(path) : `${parent}/${folderNameOf(path)}`;
832
+ const folder = folderRows(state).find((row) => row.path === nextPath.normalize("NFC"));
833
+ if (parsed.flags.json) {
834
+ printJsonDocument({
835
+ ok: true,
836
+ folder,
837
+ issues: state.issues
838
+ });
839
+ return;
840
+ }
841
+ printMutationResult(`Moved folder "${path}" to "${folder?.path ?? nextPath}".`, null, state);
842
+ }
843
+ async function runFolderDelete(argumentList) {
844
+ const parsed = parseArguments(argumentList, {
845
+ ...COMMON_FLAGS,
846
+ recursive: { takesValue: false }
847
+ }, "fundus folder delete");
848
+ const [path] = requirePositionals(parsed, ["<path>"], "fundus folder delete");
849
+ if (!parsed.flags.recursive) throw new CliUsageError("`fundus folder delete` requires --recursive because it deletes the subtree and its assets.");
850
+ const state = await (await createOperations()).deleteFolder(path);
851
+ if (parsed.flags.json) {
852
+ printJsonDocument({
853
+ ok: true,
854
+ deletedFolderPath: path,
855
+ issues: state.issues
856
+ });
857
+ return;
858
+ }
859
+ printMutationResult(`Deleted folder "${path}" recursively.`, null, state);
860
+ }
861
+ async function runFolderCommand(argumentList) {
862
+ const [subcommand, ...rest] = argumentList;
863
+ if (subcommand === void 0 || subcommand === "--help") {
864
+ console.log(FOLDER_HELP);
865
+ if (subcommand === void 0) throw new CliUsageError("`fundus folder` needs a subcommand: list, create, rename, move, delete.");
866
+ return;
867
+ }
868
+ if (hasHelpFlag(rest)) {
869
+ console.log(FOLDER_SUBCOMMAND_HELP[subcommand] ?? FOLDER_HELP);
870
+ return;
871
+ }
872
+ switch (subcommand) {
873
+ case "list": return runFolderList(rest);
874
+ case "create": return runFolderCreate(rest);
875
+ case "rename": return runFolderRename(rest);
876
+ case "move": return runFolderMove(rest);
877
+ case "delete": return runFolderDelete(rest);
878
+ default: throw new CliUsageError(`Unknown subcommand "folder ${subcommand}" — run \`fundus folder --help\`.`);
879
+ }
880
+ }
881
+ const MANIFEST_HELP = `fundus manifest — manage manifests (delivery units & preload groups)
882
+
883
+ Usage: fundus manifest <subcommand> …
884
+
885
+ A manifest is a delivery unit and a preload group: only assets in at
886
+ least one manifest (explicitly, or passively via the reference closure)
887
+ are processed into proxies and reachable from host code. Assets join and
888
+ leave manifests via \`fundus asset set <id> --manifests <a,b>\`. All
889
+ subcommands accept --json and --help.
890
+
891
+ Subcommands:
892
+ list list manifests with members and delivered bytes
893
+ create create an empty manifest (fundus manifest create <name>)
894
+ set set a manifest's editor color / emoji appearance
895
+ palette list the selectable appearance colors and emojis
896
+ rename rename a manifest (fundus manifest rename <name> <newName>)
897
+ delete delete a manifest and strip its memberships
898
+
899
+ Run \`fundus manifest <subcommand> --help\` for the focused page.
900
+ `;
901
+ const MANIFEST_SUBCOMMAND_HELP = {
902
+ list: `fundus manifest list — list manifests
903
+
904
+ Usage: fundus manifest list [--json]
905
+
906
+ One line per manifest: name, delivered bytes (the preload-budget
907
+ number, over the full delivery set), explicit members, and passive
908
+ members (assets pulled in via the reference closure, with via-
909
+ attribution in the JSON output).
910
+
911
+ Flags:
912
+ --json print {"ok": true, "manifests": [{"name", "color", "emoji",
913
+ "explicitMemberIds", "passiveMembers", "deliveredBytes"}, …]}
914
+ --help this help
915
+
916
+ Read-only: nothing is processed or written.
917
+
918
+ Example:
919
+ fundus manifest list --json
920
+ `,
921
+ create: `fundus manifest create — create an empty manifest
922
+
923
+ Usage: fundus manifest create <name> [--json]
924
+
925
+ <name> must be a camelCase identifier; its kebab-case form names the
926
+ generated module (mainCatalog → main-catalog.generated.ts). Mutations run
927
+ the autosave chain, so the module is generated immediately.
928
+
929
+ Flags:
930
+ --json print {"ok": true, "manifest": {…}, "issues": […]}
931
+ --help this help
932
+
933
+ Example:
934
+ fundus manifest create mainCatalog --json
935
+ `,
936
+ set: `fundus manifest set — set a manifest's editor appearance
937
+
938
+ Usage: fundus manifest set <name> [--color <name|hex|none>] [--emoji <emoji|none>] [--json]
939
+
940
+ Tags the manifest with a color and/or an emoji, shown on its label
941
+ throughout the editor UI. Both are chosen from the fixed sets below;
942
+ pass "none" (or an empty string) to clear either. At least one of
943
+ --color/--emoji is required. Runs the autosave chain.
944
+
945
+ Colors (name or #hex):
946
+ ${MANIFEST_COLORS.map((color) => `${color.name} (${color.value})`).join(", ")}
947
+
948
+ Emojis:
949
+ ${MANIFEST_EMOJIS.join(" ")}
950
+
951
+ Flags:
952
+ --color <name|hex|none> palette name ("blue"), hex value ("#3b82f6"),
953
+ or "none"/"" to clear
954
+ --emoji <emoji|none> one of the emojis above, or "none"/"" to clear
955
+ --json print the created/updated manifest row + issues
956
+ --help this help
957
+
958
+ Example:
959
+ fundus manifest set mainCatalog --color blue --emoji 🗂️ --json
960
+ `,
961
+ palette: `fundus manifest palette — list the selectable appearance colors and emojis
962
+
963
+ Usage: fundus manifest palette [--json]
964
+
965
+ The closed sets \`fundus manifest set\` validates against. Read this first
966
+ to discover valid --color / --emoji values programmatically instead of
967
+ guessing.
968
+
969
+ Flags:
970
+ --json print {"ok": true, "colors": [{"name", "value"}, …], "emojis": […]}
971
+ --help this help
972
+
973
+ Read-only: nothing is processed or written.
974
+
975
+ Example:
976
+ fundus manifest palette --json | jq '.emojis'
977
+ `,
978
+ rename: `fundus manifest rename — rename a manifest
979
+
980
+ Usage: fundus manifest rename <name> <newName> [--json]
981
+
982
+ <newName> must be a camelCase identifier whose kebab-case form is
983
+ distinct from every other manifest's. The rename keeps the manifest's
984
+ appearance, rewrites every asset's membership, and replaces the
985
+ generated module (the old file is removed, the new one written). Host
986
+ code importing the module must be updated by hand — both the module
987
+ file name and the exported const change with the manifest name
988
+ (mainCatalog → main-catalog.generated.ts / \`mainCatalog\`).
989
+
990
+ Flags:
991
+ --json print {"ok": true, "manifest": {…}, "issues": […]}
992
+ --help this help
993
+
994
+ Example:
995
+ fundus manifest rename mainCatalog primaryCatalog --json
996
+ `,
997
+ delete: `fundus manifest delete — delete a manifest
998
+
999
+ Usage: fundus manifest delete <name> [--json]
1000
+
1001
+ Deletes the manifest, strips it from every asset's membership, and
1002
+ removes its generated module. Assets left in no manifest stop being
1003
+ delivered (they warn as unreachable).
1004
+
1005
+ Flags:
1006
+ --json print {"ok": true, "deletedManifestName": "<name>", "issues": […]}
1007
+ --help this help
1008
+
1009
+ Example:
1010
+ fundus manifest delete obsoleteShelf --json
1011
+ `
1012
+ };
1013
+ async function runManifestList(argumentList) {
1014
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus manifest list");
1015
+ requirePositionals(parsed, [], "fundus manifest list");
1016
+ const manifests = manifestRows(await (await createOperations()).state());
1017
+ if (parsed.flags.json) {
1018
+ printJsonDocument({
1019
+ ok: true,
1020
+ manifests
1021
+ });
1022
+ return;
1023
+ }
1024
+ if (manifests.length === 0) {
1025
+ console.log("No manifests.");
1026
+ return;
1027
+ }
1028
+ const labels = manifests.map(manifestLabel);
1029
+ const nameWidth = Math.max(0, ...labels.map((label) => label.length));
1030
+ manifests.forEach((row, index) => {
1031
+ const passiveSuffix = row.passiveMembers.length > 0 ? ` + passive: ${row.passiveMembers.map((member) => member.assetId).join(", ")}` : "";
1032
+ const colorSuffix = row.color ? ` ${row.color}` : "";
1033
+ console.log(`${labels[index].padEnd(nameWidth)} ${formatBytes(row.deliveredBytes).padStart(9)} ${row.explicitMemberIds.join(", ") || "(empty)"}${passiveSuffix}${colorSuffix}`);
1034
+ });
1035
+ }
1036
+ async function runManifestCreate(argumentList) {
1037
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus manifest create");
1038
+ const [name] = requirePositionals(parsed, ["<name>"], "fundus manifest create");
1039
+ const state = await (await createOperations()).createManifest(name);
1040
+ const manifest = manifestRows(state).find((row) => row.name === name);
1041
+ if (parsed.flags.json) {
1042
+ printJsonDocument({
1043
+ ok: true,
1044
+ manifest,
1045
+ issues: state.issues
1046
+ });
1047
+ return;
1048
+ }
1049
+ printMutationResult(`Created manifest "${name}".`, null, state);
1050
+ }
1051
+ /** Resolve a `--color`/`--emoji` flag value to a set/clear/omit intent. */
1052
+ function resolveAppearanceFlag(value, resolve, describe) {
1053
+ if (typeof value !== "string") return void 0;
1054
+ const token = value.trim();
1055
+ if (token === "" || token.toLowerCase() === "none") return null;
1056
+ const resolved = resolve(token);
1057
+ if (resolved === null) throw new Error(describe(token));
1058
+ return resolved;
1059
+ }
1060
+ async function runManifestSet(argumentList) {
1061
+ const parsed = parseArguments(argumentList, {
1062
+ ...COMMON_FLAGS,
1063
+ color: { takesValue: true },
1064
+ emoji: { takesValue: true }
1065
+ }, "fundus manifest set");
1066
+ const [name] = requirePositionals(parsed, ["<name>"], "fundus manifest set");
1067
+ if (parsed.flags.color === void 0 && parsed.flags.emoji === void 0) throw new CliUsageError("`fundus manifest set` needs --color and/or --emoji — run `fundus manifest set --help`.");
1068
+ const color = resolveAppearanceFlag(parsed.flags.color, resolveManifestColor, (token) => `"${token}" is not a selectable manifest color — choose one of: ${MANIFEST_COLORS.map((entry) => `${entry.name} (${entry.value})`).join(", ")}, or "none".`);
1069
+ const emoji = resolveAppearanceFlag(parsed.flags.emoji, (token) => manifestEmojiError(token) === null ? token : null, (token) => manifestEmojiError(token) ?? `"${token}" is not a selectable manifest emoji.`);
1070
+ const state = await (await createOperations()).setManifestAppearance(name, {
1071
+ color,
1072
+ emoji
1073
+ });
1074
+ const manifest = manifestRows(state).find((row) => row.name === name);
1075
+ if (parsed.flags.json) {
1076
+ printJsonDocument({
1077
+ ok: true,
1078
+ manifest,
1079
+ issues: state.issues
1080
+ });
1081
+ return;
1082
+ }
1083
+ printMutationResult(`Updated manifest "${name}" appearance.`, null, state);
1084
+ }
1085
+ async function runManifestPalette(argumentList) {
1086
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus manifest palette");
1087
+ requirePositionals(parsed, [], "fundus manifest palette");
1088
+ if (parsed.flags.json) {
1089
+ printJsonDocument({
1090
+ ok: true,
1091
+ colors: MANIFEST_COLORS,
1092
+ emojis: MANIFEST_EMOJIS
1093
+ });
1094
+ return;
1095
+ }
1096
+ console.log("Colors:");
1097
+ const nameWidth = Math.max(0, ...MANIFEST_COLORS.map((color) => color.name.length));
1098
+ for (const color of MANIFEST_COLORS) console.log(` ${color.name.padEnd(nameWidth)} ${color.value}`);
1099
+ console.log("");
1100
+ console.log(`Emojis (${MANIFEST_EMOJIS.length}):`);
1101
+ console.log(` ${MANIFEST_EMOJIS.join(" ")}`);
1102
+ }
1103
+ async function runManifestRename(argumentList) {
1104
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus manifest rename");
1105
+ const [name, newName] = requirePositionals(parsed, ["<name>", "<newName>"], "fundus manifest rename");
1106
+ const state = await (await createOperations()).renameManifest(name, newName);
1107
+ const manifest = manifestRows(state).find((row) => row.name === newName);
1108
+ if (parsed.flags.json) {
1109
+ printJsonDocument({
1110
+ ok: true,
1111
+ manifest,
1112
+ issues: state.issues
1113
+ });
1114
+ return;
1115
+ }
1116
+ printMutationResult(`Renamed manifest "${name}" to "${newName}" — update host imports of the generated module.`, null, state);
1117
+ }
1118
+ async function runManifestDelete(argumentList) {
1119
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus manifest delete");
1120
+ const [name] = requirePositionals(parsed, ["<name>"], "fundus manifest delete");
1121
+ const state = await (await createOperations()).deleteManifest(name);
1122
+ if (parsed.flags.json) {
1123
+ printJsonDocument({
1124
+ ok: true,
1125
+ deletedManifestName: name,
1126
+ issues: state.issues
1127
+ });
1128
+ return;
1129
+ }
1130
+ printMutationResult(`Deleted manifest "${name}" and stripped it from every asset.`, null, state);
1131
+ }
1132
+ async function runManifestCommand(argumentList) {
1133
+ const [subcommand, ...rest] = argumentList;
1134
+ if (subcommand === void 0 || subcommand === "--help") {
1135
+ console.log(MANIFEST_HELP);
1136
+ if (subcommand === void 0) throw new CliUsageError("`fundus manifest` needs a subcommand: list, create, set, palette, rename, delete.");
1137
+ return;
1138
+ }
1139
+ if (hasHelpFlag(rest)) {
1140
+ console.log(MANIFEST_SUBCOMMAND_HELP[subcommand] ?? MANIFEST_HELP);
1141
+ return;
1142
+ }
1143
+ switch (subcommand) {
1144
+ case "list": return runManifestList(rest);
1145
+ case "create": return runManifestCreate(rest);
1146
+ case "set": return runManifestSet(rest);
1147
+ case "palette": return runManifestPalette(rest);
1148
+ case "rename": return runManifestRename(rest);
1149
+ case "delete": return runManifestDelete(rest);
1150
+ default: throw new CliUsageError(`Unknown subcommand "manifest ${subcommand}" — run \`fundus manifest --help\`.`);
1151
+ }
1152
+ }
1153
+ const BUILD_HELP = `fundus build — run the headless pipeline
1154
+
1155
+ Usage: fundus build [--json]
1156
+
1157
+ Validates the library, processes every stale or missing proxy, prunes
1158
+ orphaned proxies, and regenerates all manifest modules. Deterministic
1159
+ and idempotent: a second run on an unchanged project processes nothing
1160
+ and rewrites byte-identical modules. Run it in CI, postinstall, or
1161
+ before the host build; commit the result.
1162
+
1163
+ Flags:
1164
+ --json print {"ok": true, "processed": […], "upToDate": […],
1165
+ "prunedProxies": […], "modulesWritten": […],
1166
+ "modulesRemoved": […]} — asset ids for processing,
1167
+ file names for pruning/codegen
1168
+ --help this help
1169
+
1170
+ A validation failure processes nothing and exits 1 (with --json:
1171
+ {"error": {"message": …}}).
1172
+
1173
+ Example:
1174
+ fundus build --json | jq '.processed'
1175
+ `;
1176
+ async function runBuildCommand(argumentList) {
1177
+ if (hasHelpFlag(argumentList)) {
1178
+ console.log(BUILD_HELP);
1179
+ return;
1180
+ }
1181
+ const parsed = parseArguments(argumentList, COMMON_FLAGS, "fundus build");
1182
+ requirePositionals(parsed, [], "fundus build");
1183
+ const { processing, codegen } = await runBuild(process.cwd());
1184
+ if (parsed.flags.json) {
1185
+ printJsonDocument({
1186
+ ok: true,
1187
+ processed: processing.processed,
1188
+ upToDate: processing.reused,
1189
+ prunedProxies: processing.pruned,
1190
+ modulesWritten: codegen.written,
1191
+ modulesRemoved: codegen.removed
1192
+ });
1193
+ return;
1194
+ }
1195
+ console.log(`fundus build: ${processing.processed.length} processed, ${processing.reused.length} up to date, ${processing.pruned.length} pruned; ${codegen.written.length} manifest module(s) written` + (codegen.removed.length > 0 ? `, ${codegen.removed.length} removed` : "") + ".");
1196
+ }
1197
+ const CHECK_HELP = `fundus check — verify the committed state (the CI gate)
1198
+
1199
+ Usage: fundus check [--allow-warnings] [--json]
1200
+
1201
+ Verify-only, no writes: the library must validate, every delivered
1202
+ asset's committed proxy must exist under its expected name (staleness
1203
+ is a name check), the proxy directory must hold no orphans, and every
1204
+ generated manifest module must be byte-identical to an in-memory
1205
+ regeneration. Warnings (unreachable asset, raw-hash drift) fail the
1206
+ check too unless --allow-warnings downgrades them; errors never
1207
+ downgrade.
1208
+
1209
+ Flags:
1210
+ --allow-warnings exit 0 despite warnings (errors still fail)
1211
+ --json print one report document:
1212
+ {"ok": <pass/fail>, "assetCount": …, "manifestCount": …,
1213
+ "errors": [<finding>, …], "warnings": [<finding>, …]}
1214
+ where a finding is {"kind", "message"} plus
1215
+ "assetId" / "manifest" / "fileName" where applicable
1216
+ --help this help
1217
+
1218
+ Exit codes: 0 = clean (or warnings allowed), 1 = failed. A failure to
1219
+ even run the check (e.g. no fundus.config.ts) exits 1 with the usual
1220
+ {"error": {"message": …}} envelope under --json.
1221
+
1222
+ Example:
1223
+ fundus check --json | jq '.errors[].kind'
1224
+ `;
1225
+ async function runCheckCommand(argumentList) {
1226
+ if (hasHelpFlag(argumentList)) {
1227
+ console.log(CHECK_HELP);
1228
+ return;
1229
+ }
1230
+ const parsed = parseArguments(argumentList, {
1231
+ ...COMMON_FLAGS,
1232
+ "allow-warnings": { takesValue: false }
1233
+ }, "fundus check");
1234
+ requirePositionals(parsed, [], "fundus check");
1235
+ const allowWarnings = parsed.flags["allow-warnings"] === true;
1236
+ const report = await runCheck(process.cwd());
1237
+ const warningsFatal = report.warnings.length > 0 && !allowWarnings;
1238
+ const failed = report.errors.length > 0 || warningsFatal;
1239
+ if (parsed.flags.json) {
1240
+ printJsonDocument({
1241
+ ok: !failed,
1242
+ assetCount: report.assetCount,
1243
+ manifestCount: report.manifestCount,
1244
+ errors: report.errors,
1245
+ warnings: report.warnings
1246
+ });
1247
+ if (failed) process.exitCode = 1;
1248
+ return;
1249
+ }
1250
+ for (const error of report.errors) console.error(`✗ ${formatCheckFinding(error)}`);
1251
+ for (const warning of report.warnings) console.error(`⚠ ${formatCheckFinding(warning)}`);
1252
+ if (failed) {
1253
+ console.error(`fundus check failed: ${report.errors.length} error(s), ${report.warnings.length} warning(s)` + (warningsFatal ? " (warnings are errors — pass --allow-warnings to downgrade them)" : "") + ".");
1254
+ process.exitCode = 1;
1255
+ return;
1256
+ }
1257
+ console.log(`fundus check: ${report.assetCount} asset(s), ${report.manifestCount} manifest(s) — clean` + (report.warnings.length > 0 ? ` (${report.warnings.length} allowed warning(s))` : "") + ".");
1258
+ }
1259
+ //#endregion
1260
+ //#region src/cli.ts
1261
+ /**
1262
+ * The fundus CLI: the full asset lifecycle drivable headlessly.
1263
+ * `init` creates the host config, `start` serves the prebuilt editor (+ API),
1264
+ * `build` runs the headless pipeline, `check` verifies without writing (the CI
1265
+ * gate), and the `state` / `asset` / `manifest` command groups let agents and
1266
+ * scripts ingest, inspect, mutate, and deliver assets without the editor UI.
1267
+ * The published binary is compiled ESM and runs directly on Node.js.
1268
+ *
1269
+ * Exit-code contract (agents rely on it):
1270
+ * 0 success
1271
+ * 1 expected error — bad invocation, unknown id, invalid parameters,
1272
+ * validation failure; with --json, stdout carries a single
1273
+ * {"error": {"message": …}} document, without it the message goes to
1274
+ * stderr
1275
+ * 2 unexpected crash (stack trace on stderr)
1276
+ */
1277
+ const ROOT_HELP = `fundus — asset management for SvelteKit hosts
1278
+
1279
+ Usage: fundus <command> … (run in the host project root, next to fundus.config.ts)
1280
+
1281
+ Setup:
1282
+ init create a template fundus.config.ts
1283
+
1284
+ Inspect:
1285
+ state the full project state: assets, manifests,
1286
+ memberships, proxies, references, warnings
1287
+ asset list list assets (--manifest/--type/--warnings-only)
1288
+ asset show <id> one asset in detail
1289
+ manifest list manifests with members and delivered bytes
1290
+ folder list persisted raw/proxy folders with direct assets
1291
+
1292
+ Mutate (each runs the editor autosave chain: write library → process
1293
+ affected assets → regenerate manifest modules):
1294
+ asset ingest <file> copy a file into the raw dir as a typed asset
1295
+ (--type, --id, --manifests, --parameters)
1296
+ asset set <id> merge parameters and/or replace manifest
1297
+ membership (--parameters, --manifests)
1298
+ asset rename <id> <newId> rename an asset id
1299
+ asset move <id> <folder|root> move an asset without changing its id
1300
+ asset delete <id> delete an asset (record, raw file, proxies)
1301
+ folder create <path> create a mirrored persisted folder
1302
+ folder rename <path> <name> rename a folder and its subtree
1303
+ folder move <path> <parent|root> move a folder and its subtree
1304
+ folder delete <path> --recursive delete a subtree and all of its assets
1305
+ manifest create <name> create a manifest
1306
+ manifest rename <name> <newName> rename a manifest (host imports of the
1307
+ generated module must be updated by hand)
1308
+ manifest delete <name> delete a manifest and strip memberships
1309
+
1310
+ Pipeline:
1311
+ start serve the prebuilt editor and API
1312
+ build process stale proxies, prune orphans,
1313
+ regenerate all manifest modules
1314
+ check verify without writing: validation, proxies
1315
+ current, modules in sync; warnings are errors
1316
+ unless --allow-warnings
1317
+
1318
+ Global flags:
1319
+ --json machine-readable output: exactly one JSON document on stdout
1320
+ ({"ok": true, …} on success, {"error": {"message": …}} on an
1321
+ expected error) — supported by every command except start;
1322
+ \`check --json\` prints its pass/fail report with the failure
1323
+ list ({"ok": false, "errors": […], …})
1324
+ --help this help; every command group and subcommand has its own
1325
+ focused page (e.g. \`fundus asset ingest --help\`)
1326
+ --version print the fundus version
1327
+
1328
+ Exit codes: 0 success · 1 expected error · 2 unexpected crash.
1329
+
1330
+ Typical agent lifecycle:
1331
+ fundus manifest create mainCatalog --json
1332
+ fundus asset ingest ./exports/navigation_panel@2x.png --type slice --json
1333
+ fundus asset set navigationPanel --parameters '{"mode": "three-horizontal"}' \\
1334
+ --manifests mainCatalog --json
1335
+ fundus check
1336
+ `;
1337
+ function printUsage() {
1338
+ console.log(ROOT_HELP);
1339
+ }
1340
+ async function printVersion() {
1341
+ const packageJson = JSON.parse(await readFile(join(import.meta.dirname, "../package.json"), "utf8"));
1342
+ console.log(`fundus ${packageJson.version ?? "unknown"}`);
1343
+ }
1344
+ /**
1345
+ * Expected errors (exit 1) are deliberate `Error` throws from the operations
1346
+ * layer and the CLI's own usage checks. Built-in error types that code throws
1347
+ * on its own (TypeError, ReferenceError, …) and non-Error throws signal a bug
1348
+ * — those crash with exit 2. A site that surfaces e.g. a JSON.parse
1349
+ * SyntaxError as an expected condition must rethrow it as a plain Error
1350
+ * (cli-io.ts's parseJsonFlag rethrows a CliUsageError).
1351
+ */
1352
+ function isUnexpectedCrash(cause) {
1353
+ if (!(cause instanceof Error)) return true;
1354
+ if (cause instanceof CliUsageError) return false;
1355
+ return cause instanceof TypeError || cause instanceof RangeError || cause instanceof ReferenceError || cause instanceof SyntaxError;
1356
+ }
1357
+ async function runCommandGroup(run) {
1358
+ const jsonOutput = process.argv.includes("--json");
1359
+ try {
1360
+ await run();
1361
+ } catch (cause) {
1362
+ if (isUnexpectedCrash(cause)) {
1363
+ console.error(cause instanceof Error ? cause.stack ?? cause.message : String(cause));
1364
+ process.exit(2);
1365
+ }
1366
+ const message = cause.message;
1367
+ if (jsonOutput) printJsonDocument({ error: { message } });
1368
+ else console.error(`fundus: ${message}`);
1369
+ process.exit(1);
1370
+ }
1371
+ }
1372
+ const START_HELP = `fundus start — serve the prebuilt editor and API
1373
+
1374
+ Usage: fundus start
1375
+
1376
+ Serves the editor SPA and the fs/processing REST API for the project
1377
+ this is run in (the directory containing fundus.config.ts). The library
1378
+ is re-read per request; mutations regenerate their derived artifacts.
1379
+ The editor is always served from the prebuilt SPA shipped in the npm package.
1380
+ The command fails if that build is missing. Stop with Ctrl-C.
1381
+
1382
+ Every edit autosaves: write library → process affected assets →
1383
+ regenerate manifest modules — the same chain the CLI mutations run.
1384
+
1385
+ Flags:
1386
+ --help this help
1387
+
1388
+ Example:
1389
+ cd my-host-app && npx fundus start
1390
+ `;
1391
+ const command = process.argv[2];
1392
+ const commandArguments = process.argv.slice(3);
1393
+ if (command === "init") await runCommandGroup(() => runInitCommand(commandArguments));
1394
+ else if (command === "build") await runCommandGroup(() => runBuildCommand(commandArguments));
1395
+ else if (command === "check") await runCommandGroup(() => runCheckCommand(commandArguments));
1396
+ else if (command === "start") await runCommandGroup(async () => {
1397
+ const parsed = parseArguments(commandArguments, { help: { takesValue: false } }, "fundus start");
1398
+ requirePositionals(parsed, [], "fundus start");
1399
+ if (parsed.flags.help) {
1400
+ console.log(START_HELP);
1401
+ return;
1402
+ }
1403
+ const { runStart } = await import("./start-D2IXOrni.js");
1404
+ await runStart(process.cwd());
1405
+ });
1406
+ else if (command === "state") await runCommandGroup(() => runStateCommand(commandArguments));
1407
+ else if (command === "asset") await runCommandGroup(() => runAssetCommand(commandArguments));
1408
+ else if (command === "folder") await runCommandGroup(() => runFolderCommand(commandArguments));
1409
+ else if (command === "manifest") await runCommandGroup(() => runManifestCommand(commandArguments));
1410
+ else if (command === "--version") await printVersion();
1411
+ else if (command === void 0) {
1412
+ printUsage();
1413
+ process.exit(1);
1414
+ } else if (command === "help" || command === "--help") printUsage();
1415
+ else {
1416
+ console.error(`fundus: unknown command "${command}" — run \`fundus --help\`.`);
1417
+ process.exit(1);
1418
+ }
1419
+ //#endregion
1420
+ export {};