@mulmoclaude/mulmoscript-plugin 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/lang/de.d.ts.map +1 -1
- package/dist/lang/en.d.ts.map +1 -1
- package/dist/lang/es.d.ts.map +1 -1
- package/dist/lang/fr.d.ts.map +1 -1
- package/dist/lang/ja.d.ts.map +1 -1
- package/dist/lang/ko.d.ts.map +1 -1
- package/dist/lang/messages.d.ts +4 -0
- package/dist/lang/messages.d.ts.map +1 -1
- package/dist/lang/ptBR.d.ts.map +1 -1
- package/dist/lang/zh.d.ts.map +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 +64 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server.cjs +423 -110
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +423 -110
- package/dist/server.js.map +1 -1
- package/dist/style.css +46 -18
- package/dist/vue/View.vue.d.ts.map +1 -1
- package/dist/vue/composables/useDeckEditor.d.ts +1 -1
- package/dist/vue/composables/useDeckEditor.d.ts.map +1 -1
- package/dist/vue/helpers.d.ts +11 -8
- package/dist/vue/helpers.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 +244 -110
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +244 -110
- 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);
|
|
@@ -190,11 +190,17 @@ async function withBeatProgress(beats, onBeat, body) {
|
|
|
190
190
|
}
|
|
191
191
|
/** Map identity for the in-flight tracker. JSON array keeps the three
|
|
192
192
|
* fields unambiguous (a human-visible delimiter could collide). */
|
|
193
|
-
function generationMapKey(kind, filePath, key) {
|
|
194
|
-
|
|
193
|
+
function generationMapKey(kind, filePath, key, root) {
|
|
194
|
+
const normalized = require_contract.normalizeRoot(root);
|
|
195
|
+
return normalized === "" ? JSON.stringify([
|
|
195
196
|
kind,
|
|
196
197
|
filePath,
|
|
197
198
|
key
|
|
199
|
+
]) : JSON.stringify([
|
|
200
|
+
kind,
|
|
201
|
+
filePath,
|
|
202
|
+
key,
|
|
203
|
+
normalized
|
|
198
204
|
]);
|
|
199
205
|
}
|
|
200
206
|
/**
|
|
@@ -205,19 +211,38 @@ function generationMapKey(kind, filePath, key) {
|
|
|
205
211
|
function createMulmoScriptServerOps(backend) {
|
|
206
212
|
const log = backend.log ?? NOOP_LOG;
|
|
207
213
|
setMulmoErrorCaptureLogger(log);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
const rootDirs = /* @__PURE__ */ new Map([["", path.default.resolve(backend.storiesDir)]]);
|
|
215
|
+
for (const [id, dir] of Object.entries(backend.extraRoots ?? {})) {
|
|
216
|
+
const trimmed = id.trim();
|
|
217
|
+
if (trimmed === "") throw new Error("mulmoScript: extraRoots key must not be empty — the empty id is reserved for the default stories root");
|
|
218
|
+
if (rootDirs.has(trimmed)) throw new Error(`mulmoScript: extraRoots keys must be distinct after trimming — "${trimmed}" is registered twice`);
|
|
219
|
+
rootDirs.set(trimmed, path.default.resolve(dir));
|
|
220
|
+
}
|
|
221
|
+
if (rootDirs.size > 1) log.warn(backend.rootScopedGenerationState === true ? "extra stories roots registered — reads, uploads and generation work; save/update still land in the DEFAULT root until this host passes `artifactsFor` (#3019)" : "extra stories roots registered — reads and uploads work, but GENERATION is refused until this host declares `rootScopedGenerationState` (its pending-generation state must carry the root, or it must keep none), and save/update land in the DEFAULT root until it passes `artifactsFor` (#3019)", { roots: [...rootDirs.keys()].filter((id) => id !== "") });
|
|
222
|
+
/** The registered directory for a wire `root`, or null when the host never
|
|
223
|
+
* registered it. Null is a REJECTION, not a fallback to the default: an
|
|
224
|
+
* unknown root must not quietly read the workspace's file of the same
|
|
225
|
+
* name. */
|
|
226
|
+
function rootDir(root) {
|
|
227
|
+
return rootDirs.get(require_contract.normalizeRoot(root)) ?? null;
|
|
228
|
+
}
|
|
229
|
+
function toStoryRef(absolutePath, root) {
|
|
230
|
+
const dir = rootDir(root);
|
|
231
|
+
if (dir === null) return null;
|
|
232
|
+
const base = ensureStoriesReal(root) ?? dir;
|
|
233
|
+
return require_plugin.storyRefWithin(base, absolutePath, path.default);
|
|
234
|
+
}
|
|
235
|
+
const storiesRealCache = /* @__PURE__ */ new Map();
|
|
236
|
+
function ensureStoriesReal(root) {
|
|
237
|
+
const dir = rootDir(root);
|
|
238
|
+
if (dir === null) return null;
|
|
239
|
+
const cached = storiesRealCache.get(dir);
|
|
240
|
+
if (cached) return cached;
|
|
217
241
|
try {
|
|
218
|
-
(0, fs.mkdirSync)(
|
|
219
|
-
|
|
220
|
-
|
|
242
|
+
if (require_contract.normalizeRoot(root) === "") (0, fs.mkdirSync)(dir, { recursive: true });
|
|
243
|
+
const real = (0, fs.realpathSync)(dir);
|
|
244
|
+
storiesRealCache.set(dir, real);
|
|
245
|
+
return real;
|
|
221
246
|
} catch {
|
|
222
247
|
return null;
|
|
223
248
|
}
|
|
@@ -233,8 +258,9 @@ function createMulmoScriptServerOps(backend) {
|
|
|
233
258
|
* stories/ is a regular directory or a legitimate symlink to another
|
|
234
259
|
* location. ENOENT and traversal are distinguished (404 vs 400).
|
|
235
260
|
*/
|
|
236
|
-
function resolveStory(filePath) {
|
|
237
|
-
|
|
261
|
+
function resolveStory(filePath, root) {
|
|
262
|
+
if (rootDir(root) === null) return opBadRequest("Unknown stories root");
|
|
263
|
+
const storiesReal = ensureStoriesReal(root);
|
|
238
264
|
if (!storiesReal) return opServerError("stories directory not available");
|
|
239
265
|
if (path.default.isAbsolute(filePath)) return opBadRequest("Invalid filePath");
|
|
240
266
|
const ARTIFACTS_STORIES = "artifacts/stories";
|
|
@@ -254,6 +280,19 @@ function createMulmoScriptServerOps(backend) {
|
|
|
254
280
|
};
|
|
255
281
|
}
|
|
256
282
|
/**
|
|
283
|
+
* Whether this root is one the host registered.
|
|
284
|
+
*
|
|
285
|
+
* Every other root-aware op learns this from `resolveStory`, which needs a
|
|
286
|
+
* file. `pendingGenerations` needs none — it only filters an in-memory map —
|
|
287
|
+
* so an unregistered root produced `{ ok: true, pending: [] }`, and a host
|
|
288
|
+
* typo or a stale card read back as "no work is running" (Codex P2 on
|
|
289
|
+
* #3015). An unknown root is a question this package cannot answer, and the
|
|
290
|
+
* answer it must not give is a confident empty one.
|
|
291
|
+
*/
|
|
292
|
+
function guardStoryRootRegistered(root) {
|
|
293
|
+
return rootDir(root) === null ? opBadRequest(`unknown stories root "${require_contract.normalizeRoot(root)}"`) : null;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
257
296
|
* Realpath containment pre-guard for wire paths handed to the phase-1
|
|
258
297
|
* core's save/reopen/update executes. The core's own path guard is
|
|
259
298
|
* lexical (it runs against the generic FileOps, whose read/write follows
|
|
@@ -265,11 +304,121 @@ function createMulmoScriptServerOps(backend) {
|
|
|
265
304
|
* validation (including the script-vs-filePath mode check) belongs to
|
|
266
305
|
* the core.
|
|
267
306
|
*/
|
|
268
|
-
function guardStoryWirePath(filePath) {
|
|
307
|
+
function guardStoryWirePath(filePath, root) {
|
|
269
308
|
if (typeof filePath !== "string" || filePath === "") return null;
|
|
270
|
-
const resolved = resolveStory(filePath);
|
|
309
|
+
const resolved = resolveStory(filePath, root);
|
|
271
310
|
return resolved.ok ? null : resolved;
|
|
272
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Whether a GENERATION may run in this root.
|
|
314
|
+
*
|
|
315
|
+
* The pair identity is complete inside this package but stops at the host
|
|
316
|
+
* boundary: a host's per-session store keys pending work by
|
|
317
|
+
* `(kind, filePath, key)` — `generationKey` in `@mulmobridge/protocol`,
|
|
318
|
+
* which bridges also consume. Two roots running the same generation in one
|
|
319
|
+
* session would collapse to one entry, and either completion would clear the
|
|
320
|
+
* other root's indicator (Codex P1 on #3015).
|
|
321
|
+
*
|
|
322
|
+
* Whose hazard it is decides who answers: a host declares
|
|
323
|
+
* `rootScopedGenerationState` when its own pending state carries the root —
|
|
324
|
+
* or when it keeps none, which is MulmoTerminal's case. It was refused for a
|
|
325
|
+
* collision it cannot have (#3019).
|
|
326
|
+
*
|
|
327
|
+
* Absent still refuses, so a host that has not thought about this keeps the
|
|
328
|
+
* shipped behaviour rather than being quietly opened up.
|
|
329
|
+
*/
|
|
330
|
+
function guardStoryGenerationRoot(root) {
|
|
331
|
+
if (require_contract.normalizeRoot(root) === "") return null;
|
|
332
|
+
const registered = guardStoryRootRegistered(root);
|
|
333
|
+
if (registered) return registered;
|
|
334
|
+
if (backend.rootScopedGenerationState === true) return null;
|
|
335
|
+
return opBadRequest("generating in a non-default stories root is not supported yet");
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The write half of the same fail-closed rule.
|
|
339
|
+
*
|
|
340
|
+
* It lives in the ops that write rather than at their dispatch sites,
|
|
341
|
+
* because a per-site guard is a list someone has to remember to extend:
|
|
342
|
+
* `save` / `updateBeat` / `updateScript` were guarded and the two upload
|
|
343
|
+
* kinds were not, so an image could still land in a named root
|
|
344
|
+
* (CodeRabbit on #3015). `save` / `update* ` cannot follow suit — they run
|
|
345
|
+
* through the package executors, not through these ops — so
|
|
346
|
+
* `test_server_roots.ts` walks the whole ops surface and fails on any op
|
|
347
|
+
* that is neither in the read-only allowlist nor refusing.
|
|
348
|
+
*/
|
|
349
|
+
/**
|
|
350
|
+
* Whether a WRITE may target this root.
|
|
351
|
+
*
|
|
352
|
+
* Reads are root-aware; writes are not. `executeMulmoScriptSave` and the
|
|
353
|
+
* update executors run against one `FileOps`, bound by the host to the
|
|
354
|
+
* default root, so a write naming another root would rewrite the DEFAULT
|
|
355
|
+
* root's identically-named file and then announce the other one as changed
|
|
356
|
+
* (#3015 review G1). Closing it was fail-closed: "readable but not yet
|
|
357
|
+
* writable" beats "wrote somewhere else and said so".
|
|
358
|
+
*
|
|
359
|
+
* It opens per root, not globally: the host answers `artifactsFor` for the
|
|
360
|
+
* roots it can serve, and a root it cannot is still refused. A host that
|
|
361
|
+
* wires nothing keeps the shipped refusal (#3019).
|
|
362
|
+
*/
|
|
363
|
+
function guardStoryWriteRoot(root) {
|
|
364
|
+
if (require_contract.normalizeRoot(root) === "") return null;
|
|
365
|
+
const registered = guardStoryRootRegistered(root);
|
|
366
|
+
if (registered) return registered;
|
|
367
|
+
return artifactsForRoot(root) === null ? opBadRequest("writing to a non-default stories root is not supported yet") : null;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* The FileOps a write to this root must go through.
|
|
371
|
+
*
|
|
372
|
+
* The default root keeps the single `artifacts` it always had, byte for
|
|
373
|
+
* byte. A named root is served only when the host both REGISTERED it
|
|
374
|
+
* (`extraRoots` — the containment boundary, so a host cannot widen the
|
|
375
|
+
* addressable set through this back door) and can supply a FileOps for it.
|
|
376
|
+
*/
|
|
377
|
+
function artifactsForRoot(root) {
|
|
378
|
+
const normalized = require_contract.normalizeRoot(root);
|
|
379
|
+
if (normalized === "") return backend.artifacts;
|
|
380
|
+
if (rootDir(normalized) === null) return null;
|
|
381
|
+
const hostOps = backend.artifactsFor?.(normalized);
|
|
382
|
+
return hostOps === void 0 || hostOps === null ? null : storiesScoped(hostOps);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Address a named root's FileOps the way the READ side addresses it.
|
|
386
|
+
*
|
|
387
|
+
* The two sides disagreed about one path segment, and the disagreement was
|
|
388
|
+
* silent. A read strips `stories/` and resolves under the registered
|
|
389
|
+
* directory (`<root>/<rel>`); a write handed the executors' wire path
|
|
390
|
+
* straight to the host's FileOps, which is rooted at that same registered
|
|
391
|
+
* directory, so the bytes landed in `<root>/stories/<rel>`. `save` then
|
|
392
|
+
* REPORTED SUCCESS and returned a wire path that could not be read back —
|
|
393
|
+
* a card pointing at nothing (#3020 review H1, from the consuming host).
|
|
394
|
+
*
|
|
395
|
+
* Stripping here makes "the directory you registered is the directory your
|
|
396
|
+
* FileOps is rooted at" true, which is what a host writes without being
|
|
397
|
+
* told. The default root is untouched: its FileOps is rooted one level up,
|
|
398
|
+
* at `<workspace>/artifacts`, and is shared with other plugins.
|
|
399
|
+
*
|
|
400
|
+
* A path that is not a stories wire path throws rather than passing
|
|
401
|
+
* through. Everything reaching here has been through `normalizeStoryPath`
|
|
402
|
+
* or `storyFilePath`, so one that has not is a bug — and letting it through
|
|
403
|
+
* would write it somewhere nobody can read, which is the failure this whole
|
|
404
|
+
* wrapper exists to end.
|
|
405
|
+
*/
|
|
406
|
+
function storiesScoped(inner) {
|
|
407
|
+
const within = (wirePath) => {
|
|
408
|
+
const relative = require_plugin.storiesRelativePath(wirePath);
|
|
409
|
+
if (relative === null) throw new Error(`mulmoScript: "${wirePath}" is not a stories path — a named root's FileOps takes stories paths only`);
|
|
410
|
+
return relative;
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
read: async (wirePath) => inner.read(within(wirePath)),
|
|
414
|
+
readBytes: async (wirePath) => inner.readBytes(within(wirePath)),
|
|
415
|
+
write: async (wirePath, data) => inner.write(within(wirePath), data),
|
|
416
|
+
readDir: async (wirePath) => inner.readDir(within(wirePath)),
|
|
417
|
+
stat: async (wirePath) => inner.stat(within(wirePath)),
|
|
418
|
+
exists: async (wirePath) => inner.exists(within(wirePath)),
|
|
419
|
+
unlink: async (wirePath) => inner.unlink(within(wirePath))
|
|
420
|
+
};
|
|
421
|
+
}
|
|
273
422
|
function ffmpegGuard() {
|
|
274
423
|
if (backend.isFfmpegAvailable?.() === false) return {
|
|
275
424
|
ok: false,
|
|
@@ -279,6 +428,13 @@ function createMulmoScriptServerOps(backend) {
|
|
|
279
428
|
return null;
|
|
280
429
|
}
|
|
281
430
|
const inFlightGenerations = /* @__PURE__ */ new Map();
|
|
431
|
+
/** Emit `root` only when it names a non-default one: an event carrying
|
|
432
|
+
* `root: ""` and one carrying nothing must stay indistinguishable to every
|
|
433
|
+
* pre-#3014 consumer. */
|
|
434
|
+
function rootField(root) {
|
|
435
|
+
const normalized = require_contract.normalizeRoot(root);
|
|
436
|
+
return normalized === "" ? {} : { root: normalized };
|
|
437
|
+
}
|
|
282
438
|
/** Tracker state and events key on the canonical `stories/<rel>` wire
|
|
283
439
|
* form: subscribers (the View's pubsub filter, `pendingGenerations`
|
|
284
440
|
* callers) match by exact string, so the accepted alias spellings
|
|
@@ -288,9 +444,23 @@ function createMulmoScriptServerOps(backend) {
|
|
|
288
444
|
function canonicalWirePath(filePath) {
|
|
289
445
|
return require_plugin.normalizeStoryPath(filePath) ?? filePath;
|
|
290
446
|
}
|
|
291
|
-
|
|
447
|
+
/**
|
|
448
|
+
* `error` and `root` travel in an options object, NOT as two trailing
|
|
449
|
+
* optional positionals.
|
|
450
|
+
*
|
|
451
|
+
* They were positional once, and appending `root` to the sixteen call sites
|
|
452
|
+
* put it in `error`'s slot at the nine that pass no error: every start event
|
|
453
|
+
* carried `error: "<root>"`, the root was dropped from the key, the tracker
|
|
454
|
+
* entry was filed under the default root — and because the matching finish
|
|
455
|
+
* DID pass both, its key differed and the entry was never deleted, leaking
|
|
456
|
+
* one row per generation for the life of the process. Two adjacent optional
|
|
457
|
+
* strings cannot be told apart by the type checker, so the shape is the only
|
|
458
|
+
* thing that can prevent it (#3015 review).
|
|
459
|
+
*/
|
|
460
|
+
function publishGeneration(chatSessionId, kind, filePath, key, finished, opts = {}) {
|
|
461
|
+
const { error, root } = opts;
|
|
292
462
|
const wirePath = canonicalWirePath(filePath);
|
|
293
|
-
const mapKey = generationMapKey(kind, wirePath, key);
|
|
463
|
+
const mapKey = generationMapKey(kind, wirePath, key, root);
|
|
294
464
|
const existing = inFlightGenerations.get(mapKey);
|
|
295
465
|
if (finished) {
|
|
296
466
|
if (existing && existing.count > 1) {
|
|
@@ -307,6 +477,7 @@ function createMulmoScriptServerOps(backend) {
|
|
|
307
477
|
kind,
|
|
308
478
|
filePath: wirePath,
|
|
309
479
|
key,
|
|
480
|
+
root: require_contract.normalizeRoot(root),
|
|
310
481
|
count: 1
|
|
311
482
|
});
|
|
312
483
|
}
|
|
@@ -315,7 +486,8 @@ function createMulmoScriptServerOps(backend) {
|
|
|
315
486
|
filePath: wirePath,
|
|
316
487
|
key,
|
|
317
488
|
done: finished,
|
|
318
|
-
...error ? { error } : {}
|
|
489
|
+
...error ? { error } : {},
|
|
490
|
+
...rootField(root)
|
|
319
491
|
};
|
|
320
492
|
backend.onGenerationEvent?.(chatSessionId, event);
|
|
321
493
|
}
|
|
@@ -326,21 +498,31 @@ function createMulmoScriptServerOps(backend) {
|
|
|
326
498
|
* save — reloading there would rebuild the element the caret is in, mid-keystroke. A write
|
|
327
499
|
* from the agent carries no origin, so everyone reloads.
|
|
328
500
|
*/
|
|
329
|
-
function publishScriptChanged(filePath, origin) {
|
|
501
|
+
function publishScriptChanged(filePath, origin, root) {
|
|
330
502
|
backend.onScriptChanged?.({
|
|
331
503
|
filePath: canonicalWirePath(filePath),
|
|
332
|
-
...origin === void 0 ? {} : { origin }
|
|
504
|
+
...origin === void 0 ? {} : { origin },
|
|
505
|
+
...rootField(root)
|
|
333
506
|
});
|
|
334
507
|
}
|
|
335
|
-
/**
|
|
336
|
-
*
|
|
337
|
-
|
|
508
|
+
/**
|
|
509
|
+
* Snapshot of generations currently in flight for one script — the View's
|
|
510
|
+
* mount-time catch-up.
|
|
511
|
+
*
|
|
512
|
+
* Filtered on the PAIR. Filtering on `filePath` alone hands a View watching
|
|
513
|
+
* `repoB/stories/deck.json` the run started for `repoA`'s identically-named
|
|
514
|
+
* deck, and the caller cannot discard it because the returned event would
|
|
515
|
+
* carry no root either (Codex P1 / CodeRabbit on #3015).
|
|
516
|
+
*/
|
|
517
|
+
function pendingGenerations(filePath, root) {
|
|
338
518
|
const wirePath = canonicalWirePath(filePath);
|
|
339
|
-
|
|
519
|
+
const wanted = require_contract.normalizeRoot(root);
|
|
520
|
+
return [...inFlightGenerations.values()].filter((entry) => entry.filePath === wirePath && entry.root === wanted).map(({ kind, key }) => ({
|
|
340
521
|
kind,
|
|
341
522
|
filePath: wirePath,
|
|
342
523
|
key,
|
|
343
|
-
done: false
|
|
524
|
+
done: false,
|
|
525
|
+
...rootField(root)
|
|
344
526
|
}));
|
|
345
527
|
}
|
|
346
528
|
/**
|
|
@@ -352,7 +534,7 @@ function createMulmoScriptServerOps(backend) {
|
|
|
352
534
|
async function runStoryOp(filePath, options, handler, deps = {}) {
|
|
353
535
|
const resolver = deps.resolveStory ?? resolveStory;
|
|
354
536
|
const build = deps.buildContext ?? buildContext;
|
|
355
|
-
const resolved = resolver(filePath);
|
|
537
|
+
const resolved = resolver(filePath, options.root);
|
|
356
538
|
if (!resolved.ok) return resolved;
|
|
357
539
|
try {
|
|
358
540
|
const context = await build(resolved.absolutePath, options.force ?? false);
|
|
@@ -373,8 +555,11 @@ function createMulmoScriptServerOps(backend) {
|
|
|
373
555
|
return opServerError(require_plugin.errorMessage(err));
|
|
374
556
|
}
|
|
375
557
|
}
|
|
376
|
-
async function beatImageOp(filePath, beatIndex) {
|
|
377
|
-
return runStoryOp(filePath, {
|
|
558
|
+
async function beatImageOp(filePath, beatIndex, root) {
|
|
559
|
+
return runStoryOp(filePath, {
|
|
560
|
+
operation: "beat-image",
|
|
561
|
+
root
|
|
562
|
+
}, async ({ context }) => {
|
|
378
563
|
const { imagePath } = (0, mulmocast.getBeatPngImagePath)(context, beatIndex);
|
|
379
564
|
if (!(0, fs.existsSync)(imagePath)) return {
|
|
380
565
|
ok: true,
|
|
@@ -386,9 +571,10 @@ function createMulmoScriptServerOps(backend) {
|
|
|
386
571
|
};
|
|
387
572
|
});
|
|
388
573
|
}
|
|
389
|
-
async function beatAudioOp(filePath, beatIndex) {
|
|
574
|
+
async function beatAudioOp(filePath, beatIndex, root) {
|
|
390
575
|
return runStoryOp(filePath, {
|
|
391
576
|
operation: "beat-audio",
|
|
577
|
+
root,
|
|
392
578
|
onContextMissing: () => ({
|
|
393
579
|
ok: true,
|
|
394
580
|
audio: null
|
|
@@ -410,8 +596,11 @@ function createMulmoScriptServerOps(backend) {
|
|
|
410
596
|
};
|
|
411
597
|
});
|
|
412
598
|
}
|
|
413
|
-
async function beatMovieOp(filePath, beatIndex) {
|
|
414
|
-
return runStoryOp(filePath, {
|
|
599
|
+
async function beatMovieOp(filePath, beatIndex, root) {
|
|
600
|
+
return runStoryOp(filePath, {
|
|
601
|
+
operation: "beat-movie",
|
|
602
|
+
root
|
|
603
|
+
}, async ({ context }) => {
|
|
415
604
|
const { movieFile, soundEffectFile, lipSyncFile } = (0, mulmocast.getBeatMoviePaths)(context, beatIndex);
|
|
416
605
|
const existing = [
|
|
417
606
|
lipSyncFile,
|
|
@@ -421,12 +610,15 @@ function createMulmoScriptServerOps(backend) {
|
|
|
421
610
|
].find((candidate) => (0, fs.existsSync)(candidate));
|
|
422
611
|
return {
|
|
423
612
|
ok: true,
|
|
424
|
-
moviePath: existing ? toStoryRef(existing) : null
|
|
613
|
+
moviePath: existing ? toStoryRef(existing, root) : null
|
|
425
614
|
};
|
|
426
615
|
});
|
|
427
616
|
}
|
|
428
|
-
async function characterImageOp(filePath, key) {
|
|
429
|
-
return runStoryOp(filePath, {
|
|
617
|
+
async function characterImageOp(filePath, key, root) {
|
|
618
|
+
return runStoryOp(filePath, {
|
|
619
|
+
operation: "character-image",
|
|
620
|
+
root
|
|
621
|
+
}, async ({ context }) => {
|
|
430
622
|
const imagePath = (0, mulmocast.getReferenceImagePath)(context, key, "png");
|
|
431
623
|
if (!(0, fs.existsSync)(imagePath)) return {
|
|
432
624
|
ok: true,
|
|
@@ -441,46 +633,51 @@ function createMulmoScriptServerOps(backend) {
|
|
|
441
633
|
/** Shared "output exists and is newer than the source script" gate for
|
|
442
634
|
* movie / PDF status. A stale artifact (script edited after it was
|
|
443
635
|
* generated) reports null so the UI re-offers the Generate button. */
|
|
444
|
-
function freshOutputRef(outputPath, absoluteFilePath) {
|
|
636
|
+
function freshOutputRef(outputPath, absoluteFilePath, root) {
|
|
445
637
|
if (!(0, fs.existsSync)(outputPath)) return null;
|
|
446
638
|
if ((0, fs.statSync)(outputPath).mtimeMs < (0, fs.statSync)(absoluteFilePath).mtimeMs) return null;
|
|
447
|
-
return toStoryRef(outputPath);
|
|
639
|
+
return toStoryRef(outputPath, root);
|
|
448
640
|
}
|
|
449
|
-
async function movieStatusOp(filePath) {
|
|
641
|
+
async function movieStatusOp(filePath, root) {
|
|
450
642
|
return runStoryOp(filePath, {
|
|
451
643
|
operation: "movie-status",
|
|
644
|
+
root,
|
|
452
645
|
onContextMissing: () => ({
|
|
453
646
|
ok: true,
|
|
454
647
|
moviePath: null
|
|
455
648
|
})
|
|
456
649
|
}, async ({ absoluteFilePath, context }) => ({
|
|
457
650
|
ok: true,
|
|
458
|
-
moviePath: freshOutputRef((0, mulmocast.movieFilePath)(context), absoluteFilePath)
|
|
651
|
+
moviePath: freshOutputRef((0, mulmocast.movieFilePath)(context), absoluteFilePath, root)
|
|
459
652
|
}));
|
|
460
653
|
}
|
|
461
|
-
async function pdfStatusOp(filePath) {
|
|
654
|
+
async function pdfStatusOp(filePath, root) {
|
|
462
655
|
return runStoryOp(filePath, {
|
|
463
656
|
operation: "pdf-status",
|
|
657
|
+
root,
|
|
464
658
|
onContextMissing: () => ({
|
|
465
659
|
ok: true,
|
|
466
660
|
pdfPath: null
|
|
467
661
|
})
|
|
468
662
|
}, async ({ absoluteFilePath, context }) => ({
|
|
469
663
|
ok: true,
|
|
470
|
-
pdfPath: freshOutputRef((0, mulmocast.pdfFilePath)(context, PDF_MODE), absoluteFilePath)
|
|
664
|
+
pdfPath: freshOutputRef((0, mulmocast.pdfFilePath)(context, PDF_MODE), absoluteFilePath, root)
|
|
471
665
|
}));
|
|
472
666
|
}
|
|
473
667
|
async function renderBeatOp(args) {
|
|
474
|
-
const { filePath, beatIndex, force, chatSessionId } = args;
|
|
668
|
+
const { filePath, beatIndex, force, chatSessionId, root } = args;
|
|
669
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
670
|
+
if (rootGuard) return rootGuard;
|
|
475
671
|
const ffmpeg = ffmpegGuard();
|
|
476
672
|
if (ffmpeg) return ffmpeg;
|
|
477
673
|
const mapKey = String(beatIndex);
|
|
478
|
-
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, false);
|
|
674
|
+
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, false, { root });
|
|
479
675
|
let genError;
|
|
480
676
|
try {
|
|
481
677
|
const result = await runStoryOp(filePath, {
|
|
482
678
|
force,
|
|
483
|
-
operation: "render-beat"
|
|
679
|
+
operation: "render-beat",
|
|
680
|
+
root
|
|
484
681
|
}, async ({ context }) => {
|
|
485
682
|
await (0, mulmocast.generateBeatImage)({
|
|
486
683
|
index: beatIndex,
|
|
@@ -497,18 +694,24 @@ function createMulmoScriptServerOps(backend) {
|
|
|
497
694
|
if (!result.ok) genError = result.error;
|
|
498
695
|
return result;
|
|
499
696
|
} finally {
|
|
500
|
-
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, true,
|
|
697
|
+
publishGeneration(chatSessionId, "beatImage", filePath, mapKey, true, {
|
|
698
|
+
error: genError,
|
|
699
|
+
root
|
|
700
|
+
});
|
|
501
701
|
}
|
|
502
702
|
}
|
|
503
703
|
async function generateBeatAudioOp(args) {
|
|
504
|
-
const { filePath, beatIndex, force, chatSessionId } = args;
|
|
704
|
+
const { filePath, beatIndex, force, chatSessionId, root } = args;
|
|
705
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
706
|
+
if (rootGuard) return rootGuard;
|
|
505
707
|
const mapKey = String(beatIndex);
|
|
506
|
-
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, false);
|
|
708
|
+
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, false, { root });
|
|
507
709
|
let genError;
|
|
508
710
|
try {
|
|
509
711
|
const result = await runStoryOp(filePath, {
|
|
510
712
|
force,
|
|
511
|
-
operation: "generate-beat-audio"
|
|
713
|
+
operation: "generate-beat-audio",
|
|
714
|
+
root
|
|
512
715
|
}, async ({ context }) => {
|
|
513
716
|
await (0, mulmocast.generateBeatAudio)(beatIndex, context, { settings: process.env });
|
|
514
717
|
const beat = context.studio.script.beats[beatIndex];
|
|
@@ -531,17 +734,23 @@ function createMulmoScriptServerOps(backend) {
|
|
|
531
734
|
if (!result.ok) genError = result.error;
|
|
532
735
|
return result;
|
|
533
736
|
} finally {
|
|
534
|
-
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, true,
|
|
737
|
+
publishGeneration(chatSessionId, "beatAudio", filePath, mapKey, true, {
|
|
738
|
+
error: genError,
|
|
739
|
+
root
|
|
740
|
+
});
|
|
535
741
|
}
|
|
536
742
|
}
|
|
537
743
|
async function renderCharacterOp(args) {
|
|
538
|
-
const { filePath, key, force, chatSessionId } = args;
|
|
539
|
-
|
|
744
|
+
const { filePath, key, force, chatSessionId, root } = args;
|
|
745
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
746
|
+
if (rootGuard) return rootGuard;
|
|
747
|
+
publishGeneration(chatSessionId, "characterImage", filePath, key, false, { root });
|
|
540
748
|
let genError;
|
|
541
749
|
try {
|
|
542
750
|
const result = await runStoryOp(filePath, {
|
|
543
751
|
force,
|
|
544
|
-
operation: "render-character"
|
|
752
|
+
operation: "render-character",
|
|
753
|
+
root
|
|
545
754
|
}, async ({ context }) => {
|
|
546
755
|
const imageEntries = context.studio.script.imageParams?.images ?? {};
|
|
547
756
|
const imageEntry = imageEntries[key];
|
|
@@ -565,11 +774,17 @@ function createMulmoScriptServerOps(backend) {
|
|
|
565
774
|
if (!result.ok) genError = result.error;
|
|
566
775
|
return result;
|
|
567
776
|
} finally {
|
|
568
|
-
publishGeneration(chatSessionId, "characterImage", filePath, key, true,
|
|
777
|
+
publishGeneration(chatSessionId, "characterImage", filePath, key, true, {
|
|
778
|
+
error: genError,
|
|
779
|
+
root
|
|
780
|
+
});
|
|
569
781
|
}
|
|
570
782
|
}
|
|
571
|
-
async function uploadBeatImageOp(filePath, beatIndex, imageData) {
|
|
572
|
-
return runStoryOp(filePath, {
|
|
783
|
+
async function uploadBeatImageOp(filePath, beatIndex, imageData, root) {
|
|
784
|
+
return runStoryOp(filePath, {
|
|
785
|
+
operation: "upload-beat-image",
|
|
786
|
+
root
|
|
787
|
+
}, async ({ context }) => {
|
|
573
788
|
const { imagePath } = (0, mulmocast.getBeatPngImagePath)(context, beatIndex);
|
|
574
789
|
const base64 = stripDataUri(imageData);
|
|
575
790
|
await backend.writeFileAtomic(imagePath, Buffer.from(base64, "base64"));
|
|
@@ -579,8 +794,11 @@ function createMulmoScriptServerOps(backend) {
|
|
|
579
794
|
};
|
|
580
795
|
});
|
|
581
796
|
}
|
|
582
|
-
async function uploadCharacterImageOp(filePath, key, imageData) {
|
|
583
|
-
return runStoryOp(filePath, {
|
|
797
|
+
async function uploadCharacterImageOp(filePath, key, imageData, root) {
|
|
798
|
+
return runStoryOp(filePath, {
|
|
799
|
+
operation: "upload-character-image",
|
|
800
|
+
root
|
|
801
|
+
}, async ({ context }) => {
|
|
584
802
|
const imagePath = (0, mulmocast.getReferenceImagePath)(context, key, "png");
|
|
585
803
|
const base64 = stripDataUri(imageData);
|
|
586
804
|
await backend.writeFileAtomic(imagePath, Buffer.from(base64, "base64"));
|
|
@@ -629,54 +847,68 @@ function createMulmoScriptServerOps(backend) {
|
|
|
629
847
|
* initiating View (and any other mounted View) reloads assets off disk
|
|
630
848
|
* as they land — the successor of the SSE per-beat events.
|
|
631
849
|
*/
|
|
632
|
-
async function generateMovieOp(filePath, chatSessionId) {
|
|
850
|
+
async function generateMovieOp(filePath, chatSessionId, root) {
|
|
851
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
852
|
+
if (rootGuard) return rootGuard;
|
|
633
853
|
const ffmpeg = ffmpegGuard();
|
|
634
854
|
if (ffmpeg) return ffmpeg;
|
|
635
|
-
const resolved = resolveStory(filePath);
|
|
855
|
+
const resolved = resolveStory(filePath, root);
|
|
636
856
|
if (!resolved.ok) return resolved;
|
|
637
857
|
const absoluteFilePath = resolved.absolutePath;
|
|
638
858
|
if (inFlightMovies.has(absoluteFilePath)) return opBadRequest("Movie generation is already in progress for this script");
|
|
639
859
|
inFlightMovies.add(absoluteFilePath);
|
|
640
|
-
publishGeneration(chatSessionId, "movie", filePath, "", false);
|
|
860
|
+
publishGeneration(chatSessionId, "movie", filePath, "", false, { root });
|
|
641
861
|
let genError;
|
|
642
862
|
try {
|
|
643
863
|
const result = await runMovieGeneration(absoluteFilePath, (event) => {
|
|
644
|
-
publishGeneration(chatSessionId, event.kind === "image" ? "beatImage" : "beatAudio", filePath, String(event.beatIndex), true);
|
|
864
|
+
publishGeneration(chatSessionId, event.kind === "image" ? "beatImage" : "beatAudio", filePath, String(event.beatIndex), true, { root });
|
|
645
865
|
});
|
|
646
866
|
if (!result.ok) {
|
|
647
867
|
genError = result.error;
|
|
648
868
|
return opServerError(result.error);
|
|
649
869
|
}
|
|
870
|
+
const movieRef = toStoryRef(result.outputPath, root);
|
|
871
|
+
if (movieRef === null) return opServerError("generated movie is outside the registered stories root");
|
|
650
872
|
return {
|
|
651
873
|
ok: true,
|
|
652
|
-
moviePath:
|
|
874
|
+
moviePath: movieRef
|
|
653
875
|
};
|
|
654
876
|
} catch (err) {
|
|
655
877
|
genError = require_plugin.errorMessage(err);
|
|
656
878
|
return opServerError(genError);
|
|
657
879
|
} finally {
|
|
658
880
|
inFlightMovies.delete(absoluteFilePath);
|
|
659
|
-
publishGeneration(chatSessionId, "movie", filePath, "", true,
|
|
881
|
+
publishGeneration(chatSessionId, "movie", filePath, "", true, {
|
|
882
|
+
error: genError,
|
|
883
|
+
root
|
|
884
|
+
});
|
|
660
885
|
}
|
|
661
886
|
}
|
|
662
|
-
function triggerAutoBackgroundMovie(absoluteFilePath, wireFilePath, chatSessionId) {
|
|
887
|
+
function triggerAutoBackgroundMovie(absoluteFilePath, wireFilePath, chatSessionId, root) {
|
|
888
|
+
if (guardStoryGenerationRoot(root)) {
|
|
889
|
+
log.warn("refused an auto background movie in a non-default stories root", {
|
|
890
|
+
filePath: wireFilePath,
|
|
891
|
+
root
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
663
895
|
if (inFlightMovies.has(absoluteFilePath)) return;
|
|
664
896
|
inFlightMovies.add(absoluteFilePath);
|
|
665
|
-
runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId);
|
|
897
|
+
runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId, root);
|
|
666
898
|
}
|
|
667
|
-
async function runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId) {
|
|
899
|
+
async function runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId, root) {
|
|
668
900
|
const errorSidecarPath = `${absoluteFilePath}.error.txt`;
|
|
669
901
|
try {
|
|
670
902
|
(0, fs.unlinkSync)(errorSidecarPath);
|
|
671
903
|
} catch {}
|
|
672
|
-
publishGeneration(chatSessionId, "movie", wireFilePath, "", false);
|
|
904
|
+
publishGeneration(chatSessionId, "movie", wireFilePath, "", false, { root });
|
|
673
905
|
let genError;
|
|
674
906
|
try {
|
|
675
907
|
const result = await runMovieGeneration(absoluteFilePath, (event) => {
|
|
676
908
|
const eventKind = event.kind === "image" ? "beatImage" : "beatAudio";
|
|
677
909
|
const key = String(event.beatIndex);
|
|
678
|
-
publishGeneration(chatSessionId, eventKind, wireFilePath, key, false);
|
|
679
|
-
setImmediate(() => publishGeneration(chatSessionId, eventKind, wireFilePath, key, true));
|
|
910
|
+
publishGeneration(chatSessionId, eventKind, wireFilePath, key, false, { root });
|
|
911
|
+
setImmediate(() => publishGeneration(chatSessionId, eventKind, wireFilePath, key, true, { root }));
|
|
680
912
|
});
|
|
681
913
|
if (!result.ok) {
|
|
682
914
|
genError = result.error;
|
|
@@ -700,7 +932,10 @@ function createMulmoScriptServerOps(backend) {
|
|
|
700
932
|
});
|
|
701
933
|
} finally {
|
|
702
934
|
inFlightMovies.delete(absoluteFilePath);
|
|
703
|
-
publishGeneration(chatSessionId, "movie", wireFilePath, "", true,
|
|
935
|
+
publishGeneration(chatSessionId, "movie", wireFilePath, "", true, {
|
|
936
|
+
error: genError,
|
|
937
|
+
root
|
|
938
|
+
});
|
|
704
939
|
}
|
|
705
940
|
}
|
|
706
941
|
async function writeErrorSidecar(errorSidecarPath, message) {
|
|
@@ -736,15 +971,17 @@ function createMulmoScriptServerOps(backend) {
|
|
|
736
971
|
}
|
|
737
972
|
/** Long-held foreground PDF generation (the package View's `generatePdf`
|
|
738
973
|
* dispatch) — the PDF sibling of `generateMovieOp`. */
|
|
739
|
-
async function generatePdfOp(filePath, chatSessionId) {
|
|
974
|
+
async function generatePdfOp(filePath, chatSessionId, root) {
|
|
975
|
+
const rootGuard = guardStoryGenerationRoot(root);
|
|
976
|
+
if (rootGuard) return rootGuard;
|
|
740
977
|
const ffmpeg = ffmpegGuard();
|
|
741
978
|
if (ffmpeg) return ffmpeg;
|
|
742
|
-
const resolved = resolveStory(filePath);
|
|
979
|
+
const resolved = resolveStory(filePath, root);
|
|
743
980
|
if (!resolved.ok) return resolved;
|
|
744
981
|
const absoluteFilePath = resolved.absolutePath;
|
|
745
982
|
if (inFlightPdfs.has(absoluteFilePath)) return opBadRequest("PDF generation is already in progress for this script");
|
|
746
983
|
inFlightPdfs.add(absoluteFilePath);
|
|
747
|
-
publishGeneration(chatSessionId, "pdf", filePath, "", false);
|
|
984
|
+
publishGeneration(chatSessionId, "pdf", filePath, "", false, { root });
|
|
748
985
|
let genError;
|
|
749
986
|
try {
|
|
750
987
|
const context = await buildContext(absoluteFilePath);
|
|
@@ -753,22 +990,27 @@ function createMulmoScriptServerOps(backend) {
|
|
|
753
990
|
return opServerError(genError);
|
|
754
991
|
}
|
|
755
992
|
const result = await runPdfGeneration(context, (beatIndex) => {
|
|
756
|
-
publishGeneration(chatSessionId, "beatImage", filePath, String(beatIndex), true);
|
|
993
|
+
publishGeneration(chatSessionId, "beatImage", filePath, String(beatIndex), true, { root });
|
|
757
994
|
});
|
|
758
995
|
if (!result.ok) {
|
|
759
996
|
genError = result.error;
|
|
760
997
|
return opServerError(result.error);
|
|
761
998
|
}
|
|
999
|
+
const pdfRef = toStoryRef(result.outputPath, root);
|
|
1000
|
+
if (pdfRef === null) return opServerError("generated PDF is outside the registered stories root");
|
|
762
1001
|
return {
|
|
763
1002
|
ok: true,
|
|
764
|
-
pdfPath:
|
|
1003
|
+
pdfPath: pdfRef
|
|
765
1004
|
};
|
|
766
1005
|
} catch (err) {
|
|
767
1006
|
genError = require_plugin.errorMessage(err);
|
|
768
1007
|
return opServerError(genError);
|
|
769
1008
|
} finally {
|
|
770
1009
|
inFlightPdfs.delete(absoluteFilePath);
|
|
771
|
-
publishGeneration(chatSessionId, "pdf", filePath, "", true,
|
|
1010
|
+
publishGeneration(chatSessionId, "pdf", filePath, "", true, {
|
|
1011
|
+
error: genError,
|
|
1012
|
+
root
|
|
1013
|
+
});
|
|
772
1014
|
}
|
|
773
1015
|
}
|
|
774
1016
|
return {
|
|
@@ -776,6 +1018,10 @@ function createMulmoScriptServerOps(backend) {
|
|
|
776
1018
|
toStoryRef,
|
|
777
1019
|
resolveStory,
|
|
778
1020
|
guardStoryWirePath,
|
|
1021
|
+
guardStoryRootRegistered,
|
|
1022
|
+
guardStoryWriteRoot,
|
|
1023
|
+
artifactsForRoot,
|
|
1024
|
+
guardStoryGenerationRoot,
|
|
779
1025
|
ffmpegGuard,
|
|
780
1026
|
runStoryOp,
|
|
781
1027
|
publishGeneration,
|
|
@@ -874,12 +1120,56 @@ var UPLOAD_KINDS = /* @__PURE__ */ new Set(["uploadBeatImage", "uploadCharacterI
|
|
|
874
1120
|
* FileOps, guarded by the instance's realpath containment
|
|
875
1121
|
* (`guardStoryWirePath`) — the core's own guard is lexical.
|
|
876
1122
|
*/
|
|
1123
|
+
/**
|
|
1124
|
+
* Stamp the root a successful result acted in.
|
|
1125
|
+
*
|
|
1126
|
+
* Only on success: a failure carries `code` and `error`, and adding a root to
|
|
1127
|
+
* it would invite a reader to treat the pair as addressable when the call did
|
|
1128
|
+
* not happen. Only when NON-default, so a result for a call that named no root
|
|
1129
|
+
* stays byte-identical to what this package returned before roots existed —
|
|
1130
|
+
* which is what keeps every existing card working untouched.
|
|
1131
|
+
*/
|
|
1132
|
+
function withRoot(result, root) {
|
|
1133
|
+
const normalized = require_contract.normalizeRoot(root);
|
|
1134
|
+
if (normalized === "") return result;
|
|
1135
|
+
if (!isRecord(result) || result.ok !== true) return result;
|
|
1136
|
+
return {
|
|
1137
|
+
...result,
|
|
1138
|
+
root: normalized
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
/** `undefined` when `root` is absent or a string; a failure envelope otherwise. */
|
|
1142
|
+
function guardSuppliedRoot(root) {
|
|
1143
|
+
if (root === void 0 || typeof root === "string") return void 0;
|
|
1144
|
+
return {
|
|
1145
|
+
ok: false,
|
|
1146
|
+
code: "bad_request",
|
|
1147
|
+
error: `mulmoScript root must be a string, got ${typeof root}`
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
877
1150
|
function createMulmoScriptDispatchHandler(ops) {
|
|
878
|
-
|
|
1151
|
+
/**
|
|
1152
|
+
* The executor context for a write, bound to the root it names.
|
|
1153
|
+
*
|
|
1154
|
+
* One `FileOps` was held for the whole handler, so the executors — which
|
|
1155
|
+
* take a WIRE path (`stories/…`) and resolve it against whatever FileOps
|
|
1156
|
+
* they are given — wrote a named root's script into the DEFAULT root's
|
|
1157
|
+
* identically-named file. Choosing here keeps `MulmoScriptExecuteContext`
|
|
1158
|
+
* and every executor unchanged: the root never reaches them, only the right
|
|
1159
|
+
* FileOps does (#3019).
|
|
1160
|
+
*/
|
|
1161
|
+
function executeContextFor(root) {
|
|
1162
|
+
const artifacts = ops.artifactsForRoot(root);
|
|
1163
|
+
return artifacts === null ? null : { files: { artifacts } };
|
|
1164
|
+
}
|
|
879
1165
|
async function saveKind(args) {
|
|
880
|
-
const
|
|
1166
|
+
const rootGuard = ops.guardStoryWriteRoot(str(args.root));
|
|
1167
|
+
if (rootGuard) return fromOpFailure(rootGuard);
|
|
1168
|
+
const guard = ops.guardStoryWirePath(args.filePath, str(args.root));
|
|
881
1169
|
if (guard) return fromOpFailure(guard);
|
|
882
|
-
const
|
|
1170
|
+
const context = executeContextFor(str(args.root));
|
|
1171
|
+
if (context === null) return invalidArgs("save");
|
|
1172
|
+
const outcome = await require_plugin.executeMulmoScriptSave(context, {
|
|
883
1173
|
script: args.script,
|
|
884
1174
|
filename: str(args.filename),
|
|
885
1175
|
filePath: str(args.filePath)
|
|
@@ -893,11 +1183,15 @@ function createMulmoScriptDispatchHandler(ops) {
|
|
|
893
1183
|
};
|
|
894
1184
|
}
|
|
895
1185
|
async function updateKind(kind, args) {
|
|
896
|
-
const
|
|
1186
|
+
const rootGuard = ops.guardStoryWriteRoot(str(args.root));
|
|
1187
|
+
if (rootGuard) return fromOpFailure(rootGuard);
|
|
1188
|
+
const guard = ops.guardStoryWirePath(args.filePath, str(args.root));
|
|
897
1189
|
if (guard) return fromOpFailure(guard);
|
|
898
|
-
const
|
|
1190
|
+
const context = executeContextFor(str(args.root));
|
|
1191
|
+
if (context === null) return invalidArgs(kind);
|
|
1192
|
+
const outcome = kind === "updateBeat" ? await require_plugin.executeUpdateBeat(context, args) : await require_plugin.executeUpdateScript(context, args);
|
|
899
1193
|
if (!outcome.ok) return fromPackageFailure(outcome);
|
|
900
|
-
ops.publishScriptChanged(str(args.filePath) ?? "", str(args.origin));
|
|
1194
|
+
ops.publishScriptChanged(str(args.filePath) ?? "", str(args.origin), str(args.root));
|
|
901
1195
|
return { ok: true };
|
|
902
1196
|
}
|
|
903
1197
|
const STATUS_OPS = {
|
|
@@ -913,30 +1207,37 @@ function createMulmoScriptDispatchHandler(ops) {
|
|
|
913
1207
|
const statusOp = STATUS_OPS[kind];
|
|
914
1208
|
if (statusOp) {
|
|
915
1209
|
const filePath = str(args.filePath);
|
|
916
|
-
return filePath ? envelope(await statusOp(filePath)) : invalidArgs(kind);
|
|
1210
|
+
return filePath ? envelope(await statusOp(filePath, str(args.root))) : invalidArgs(kind);
|
|
917
1211
|
}
|
|
918
1212
|
if (kind === "characterImage") {
|
|
919
1213
|
const parsed = keyArgs(args);
|
|
920
|
-
return parsed ? envelope(await ops.characterImageOp(parsed.filePath, parsed.key)) : invalidArgs(kind);
|
|
1214
|
+
return parsed ? envelope(await ops.characterImageOp(parsed.filePath, parsed.key, str(args.root))) : invalidArgs(kind);
|
|
921
1215
|
}
|
|
922
1216
|
const parsed = beatArgs(args);
|
|
923
1217
|
if (!parsed) return invalidArgs(kind);
|
|
924
|
-
return envelope(await BEAT_PROBE_OPS[kind](parsed.filePath, parsed.beatIndex));
|
|
1218
|
+
return envelope(await BEAT_PROBE_OPS[kind](parsed.filePath, parsed.beatIndex, str(args.root)));
|
|
1219
|
+
}
|
|
1220
|
+
/** Movie and PDF take the whole script; the other generate kinds take a beat
|
|
1221
|
+
* or a character within it. */
|
|
1222
|
+
async function wholeScriptGenerationKind(kind, args) {
|
|
1223
|
+
const filePath = str(args.filePath);
|
|
1224
|
+
if (!filePath) return invalidArgs(kind);
|
|
1225
|
+
const chatSessionId = str(args.chatSessionId);
|
|
1226
|
+
const root = str(args.root);
|
|
1227
|
+
return envelope(kind === "generateMovie" ? await ops.generateMovieOp(filePath, chatSessionId, root) : await ops.generatePdfOp(filePath, chatSessionId, root));
|
|
925
1228
|
}
|
|
926
1229
|
async function generateKind(kind, args) {
|
|
1230
|
+
if (kind === "generateMovie" || kind === "generatePdf") return wholeScriptGenerationKind(kind, args);
|
|
927
1231
|
const chatSessionId = str(args.chatSessionId);
|
|
1232
|
+
const root = str(args.root);
|
|
928
1233
|
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
1234
|
if (kind === "renderCharacter") {
|
|
935
1235
|
const parsed = keyArgs(args);
|
|
936
1236
|
return parsed ? envelope(await ops.renderCharacterOp({
|
|
937
1237
|
...parsed,
|
|
938
1238
|
force,
|
|
939
|
-
chatSessionId
|
|
1239
|
+
chatSessionId,
|
|
1240
|
+
root
|
|
940
1241
|
})) : invalidArgs(kind);
|
|
941
1242
|
}
|
|
942
1243
|
const parsed = beatArgs(args);
|
|
@@ -944,11 +1245,13 @@ function createMulmoScriptDispatchHandler(ops) {
|
|
|
944
1245
|
return envelope(kind === "renderBeat" ? await ops.renderBeatOp({
|
|
945
1246
|
...parsed,
|
|
946
1247
|
force,
|
|
947
|
-
chatSessionId
|
|
1248
|
+
chatSessionId,
|
|
1249
|
+
root
|
|
948
1250
|
}) : await ops.generateBeatAudioOp({
|
|
949
1251
|
...parsed,
|
|
950
1252
|
force,
|
|
951
|
-
chatSessionId
|
|
1253
|
+
chatSessionId,
|
|
1254
|
+
root
|
|
952
1255
|
}));
|
|
953
1256
|
}
|
|
954
1257
|
async function uploadKind(kind, args) {
|
|
@@ -956,33 +1259,43 @@ function createMulmoScriptDispatchHandler(ops) {
|
|
|
956
1259
|
if (!imageData) return invalidArgs(kind);
|
|
957
1260
|
if (kind === "uploadCharacterImage") {
|
|
958
1261
|
const parsed = keyArgs(args);
|
|
959
|
-
return parsed ? envelope(await ops.uploadCharacterImageOp(parsed.filePath, parsed.key, imageData)) : invalidArgs(kind);
|
|
1262
|
+
return parsed ? envelope(await ops.uploadCharacterImageOp(parsed.filePath, parsed.key, imageData, str(args.root))) : invalidArgs(kind);
|
|
960
1263
|
}
|
|
961
1264
|
const parsed = beatArgs(args);
|
|
962
1265
|
if (!parsed) return invalidArgs(kind);
|
|
963
|
-
return envelope(await ops.uploadBeatImageOp(parsed.filePath, parsed.beatIndex, imageData));
|
|
1266
|
+
return envelope(await ops.uploadBeatImageOp(parsed.filePath, parsed.beatIndex, imageData, str(args.root)));
|
|
964
1267
|
}
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
if (!
|
|
1268
|
+
async function pendingKind(kind, args) {
|
|
1269
|
+
const filePath = str(args.filePath);
|
|
1270
|
+
if (!filePath) return invalidArgs(kind);
|
|
1271
|
+
const root = str(args.root);
|
|
1272
|
+
const rootGuard = ops.guardStoryRootRegistered(root);
|
|
1273
|
+
if (rootGuard) return fromOpFailure(rootGuard);
|
|
1274
|
+
return {
|
|
1275
|
+
ok: true,
|
|
1276
|
+
pending: ops.pendingGenerations(filePath, root)
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
/** Which handler serves this kind. Routing only — the caller tags the answer. */
|
|
1280
|
+
async function route(kind, args) {
|
|
968
1281
|
if (kind === "save") return saveKind(args);
|
|
969
1282
|
if (kind === "updateBeat" || kind === "updateScript") return updateKind(kind, args);
|
|
970
1283
|
if (PROBE_KINDS.has(kind)) return probeKind(kind, args);
|
|
971
1284
|
if (GENERATE_KINDS.has(kind)) return generateKind(kind, args);
|
|
972
1285
|
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
|
-
}
|
|
1286
|
+
if (kind === "pendingGenerations") return pendingKind(kind, args);
|
|
981
1287
|
return {
|
|
982
1288
|
ok: false,
|
|
983
1289
|
code: "bad_request",
|
|
984
1290
|
error: `unknown mulmoScript dispatch kind "${kind}"`
|
|
985
1291
|
};
|
|
1292
|
+
}
|
|
1293
|
+
return async (args) => {
|
|
1294
|
+
const kind = str(args.kind);
|
|
1295
|
+
if (!kind) return invalidArgs("<missing>");
|
|
1296
|
+
const malformedRoot = guardSuppliedRoot(args.root);
|
|
1297
|
+
if (malformedRoot) return malformedRoot;
|
|
1298
|
+
return withRoot(await route(kind, args), str(args.root));
|
|
986
1299
|
};
|
|
987
1300
|
}
|
|
988
1301
|
//#endregion
|