@scenar/cli 0.9.1 → 0.11.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/example-bundle/assets/index-DxmqjG-8.js +388 -0
- package/example-bundle/assets/style-B2sxnVVZ.css +1 -0
- package/example-bundle/embed.js +1 -1
- package/example-bundle/index.html +2 -2
- package/example-bundle/logo.png +0 -0
- package/example-bundle/pack-manifest.json +33 -9
- package/example-bundle/soundtrack/music.mp3 +0 -0
- package/example-bundle/soundtrack/sfx/click.mp3 +0 -0
- package/example-bundle/soundtrack/sfx/keystroke.mp3 +0 -0
- package/package.json +4 -4
- package/src/__tests__/collect-pack-shots.test.ts +27 -1
- package/src/__tests__/elevenlabs-provider.test.ts +187 -0
- package/src/__tests__/embed-snippet.test.ts +5 -0
- package/src/__tests__/find-authored-soundtrack.test.ts +54 -0
- package/src/__tests__/find-authored-title-cards.test.ts +65 -0
- package/src/__tests__/find-authored-viewport.test.ts +58 -0
- package/src/__tests__/load-bundle.test.ts +67 -0
- package/src/__tests__/narrate-command.test.ts +65 -1
- package/src/__tests__/narration-cache.test.ts +134 -0
- package/src/__tests__/pack-bundle-contract.test.ts +2 -0
- package/src/__tests__/pack-generate-embed-entry.test.ts +99 -16
- package/src/__tests__/presenter-command.test.ts +366 -0
- package/src/__tests__/render-command.test.ts +11 -0
- package/src/__tests__/render-generate-entry.test.ts +102 -0
- package/src/__tests__/resolve-pack-viewport.test.ts +46 -0
- package/src/__tests__/resolve-provider.test.ts +32 -3
- package/src/__tests__/scenario-validator.test.ts +174 -0
- package/src/__tests__/stage-assets.test.ts +269 -0
- package/src/commands/narrate.js +1 -1
- package/src/commands/narrate.js.map +1 -1
- package/src/commands/narrate.ts +1 -1
- package/src/commands/pack.d.ts.map +1 -1
- package/src/commands/pack.js +23 -4
- package/src/commands/pack.js.map +1 -1
- package/src/commands/pack.ts +26 -5
- package/src/commands/presenter.d.ts +3 -0
- package/src/commands/presenter.d.ts.map +1 -0
- package/src/commands/presenter.js +89 -0
- package/src/commands/presenter.js.map +1 -0
- package/src/commands/presenter.ts +107 -0
- package/src/commands/render.d.ts.map +1 -1
- package/src/commands/render.js +2 -0
- package/src/commands/render.js.map +1 -1
- package/src/commands/render.ts +6 -0
- package/src/embed/embed-snippet.d.ts +5 -0
- package/src/embed/embed-snippet.d.ts.map +1 -1
- package/src/embed/embed-snippet.js +6 -1
- package/src/embed/embed-snippet.js.map +1 -1
- package/src/embed/embed-snippet.ts +6 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +2 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +2 -0
- package/src/narrate/run-narrate.d.ts +15 -1
- package/src/narrate/run-narrate.d.ts.map +1 -1
- package/src/narrate/run-narrate.js +11 -4
- package/src/narrate/run-narrate.js.map +1 -1
- package/src/narrate/run-narrate.ts +15 -6
- package/src/pack/bundle-contract.d.ts +1 -0
- package/src/pack/bundle-contract.d.ts.map +1 -1
- package/src/pack/bundle-contract.js +3 -0
- package/src/pack/bundle-contract.js.map +1 -1
- package/src/pack/bundle-contract.ts +3 -0
- package/src/pack/collect-pack-shots.d.ts +16 -5
- package/src/pack/collect-pack-shots.d.ts.map +1 -1
- package/src/pack/collect-pack-shots.js +13 -4
- package/src/pack/collect-pack-shots.js.map +1 -1
- package/src/pack/collect-pack-shots.ts +35 -8
- package/src/pack/generate-embed-entry.d.ts +7 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -1
- package/src/pack/generate-embed-entry.js +105 -7
- package/src/pack/generate-embed-entry.js.map +1 -1
- package/src/pack/generate-embed-entry.ts +110 -7
- package/src/pack/run-pack.d.ts +8 -2
- package/src/pack/run-pack.d.ts.map +1 -1
- package/src/pack/run-pack.js +120 -17
- package/src/pack/run-pack.js.map +1 -1
- package/src/pack/run-pack.ts +164 -19
- package/src/pack/viewport.d.ts +17 -0
- package/src/pack/viewport.d.ts.map +1 -1
- package/src/pack/viewport.js +19 -0
- package/src/pack/viewport.js.map +1 -1
- package/src/pack/viewport.ts +29 -0
- package/src/presenter/heygen-client.d.ts +69 -0
- package/src/presenter/heygen-client.d.ts.map +1 -0
- package/src/presenter/heygen-client.js +154 -0
- package/src/presenter/heygen-client.js.map +1 -0
- package/src/presenter/heygen-client.ts +224 -0
- package/src/presenter/run-presenter.d.ts +67 -0
- package/src/presenter/run-presenter.d.ts.map +1 -0
- package/src/presenter/run-presenter.js +348 -0
- package/src/presenter/run-presenter.js.map +1 -0
- package/src/presenter/run-presenter.ts +536 -0
- package/src/render/generate-entry.d.ts +4 -0
- package/src/render/generate-entry.d.ts.map +1 -1
- package/src/render/generate-entry.js +75 -7
- package/src/render/generate-entry.js.map +1 -1
- package/src/render/generate-entry.ts +84 -7
- package/src/render/run-render.d.ts +6 -0
- package/src/render/run-render.d.ts.map +1 -1
- package/src/render/run-render.js +58 -0
- package/src/render/run-render.js.map +1 -1
- package/src/render/run-render.ts +72 -0
- package/src/render/stage-assets.d.ts +44 -0
- package/src/render/stage-assets.d.ts.map +1 -0
- package/src/render/stage-assets.js +91 -0
- package/src/render/stage-assets.js.map +1 -0
- package/src/render/stage-assets.ts +130 -0
- package/src/tts/echogarden.d.ts.map +1 -1
- package/src/tts/echogarden.js +4 -2
- package/src/tts/echogarden.js.map +1 -1
- package/src/tts/echogarden.ts +5 -2
- package/src/tts/edge-tts.d.ts.map +1 -1
- package/src/tts/edge-tts.js +2 -1
- package/src/tts/edge-tts.js.map +1 -1
- package/src/tts/edge-tts.ts +2 -1
- package/src/tts/elevenlabs.d.ts +15 -0
- package/src/tts/elevenlabs.d.ts.map +1 -0
- package/src/tts/elevenlabs.js +73 -0
- package/src/tts/elevenlabs.js.map +1 -0
- package/src/tts/elevenlabs.ts +105 -0
- package/src/tts/openai.d.ts.map +1 -1
- package/src/tts/openai.js +2 -1
- package/src/tts/openai.js.map +1 -1
- package/src/tts/openai.ts +2 -1
- package/src/tts/resolve-provider.d.ts +1 -1
- package/src/tts/resolve-provider.d.ts.map +1 -1
- package/src/tts/resolve-provider.js +10 -3
- package/src/tts/resolve-provider.js.map +1 -1
- package/src/tts/resolve-provider.ts +12 -4
- package/src/tts/types.d.ts +9 -0
- package/src/tts/types.d.ts.map +1 -1
- package/src/tts/types.ts +10 -0
- package/src/util/confirm.d.ts +11 -0
- package/src/util/confirm.d.ts.map +1 -0
- package/src/util/confirm.js +24 -0
- package/src/util/confirm.js.map +1 -0
- package/src/util/confirm.ts +23 -0
- package/src/util/load-bundle.d.ts +17 -1
- package/src/util/load-bundle.d.ts.map +1 -1
- package/src/util/load-bundle.js +19 -11
- package/src/util/load-bundle.js.map +1 -1
- package/src/util/load-bundle.ts +38 -12
- package/src/util/load-ts.d.ts +100 -0
- package/src/util/load-ts.d.ts.map +1 -1
- package/src/util/load-ts.js +176 -0
- package/src/util/load-ts.js.map +1 -1
- package/src/util/load-ts.ts +227 -0
- package/src/util/narration-cache.d.ts +13 -5
- package/src/util/narration-cache.d.ts.map +1 -1
- package/src/util/narration-cache.js +10 -8
- package/src/util/narration-cache.js.map +1 -1
- package/src/util/narration-cache.ts +22 -6
- package/src/util/presenter-cache.d.ts +44 -0
- package/src/util/presenter-cache.d.ts.map +1 -0
- package/src/util/presenter-cache.js +43 -0
- package/src/util/presenter-cache.js.map +1 -0
- package/src/util/presenter-cache.ts +101 -0
- package/src/util/scenario-assets.d.ts +47 -0
- package/src/util/scenario-assets.d.ts.map +1 -0
- package/src/util/scenario-assets.js +71 -0
- package/src/util/scenario-assets.js.map +1 -0
- package/src/util/scenario-assets.ts +111 -0
- package/src/util/soundtrack-assets.d.ts +34 -0
- package/src/util/soundtrack-assets.d.ts.map +1 -0
- package/src/util/soundtrack-assets.js +48 -0
- package/src/util/soundtrack-assets.js.map +1 -0
- package/src/util/soundtrack-assets.ts +69 -0
- package/src/validate/scenario-validator.d.ts.map +1 -1
- package/src/validate/scenario-validator.js +82 -0
- package/src/validate/scenario-validator.js.map +1 -1
- package/src/validate/scenario-validator.ts +110 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/example-bundle/assets/index-DlmG1Uh7.js +0 -388
- package/example-bundle/assets/style-BjquDnTE.css +0 -1
|
@@ -44,6 +44,66 @@ describe("validateScenario", () => {
|
|
|
44
44
|
expect(validateScenario(s).valid).toBe(true);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
// --- Presenter (per-step opt-in, requires narration) ---
|
|
48
|
+
|
|
49
|
+
it("accepts presenter: true on a narrated step", () => {
|
|
50
|
+
const s = validScenario();
|
|
51
|
+
s.steps[0] = { ...s.steps[0]!, presenter: true } as typeof s.steps[0];
|
|
52
|
+
expect(validateScenario(s).valid).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("accepts presenter: false on a step without narration", () => {
|
|
56
|
+
const s = validScenario();
|
|
57
|
+
s.steps[0] = {
|
|
58
|
+
view: "intro",
|
|
59
|
+
delayMs: 0,
|
|
60
|
+
presenter: false,
|
|
61
|
+
} as unknown as typeof s.steps[0];
|
|
62
|
+
expect(validateScenario(s).valid).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("rejects presenter: true on a step without narration, naming step, field, and rule", () => {
|
|
66
|
+
const s = validScenario();
|
|
67
|
+
s.steps[0] = {
|
|
68
|
+
view: "intro",
|
|
69
|
+
delayMs: 0,
|
|
70
|
+
presenter: true,
|
|
71
|
+
} as unknown as typeof s.steps[0];
|
|
72
|
+
|
|
73
|
+
const result = validateScenario(s);
|
|
74
|
+
expect(result.valid).toBe(false);
|
|
75
|
+
expect(result.errors[0]!.path).toBe("steps[0].presenter");
|
|
76
|
+
expect(result.errors[0]!.reason).toBe(
|
|
77
|
+
"presenter requires narration_text — a presenter clip is derived from the step's narration audio.",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("rejects presenter: true when narrationText is empty", () => {
|
|
82
|
+
const s = validScenario();
|
|
83
|
+
s.steps[0] = {
|
|
84
|
+
...s.steps[0]!,
|
|
85
|
+
narrationText: "",
|
|
86
|
+
presenter: true,
|
|
87
|
+
} as typeof s.steps[0];
|
|
88
|
+
|
|
89
|
+
const result = validateScenario(s);
|
|
90
|
+
expect(result.valid).toBe(false);
|
|
91
|
+
expect(result.errors[0]!.path).toBe("steps[0].presenter");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects a non-boolean presenter", () => {
|
|
95
|
+
const s = validScenario();
|
|
96
|
+
s.steps[0] = {
|
|
97
|
+
...s.steps[0]!,
|
|
98
|
+
presenter: "yes",
|
|
99
|
+
} as unknown as typeof s.steps[0];
|
|
100
|
+
|
|
101
|
+
const result = validateScenario(s);
|
|
102
|
+
expect(result.valid).toBe(false);
|
|
103
|
+
expect(result.errors[0]!.path).toBe("steps[0].presenter");
|
|
104
|
+
expect(result.errors[0]!.reason).toBe("presenter must be a boolean.");
|
|
105
|
+
});
|
|
106
|
+
|
|
47
107
|
// --- Root-level errors ---
|
|
48
108
|
|
|
49
109
|
it("rejects null", () => {
|
|
@@ -349,4 +409,118 @@ describe("validateScenario", () => {
|
|
|
349
409
|
expect(result.valid).toBe(false);
|
|
350
410
|
expect(result.errors.length).toBeGreaterThanOrEqual(4);
|
|
351
411
|
});
|
|
412
|
+
|
|
413
|
+
// --- Soundtrack ---
|
|
414
|
+
|
|
415
|
+
it("accepts a valid soundtrack, including explicit 0.0 volumes", () => {
|
|
416
|
+
const result = validateScenario({
|
|
417
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
418
|
+
soundtrack: {
|
|
419
|
+
musicSrc: "./soundtrack/music.mp3",
|
|
420
|
+
musicVolume: 0.25,
|
|
421
|
+
duckingVolume: 0,
|
|
422
|
+
sfx: true,
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
expect(result.valid).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("accepts a soundtrack with only sfx (no music)", () => {
|
|
429
|
+
const result = validateScenario({
|
|
430
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
431
|
+
soundtrack: { sfx: true },
|
|
432
|
+
});
|
|
433
|
+
expect(result.valid).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("rejects a non-object soundtrack", () => {
|
|
437
|
+
const result = validateScenario({
|
|
438
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
439
|
+
soundtrack: "music.mp3",
|
|
440
|
+
});
|
|
441
|
+
expect(result.errors).toContainEqual(
|
|
442
|
+
expect.objectContaining({ path: "soundtrack" }),
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("rejects an empty musicSrc", () => {
|
|
447
|
+
const result = validateScenario({
|
|
448
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
449
|
+
soundtrack: { musicSrc: "" },
|
|
450
|
+
});
|
|
451
|
+
expect(result.errors).toContainEqual(
|
|
452
|
+
expect.objectContaining({ path: "soundtrack.musicSrc" }),
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it.each([
|
|
457
|
+
["musicVolume above 1", { musicVolume: 1.2 }, "soundtrack.musicVolume"],
|
|
458
|
+
["musicVolume negative", { musicVolume: -0.1 }, "soundtrack.musicVolume"],
|
|
459
|
+
["musicVolume non-numeric", { musicVolume: "loud" }, "soundtrack.musicVolume"],
|
|
460
|
+
["duckingVolume above 1", { duckingVolume: 2 }, "soundtrack.duckingVolume"],
|
|
461
|
+
])("rejects %s", (_name, soundtrack, expectedPath) => {
|
|
462
|
+
const result = validateScenario({
|
|
463
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
464
|
+
soundtrack,
|
|
465
|
+
});
|
|
466
|
+
expect(result.errors).toContainEqual(
|
|
467
|
+
expect.objectContaining({ path: expectedPath }),
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("rejects a non-boolean sfx", () => {
|
|
472
|
+
const result = validateScenario({
|
|
473
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
474
|
+
soundtrack: { sfx: "yes" },
|
|
475
|
+
});
|
|
476
|
+
expect(result.errors).toContainEqual(
|
|
477
|
+
expect.objectContaining({ path: "soundtrack.sfx" }),
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("accepts valid title cards, one-sided or both", () => {
|
|
482
|
+
for (const titleCards of [
|
|
483
|
+
{ intro: { title: "Acme", subtitle: "Fast", logoSrc: "./logo.png" } },
|
|
484
|
+
{ outro: { title: "Try it", ctaText: "acme.dev", durationMs: 4000 } },
|
|
485
|
+
{ intro: { title: "Acme" }, outro: { title: "Bye" } },
|
|
486
|
+
]) {
|
|
487
|
+
const result = validateScenario({
|
|
488
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
489
|
+
titleCards,
|
|
490
|
+
});
|
|
491
|
+
expect(result.valid).toBe(true);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("rejects a non-object titleCards", () => {
|
|
496
|
+
const result = validateScenario({
|
|
497
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
498
|
+
titleCards: "intro",
|
|
499
|
+
});
|
|
500
|
+
expect(result.errors).toContainEqual(
|
|
501
|
+
expect.objectContaining({ path: "titleCards" }),
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it.each([
|
|
506
|
+
["a missing title", { intro: {} }, "titleCards.intro.title"],
|
|
507
|
+
["an empty title", { outro: { title: "" } }, "titleCards.outro.title"],
|
|
508
|
+
["an empty subtitle", { intro: { title: "T", subtitle: "" } }, "titleCards.intro.subtitle"],
|
|
509
|
+
["an empty logoSrc", { intro: { title: "T", logoSrc: "" } }, "titleCards.intro.logoSrc"],
|
|
510
|
+
["an empty ctaText", { outro: { title: "T", ctaText: "" } }, "titleCards.outro.ctaText"],
|
|
511
|
+
["a zero durationMs", { intro: { title: "T", durationMs: 0 } }, "titleCards.intro.durationMs"],
|
|
512
|
+
[
|
|
513
|
+
"a negative durationMs",
|
|
514
|
+
{ intro: { title: "T", durationMs: -100 } },
|
|
515
|
+
"titleCards.intro.durationMs",
|
|
516
|
+
],
|
|
517
|
+
])("rejects a card with %s", (_name, titleCards, expectedPath) => {
|
|
518
|
+
const result = validateScenario({
|
|
519
|
+
steps: [{ view: "x", delayMs: 0 }],
|
|
520
|
+
titleCards,
|
|
521
|
+
});
|
|
522
|
+
expect(result.errors).toContainEqual(
|
|
523
|
+
expect.objectContaining({ path: expectedPath }),
|
|
524
|
+
);
|
|
525
|
+
});
|
|
352
526
|
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, writeFile, rm, access } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { stageRenderAssets } from "../render/stage-assets.js";
|
|
7
|
+
import {
|
|
8
|
+
SFX_DEST_PATHS,
|
|
9
|
+
isRemoteUrl,
|
|
10
|
+
resolveMusicAsset,
|
|
11
|
+
resolveSfxAssetPaths,
|
|
12
|
+
} from "../util/soundtrack-assets.js";
|
|
13
|
+
import { resolveLogoAsset } from "../util/scenario-assets.js";
|
|
14
|
+
|
|
15
|
+
let scenarioDir: string;
|
|
16
|
+
let publicDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
scenarioDir = await mkdtemp(join(tmpdir(), "scenar-stage-"));
|
|
20
|
+
publicDir = join(scenarioDir, ".scenar-render", "public");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await rm(scenarioDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async function exists(path: string): Promise<boolean> {
|
|
28
|
+
return access(path).then(
|
|
29
|
+
() => true,
|
|
30
|
+
() => false,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("stageRenderAssets", () => {
|
|
35
|
+
it("stages narration clips flat at the public root (srcs are ./step-N.mp3)", async () => {
|
|
36
|
+
await mkdir(join(scenarioDir, "narration"), { recursive: true });
|
|
37
|
+
await writeFile(join(scenarioDir, "narration", "step-0.mp3"), "audio-0");
|
|
38
|
+
await writeFile(join(scenarioDir, "narration", "step-2.mp3"), "audio-2");
|
|
39
|
+
await writeFile(join(scenarioDir, "narration", "manifest.json"), "{}");
|
|
40
|
+
|
|
41
|
+
const staged = await stageRenderAssets({
|
|
42
|
+
scenarioDir,
|
|
43
|
+
publicDir,
|
|
44
|
+
hasNarration: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(staged).toBe(2);
|
|
48
|
+
expect(await readFile(join(publicDir, "step-0.mp3"), "utf-8")).toBe("audio-0");
|
|
49
|
+
expect(await readFile(join(publicDir, "step-2.mp3"), "utf-8")).toBe("audio-2");
|
|
50
|
+
// The manifest itself is imported by the entry, never served.
|
|
51
|
+
expect(await exists(join(publicDir, "manifest.json"))).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("stages presenter clips flat at the public root beside narration (srcs are ./step-N.mp4)", async () => {
|
|
55
|
+
await mkdir(join(scenarioDir, "narration"), { recursive: true });
|
|
56
|
+
await writeFile(join(scenarioDir, "narration", "step-0.mp3"), "audio-0");
|
|
57
|
+
await writeFile(join(scenarioDir, "narration", "manifest.json"), "{}");
|
|
58
|
+
await mkdir(join(scenarioDir, "presenter"), { recursive: true });
|
|
59
|
+
await writeFile(join(scenarioDir, "presenter", "step-0.mp4"), "clip-0");
|
|
60
|
+
await writeFile(join(scenarioDir, "presenter", "manifest.json"), "{}");
|
|
61
|
+
await writeFile(join(scenarioDir, "presenter", ".presenter-cache.json"), "{}");
|
|
62
|
+
|
|
63
|
+
const staged = await stageRenderAssets({
|
|
64
|
+
scenarioDir,
|
|
65
|
+
publicDir,
|
|
66
|
+
hasNarration: true,
|
|
67
|
+
hasPresenter: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// step-0.mp3 + step-0.mp4 — same basename, different extensions, no collision.
|
|
71
|
+
expect(staged).toBe(2);
|
|
72
|
+
expect(await readFile(join(publicDir, "step-0.mp3"), "utf-8")).toBe("audio-0");
|
|
73
|
+
expect(await readFile(join(publicDir, "step-0.mp4"), "utf-8")).toBe("clip-0");
|
|
74
|
+
// Manifests are imported by the entry; the cache never leaves the scenario.
|
|
75
|
+
expect(await exists(join(publicDir, "manifest.json"))).toBe(false);
|
|
76
|
+
expect(await exists(join(publicDir, ".presenter-cache.json"))).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("stages no presenter clips when the scenario has none", async () => {
|
|
80
|
+
const staged = await stageRenderAssets({
|
|
81
|
+
scenarioDir,
|
|
82
|
+
publicDir,
|
|
83
|
+
hasNarration: false,
|
|
84
|
+
hasPresenter: false,
|
|
85
|
+
});
|
|
86
|
+
expect(staged).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("stages the music asset at its scenario-relative path", async () => {
|
|
90
|
+
await mkdir(join(scenarioDir, "soundtrack"), { recursive: true });
|
|
91
|
+
await writeFile(join(scenarioDir, "soundtrack", "music.mp3"), "music-bytes");
|
|
92
|
+
|
|
93
|
+
const staged = await stageRenderAssets({
|
|
94
|
+
scenarioDir,
|
|
95
|
+
publicDir,
|
|
96
|
+
hasNarration: false,
|
|
97
|
+
soundtrack: { musicSrc: "./soundtrack/music.mp3" },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(staged).toBe(1);
|
|
101
|
+
expect(await readFile(join(publicDir, "soundtrack", "music.mp3"), "utf-8")).toBe(
|
|
102
|
+
"music-bytes",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("stages the built-in SFX set at the shared destinations", async () => {
|
|
107
|
+
const clickSource = join(scenarioDir, "fake-click.mp3");
|
|
108
|
+
const keystrokeSource = join(scenarioDir, "fake-keystroke.mp3");
|
|
109
|
+
await writeFile(clickSource, "click-bytes");
|
|
110
|
+
await writeFile(keystrokeSource, "keystroke-bytes");
|
|
111
|
+
|
|
112
|
+
const staged = await stageRenderAssets({
|
|
113
|
+
scenarioDir,
|
|
114
|
+
publicDir,
|
|
115
|
+
hasNarration: false,
|
|
116
|
+
soundtrack: { sfx: true },
|
|
117
|
+
sfxPaths: { click: clickSource, keystroke: keystrokeSource },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(staged).toBe(2);
|
|
121
|
+
expect(await readFile(join(publicDir, SFX_DEST_PATHS.click), "utf-8")).toBe("click-bytes");
|
|
122
|
+
expect(await readFile(join(publicDir, SFX_DEST_PATHS.keystroke), "utf-8")).toBe(
|
|
123
|
+
"keystroke-bytes",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("stages title-card logos at their scenario-relative paths", async () => {
|
|
128
|
+
await writeFile(join(scenarioDir, "logo.png"), "logo-bytes");
|
|
129
|
+
|
|
130
|
+
const staged = await stageRenderAssets({
|
|
131
|
+
scenarioDir,
|
|
132
|
+
publicDir,
|
|
133
|
+
hasNarration: false,
|
|
134
|
+
titleCards: { intro: { title: "Acme", logoSrc: "./logo.png" } },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(staged).toBe(1);
|
|
138
|
+
expect(await readFile(join(publicDir, "logo.png"), "utf-8")).toBe("logo-bytes");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("stages a logo shared by intro and outro exactly once", async () => {
|
|
142
|
+
await writeFile(join(scenarioDir, "logo.png"), "logo-bytes");
|
|
143
|
+
|
|
144
|
+
const staged = await stageRenderAssets({
|
|
145
|
+
scenarioDir,
|
|
146
|
+
publicDir,
|
|
147
|
+
hasNarration: false,
|
|
148
|
+
titleCards: {
|
|
149
|
+
intro: { title: "Acme", logoSrc: "./logo.png" },
|
|
150
|
+
outro: { title: "Bye", logoSrc: "./logo.png" },
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(staged).toBe(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("stages nothing (and reports zero) for a silent, card-less scenario", async () => {
|
|
158
|
+
const staged = await stageRenderAssets({
|
|
159
|
+
scenarioDir,
|
|
160
|
+
publicDir,
|
|
161
|
+
hasNarration: false,
|
|
162
|
+
});
|
|
163
|
+
expect(staged).toBe(0);
|
|
164
|
+
expect(await exists(publicDir)).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("skips remote music URLs — they are referenced, not copied", async () => {
|
|
168
|
+
const staged = await stageRenderAssets({
|
|
169
|
+
scenarioDir,
|
|
170
|
+
publicDir,
|
|
171
|
+
hasNarration: false,
|
|
172
|
+
soundtrack: { musicSrc: "https://cdn.example.com/music.mp3" },
|
|
173
|
+
});
|
|
174
|
+
expect(staged).toBe(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("fails loudly when the music file is missing (a silent render otherwise)", async () => {
|
|
178
|
+
await expect(
|
|
179
|
+
stageRenderAssets({
|
|
180
|
+
scenarioDir,
|
|
181
|
+
publicDir,
|
|
182
|
+
hasNarration: false,
|
|
183
|
+
soundtrack: { musicSrc: "./soundtrack/missing.mp3" },
|
|
184
|
+
}),
|
|
185
|
+
).rejects.toThrow(/not found/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("fails loudly when a card's logo file is missing (a broken card otherwise)", async () => {
|
|
189
|
+
await expect(
|
|
190
|
+
stageRenderAssets({
|
|
191
|
+
scenarioDir,
|
|
192
|
+
publicDir,
|
|
193
|
+
hasNarration: false,
|
|
194
|
+
titleCards: { outro: { title: "Bye", logoSrc: "./missing.png" } },
|
|
195
|
+
}),
|
|
196
|
+
).rejects.toThrow(/not found/);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("resolveMusicAsset", () => {
|
|
201
|
+
it("rejects paths escaping the scenario directory", async () => {
|
|
202
|
+
await expect(resolveMusicAsset(scenarioDir, "../outside.mp3")).rejects.toThrow(
|
|
203
|
+
/outside the scenario directory/,
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("returns null for remote URLs", async () => {
|
|
208
|
+
expect(await resolveMusicAsset(scenarioDir, "https://cdn.example.com/a.mp3")).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("resolveLogoAsset", () => {
|
|
213
|
+
it("rejects paths escaping the scenario directory", async () => {
|
|
214
|
+
await expect(
|
|
215
|
+
resolveLogoAsset(scenarioDir, "../outside.png", "titleCards.intro.logoSrc"),
|
|
216
|
+
).rejects.toThrow(/outside the scenario directory/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("returns null for remote URLs", async () => {
|
|
220
|
+
expect(
|
|
221
|
+
await resolveLogoAsset(
|
|
222
|
+
scenarioDir,
|
|
223
|
+
"https://cdn.example.com/logo.png",
|
|
224
|
+
"titleCards.intro.logoSrc",
|
|
225
|
+
),
|
|
226
|
+
).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("rejects extensions the deploy contract cannot serve — svg above all", async () => {
|
|
230
|
+
await writeFile(join(scenarioDir, "logo.svg"), "<svg/>");
|
|
231
|
+
await expect(
|
|
232
|
+
resolveLogoAsset(scenarioDir, "./logo.svg", "titleCards.intro.logoSrc"),
|
|
233
|
+
).rejects.toThrow(/not a supported image format/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("accepts every allowlisted raster format", async () => {
|
|
237
|
+
for (const ext of ["png", "jpg", "jpeg", "gif", "webp", "avif"]) {
|
|
238
|
+
await writeFile(join(scenarioDir, `logo.${ext}`), "bytes");
|
|
239
|
+
const asset = await resolveLogoAsset(
|
|
240
|
+
scenarioDir,
|
|
241
|
+
`./logo.${ext}`,
|
|
242
|
+
"titleCards.intro.logoSrc",
|
|
243
|
+
);
|
|
244
|
+
expect(asset?.destRelPath).toBe(`logo.${ext}`);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("isRemoteUrl", () => {
|
|
250
|
+
it.each([
|
|
251
|
+
["https://cdn.example.com/a.mp3", true],
|
|
252
|
+
["http://x/a.mp3", true],
|
|
253
|
+
["./soundtrack/music.mp3", false],
|
|
254
|
+
["soundtrack/music.mp3", false],
|
|
255
|
+
])("%s -> %s", (src, expected) => {
|
|
256
|
+
expect(isRemoteUrl(src)).toBe(expected);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("resolveSfxAssetPaths", () => {
|
|
261
|
+
it("resolves the real assets from a workspace context", async () => {
|
|
262
|
+
// This test file lives inside the repo, where @scenar/react is
|
|
263
|
+
// installed — the same resolution a consumer project gets.
|
|
264
|
+
const fromDir = dirname(fileURLToPath(import.meta.url));
|
|
265
|
+
const paths = resolveSfxAssetPaths(fromDir);
|
|
266
|
+
expect(await exists(paths.click)).toBe(true);
|
|
267
|
+
expect(await exists(paths.keystroke)).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
});
|
package/src/commands/narrate.js
CHANGED
|
@@ -6,7 +6,7 @@ export function registerNarrateCommand(program) {
|
|
|
6
6
|
"Accepts a YAML file, a TypeScript steps file, or a directory\n" +
|
|
7
7
|
"containing scenario subdirectories (each with a steps.ts).")
|
|
8
8
|
.argument("<file-or-dir>", "path to scenario file (.yaml/.ts) or directory")
|
|
9
|
-
.option("--tts <provider>", "TTS provider: echogarden, edge-tts, or
|
|
9
|
+
.option("--tts <provider>", "TTS provider: echogarden, edge-tts, openai, or elevenlabs", "echogarden")
|
|
10
10
|
.option("--out <dir>", "output directory for audio files")
|
|
11
11
|
.option("--voice <voice>", "voice name (provider-specific)")
|
|
12
12
|
.option("--base-url <path>", "URL path prefix for src fields in manifest")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AASvD,MAAM,UAAU,sBAAsB,CAAC,OAAgB;IACrD,OAAO;SACJ,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CACV,mDAAmD;QACjD,gEAAgE;QAChE,4DAA4D,CAC/D;SACA,QAAQ,CAAC,eAAe,EAAE,gDAAgD,CAAC;SAC3E,MAAM,CAAC,kBAAkB,EAAE
|
|
1
|
+
{"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"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"}
|
package/src/commands/narrate.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function registerNarrateCommand(program: Command): void {
|
|
|
17
17
|
"containing scenario subdirectories (each with a steps.ts).",
|
|
18
18
|
)
|
|
19
19
|
.argument("<file-or-dir>", "path to scenario file (.yaml/.ts) or directory")
|
|
20
|
-
.option("--tts <provider>", "TTS provider: echogarden, edge-tts, or
|
|
20
|
+
.option("--tts <provider>", "TTS provider: echogarden, edge-tts, openai, or elevenlabs", "echogarden")
|
|
21
21
|
.option("--out <dir>", "output directory for audio files")
|
|
22
22
|
.option("--voice <voice>", "voice name (provider-specific)")
|
|
23
23
|
.option("--base-url <path>", "URL path prefix for src fields in manifest")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pack.d.ts","sourceRoot":"","sources":["../../../src/commands/pack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"pack.d.ts","sourceRoot":"","sources":["../../../src/commands/pack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6D1D"}
|
package/src/commands/pack.js
CHANGED
|
@@ -22,8 +22,12 @@ export function registerPackCommand(program) {
|
|
|
22
22
|
"Output defaults to ./<scenario-id>-bundle. Use --out to override.")
|
|
23
23
|
.argument("<dir>", "path to a scenario directory (must contain steps.ts)")
|
|
24
24
|
.option("--out <path>", "output directory for the bundle")
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// No commander defaults on the viewport flags: an absent flag must stay
|
|
26
|
+
// absent so the scenario's own authored viewport can apply (resolution
|
|
27
|
+
// in runPack: flags > authored > default). A baked-in default here made
|
|
28
|
+
// `createScenario({ viewport })` silently unreachable.
|
|
29
|
+
.option("--width <number>", `canonical viewport width in px (default: the scenario's authored viewport, else ${DEFAULT_WIDTH})`)
|
|
30
|
+
.option("--shell-height <number>", `shell height in px (default: the scenario's authored viewport, else ${DEFAULT_SHELL_HEIGHT})`)
|
|
27
31
|
.option("--stage", "float the scenario on a backdrop with a window shadow (screen-recording framing); " +
|
|
28
32
|
"the embed paints its own background instead of staying transparent")
|
|
29
33
|
.option("--keep-temp", "keep the generated entry directory for debugging")
|
|
@@ -32,8 +36,8 @@ export function registerPackCommand(program) {
|
|
|
32
36
|
const result = await runPack({
|
|
33
37
|
scenarioDir: dir,
|
|
34
38
|
outDir: options.out,
|
|
35
|
-
width:
|
|
36
|
-
shellHeight:
|
|
39
|
+
width: parseViewportFlag("--width", options.width),
|
|
40
|
+
shellHeight: parseViewportFlag("--shell-height", options.shellHeight),
|
|
37
41
|
stage: options.stage ?? false,
|
|
38
42
|
keepTemp: options.keepTemp,
|
|
39
43
|
onLog: (message) => process.stderr.write(`${message}\n`),
|
|
@@ -49,6 +53,21 @@ export function registerPackCommand(program) {
|
|
|
49
53
|
}
|
|
50
54
|
});
|
|
51
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse a viewport flag into a positive number, or `undefined` when the flag
|
|
58
|
+
* was not given. A malformed value errors instead of silently falling back —
|
|
59
|
+
* the previous `Number(x) || DEFAULT` coercion turned `--width 128O` (typo)
|
|
60
|
+
* into a default-size bundle with no warning.
|
|
61
|
+
*/
|
|
62
|
+
function parseViewportFlag(flag, value) {
|
|
63
|
+
if (value === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
const parsed = Number(value);
|
|
66
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
67
|
+
throw new Error(`${flag} must be a positive number, got "${value}".`);
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
52
71
|
function formatBytes(bytes) {
|
|
53
72
|
if (bytes < 1024)
|
|
54
73
|
return `${bytes} B`;
|
package/src/commands/pack.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pack.js","sourceRoot":"","sources":["../../../src/commands/pack.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE9C,yEAAyE;AACzE,MAAM,aAAa,GAAG,gBAAgB,CAAC,KAAK,CAAC;AAE7C,0CAA0C;AAC1C,MAAM,oBAAoB,GAAG,gBAAgB,CAAC,MAAM,CAAC;AAUrD,MAAM,UAAU,mBAAmB,CAAC,OAAgB;IAClD,OAAO;SACJ,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CACV,4DAA4D;QAC1D,mDAAmD;QACnD,6DAA6D;QAC7D,qEAAqE;QACrE,8DAA8D;QAC9D,yCAAyC;QACzC,kEAAkE;QAClE,oEAAoE;QACpE,kEAAkE;QAClE,oEAAoE;QACpE,oEAAoE;QACpE,yDAAyD;QACzD,mEAAmE,CACtE;SACA,QAAQ,CAAC,OAAO,EAAE,sDAAsD,CAAC;SACzE,MAAM,CAAC,cAAc,EAAE,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"pack.js","sourceRoot":"","sources":["../../../src/commands/pack.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE9C,yEAAyE;AACzE,MAAM,aAAa,GAAG,gBAAgB,CAAC,KAAK,CAAC;AAE7C,0CAA0C;AAC1C,MAAM,oBAAoB,GAAG,gBAAgB,CAAC,MAAM,CAAC;AAUrD,MAAM,UAAU,mBAAmB,CAAC,OAAgB;IAClD,OAAO;SACJ,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CACV,4DAA4D;QAC1D,mDAAmD;QACnD,6DAA6D;QAC7D,qEAAqE;QACrE,8DAA8D;QAC9D,yCAAyC;QACzC,kEAAkE;QAClE,oEAAoE;QACpE,kEAAkE;QAClE,oEAAoE;QACpE,oEAAoE;QACpE,yDAAyD;QACzD,mEAAmE,CACtE;SACA,QAAQ,CAAC,OAAO,EAAE,sDAAsD,CAAC;SACzE,MAAM,CAAC,cAAc,EAAE,iCAAiC,CAAC;QAC1D,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,uDAAuD;SACtD,MAAM,CACL,kBAAkB,EAClB,mFAAmF,aAAa,GAAG,CACpG;SACA,MAAM,CACL,yBAAyB,EACzB,uEAAuE,oBAAoB,GAAG,CAC/F;SACA,MAAM,CACL,SAAS,EACT,oFAAoF;QAClF,oEAAoE,CACvE;SACA,MAAM,CAAC,aAAa,EAAE,kDAAkD,CAAC;SACzE,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,OAAoB,EAAE,EAAE;QAClD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;gBAC3B,WAAW,EAAE,GAAG;gBAChB,MAAM,EAAE,OAAO,CAAC,GAAG;gBACnB,KAAK,EAAE,iBAAiB,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC;gBAClD,WAAW,EAAE,iBAAiB,CAAC,gBAAgB,EAAE,OAAO,CAAC,WAAW,CAAC;gBACrE,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,KAAK;gBAC7B,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC;aACzD,CAAC,CAAC;YAEH,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6BAA6B,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,SAAS;gBAChE,IAAI,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,MAAM,CAAC,MAAM,IAAI,CAC9D,CAAC;YACF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QAClE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACnE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAC;YACvD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,IAAY,EAAE,KAAyB;IAChE,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,oCAAoC,KAAK,IAAI,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,GAAG,KAAK,IAAI,CAAC;IACtC,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACnE,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AACrD,CAAC"}
|
package/src/commands/pack.ts
CHANGED
|
@@ -36,11 +36,17 @@ export function registerPackCommand(program: Command): void {
|
|
|
36
36
|
)
|
|
37
37
|
.argument("<dir>", "path to a scenario directory (must contain steps.ts)")
|
|
38
38
|
.option("--out <path>", "output directory for the bundle")
|
|
39
|
-
|
|
39
|
+
// No commander defaults on the viewport flags: an absent flag must stay
|
|
40
|
+
// absent so the scenario's own authored viewport can apply (resolution
|
|
41
|
+
// in runPack: flags > authored > default). A baked-in default here made
|
|
42
|
+
// `createScenario({ viewport })` silently unreachable.
|
|
43
|
+
.option(
|
|
44
|
+
"--width <number>",
|
|
45
|
+
`canonical viewport width in px (default: the scenario's authored viewport, else ${DEFAULT_WIDTH})`,
|
|
46
|
+
)
|
|
40
47
|
.option(
|
|
41
48
|
"--shell-height <number>",
|
|
42
|
-
`shell height in px (default: ${DEFAULT_SHELL_HEIGHT})`,
|
|
43
|
-
String(DEFAULT_SHELL_HEIGHT),
|
|
49
|
+
`shell height in px (default: the scenario's authored viewport, else ${DEFAULT_SHELL_HEIGHT})`,
|
|
44
50
|
)
|
|
45
51
|
.option(
|
|
46
52
|
"--stage",
|
|
@@ -53,8 +59,8 @@ export function registerPackCommand(program: Command): void {
|
|
|
53
59
|
const result = await runPack({
|
|
54
60
|
scenarioDir: dir,
|
|
55
61
|
outDir: options.out,
|
|
56
|
-
width:
|
|
57
|
-
shellHeight:
|
|
62
|
+
width: parseViewportFlag("--width", options.width),
|
|
63
|
+
shellHeight: parseViewportFlag("--shell-height", options.shellHeight),
|
|
58
64
|
stage: options.stage ?? false,
|
|
59
65
|
keepTemp: options.keepTemp,
|
|
60
66
|
onLog: (message) => process.stderr.write(`${message}\n`),
|
|
@@ -73,6 +79,21 @@ export function registerPackCommand(program: Command): void {
|
|
|
73
79
|
});
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Parse a viewport flag into a positive number, or `undefined` when the flag
|
|
84
|
+
* was not given. A malformed value errors instead of silently falling back —
|
|
85
|
+
* the previous `Number(x) || DEFAULT` coercion turned `--width 128O` (typo)
|
|
86
|
+
* into a default-size bundle with no warning.
|
|
87
|
+
*/
|
|
88
|
+
function parseViewportFlag(flag: string, value: string | undefined): number | undefined {
|
|
89
|
+
if (value === undefined) return undefined;
|
|
90
|
+
const parsed = Number(value);
|
|
91
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
92
|
+
throw new Error(`${flag} must be a positive number, got "${value}".`);
|
|
93
|
+
}
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
function formatBytes(bytes: number): string {
|
|
77
98
|
if (bytes < 1024) return `${bytes} B`;
|
|
78
99
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../../../src/commands/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyF/D"}
|