@scenar/cli 0.1.20 → 0.2.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.
Files changed (94) hide show
  1. package/package.json +17 -5
  2. package/src/__tests__/publish-command.test.ts +46 -0
  3. package/src/__tests__/publish-flow.test.ts +220 -0
  4. package/src/__tests__/read-viewport.test.ts +45 -0
  5. package/src/__tests__/run-publish.test.ts +92 -0
  6. package/src/__tests__/serve-command.test.ts +44 -0
  7. package/src/__tests__/serve-request-path.test.ts +67 -0
  8. package/src/__tests__/serve-static-server.test.ts +88 -0
  9. package/src/api.d.ts +31 -0
  10. package/src/api.d.ts.map +1 -0
  11. package/src/api.js +28 -0
  12. package/src/api.js.map +1 -0
  13. package/src/api.ts +55 -0
  14. package/src/bundle/read-viewport.d.ts +23 -0
  15. package/src/bundle/read-viewport.d.ts.map +1 -0
  16. package/src/bundle/read-viewport.js +25 -0
  17. package/src/bundle/read-viewport.js.map +1 -0
  18. package/src/bundle/read-viewport.ts +36 -0
  19. package/src/commands/deploy.d.ts.map +1 -1
  20. package/src/commands/deploy.js +8 -24
  21. package/src/commands/deploy.js.map +1 -1
  22. package/src/commands/deploy.ts +10 -28
  23. package/src/commands/narrate.d.ts.map +1 -1
  24. package/src/commands/narrate.js +39 -170
  25. package/src/commands/narrate.js.map +1 -1
  26. package/src/commands/narrate.ts +48 -289
  27. package/src/commands/pack.d.ts.map +1 -1
  28. package/src/commands/pack.js +12 -112
  29. package/src/commands/pack.js.map +1 -1
  30. package/src/commands/pack.ts +12 -128
  31. package/src/commands/publish.d.ts +3 -0
  32. package/src/commands/publish.d.ts.map +1 -0
  33. package/src/commands/publish.js +64 -0
  34. package/src/commands/publish.js.map +1 -0
  35. package/src/commands/publish.ts +86 -0
  36. package/src/commands/render.d.ts.map +1 -1
  37. package/src/commands/render.js +12 -194
  38. package/src/commands/render.js.map +1 -1
  39. package/src/commands/render.ts +19 -286
  40. package/src/commands/serve.d.ts +3 -0
  41. package/src/commands/serve.d.ts.map +1 -0
  42. package/src/commands/serve.js +91 -0
  43. package/src/commands/serve.js.map +1 -0
  44. package/src/commands/serve.ts +112 -0
  45. package/src/index.d.ts.map +1 -1
  46. package/src/index.js +4 -0
  47. package/src/index.js.map +1 -1
  48. package/src/index.ts +4 -0
  49. package/src/narrate/run-narrate.d.ts +43 -0
  50. package/src/narrate/run-narrate.d.ts.map +1 -0
  51. package/src/narrate/run-narrate.js +174 -0
  52. package/src/narrate/run-narrate.js.map +1 -0
  53. package/src/narrate/run-narrate.ts +298 -0
  54. package/src/pack/run-pack.d.ts +34 -0
  55. package/src/pack/run-pack.d.ts.map +1 -0
  56. package/src/pack/run-pack.js +117 -0
  57. package/src/pack/run-pack.js.map +1 -0
  58. package/src/pack/run-pack.ts +163 -0
  59. package/src/publish/github.d.ts +11 -0
  60. package/src/publish/github.d.ts.map +1 -0
  61. package/src/publish/github.js +197 -0
  62. package/src/publish/github.js.map +1 -0
  63. package/src/publish/github.ts +216 -0
  64. package/src/publish/publish-flow.d.ts +110 -0
  65. package/src/publish/publish-flow.d.ts.map +1 -0
  66. package/src/publish/publish-flow.js +117 -0
  67. package/src/publish/publish-flow.js.map +1 -0
  68. package/src/publish/publish-flow.ts +199 -0
  69. package/src/publish/run-publish.d.ts +41 -0
  70. package/src/publish/run-publish.d.ts.map +1 -0
  71. package/src/publish/run-publish.js +77 -0
  72. package/src/publish/run-publish.js.map +1 -0
  73. package/src/publish/run-publish.ts +131 -0
  74. package/src/render/run-render.d.ts +31 -0
  75. package/src/render/run-render.d.ts.map +1 -0
  76. package/src/render/run-render.js +196 -0
  77. package/src/render/run-render.js.map +1 -0
  78. package/src/render/run-render.ts +287 -0
  79. package/src/serve/request-path.d.ts +23 -0
  80. package/src/serve/request-path.d.ts.map +1 -0
  81. package/src/serve/request-path.js +52 -0
  82. package/src/serve/request-path.js.map +1 -0
  83. package/src/serve/request-path.ts +55 -0
  84. package/src/serve/run-serve.d.ts +31 -0
  85. package/src/serve/run-serve.d.ts.map +1 -0
  86. package/src/serve/run-serve.js +35 -0
  87. package/src/serve/run-serve.js.map +1 -0
  88. package/src/serve/run-serve.ts +68 -0
  89. package/src/serve/static-server.d.ts +38 -0
  90. package/src/serve/static-server.d.ts.map +1 -0
  91. package/src/serve/static-server.js +91 -0
  92. package/src/serve/static-server.js.map +1 -0
  93. package/src/serve/static-server.ts +118 -0
  94. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,174 @@
1
+ import { mkdir, writeFile, stat } from "node:fs/promises";
2
+ import { join, resolve, extname, basename, dirname } from "node:path";
3
+ import { loadScenarioYaml } from "../util/load-yaml.js";
4
+ import { loadStepsFromTs } from "../util/load-ts.js";
5
+ import { discoverScenarios } from "../util/discover-scenarios.js";
6
+ import { computeHash, loadCache, saveCache, fileExists, isCached, getCachedDuration, buildCacheFile, } from "../util/narration-cache.js";
7
+ import { validateScenario } from "../validate/scenario-validator.js";
8
+ import { resolveProvider } from "../tts/resolve-provider.js";
9
+ function extractNarratedSteps(steps) {
10
+ const result = [];
11
+ for (let i = 0; i < steps.length; i++) {
12
+ const text = steps[i].narration ?? steps[i].narrationText;
13
+ if (typeof text === "string" && text.length > 0) {
14
+ result.push({ index: i, text });
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ function buildRuntimeManifest(totalSteps, entries) {
20
+ const steps = Array.from({ length: totalSteps }, () => null);
21
+ for (const [idx, entry] of entries) {
22
+ steps[idx] = entry;
23
+ }
24
+ return { steps };
25
+ }
26
+ async function loadStepsFromFile(filePath) {
27
+ const ext = extname(filePath).toLowerCase();
28
+ if (ext === ".ts" || ext === ".tsx") {
29
+ const steps = await loadStepsFromTs(filePath);
30
+ return { steps, totalSteps: steps.length };
31
+ }
32
+ const scenario = await loadScenarioYaml(filePath);
33
+ const validation = validateScenario(scenario);
34
+ if (!validation.valid) {
35
+ throw new Error(`Scenario has ${validation.errors.length} error(s). Run 'scenar validate ${filePath}' for details.`);
36
+ }
37
+ const steps = scenario["steps"];
38
+ if (!steps) {
39
+ throw new Error("No steps found in scenario.");
40
+ }
41
+ return { steps: steps, totalSteps: steps.length };
42
+ }
43
+ async function generateNarration(opts) {
44
+ const { scenarioId, narratedSteps, totalSteps, outDir, voice, baseUrl, provider, onLog } = opts;
45
+ await mkdir(outDir, { recursive: true });
46
+ const existingCache = await loadCache(outDir);
47
+ const stats = { generated: 0, cached: 0 };
48
+ const manifestEntries = new Map();
49
+ const cacheEntries = new Map();
50
+ for (const step of narratedSteps) {
51
+ const hash = computeHash(step.text, voice);
52
+ const fileName = `step-${step.index}.mp3`;
53
+ const mp3Path = join(outDir, fileName);
54
+ const srcPrefix = baseUrl ? `${baseUrl.replace(/\/$/, "")}/${scenarioId}` : `.`;
55
+ const src = `${srcPrefix}/${fileName}`;
56
+ if (isCached(existingCache, step.index, hash, voice) && (await fileExists(mp3Path))) {
57
+ const durationMs = getCachedDuration(existingCache, step.index);
58
+ manifestEntries.set(step.index, { src, durationMs });
59
+ cacheEntries.set(step.index, { hash, durationMs });
60
+ stats.cached++;
61
+ onLog(` step ${step.index}: cached`);
62
+ continue;
63
+ }
64
+ onLog(` step ${step.index}: generating...`);
65
+ const result = await provider.synthesize(step.text, { voice: voice || undefined });
66
+ await writeFile(mp3Path, result.audio);
67
+ manifestEntries.set(step.index, { src, durationMs: result.durationMs });
68
+ cacheEntries.set(step.index, { hash, durationMs: result.durationMs });
69
+ stats.generated++;
70
+ onLog(` step ${step.index}: ${result.durationMs}ms (${result.audio.length} bytes)`);
71
+ }
72
+ const manifest = buildRuntimeManifest(totalSteps, manifestEntries);
73
+ await writeFile(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
74
+ const cache = buildCacheFile(voice, totalSteps, cacheEntries);
75
+ await saveCache(outDir, cache);
76
+ onLog(`\x1b[32m+\x1b[0m ${scenarioId}: ${stats.generated} generated, ${stats.cached} cached`);
77
+ return stats;
78
+ }
79
+ /**
80
+ * Generate narration audio for a scenario file or a directory of scenarios —
81
+ * the pure orchestration behind `scenar narrate`. Resolves the TTS provider,
82
+ * synthesizes per-step audio (incremental via a content-hash cache), writes each
83
+ * scenario's manifest, and returns aggregate stats. No process/exit coupling.
84
+ */
85
+ export async function runNarrate(options) {
86
+ const onLog = options.onLog ?? (() => { });
87
+ const provider = await resolveProvider(options.tts ?? "echogarden");
88
+ const resolved = resolve(options.target);
89
+ const info = await stat(resolved);
90
+ return info.isDirectory()
91
+ ? narrateDirectory(resolved, options, provider, onLog)
92
+ : narrateSingleFile(resolved, options, provider, onLog);
93
+ }
94
+ async function narrateSingleFile(filePath, options, provider, onLog) {
95
+ const { steps, totalSteps } = await loadStepsFromFile(filePath);
96
+ const narratedSteps = extractNarratedSteps(steps);
97
+ const id = basename(dirname(filePath));
98
+ if (narratedSteps.length === 0) {
99
+ return {
100
+ mode: "file",
101
+ scenarios: [{ id, generated: 0, cached: 0, skipped: true }],
102
+ totalGenerated: 0,
103
+ totalCached: 0,
104
+ totalSkipped: 1,
105
+ errors: [],
106
+ };
107
+ }
108
+ const outDir = options.out ?? join(dirname(filePath), "narration");
109
+ const stats = await generateNarration({
110
+ scenarioId: id,
111
+ narratedSteps,
112
+ totalSteps,
113
+ outDir,
114
+ voice: options.voice ?? "",
115
+ baseUrl: options.baseUrl,
116
+ provider,
117
+ onLog,
118
+ });
119
+ return {
120
+ mode: "file",
121
+ scenarios: [{ id, generated: stats.generated, cached: stats.cached, skipped: false }],
122
+ totalGenerated: stats.generated,
123
+ totalCached: stats.cached,
124
+ totalSkipped: 0,
125
+ errors: [],
126
+ };
127
+ }
128
+ async function narrateDirectory(dirPath, options, provider, onLog) {
129
+ const scenarios = await discoverScenarios(dirPath);
130
+ const result = [];
131
+ const errors = [];
132
+ let totalGenerated = 0;
133
+ let totalCached = 0;
134
+ let totalSkipped = 0;
135
+ if (scenarios.length > 0) {
136
+ onLog(`Discovered ${scenarios.length} scenario(s)`);
137
+ }
138
+ for (const scenario of scenarios) {
139
+ onLog(` ${scenario.id}`);
140
+ try {
141
+ const steps = await loadStepsFromTs(scenario.stepsPath);
142
+ const narratedSteps = extractNarratedSteps(steps);
143
+ if (narratedSteps.length === 0) {
144
+ onLog(" (no narration)");
145
+ totalSkipped++;
146
+ result.push({ id: scenario.id, generated: 0, cached: 0, skipped: true });
147
+ continue;
148
+ }
149
+ const outDir = options.out
150
+ ? join(options.out, scenario.id)
151
+ : join(dirPath, scenario.id, "narration");
152
+ const stats = await generateNarration({
153
+ scenarioId: scenario.id,
154
+ narratedSteps,
155
+ totalSteps: steps.length,
156
+ outDir,
157
+ voice: options.voice ?? "",
158
+ baseUrl: options.baseUrl,
159
+ provider,
160
+ onLog,
161
+ });
162
+ totalGenerated += stats.generated;
163
+ totalCached += stats.cached;
164
+ result.push({ id: scenario.id, generated: stats.generated, cached: stats.cached, skipped: false });
165
+ }
166
+ catch (error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+ onLog(` \x1b[31mfailed\x1b[0m: ${message}`);
169
+ errors.push({ id: scenario.id, message });
170
+ }
171
+ }
172
+ return { mode: "directory", scenarios: result, totalGenerated, totalCached, totalSkipped, errors };
173
+ }
174
+ //# sourceMappingURL=run-narrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-narrate.js","sourceRoot":"","sources":["../../../src/narrate/run-narrate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,298 @@
1
+ import { mkdir, writeFile, stat } from "node:fs/promises";
2
+ import { join, resolve, extname, basename, dirname } from "node:path";
3
+ import { loadScenarioYaml } from "../util/load-yaml.js";
4
+ import { loadStepsFromTs } from "../util/load-ts.js";
5
+ import { discoverScenarios } from "../util/discover-scenarios.js";
6
+ import {
7
+ computeHash,
8
+ loadCache,
9
+ saveCache,
10
+ fileExists,
11
+ isCached,
12
+ getCachedDuration,
13
+ buildCacheFile,
14
+ } from "../util/narration-cache.js";
15
+ import { validateScenario } from "../validate/scenario-validator.js";
16
+ import { resolveProvider } from "../tts/resolve-provider.js";
17
+ import type { TtsProvider } from "../tts/types.js";
18
+
19
+ /** Options for {@link runNarrate}. Paths may be relative; resolved here. */
20
+ export interface RunNarrateOptions {
21
+ /** A scenario YAML file, a .ts steps file, or a directory of scenarios. */
22
+ readonly target: string;
23
+ /** TTS provider name: echogarden, edge-tts, or openai (default echogarden). */
24
+ readonly tts?: string;
25
+ /** Output directory for audio (default: alongside the scenario). */
26
+ readonly out?: string;
27
+ /** Voice name (provider-specific). */
28
+ readonly voice?: string;
29
+ /** URL path prefix for the `src` fields written into the manifest. */
30
+ readonly baseUrl?: string;
31
+ /** Progress sink for per-step/per-scenario messages. */
32
+ readonly onLog?: (message: string) => void;
33
+ }
34
+
35
+ /** Per-scenario narration outcome. */
36
+ export interface NarratedScenario {
37
+ readonly id: string;
38
+ readonly generated: number;
39
+ readonly cached: number;
40
+ /** True when the scenario had no narration text (nothing to synthesize). */
41
+ readonly skipped: boolean;
42
+ }
43
+
44
+ /** Aggregate result of a narrate run. */
45
+ export interface NarrateResult {
46
+ readonly mode: "file" | "directory";
47
+ readonly scenarios: NarratedScenario[];
48
+ readonly totalGenerated: number;
49
+ readonly totalCached: number;
50
+ readonly totalSkipped: number;
51
+ readonly errors: Array<{ id: string; message: string }>;
52
+ }
53
+
54
+ interface StepWithNarration {
55
+ index: number;
56
+ text: string;
57
+ }
58
+
59
+ function extractNarratedSteps(
60
+ steps: Array<{ narration?: string; narrationText?: string }>,
61
+ ): StepWithNarration[] {
62
+ const result: StepWithNarration[] = [];
63
+ for (let i = 0; i < steps.length; i++) {
64
+ const text = steps[i]!.narration ?? steps[i]!.narrationText;
65
+ if (typeof text === "string" && text.length > 0) {
66
+ result.push({ index: i, text });
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+ interface RuntimeManifestEntry {
73
+ src: string;
74
+ durationMs: number;
75
+ }
76
+
77
+ interface RuntimeManifest {
78
+ steps: (RuntimeManifestEntry | null)[];
79
+ }
80
+
81
+ function buildRuntimeManifest(
82
+ totalSteps: number,
83
+ entries: Map<number, { src: string; durationMs: number }>,
84
+ ): RuntimeManifest {
85
+ const steps: (RuntimeManifestEntry | null)[] = Array.from({ length: totalSteps }, () => null);
86
+ for (const [idx, entry] of entries) {
87
+ steps[idx] = entry;
88
+ }
89
+ return { steps };
90
+ }
91
+
92
+ async function loadStepsFromFile(
93
+ filePath: string,
94
+ ): Promise<{ steps: Array<{ narration?: string; narrationText?: string }>; totalSteps: number }> {
95
+ const ext = extname(filePath).toLowerCase();
96
+
97
+ if (ext === ".ts" || ext === ".tsx") {
98
+ const steps = await loadStepsFromTs(filePath);
99
+ return { steps, totalSteps: steps.length };
100
+ }
101
+
102
+ const scenario = await loadScenarioYaml(filePath);
103
+ const validation = validateScenario(scenario);
104
+ if (!validation.valid) {
105
+ throw new Error(
106
+ `Scenario has ${validation.errors.length} error(s). Run 'scenar validate ${filePath}' for details.`,
107
+ );
108
+ }
109
+
110
+ const steps = (scenario as Record<string, unknown>)["steps"] as
111
+ | Array<Record<string, unknown>>
112
+ | undefined;
113
+ if (!steps) {
114
+ throw new Error("No steps found in scenario.");
115
+ }
116
+ return { steps: steps as Array<{ narrationText?: string }>, totalSteps: steps.length };
117
+ }
118
+
119
+ interface GenerateOptions {
120
+ scenarioId: string;
121
+ narratedSteps: StepWithNarration[];
122
+ totalSteps: number;
123
+ outDir: string;
124
+ voice: string;
125
+ baseUrl?: string;
126
+ provider: TtsProvider;
127
+ onLog: (message: string) => void;
128
+ }
129
+
130
+ interface GenerateStats {
131
+ generated: number;
132
+ cached: number;
133
+ }
134
+
135
+ async function generateNarration(opts: GenerateOptions): Promise<GenerateStats> {
136
+ const { scenarioId, narratedSteps, totalSteps, outDir, voice, baseUrl, provider, onLog } = opts;
137
+
138
+ await mkdir(outDir, { recursive: true });
139
+
140
+ const existingCache = await loadCache(outDir);
141
+ const stats: GenerateStats = { generated: 0, cached: 0 };
142
+ const manifestEntries = new Map<number, { src: string; durationMs: number }>();
143
+ const cacheEntries = new Map<number, { hash: string; durationMs: number }>();
144
+
145
+ for (const step of narratedSteps) {
146
+ const hash = computeHash(step.text, voice);
147
+ const fileName = `step-${step.index}.mp3`;
148
+ const mp3Path = join(outDir, fileName);
149
+
150
+ const srcPrefix = baseUrl ? `${baseUrl.replace(/\/$/, "")}/${scenarioId}` : `.`;
151
+ const src = `${srcPrefix}/${fileName}`;
152
+
153
+ if (isCached(existingCache, step.index, hash, voice) && (await fileExists(mp3Path))) {
154
+ const durationMs = getCachedDuration(existingCache!, step.index);
155
+ manifestEntries.set(step.index, { src, durationMs });
156
+ cacheEntries.set(step.index, { hash, durationMs });
157
+ stats.cached++;
158
+ onLog(` step ${step.index}: cached`);
159
+ continue;
160
+ }
161
+
162
+ onLog(` step ${step.index}: generating...`);
163
+ const result = await provider.synthesize(step.text, { voice: voice || undefined });
164
+
165
+ await writeFile(mp3Path, result.audio);
166
+ manifestEntries.set(step.index, { src, durationMs: result.durationMs });
167
+ cacheEntries.set(step.index, { hash, durationMs: result.durationMs });
168
+ stats.generated++;
169
+ onLog(` step ${step.index}: ${result.durationMs}ms (${result.audio.length} bytes)`);
170
+ }
171
+
172
+ const manifest = buildRuntimeManifest(totalSteps, manifestEntries);
173
+ await writeFile(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
174
+
175
+ const cache = buildCacheFile(voice, totalSteps, cacheEntries);
176
+ await saveCache(outDir, cache);
177
+
178
+ onLog(`\x1b[32m+\x1b[0m ${scenarioId}: ${stats.generated} generated, ${stats.cached} cached`);
179
+ return stats;
180
+ }
181
+
182
+ /**
183
+ * Generate narration audio for a scenario file or a directory of scenarios —
184
+ * the pure orchestration behind `scenar narrate`. Resolves the TTS provider,
185
+ * synthesizes per-step audio (incremental via a content-hash cache), writes each
186
+ * scenario's manifest, and returns aggregate stats. No process/exit coupling.
187
+ */
188
+ export async function runNarrate(options: RunNarrateOptions): Promise<NarrateResult> {
189
+ const onLog = options.onLog ?? (() => {});
190
+ const provider = await resolveProvider(options.tts ?? "echogarden");
191
+ const resolved = resolve(options.target);
192
+ const info = await stat(resolved);
193
+
194
+ return info.isDirectory()
195
+ ? narrateDirectory(resolved, options, provider, onLog)
196
+ : narrateSingleFile(resolved, options, provider, onLog);
197
+ }
198
+
199
+ async function narrateSingleFile(
200
+ filePath: string,
201
+ options: RunNarrateOptions,
202
+ provider: TtsProvider,
203
+ onLog: (message: string) => void,
204
+ ): Promise<NarrateResult> {
205
+ const { steps, totalSteps } = await loadStepsFromFile(filePath);
206
+ const narratedSteps = extractNarratedSteps(steps);
207
+ const id = basename(dirname(filePath));
208
+
209
+ if (narratedSteps.length === 0) {
210
+ return {
211
+ mode: "file",
212
+ scenarios: [{ id, generated: 0, cached: 0, skipped: true }],
213
+ totalGenerated: 0,
214
+ totalCached: 0,
215
+ totalSkipped: 1,
216
+ errors: [],
217
+ };
218
+ }
219
+
220
+ const outDir = options.out ?? join(dirname(filePath), "narration");
221
+ const stats = await generateNarration({
222
+ scenarioId: id,
223
+ narratedSteps,
224
+ totalSteps,
225
+ outDir,
226
+ voice: options.voice ?? "",
227
+ baseUrl: options.baseUrl,
228
+ provider,
229
+ onLog,
230
+ });
231
+
232
+ return {
233
+ mode: "file",
234
+ scenarios: [{ id, generated: stats.generated, cached: stats.cached, skipped: false }],
235
+ totalGenerated: stats.generated,
236
+ totalCached: stats.cached,
237
+ totalSkipped: 0,
238
+ errors: [],
239
+ };
240
+ }
241
+
242
+ async function narrateDirectory(
243
+ dirPath: string,
244
+ options: RunNarrateOptions,
245
+ provider: TtsProvider,
246
+ onLog: (message: string) => void,
247
+ ): Promise<NarrateResult> {
248
+ const scenarios = await discoverScenarios(dirPath);
249
+ const result: NarratedScenario[] = [];
250
+ const errors: Array<{ id: string; message: string }> = [];
251
+ let totalGenerated = 0;
252
+ let totalCached = 0;
253
+ let totalSkipped = 0;
254
+
255
+ if (scenarios.length > 0) {
256
+ onLog(`Discovered ${scenarios.length} scenario(s)`);
257
+ }
258
+
259
+ for (const scenario of scenarios) {
260
+ onLog(` ${scenario.id}`);
261
+ try {
262
+ const steps = await loadStepsFromTs(scenario.stepsPath);
263
+ const narratedSteps = extractNarratedSteps(steps);
264
+
265
+ if (narratedSteps.length === 0) {
266
+ onLog(" (no narration)");
267
+ totalSkipped++;
268
+ result.push({ id: scenario.id, generated: 0, cached: 0, skipped: true });
269
+ continue;
270
+ }
271
+
272
+ const outDir = options.out
273
+ ? join(options.out, scenario.id)
274
+ : join(dirPath, scenario.id, "narration");
275
+
276
+ const stats = await generateNarration({
277
+ scenarioId: scenario.id,
278
+ narratedSteps,
279
+ totalSteps: steps.length,
280
+ outDir,
281
+ voice: options.voice ?? "",
282
+ baseUrl: options.baseUrl,
283
+ provider,
284
+ onLog,
285
+ });
286
+
287
+ totalGenerated += stats.generated;
288
+ totalCached += stats.cached;
289
+ result.push({ id: scenario.id, generated: stats.generated, cached: stats.cached, skipped: false });
290
+ } catch (error) {
291
+ const message = error instanceof Error ? error.message : String(error);
292
+ onLog(` \x1b[31mfailed\x1b[0m: ${message}`);
293
+ errors.push({ id: scenario.id, message });
294
+ }
295
+ }
296
+
297
+ return { mode: "directory", scenarios: result, totalGenerated, totalCached, totalSkipped, errors };
298
+ }
@@ -0,0 +1,34 @@
1
+ import { type PackManifest } from "./pack-manifest.js";
2
+ /** Options for {@link runPack}. Paths may be relative; they are resolved here. */
3
+ export interface RunPackOptions {
4
+ /** Path to a scenario directory (must contain steps.ts + a renderStep export). */
5
+ readonly scenarioDir: string;
6
+ /** Output directory for the bundle (default: ./<scenario-id>-bundle). */
7
+ readonly outDir?: string;
8
+ /** Canonical viewport width in px (default: {@link DEFAULT_VIEWPORT}.width). */
9
+ readonly width?: number;
10
+ /** Shell/container height in px (default: {@link DEFAULT_VIEWPORT}.height). */
11
+ readonly shellHeight?: number;
12
+ /** Keep the generated entry directory for debugging (default: false). */
13
+ readonly keepTemp?: boolean;
14
+ /** Progress sink for mid-operation messages. */
15
+ readonly onLog?: (message: string) => void;
16
+ }
17
+ /** The outcome of a successful pack. */
18
+ export interface PackResult {
19
+ readonly scenarioId: string;
20
+ readonly outDir: string;
21
+ readonly renderFilePath: string;
22
+ readonly providersPath: string | null;
23
+ readonly hasNarration: boolean;
24
+ readonly manifest: PackManifest;
25
+ readonly totalBytes: number;
26
+ }
27
+ /**
28
+ * Bundle a scenario directory into a self-contained, hosted embed — the pure
29
+ * orchestration behind `scenar pack`, with no process/exit coupling so both the
30
+ * CLI command and the MCP server can call it. Throws on any failure (invalid
31
+ * directory, bundler error, allowlist violation); the caller formats it.
32
+ */
33
+ export declare function runPack(options: RunPackOptions): Promise<PackResult>;
34
+ //# sourceMappingURL=run-pack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-pack.d.ts","sourceRoot":"","sources":["../../../src/pack/run-pack.ts"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAM5B,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAmF1E"}
@@ -0,0 +1,117 @@
1
+ import { resolve, join, basename } from "node:path";
2
+ import { stat, mkdir, writeFile, rm, readdir, copyFile, access } from "node:fs/promises";
3
+ import { detectRenderExport } from "../render/detect-render-export.js";
4
+ import { resolveProvidersPath } from "../render/resolve-providers.js";
5
+ import { generateEmbedEntry, generateEmbedHtml } from "./generate-embed-entry.js";
6
+ import { runViteBuild } from "./build.js";
7
+ import { buildPackManifest, writePackManifest, writeScenarioJson, verifyManifestFilesExist, } from "./pack-manifest.js";
8
+ import { DEFAULT_VIEWPORT } from "./viewport.js";
9
+ /** Generator version stamped into scenario.json (kept in sync with the CLI). */
10
+ const PACK_GENERATOR_VERSION = "0.0.1";
11
+ /**
12
+ * Bundle a scenario directory into a self-contained, hosted embed — the pure
13
+ * orchestration behind `scenar pack`, with no process/exit coupling so both the
14
+ * CLI command and the MCP server can call it. Throws on any failure (invalid
15
+ * directory, bundler error, allowlist violation); the caller formats it.
16
+ */
17
+ export async function runPack(options) {
18
+ const onLog = options.onLog ?? (() => { });
19
+ const scenarioDir = resolve(options.scenarioDir);
20
+ const info = await stat(scenarioDir).catch(() => null);
21
+ if (!info) {
22
+ throw new Error(`${options.scenarioDir} does not exist.`);
23
+ }
24
+ if (!info.isDirectory()) {
25
+ throw new Error(`${options.scenarioDir} is not a directory. ` +
26
+ "The pack command requires a scenario directory (with steps.ts).");
27
+ }
28
+ const scenarioId = basename(scenarioDir);
29
+ const outDir = options.outDir ? resolve(options.outDir) : resolve(`./${scenarioId}-bundle`);
30
+ const width = options.width ?? DEFAULT_VIEWPORT.width;
31
+ const shellHeight = options.shellHeight ?? DEFAULT_VIEWPORT.height;
32
+ // The entry must live inside the scenario directory so the bundler's
33
+ // node_modules resolution walks up into the consumer project.
34
+ const tempDir = join(scenarioDir, ".scenar-pack");
35
+ try {
36
+ const renderFilePath = await detectRenderExport(scenarioDir);
37
+ const providersPath = await resolveProvidersPath(scenarioDir);
38
+ const hasNarration = await fileExists(join(scenarioDir, "narration", "manifest.json"));
39
+ onLog(`Scenario: ${scenarioId}`);
40
+ onLog(`Render: ${renderFilePath}`);
41
+ onLog(`Providers: ${providersPath ?? "none"}`);
42
+ onLog(`Narration: ${hasNarration ? "yes (manifest found)" : "none"}`);
43
+ onLog(`Output: ${outDir}`);
44
+ // 1. Generate the browser entry + index.html into the temp dir.
45
+ const entrySource = generateEmbedEntry({
46
+ scenarioDir,
47
+ renderFilePath,
48
+ scenarioId,
49
+ hasNarration,
50
+ providersPath,
51
+ canonicalWidth: width,
52
+ shellHeight,
53
+ });
54
+ await mkdir(tempDir, { recursive: true });
55
+ const entryFileName = "entry.tsx";
56
+ const entryHtmlPath = join(tempDir, "index.html");
57
+ await writeFile(join(tempDir, entryFileName), entrySource, "utf-8");
58
+ await writeFile(entryHtmlPath, generateEmbedHtml(scenarioId, entryFileName), "utf-8");
59
+ // 2. Build the static bundle with Vite.
60
+ onLog("Bundling embed with Vite...");
61
+ await runViteBuild({ root: tempDir, outDir, entryHtmlPath });
62
+ // 3. Copy the narration manifest + audio. The embed fetches
63
+ // ./narration/manifest.json at runtime and resolves each clip's
64
+ // relative src against it, so both must ship in the bundle.
65
+ if (hasNarration) {
66
+ await copyNarration(scenarioDir, outDir);
67
+ }
68
+ // 4. Write the required scenario.json descriptor, recording the canonical
69
+ // viewport baked into the bundle so `deploy`/`serve`/`publish` can derive
70
+ // a correctly-proportioned embed snippet (DD-004).
71
+ await writeScenarioJson(outDir, scenarioId, PACK_GENERATOR_VERSION, {
72
+ width,
73
+ height: shellHeight,
74
+ });
75
+ // 5. Compute + write the deploy-facing pack manifest (validates the bundle
76
+ // against the backend allowlist; throws on any violation).
77
+ const manifest = await buildPackManifest(outDir, scenarioId);
78
+ await verifyManifestFilesExist(outDir, manifest);
79
+ await writePackManifest(outDir, manifest);
80
+ const totalBytes = manifest.files.reduce((sum, f) => sum + f.sizeBytes, 0);
81
+ return { scenarioId, outDir, renderFilePath, providersPath, hasNarration, manifest, totalBytes };
82
+ }
83
+ finally {
84
+ if (!options.keepTemp) {
85
+ await rm(tempDir, { recursive: true, force: true }).catch(() => { });
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Copy the narration manifest and every *.mp3 from <scenarioDir>/narration into
91
+ * <outDir>/narration. The manifest is the runtime index the embed fetches; the
92
+ * mp3s are the clips it references by relative src.
93
+ */
94
+ async function copyNarration(scenarioDir, outDir) {
95
+ const srcDir = join(scenarioDir, "narration");
96
+ const destDir = join(outDir, "narration");
97
+ await mkdir(destDir, { recursive: true });
98
+ const entries = await readdir(srcDir, { withFileTypes: true });
99
+ for (const entry of entries) {
100
+ if (!entry.isFile())
101
+ continue;
102
+ const name = entry.name.toLowerCase();
103
+ if (name === "manifest.json" || name.endsWith(".mp3")) {
104
+ await copyFile(join(srcDir, entry.name), join(destDir, entry.name));
105
+ }
106
+ }
107
+ }
108
+ async function fileExists(path) {
109
+ try {
110
+ await access(path);
111
+ return true;
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ //# sourceMappingURL=run-pack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-pack.js","sourceRoot":"","sources":["../../../src/pack/run-pack.ts"],"names":[],"mappings":"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"}