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