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.
- package/dist/build-CZYFLLvl.js +191 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1420 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/core/image-Dqmz8FXS.js +54 -0
- package/dist/core/index.d.ts +499 -0
- package/dist/core/index.js +962 -0
- package/dist/core/reference-graph-CjuHbLDw.d.ts +280 -0
- package/dist/core/testing.d.ts +27 -0
- package/dist/core/testing.js +80 -0
- package/dist/editor/200.html +37 -0
- package/dist/editor/_app/immutable/assets/0.DLhIsYA_.css +1 -0
- package/dist/editor/_app/immutable/assets/2.DIbbW7U1.css +1 -0
- package/dist/editor/_app/immutable/assets/3.C-9vFQ9A.css +1 -0
- package/dist/editor/_app/immutable/assets/4.BS8ElpOV.css +1 -0
- package/dist/editor/_app/immutable/assets/ConfirmDialog.CHEISE-L.css +1 -0
- package/dist/editor/_app/immutable/assets/LibraryContextMenu.CDjCmdV-.css +1 -0
- package/dist/editor/_app/immutable/chunks/BfPE5wdt.js +1 -0
- package/dist/editor/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/dist/editor/_app/immutable/chunks/CNRz3sSw.js +3 -0
- package/dist/editor/_app/immutable/chunks/CO4RBJfa.js +1 -0
- package/dist/editor/_app/immutable/chunks/Cx6qtxJg.js +1 -0
- package/dist/editor/_app/immutable/chunks/DjdrdRzT.js +1 -0
- package/dist/editor/_app/immutable/chunks/DjvOiDmq.js +1 -0
- package/dist/editor/_app/immutable/chunks/n7mvYc42.js +5 -0
- package/dist/editor/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/dist/editor/_app/immutable/entry/app.BkqbfI8n.js +2 -0
- package/dist/editor/_app/immutable/entry/start.C8HHOLly.js +1 -0
- package/dist/editor/_app/immutable/nodes/0.xdMI5GEU.js +2 -0
- package/dist/editor/_app/immutable/nodes/1.D9p2tfoJ.js +1 -0
- package/dist/editor/_app/immutable/nodes/2.BrA0Hyzu.js +3 -0
- package/dist/editor/_app/immutable/nodes/3.WO7tZH_d.js +104 -0
- package/dist/editor/_app/immutable/nodes/4.DQtx17aW.js +2 -0
- package/dist/editor/_app/version.json +1 -0
- package/dist/index.d.ts +370 -0
- package/dist/index.js +4 -0
- package/dist/operations-DYM-5KVl.js +1797 -0
- package/dist/runtime/chroma-key-gl.d.ts +23 -0
- package/dist/runtime/chroma-key-gl.js +290 -0
- package/dist/runtime/components/ChromaKeyVideo.svelte +225 -0
- package/dist/runtime/components/Image.svelte +18 -0
- package/dist/runtime/components/Slice.svelte +35 -0
- package/dist/runtime/components/SliceCanvas.svelte +162 -0
- package/dist/runtime/components/SliceDom.svelte +104 -0
- package/dist/runtime/components/Video.svelte +60 -0
- package/dist/runtime/geometry.d.ts +75 -0
- package/dist/runtime/geometry.js +209 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/kind-handlers.d.ts +46 -0
- package/dist/runtime/kind-handlers.js +86 -0
- package/dist/runtime/manifest.d.ts +60 -0
- package/dist/runtime/manifest.js +117 -0
- package/dist/runtime/observe-border-box-size.d.ts +5 -0
- package/dist/runtime/observe-border-box-size.js +12 -0
- package/dist/runtime/preload-registry.d.ts +26 -0
- package/dist/runtime/preload-registry.js +68 -0
- package/dist/start-Bhnqt9Dc.js +279 -0
- package/dist/start-D2IXOrni.js +2 -0
- 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 {};
|