@navipartner/demo-video 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,272 +1,425 @@
1
- #!/usr/bin/env node
2
- /* eslint-disable no-console -- a CLI; the console is its output */
3
- import { spawnSync } from "node:child_process";
4
- import fs from "node:fs";
5
- import { createRequire } from "node:module";
6
- import path from "node:path";
7
-
8
- import { listStories, parseArgs } from "./args.mjs";
9
- import { encodeRecording, extractStillsFrom } from "./encode.mjs";
10
- import { initApp, initRepo } from "./init.mjs";
11
- import { appMenu, menuMarkdown, pick, pickExact } from "./pick.mjs";
12
- import { writeRunner } from "./runner.mjs";
13
-
14
- // Runs the app's own Playwright (resolved from the app, never from this
15
- // package) against the config this CLI writes into demo/out/.work/runner/.
16
-
17
- // A run's work folder goes when the command ends. A failure first keeps
18
- // Playwright's results (its error context) in demo/out/failed/; a success
19
- // clears an older failure, so what's there is always the latest.
20
- const finish = (args, ok) => {
21
- const failed = path.join(args.outDir, "failed");
22
- fs.rmSync(failed, { recursive: true, force: true });
23
- const results = path.join(args.workDir, "test-results");
24
- if (!ok && fs.existsSync(results)) {
25
- fs.cpSync(results, failed, { recursive: true });
26
- console.error(`Playwright's output for this failure: ${failed}`);
27
- }
28
- fs.rmSync(args.workDir, { recursive: true, force: true });
29
- const work = path.dirname(args.workDir);
30
- if (fs.existsSync(work) && fs.readdirSync(work).length === 0) {
31
- fs.rmdirSync(work);
32
- }
33
- };
34
-
35
- const runPlaywright = (args, mode) => {
36
- const require = createRequire(path.join(args.appDir, "package.json"));
37
- const cli = require.resolve("@playwright/test/cli");
38
- fs.rmSync(args.workDir, { recursive: true, force: true });
39
- fs.mkdirSync(args.workDir, { recursive: true });
40
- writeRunner(args);
41
- const result = spawnSync(
42
- process.execPath,
43
- [cli, "test", "-c", args.configPath],
44
- {
45
- cwd: args.appDir,
46
- // With --json, Playwright's own output goes to stderr so stdout is pure JSON.
47
- stdio: ["inherit", args.json ? 2 : "inherit", "inherit"],
48
- env: {
49
- ...process.env,
50
- DEMO_MODE: mode,
51
- DEMO_STORYBOARD: args.storyboardPath ?? "",
52
- DEMO_BRAND: args.brand ?? "",
53
- DEMO_OUT: args.outDir,
54
- DEMO_WORK: args.workDir,
55
- DEMO_DRAFT: args.draft ? "1" : "",
56
- },
57
- },
58
- );
59
- return result.status ?? 1;
60
- };
61
-
62
- const printReport = (report) => {
63
- if (report.ok) {
64
- console.log(`✓ story passes (${report.steps} steps)`);
65
- return;
66
- }
67
- const where = report.step
68
- ? `step ${report.step} (${report.do}${report.target ? ` ${report.target}` : ""})`
69
- : "before the first step";
70
- console.log(`✗ ${where}: ${report.error}`);
71
- if (report.visible?.length) {
72
- console.log(` visible testids: ${report.visible.join(", ")}`);
73
- }
74
- };
75
-
76
- // demo-video init [--app <dir>] [--dry-run]: needs no demo/ yet, so it skips
77
- // parseArgs.
78
- const runInit = (argv) => {
79
- const dryRun = argv.includes("--dry-run");
80
- const at = argv.indexOf("--app");
81
- const value = at === -1 ? null : argv[at + 1];
82
- // A flag after --app (--app --dry-run) is not its directory.
83
- const appMissing = at !== -1 && (!value || value.startsWith("-"));
84
- const app = appMissing ? null : value;
85
- const unknown = argv.filter(
86
- (a, i) =>
87
- a !== "--dry-run" && a !== "--app" && !(app && argv[i - 1] === "--app"),
88
- );
89
- if (appMissing || unknown.length) {
90
- // Name what was wrong: "-app" for "--app", or a second command on the line.
91
- const problem = appMissing
92
- ? "--app needs a directory right after it"
93
- : `Unknown argument${unknown.length > 1 ? "s" : ""}: ${unknown.join(" ")}`;
94
- console.error(
95
- `${problem}\nUsage: demo-video init [--app <dir>] [--dry-run]`,
96
- );
97
- return 2;
98
- }
99
- const root = process.cwd();
100
- const result = app
101
- ? initApp(path.resolve(root, app), { dryRun })
102
- : initRepo(root, { dryRun });
103
- console.log(
104
- `demo-video init${dryRun ? " (dry run: nothing written)" : ""}: ${app ?? "repo"}`,
105
- );
106
- const width = Math.max(...result.changes.map(([, what]) => what.length));
107
- for (const [status, what, detail] of result.changes) {
108
- console.log(` ${status.padEnd(8)} ${what.padEnd(width)} ${detail}`);
109
- }
110
- if (result.todo.length) {
111
- console.log("\nBy hand:");
112
- for (const item of result.todo) {
113
- console.log(` - ${item}`);
114
- }
115
- }
116
- return 0;
117
- };
118
-
119
- // demo-video pick [--request "<text>" | --app <a> --story <s> --brand <b>]
120
- // [--json] <app dir>...: which existing story and brand a request means
121
- // (see pick.mjs). --request reads a sentence (a /demo-video comment); the
122
- // flags are exact names (the Run workflow form).
123
- const runPick = (argv) => {
124
- const flags = {};
125
- const dirs = [];
126
- let json = false;
127
- for (let i = 0; i < argv.length; i++) {
128
- const arg = argv[i];
129
- if (arg === "--json") {
130
- json = true;
131
- } else if (["--request", "--app", "--story", "--brand"].includes(arg)) {
132
- flags[arg.slice(2)] = argv[++i] ?? "";
133
- } else {
134
- dirs.push(arg);
135
- }
136
- }
137
- if (dirs.length === 0) {
138
- console.error(
139
- 'Usage: demo-video pick [--request "<text>" | --app <a> --story <s> --brand <b>] [--json] <app dir>...',
140
- );
141
- return 2;
142
- }
143
- const apps = dirs.map((d) => appMenu(path.resolve(d)));
144
- const result =
145
- flags.request !== undefined
146
- ? pick(flags.request, apps)
147
- : pickExact(apps, flags);
148
- const out = result.menu
149
- ? {
150
- menu: true,
151
- reason: result.reason,
152
- markdown: menuMarkdown(result.apps ?? apps, result.reason),
153
- }
154
- : result;
155
- if (json) {
156
- console.log(JSON.stringify(out));
157
- } else if (out.menu) {
158
- console.log(out.markdown);
159
- } else {
160
- console.log(
161
- `${out.app}: story ${out.story}, brand ${out.brand ?? "(the app's default)"}`,
162
- );
163
- }
164
- return 0;
165
- };
166
-
167
- const run = () => {
168
- if (process.argv[2] === "init") {
169
- return runInit(process.argv.slice(3));
170
- }
171
- if (process.argv[2] === "pick") {
172
- return runPick(process.argv.slice(3));
173
- }
174
- let args;
175
- try {
176
- args = parseArgs(process.argv.slice(2), process.cwd());
177
- } catch (error) {
178
- console.error(error.message);
179
- return 2;
180
- }
181
- switch (args.command) {
182
- case "check": {
183
- const status = runPlaywright(args, "check");
184
- // The report comes back through the work folder and goes with it.
185
- const reportPath = path.join(args.workDir, "check.json");
186
- const report = fs.existsSync(reportPath)
187
- ? JSON.parse(fs.readFileSync(reportPath, "utf8"))
188
- : {
189
- ok: false,
190
- error:
191
- "the dry run produced no report (Playwright did not run the demo test — see its output above)",
192
- };
193
- const ok = report.ok && status === 0;
194
- finish(args, ok);
195
- if (args.json) {
196
- console.log(JSON.stringify(report, null, 2));
197
- } else {
198
- printReport(report);
199
- }
200
- return ok ? 0 : 1;
201
- }
202
- case "record": {
203
- // The recording stays in the work folder for `encode`.
204
- const status = runPlaywright(args, "record");
205
- if (status !== 0) {
206
- finish(args, false);
207
- }
208
- return status;
209
- }
210
- case "render": {
211
- const status = runPlaywright(args, "record");
212
- if (status !== 0) {
213
- finish(args, false);
214
- return status;
215
- }
216
- fs.mkdirSync(args.outDir, { recursive: true });
217
- const result = encodeRecording(args.workDir, args.outDir);
218
- finish(args, true);
219
- if (args.json) {
220
- // For CI: { output, duration, stills, sheet } on stdout, nothing else.
221
- console.log(JSON.stringify(result));
222
- } else {
223
- console.log(`Wrote ${result.output} (${result.duration.toFixed(1)} s)`);
224
- console.log(`Review: ${result.sheet} (one frame a second)`);
225
- }
226
- return 0;
227
- }
228
- case "encode": {
229
- fs.mkdirSync(args.outDir, { recursive: true });
230
- const { output, duration } = encodeRecording(args.workDir, args.outDir);
231
- finish(args, true);
232
- console.log(`Wrote ${output} (${duration.toFixed(1)} s)`);
233
- return 0;
234
- }
235
- case "list": {
236
- // For people and the Linear agent: which stories exist, which is primary.
237
- const stories = listStories(args.demoDir);
238
- if (args.json) {
239
- console.log(JSON.stringify(stories));
240
- } else if (stories.length === 0) {
241
- console.log("No stories yet in demo/stories/.");
242
- } else {
243
- for (const s of stories) {
244
- console.log(
245
- `${s.primary ? "* " : " "}${s.name}${s.description ? `: ${s.description}` : ""}${s.extends ? ` (${s.extends}, for ${s.brand})` : ""}`,
246
- );
247
- }
248
- }
249
- return 0;
250
- }
251
- case "stills":
252
- console.log(`Stills from ${extractStillsFrom(args.outDir)}`);
253
- return 0;
254
- }
255
- return 2;
256
- };
257
-
258
- const main = () => {
259
- try {
260
- return run();
261
- } catch (error) {
262
- const message = error instanceof Error ? error.message : String(error);
263
- if (process.argv.includes("--json")) {
264
- console.log(JSON.stringify({ ok: false, error: message }));
265
- } else {
266
- console.error(message);
267
- }
268
- return 1;
269
- }
270
- };
271
-
272
- process.exitCode = main();
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console -- a CLI; the console is its output */
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import { createRequire } from "node:module";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+
9
+ import { listStories, parseArgs } from "./args.mjs";
10
+ import { encodeRecording, extractStillsFrom } from "./encode.mjs";
11
+ import { initApp, initProject } from "./init.mjs";
12
+ import { appMenu, menuMarkdown, pick, pickExact } from "./pick.mjs";
13
+ import { buildPitchBrand } from "./pitch.mjs";
14
+ import { writeRunner } from "./runner.mjs";
15
+
16
+ // Runs the app's own Playwright (resolved from the app, never from this
17
+ // package) against the config this CLI writes into demo/out/.work/runner/.
18
+
19
+ // A run's work folder goes when the command ends. A failure first keeps
20
+ // Playwright's results (its error context) in demo/out/failed/; a success
21
+ // clears an older failure, so what's there is always the latest.
22
+ const finish = (args, ok) => {
23
+ const failed = path.join(args.outDir, "failed");
24
+ fs.rmSync(failed, { recursive: true, force: true });
25
+ const results = path.join(args.workDir, "test-results");
26
+ if (!ok && fs.existsSync(results)) {
27
+ fs.cpSync(results, failed, { recursive: true });
28
+ console.error(`Playwright's output for this failure: ${failed}`);
29
+ }
30
+ fs.rmSync(args.workDir, { recursive: true, force: true });
31
+ const work = path.dirname(args.workDir);
32
+ if (fs.existsSync(work) && fs.readdirSync(work).length === 0) {
33
+ fs.rmdirSync(work);
34
+ }
35
+ };
36
+
37
+ const runPlaywright = (args, mode) => {
38
+ const require = createRequire(path.join(args.appDir, "package.json"));
39
+ const cli = require.resolve("@playwright/test/cli");
40
+ fs.rmSync(args.workDir, { recursive: true, force: true });
41
+ fs.mkdirSync(args.workDir, { recursive: true });
42
+ writeRunner(args);
43
+ const result = spawnSync(
44
+ process.execPath,
45
+ [cli, "test", "-c", args.configPath],
46
+ {
47
+ cwd: args.appDir,
48
+ // With --json, Playwright's own output goes to stderr so stdout is pure JSON.
49
+ stdio: ["inherit", args.json ? 2 : "inherit", "inherit"],
50
+ env: {
51
+ ...process.env,
52
+ DEMO_MODE: mode,
53
+ DEMO_STORYBOARD: args.storyboardPath ?? "",
54
+ DEMO_BRAND: args.brand ?? "",
55
+ DEMO_BRANDS: args.brandsDir ?? "",
56
+ DEMO_OUT: args.outDir,
57
+ DEMO_WORK: args.workDir,
58
+ DEMO_DRAFT: args.draft ? "1" : "",
59
+ },
60
+ },
61
+ );
62
+ return result.status ?? 1;
63
+ };
64
+
65
+ const printReport = (report) => {
66
+ if (report.ok) {
67
+ console.log(`✓ story passes (${report.steps} steps)`);
68
+ return;
69
+ }
70
+ const where = report.step
71
+ ? `step ${report.step} (${report.do}${report.target ? ` ${report.target}` : ""})`
72
+ : "before the first step";
73
+ console.log(`✗ ${where}: ${report.error}`);
74
+ if (report.visible?.length) {
75
+ console.log(` visible testids: ${report.visible.join(", ")}`);
76
+ }
77
+ };
78
+
79
+ // demo-video init [--app <dir>] [--dry-run]: needs no demo/ yet, so it skips
80
+ // parseArgs. Without --app: the repo, plus its app if it has exactly one.
81
+ const runInit = (argv) => {
82
+ const dryRun = argv.includes("--dry-run");
83
+ const at = argv.indexOf("--app");
84
+ const value = at === -1 ? null : argv[at + 1];
85
+ // A flag after --app (--app --dry-run) is not its directory.
86
+ const appMissing = at !== -1 && (!value || value.startsWith("-"));
87
+ const app = appMissing ? null : value;
88
+ const unknown = argv.filter(
89
+ (a, i) =>
90
+ a !== "--dry-run" && a !== "--app" && !(app && argv[i - 1] === "--app"),
91
+ );
92
+ if (appMissing || unknown.length) {
93
+ // Name what was wrong: "-app" for "--app", or a second command on the line.
94
+ const problem = appMissing
95
+ ? "--app needs a directory right after it"
96
+ : `Unknown argument${unknown.length > 1 ? "s" : ""}: ${unknown.join(" ")}`;
97
+ console.error(
98
+ `${problem}\nUsage: demo-video init [--app <dir>] [--dry-run]`,
99
+ );
100
+ return 2;
101
+ }
102
+ const root = process.cwd();
103
+ const result = app
104
+ ? initApp(path.resolve(root, app), { dryRun })
105
+ : initProject(root, { dryRun });
106
+ const what = app
107
+ ? `app ${app}`
108
+ : result.apps.length === 1
109
+ ? `repo and its app${result.apps[0] === "." ? " (the repo root)" : ` ${result.apps[0]}`}`
110
+ : "repo";
111
+ console.log(
112
+ `demo-video init${dryRun ? " (dry run: nothing written)" : ""}: ${what}`,
113
+ );
114
+ const width = Math.max(...result.changes.map(([, what]) => what.length));
115
+ for (const [status, what, detail] of result.changes) {
116
+ console.log(` ${status.padEnd(8)} ${what.padEnd(width)} ${detail}`);
117
+ }
118
+ if (result.todo.length) {
119
+ console.log("\nBy hand:");
120
+ for (const item of result.todo) {
121
+ console.log(` - ${item}`);
122
+ }
123
+ }
124
+ return 0;
125
+ };
126
+
127
+ // demo-video pick [--request "<text>" | --app <a> --story <s> --brand <b>]
128
+ // [--json] <app dir>...: which existing story and brand a request means
129
+ // (see pick.mjs). --request reads a sentence (a /demo-video comment); the
130
+ // flags are exact names (the Run workflow form).
131
+ const runPick = (argv) => {
132
+ const flags = {};
133
+ const dirs = [];
134
+ let json = false;
135
+ for (let i = 0; i < argv.length; i++) {
136
+ const arg = argv[i];
137
+ if (arg === "--json") {
138
+ json = true;
139
+ } else if (["--request", "--app", "--story", "--brand"].includes(arg)) {
140
+ flags[arg.slice(2)] = argv[++i] ?? "";
141
+ } else {
142
+ dirs.push(arg);
143
+ }
144
+ }
145
+ if (dirs.length === 0) {
146
+ console.error(
147
+ 'Usage: demo-video pick [--request "<text>" | --app <a> --story <s> --brand <b>] [--json] <app dir>...',
148
+ );
149
+ return 2;
150
+ }
151
+ const apps = dirs.map((d) => appMenu(path.resolve(d)));
152
+ const result =
153
+ flags.request !== undefined
154
+ ? pick(flags.request, apps)
155
+ : pickExact(apps, flags);
156
+ const out = result.menu
157
+ ? {
158
+ menu: true,
159
+ reason: result.reason,
160
+ markdown: menuMarkdown(result.apps ?? apps, result.reason),
161
+ }
162
+ : result;
163
+ if (json) {
164
+ console.log(JSON.stringify(out));
165
+ } else if (out.menu) {
166
+ console.log(out.markdown);
167
+ } else {
168
+ console.log(
169
+ `${out.app}: story ${out.story}, brand ${out.brand ?? "(the app's default)"}`,
170
+ );
171
+ }
172
+ return 0;
173
+ };
174
+
175
+ // demo-video pitch <proposal.json> [--app <dir>] [--story <name>] [--out <dir>]
176
+ // [--json]: a pitch draft from a researched proposal (the skill's linear.md).
177
+ // It builds the brand in a temp folder (pitch.mjs), checks the story with
178
+ // it, films it at 30 fps and moves the video and its contact sheet to --out
179
+ // (default: the proposal's folder). The brand and the repo's copies go.
180
+ const runPitch = async (argv) => {
181
+ const flags = { app: ".", story: null, out: null };
182
+ let file = null;
183
+ for (let i = 0; i < argv.length; i++) {
184
+ const arg = argv[i];
185
+ if (["--app", "--story", "--out"].includes(arg)) {
186
+ flags[arg.slice(2)] = argv[++i] ?? null;
187
+ } else if (arg !== "--json") {
188
+ file = arg;
189
+ }
190
+ }
191
+ if (!file) {
192
+ console.error(
193
+ "Usage: demo-video pitch <proposal.json> [--app <dir>] [--story <name>] [--out <dir>] [--json]",
194
+ );
195
+ return 2;
196
+ }
197
+ const proposalPath = path.resolve(file);
198
+ const out = path.resolve(flags.out ?? path.dirname(proposalPath));
199
+ const brandsDir = fs.mkdtempSync(path.join(os.tmpdir(), "demo-video-pitch-"));
200
+ try {
201
+ const built = await buildPitchBrand(
202
+ JSON.parse(fs.readFileSync(proposalPath, "utf8")),
203
+ brandsDir,
204
+ );
205
+ // --json keeps Playwright's output off stdout.
206
+ const common = [
207
+ "--app",
208
+ flags.app,
209
+ ...(flags.story ? ["--story", flags.story] : []),
210
+ "--brand",
211
+ built.brand,
212
+ "--brands",
213
+ brandsDir,
214
+ "--json",
215
+ ];
216
+ const check = parseArgs(["check", ...common], process.cwd());
217
+ const checked = runPlaywright(check, "check");
218
+ const reportPath = path.join(check.workDir, "check.json");
219
+ const report = fs.existsSync(reportPath)
220
+ ? JSON.parse(fs.readFileSync(reportPath, "utf8"))
221
+ : { ok: false, error: "the dry run produced no report" };
222
+ finish(check, report.ok && checked === 0);
223
+ if (!report.ok || checked !== 0) {
224
+ throw new Error(
225
+ `The researched brand doesn't film with this story: ${report.error}`,
226
+ );
227
+ }
228
+ // A pitch is a draft for one customer: 30 fps, about half the time.
229
+ const render = parseArgs(["render", ...common, "--draft"], process.cwd());
230
+ if (runPlaywright(render, "record") !== 0) {
231
+ finish(render, false);
232
+ throw new Error("Filming the pitch draft failed (see above)");
233
+ }
234
+ fs.mkdirSync(render.outDir, { recursive: true });
235
+ const result = encodeRecording(render.workDir, render.outDir);
236
+ finish(render, true);
237
+ // The video leaves the repo; its stills and sheet there go.
238
+ fs.mkdirSync(out, { recursive: true });
239
+ const video = path.join(out, path.basename(result.output));
240
+ const sheet = path.join(
241
+ out,
242
+ `${path.basename(result.output, ".mp4")}-sheet.jpg`,
243
+ );
244
+ fs.copyFileSync(result.output, video);
245
+ fs.copyFileSync(result.sheet, sheet);
246
+ for (const f of [result.output, result.sheet, ...result.stills]) {
247
+ fs.rmSync(f, { force: true });
248
+ }
249
+ const review = path.dirname(result.sheet);
250
+ if (fs.existsSync(review) && fs.readdirSync(review).length === 0) {
251
+ fs.rmdirSync(review);
252
+ }
253
+ const summary = { ...built, video, sheet, duration: result.duration };
254
+ if (argv.includes("--json")) {
255
+ console.log(JSON.stringify(summary));
256
+ } else {
257
+ console.log(
258
+ `Wrote ${video} (${result.duration.toFixed(1)} s) and ${sheet}`,
259
+ );
260
+ }
261
+ return 0;
262
+ } finally {
263
+ fs.rmSync(brandsDir, { recursive: true, force: true });
264
+ }
265
+ };
266
+
267
+ const run = () => {
268
+ if (process.argv[2] === "pitch") {
269
+ return runPitch(process.argv.slice(3));
270
+ }
271
+ if (process.argv[2] === "init") {
272
+ return runInit(process.argv.slice(3));
273
+ }
274
+ if (process.argv[2] === "pick") {
275
+ return runPick(process.argv.slice(3));
276
+ }
277
+ let args;
278
+ try {
279
+ args = parseArgs(process.argv.slice(2), process.cwd());
280
+ } catch (error) {
281
+ console.error(error.message);
282
+ return 2;
283
+ }
284
+ switch (args.command) {
285
+ case "check": {
286
+ const status = runPlaywright(args, "check");
287
+ // The report comes back through the work folder and goes with it.
288
+ const reportPath = path.join(args.workDir, "check.json");
289
+ const report = fs.existsSync(reportPath)
290
+ ? JSON.parse(fs.readFileSync(reportPath, "utf8"))
291
+ : {
292
+ ok: false,
293
+ error:
294
+ "the dry run produced no report (Playwright did not run the demo test — see its output above)",
295
+ };
296
+ const ok = report.ok && status === 0;
297
+ finish(args, ok);
298
+ if (args.json) {
299
+ console.log(JSON.stringify(report, null, 2));
300
+ } else {
301
+ printReport(report);
302
+ }
303
+ return ok ? 0 : 1;
304
+ }
305
+ case "record": {
306
+ // The recording stays in the work folder for `encode`.
307
+ const status = runPlaywright(args, "record");
308
+ if (status !== 0) {
309
+ finish(args, false);
310
+ }
311
+ return status;
312
+ }
313
+ case "render": {
314
+ const status = runPlaywright(args, "record");
315
+ if (status !== 0) {
316
+ finish(args, false);
317
+ return status;
318
+ }
319
+ fs.mkdirSync(args.outDir, { recursive: true });
320
+ const result = encodeRecording(args.workDir, args.outDir);
321
+ finish(args, true);
322
+ if (args.json) {
323
+ // For CI: { output, duration, stills, sheet } on stdout, nothing else.
324
+ console.log(JSON.stringify(result));
325
+ } else {
326
+ console.log(`Wrote ${result.output} (${result.duration.toFixed(1)} s)`);
327
+ console.log(`Review: ${result.sheet} (one frame a second)`);
328
+ }
329
+ return 0;
330
+ }
331
+ case "encode": {
332
+ fs.mkdirSync(args.outDir, { recursive: true });
333
+ const { output, duration } = encodeRecording(args.workDir, args.outDir);
334
+ finish(args, true);
335
+ console.log(`Wrote ${output} (${duration.toFixed(1)} s)`);
336
+ return 0;
337
+ }
338
+ case "list": {
339
+ // For people and the workflows: which stories exist, which is primary.
340
+ const stories = listStories(args.demoDir);
341
+ if (args.json) {
342
+ console.log(JSON.stringify(stories));
343
+ } else if (stories.length === 0) {
344
+ console.log("No stories yet in demo/stories/.");
345
+ } else {
346
+ for (const s of stories) {
347
+ console.log(
348
+ `${s.primary ? "* " : " "}${s.name}${s.description ? `: ${s.description}` : ""}${s.extends ? ` (${s.extends}, for ${s.brand})` : ""}`,
349
+ );
350
+ }
351
+ }
352
+ return 0;
353
+ }
354
+ case "stills":
355
+ console.log(`Stills from ${extractStillsFrom(args.outDir)}`);
356
+ return 0;
357
+ case "brand-needs": {
358
+ const status = runPlaywright(args, "needs");
359
+ const needsPath = path.join(args.workDir, "needs.json");
360
+ const needs =
361
+ status === 0 && fs.existsSync(needsPath)
362
+ ? JSON.parse(fs.readFileSync(needsPath, "utf8"))
363
+ : null;
364
+ finish(args, Boolean(needs));
365
+ if (!needs) {
366
+ throw new Error("Couldn't read the adapter's brand needs (see above)");
367
+ }
368
+ if (args.json) {
369
+ console.log(JSON.stringify(needs, null, 2));
370
+ return 0;
371
+ }
372
+ const lines = (entries) =>
373
+ Object.entries(entries).map(
374
+ ([key, need]) =>
375
+ ` ${key}${need.optional ? " (optional)" : ""}: ${need.what}`,
376
+ );
377
+ console.log(`Every brand, for the video:`);
378
+ for (const [key, what] of Object.entries(needs.video)) {
379
+ console.log(` ${key}: ${what}`);
380
+ }
381
+ const own = [
382
+ ...lines(
383
+ Object.fromEntries(
384
+ Object.entries(needs.appNeeds.config).map(([k, v]) => [
385
+ `config.${k}`,
386
+ v,
387
+ ]),
388
+ ),
389
+ ),
390
+ ...lines(
391
+ Object.fromEntries(
392
+ Object.entries(needs.appNeeds.assets).map(([k, v]) => [
393
+ `assets.${k}`,
394
+ v,
395
+ ]),
396
+ ),
397
+ ),
398
+ ];
399
+ console.log(`\n${needs.app} itself:`);
400
+ console.log(
401
+ own.length
402
+ ? own.join("\n")
403
+ : " (nothing listed: the adapter has no brandNeeds)",
404
+ );
405
+ return 0;
406
+ }
407
+ }
408
+ return 2;
409
+ };
410
+
411
+ const main = async () => {
412
+ try {
413
+ return await run();
414
+ } catch (error) {
415
+ const message = error instanceof Error ? error.message : String(error);
416
+ if (process.argv.includes("--json")) {
417
+ console.log(JSON.stringify({ ok: false, error: message }));
418
+ } else {
419
+ console.error(message);
420
+ }
421
+ return 1;
422
+ }
423
+ };
424
+
425
+ process.exitCode = await main();