@kadj-amoah/showrunner 1.1.0 → 1.1.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.
- package/CHANGELOG.md +32 -0
- package/README.md +48 -2
- package/dist/cli.js +253 -72
- package/dist/cli.js.map +1 -1
- package/dist/index.js +35 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -4
package/dist/cli.js
CHANGED
|
@@ -687,7 +687,7 @@ var HEADER = `// scripts/playwright_demo.ts
|
|
|
687
687
|
function renderPlaywrightSpec(inputs) {
|
|
688
688
|
const { manifest, recording, targetUrl, videoDir, traceDir } = inputs;
|
|
689
689
|
const launchLines = [
|
|
690
|
-
`import { chromium, firefox, webkit,
|
|
690
|
+
`import { chromium, firefox, webkit, type BrowserContext } from 'playwright-core';`,
|
|
691
691
|
`import { mkdirSync } from 'node:fs';`,
|
|
692
692
|
``,
|
|
693
693
|
`const VIDEO_DIR = ${JSON.stringify(videoDir)};`,
|
|
@@ -774,7 +774,7 @@ function renderAction(action) {
|
|
|
774
774
|
case "wait":
|
|
775
775
|
return `await page.waitForTimeout(${action.ms});`;
|
|
776
776
|
case "assert_visible":
|
|
777
|
-
return `await
|
|
777
|
+
return `await page.locator(${JSON.stringify(flattenSelector(action.selector))}).first().waitFor({ state: 'visible' });`;
|
|
778
778
|
case "press":
|
|
779
779
|
return action.selector ? `await page.locator(${JSON.stringify(flattenSelector(action.selector))}).first().press(${JSON.stringify(action.key)});` : `await page.keyboard.press(${JSON.stringify(action.key)});`;
|
|
780
780
|
case "custom":
|
|
@@ -1229,7 +1229,7 @@ async function generateManifest(opts) {
|
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
1231
|
// src/scriptGen/domPreflight.ts
|
|
1232
|
-
import { chromium, firefox, webkit } from "playwright";
|
|
1232
|
+
import { chromium, firefox, webkit } from "playwright-core";
|
|
1233
1233
|
|
|
1234
1234
|
// src/recording/selectorHeuristic.ts
|
|
1235
1235
|
var SELECTOR_FOR_FN_SOURCE = `
|
|
@@ -1318,7 +1318,7 @@ async function scrapeSelectorInventory(opts) {
|
|
|
1318
1318
|
try {
|
|
1319
1319
|
await page.waitForLoadState("networkidle", { timeout: networkIdleMs });
|
|
1320
1320
|
} catch {
|
|
1321
|
-
logger.debug("domPreflight: networkidle did not fire within budget \
|
|
1321
|
+
logger.debug("domPreflight: networkidle did not fire within budget \xE2\u20AC\u201D proceeding");
|
|
1322
1322
|
}
|
|
1323
1323
|
const items = await page.evaluate(
|
|
1324
1324
|
buildScrapeScript(SELECTOR_FOR_FN_SOURCE, maxItems)
|
|
@@ -2219,7 +2219,7 @@ import {
|
|
|
2219
2219
|
chromium as chromium2,
|
|
2220
2220
|
firefox as firefox2,
|
|
2221
2221
|
webkit as webkit2
|
|
2222
|
-
} from "playwright";
|
|
2222
|
+
} from "playwright-core";
|
|
2223
2223
|
|
|
2224
2224
|
// src/manifest/alignment.ts
|
|
2225
2225
|
import { readFile as readFile6 } from "fs/promises";
|
|
@@ -2330,7 +2330,7 @@ async function buildStorageState(cookiesFile, localStorageFile, configDir) {
|
|
|
2330
2330
|
cookiesJson.origins = [...cookiesJson.origins ?? [], ...lsJson];
|
|
2331
2331
|
} else {
|
|
2332
2332
|
logger.warn(
|
|
2333
|
-
`local_storage_file at ${lsAbs} is not a Playwright origins list \
|
|
2333
|
+
`local_storage_file at ${lsAbs} is not a Playwright origins list \xE2\u20AC\u201D skipping. Use storageState() output format.`
|
|
2334
2334
|
);
|
|
2335
2335
|
}
|
|
2336
2336
|
} catch (err) {
|
|
@@ -2371,7 +2371,7 @@ function buildFormFill(auth) {
|
|
|
2371
2371
|
} catch (err) {
|
|
2372
2372
|
const cause = err instanceof Error ? err.message : String(err);
|
|
2373
2373
|
throw new AuthError(
|
|
2374
|
-
`Form auth: did not reach ${auth.success_url_pattern} within ${auth.timeout_ms}ms \
|
|
2374
|
+
`Form auth: did not reach ${auth.success_url_pattern} within ${auth.timeout_ms}ms \xE2\u20AC\u201D ${cause}`
|
|
2375
2375
|
);
|
|
2376
2376
|
}
|
|
2377
2377
|
};
|
|
@@ -2380,7 +2380,7 @@ function buildFormFill(auth) {
|
|
|
2380
2380
|
// src/recording/actions.ts
|
|
2381
2381
|
import { mkdir as mkdir5 } from "fs/promises";
|
|
2382
2382
|
import { dirname as dirname5, join as join2 } from "path";
|
|
2383
|
-
import { errors as playwrightErrors } from "playwright";
|
|
2383
|
+
import { errors as playwrightErrors } from "playwright-core";
|
|
2384
2384
|
|
|
2385
2385
|
// src/recording/cursorOverlay.ts
|
|
2386
2386
|
var CURSOR_INIT_SCRIPT = `
|
|
@@ -2454,7 +2454,7 @@ var CURSOR_INIT_SCRIPT = `
|
|
|
2454
2454
|
|
|
2455
2455
|
if (tryInstall()) return;
|
|
2456
2456
|
|
|
2457
|
-
console.log('[showrunner-cursor] deferring \
|
|
2457
|
+
console.log('[showrunner-cursor] deferring \xE2\u20AC\u201D body not ready');
|
|
2458
2458
|
|
|
2459
2459
|
const tick = () => { if (tryInstall()) cleanup(); };
|
|
2460
2460
|
const observer = new MutationObserver(tick);
|
|
@@ -2738,7 +2738,7 @@ async function recordDemo(opts) {
|
|
|
2738
2738
|
const tFirstSegmentStart = performance.now();
|
|
2739
2739
|
const preSegmentOffsetSec = (tFirstSegmentStart - tRecordingStart) / 1e3;
|
|
2740
2740
|
logger.info(
|
|
2741
|
-
`pre-segment recording offset: ${preSegmentOffsetSec.toFixed(2)}s (page.goto + setup) \
|
|
2741
|
+
`pre-segment recording offset: ${preSegmentOffsetSec.toFixed(2)}s (page.goto + setup) \xE2\u20AC\u201D slice_plan will be offset by this amount`
|
|
2742
2742
|
);
|
|
2743
2743
|
const t0 = tFirstSegmentStart;
|
|
2744
2744
|
let cursorPos = {
|
|
@@ -2817,7 +2817,7 @@ async function recordDemo(opts) {
|
|
|
2817
2817
|
);
|
|
2818
2818
|
if (adjusted < motionMs) {
|
|
2819
2819
|
warnings.push(
|
|
2820
|
-
`cursor motion for action at ${arrivalAt}s clamped from ${Math.round(motionMs)}ms to ${adjusted}ms \
|
|
2820
|
+
`cursor motion for action at ${arrivalAt}s clamped from ${Math.round(motionMs)}ms to ${adjusted}ms \xE2\u20AC\u201D manifest timing tight`
|
|
2821
2821
|
);
|
|
2822
2822
|
}
|
|
2823
2823
|
motionMs = adjusted;
|
|
@@ -2865,14 +2865,14 @@ async function recordDemo(opts) {
|
|
|
2865
2865
|
if (outcome.status === "skipped") {
|
|
2866
2866
|
warnings.push(`${action.type}: ${outcome.reason}`);
|
|
2867
2867
|
if (outcome.screenshot) failureScreenshots.push(outcome.screenshot);
|
|
2868
|
-
logger.warn(`segment ${seg.id} \
|
|
2868
|
+
logger.warn(`segment ${seg.id} \xE2\u20AC\u201D ${action.type} skipped`, {
|
|
2869
2869
|
reason: outcome.reason,
|
|
2870
2870
|
screenshot: outcome.screenshot
|
|
2871
2871
|
});
|
|
2872
2872
|
} else if (outcome.status === "segment_failed") {
|
|
2873
2873
|
failure = outcome.reason;
|
|
2874
2874
|
if (outcome.screenshot) failureScreenshots.push(outcome.screenshot);
|
|
2875
|
-
logger.error(`segment ${seg.id} \
|
|
2875
|
+
logger.error(`segment ${seg.id} \xE2\u20AC\u201D failed`, {
|
|
2876
2876
|
reason: outcome.reason,
|
|
2877
2877
|
screenshot: outcome.screenshot
|
|
2878
2878
|
});
|
|
@@ -2889,7 +2889,7 @@ async function recordDemo(opts) {
|
|
|
2889
2889
|
await page.waitForTimeout(Math.round(remaining * 1e3));
|
|
2890
2890
|
} else if (remaining < -0.5) {
|
|
2891
2891
|
warnings.push(
|
|
2892
|
-
`segment took ${elapsedInSegment.toFixed(2)}s but only ${allocated.toFixed(2)}s allocated \
|
|
2892
|
+
`segment took ${elapsedInSegment.toFixed(2)}s but only ${allocated.toFixed(2)}s allocated \xE2\u20AC\u201D manifest timing tight for this segment`
|
|
2893
2893
|
);
|
|
2894
2894
|
}
|
|
2895
2895
|
const segEnd = (performance.now() - t0) / 1e3;
|
|
@@ -2924,7 +2924,7 @@ async function recordDemo(opts) {
|
|
|
2924
2924
|
}
|
|
2925
2925
|
recordingPath = dest;
|
|
2926
2926
|
} else {
|
|
2927
|
-
logger.warn("Recording context did not produce a video file \
|
|
2927
|
+
logger.warn("Recording context did not produce a video file \xE2\u20AC\u201D recordVideo may have been ignored");
|
|
2928
2928
|
}
|
|
2929
2929
|
return {
|
|
2930
2930
|
recording_path: recordingPath,
|
|
@@ -2958,10 +2958,10 @@ async function resolveStorageState(browser, recording, authPlan) {
|
|
|
2958
2958
|
var PreflightError = class extends Error {
|
|
2959
2959
|
constructor(failures) {
|
|
2960
2960
|
const lines = failures.map(
|
|
2961
|
-
(f) => ` - ${f.segment}#${f.actionIndex} \
|
|
2961
|
+
(f) => ` - ${f.segment}#${f.actionIndex} \xE2\u2020\u2019 [${f.selectors.join(" | ")}]`
|
|
2962
2962
|
);
|
|
2963
2963
|
super(
|
|
2964
|
-
`Pre-flight check failed \
|
|
2964
|
+
`Pre-flight check failed \xE2\u20AC\u201D ${failures.length} selector(s) did not resolve on ${failures[0]?.segment ? `the live target page` : "the page"}:
|
|
2965
2965
|
${lines.join("\n")}
|
|
2966
2966
|
|
|
2967
2967
|
Fix the manifest selectors, or set recording.preflight: false to skip this check.`
|
|
@@ -5013,6 +5013,16 @@ function resolveMaybe(p, configDir) {
|
|
|
5013
5013
|
var STAGE_NAMES = ["comprehension", "script", "voiceover", "record", "mux"];
|
|
5014
5014
|
|
|
5015
5015
|
// src/pipeline/run.ts
|
|
5016
|
+
var PipelineStageError = class extends Error {
|
|
5017
|
+
constructor(stage, message, cause) {
|
|
5018
|
+
super(message);
|
|
5019
|
+
this.stage = stage;
|
|
5020
|
+
this.cause = cause;
|
|
5021
|
+
}
|
|
5022
|
+
stage;
|
|
5023
|
+
cause;
|
|
5024
|
+
name = "PipelineStageError";
|
|
5025
|
+
};
|
|
5016
5026
|
var STAGES_BY_NAME = {
|
|
5017
5027
|
comprehension: comprehensionStage,
|
|
5018
5028
|
script: scriptStage,
|
|
@@ -5052,7 +5062,14 @@ async function run(loaded, options = {}) {
|
|
|
5052
5062
|
continue;
|
|
5053
5063
|
}
|
|
5054
5064
|
const stage = STAGES_BY_NAME[name];
|
|
5055
|
-
|
|
5065
|
+
let result;
|
|
5066
|
+
try {
|
|
5067
|
+
result = await stage.run(ctx);
|
|
5068
|
+
} catch (err) {
|
|
5069
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5070
|
+
logger.event({ stage: name, status: "failed", error: message });
|
|
5071
|
+
throw new PipelineStageError(name, message, err);
|
|
5072
|
+
}
|
|
5056
5073
|
results.push(result);
|
|
5057
5074
|
logger.event({
|
|
5058
5075
|
stage: name,
|
|
@@ -5117,10 +5134,11 @@ async function writeBuildManifest(path, payload) {
|
|
|
5117
5134
|
|
|
5118
5135
|
// src/commands/doctor.ts
|
|
5119
5136
|
import { spawn as spawn3 } from "child_process";
|
|
5120
|
-
import { stat as stat9, access, constants } from "fs/promises";
|
|
5121
|
-
import { isAbsolute as isAbsolute7, resolve as resolve13, dirname as dirname9 } from "path";
|
|
5137
|
+
import { stat as stat9, access, constants, readdir } from "fs/promises";
|
|
5138
|
+
import { isAbsolute as isAbsolute7, resolve as resolve13, dirname as dirname9, join as join9 } from "path";
|
|
5139
|
+
import { homedir, platform as osPlatform } from "os";
|
|
5122
5140
|
import { request as undiciRequest } from "undici";
|
|
5123
|
-
import { chromium as chromium3, firefox as firefox3, webkit as webkit3 } from "playwright";
|
|
5141
|
+
import { chromium as chromium3, firefox as firefox3, webkit as webkit3 } from "playwright-core";
|
|
5124
5142
|
|
|
5125
5143
|
// src/config/providerEnv.ts
|
|
5126
5144
|
function inspectProviderEnv(config) {
|
|
@@ -5188,6 +5206,7 @@ async function doctorCommand(opts) {
|
|
|
5188
5206
|
for (const r of results) printRow(r);
|
|
5189
5207
|
const summary = summarize(results);
|
|
5190
5208
|
if (summary.fail > 0) {
|
|
5209
|
+
printFixOrder(results);
|
|
5191
5210
|
logger.error(`doctor: ${summary.fail} FAIL, ${summary.warn} WARN, ${summary.pass} PASS`);
|
|
5192
5211
|
} else if (summary.warn > 0) {
|
|
5193
5212
|
logger.warn(`doctor: ${summary.warn} WARN, ${summary.pass} PASS`);
|
|
@@ -5213,10 +5232,18 @@ async function runDoctorChecks(configPath) {
|
|
|
5213
5232
|
const { config, configDir } = loaded;
|
|
5214
5233
|
for (const row of inspectProviderEnv(config)) {
|
|
5215
5234
|
const suffix = row.stage ? ` (${row.stage} override)` : "";
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5235
|
+
if (row.set) {
|
|
5236
|
+
results.push({
|
|
5237
|
+
status: "PASS",
|
|
5238
|
+
label: `${row.slot} provider=${row.provider}${suffix}, ${row.envVar} set`
|
|
5239
|
+
});
|
|
5240
|
+
} else {
|
|
5241
|
+
results.push({
|
|
5242
|
+
status: "FAIL",
|
|
5243
|
+
label: `${row.slot} provider=${row.provider}${suffix}, ${row.envVar} NOT set`,
|
|
5244
|
+
detail: providerEnvHint(row.slot, row.provider, row.envVar)
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5220
5247
|
}
|
|
5221
5248
|
results.push(await checkBinary("ffmpeg", ["-version"]));
|
|
5222
5249
|
results.push(await checkBinary("ffprobe", ["-version"]));
|
|
@@ -5271,9 +5298,20 @@ function summarize(results) {
|
|
|
5271
5298
|
fail: results.filter((r) => r.status === "FAIL").length
|
|
5272
5299
|
};
|
|
5273
5300
|
}
|
|
5301
|
+
function printFixOrder(results) {
|
|
5302
|
+
const fails = results.filter((r) => r.status === "FAIL");
|
|
5303
|
+
if (fails.length === 0) return;
|
|
5304
|
+
process.stdout.write("\n");
|
|
5305
|
+
logger.info("To fix:");
|
|
5306
|
+
fails.forEach((r, i) => {
|
|
5307
|
+
const hint = r.detail ? ` \u2014 ${r.detail}` : "";
|
|
5308
|
+
logger.info(` ${i + 1}. ${r.label}${hint}`);
|
|
5309
|
+
});
|
|
5310
|
+
process.stdout.write("\n");
|
|
5311
|
+
}
|
|
5274
5312
|
function printRow(r) {
|
|
5275
5313
|
const tag = `[${r.status}]`;
|
|
5276
|
-
const line = `${tag} ${r.label}${r.detail ? ` \
|
|
5314
|
+
const line = `${tag} ${r.label}${r.detail ? ` \xE2\u20AC\u201D ${r.detail}` : ""}`;
|
|
5277
5315
|
if (r.status === "FAIL") logger.error(line);
|
|
5278
5316
|
else if (r.status === "WARN") logger.warn(line);
|
|
5279
5317
|
else logger.info(line);
|
|
@@ -5286,18 +5324,44 @@ async function checkBinary(name, args) {
|
|
|
5286
5324
|
stdout += chunk.toString("utf8");
|
|
5287
5325
|
});
|
|
5288
5326
|
child.on("error", () => {
|
|
5289
|
-
resolve27({
|
|
5327
|
+
resolve27({
|
|
5328
|
+
status: "FAIL",
|
|
5329
|
+
label: `${name} not on PATH`,
|
|
5330
|
+
detail: installHintFor(name)
|
|
5331
|
+
});
|
|
5290
5332
|
});
|
|
5291
5333
|
child.on("exit", (code) => {
|
|
5292
5334
|
if (code === 0) {
|
|
5293
5335
|
const firstLine = stdout.split("\n")[0]?.trim() ?? "";
|
|
5294
5336
|
resolve27({ status: "PASS", label: `${name} present`, detail: firstLine });
|
|
5295
5337
|
} else {
|
|
5296
|
-
resolve27({
|
|
5338
|
+
resolve27({
|
|
5339
|
+
status: "FAIL",
|
|
5340
|
+
label: `${name} exited with code ${code}`,
|
|
5341
|
+
detail: installHintFor(name)
|
|
5342
|
+
});
|
|
5297
5343
|
}
|
|
5298
5344
|
});
|
|
5299
5345
|
});
|
|
5300
5346
|
}
|
|
5347
|
+
function providerEnvHint(slot, provider, envVar) {
|
|
5348
|
+
const dashboards = {
|
|
5349
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
5350
|
+
openai: "https://platform.openai.com/api-keys",
|
|
5351
|
+
elevenlabs: "https://elevenlabs.io/app/settings/api-keys"
|
|
5352
|
+
};
|
|
5353
|
+
const dash = dashboards[provider];
|
|
5354
|
+
const dashHint = dash ? ` (get a key from ${dash})` : "";
|
|
5355
|
+
const altHint = slot === "llm" ? ` \u2014 or switch llm.default.provider to "agent_bridge" in demo.yaml to use a local CLI agent (no API key needed)` : "";
|
|
5356
|
+
return `add ${envVar}=... to your project's .env file${dashHint}${altHint}`;
|
|
5357
|
+
}
|
|
5358
|
+
function installHintFor(binary) {
|
|
5359
|
+
const p = osPlatform();
|
|
5360
|
+
if (binary !== "ffmpeg" && binary !== "ffprobe") return "";
|
|
5361
|
+
if (p === "darwin") return "install via `brew install ffmpeg`";
|
|
5362
|
+
if (p === "win32") return "install via `winget install Gyan.FFmpeg` or `choco install ffmpeg`";
|
|
5363
|
+
return "install via `apt install ffmpeg` (Debian/Ubuntu), `pacman -S ffmpeg` (Arch), or `dnf install ffmpeg` (Fedora)";
|
|
5364
|
+
}
|
|
5301
5365
|
async function checkPlaywrightBrowser(config) {
|
|
5302
5366
|
const browserName = config.recording.browser;
|
|
5303
5367
|
try {
|
|
@@ -5309,13 +5373,48 @@ async function checkPlaywrightBrowser(config) {
|
|
|
5309
5373
|
detail: exec
|
|
5310
5374
|
};
|
|
5311
5375
|
} catch (err) {
|
|
5376
|
+
const sudoHit = await probeRootCache(browserName);
|
|
5377
|
+
const fallback = `run \`npx playwright install ${browserName}\``;
|
|
5378
|
+
if (sudoHit) {
|
|
5379
|
+
return {
|
|
5380
|
+
status: "FAIL",
|
|
5381
|
+
label: `playwright ${browserName} binary missing for current user, but found in root/admin cache`,
|
|
5382
|
+
detail: `${sudoHit} \u2014 re-run install WITHOUT sudo so the browser lands in your user cache: ${fallback}`
|
|
5383
|
+
};
|
|
5384
|
+
}
|
|
5312
5385
|
return {
|
|
5313
5386
|
status: "FAIL",
|
|
5314
5387
|
label: `playwright ${browserName} binary missing`,
|
|
5315
|
-
detail:
|
|
5388
|
+
detail: `${fallback} (${err instanceof Error ? err.message : String(err)})`
|
|
5316
5389
|
};
|
|
5317
5390
|
}
|
|
5318
5391
|
}
|
|
5392
|
+
async function probeRootCache(browserName) {
|
|
5393
|
+
const candidates = [];
|
|
5394
|
+
const home = homedir();
|
|
5395
|
+
const p = osPlatform();
|
|
5396
|
+
if (p === "linux") {
|
|
5397
|
+
candidates.push("/root/.cache/ms-playwright");
|
|
5398
|
+
} else if (p === "darwin") {
|
|
5399
|
+
candidates.push("/var/root/Library/Caches/ms-playwright");
|
|
5400
|
+
} else if (p === "win32") {
|
|
5401
|
+
const systemRoot = process.env["SystemRoot"] ?? "C:\\Windows";
|
|
5402
|
+
candidates.push(join9(systemRoot, "System32", "config", "systemprofile", "AppData", "Local", "ms-playwright"));
|
|
5403
|
+
}
|
|
5404
|
+
if (candidates.length === 0) return null;
|
|
5405
|
+
if (home === "/root" || home === "/var/root") return null;
|
|
5406
|
+
for (const dir of candidates) {
|
|
5407
|
+
try {
|
|
5408
|
+
const entries = await readdir(dir);
|
|
5409
|
+
const match = entries.find((e) => e.toLowerCase().startsWith(browserName.toLowerCase()));
|
|
5410
|
+
if (match) {
|
|
5411
|
+
return `found at ${join9(dir, match)}`;
|
|
5412
|
+
}
|
|
5413
|
+
} catch {
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
return null;
|
|
5417
|
+
}
|
|
5319
5418
|
async function checkTargetReachable(url) {
|
|
5320
5419
|
try {
|
|
5321
5420
|
const start = Date.now();
|
|
@@ -5336,10 +5435,12 @@ async function checkTargetReachable(url) {
|
|
|
5336
5435
|
label: `target ${url} returned HTTP ${res.statusCode}`
|
|
5337
5436
|
};
|
|
5338
5437
|
} catch (err) {
|
|
5438
|
+
const reason = err instanceof Error ? err.message.trim() : String(err).trim();
|
|
5439
|
+
const prefix = reason ? `${reason} \u2014 ` : "";
|
|
5339
5440
|
return {
|
|
5340
5441
|
status: "FAIL",
|
|
5341
5442
|
label: `target ${url} not reachable`,
|
|
5342
|
-
detail:
|
|
5443
|
+
detail: `${prefix}start your dev server on this URL, or change \`recording.target_url\` in demo.yaml`
|
|
5343
5444
|
};
|
|
5344
5445
|
}
|
|
5345
5446
|
}
|
|
@@ -5347,7 +5448,11 @@ async function checkScript(label, abs) {
|
|
|
5347
5448
|
try {
|
|
5348
5449
|
await stat9(abs);
|
|
5349
5450
|
} catch {
|
|
5350
|
-
return {
|
|
5451
|
+
return {
|
|
5452
|
+
status: "FAIL",
|
|
5453
|
+
label: `${label} not found`,
|
|
5454
|
+
detail: `expected at ${abs} \u2014 re-scaffold (\`showrunner init\` writes these) or remove the entry from demo.yaml's recording.state block`
|
|
5455
|
+
};
|
|
5351
5456
|
}
|
|
5352
5457
|
if (process.platform !== "win32") {
|
|
5353
5458
|
try {
|
|
@@ -5402,6 +5507,13 @@ function formatResolution(r) {
|
|
|
5402
5507
|
}
|
|
5403
5508
|
|
|
5404
5509
|
// src/commands/run.ts
|
|
5510
|
+
var STAGE_REMEDIATION = {
|
|
5511
|
+
comprehension: "Check the LLM provider env (or switch llm.default.provider to agent_bridge in demo.yaml). If you already have a product_model.json, skip this stage: --stages script,record,voiceover,mux",
|
|
5512
|
+
script: "Manifest generation failed. Inspect scripts/manifest.json if it was partially written. If the LLM is misbehaving, hand-author scripts/manifest.json and resume: --stages record,voiceover,mux",
|
|
5513
|
+
record: "Playwright recording failed. Inspect with `showrunner trace -c <config> --all` or `--segment <id>`. Demo a problem segment manually with `showrunner record-actions -c <config> --segment <id>`.",
|
|
5514
|
+
voiceover: "TTS synthesis or alignment failed. Check your TTS provider env. If alignment is the issue, set voiceover.alignment_strategy: best_effort in demo.yaml.",
|
|
5515
|
+
mux: "ffmpeg mux failed. Check the `doctor` row about free disk + RAM. Cap libx264 threads with `SHOWRUNNER_FFMPEG_THREADS=1` if RAM is tight."
|
|
5516
|
+
};
|
|
5405
5517
|
async function runCommand2(opts) {
|
|
5406
5518
|
let loaded;
|
|
5407
5519
|
try {
|
|
@@ -5470,6 +5582,12 @@ async function runCommand2(opts) {
|
|
|
5470
5582
|
buildManifest: result.buildManifestPath
|
|
5471
5583
|
});
|
|
5472
5584
|
} catch (err) {
|
|
5585
|
+
if (err instanceof PipelineStageError) {
|
|
5586
|
+
logger.error(`Pipeline failed in stage \`${err.stage}\`: ${err.message}`);
|
|
5587
|
+
const remediation = STAGE_REMEDIATION[err.stage];
|
|
5588
|
+
if (remediation) logger.error(`To fix: ${remediation}`);
|
|
5589
|
+
process.exit(1);
|
|
5590
|
+
}
|
|
5473
5591
|
const message = err instanceof Error ? err.message : String(err);
|
|
5474
5592
|
logger.error(`Pipeline failed: ${message}`);
|
|
5475
5593
|
process.exit(1);
|
|
@@ -5478,7 +5596,7 @@ async function runCommand2(opts) {
|
|
|
5478
5596
|
|
|
5479
5597
|
// src/commands/init.ts
|
|
5480
5598
|
import { mkdir as mkdir11, writeFile as writeFile12, access as access2 } from "fs/promises";
|
|
5481
|
-
import { dirname as dirname10, isAbsolute as isAbsolute8, join as
|
|
5599
|
+
import { dirname as dirname10, isAbsolute as isAbsolute8, join as join10, resolve as resolve15 } from "path";
|
|
5482
5600
|
var LLM_CHOICES = ["anthropic", "openai", "agent_bridge"];
|
|
5483
5601
|
var TTS_CHOICES = ["elevenlabs", "openai", "custom"];
|
|
5484
5602
|
var PLACEHOLDER_FILES = [
|
|
@@ -5504,7 +5622,7 @@ async function initCommand(opts) {
|
|
|
5504
5622
|
resolution: resolvePreset(resolutionPreset)
|
|
5505
5623
|
};
|
|
5506
5624
|
const parent = isAbsolute8(opts.dir) ? opts.dir : resolve15(process.cwd(), opts.dir);
|
|
5507
|
-
const projectRoot =
|
|
5625
|
+
const projectRoot = join10(parent, opts.name);
|
|
5508
5626
|
if (!opts.force && await pathExists(projectRoot)) {
|
|
5509
5627
|
logger.error(
|
|
5510
5628
|
`Directory already exists: ${projectRoot}. Pass --force to overwrite, or choose a different --name/--dir.`
|
|
@@ -5513,34 +5631,59 @@ async function initCommand(opts) {
|
|
|
5513
5631
|
}
|
|
5514
5632
|
await mkdir11(projectRoot, { recursive: true });
|
|
5515
5633
|
for (const rel of PLACEHOLDER_FILES) {
|
|
5516
|
-
const dest =
|
|
5634
|
+
const dest = join10(projectRoot, rel);
|
|
5517
5635
|
await mkdir11(dirname10(dest), { recursive: true });
|
|
5518
5636
|
await writeFile12(dest, "", "utf8");
|
|
5519
5637
|
}
|
|
5520
|
-
await writeFile12(
|
|
5521
|
-
await writeFile12(
|
|
5522
|
-
await writeFile12(
|
|
5523
|
-
await mkdir11(
|
|
5524
|
-
await writeFile12(
|
|
5525
|
-
await writeFile12(
|
|
5526
|
-
await writeFile12(
|
|
5638
|
+
await writeFile12(join10(projectRoot, "demo.yaml"), demoYamlTemplate(resolved), "utf8");
|
|
5639
|
+
await writeFile12(join10(projectRoot, ".env.example"), envExampleTemplate(resolved), "utf8");
|
|
5640
|
+
await writeFile12(join10(projectRoot, ".gitignore"), gitignoreTemplate(), "utf8");
|
|
5641
|
+
await mkdir11(join10(projectRoot, "docs"), { recursive: true });
|
|
5642
|
+
await writeFile12(join10(projectRoot, "docs/PRD.md"), prdStubTemplate(resolved), "utf8");
|
|
5643
|
+
await writeFile12(join10(projectRoot, "scripts/manifest.json"), starterManifest(resolved), "utf8");
|
|
5644
|
+
await writeFile12(join10(projectRoot, "scripts/seed_demo_data.sh"), seedScript(), {
|
|
5527
5645
|
mode: 493
|
|
5528
5646
|
});
|
|
5529
|
-
await writeFile12(
|
|
5647
|
+
await writeFile12(join10(projectRoot, "scripts/reset_demo_data.sh"), resetScript(), {
|
|
5530
5648
|
mode: 493
|
|
5531
5649
|
});
|
|
5532
|
-
await writeFile12(
|
|
5650
|
+
await writeFile12(join10(projectRoot, "scripts/teardown.sh"), teardownScript(), {
|
|
5533
5651
|
mode: 493
|
|
5534
5652
|
});
|
|
5535
|
-
await writeFile12(
|
|
5653
|
+
await writeFile12(join10(projectRoot, "README.md"), readmeTemplate(resolved), "utf8");
|
|
5536
5654
|
logger.info(`Showrunner project scaffolded at ${projectRoot} (llm=${resolved.llm}, tts=${resolved.tts})`);
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5655
|
+
printNextSteps(opts.name, resolved);
|
|
5656
|
+
}
|
|
5657
|
+
function printNextSteps(projectName, resolved) {
|
|
5658
|
+
const envVars = requiredEnvVars(resolved);
|
|
5659
|
+
const lines = ["", `Next:`, ""];
|
|
5660
|
+
let step = 1;
|
|
5661
|
+
lines.push(` ${step++}. cd ${projectName}`);
|
|
5662
|
+
if (envVars.length > 0) {
|
|
5663
|
+
const keysList = envVars.join(", ");
|
|
5664
|
+
lines.push(` ${step++}. cp .env.example .env # then paste in: ${keysList}`);
|
|
5665
|
+
} else {
|
|
5666
|
+
lines.push(
|
|
5667
|
+
` ${step++}. (no .env needed \u2014 agent_bridge LLM + ${resolved.tts} TTS don't require API keys)`
|
|
5668
|
+
);
|
|
5669
|
+
}
|
|
5670
|
+
lines.push(` ${step++}. $EDITOR docs/PRD.md # replace the stub with your product brief`);
|
|
5671
|
+
lines.push(` ${step++}. showrunner doctor -c demo.yaml`);
|
|
5672
|
+
lines.push(` ${step++}. showrunner run -c demo.yaml # \u2192 output/demo_final.mp4`);
|
|
5673
|
+
lines.push("");
|
|
5674
|
+
lines.push(
|
|
5675
|
+
`Optional: \`showrunner understand -c demo.yaml --interactive\` if you'd rather answer five questions than write the PRD upfront.`
|
|
5676
|
+
);
|
|
5677
|
+
lines.push("");
|
|
5678
|
+
process.stdout.write(lines.join("\n"));
|
|
5679
|
+
}
|
|
5680
|
+
function requiredEnvVars(resolved) {
|
|
5681
|
+
const vars = /* @__PURE__ */ new Set();
|
|
5682
|
+
if (resolved.llm === "anthropic") vars.add("ANTHROPIC_API_KEY");
|
|
5683
|
+
if (resolved.llm === "openai") vars.add("OPENAI_API_KEY");
|
|
5684
|
+
if (resolved.tts === "elevenlabs") vars.add("ELEVENLABS_API_KEY");
|
|
5685
|
+
if (resolved.tts === "openai") vars.add("OPENAI_API_KEY");
|
|
5686
|
+
return [...vars];
|
|
5544
5687
|
}
|
|
5545
5688
|
function validateChoice(value, allowed, defaultValue, flagName) {
|
|
5546
5689
|
if (!value) return defaultValue;
|
|
@@ -6123,8 +6266,8 @@ async function pathExists2(p) {
|
|
|
6123
6266
|
|
|
6124
6267
|
// src/commands/rerunSegment.ts
|
|
6125
6268
|
import { mkdir as mkdir12, rename as rename3, writeFile as writeFile13 } from "fs/promises";
|
|
6126
|
-
import { join as
|
|
6127
|
-
import { chromium as chromium4, firefox as firefox4, webkit as webkit4 } from "playwright";
|
|
6269
|
+
import { join as join11, resolve as resolve19 } from "path";
|
|
6270
|
+
import { chromium as chromium4, firefox as firefox4, webkit as webkit4 } from "playwright-core";
|
|
6128
6271
|
var RERUN_BUFFER_MS = 500;
|
|
6129
6272
|
var browserMap4 = { chromium: chromium4, firefox: firefox4, webkit: webkit4 };
|
|
6130
6273
|
async function rerunSegmentCommand(opts) {
|
|
@@ -6242,17 +6385,17 @@ async function rerunSegmentCommand(opts) {
|
|
|
6242
6385
|
await page.waitForTimeout(config.recording.segment_buffer_ms);
|
|
6243
6386
|
const traceDir = resolve19(configDir, config.recording.trace_dir);
|
|
6244
6387
|
await mkdir12(traceDir, { recursive: true });
|
|
6245
|
-
await ctx.tracing.stopChunk({ path:
|
|
6388
|
+
await ctx.tracing.stopChunk({ path: join11(traceDir, `${segment.id}.zip`) });
|
|
6246
6389
|
const videoHandle = page.video();
|
|
6247
6390
|
await ctx.close();
|
|
6248
6391
|
if (!videoHandle) {
|
|
6249
|
-
logger.error("No video captured \
|
|
6392
|
+
logger.error("No video captured \xE2\u20AC\u201D recordVideo may have been ignored");
|
|
6250
6393
|
process.exit(1);
|
|
6251
6394
|
}
|
|
6252
6395
|
const original = await videoHandle.path();
|
|
6253
|
-
const dest =
|
|
6396
|
+
const dest = join11(videoDir, `${segment.id}.webm`);
|
|
6254
6397
|
await rename3(original, dest);
|
|
6255
|
-
const metadataPath =
|
|
6398
|
+
const metadataPath = join11(videoDir, `${segment.id}.rerun.json`);
|
|
6256
6399
|
await writeFile13(
|
|
6257
6400
|
metadataPath,
|
|
6258
6401
|
JSON.stringify(
|
|
@@ -6270,7 +6413,7 @@ async function rerunSegmentCommand(opts) {
|
|
|
6270
6413
|
) + "\n",
|
|
6271
6414
|
"utf8"
|
|
6272
6415
|
);
|
|
6273
|
-
logger.info(`Segment ${segment.id} re-recorded \
|
|
6416
|
+
logger.info(`Segment ${segment.id} re-recorded \xE2\u2020\u2019 ${dest}`, { metadata: metadataPath });
|
|
6274
6417
|
if (failure) {
|
|
6275
6418
|
process.exit(1);
|
|
6276
6419
|
}
|
|
@@ -6291,7 +6434,7 @@ import {
|
|
|
6291
6434
|
chromium as chromium5,
|
|
6292
6435
|
firefox as firefox5,
|
|
6293
6436
|
webkit as webkit5
|
|
6294
|
-
} from "playwright";
|
|
6437
|
+
} from "playwright-core";
|
|
6295
6438
|
var browserMap5 = { chromium: chromium5, firefox: firefox5, webkit: webkit5 };
|
|
6296
6439
|
async function launchHeadedSession(opts) {
|
|
6297
6440
|
const { recording, configDir } = opts;
|
|
@@ -6408,8 +6551,8 @@ Then re-run \`showrunner run --config <demo.yaml>\`.
|
|
|
6408
6551
|
}
|
|
6409
6552
|
|
|
6410
6553
|
// src/commands/trace.ts
|
|
6411
|
-
import { readdir, stat as stat12 } from "fs/promises";
|
|
6412
|
-
import { isAbsolute as isAbsolute11, join as
|
|
6554
|
+
import { readdir as readdir2, stat as stat12 } from "fs/promises";
|
|
6555
|
+
import { isAbsolute as isAbsolute11, join as join12, resolve as resolve22 } from "path";
|
|
6413
6556
|
async function traceCommand(opts) {
|
|
6414
6557
|
let loaded;
|
|
6415
6558
|
try {
|
|
@@ -6423,7 +6566,7 @@ async function traceCommand(opts) {
|
|
|
6423
6566
|
}
|
|
6424
6567
|
const traceDir = resolve22(loaded.configDir, loaded.config.recording.trace_dir);
|
|
6425
6568
|
const videoDir = resolve22(loaded.configDir, loaded.config.recording.output_dir);
|
|
6426
|
-
const slicePlanPath =
|
|
6569
|
+
const slicePlanPath = join12(videoDir, "slice_plan.json");
|
|
6427
6570
|
if (opts.all) {
|
|
6428
6571
|
let plan;
|
|
6429
6572
|
try {
|
|
@@ -6434,14 +6577,14 @@ async function traceCommand(opts) {
|
|
|
6434
6577
|
process.exit(1);
|
|
6435
6578
|
}
|
|
6436
6579
|
for (const seg of plan.segments) {
|
|
6437
|
-
const tracePath = seg.trace_path && isAbsolute11(seg.trace_path) ? seg.trace_path : seg.trace_path ? resolve22(loaded.configDir, seg.trace_path) :
|
|
6580
|
+
const tracePath = seg.trace_path && isAbsolute11(seg.trace_path) ? seg.trace_path : seg.trace_path ? resolve22(loaded.configDir, seg.trace_path) : join12(traceDir, `${seg.id}.zip`);
|
|
6438
6581
|
logger.info(`Opening trace for ${seg.id}`, { path: tracePath });
|
|
6439
6582
|
await openTrace(tracePath);
|
|
6440
6583
|
}
|
|
6441
6584
|
return;
|
|
6442
6585
|
}
|
|
6443
6586
|
if (opts.segment) {
|
|
6444
|
-
const tracePath =
|
|
6587
|
+
const tracePath = join12(traceDir, `${opts.segment}.zip`);
|
|
6445
6588
|
if (!await fileExists8(tracePath)) {
|
|
6446
6589
|
logger.error(`No trace found at ${tracePath}`);
|
|
6447
6590
|
process.exit(1);
|
|
@@ -6482,7 +6625,7 @@ async function openTrace(tracePath) {
|
|
|
6482
6625
|
}
|
|
6483
6626
|
async function listTraces(dir) {
|
|
6484
6627
|
try {
|
|
6485
|
-
const entries = await
|
|
6628
|
+
const entries = await readdir2(dir);
|
|
6486
6629
|
return entries.filter((e) => e.endsWith(".zip")).sort();
|
|
6487
6630
|
} catch {
|
|
6488
6631
|
return [];
|
|
@@ -6798,8 +6941,8 @@ async function understandCommand(opts) {
|
|
|
6798
6941
|
}
|
|
6799
6942
|
|
|
6800
6943
|
// src/commands/instrument.ts
|
|
6801
|
-
import { mkdir as mkdir16, readdir as
|
|
6802
|
-
import { dirname as dirname13, isAbsolute as isAbsolute13, join as
|
|
6944
|
+
import { mkdir as mkdir16, readdir as readdir3, writeFile as writeFile15 } from "fs/promises";
|
|
6945
|
+
import { dirname as dirname13, isAbsolute as isAbsolute13, join as join13, resolve as resolve25, relative as relative2 } from "path";
|
|
6803
6946
|
|
|
6804
6947
|
// src/instrument/scan.ts
|
|
6805
6948
|
import { readFile as readFile11 } from "fs/promises";
|
|
@@ -7022,9 +7165,9 @@ async function instrumentCommand(opts) {
|
|
|
7022
7165
|
const filesToScan = /* @__PURE__ */ new Set();
|
|
7023
7166
|
const IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
7024
7167
|
async function walk(root, accept) {
|
|
7025
|
-
const entries = await
|
|
7168
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
7026
7169
|
for (const entry of entries) {
|
|
7027
|
-
const abs =
|
|
7170
|
+
const abs = join13(root, entry.name);
|
|
7028
7171
|
if (entry.isDirectory()) {
|
|
7029
7172
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
7030
7173
|
await walk(abs, accept);
|
|
@@ -7163,7 +7306,7 @@ var IN_PAGE_SCRIPT = `
|
|
|
7163
7306
|
send('submit', el, null);
|
|
7164
7307
|
}, true);
|
|
7165
7308
|
|
|
7166
|
-
// Scroll capture \
|
|
7309
|
+
// Scroll capture \xE2\u20AC\u201D debounce 250ms, coalesce within window, emit direction + accumulated deltaY.
|
|
7167
7310
|
let scrollLastY = window.scrollY;
|
|
7168
7311
|
let scrollAccum = 0;
|
|
7169
7312
|
let scrollTimer = null;
|
|
@@ -7186,7 +7329,7 @@ var IN_PAGE_SCRIPT = `
|
|
|
7186
7329
|
const delta = currentY - scrollLastY;
|
|
7187
7330
|
scrollLastY = currentY;
|
|
7188
7331
|
if (delta === 0) return;
|
|
7189
|
-
// Direction change \
|
|
7332
|
+
// Direction change \xE2\u2020\u2019 flush the previous burst immediately.
|
|
7190
7333
|
if (scrollAccum !== 0 && Math.sign(delta) !== Math.sign(scrollAccum)) {
|
|
7191
7334
|
if (scrollTimer !== null) { clearTimeout(scrollTimer); scrollTimer = null; }
|
|
7192
7335
|
flushScroll();
|
|
@@ -7416,10 +7559,12 @@ async function fileExists10(path) {
|
|
|
7416
7559
|
|
|
7417
7560
|
// src/cli.ts
|
|
7418
7561
|
var program = new Command();
|
|
7419
|
-
program.name("showrunner").description("Automated product demo recording & production tool").version("1.1.
|
|
7562
|
+
program.name("showrunner").description("Automated product demo recording & production tool").version("1.1.1").option("--json", "emit structured JSON logs to stdout").option("--log-level <level>", "log level (debug|info|warn|error)").hook("preAction", (thisCmd) => {
|
|
7420
7563
|
const opts = thisCmd.opts();
|
|
7421
7564
|
if (opts.json) logger.setJson(true);
|
|
7422
7565
|
if (opts.logLevel) logger.setLevel(opts.logLevel);
|
|
7566
|
+
}).action(async () => {
|
|
7567
|
+
await printWelcome();
|
|
7423
7568
|
});
|
|
7424
7569
|
var stageChoices = [...STAGE_NAMES];
|
|
7425
7570
|
program.command("run").description("Run the full Showrunner pipeline").requiredOption("-c, --config <path>", "path to demo.yaml").addOption(
|
|
@@ -7463,6 +7608,42 @@ program.parseAsync(process.argv).catch((err) => {
|
|
|
7463
7608
|
logger.error(message);
|
|
7464
7609
|
process.exit(1);
|
|
7465
7610
|
});
|
|
7611
|
+
async function printWelcome() {
|
|
7612
|
+
const { access: access3 } = await import("fs/promises");
|
|
7613
|
+
const { resolve: resolve27 } = await import("path");
|
|
7614
|
+
const demoYaml = resolve27(process.cwd(), "demo.yaml");
|
|
7615
|
+
let inProject = false;
|
|
7616
|
+
try {
|
|
7617
|
+
await access3(demoYaml);
|
|
7618
|
+
inProject = true;
|
|
7619
|
+
} catch {
|
|
7620
|
+
}
|
|
7621
|
+
const lines = ["", `Showrunner v1.1.1 \u2014 automated product-demo recording & production`, ""];
|
|
7622
|
+
if (inProject) {
|
|
7623
|
+
lines.push(`Detected demo.yaml in this directory. Likely next:`);
|
|
7624
|
+
lines.push(``);
|
|
7625
|
+
lines.push(` showrunner doctor -c demo.yaml # preflight checks`);
|
|
7626
|
+
lines.push(` showrunner run -c demo.yaml # run the full pipeline`);
|
|
7627
|
+
lines.push(``);
|
|
7628
|
+
lines.push(`Other commands: \`showrunner --help\``);
|
|
7629
|
+
} else {
|
|
7630
|
+
lines.push(`No demo.yaml here. To scaffold a new project:`);
|
|
7631
|
+
lines.push(``);
|
|
7632
|
+
lines.push(` showrunner init --name my-demo --url http://localhost:3000`);
|
|
7633
|
+
lines.push(``);
|
|
7634
|
+
lines.push(`Then inside the new directory:`);
|
|
7635
|
+
lines.push(``);
|
|
7636
|
+
lines.push(` cd my-demo`);
|
|
7637
|
+
lines.push(` cp .env.example .env # paste provider keys, or use agent_bridge (no keys)`);
|
|
7638
|
+
lines.push(` $EDITOR docs/PRD.md # write your product brief`);
|
|
7639
|
+
lines.push(` showrunner doctor -c demo.yaml # preflight`);
|
|
7640
|
+
lines.push(` showrunner run -c demo.yaml # full pipeline \u2192 output/demo_final.mp4`);
|
|
7641
|
+
lines.push(``);
|
|
7642
|
+
lines.push(`See all commands: \`showrunner --help\``);
|
|
7643
|
+
}
|
|
7644
|
+
lines.push("");
|
|
7645
|
+
process.stdout.write(lines.join("\n"));
|
|
7646
|
+
}
|
|
7466
7647
|
function parseStages(value) {
|
|
7467
7648
|
const requested = value.split(",").map((s) => s.trim());
|
|
7468
7649
|
const invalid = requested.filter((s) => !stageChoices.includes(s));
|