@mulmoclaude/mulmoscript-plugin 4.4.0 → 4.5.1
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/contract-BHqxMUWm.cjs +82 -0
- package/dist/contract-BHqxMUWm.cjs.map +1 -0
- package/dist/contract-BnIl6w_f.js +53 -0
- package/dist/contract-BnIl6w_f.js.map +1 -0
- package/dist/core/contract.d.ts +80 -6
- package/dist/core/contract.d.ts.map +1 -1
- package/dist/core/definition.d.ts.map +1 -1
- package/dist/core/paths.d.ts +41 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/{plugin-BOq5YkTl.cjs → plugin-C5JC2lNR.cjs} +51 -1
- package/dist/plugin-C5JC2lNR.cjs.map +1 -0
- package/dist/{plugin-CAhxJ5WM.js → plugin-Jo8iGv4K.js} +40 -2
- package/dist/plugin-Jo8iGv4K.js.map +1 -0
- package/dist/server/dispatch.d.ts +0 -6
- package/dist/server/dispatch.d.ts.map +1 -1
- package/dist/server/ops.d.ts +29 -18
- package/dist/server/ops.d.ts.map +1 -1
- package/dist/server/types.d.ts +75 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server.cjs +468 -110
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +468 -110
- package/dist/server.js.map +1 -1
- package/dist/style.css +30 -18
- package/dist/vue/View.vue.d.ts.map +1 -1
- package/dist/vue/composables/useDeckEditor.d.ts.map +1 -1
- package/dist/vue/index.d.ts +1 -1
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/subscription.d.ts +16 -0
- package/dist/vue/subscription.d.ts.map +1 -0
- package/dist/vue/transport.d.ts +16 -5
- package/dist/vue/transport.d.ts.map +1 -1
- package/dist/vue.cjs +110 -21
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +110 -21
- package/dist/vue.js.map +1 -1
- package/package.json +11 -11
- package/dist/contract-BhN3yKJC.cjs +0 -36
- package/dist/contract-BhN3yKJC.cjs.map +0 -1
- package/dist/contract-CIt1ZAtt.js +0 -19
- package/dist/contract-CIt1ZAtt.js.map +0 -1
- package/dist/plugin-BOq5YkTl.cjs.map +0 -1
- package/dist/plugin-CAhxJ5WM.js.map +0 -1
package/dist/server.cjs
CHANGED
|
@@ -21,8 +21,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
const require_plugin = require("./plugin-
|
|
25
|
-
const require_contract = require("./contract-
|
|
24
|
+
const require_plugin = require("./plugin-C5JC2lNR.cjs");
|
|
25
|
+
const require_contract = require("./contract-BHqxMUWm.cjs");
|
|
26
26
|
let fs = require("fs");
|
|
27
27
|
let path = require("path");
|
|
28
28
|
path = __toESM(path, 1);
|
|
@@ -42,6 +42,29 @@ async function fileToDataUri(filePath, mimeType) {
|
|
|
42
42
|
return `data:${mimeType};base64,${(await (0, node_fs_promises.readFile)(filePath)).toString("base64")}`;
|
|
43
43
|
}
|
|
44
44
|
//#endregion
|
|
45
|
+
//#region src/server/types.ts
|
|
46
|
+
/**
|
|
47
|
+
* Host capabilities the server ops run against. Only genuinely
|
|
48
|
+
* host-specific transport lives here — the mulmocast orchestration, path
|
|
49
|
+
* containment, and generation-state tracking are all in-package.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Which named-root capabilities a host has NOT wired.
|
|
53
|
+
*
|
|
54
|
+
* Pure and exported so the rule can be tested without building a server: the
|
|
55
|
+
* condition used to key on the root COUNT alone, so a host that HAD passed
|
|
56
|
+
* `artifactsFor` was told at every boot that its writes land in the default
|
|
57
|
+
* root — the opposite of what the code then did (#3022).
|
|
58
|
+
*
|
|
59
|
+
* Reads and uploads need nothing from the host, so they never appear here.
|
|
60
|
+
*/
|
|
61
|
+
function missingRootCapabilities(backend) {
|
|
62
|
+
const missing = [];
|
|
63
|
+
if (backend.rootScopedGenerationState !== true) missing.push("GENERATION is refused until this host declares `rootScopedGenerationState` (its pending-generation state must carry the root, or it must keep none)");
|
|
64
|
+
if (backend.artifactsFor === void 0) missing.push("save/update land in the DEFAULT root until this host passes `artifactsFor`");
|
|
65
|
+
return missing;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
45
68
|
//#region src/server/mulmoErrorCapture.ts
|
|
46
69
|
var capturedErrors = new node_async_hooks.AsyncLocalStorage();
|
|
47
70
|
var loggerInstalled = false;
|
|
@@ -190,11 +213,17 @@ async function withBeatProgress(beats, onBeat, body) {
|
|
|
190
213
|
}
|
|
191
214
|
/** Map identity for the in-flight tracker. JSON array keeps the three
|
|
192
215
|
* fields unambiguous (a human-visible delimiter could collide). */
|
|
193
|
-
function generationMapKey(kind, filePath, key) {
|
|
194
|
-
|
|
216
|
+
function generationMapKey(kind, filePath, key, root) {
|
|
217
|
+
const normalized = require_contract.normalizeRoot(root);
|
|
218
|
+
return normalized === "" ? JSON.stringify([
|
|
195
219
|
kind,
|
|
196
220
|
filePath,
|
|
197
221
|
key
|
|
222
|
+
]) : JSON.stringify([
|
|
223
|
+
kind,
|
|
224
|
+
filePath,
|
|
225
|
+
key,
|
|
226
|
+
normalized
|
|
198
227
|
]);
|
|
199
228
|
}
|
|
200
229
|
/**
|
|
@@ -205,19 +234,60 @@ function generationMapKey(kind, filePath, key) {
|
|
|
205
234
|
function createMulmoScriptServerOps(backend) {
|
|
206
235
|
const log = backend.log ?? NOOP_LOG;
|
|
207
236
|
setMulmoErrorCaptureLogger(log);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
const rootDirs = /* @__PURE__ */ new Map([["", path.default.resolve(backend.storiesDir)]]);
|
|
238
|
+
for (const [id, dir] of Object.entries(backend.extraRoots ?? {})) {
|
|
239
|
+
const trimmed = id.trim();
|
|
240
|
+
if (trimmed === "") throw new Error("mulmoScript: extraRoots key must not be empty — the empty id is reserved for the default stories root");
|
|
241
|
+
if (rootDirs.has(trimmed)) throw new Error(`mulmoScript: extraRoots keys must be distinct after trimming — "${trimmed}" is registered twice`);
|
|
242
|
+
rootDirs.set(trimmed, path.default.resolve(dir));
|
|
243
|
+
}
|
|
244
|
+
warnAboutUnwiredRoots();
|
|
245
|
+
/**
|
|
246
|
+
* Tell a host which named-root capabilities it has not wired — and nothing
|
|
247
|
+
* when it has wired them all.
|
|
248
|
+
*
|
|
249
|
+
* The condition used to be the root COUNT alone, so a host that had passed
|
|
250
|
+
* `artifactsFor` was still told, at every boot, that its writes land in the
|
|
251
|
+
* default root. That is not a stale wording: it is the opposite of what the
|
|
252
|
+
* code then does, read by the hosts that got the wiring RIGHT (#3022, from
|
|
253
|
+
* the consuming host).
|
|
254
|
+
*
|
|
255
|
+
* Silence when nothing is missing, because a warning that always fires is
|
|
256
|
+
* one people learn to skip — and then the host that really did forget
|
|
257
|
+
* `artifactsFor` cannot tell either. Each clause is emitted only when that
|
|
258
|
+
* capability is actually absent, so the message says what is true for THIS
|
|
259
|
+
* host rather than what was true when it was written.
|
|
260
|
+
*/
|
|
261
|
+
function warnAboutUnwiredRoots() {
|
|
262
|
+
if (rootDirs.size <= 1) return;
|
|
263
|
+
const missing = missingRootCapabilities(backend);
|
|
264
|
+
if (missing.length === 0) return;
|
|
265
|
+
log.warn(`extra stories roots registered — reads and uploads work, but ${missing.join(", and ")} (#3019)`, { roots: [...rootDirs.keys()].filter((id) => id !== "") });
|
|
266
|
+
}
|
|
267
|
+
/** The registered directory for a wire `root`, or null when the host never
|
|
268
|
+
* registered it. Null is a REJECTION, not a fallback to the default: an
|
|
269
|
+
* unknown root must not quietly read the workspace's file of the same
|
|
270
|
+
* name. */
|
|
271
|
+
function rootDir(root) {
|
|
272
|
+
return rootDirs.get(require_contract.normalizeRoot(root)) ?? null;
|
|
273
|
+
}
|
|
274
|
+
function toStoryRef(absolutePath, root) {
|
|
275
|
+
const dir = rootDir(root);
|
|
276
|
+
if (dir === null) return null;
|
|
277
|
+
const base = ensureStoriesReal(root) ?? dir;
|
|
278
|
+
return require_plugin.storyRefWithin(base, absolutePath, path.default);
|
|
279
|
+
}
|
|
280
|
+
const storiesRealCache = /* @__PURE__ */ new Map();
|
|
281
|
+
function ensureStoriesReal(root) {
|
|
282
|
+
const dir = rootDir(root);
|
|
283
|
+
if (dir === null) return null;
|
|
284
|
+
const cached = storiesRealCache.get(dir);
|
|
285
|
+
if (cached) return cached;
|
|
217
286
|
try {
|
|
218
|
-
(0, fs.mkdirSync)(
|
|
219
|
-
|
|
220
|
-
|
|
287
|
+
if (require_contract.normalizeRoot(root) === "") (0, fs.mkdirSync)(dir, { recursive: true });
|
|
288
|
+
const real = (0, fs.realpathSync)(dir);
|
|
289
|
+
storiesRealCache.set(dir, real);
|
|
290
|
+
return real;
|
|
221
291
|
} catch {
|
|
222
292
|
return null;
|
|
223
293
|
}
|
|
@@ -233,8 +303,9 @@ function createMulmoScriptServerOps(backend) {
|
|
|
233
303
|
* stories/ is a regular directory or a legitimate symlink to another
|
|
234
304
|
* location. ENOENT and traversal are distinguished (404 vs 400).
|
|
235
305
|
*/
|
|
236
|
-
function resolveStory(filePath) {
|
|
237
|
-
|
|
306
|
+
function resolveStory(filePath, root) {
|
|
307
|
+
if (rootDir(root) === null) return opBadRequest("Unknown stories root");
|
|
308
|
+
const storiesReal = ensureStoriesReal(root);
|
|
238
309
|
if (!storiesReal) return opServerError("stories directory not available");
|
|
239
310
|
if (path.default.isAbsolute(filePath)) return opBadRequest("Invalid filePath");
|
|
240
311
|
const ARTIFACTS_STORIES = "artifacts/stories";
|
|
@@ -254,6 +325,19 @@ function createMulmoScriptServerOps(backend) {
|
|
|
254
325
|
};
|
|
255
326
|
}
|
|
256
327
|
/**
|
|
328
|
+
* Whether this root is one the host registered.
|
|
329
|
+
*
|
|
330
|
+
* Every other root-aware op learns this from `resolveStory`, which needs a
|
|
331
|
+
* file. `pendingGenerations` needs none — it only filters an in-memory map —
|
|
332
|
+
* so an unregistered root produced `{ ok: true, pending: [] }`, and a host
|
|
333
|
+
* typo or a stale card read back as "no work is running" (Codex P2 on
|
|
334
|
+
* #3015). An unknown root is a question this package cannot answer, and the
|
|
335
|
+
* answer it must not give is a confident empty one.
|
|
336
|
+
*/
|
|
337
|
+
function guardStoryRootRegistered(root) {
|
|
338
|
+
return rootDir(root) === null ? opBadRequest(`unknown stories root "${require_contract.normalizeRoot(root)}"`) : null;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
257
341
|
* Realpath containment pre-guard for wire paths handed to the phase-1
|
|
258
342
|
* core's save/reopen/update executes. The core's own path guard is
|
|
259
343
|
* lexical (it runs against the generic FileOps, whose read/write follows
|
|
@@ -265,11 +349,121 @@ function createMulmoScriptServerOps(backend) {
|
|
|
265
349
|
* validation (including the script-vs-filePath mode check) belongs to
|
|
266
350
|
* the core.
|
|
267
351
|
*/
|
|
268
|
-
function guardStoryWirePath(filePath) {
|
|
352
|
+
function guardStoryWirePath(filePath, root) {
|
|
269
353
|
if (typeof filePath !== "string" || filePath === "") return null;
|
|
270
|
-
const resolved = resolveStory(filePath);
|
|
354
|
+
const resolved = resolveStory(filePath, root);
|
|
271
355
|
return resolved.ok ? null : resolved;
|
|
272
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Whether a GENERATION may run in this root.
|
|
359
|
+
*
|
|
360
|
+
* The pair identity is complete inside this package but stops at the host
|
|
361
|
+
* boundary: a host's per-session store keys pending work by
|
|
362
|
+
* `(kind, filePath, key)` — `generationKey` in `@mulmobridge/protocol`,
|
|
363
|
+
* which bridges also consume. Two roots running the same generation in one
|
|
364
|
+
* session would collapse to one entry, and either completion would clear the
|
|
365
|
+
* other root's indicator (Codex P1 on #3015).
|
|
366
|
+
*
|
|
367
|
+
* Whose hazard it is decides who answers: a host declares
|
|
368
|
+
* `rootScopedGenerationState` when its own pending state carries the root —
|
|
369
|
+
* or when it keeps none, which is MulmoTerminal's case. It was refused for a
|
|
370
|
+
* collision it cannot have (#3019).
|
|
371
|
+
*
|
|
372
|
+
* Absent still refuses, so a host that has not thought about this keeps the
|
|
373
|
+
* shipped behaviour rather than being quietly opened up.
|
|
374
|
+
*/
|
|
375
|
+
function guardStoryGenerationRoot(root) {
|
|
376
|
+
if (require_contract.normalizeRoot(root) === "") return null;
|
|
377
|
+
const registered = guardStoryRootRegistered(root);
|
|
378
|
+
if (registered) return registered;
|
|
379
|
+
if (backend.rootScopedGenerationState === true) return null;
|
|
380
|
+
return opBadRequest("generating in a non-default stories root is not supported yet");
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The write half of the same fail-closed rule.
|
|
384
|
+
*
|
|
385
|
+
* It lives in the ops that write rather than at their dispatch sites,
|
|
386
|
+
* because a per-site guard is a list someone has to remember to extend:
|
|
387
|
+
* `save` / `updateBeat` / `updateScript` were guarded and the two upload
|
|
388
|
+
* kinds were not, so an image could still land in a named root
|
|
389
|
+
* (CodeRabbit on #3015). `save` / `update* ` cannot follow suit — they run
|
|
390
|
+
* through the package executors, not through these ops — so
|
|
391
|
+
* `test_server_roots.ts` walks the whole ops surface and fails on any op
|
|
392
|
+
* that is neither in the read-only allowlist nor refusing.
|
|
393
|
+
*/
|
|
394
|
+
/**
|
|
395
|
+
* Whether a WRITE may target this root.
|
|
396
|
+
*
|
|
397
|
+
* Reads are root-aware; writes are not. `executeMulmoScriptSave` and the
|
|
398
|
+
* update executors run against one `FileOps`, bound by the host to the
|
|
399
|
+
* default root, so a write naming another root would rewrite the DEFAULT
|
|
400
|
+
* root's identically-named file and then announce the other one as changed
|
|
401
|
+
* (#3015 review G1). Closing it was fail-closed: "readable but not yet
|
|
402
|
+
* writable" beats "wrote somewhere else and said so".
|
|
403
|
+
*
|
|
404
|
+
* It opens per root, not globally: the host answers `artifactsFor` for the
|
|
405
|
+
* roots it can serve, and a root it cannot is still refused. A host that
|
|
406
|
+
* wires nothing keeps the shipped refusal (#3019).
|
|
407
|
+
*/
|
|
408
|
+
function guardStoryWriteRoot(root) {
|
|
409
|
+
if (require_contract.normalizeRoot(root) === "") return null;
|
|
410
|
+
const registered = guardStoryRootRegistered(root);
|
|
411
|
+
if (registered) return registered;
|
|
412
|
+
return artifactsForRoot(root) === null ? opBadRequest("writing to a non-default stories root is not supported yet") : null;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* The FileOps a write to this root must go through.
|
|
416
|
+
*
|
|
417
|
+
* The default root keeps the single `artifacts` it always had, byte for
|
|
418
|
+
* byte. A named root is served only when the host both REGISTERED it
|
|
419
|
+
* (`extraRoots` — the containment boundary, so a host cannot widen the
|
|
420
|
+
* addressable set through this back door) and can supply a FileOps for it.
|
|
421
|
+
*/
|
|
422
|
+
function artifactsForRoot(root) {
|
|
423
|
+
const normalized = require_contract.normalizeRoot(root);
|
|
424
|
+
if (normalized === "") return backend.artifacts;
|
|
425
|
+
if (rootDir(normalized) === null) return null;
|
|
426
|
+
const hostOps = backend.artifactsFor?.(normalized);
|
|
427
|
+
return hostOps === void 0 || hostOps === null ? null : storiesScoped(hostOps);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Address a named root's FileOps the way the READ side addresses it.
|
|
431
|
+
*
|
|
432
|
+
* The two sides disagreed about one path segment, and the disagreement was
|
|
433
|
+
* silent. A read strips `stories/` and resolves under the registered
|
|
434
|
+
* directory (`<root>/<rel>`); a write handed the executors' wire path
|
|
435
|
+
* straight to the host's FileOps, which is rooted at that same registered
|
|
436
|
+
* directory, so the bytes landed in `<root>/stories/<rel>`. `save` then
|
|
437
|
+
* REPORTED SUCCESS and returned a wire path that could not be read back —
|
|
438
|
+
* a card pointing at nothing (#3020 review H1, from the consuming host).
|
|
439
|
+
*
|
|
440
|
+
* Stripping here makes "the directory you registered is the directory your
|
|
441
|
+
* FileOps is rooted at" true, which is what a host writes without being
|
|
442
|
+
* told. The default root is untouched: its FileOps is rooted one level up,
|
|
443
|
+
* at `<workspace>/artifacts`, and is shared with other plugins.
|
|
444
|
+
*
|
|
445
|
+
* A path that is not a stories wire path throws rather than passing
|
|
446
|
+
* through. Everything reaching here has been through `normalizeStoryPath`
|
|
447
|
+
* or `storyFilePath`, so one that has not is a bug — and letting it through
|
|
448
|
+
* would write it somewhere nobody can read, which is the failure this whole
|
|
449
|
+
* wrapper exists to end.
|
|
450
|
+
*/
|
|
451
|
+
function storiesScoped(inner) {
|
|
452
|
+
const within = (wirePath) => {
|
|
453
|
+
const relative = require_plugin.storiesRelativePath(wirePath);
|
|
454
|
+
if (relative === null) throw new Error(`mulmoScript: "${wirePath}" is not a stories path — a named root's FileOps takes stories paths only`);
|
|
455
|
+
return relative;
|
|
456
|
+
};
|
|
457
|
+
return {
|
|
458
|
+
read: async (wirePath) => inner.read(within(wirePath)),
|
|
459
|
+
readBytes: async (wirePath) => inner.readBytes(within(wirePath)),
|
|
460
|
+
write: async (wirePath, data) => inner.write(within(wirePath), data),
|
|
461
|
+
readDir: async (wirePath) => inner.readDir(within(wirePath)),
|
|
462
|
+
stat: async (wirePath) => inner.stat(within(wirePath)),
|
|
463
|
+
exists: async (wirePath) => inner.exists(within(wirePath)),
|
|
464
|
+
unlink: async (wirePath) => inner.unlink(within(wirePath))
|
|
465
|
+
};
|
|
466
|
+
}
|
|
273
467
|
function ffmpegGuard() {
|
|
274
468
|
if (backend.isFfmpegAvailable?.() === false) return {
|
|
275
469
|
ok: false,
|
|
@@ -279,6 +473,13 @@ function createMulmoScriptServerOps(backend) {
|
|
|
279
473
|
return null;
|
|
280
474
|
}
|
|
281
475
|
const inFlightGenerations = /* @__PURE__ */ new Map();
|
|
476
|
+
/** Emit `root` only when it names a non-default one: an event carrying
|
|
477
|
+
* `root: ""` and one carrying nothing must stay indistinguishable to every
|
|
478
|
+
* pre-#3014 consumer. */
|
|
479
|
+
function rootField(root) {
|
|
480
|
+
const normalized = require_contract.normalizeRoot(root);
|
|
481
|
+
return normalized === "" ? {} : { root: normalized };
|
|
482
|
+
}
|
|
282
483
|
/** Tracker state and events key on the canonical `stories/<rel>` wire
|
|
283
484
|
* form: subscribers (the View's pubsub filter, `pendingGenerations`
|
|
284
485
|
* callers) match by exact string, so the accepted alias spellings
|
|
@@ -288,9 +489,23 @@ function createMulmoScriptServerOps(backend) {
|
|
|
288
489
|
function canonicalWirePath(filePath) {
|
|
289
490
|
return require_plugin.normalizeStoryPath(filePath) ?? filePath;
|
|
290
491
|
}
|
|
291
|
-
|
|
492
|
+
/**
|
|
493
|
+
* `error` and `root` travel in an options object, NOT as two trailing
|
|
494
|
+
* optional positionals.
|
|
495
|
+
*
|
|
496
|
+
* They were positional once, and appending `root` to the sixteen call sites
|
|
497
|
+
* put it in `error`'s slot at the nine that pass no error: every start event
|
|
498
|
+
* carried `error: "<root>"`, the root was dropped from the key, the tracker
|
|
499
|
+
* entry was filed under the default root — and because the matching finish
|
|
500
|
+
* DID pass both, its key differed and the entry was never deleted, leaking
|
|
501
|
+
* one row per generation for the life of the process. Two adjacent optional
|
|
502
|
+
* strings cannot be told apart by the type checker, so the shape is the only
|
|
503
|
+
* thing that can prevent it (#3015 review).
|
|
504
|
+
*/
|
|
505
|
+
function publishGeneration(chatSessionId, kind, filePath, key, finished, opts = {}) {
|
|
506
|
+
const { error, root } = opts;
|
|
292
507
|
const wirePath = canonicalWirePath(filePath);
|
|
293
|
-
const mapKey = generationMapKey(kind, wirePath, key);
|
|
508
|
+
const mapKey = generationMapKey(kind, wirePath, key, root);
|
|
294
509
|
const existing = inFlightGenerations.get(mapKey);
|
|
295
510
|
if (finished) {
|
|
296
511
|
if (existing && existing.count > 1) {
|
|
@@ -307,6 +522,7 @@ function createMulmoScriptServerOps(backend) {
|
|
|
307
522
|
kind,
|
|
308
523
|
filePath: wirePath,
|
|
309
524
|
key,
|
|
525
|
+
root: require_contract.normalizeRoot(root),
|
|
310
526
|
count: 1
|
|
311
527
|
});
|
|
312
528
|
}
|
|
@@ -315,7 +531,8 @@ function createMulmoScriptServerOps(backend) {
|
|
|
315
531
|
filePath: wirePath,
|
|
316
532
|
key,
|
|
317
533
|
done: finished,
|
|
318
|
-
...error ? { error } : {}
|
|
534
|
+
...error ? { error } : {},
|
|
535
|
+
...rootField(root)
|
|
319
536
|
};
|
|
320
537
|
backend.onGenerationEvent?.(chatSessionId, event);
|
|
321
538
|
}
|
|
@@ -326,21 +543,31 @@ function createMulmoScriptServerOps(backend) {
|
|
|
326
543
|
* save — reloading there would rebuild the element the caret is in, mid-keystroke. A write
|
|
327
544
|
* from the agent carries no origin, so everyone reloads.
|
|
328
545
|
*/
|
|
329
|
-
function publishScriptChanged(filePath, origin) {
|
|
546
|
+
function publishScriptChanged(filePath, origin, root) {
|
|
330
547
|
backend.onScriptChanged?.({
|
|
331
548
|
filePath: canonicalWirePath(filePath),
|
|
332
|
-
...origin === void 0 ? {} : { origin }
|
|
549
|
+
...origin === void 0 ? {} : { origin },
|
|
550
|
+
...rootField(root)
|
|
333
551
|
});
|
|
334
552
|
}
|
|
335
|
-
/**
|
|
336
|
-
*
|
|
337
|
-
|
|
553
|
+
/**
|
|
554
|
+
* Snapshot of generations currently in flight for one script — the View's
|
|
555
|
+
* mount-time catch-up.
|
|
556
|
+
*
|
|
557
|
+
* Filtered on the PAIR. Filtering on `filePath` alone hands a View watching
|
|
558
|
+
* `repoB/stories/deck.json` the run started for `repoA`'s identically-named
|
|
559
|
+
* deck, and the caller cannot discard it because the returned event would
|
|
560
|
+
* carry no root either (Codex P1 / CodeRabbit on #3015).
|
|
561
|
+
*/
|
|
562
|
+
function pendingGenerations(filePath, root) {
|
|
338
563
|
const wirePath = canonicalWirePath(filePath);
|
|
339
|
-
|
|
564
|
+
const wanted = require_contract.normalizeRoot(root);
|
|
565
|
+
return [...inFlightGenerations.values()].filter((entry) => entry.filePath === wirePath && entry.root === wanted).map(({ kind, key }) => ({
|
|
340
566
|
kind,
|
|
341
567
|
filePath: wirePath,
|
|
342
568
|
key,
|
|
343
|
-
done: false
|
|
569
|
+
done: false,
|
|
570
|
+
...rootField(root)
|
|
344
571
|
}));
|
|
345
572
|
}
|
|
346
573
|
/**
|
|
@@ -352,7 +579,7 @@ function createMulmoScriptServerOps(backend) {
|
|
|
352
579
|
async function runStoryOp(filePath, options, handler, deps = {}) {
|
|
353
580
|
const resolver = deps.resolveStory ?? resolveStory;
|
|
354
581
|
const build = deps.buildContext ?? buildContext;
|
|
355
|
-
const resolved = resolver(filePath);
|
|
582
|
+
const resolved = resolver(filePath, options.root);
|
|
356
583
|
if (!resolved.ok) return resolved;
|
|
357
584
|
try {
|
|
358
585
|
const context = await build(resolved.absolutePath, options.force ?? false);
|
|
@@ -373,8 +600,11 @@ function createMulmoScriptServerOps(backend) {
|
|
|
373
600
|
return opServerError(require_plugin.errorMessage(err));
|
|
374
601
|
}
|
|
375
602
|
}
|
|
376
|
-
async function beatImageOp(filePath, beatIndex) {
|
|
377
|
-
return runStoryOp(filePath, {
|
|
603
|
+
async function beatImageOp(filePath, beatIndex, root) {
|
|
604
|
+
return runStoryOp(filePath, {
|
|
605
|
+
operation: "beat-image",
|
|
606
|
+
root
|
|
607
|
+
}, async ({ context }) => {
|
|
378
608
|
const { imagePath } = (0, mulmocast.getBeatPngImagePath)(context, beatIndex);
|
|
379
609
|
if (!(0, fs.existsSync)(imagePath)) return {
|
|
380
610
|
ok: true,
|
|
@@ -386,9 +616,10 @@ function createMulmoScriptServerOps(backend) {
|
|
|
386
616
|
};
|
|
387
617
|
});
|
|
388
618
|
}
|
|
389
|
-
async function beatAudioOp(filePath, beatIndex) {
|
|
619
|
+
async function beatAudioOp(filePath, beatIndex, root) {
|
|
390
620
|
return runStoryOp(filePath, {
|
|
391
621
|
operation: "beat-audio",
|
|
622
|
+
root,
|
|
392
623
|
onContextMissing: () => ({
|
|
393
624
|
ok: true,
|
|
394
625
|
audio: null
|
|
@@ -410,8 +641,11 @@ function createMulmoScriptServerOps(backend) {
|
|
|
410
641
|
};
|
|
411
642
|
});
|
|
412
643
|
}
|
|
413
|
-
async function beatMovieOp(filePath, beatIndex) {
|
|
414
|
-
return runStoryOp(filePath, {
|
|
644
|
+
async function beatMovieOp(filePath, beatIndex, root) {
|
|
645
|
+
return runStoryOp(filePath, {
|
|
646
|
+
operation: "beat-movie",
|
|
647
|
+
root
|
|
648
|
+
}, async ({ context }) => {
|
|
415
649
|
const { movieFile, soundEffectFile, lipSyncFile } = (0, mulmocast.getBeatMoviePaths)(context, beatIndex);
|
|
416
650
|
const existing = [
|
|
417
651
|
lipSyncFile,
|
|
@@ -421,12 +655,15 @@ function createMulmoScriptServerOps(backend) {
|
|
|
421
655
|
].find((candidate) => (0, fs.existsSync)(candidate));
|
|
422
656
|
return {
|
|
423
657
|
ok: true,
|
|
424
|
-
moviePath: existing ? toStoryRef(existing) : null
|
|
658
|
+
moviePath: existing ? toStoryRef(existing, root) : null
|
|
425
659
|
};
|
|
426
660
|
});
|
|
427
661
|
}
|
|
428
|
-
async function characterImageOp(filePath, key) {
|
|
429
|
-
return runStoryOp(filePath, {
|
|
662
|
+
async function characterImageOp(filePath, key, root) {
|
|
663
|
+
return runStoryOp(filePath, {
|
|
664
|
+
operation: "character-image",
|
|
665
|
+
root
|
|
666
|
+
}, async ({ context }) => {
|
|
430
667
|
const imagePath = (0, mulmocast.getReferenceImagePath)(context, key, "png");
|
|
431
668
|
if (!(0, fs.existsSync)(imagePath)) return {
|
|
432
669
|
ok: true,
|
|
@@ -441,46 +678,51 @@ function createMulmoScriptServerOps(backend) {
|
|
|
441
678
|
/** Shared "output exists and is newer than the source script" gate for
|
|
442
679
|
* movie / PDF status. A stale artifact (script edited after it was
|
|
443
680
|
* generated) reports null so the UI re-offers the Generate button. */
|
|
444
|
-
function freshOutputRef(outputPath, absoluteFilePath) {
|
|
681
|
+
function freshOutputRef(outputPath, absoluteFilePath, root) {
|
|
445
682
|
if (!(0, fs.existsSync)(outputPath)) return null;
|
|
446
683
|
if ((0, fs.statSync)(outputPath).mtimeMs < (0, fs.statSync)(absoluteFilePath).mtimeMs) return null;
|
|
447
|
-
return toStoryRef(outputPath);
|
|
684
|
+
return toStoryRef(outputPath, root);
|
|
448
685
|
}
|
|
449
|
-
async function movieStatusOp(filePath) {
|
|
686
|
+
async function movieStatusOp(filePath, root) {
|
|
450
687
|
return runStoryOp(filePath, {
|
|
451
688
|
operation: "movie-status",
|
|
689
|
+
root,
|
|
452
690
|
onContextMissing: () => ({
|
|
453
691
|
ok: true,
|
|
454
692
|
moviePath: null
|
|
455
693
|
})
|
|
456
694
|
}, async ({ absoluteFilePath, context }) => ({
|
|
457
695
|
ok: true,
|
|
458
|
-
moviePath: freshOutputRef((0, mulmocast.movieFilePath)(context), absoluteFilePath)
|
|
696
|
+
moviePath: freshOutputRef((0, mulmocast.movieFilePath)(context), absoluteFilePath, root)
|
|
459
697
|
}));
|
|
460
698
|
}
|
|
461
|
-
async function pdfStatusOp(filePath) {
|
|
699
|
+
async function pdfStatusOp(filePath, root) {
|
|
462
700
|
return runStoryOp(filePath, {
|
|
463
701
|
operation: "pdf-status",
|
|
702
|
+
root,
|
|
464
703
|
onContextMissing: () => ({
|
|
465
704
|
ok: true,
|
|
466
705
|
pdfPath: null
|
|
467
706
|
})
|
|
468
707
|
}, async ({ absoluteFilePath, context }) => ({
|
|
469
708
|
ok: true,
|
|
470
|
-
pdfPath: freshOutputRef((0, mulmocast.pdfFilePath)(context, PDF_MODE), absoluteFilePath)
|
|
709
|
+
pdfPath: freshOutputRef((0, mulmocast.pdfFilePath)(context, PDF_MODE), absoluteFilePath, root)
|
|
471
710
|
}));
|
|
472
711
|
}
|
|
473
712
|
async function renderBeatOp(args) {
|
|
474
|
-
const { filePath, beatIndex, force, chatSessionId } = args;
|
|
713
|
+
const { filePath, beatIndex, force, chatSessionId, root } = args;
|
|
714
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
715
|
+
if (rootGuard) return rootGuard;
|
|
475
716
|
const ffmpeg = ffmpegGuard();
|
|
476
717
|
if (ffmpeg) return ffmpeg;
|
|
477
718
|
const mapKey = String(beatIndex);
|
|
478
|
-
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, false);
|
|
719
|
+
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, false, { root });
|
|
479
720
|
let genError;
|
|
480
721
|
try {
|
|
481
722
|
const result = await runStoryOp(filePath, {
|
|
482
723
|
force,
|
|
483
|
-
operation: "render-beat"
|
|
724
|
+
operation: "render-beat",
|
|
725
|
+
root
|
|
484
726
|
}, async ({ context }) => {
|
|
485
727
|
await (0, mulmocast.generateBeatImage)({
|
|
486
728
|
index: beatIndex,
|
|
@@ -497,18 +739,24 @@ function createMulmoScriptServerOps(backend) {
|
|
|
497
739
|
if (!result.ok) genError = result.error;
|
|
498
740
|
return result;
|
|
499
741
|
} finally {
|
|
500
|
-
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, true,
|
|
742
|
+
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, true, {
|
|
743
|
+
error: genError,
|
|
744
|
+
root
|
|
745
|
+
});
|
|
501
746
|
}
|
|
502
747
|
}
|
|
503
748
|
async function generateBeatAudioOp(args) {
|
|
504
|
-
const { filePath, beatIndex, force, chatSessionId } = args;
|
|
749
|
+
const { filePath, beatIndex, force, chatSessionId, root } = args;
|
|
750
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
751
|
+
if (rootGuard) return rootGuard;
|
|
505
752
|
const mapKey = String(beatIndex);
|
|
506
|
-
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, false);
|
|
753
|
+
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, false, { root });
|
|
507
754
|
let genError;
|
|
508
755
|
try {
|
|
509
756
|
const result = await runStoryOp(filePath, {
|
|
510
757
|
force,
|
|
511
|
-
operation: "generate-beat-audio"
|
|
758
|
+
operation: "generate-beat-audio",
|
|
759
|
+
root
|
|
512
760
|
}, async ({ context }) => {
|
|
513
761
|
await (0, mulmocast.generateBeatAudio)(beatIndex, context, { settings: process.env });
|
|
514
762
|
const beat = context.studio.script.beats[beatIndex];
|
|
@@ -531,17 +779,23 @@ function createMulmoScriptServerOps(backend) {
|
|
|
531
779
|
if (!result.ok) genError = result.error;
|
|
532
780
|
return result;
|
|
533
781
|
} finally {
|
|
534
|
-
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, true,
|
|
782
|
+
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, true, {
|
|
783
|
+
error: genError,
|
|
784
|
+
root
|
|
785
|
+
});
|
|
535
786
|
}
|
|
536
787
|
}
|
|
537
788
|
async function renderCharacterOp(args) {
|
|
538
|
-
const { filePath, key, force, chatSessionId } = args;
|
|
539
|
-
|
|
789
|
+
const { filePath, key, force, chatSessionId, root } = args;
|
|
790
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
791
|
+
if (rootGuard) return rootGuard;
|
|
792
|
+
publishGeneration(chatSessionId, "characterImage", filePath, key, false, { root });
|
|
540
793
|
let genError;
|
|
541
794
|
try {
|
|
542
795
|
const result = await runStoryOp(filePath, {
|
|
543
796
|
force,
|
|
544
|
-
operation: "render-character"
|
|
797
|
+
operation: "render-character",
|
|
798
|
+
root
|
|
545
799
|
}, async ({ context }) => {
|
|
546
800
|
const imageEntries = context.studio.script.imageParams?.images ?? {};
|
|
547
801
|
const imageEntry = imageEntries[key];
|
|
@@ -565,11 +819,17 @@ function createMulmoScriptServerOps(backend) {
|
|
|
565
819
|
if (!result.ok) genError = result.error;
|
|
566
820
|
return result;
|
|
567
821
|
} finally {
|
|
568
|
-
publishGeneration(chatSessionId, "characterImage", filePath, key, true,
|
|
822
|
+
publishGeneration(chatSessionId, "characterImage", filePath, key, true, {
|
|
823
|
+
error: genError,
|
|
824
|
+
root
|
|
825
|
+
});
|
|
569
826
|
}
|
|
570
827
|
}
|
|
571
|
-
async function uploadBeatImageOp(filePath, beatIndex, imageData) {
|
|
572
|
-
return runStoryOp(filePath, {
|
|
828
|
+
async function uploadBeatImageOp(filePath, beatIndex, imageData, root) {
|
|
829
|
+
return runStoryOp(filePath, {
|
|
830
|
+
operation: "upload-beat-image",
|
|
831
|
+
root
|
|
832
|
+
}, async ({ context }) => {
|
|
573
833
|
const { imagePath } = (0, mulmocast.getBeatPngImagePath)(context, beatIndex);
|
|
574
834
|
const base64 = stripDataUri(imageData);
|
|
575
835
|
await backend.writeFileAtomic(imagePath, Buffer.from(base64, "base64"));
|
|
@@ -579,8 +839,11 @@ function createMulmoScriptServerOps(backend) {
|
|
|
579
839
|
};
|
|
580
840
|
});
|
|
581
841
|
}
|
|
582
|
-
async function uploadCharacterImageOp(filePath, key, imageData) {
|
|
583
|
-
return runStoryOp(filePath, {
|
|
842
|
+
async function uploadCharacterImageOp(filePath, key, imageData, root) {
|
|
843
|
+
return runStoryOp(filePath, {
|
|
844
|
+
operation: "upload-character-image",
|
|
845
|
+
root
|
|
846
|
+
}, async ({ context }) => {
|
|
584
847
|
const imagePath = (0, mulmocast.getReferenceImagePath)(context, key, "png");
|
|
585
848
|
const base64 = stripDataUri(imageData);
|
|
586
849
|
await backend.writeFileAtomic(imagePath, Buffer.from(base64, "base64"));
|
|
@@ -629,54 +892,68 @@ function createMulmoScriptServerOps(backend) {
|
|
|
629
892
|
* initiating View (and any other mounted View) reloads assets off disk
|
|
630
893
|
* as they land — the successor of the SSE per-beat events.
|
|
631
894
|
*/
|
|
632
|
-
async function generateMovieOp(filePath, chatSessionId) {
|
|
895
|
+
async function generateMovieOp(filePath, chatSessionId, root) {
|
|
896
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
897
|
+
if (rootGuard) return rootGuard;
|
|
633
898
|
const ffmpeg = ffmpegGuard();
|
|
634
899
|
if (ffmpeg) return ffmpeg;
|
|
635
|
-
const resolved = resolveStory(filePath);
|
|
900
|
+
const resolved = resolveStory(filePath, root);
|
|
636
901
|
if (!resolved.ok) return resolved;
|
|
637
902
|
const absoluteFilePath = resolved.absolutePath;
|
|
638
903
|
if (inFlightMovies.has(absoluteFilePath)) return opBadRequest("Movie generation is already in progress for this script");
|
|
639
904
|
inFlightMovies.add(absoluteFilePath);
|
|
640
|
-
publishGeneration(chatSessionId, "movie", filePath, "", false);
|
|
905
|
+
publishGeneration(chatSessionId, "movie", filePath, "", false, { root });
|
|
641
906
|
let genError;
|
|
642
907
|
try {
|
|
643
908
|
const result = await runMovieGeneration(absoluteFilePath, (event) => {
|
|
644
|
-
publishGeneration(chatSessionId, event.kind === "image" ? "beatImage" : "beatAudio", filePath, String(event.beatIndex), true);
|
|
909
|
+
publishGeneration(chatSessionId, event.kind === "image" ? "beatImage" : "beatAudio", filePath, String(event.beatIndex), true, { root });
|
|
645
910
|
});
|
|
646
911
|
if (!result.ok) {
|
|
647
912
|
genError = result.error;
|
|
648
913
|
return opServerError(result.error);
|
|
649
914
|
}
|
|
915
|
+
const movieRef = toStoryRef(result.outputPath, root);
|
|
916
|
+
if (movieRef === null) return opServerError("generated movie is outside the registered stories root");
|
|
650
917
|
return {
|
|
651
918
|
ok: true,
|
|
652
|
-
moviePath:
|
|
919
|
+
moviePath: movieRef
|
|
653
920
|
};
|
|
654
921
|
} catch (err) {
|
|
655
922
|
genError = require_plugin.errorMessage(err);
|
|
656
923
|
return opServerError(genError);
|
|
657
924
|
} finally {
|
|
658
925
|
inFlightMovies.delete(absoluteFilePath);
|
|
659
|
-
publishGeneration(chatSessionId, "movie", filePath, "", true,
|
|
926
|
+
publishGeneration(chatSessionId, "movie", filePath, "", true, {
|
|
927
|
+
error: genError,
|
|
928
|
+
root
|
|
929
|
+
});
|
|
660
930
|
}
|
|
661
931
|
}
|
|
662
|
-
function triggerAutoBackgroundMovie(absoluteFilePath, wireFilePath, chatSessionId) {
|
|
932
|
+
function triggerAutoBackgroundMovie(absoluteFilePath, wireFilePath, chatSessionId, root) {
|
|
933
|
+
if (guardStoryGenerationRoot(root)) {
|
|
934
|
+
log.warn("refused an auto background movie in a non-default stories root", {
|
|
935
|
+
filePath: wireFilePath,
|
|
936
|
+
root
|
|
937
|
+
});
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
663
940
|
if (inFlightMovies.has(absoluteFilePath)) return;
|
|
664
941
|
inFlightMovies.add(absoluteFilePath);
|
|
665
|
-
runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId);
|
|
942
|
+
runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId, root);
|
|
666
943
|
}
|
|
667
|
-
async function runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId) {
|
|
944
|
+
async function runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId, root) {
|
|
668
945
|
const errorSidecarPath = `${absoluteFilePath}.error.txt`;
|
|
669
946
|
try {
|
|
670
947
|
(0, fs.unlinkSync)(errorSidecarPath);
|
|
671
948
|
} catch {}
|
|
672
|
-
publishGeneration(chatSessionId, "movie", wireFilePath, "", false);
|
|
949
|
+
publishGeneration(chatSessionId, "movie", wireFilePath, "", false, { root });
|
|
673
950
|
let genError;
|
|
674
951
|
try {
|
|
675
952
|
const result = await runMovieGeneration(absoluteFilePath, (event) => {
|
|
676
953
|
const eventKind = event.kind === "image" ? "beatImage" : "beatAudio";
|
|
677
954
|
const key = String(event.beatIndex);
|
|
678
|
-
publishGeneration(chatSessionId, eventKind, wireFilePath, key, false);
|
|
679
|
-
setImmediate(() => publishGeneration(chatSessionId, eventKind, wireFilePath, key, true));
|
|
955
|
+
publishGeneration(chatSessionId, eventKind, wireFilePath, key, false, { root });
|
|
956
|
+
setImmediate(() => publishGeneration(chatSessionId, eventKind, wireFilePath, key, true, { root }));
|
|
680
957
|
});
|
|
681
958
|
if (!result.ok) {
|
|
682
959
|
genError = result.error;
|
|
@@ -700,7 +977,10 @@ function createMulmoScriptServerOps(backend) {
|
|
|
700
977
|
});
|
|
701
978
|
} finally {
|
|
702
979
|
inFlightMovies.delete(absoluteFilePath);
|
|
703
|
-
publishGeneration(chatSessionId, "movie", wireFilePath, "", true,
|
|
980
|
+
publishGeneration(chatSessionId, "movie", wireFilePath, "", true, {
|
|
981
|
+
error: genError,
|
|
982
|
+
root
|
|
983
|
+
});
|
|
704
984
|
}
|
|
705
985
|
}
|
|
706
986
|
async function writeErrorSidecar(errorSidecarPath, message) {
|
|
@@ -736,15 +1016,17 @@ function createMulmoScriptServerOps(backend) {
|
|
|
736
1016
|
}
|
|
737
1017
|
/** Long-held foreground PDF generation (the package View's `generatePdf`
|
|
738
1018
|
* dispatch) — the PDF sibling of `generateMovieOp`. */
|
|
739
|
-
async function generatePdfOp(filePath, chatSessionId) {
|
|
1019
|
+
async function generatePdfOp(filePath, chatSessionId, root) {
|
|
1020
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
1021
|
+
if (rootGuard) return rootGuard;
|
|
740
1022
|
const ffmpeg = ffmpegGuard();
|
|
741
1023
|
if (ffmpeg) return ffmpeg;
|
|
742
|
-
const resolved = resolveStory(filePath);
|
|
1024
|
+
const resolved = resolveStory(filePath, root);
|
|
743
1025
|
if (!resolved.ok) return resolved;
|
|
744
1026
|
const absoluteFilePath = resolved.absolutePath;
|
|
745
1027
|
if (inFlightPdfs.has(absoluteFilePath)) return opBadRequest("PDF generation is already in progress for this script");
|
|
746
1028
|
inFlightPdfs.add(absoluteFilePath);
|
|
747
|
-
publishGeneration(chatSessionId, "pdf", filePath, "", false);
|
|
1029
|
+
publishGeneration(chatSessionId, "pdf", filePath, "", false, { root });
|
|
748
1030
|
let genError;
|
|
749
1031
|
try {
|
|
750
1032
|
const context = await buildContext(absoluteFilePath);
|
|
@@ -753,22 +1035,27 @@ function createMulmoScriptServerOps(backend) {
|
|
|
753
1035
|
return opServerError(genError);
|
|
754
1036
|
}
|
|
755
1037
|
const result = await runPdfGeneration(context, (beatIndex) => {
|
|
756
|
-
publishGeneration(chatSessionId, "beatImage", filePath, String(beatIndex), true);
|
|
1038
|
+
publishGeneration(chatSessionId, "beatImage", filePath, String(beatIndex), true, { root });
|
|
757
1039
|
});
|
|
758
1040
|
if (!result.ok) {
|
|
759
1041
|
genError = result.error;
|
|
760
1042
|
return opServerError(result.error);
|
|
761
1043
|
}
|
|
1044
|
+
const pdfRef = toStoryRef(result.outputPath, root);
|
|
1045
|
+
if (pdfRef === null) return opServerError("generated PDF is outside the registered stories root");
|
|
762
1046
|
return {
|
|
763
1047
|
ok: true,
|
|
764
|
-
pdfPath:
|
|
1048
|
+
pdfPath: pdfRef
|
|
765
1049
|
};
|
|
766
1050
|
} catch (err) {
|
|
767
1051
|
genError = require_plugin.errorMessage(err);
|
|
768
1052
|
return opServerError(genError);
|
|
769
1053
|
} finally {
|
|
770
1054
|
inFlightPdfs.delete(absoluteFilePath);
|
|
771
|
-
publishGeneration(chatSessionId, "pdf", filePath, "", true,
|
|
1055
|
+
publishGeneration(chatSessionId, "pdf", filePath, "", true, {
|
|
1056
|
+
error: genError,
|
|
1057
|
+
root
|
|
1058
|
+
});
|
|
772
1059
|
}
|
|
773
1060
|
}
|
|
774
1061
|
return {
|
|
@@ -776,6 +1063,10 @@ function createMulmoScriptServerOps(backend) {
|
|
|
776
1063
|
toStoryRef,
|
|
777
1064
|
resolveStory,
|
|
778
1065
|
guardStoryWirePath,
|
|
1066
|
+
guardStoryRootRegistered,
|
|
1067
|
+
guardStoryWriteRoot,
|
|
1068
|
+
artifactsForRoot,
|
|
1069
|
+
guardStoryGenerationRoot,
|
|
779
1070
|
ffmpegGuard,
|
|
780
1071
|
runStoryOp,
|
|
781
1072
|
publishGeneration,
|
|
@@ -874,12 +1165,56 @@ var UPLOAD_KINDS = /* @__PURE__ */ new Set(["uploadBeatImage", "uploadCharacterI
|
|
|
874
1165
|
* FileOps, guarded by the instance's realpath containment
|
|
875
1166
|
* (`guardStoryWirePath`) — the core's own guard is lexical.
|
|
876
1167
|
*/
|
|
1168
|
+
/**
|
|
1169
|
+
* Stamp the root a successful result acted in.
|
|
1170
|
+
*
|
|
1171
|
+
* Only on success: a failure carries `code` and `error`, and adding a root to
|
|
1172
|
+
* it would invite a reader to treat the pair as addressable when the call did
|
|
1173
|
+
* not happen. Only when NON-default, so a result for a call that named no root
|
|
1174
|
+
* stays byte-identical to what this package returned before roots existed —
|
|
1175
|
+
* which is what keeps every existing card working untouched.
|
|
1176
|
+
*/
|
|
1177
|
+
function withRoot(result, root) {
|
|
1178
|
+
const normalized = require_contract.normalizeRoot(root);
|
|
1179
|
+
if (normalized === "") return result;
|
|
1180
|
+
if (!isRecord(result) || result.ok !== true) return result;
|
|
1181
|
+
return {
|
|
1182
|
+
...result,
|
|
1183
|
+
root: normalized
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
/** `undefined` when `root` is absent or a string; a failure envelope otherwise. */
|
|
1187
|
+
function guardSuppliedRoot(root) {
|
|
1188
|
+
if (root === void 0 || typeof root === "string") return void 0;
|
|
1189
|
+
return {
|
|
1190
|
+
ok: false,
|
|
1191
|
+
code: "bad_request",
|
|
1192
|
+
error: `mulmoScript root must be a string, got ${typeof root}`
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
877
1195
|
function createMulmoScriptDispatchHandler(ops) {
|
|
878
|
-
|
|
1196
|
+
/**
|
|
1197
|
+
* The executor context for a write, bound to the root it names.
|
|
1198
|
+
*
|
|
1199
|
+
* One `FileOps` was held for the whole handler, so the executors — which
|
|
1200
|
+
* take a WIRE path (`stories/…`) and resolve it against whatever FileOps
|
|
1201
|
+
* they are given — wrote a named root's script into the DEFAULT root's
|
|
1202
|
+
* identically-named file. Choosing here keeps `MulmoScriptExecuteContext`
|
|
1203
|
+
* and every executor unchanged: the root never reaches them, only the right
|
|
1204
|
+
* FileOps does (#3019).
|
|
1205
|
+
*/
|
|
1206
|
+
function executeContextFor(root) {
|
|
1207
|
+
const artifacts = ops.artifactsForRoot(root);
|
|
1208
|
+
return artifacts === null ? null : { files: { artifacts } };
|
|
1209
|
+
}
|
|
879
1210
|
async function saveKind(args) {
|
|
880
|
-
const
|
|
1211
|
+
const rootGuard = ops.guardStoryWriteRoot(str(args.root));
|
|
1212
|
+
if (rootGuard) return fromOpFailure(rootGuard);
|
|
1213
|
+
const guard = ops.guardStoryWirePath(args.filePath, str(args.root));
|
|
881
1214
|
if (guard) return fromOpFailure(guard);
|
|
882
|
-
const
|
|
1215
|
+
const context = executeContextFor(str(args.root));
|
|
1216
|
+
if (context === null) return invalidArgs("save");
|
|
1217
|
+
const outcome = await require_plugin.executeMulmoScriptSave(context, {
|
|
883
1218
|
script: args.script,
|
|
884
1219
|
filename: str(args.filename),
|
|
885
1220
|
filePath: str(args.filePath)
|
|
@@ -893,11 +1228,15 @@ function createMulmoScriptDispatchHandler(ops) {
|
|
|
893
1228
|
};
|
|
894
1229
|
}
|
|
895
1230
|
async function updateKind(kind, args) {
|
|
896
|
-
const
|
|
1231
|
+
const rootGuard = ops.guardStoryWriteRoot(str(args.root));
|
|
1232
|
+
if (rootGuard) return fromOpFailure(rootGuard);
|
|
1233
|
+
const guard = ops.guardStoryWirePath(args.filePath, str(args.root));
|
|
897
1234
|
if (guard) return fromOpFailure(guard);
|
|
898
|
-
const
|
|
1235
|
+
const context = executeContextFor(str(args.root));
|
|
1236
|
+
if (context === null) return invalidArgs(kind);
|
|
1237
|
+
const outcome = kind === "updateBeat" ? await require_plugin.executeUpdateBeat(context, args) : await require_plugin.executeUpdateScript(context, args);
|
|
899
1238
|
if (!outcome.ok) return fromPackageFailure(outcome);
|
|
900
|
-
ops.publishScriptChanged(str(args.filePath) ?? "", str(args.origin));
|
|
1239
|
+
ops.publishScriptChanged(str(args.filePath) ?? "", str(args.origin), str(args.root));
|
|
901
1240
|
return { ok: true };
|
|
902
1241
|
}
|
|
903
1242
|
const STATUS_OPS = {
|
|
@@ -913,30 +1252,37 @@ function createMulmoScriptDispatchHandler(ops) {
|
|
|
913
1252
|
const statusOp = STATUS_OPS[kind];
|
|
914
1253
|
if (statusOp) {
|
|
915
1254
|
const filePath = str(args.filePath);
|
|
916
|
-
return filePath ? envelope(await statusOp(filePath)) : invalidArgs(kind);
|
|
1255
|
+
return filePath ? envelope(await statusOp(filePath, str(args.root))) : invalidArgs(kind);
|
|
917
1256
|
}
|
|
918
1257
|
if (kind === "characterImage") {
|
|
919
1258
|
const parsed = keyArgs(args);
|
|
920
|
-
return parsed ? envelope(await ops.characterImageOp(parsed.filePath, parsed.key)) : invalidArgs(kind);
|
|
1259
|
+
return parsed ? envelope(await ops.characterImageOp(parsed.filePath, parsed.key, str(args.root))) : invalidArgs(kind);
|
|
921
1260
|
}
|
|
922
1261
|
const parsed = beatArgs(args);
|
|
923
1262
|
if (!parsed) return invalidArgs(kind);
|
|
924
|
-
return envelope(await BEAT_PROBE_OPS[kind](parsed.filePath, parsed.beatIndex));
|
|
1263
|
+
return envelope(await BEAT_PROBE_OPS[kind](parsed.filePath, parsed.beatIndex, str(args.root)));
|
|
1264
|
+
}
|
|
1265
|
+
/** Movie and PDF take the whole script; the other generate kinds take a beat
|
|
1266
|
+
* or a character within it. */
|
|
1267
|
+
async function wholeScriptGenerationKind(kind, args) {
|
|
1268
|
+
const filePath = str(args.filePath);
|
|
1269
|
+
if (!filePath) return invalidArgs(kind);
|
|
1270
|
+
const chatSessionId = str(args.chatSessionId);
|
|
1271
|
+
const root = str(args.root);
|
|
1272
|
+
return envelope(kind === "generateMovie" ? await ops.generateMovieOp(filePath, chatSessionId, root) : await ops.generatePdfOp(filePath, chatSessionId, root));
|
|
925
1273
|
}
|
|
926
1274
|
async function generateKind(kind, args) {
|
|
1275
|
+
if (kind === "generateMovie" || kind === "generatePdf") return wholeScriptGenerationKind(kind, args);
|
|
927
1276
|
const chatSessionId = str(args.chatSessionId);
|
|
1277
|
+
const root = str(args.root);
|
|
928
1278
|
const force = args.force === true;
|
|
929
|
-
if (kind === "generateMovie" || kind === "generatePdf") {
|
|
930
|
-
const filePath = str(args.filePath);
|
|
931
|
-
if (!filePath) return invalidArgs(kind);
|
|
932
|
-
return envelope(kind === "generateMovie" ? await ops.generateMovieOp(filePath, chatSessionId) : await ops.generatePdfOp(filePath, chatSessionId));
|
|
933
|
-
}
|
|
934
1279
|
if (kind === "renderCharacter") {
|
|
935
1280
|
const parsed = keyArgs(args);
|
|
936
1281
|
return parsed ? envelope(await ops.renderCharacterOp({
|
|
937
1282
|
...parsed,
|
|
938
1283
|
force,
|
|
939
|
-
chatSessionId
|
|
1284
|
+
chatSessionId,
|
|
1285
|
+
root
|
|
940
1286
|
})) : invalidArgs(kind);
|
|
941
1287
|
}
|
|
942
1288
|
const parsed = beatArgs(args);
|
|
@@ -944,11 +1290,13 @@ function createMulmoScriptDispatchHandler(ops) {
|
|
|
944
1290
|
return envelope(kind === "renderBeat" ? await ops.renderBeatOp({
|
|
945
1291
|
...parsed,
|
|
946
1292
|
force,
|
|
947
|
-
chatSessionId
|
|
1293
|
+
chatSessionId,
|
|
1294
|
+
root
|
|
948
1295
|
}) : await ops.generateBeatAudioOp({
|
|
949
1296
|
...parsed,
|
|
950
1297
|
force,
|
|
951
|
-
chatSessionId
|
|
1298
|
+
chatSessionId,
|
|
1299
|
+
root
|
|
952
1300
|
}));
|
|
953
1301
|
}
|
|
954
1302
|
async function uploadKind(kind, args) {
|
|
@@ -956,33 +1304,43 @@ function createMulmoScriptDispatchHandler(ops) {
|
|
|
956
1304
|
if (!imageData) return invalidArgs(kind);
|
|
957
1305
|
if (kind === "uploadCharacterImage") {
|
|
958
1306
|
const parsed = keyArgs(args);
|
|
959
|
-
return parsed ? envelope(await ops.uploadCharacterImageOp(parsed.filePath, parsed.key, imageData)) : invalidArgs(kind);
|
|
1307
|
+
return parsed ? envelope(await ops.uploadCharacterImageOp(parsed.filePath, parsed.key, imageData, str(args.root))) : invalidArgs(kind);
|
|
960
1308
|
}
|
|
961
1309
|
const parsed = beatArgs(args);
|
|
962
1310
|
if (!parsed) return invalidArgs(kind);
|
|
963
|
-
return envelope(await ops.uploadBeatImageOp(parsed.filePath, parsed.beatIndex, imageData));
|
|
1311
|
+
return envelope(await ops.uploadBeatImageOp(parsed.filePath, parsed.beatIndex, imageData, str(args.root)));
|
|
964
1312
|
}
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
if (!
|
|
1313
|
+
async function pendingKind(kind, args) {
|
|
1314
|
+
const filePath = str(args.filePath);
|
|
1315
|
+
if (!filePath) return invalidArgs(kind);
|
|
1316
|
+
const root = str(args.root);
|
|
1317
|
+
const rootGuard = ops.guardStoryRootRegistered(root);
|
|
1318
|
+
if (rootGuard) return fromOpFailure(rootGuard);
|
|
1319
|
+
return {
|
|
1320
|
+
ok: true,
|
|
1321
|
+
pending: ops.pendingGenerations(filePath, root)
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
/** Which handler serves this kind. Routing only — the caller tags the answer. */
|
|
1325
|
+
async function route(kind, args) {
|
|
968
1326
|
if (kind === "save") return saveKind(args);
|
|
969
1327
|
if (kind === "updateBeat" || kind === "updateScript") return updateKind(kind, args);
|
|
970
1328
|
if (PROBE_KINDS.has(kind)) return probeKind(kind, args);
|
|
971
1329
|
if (GENERATE_KINDS.has(kind)) return generateKind(kind, args);
|
|
972
1330
|
if (UPLOAD_KINDS.has(kind)) return uploadKind(kind, args);
|
|
973
|
-
if (kind === "pendingGenerations")
|
|
974
|
-
const filePath = str(args.filePath);
|
|
975
|
-
if (!filePath) return invalidArgs(kind);
|
|
976
|
-
return {
|
|
977
|
-
ok: true,
|
|
978
|
-
pending: ops.pendingGenerations(filePath)
|
|
979
|
-
};
|
|
980
|
-
}
|
|
1331
|
+
if (kind === "pendingGenerations") return pendingKind(kind, args);
|
|
981
1332
|
return {
|
|
982
1333
|
ok: false,
|
|
983
1334
|
code: "bad_request",
|
|
984
1335
|
error: `unknown mulmoScript dispatch kind "${kind}"`
|
|
985
1336
|
};
|
|
1337
|
+
}
|
|
1338
|
+
return async (args) => {
|
|
1339
|
+
const kind = str(args.kind);
|
|
1340
|
+
if (!kind) return invalidArgs("<missing>");
|
|
1341
|
+
const malformedRoot = guardSuppliedRoot(args.root);
|
|
1342
|
+
if (malformedRoot) return malformedRoot;
|
|
1343
|
+
return withRoot(await route(kind, args), str(args.root));
|
|
986
1344
|
};
|
|
987
1345
|
}
|
|
988
1346
|
//#endregion
|