@kadj-amoah/showrunner 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1413,10 +1413,10 @@ function extractAndValidate(response, schema) {
1413
1413
  if (response.parsed_output !== void 0 && response.parsed_output !== null) {
1414
1414
  return schema.parse(response.parsed_output);
1415
1415
  }
1416
- const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
1416
+ const text2 = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
1417
1417
  throw new LLMProviderError(
1418
- text.length > 0 ? `Anthropic returned no parseable structured output. Raw text:
1419
- ${text.slice(0, 2e3)}` : "Anthropic returned an empty response with no parseable output.",
1418
+ text2.length > 0 ? `Anthropic returned no parseable structured output. Raw text:
1419
+ ${text2.slice(0, 2e3)}` : "Anthropic returned an empty response with no parseable output.",
1420
1420
  "anthropic"
1421
1421
  );
1422
1422
  }
@@ -1824,7 +1824,7 @@ async function fileExists2(p) {
1824
1824
  }
1825
1825
  }
1826
1826
  function sleep(ms) {
1827
- return new Promise((resolve27) => setTimeout(resolve27, ms));
1827
+ return new Promise((resolve28) => setTimeout(resolve28, ms));
1828
1828
  }
1829
1829
 
1830
1830
  // src/providers/llm/custom.ts
@@ -2351,20 +2351,20 @@ function isOriginsList(value) {
2351
2351
  function buildFormFill(auth) {
2352
2352
  return async (page) => {
2353
2353
  const email = process.env[auth.fields.email.env];
2354
- const password = process.env[auth.fields.password.env];
2354
+ const password2 = process.env[auth.fields.password.env];
2355
2355
  if (!email) {
2356
2356
  throw new AuthError(
2357
2357
  `Form auth: env var ${auth.fields.email.env} is not set`
2358
2358
  );
2359
2359
  }
2360
- if (!password) {
2360
+ if (!password2) {
2361
2361
  throw new AuthError(
2362
2362
  `Form auth: env var ${auth.fields.password.env} is not set`
2363
2363
  );
2364
2364
  }
2365
2365
  await page.goto(auth.login_url);
2366
2366
  await page.fill(auth.fields.email.selector, email);
2367
- await page.fill(auth.fields.password.selector, password);
2367
+ await page.fill(auth.fields.password.selector, password2);
2368
2368
  await page.click(auth.submit_selector);
2369
2369
  try {
2370
2370
  await page.waitForURL(auth.success_url_pattern, { timeout: auth.timeout_ms });
@@ -2719,9 +2719,9 @@ async function recordDemo(opts) {
2719
2719
  const page = await context.newPage();
2720
2720
  if (recording.cursor_highlight) {
2721
2721
  page.on("console", (msg) => {
2722
- const text = msg.text();
2723
- if (text.startsWith("[showrunner-cursor]")) {
2724
- logger.debug(`browser: ${text}`);
2722
+ const text2 = msg.text();
2723
+ if (text2.startsWith("[showrunner-cursor]")) {
2724
+ logger.debug(`browser: ${text2}`);
2725
2725
  }
2726
2726
  });
2727
2727
  }
@@ -4602,8 +4602,8 @@ async function fileExists7(path) {
4602
4602
  return false;
4603
4603
  }
4604
4604
  }
4605
- function escapeFilterText(text) {
4606
- return text.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/:/g, "\\:").replace(/,/g, "\\,");
4605
+ function escapeFilterText(text2) {
4606
+ return text2.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/:/g, "\\:").replace(/,/g, "\\,");
4607
4607
  }
4608
4608
  function escapeFilterPath(path) {
4609
4609
  return path.replace(/\\/g, "/").replace(/:/g, "\\\\:");
@@ -4680,14 +4680,14 @@ function chunkAlignmentToCues(alignment, segmentStartSec) {
4680
4680
  let lastBoundaryIdx = -1;
4681
4681
  let i = 0;
4682
4682
  const pushCue = (endIdx) => {
4683
- const text = cueChars.join("").replace(/\s+/g, " ").trim();
4684
- if (text.length === 0) return;
4683
+ const text2 = cueChars.join("").replace(/\s+/g, " ").trim();
4684
+ if (text2.length === 0) return;
4685
4685
  cues.push({
4686
4686
  index: 0,
4687
4687
  // assigned by caller
4688
4688
  startSec: segmentStartSec + cueStart,
4689
4689
  endSec: segmentStartSec + ends[endIdx],
4690
- text
4690
+ text: text2
4691
4691
  });
4692
4692
  };
4693
4693
  while (i < chars.length) {
@@ -5317,14 +5317,14 @@ function printRow(r) {
5317
5317
  else logger.info(line);
5318
5318
  }
5319
5319
  async function checkBinary(name, args) {
5320
- return new Promise((resolve27) => {
5320
+ return new Promise((resolve28) => {
5321
5321
  const child = spawn3(name, args, { stdio: ["ignore", "pipe", "pipe"] });
5322
5322
  let stdout = "";
5323
5323
  child.stdout?.on("data", (chunk) => {
5324
5324
  stdout += chunk.toString("utf8");
5325
5325
  });
5326
5326
  child.on("error", () => {
5327
- resolve27({
5327
+ resolve28({
5328
5328
  status: "FAIL",
5329
5329
  label: `${name} not on PATH`,
5330
5330
  detail: installHintFor(name)
@@ -5333,9 +5333,9 @@ async function checkBinary(name, args) {
5333
5333
  child.on("exit", (code) => {
5334
5334
  if (code === 0) {
5335
5335
  const firstLine = stdout.split("\n")[0]?.trim() ?? "";
5336
- resolve27({ status: "PASS", label: `${name} present`, detail: firstLine });
5336
+ resolve28({ status: "PASS", label: `${name} present`, detail: firstLine });
5337
5337
  } else {
5338
- resolve27({
5338
+ resolve28({
5339
5339
  status: "FAIL",
5340
5340
  label: `${name} exited with code ${code}`,
5341
5341
  detail: installHintFor(name)
@@ -5595,8 +5595,654 @@ async function runCommand2(opts) {
5595
5595
  }
5596
5596
 
5597
5597
  // src/commands/init.ts
5598
- import { mkdir as mkdir11, writeFile as writeFile12, access as access2 } from "fs/promises";
5599
- import { dirname as dirname10, isAbsolute as isAbsolute8, join as join10, resolve as resolve15 } from "path";
5598
+ import { mkdir as mkdir11, writeFile as writeFile12, access as access3 } from "fs/promises";
5599
+ import { dirname as dirname10, isAbsolute as isAbsolute8, join as join11, resolve as resolve15 } from "path";
5600
+
5601
+ // src/setup/detect.ts
5602
+ import { spawn as spawn4 } from "child_process";
5603
+ import { access as access2, stat as stat10 } from "fs/promises";
5604
+ async function detectEnvironment() {
5605
+ const [claudeCli, ffmpeg, ffprobe, chromium6] = await Promise.all([
5606
+ isOnPath("claude"),
5607
+ isOnPath("ffmpeg"),
5608
+ isOnPath("ffprobe"),
5609
+ chromiumInstalled()
5610
+ ]);
5611
+ return {
5612
+ claudeCli,
5613
+ ffmpeg,
5614
+ ffprobe,
5615
+ chromium: chromium6,
5616
+ envVars: {
5617
+ anthropic: Boolean(process.env["ANTHROPIC_API_KEY"]),
5618
+ openai: Boolean(process.env["OPENAI_API_KEY"]),
5619
+ elevenlabs: Boolean(process.env["ELEVENLABS_API_KEY"])
5620
+ }
5621
+ };
5622
+ }
5623
+ async function isOnPath(binary) {
5624
+ return new Promise((resolve28) => {
5625
+ const useShell = process.platform === "win32";
5626
+ const child = spawn4(binary, ["--version"], {
5627
+ stdio: ["ignore", "ignore", "ignore"],
5628
+ shell: useShell
5629
+ });
5630
+ let settled = false;
5631
+ const finish = (ok) => {
5632
+ if (settled) return;
5633
+ settled = true;
5634
+ resolve28(ok);
5635
+ };
5636
+ child.on("error", () => finish(false));
5637
+ child.on("exit", (code) => finish(code === 0));
5638
+ setTimeout(() => {
5639
+ if (!settled) {
5640
+ child.kill("SIGKILL");
5641
+ finish(false);
5642
+ }
5643
+ }, 4e3);
5644
+ });
5645
+ }
5646
+ async function chromiumInstalled() {
5647
+ try {
5648
+ const { chromium: chromium6 } = await import("playwright-core");
5649
+ const exec = chromium6.executablePath();
5650
+ await stat10(exec);
5651
+ return true;
5652
+ } catch {
5653
+ return false;
5654
+ }
5655
+ }
5656
+
5657
+ // src/setup/wizard.ts
5658
+ import {
5659
+ intro,
5660
+ outro,
5661
+ text,
5662
+ password,
5663
+ select,
5664
+ confirm,
5665
+ note,
5666
+ spinner,
5667
+ isCancel,
5668
+ cancel
5669
+ } from "@clack/prompts";
5670
+
5671
+ // src/setup/targetProbe.ts
5672
+ import { request as undiciRequest2 } from "undici";
5673
+ var DEFAULT_TIMEOUT_MS = 4e3;
5674
+ async function probeUrl(url, timeoutMs = DEFAULT_TIMEOUT_MS) {
5675
+ const start = Date.now();
5676
+ try {
5677
+ const res = await undiciRequest2(url, {
5678
+ method: "HEAD",
5679
+ bodyTimeout: timeoutMs,
5680
+ headersTimeout: timeoutMs
5681
+ });
5682
+ const elapsedMs = Date.now() - start;
5683
+ return {
5684
+ url,
5685
+ reachable: res.statusCode >= 200 && res.statusCode < 500,
5686
+ statusCode: res.statusCode,
5687
+ elapsedMs
5688
+ };
5689
+ } catch (err) {
5690
+ return {
5691
+ url,
5692
+ reachable: false,
5693
+ reason: err instanceof Error ? err.message : String(err),
5694
+ elapsedMs: Date.now() - start
5695
+ };
5696
+ }
5697
+ }
5698
+
5699
+ // src/setup/portScan.ts
5700
+ var COMMON_DEV_PORTS = [3e3, 3001, 4321, 5173, 5174, 8e3, 8080];
5701
+ async function scanLocalPorts(host = "localhost", ports = COMMON_DEV_PORTS, timeoutMs = 800) {
5702
+ const probes = ports.map(async (port) => {
5703
+ const url = `http://${host}:${port}`;
5704
+ const result = await probeUrl(url, timeoutMs);
5705
+ if (!result.reachable) return null;
5706
+ return {
5707
+ url,
5708
+ port,
5709
+ statusCode: result.statusCode ?? 0,
5710
+ elapsedMs: result.elapsedMs ?? 0
5711
+ };
5712
+ });
5713
+ const all = await Promise.all(probes);
5714
+ return all.filter((x) => x !== null).sort((a, b) => a.port - b.port);
5715
+ }
5716
+
5717
+ // src/setup/agentDiscover.ts
5718
+ import { z as z9 } from "zod";
5719
+
5720
+ // src/setup/projectSnapshot.ts
5721
+ import { readFile as readFile10, readdir as readdir2, stat as stat11 } from "fs/promises";
5722
+ import { join as join10, relative } from "path";
5723
+ var FILE_CAP_BYTES = 2048;
5724
+ var README_LINE_CAP = 100;
5725
+ var CONFIG_FILE_PATTERNS = [
5726
+ /^vite\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5727
+ /^next\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5728
+ /^astro\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5729
+ /^nuxt\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5730
+ /^svelte\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5731
+ /^vue\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5732
+ /^webpack\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5733
+ /^remix\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5734
+ /^gatsby-config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5735
+ /^rsbuild\.config\.(?:js|mjs|cjs|ts|mts|cts)$/,
5736
+ /^manage\.py$/,
5737
+ /^pyproject\.toml$/,
5738
+ /^Cargo\.toml$/
5739
+ ];
5740
+ async function collectProjectSnapshot(projectDir) {
5741
+ const sections = [];
5742
+ const pkg = await readFileSafe(join10(projectDir, "package.json"));
5743
+ if (pkg) sections.push({ label: "package.json", path: "package.json", body: pkg });
5744
+ for (const candidate of ["README.md", "readme.md", "README.MD"]) {
5745
+ const r = await readFileSafe(join10(projectDir, candidate), { lineCap: README_LINE_CAP });
5746
+ if (r) {
5747
+ sections.push({ label: "README (head)", path: candidate, body: r });
5748
+ break;
5749
+ }
5750
+ }
5751
+ const envEx = await readFileSafe(join10(projectDir, ".env.example"));
5752
+ if (envEx) sections.push({ label: ".env.example", path: ".env.example", body: envEx });
5753
+ else {
5754
+ const envSample = await readFileSafe(join10(projectDir, ".env.sample"));
5755
+ if (envSample) sections.push({ label: ".env.sample", path: ".env.sample", body: envSample });
5756
+ }
5757
+ let topEntries;
5758
+ try {
5759
+ topEntries = await readdir2(projectDir);
5760
+ } catch {
5761
+ topEntries = [];
5762
+ }
5763
+ for (const entry of topEntries) {
5764
+ if (CONFIG_FILE_PATTERNS.some((re) => re.test(entry))) {
5765
+ const body = await readFileSafe(join10(projectDir, entry));
5766
+ if (body) sections.push({ label: entry, path: entry, body });
5767
+ }
5768
+ }
5769
+ return { projectDir, sections };
5770
+ }
5771
+ function renderSnapshot(snap) {
5772
+ if (snap.sections.length === 0) {
5773
+ return `(empty or non-readable project at ${snap.projectDir})`;
5774
+ }
5775
+ const out = [];
5776
+ for (const s of snap.sections) {
5777
+ out.push(`### ${s.label} \u2014 ${s.path}`);
5778
+ out.push("```");
5779
+ out.push(s.body.trim());
5780
+ out.push("```");
5781
+ out.push("");
5782
+ }
5783
+ return out.join("\n");
5784
+ }
5785
+ async function readFileSafe(path, options = {}) {
5786
+ try {
5787
+ const s = await stat11(path);
5788
+ if (!s.isFile()) return null;
5789
+ } catch {
5790
+ return null;
5791
+ }
5792
+ let text2;
5793
+ try {
5794
+ text2 = await readFile10(path, "utf8");
5795
+ } catch {
5796
+ return null;
5797
+ }
5798
+ if (options.lineCap !== void 0) {
5799
+ const lines = text2.split("\n");
5800
+ if (lines.length > options.lineCap) {
5801
+ text2 = lines.slice(0, options.lineCap).join("\n") + `
5802
+ \u2026[truncated at ${options.lineCap} lines]`;
5803
+ }
5804
+ }
5805
+ const byteCap = options.byteCap ?? FILE_CAP_BYTES;
5806
+ if (Buffer.byteLength(text2, "utf8") > byteCap) {
5807
+ text2 = text2.slice(0, byteCap) + `
5808
+ \u2026[truncated at ${byteCap} bytes]`;
5809
+ }
5810
+ return text2;
5811
+ }
5812
+
5813
+ // src/setup/agentDiscover.ts
5814
+ var DevServerProposalSchema = z9.object({
5815
+ command: z9.string().min(1).describe('Executable name, e.g. "npm" or "pnpm".'),
5816
+ args: z9.array(z9.string()).describe('CLI arguments, e.g. ["run", "dev"].'),
5817
+ url: z9.string().url().describe("URL the dev server will listen on once started."),
5818
+ confidence: z9.enum(["high", "medium", "low"]),
5819
+ rationale: z9.string().min(1).describe("One short sentence on why this is the right command.")
5820
+ });
5821
+ var SYSTEM_PROMPT = [
5822
+ "You are helping the Showrunner CLI identify how to start a project's development server.",
5823
+ "You will be given a project snapshot (package.json, README head, common framework config files, .env.example).",
5824
+ "",
5825
+ "Your job: identify the command + arguments to run the dev server, and the URL it will listen on.",
5826
+ "",
5827
+ "Rules:",
5828
+ '- Use the scripts in package.json as your primary signal. Prefer "dev" > "start" > "serve" when present.',
5829
+ "- For Vite default port = 5173; Next/CRA = 3000; Astro = 4321; Vue CLI = 8080; Django = 8000.",
5830
+ "- If the project explicitly sets a port (via env, config, or CLI flags), use that, NOT the framework default.",
5831
+ '- If the snapshot is too sparse to tell, set confidence to "low" and use your best guess for both command and URL.',
5832
+ '- "confidence" reflects how sure you are. "high" = explicit script + config-confirmed port; "medium" = explicit script, default port; "low" = guessing.',
5833
+ '- "rationale" is one short sentence, plain English. No marketing speak.',
5834
+ "",
5835
+ "Return only the structured output. No prose outside the JSON."
5836
+ ].join("\n");
5837
+ async function discoverDevServer(projectDir, opts = {}) {
5838
+ const snapshot = await collectProjectSnapshot(projectDir);
5839
+ const rendered = renderSnapshot(snapshot);
5840
+ const provider = new AgentBridgeLLMProvider({
5841
+ mode: "spawn",
5842
+ command: opts.command ?? "claude",
5843
+ args: opts.args ?? ["-p", "--output-format", "json"],
5844
+ timeoutMs: opts.timeoutMs ?? 9e4
5845
+ });
5846
+ const userPrompt = [
5847
+ `Project directory: ${projectDir}`,
5848
+ "",
5849
+ "Project snapshot follows:",
5850
+ "",
5851
+ rendered,
5852
+ "",
5853
+ "Identify the dev-server command + args + URL."
5854
+ ].join("\n");
5855
+ return await provider.generateStructured({
5856
+ systemPrompt: SYSTEM_PROMPT,
5857
+ userPrompt,
5858
+ schema: DevServerProposalSchema,
5859
+ schemaName: "DevServerProposal"
5860
+ });
5861
+ }
5862
+
5863
+ // src/setup/spawnDevServer.ts
5864
+ import { spawn as spawn5 } from "child_process";
5865
+ async function spawnAndWait(opts) {
5866
+ const waitTimeoutMs = opts.waitTimeoutMs ?? 6e4;
5867
+ const pollIntervalMs = opts.pollIntervalMs ?? 750;
5868
+ const useShell = process.platform === "win32";
5869
+ let child;
5870
+ try {
5871
+ child = spawn5(opts.command, opts.args, {
5872
+ cwd: opts.cwd,
5873
+ detached: true,
5874
+ stdio: "ignore",
5875
+ shell: useShell
5876
+ });
5877
+ child.unref();
5878
+ } catch (err) {
5879
+ return {
5880
+ ok: false,
5881
+ command: opts.command,
5882
+ args: opts.args,
5883
+ url: opts.url,
5884
+ reason: `failed to spawn: ${err instanceof Error ? err.message : String(err)}`
5885
+ };
5886
+ }
5887
+ const pid = child.pid;
5888
+ const start = Date.now();
5889
+ let lastReason = "";
5890
+ while (Date.now() - start < waitTimeoutMs) {
5891
+ if (child.exitCode !== null) {
5892
+ return {
5893
+ ok: false,
5894
+ pid,
5895
+ command: opts.command,
5896
+ args: opts.args,
5897
+ url: opts.url,
5898
+ reason: `child process exited with code ${child.exitCode} before the URL came up`
5899
+ };
5900
+ }
5901
+ const probe = await probeUrl(opts.url, pollIntervalMs);
5902
+ if (probe.reachable) {
5903
+ return {
5904
+ ok: true,
5905
+ pid,
5906
+ command: opts.command,
5907
+ args: opts.args,
5908
+ url: opts.url
5909
+ };
5910
+ }
5911
+ lastReason = probe.reason ?? `HTTP ${probe.statusCode ?? "?"}`;
5912
+ await sleep3(pollIntervalMs);
5913
+ }
5914
+ return {
5915
+ ok: false,
5916
+ pid,
5917
+ command: opts.command,
5918
+ args: opts.args,
5919
+ url: opts.url,
5920
+ reason: `timed out after ${waitTimeoutMs}ms waiting for ${opts.url} (last: ${lastReason}). Child still running as pid ${pid}.`
5921
+ };
5922
+ }
5923
+ function sleep3(ms) {
5924
+ return new Promise((r) => setTimeout(r, ms));
5925
+ }
5926
+
5927
+ // src/setup/wizard.ts
5928
+ var SLUG_RE = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/i;
5929
+ async function runWizard(env) {
5930
+ intro("Showrunner setup");
5931
+ note(formatDetection(env), "Detected on this machine");
5932
+ const projectName = await ask(
5933
+ text({
5934
+ message: "What's the project directory name?",
5935
+ placeholder: "showrunner-demo",
5936
+ defaultValue: "showrunner-demo",
5937
+ validate: (v) => {
5938
+ if (!v) return void 0;
5939
+ if (!SLUG_RE.test(v)) {
5940
+ return "Use letters/digits/dash/underscore/dot, starting & ending with alphanumeric.";
5941
+ }
5942
+ return void 0;
5943
+ }
5944
+ })
5945
+ );
5946
+ if (projectName === null) return null;
5947
+ const resolutionPreset = await ask(
5948
+ select({
5949
+ message: "Recording resolution preset?",
5950
+ options: [
5951
+ { value: "low", label: "low (854\xD7480) \u2014 draft / fast iteration" },
5952
+ { value: "standard", label: "standard (1280\xD7720) \u2014 recommended default" },
5953
+ { value: "high", label: "high (1920\xD71080)" },
5954
+ { value: "extreme", label: "extreme (3840\xD72160) \u2014 needs serious RAM" }
5955
+ ],
5956
+ initialValue: "standard"
5957
+ })
5958
+ );
5959
+ if (resolutionPreset === null) return null;
5960
+ if (!RESOLUTION_PRESETS.includes(resolutionPreset)) {
5961
+ cancel("Invalid resolution preset.");
5962
+ return null;
5963
+ }
5964
+ const llmDefault = env.claudeCli ? "agent_bridge" : "anthropic";
5965
+ const llm = await ask(
5966
+ select({
5967
+ message: "Which LLM provider should drive comprehension + script?",
5968
+ options: [
5969
+ {
5970
+ value: "agent_bridge",
5971
+ label: env.claudeCli ? "agent_bridge \u2014 uses your local `claude` CLI (detected). No API key required." : "agent_bridge \u2014 uses a local headless agent CLI. No API key required."
5972
+ },
5973
+ {
5974
+ value: "anthropic",
5975
+ label: env.envVars.anthropic ? "anthropic \u2014 Claude via API (ANTHROPIC_API_KEY already in env)" : "anthropic \u2014 Claude via API (you'll paste an API key in a moment)"
5976
+ },
5977
+ {
5978
+ value: "openai",
5979
+ label: env.envVars.openai ? "openai \u2014 GPT via API (OPENAI_API_KEY already in env)" : "openai \u2014 GPT via API (you'll paste an API key in a moment)"
5980
+ }
5981
+ ],
5982
+ initialValue: llmDefault
5983
+ })
5984
+ );
5985
+ if (llm === null) return null;
5986
+ const collectedKeys = {};
5987
+ if (llm === "anthropic" && !env.envVars.anthropic) {
5988
+ const key = await askKey("ANTHROPIC_API_KEY", "https://console.anthropic.com/settings/keys");
5989
+ if (key === null) return null;
5990
+ if (key.length > 0) collectedKeys["ANTHROPIC_API_KEY"] = key;
5991
+ }
5992
+ if (llm === "openai" && !env.envVars.openai) {
5993
+ const key = await askKey("OPENAI_API_KEY", "https://platform.openai.com/api-keys");
5994
+ if (key === null) return null;
5995
+ if (key.length > 0) collectedKeys["OPENAI_API_KEY"] = key;
5996
+ }
5997
+ const ttsDefault = llm === "agent_bridge" && !env.envVars.openai && !env.envVars.elevenlabs ? "custom" : "elevenlabs";
5998
+ const tts = await ask(
5999
+ select({
6000
+ message: "Which TTS provider for voiceover?",
6001
+ options: [
6002
+ {
6003
+ value: "elevenlabs",
6004
+ label: env.envVars.elevenlabs ? "elevenlabs \u2014 best alignment (ELEVENLABS_API_KEY already in env)" : "elevenlabs \u2014 best alignment (you'll paste an API key)"
6005
+ },
6006
+ {
6007
+ value: "openai",
6008
+ label: env.envVars.openai || llm === "openai" ? "openai \u2014 tts-1-hd (reuses your OpenAI key)" : "openai \u2014 tts-1-hd (you'll paste an API key)"
6009
+ },
6010
+ {
6011
+ value: "custom",
6012
+ label: "custom \u2014 plug in your own TTSProvider module. No API key required here."
6013
+ }
6014
+ ],
6015
+ initialValue: ttsDefault
6016
+ })
6017
+ );
6018
+ if (tts === null) return null;
6019
+ if (tts === "elevenlabs" && !env.envVars.elevenlabs) {
6020
+ const key = await askKey("ELEVENLABS_API_KEY", "https://elevenlabs.io/app/settings/api-keys");
6021
+ if (key === null) return null;
6022
+ if (key.length > 0) collectedKeys["ELEVENLABS_API_KEY"] = key;
6023
+ }
6024
+ if (tts === "openai" && !env.envVars.openai && !collectedKeys["OPENAI_API_KEY"]) {
6025
+ const key = await askKey("OPENAI_API_KEY", "https://platform.openai.com/api-keys");
6026
+ if (key === null) return null;
6027
+ if (key.length > 0) collectedKeys["OPENAI_API_KEY"] = key;
6028
+ }
6029
+ const resolved = await resolveTargetUrl({
6030
+ env,
6031
+ llm,
6032
+ projectDir: process.cwd()
6033
+ });
6034
+ if (resolved === null) return null;
6035
+ const url = resolved.url;
6036
+ const proceed = await ask(
6037
+ confirm({
6038
+ message: `Scaffold ${projectName}/ with these choices?`,
6039
+ initialValue: true
6040
+ })
6041
+ );
6042
+ if (proceed === null || proceed === false) {
6043
+ cancel("Setup cancelled. Nothing written.");
6044
+ return null;
6045
+ }
6046
+ outro("Scaffolding now...");
6047
+ return {
6048
+ projectName,
6049
+ url,
6050
+ llm,
6051
+ tts,
6052
+ resolutionPreset,
6053
+ collectedKeys
6054
+ };
6055
+ }
6056
+ async function askKey(envVar, dashUrl) {
6057
+ const value = await ask(
6058
+ password({
6059
+ message: `Paste your ${envVar} (or press enter to skip and fill .env later)`
6060
+ })
6061
+ );
6062
+ if (value === null) return null;
6063
+ return typeof value === "string" ? value.trim() : "";
6064
+ }
6065
+ async function ask(promptResult) {
6066
+ const result = await promptResult;
6067
+ if (isCancel(result)) {
6068
+ cancel("Setup cancelled.");
6069
+ return null;
6070
+ }
6071
+ return result;
6072
+ }
6073
+ function formatDetection(env) {
6074
+ const lines = [];
6075
+ lines.push(`claude CLI: ${env.claudeCli ? "found" : "not found"}`);
6076
+ lines.push(`ffmpeg: ${env.ffmpeg ? "found" : "NOT found \u2190 required for muxing"}`);
6077
+ lines.push(`ffprobe: ${env.ffprobe ? "found" : "NOT found \u2190 required for muxing"}`);
6078
+ lines.push(`chromium: ${env.chromium ? "installed" : "NOT installed \u2190 run `showrunner install-browser`"}`);
6079
+ lines.push("");
6080
+ lines.push("env vars:");
6081
+ lines.push(` ANTHROPIC_API_KEY: ${env.envVars.anthropic ? "set" : "unset"}`);
6082
+ lines.push(` OPENAI_API_KEY: ${env.envVars.openai ? "set" : "unset"}`);
6083
+ lines.push(` ELEVENLABS_API_KEY: ${env.envVars.elevenlabs ? "set" : "unset"}`);
6084
+ return lines.join("\n");
6085
+ }
6086
+ async function resolveTargetUrl(input) {
6087
+ const { env, llm, projectDir } = input;
6088
+ const initialUrl = await ask(
6089
+ text({
6090
+ message: "What URL is your product dev server on?",
6091
+ placeholder: "http://localhost:3000",
6092
+ defaultValue: "http://localhost:3000",
6093
+ validate: (v) => {
6094
+ if (!v) return void 0;
6095
+ try {
6096
+ new URL(v);
6097
+ return void 0;
6098
+ } catch {
6099
+ return "Must be a valid URL (e.g. http://localhost:3000).";
6100
+ }
6101
+ }
6102
+ })
6103
+ );
6104
+ if (initialUrl === null) return null;
6105
+ const probeSpin = spinner();
6106
+ probeSpin.start(`Probing ${initialUrl} ...`);
6107
+ const directProbe = await probeUrl(initialUrl);
6108
+ if (directProbe.reachable) {
6109
+ probeSpin.stop(`${initialUrl} reachable (HTTP ${directProbe.statusCode}, ${directProbe.elapsedMs}ms).`);
6110
+ return { url: initialUrl };
6111
+ }
6112
+ probeSpin.stop(`${initialUrl} not reachable yet.`);
6113
+ const scanSpin = spinner();
6114
+ scanSpin.start("Scanning common dev-server ports on localhost...");
6115
+ let scanHits = [];
6116
+ try {
6117
+ scanHits = await scanLocalPorts("localhost");
6118
+ } catch {
6119
+ }
6120
+ if (scanHits.length > 0) {
6121
+ scanSpin.stop(`Found ${scanHits.length} responding port${scanHits.length === 1 ? "" : "s"} on localhost.`);
6122
+ const pick = await ask(
6123
+ select({
6124
+ message: "Use one of the running servers?",
6125
+ options: [
6126
+ ...scanHits.map((h) => ({
6127
+ value: h.url,
6128
+ label: `${h.url} (HTTP ${h.statusCode})`
6129
+ })),
6130
+ { value: "__keep__", label: `No \u2014 keep what I typed (${initialUrl})` },
6131
+ { value: "__agent__", label: "No \u2014 let the agent figure it out (only useful with agent_bridge + claude)" }
6132
+ ],
6133
+ initialValue: scanHits[0]?.url ?? "__keep__"
6134
+ })
6135
+ );
6136
+ if (pick === null) return null;
6137
+ if (pick !== "__keep__" && pick !== "__agent__") {
6138
+ return { url: pick };
6139
+ }
6140
+ if (pick === "__keep__") {
6141
+ warnFallback(initialUrl);
6142
+ return { url: initialUrl, warnedUnreachable: true };
6143
+ }
6144
+ } else {
6145
+ scanSpin.stop("No common dev ports responding on localhost.");
6146
+ }
6147
+ const agentAvailable = llm === "agent_bridge" && env.claudeCli;
6148
+ if (!agentAvailable) {
6149
+ if (scanHits.length === 0) {
6150
+ warnFallback(initialUrl);
6151
+ return { url: initialUrl, warnedUnreachable: true };
6152
+ }
6153
+ return { url: initialUrl, warnedUnreachable: true };
6154
+ }
6155
+ const useAgent = await ask(
6156
+ confirm({
6157
+ message: "Want me to inspect this directory with the `claude` agent and propose a dev-server command + URL?",
6158
+ initialValue: true
6159
+ })
6160
+ );
6161
+ if (useAgent === null) return null;
6162
+ if (useAgent === false) {
6163
+ warnFallback(initialUrl);
6164
+ return { url: initialUrl, warnedUnreachable: true };
6165
+ }
6166
+ const discoverSpin = spinner();
6167
+ discoverSpin.start("Asking the agent to read your project...");
6168
+ let proposal;
6169
+ try {
6170
+ proposal = await discoverDevServer(projectDir);
6171
+ discoverSpin.stop("Agent returned a proposal.");
6172
+ } catch (err) {
6173
+ discoverSpin.stop("Agent inspection failed.");
6174
+ note(
6175
+ `${err instanceof Error ? err.message : String(err)}
6176
+
6177
+ Keeping your typed URL for now. Start your server, then run:
6178
+ showrunner set-target -c demo.yaml --url ${initialUrl}`,
6179
+ "Agent error"
6180
+ );
6181
+ return { url: initialUrl, warnedUnreachable: true };
6182
+ }
6183
+ note(formatProposal(proposal, projectDir), "Agent proposal");
6184
+ const spawnIt = await ask(
6185
+ confirm({
6186
+ message: `Spawn \`${proposal.command} ${proposal.args.join(" ")}\` in ${projectDir} and wait for ${proposal.url}?`,
6187
+ initialValue: proposal.confidence !== "low"
6188
+ })
6189
+ );
6190
+ if (spawnIt === null) return null;
6191
+ if (spawnIt === false) {
6192
+ note(
6193
+ `OK \u2014 start the server yourself, then run:
6194
+ showrunner set-target -c demo.yaml --url ${proposal.url}`,
6195
+ "Skipping spawn"
6196
+ );
6197
+ return { url: proposal.url, warnedUnreachable: true };
6198
+ }
6199
+ const spawnSpin = spinner();
6200
+ spawnSpin.start(`Spawning and waiting for ${proposal.url} (up to 60s)...`);
6201
+ const spawnResult = await spawnAndWait({
6202
+ command: proposal.command,
6203
+ args: proposal.args,
6204
+ url: proposal.url,
6205
+ cwd: projectDir
6206
+ });
6207
+ if (spawnResult.ok) {
6208
+ spawnSpin.stop(`Dev server up at ${proposal.url} (pid ${spawnResult.pid}).`);
6209
+ note(
6210
+ `When you're done, stop it with: kill ${spawnResult.pid}`,
6211
+ "Server running"
6212
+ );
6213
+ return { url: proposal.url, spawnedPid: spawnResult.pid };
6214
+ }
6215
+ spawnSpin.stop(`Spawn or wait failed.`);
6216
+ note(
6217
+ `${spawnResult.reason ?? "unknown error"}
6218
+
6219
+ Keeping ${proposal.url} as the configured target. If the server eventually comes up, no action needed; otherwise run:
6220
+ showrunner set-target -c demo.yaml --url <actual-url>`,
6221
+ "Spawn issue"
6222
+ );
6223
+ return { url: proposal.url, warnedUnreachable: true };
6224
+ }
6225
+ function warnFallback(url) {
6226
+ note(
6227
+ `That's fine \u2014 you can start your dev server later. When it's up, run:
6228
+
6229
+ showrunner set-target -c demo.yaml --url ${url}
6230
+
6231
+ to re-probe and update the config.`,
6232
+ "Heads up"
6233
+ );
6234
+ }
6235
+ function formatProposal(p, cwd) {
6236
+ return [
6237
+ `command: ${p.command} ${p.args.join(" ")}`,
6238
+ `cwd: ${cwd}`,
6239
+ `url: ${p.url}`,
6240
+ `confidence: ${p.confidence}`,
6241
+ `rationale: ${p.rationale}`
6242
+ ].join("\n");
6243
+ }
6244
+
6245
+ // src/commands/init.ts
5600
6246
  var LLM_CHOICES = ["anthropic", "openai", "agent_bridge"];
5601
6247
  var TTS_CHOICES = ["elevenlabs", "openai", "custom"];
5602
6248
  var PLACEHOLDER_FILES = [
@@ -5608,22 +6254,52 @@ var PLACEHOLDER_FILES = [
5608
6254
  "output/.gitkeep"
5609
6255
  ];
5610
6256
  async function initCommand(opts) {
5611
- const resolutionPreset = validateChoice(
5612
- opts.resolution,
5613
- [...RESOLUTION_PRESETS],
5614
- "standard",
5615
- "--resolution"
5616
- );
5617
- const resolved = {
5618
- ...opts,
5619
- llm: validateChoice(opts.llmProvider, LLM_CHOICES, "anthropic", "--llm-provider"),
5620
- tts: validateChoice(opts.ttsProvider, TTS_CHOICES, "elevenlabs", "--tts-provider"),
5621
- resolutionPreset,
5622
- resolution: resolvePreset(resolutionPreset)
5623
- };
5624
- const parent = isAbsolute8(opts.dir) ? opts.dir : resolve15(process.cwd(), opts.dir);
5625
- const projectRoot = join10(parent, opts.name);
5626
- if (!opts.force && await pathExists(projectRoot)) {
6257
+ const wantsInteractive = Boolean(process.stdout.isTTY) && !opts.yes;
6258
+ let resolved;
6259
+ let collectedKeys = {};
6260
+ if (wantsInteractive) {
6261
+ const env = await detectEnvironment();
6262
+ const wiz = await runWizard(env);
6263
+ if (!wiz) return;
6264
+ resolved = {
6265
+ name: wiz.projectName,
6266
+ url: wiz.url,
6267
+ dir: opts.dir,
6268
+ force: opts.force,
6269
+ llm: wiz.llm,
6270
+ tts: wiz.tts,
6271
+ resolutionPreset: wiz.resolutionPreset,
6272
+ resolution: resolvePreset(wiz.resolutionPreset)
6273
+ };
6274
+ collectedKeys = wiz.collectedKeys;
6275
+ } else {
6276
+ const resolutionPreset = validateChoice(
6277
+ opts.resolution,
6278
+ [...RESOLUTION_PRESETS],
6279
+ "standard",
6280
+ "--resolution"
6281
+ );
6282
+ resolved = {
6283
+ ...opts,
6284
+ llm: validateChoice(
6285
+ opts.llmProvider,
6286
+ LLM_CHOICES,
6287
+ "anthropic",
6288
+ "--llm-provider"
6289
+ ),
6290
+ tts: validateChoice(
6291
+ opts.ttsProvider,
6292
+ TTS_CHOICES,
6293
+ "elevenlabs",
6294
+ "--tts-provider"
6295
+ ),
6296
+ resolutionPreset,
6297
+ resolution: resolvePreset(resolutionPreset)
6298
+ };
6299
+ }
6300
+ const parent = isAbsolute8(resolved.dir) ? resolved.dir : resolve15(process.cwd(), resolved.dir);
6301
+ const projectRoot = join11(parent, resolved.name);
6302
+ if (!resolved.force && await pathExists(projectRoot)) {
5627
6303
  logger.error(
5628
6304
  `Directory already exists: ${projectRoot}. Pass --force to overwrite, or choose a different --name/--dir.`
5629
6305
  );
@@ -5631,48 +6307,74 @@ async function initCommand(opts) {
5631
6307
  }
5632
6308
  await mkdir11(projectRoot, { recursive: true });
5633
6309
  for (const rel of PLACEHOLDER_FILES) {
5634
- const dest = join10(projectRoot, rel);
6310
+ const dest = join11(projectRoot, rel);
5635
6311
  await mkdir11(dirname10(dest), { recursive: true });
5636
6312
  await writeFile12(dest, "", "utf8");
5637
6313
  }
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(), {
6314
+ await writeFile12(join11(projectRoot, "demo.yaml"), demoYamlTemplate(resolved), "utf8");
6315
+ await writeFile12(join11(projectRoot, ".env.example"), envExampleTemplate(resolved), "utf8");
6316
+ await writeFile12(join11(projectRoot, ".gitignore"), gitignoreTemplate(), "utf8");
6317
+ await mkdir11(join11(projectRoot, "docs"), { recursive: true });
6318
+ await writeFile12(join11(projectRoot, "docs/PRD.md"), prdStubTemplate(resolved), "utf8");
6319
+ await writeFile12(join11(projectRoot, "scripts/manifest.json"), starterManifest(resolved), "utf8");
6320
+ await writeFile12(join11(projectRoot, "scripts/seed_demo_data.sh"), seedScript(), {
5645
6321
  mode: 493
5646
6322
  });
5647
- await writeFile12(join10(projectRoot, "scripts/reset_demo_data.sh"), resetScript(), {
6323
+ await writeFile12(join11(projectRoot, "scripts/reset_demo_data.sh"), resetScript(), {
5648
6324
  mode: 493
5649
6325
  });
5650
- await writeFile12(join10(projectRoot, "scripts/teardown.sh"), teardownScript(), {
6326
+ await writeFile12(join11(projectRoot, "scripts/teardown.sh"), teardownScript(), {
5651
6327
  mode: 493
5652
6328
  });
5653
- await writeFile12(join10(projectRoot, "README.md"), readmeTemplate(resolved), "utf8");
6329
+ await writeFile12(join11(projectRoot, "README.md"), readmeTemplate(resolved), "utf8");
6330
+ if (Object.keys(collectedKeys).length > 0) {
6331
+ await writeFile12(join11(projectRoot, ".env"), buildEnvFile(collectedKeys, resolved), "utf8");
6332
+ }
5654
6333
  logger.info(`Showrunner project scaffolded at ${projectRoot} (llm=${resolved.llm}, tts=${resolved.tts})`);
5655
- printNextSteps(opts.name, resolved);
6334
+ printNextSteps(resolved.name, resolved, collectedKeys);
6335
+ }
6336
+ function buildEnvFile(keys, resolved) {
6337
+ const lines = [`# Showrunner secrets \u2014 generated by \`showrunner init\`.`];
6338
+ for (const [k, v] of Object.entries(keys)) {
6339
+ lines.push(`${k}=${v}`);
6340
+ }
6341
+ lines.push("");
6342
+ lines.push(`# Optional: form / setup-script auth.`);
6343
+ lines.push(`DEMO_EMAIL=`);
6344
+ lines.push(`DEMO_PASSWORD=`);
6345
+ lines.push("");
6346
+ lines.push(`# Optional log level: debug | info | warn | error`);
6347
+ lines.push(`SHOWRUNNER_LOG_LEVEL=info`);
6348
+ lines.push("");
6349
+ if (resolved.llm === "openai" || resolved.tts === "openai") {
6350
+ }
6351
+ return lines.join("\n");
5656
6352
  }
5657
- function printNextSteps(projectName, resolved) {
6353
+ function printNextSteps(projectName, resolved, collectedKeys) {
5658
6354
  const envVars = requiredEnvVars(resolved);
6355
+ const keysProvidedByWizard = Object.keys(collectedKeys);
6356
+ const remainingEnvVars = envVars.filter((v) => !keysProvidedByWizard.includes(v));
5659
6357
  const lines = ["", `Next:`, ""];
5660
6358
  let step = 1;
5661
6359
  lines.push(` ${step++}. cd ${projectName}`);
5662
- if (envVars.length > 0) {
5663
- lines.push(` ${step++}. cp .env.example .env`);
6360
+ if (envVars.length === 0) {
6361
+ lines.push(
6362
+ ` ${step++}. (.env not needed \u2014 agent_bridge LLM + ${resolved.tts} TTS don't use API keys.)`
6363
+ );
6364
+ } else if (remainingEnvVars.length === 0) {
6365
+ lines.push(` ${step++}. .env is already populated with the keys you pasted.`);
6366
+ } else {
6367
+ if (keysProvidedByWizard.length === 0) {
6368
+ lines.push(` ${step++}. cp .env.example .env`);
6369
+ }
5664
6370
  lines.push(` ${step++}. Edit .env to fill in:`);
5665
- for (const v of envVars) {
6371
+ for (const v of remainingEnvVars) {
5666
6372
  const dash = PROVIDER_DASHBOARDS[v];
5667
6373
  lines.push(` ${v}${dash ? ` (get it at ${dash})` : ""}`);
5668
6374
  }
5669
6375
  lines.push(
5670
6376
  ` (No keys? Switch llm.default.provider to \`agent_bridge\` in demo.yaml \u2014 uses your local Claude CLI instead.)`
5671
6377
  );
5672
- } else {
5673
- lines.push(
5674
- ` ${step++}. (.env not needed \u2014 agent_bridge LLM + ${resolved.tts} TTS don't use API keys.)`
5675
- );
5676
6378
  }
5677
6379
  lines.push(` ${step++}. Edit docs/PRD.md (the stub explains what each section is for).`);
5678
6380
  lines.push(` ${step++}. showrunner doctor -c demo.yaml`);
@@ -5708,7 +6410,7 @@ function validateChoice(value, allowed, defaultValue, flagName) {
5708
6410
  }
5709
6411
  async function pathExists(p) {
5710
6412
  try {
5711
- await access2(p);
6413
+ await access3(p);
5712
6414
  return true;
5713
6415
  } catch {
5714
6416
  return false;
@@ -6127,22 +6829,22 @@ and \`at_word\` actions degrade to \`at\`).
6127
6829
  }
6128
6830
 
6129
6831
  // src/commands/installBrowser.ts
6130
- import { spawn as spawn4 } from "child_process";
6131
- import { access as access3 } from "fs/promises";
6832
+ import { spawn as spawn6 } from "child_process";
6833
+ import { access as access4 } from "fs/promises";
6132
6834
  import { fileURLToPath } from "url";
6133
- import { dirname as dirname11, join as join11 } from "path";
6835
+ import { dirname as dirname11, join as join12 } from "path";
6134
6836
  var DEFAULT_BROWSER = "chromium";
6135
6837
  async function installBrowserCommand(opts) {
6136
6838
  const browser = opts.browser ?? DEFAULT_BROWSER;
6137
6839
  const cli = await resolvePlaywrightCoreCli();
6138
6840
  logger.info(`installing Playwright ${browser} (via bundled playwright-core, no project required)`);
6139
- const child = spawn4(process.execPath, [cli, "install", browser], {
6841
+ const child = spawn6(process.execPath, [cli, "install", browser], {
6140
6842
  stdio: "inherit",
6141
6843
  env: process.env
6142
6844
  });
6143
- const code = await new Promise((resolve27, reject) => {
6845
+ const code = await new Promise((resolve28, reject) => {
6144
6846
  child.on("error", reject);
6145
- child.on("close", (c) => resolve27(c ?? 0));
6847
+ child.on("close", (c) => resolve28(c ?? 0));
6146
6848
  });
6147
6849
  if (code !== 0) {
6148
6850
  logger.error(`playwright install exited with code ${code}`);
@@ -6155,9 +6857,9 @@ async function resolvePlaywrightCoreCli() {
6155
6857
  let dir = dirname11(here);
6156
6858
  const root = dir.split(/[\\/]/)[0] + "/";
6157
6859
  while (dir && dir !== root) {
6158
- const candidate = join11(dir, "node_modules", "playwright-core", "cli.js");
6860
+ const candidate = join12(dir, "node_modules", "playwright-core", "cli.js");
6159
6861
  try {
6160
- await access3(candidate);
6862
+ await access4(candidate);
6161
6863
  return candidate;
6162
6864
  } catch {
6163
6865
  }
@@ -6170,9 +6872,60 @@ async function resolvePlaywrightCoreCli() {
6170
6872
  );
6171
6873
  }
6172
6874
 
6173
- // src/commands/validate.ts
6174
- import { stat as stat10 } from "fs/promises";
6875
+ // src/commands/setTarget.ts
6876
+ import { readFile as readFile11, writeFile as writeFile13 } from "fs/promises";
6175
6877
  import { isAbsolute as isAbsolute9, resolve as resolve16 } from "path";
6878
+ import yaml2 from "js-yaml";
6879
+ async function setTargetCommand(opts) {
6880
+ try {
6881
+ new URL(opts.url);
6882
+ } catch {
6883
+ logger.error(`Invalid URL: ${opts.url}`);
6884
+ process.exit(2);
6885
+ }
6886
+ try {
6887
+ await loadConfig(opts.config);
6888
+ } catch (err) {
6889
+ if (err instanceof ConfigError) {
6890
+ logger.error(err.message);
6891
+ process.exit(2);
6892
+ }
6893
+ throw err;
6894
+ }
6895
+ if (!opts.force) {
6896
+ logger.info(`probing ${opts.url} ...`);
6897
+ const probe = await probeUrl(opts.url);
6898
+ if (!probe.reachable) {
6899
+ logger.error(
6900
+ `target ${opts.url} not reachable${probe.reason ? ` (${probe.reason})` : ""}. Start your dev server first, or pass --force to set the URL anyway.`
6901
+ );
6902
+ process.exit(1);
6903
+ }
6904
+ logger.info(`reachable (HTTP ${probe.statusCode}, ${probe.elapsedMs}ms).`);
6905
+ }
6906
+ const absPath = isAbsolute9(opts.config) ? opts.config : resolve16(process.cwd(), opts.config);
6907
+ const rawText = await readFile11(absPath, "utf8");
6908
+ const doc = yaml2.load(rawText);
6909
+ if (!doc || typeof doc !== "object" || !doc["recording"] || typeof doc["recording"] !== "object") {
6910
+ logger.error(
6911
+ `config has no \`recording\` block \u2014 has it been edited by hand into an invalid shape?`
6912
+ );
6913
+ process.exit(2);
6914
+ }
6915
+ doc["recording"]["target_url"] = opts.url;
6916
+ const newText = yaml2.dump(doc, {
6917
+ lineWidth: 100,
6918
+ noRefs: true,
6919
+ sortKeys: false
6920
+ });
6921
+ await writeFile13(absPath, newText, "utf8");
6922
+ logger.info(`updated recording.target_url \u2192 ${opts.url}`);
6923
+ logger.info(`(comments in demo.yaml may have been stripped \u2014 re-add them if needed.)`);
6924
+ }
6925
+
6926
+ // src/commands/validate.ts
6927
+ import { stat as stat12 } from "fs/promises";
6928
+ import { isAbsolute as isAbsolute10, resolve as resolve17 } from "path";
6176
6929
  async function validateCommand(opts) {
6177
6930
  try {
6178
6931
  process.loadEnvFile?.();
@@ -6207,9 +6960,9 @@ async function checkReferencedPaths(config, configDir) {
6207
6960
  const warnings = [];
6208
6961
  const check = async (relPath, label) => {
6209
6962
  if (!relPath) return;
6210
- const abs = isAbsolute9(relPath) ? relPath : resolve16(configDir, relPath);
6963
+ const abs = isAbsolute10(relPath) ? relPath : resolve17(configDir, relPath);
6211
6964
  try {
6212
- await stat10(abs);
6965
+ await stat12(abs);
6213
6966
  } catch {
6214
6967
  warnings.push(`${label} not found: ${abs}`);
6215
6968
  }
@@ -6237,7 +6990,7 @@ async function checkReferencedPaths(config, configDir) {
6237
6990
  }
6238
6991
 
6239
6992
  // src/commands/printVo.ts
6240
- import { resolve as resolve17 } from "path";
6993
+ import { resolve as resolve18 } from "path";
6241
6994
  async function printVoCommand(opts) {
6242
6995
  let loaded;
6243
6996
  try {
@@ -6249,7 +7002,7 @@ async function printVoCommand(opts) {
6249
7002
  }
6250
7003
  throw err;
6251
7004
  }
6252
- const manifestPath = resolve17(loaded.configDir, "./scripts/manifest.json");
7005
+ const manifestPath = resolve18(loaded.configDir, "./scripts/manifest.json");
6253
7006
  let manifest;
6254
7007
  try {
6255
7008
  manifest = await readManifest(manifestPath);
@@ -6260,13 +7013,13 @@ async function printVoCommand(opts) {
6260
7013
  }
6261
7014
  throw err;
6262
7015
  }
6263
- const text = renderVoScript(manifest, { projectName: loaded.config.project.name });
6264
- process.stdout.write(text);
7016
+ const text2 = renderVoScript(manifest, { projectName: loaded.config.project.name });
7017
+ process.stdout.write(text2);
6265
7018
  }
6266
7019
 
6267
7020
  // src/commands/approveVo.ts
6268
- import { rm as rm2, stat as stat11 } from "fs/promises";
6269
- import { resolve as resolve18 } from "path";
7021
+ import { rm as rm2, stat as stat13 } from "fs/promises";
7022
+ import { resolve as resolve19 } from "path";
6270
7023
  async function approveVoCommand(opts) {
6271
7024
  let loaded;
6272
7025
  try {
@@ -6278,9 +7031,9 @@ async function approveVoCommand(opts) {
6278
7031
  }
6279
7032
  throw err;
6280
7033
  }
6281
- const manifestPath = resolve18(loaded.configDir, "./scripts/manifest.json");
6282
- const voScriptPath = resolve18(loaded.configDir, "./scripts/vo_script.txt");
6283
- const lockPath = resolve18(loaded.configDir, ".showrunner-lock");
7034
+ const manifestPath = resolve19(loaded.configDir, "./scripts/manifest.json");
7035
+ const voScriptPath = resolve19(loaded.configDir, "./scripts/vo_script.txt");
7036
+ const lockPath = resolve19(loaded.configDir, ".showrunner-lock");
6284
7037
  let manifest;
6285
7038
  try {
6286
7039
  manifest = await readManifest(manifestPath);
@@ -6313,7 +7066,7 @@ async function approveVoCommand(opts) {
6313
7066
  }
6314
7067
  async function pathExists2(p) {
6315
7068
  try {
6316
- await stat11(p);
7069
+ await stat13(p);
6317
7070
  return true;
6318
7071
  } catch {
6319
7072
  return false;
@@ -6321,8 +7074,8 @@ async function pathExists2(p) {
6321
7074
  }
6322
7075
 
6323
7076
  // src/commands/rerunSegment.ts
6324
- import { mkdir as mkdir12, rename as rename3, writeFile as writeFile13 } from "fs/promises";
6325
- import { join as join12, resolve as resolve19 } from "path";
7077
+ import { mkdir as mkdir12, rename as rename3, writeFile as writeFile14 } from "fs/promises";
7078
+ import { join as join13, resolve as resolve20 } from "path";
6326
7079
  import { chromium as chromium4, firefox as firefox4, webkit as webkit4 } from "playwright-core";
6327
7080
  var RERUN_BUFFER_MS = 500;
6328
7081
  var browserMap4 = { chromium: chromium4, firefox: firefox4, webkit: webkit4 };
@@ -6338,8 +7091,8 @@ async function rerunSegmentCommand(opts) {
6338
7091
  throw err;
6339
7092
  }
6340
7093
  const { config, configDir } = loaded;
6341
- const manifestPath = resolve19(configDir, "./scripts/manifest.json");
6342
- const videoDir = resolve19(configDir, config.recording.output_dir);
7094
+ const manifestPath = resolve20(configDir, "./scripts/manifest.json");
7095
+ const videoDir = resolve20(configDir, config.recording.output_dir);
6343
7096
  let manifest;
6344
7097
  try {
6345
7098
  manifest = await readManifest(manifestPath);
@@ -6439,9 +7192,9 @@ async function rerunSegmentCommand(opts) {
6439
7192
  }
6440
7193
  }
6441
7194
  await page.waitForTimeout(config.recording.segment_buffer_ms);
6442
- const traceDir = resolve19(configDir, config.recording.trace_dir);
7195
+ const traceDir = resolve20(configDir, config.recording.trace_dir);
6443
7196
  await mkdir12(traceDir, { recursive: true });
6444
- await ctx.tracing.stopChunk({ path: join12(traceDir, `${segment.id}.zip`) });
7197
+ await ctx.tracing.stopChunk({ path: join13(traceDir, `${segment.id}.zip`) });
6445
7198
  const videoHandle = page.video();
6446
7199
  await ctx.close();
6447
7200
  if (!videoHandle) {
@@ -6449,10 +7202,10 @@ async function rerunSegmentCommand(opts) {
6449
7202
  process.exit(1);
6450
7203
  }
6451
7204
  const original = await videoHandle.path();
6452
- const dest = join12(videoDir, `${segment.id}.webm`);
7205
+ const dest = join13(videoDir, `${segment.id}.webm`);
6453
7206
  await rename3(original, dest);
6454
- const metadataPath = join12(videoDir, `${segment.id}.rerun.json`);
6455
- await writeFile13(
7207
+ const metadataPath = join13(videoDir, `${segment.id}.rerun.json`);
7208
+ await writeFile14(
6456
7209
  metadataPath,
6457
7210
  JSON.stringify(
6458
7211
  {
@@ -6480,12 +7233,12 @@ async function rerunSegmentCommand(opts) {
6480
7233
 
6481
7234
  // src/commands/captureAuth.ts
6482
7235
  import { mkdir as mkdir14 } from "fs/promises";
6483
- import { dirname as dirname12, isAbsolute as isAbsolute10, resolve as resolve21 } from "path";
7236
+ import { dirname as dirname12, isAbsolute as isAbsolute11, resolve as resolve22 } from "path";
6484
7237
  import { createInterface } from "readline/promises";
6485
7238
 
6486
7239
  // src/recording/headed.ts
6487
7240
  import { mkdir as mkdir13 } from "fs/promises";
6488
- import { resolve as resolve20 } from "path";
7241
+ import { resolve as resolve21 } from "path";
6489
7242
  import {
6490
7243
  chromium as chromium5,
6491
7244
  firefox as firefox5,
@@ -6499,7 +7252,7 @@ async function launchHeadedSession(opts) {
6499
7252
  viewport: { width: recording.viewport.width, height: recording.viewport.height }
6500
7253
  };
6501
7254
  if (opts.recordVideo) {
6502
- const videoDir = resolve20(configDir, recording.output_dir);
7255
+ const videoDir = resolve21(configDir, recording.output_dir);
6503
7256
  await mkdir13(videoDir, { recursive: true });
6504
7257
  contextOptions.recordVideo = {
6505
7258
  dir: videoDir,
@@ -6531,9 +7284,9 @@ async function launchHeadedSession(opts) {
6531
7284
  const page = await context.newPage();
6532
7285
  if (opts.withCursor) {
6533
7286
  page.on("console", (msg) => {
6534
- const text = msg.text();
6535
- if (text.startsWith("[showrunner-cursor]")) {
6536
- logger.debug(`browser: ${text}`);
7287
+ const text2 = msg.text();
7288
+ if (text2.startsWith("[showrunner-cursor]")) {
7289
+ logger.debug(`browser: ${text2}`);
6537
7290
  }
6538
7291
  });
6539
7292
  }
@@ -6560,13 +7313,13 @@ async function captureAuthCommand(opts) {
6560
7313
  }
6561
7314
  throw err;
6562
7315
  }
6563
- const envFile = resolve21(loaded.configDir, ".env");
7316
+ const envFile = resolve22(loaded.configDir, ".env");
6564
7317
  try {
6565
7318
  process.loadEnvFile(envFile);
6566
7319
  } catch {
6567
7320
  }
6568
7321
  const cookiesRel = opts.outputCookies ?? "./auth/session.json";
6569
- const cookiesPath = isAbsolute10(cookiesRel) ? cookiesRel : resolve21(loaded.configDir, cookiesRel);
7322
+ const cookiesPath = isAbsolute11(cookiesRel) ? cookiesRel : resolve22(loaded.configDir, cookiesRel);
6570
7323
  await mkdir14(dirname12(cookiesPath), { recursive: true });
6571
7324
  logger.info("Launching headed browser for auth capture", {
6572
7325
  target: loaded.config.recording.target_url,
@@ -6607,8 +7360,8 @@ Then re-run \`showrunner run --config <demo.yaml>\`.
6607
7360
  }
6608
7361
 
6609
7362
  // src/commands/trace.ts
6610
- import { readdir as readdir2, stat as stat12 } from "fs/promises";
6611
- import { isAbsolute as isAbsolute11, join as join13, resolve as resolve22 } from "path";
7363
+ import { readdir as readdir3, stat as stat14 } from "fs/promises";
7364
+ import { isAbsolute as isAbsolute12, join as join14, resolve as resolve23 } from "path";
6612
7365
  async function traceCommand(opts) {
6613
7366
  let loaded;
6614
7367
  try {
@@ -6620,9 +7373,9 @@ async function traceCommand(opts) {
6620
7373
  }
6621
7374
  throw err;
6622
7375
  }
6623
- const traceDir = resolve22(loaded.configDir, loaded.config.recording.trace_dir);
6624
- const videoDir = resolve22(loaded.configDir, loaded.config.recording.output_dir);
6625
- const slicePlanPath = join13(videoDir, "slice_plan.json");
7376
+ const traceDir = resolve23(loaded.configDir, loaded.config.recording.trace_dir);
7377
+ const videoDir = resolve23(loaded.configDir, loaded.config.recording.output_dir);
7378
+ const slicePlanPath = join14(videoDir, "slice_plan.json");
6626
7379
  if (opts.all) {
6627
7380
  let plan;
6628
7381
  try {
@@ -6633,14 +7386,14 @@ async function traceCommand(opts) {
6633
7386
  process.exit(1);
6634
7387
  }
6635
7388
  for (const seg of plan.segments) {
6636
- const tracePath = seg.trace_path && isAbsolute11(seg.trace_path) ? seg.trace_path : seg.trace_path ? resolve22(loaded.configDir, seg.trace_path) : join13(traceDir, `${seg.id}.zip`);
7389
+ const tracePath = seg.trace_path && isAbsolute12(seg.trace_path) ? seg.trace_path : seg.trace_path ? resolve23(loaded.configDir, seg.trace_path) : join14(traceDir, `${seg.id}.zip`);
6637
7390
  logger.info(`Opening trace for ${seg.id}`, { path: tracePath });
6638
7391
  await openTrace(tracePath);
6639
7392
  }
6640
7393
  return;
6641
7394
  }
6642
7395
  if (opts.segment) {
6643
- const tracePath = join13(traceDir, `${opts.segment}.zip`);
7396
+ const tracePath = join14(traceDir, `${opts.segment}.zip`);
6644
7397
  if (!await fileExists8(tracePath)) {
6645
7398
  logger.error(`No trace found at ${tracePath}`);
6646
7399
  process.exit(1);
@@ -6681,7 +7434,7 @@ async function openTrace(tracePath) {
6681
7434
  }
6682
7435
  async function listTraces(dir) {
6683
7436
  try {
6684
- const entries = await readdir2(dir);
7437
+ const entries = await readdir3(dir);
6685
7438
  return entries.filter((e) => e.endsWith(".zip")).sort();
6686
7439
  } catch {
6687
7440
  return [];
@@ -6689,7 +7442,7 @@ async function listTraces(dir) {
6689
7442
  }
6690
7443
  async function fileExists8(path) {
6691
7444
  try {
6692
- await stat12(path);
7445
+ await stat14(path);
6693
7446
  return true;
6694
7447
  } catch {
6695
7448
  return false;
@@ -6697,8 +7450,8 @@ async function fileExists8(path) {
6697
7450
  }
6698
7451
 
6699
7452
  // src/commands/preview.ts
6700
- import { stat as stat13 } from "fs/promises";
6701
- import { resolve as resolve23 } from "path";
7453
+ import { stat as stat15 } from "fs/promises";
7454
+ import { resolve as resolve24 } from "path";
6702
7455
  async function previewCommand(opts) {
6703
7456
  let loaded;
6704
7457
  try {
@@ -6710,7 +7463,7 @@ async function previewCommand(opts) {
6710
7463
  }
6711
7464
  throw err;
6712
7465
  }
6713
- const previewSpec = resolve23(loaded.configDir, "./scripts/playwright_demo.spec.ts");
7466
+ const previewSpec = resolve24(loaded.configDir, "./scripts/playwright_demo.spec.ts");
6714
7467
  if (!await fileExists9(previewSpec)) {
6715
7468
  logger.error(
6716
7469
  `No preview spec at ${previewSpec}. Run \`showrunner run --stages script\` first to generate it.`
@@ -6736,7 +7489,7 @@ async function previewCommand(opts) {
6736
7489
  }
6737
7490
  async function fileExists9(path) {
6738
7491
  try {
6739
- await stat13(path);
7492
+ await stat15(path);
6740
7493
  return true;
6741
7494
  } catch {
6742
7495
  return false;
@@ -6744,8 +7497,8 @@ async function fileExists9(path) {
6744
7497
  }
6745
7498
 
6746
7499
  // src/commands/understand.ts
6747
- import { mkdir as mkdir15, readFile as readFile10, writeFile as writeFile14 } from "fs/promises";
6748
- import { dirname as dirname13, isAbsolute as isAbsolute12, resolve as resolve24 } from "path";
7500
+ import { mkdir as mkdir15, readFile as readFile12, writeFile as writeFile15 } from "fs/promises";
7501
+ import { dirname as dirname13, isAbsolute as isAbsolute13, resolve as resolve25 } from "path";
6749
7502
 
6750
7503
  // src/productModel/prompts.ts
6751
7504
  var PRODUCT_MODEL_SYSTEM_PROMPT = `You build product_model.json for Showrunner, an automated product demo recorder.
@@ -6869,13 +7622,13 @@ var LineReader = class {
6869
7622
  if (this.closed) {
6870
7623
  return Promise.reject(new Error("stdin closed before answer was provided"));
6871
7624
  }
6872
- return new Promise((resolve27, reject) => {
6873
- this.waiter = resolve27;
7625
+ return new Promise((resolve28, reject) => {
7626
+ this.waiter = resolve28;
6874
7627
  this.rejecter = reject;
6875
7628
  });
6876
7629
  }
6877
7630
  };
6878
- async function ask(reader, prompt) {
7631
+ async function ask2(reader, prompt) {
6879
7632
  process.stdout.write(prompt);
6880
7633
  return reader.read();
6881
7634
  }
@@ -6886,23 +7639,23 @@ async function runInteractiveQA() {
6886
7639
  process.stdout.write(
6887
7640
  "\nLet's build a product_model interactively. Answer each prompt with a short line.\n\n"
6888
7641
  );
6889
- const productName = (await ask(reader, "1. Product name: ")).trim();
7642
+ const productName = (await ask2(reader, "1. Product name: ")).trim();
6890
7643
  if (!productName) throw new Error("product name is required");
6891
- const tagline = (await ask(reader, "2. One-sentence tagline: ")).trim();
6892
- const primaryUser = (await ask(reader, "3. Who is the primary user? ")).trim();
6893
- const featuresLine = (await ask(reader, "4. Top 3-6 features, comma-separated:\n ")).trim();
7644
+ const tagline = (await ask2(reader, "2. One-sentence tagline: ")).trim();
7645
+ const primaryUser = (await ask2(reader, "3. Who is the primary user? ")).trim();
7646
+ const featuresLine = (await ask2(reader, "4. Top 3-6 features, comma-separated:\n ")).trim();
6894
7647
  const topFeatures = featuresLine.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6895
- const flowCountStr = (await ask(reader, "5. How many flows would you like to demo (2-4)? ")).trim();
7648
+ const flowCountStr = (await ask2(reader, "5. How many flows would you like to demo (2-4)? ")).trim();
6896
7649
  const flowCount = clamp(parseInt(flowCountStr, 10) || 2, 2, 4);
6897
7650
  const topFlows = [];
6898
7651
  for (let i = 0; i < flowCount; i++) {
6899
- const name = (await ask(reader, ` Flow ${i + 1} name: `)).trim();
6900
- const stepsLine = (await ask(reader, ` Flow ${i + 1} steps, '|' separated:
7652
+ const name = (await ask2(reader, ` Flow ${i + 1} name: `)).trim();
7653
+ const stepsLine = (await ask2(reader, ` Flow ${i + 1} steps, '|' separated:
6901
7654
  `)).trim();
6902
7655
  const steps = stepsLine.split("|").map((s) => s.trim()).filter((s) => s.length > 0);
6903
7656
  topFlows.push({ name, steps });
6904
7657
  }
6905
- const durationStr = (await ask(reader, "6. Target demo duration in seconds (60-120, default 75): ")).trim();
7658
+ const durationStr = (await ask2(reader, "6. Target demo duration in seconds (60-120, default 75): ")).trim();
6906
7659
  const suggestedDurationSeconds = clamp(parseInt(durationStr, 10) || 75, 30, 180);
6907
7660
  return {
6908
7661
  productName,
@@ -6943,13 +7696,13 @@ async function understandCommand(opts) {
6943
7696
  }
6944
7697
  sources = loaded.config.comprehension.sources.map((s) => ({ path: s.path, type: s.type }));
6945
7698
  llmConfig = loaded.config.llm;
6946
- const envFile = resolve24(loaded.configDir, ".env");
7699
+ const envFile = resolve25(loaded.configDir, ".env");
6947
7700
  try {
6948
7701
  process.loadEnvFile(envFile);
6949
7702
  } catch {
6950
7703
  }
6951
7704
  }
6952
- const outputPath = isAbsolute12(outputRel) ? outputRel : resolve24(configDir, outputRel);
7705
+ const outputPath = isAbsolute13(outputRel) ? outputRel : resolve25(configDir, outputRel);
6953
7706
  await mkdir15(dirname13(outputPath), { recursive: true });
6954
7707
  let productModel;
6955
7708
  const provider = resolveDefaultLLMProvider({ configDir, llm: llmConfig });
@@ -6967,9 +7720,9 @@ async function understandCommand(opts) {
6967
7720
  }
6968
7721
  const docs = [];
6969
7722
  for (const src of sources) {
6970
- const abs = isAbsolute12(src.path) ? src.path : resolve24(configDir, src.path);
7723
+ const abs = isAbsolute13(src.path) ? src.path : resolve25(configDir, src.path);
6971
7724
  try {
6972
- const content = await readFile10(abs, "utf8");
7725
+ const content = await readFile12(abs, "utf8");
6973
7726
  docs.push({ path: src.path, type: src.type, content });
6974
7727
  } catch (err) {
6975
7728
  const cause = err instanceof Error ? err.message : String(err);
@@ -6992,16 +7745,16 @@ async function understandCommand(opts) {
6992
7745
  }
6993
7746
  throw err;
6994
7747
  }
6995
- await writeFile14(outputPath, JSON.stringify(productModel, null, 2) + "\n", "utf8");
7748
+ await writeFile15(outputPath, JSON.stringify(productModel, null, 2) + "\n", "utf8");
6996
7749
  logger.info("Wrote product_model.json", { path: outputPath });
6997
7750
  }
6998
7751
 
6999
7752
  // src/commands/instrument.ts
7000
- import { mkdir as mkdir16, readdir as readdir3, writeFile as writeFile15 } from "fs/promises";
7001
- import { dirname as dirname14, isAbsolute as isAbsolute13, join as join14, resolve as resolve25, relative as relative2 } from "path";
7753
+ import { mkdir as mkdir16, readdir as readdir4, writeFile as writeFile16 } from "fs/promises";
7754
+ import { dirname as dirname14, isAbsolute as isAbsolute14, join as join15, resolve as resolve26, relative as relative3 } from "path";
7002
7755
 
7003
7756
  // src/instrument/scan.ts
7004
- import { readFile as readFile11 } from "fs/promises";
7757
+ import { readFile as readFile13 } from "fs/promises";
7005
7758
  import { parse } from "@babel/parser";
7006
7759
  import _traverse from "@babel/traverse";
7007
7760
  var traverse = _traverse.default ?? _traverse;
@@ -7019,7 +7772,7 @@ var TARGET_TAGS = /* @__PURE__ */ new Set([
7019
7772
  "Form"
7020
7773
  ]);
7021
7774
  async function scanFile(filePath) {
7022
- const source = await readFile11(filePath, "utf8");
7775
+ const source = await readFile13(filePath, "utf8");
7023
7776
  let ast;
7024
7777
  try {
7025
7778
  ast = parse(source, {
@@ -7078,23 +7831,23 @@ function pickAttr(node, attr) {
7078
7831
  }
7079
7832
 
7080
7833
  // src/instrument/suggest.ts
7081
- import { z as z9 } from "zod";
7834
+ import { z as z10 } from "zod";
7082
7835
  var DEFAULT_MAX_TOKENS5 = 8e3;
7083
- var suggestionSchema = z9.object({
7084
- suggestions: z9.array(
7085
- z9.object({
7086
- file: z9.string(),
7087
- line: z9.number().int().positive(),
7088
- original: z9.string(),
7089
- replacement: z9.string(),
7090
- reasoning: z9.string().default("")
7836
+ var suggestionSchema = z10.object({
7837
+ suggestions: z10.array(
7838
+ z10.object({
7839
+ file: z10.string(),
7840
+ line: z10.number().int().positive(),
7841
+ original: z10.string(),
7842
+ replacement: z10.string(),
7843
+ reasoning: z10.string().default("")
7091
7844
  })
7092
7845
  )
7093
7846
  });
7094
7847
  var InstrumentSuggestionError = class extends Error {
7095
7848
  name = "InstrumentSuggestionError";
7096
7849
  };
7097
- var SYSTEM_PROMPT = `You add data-testid attributes to JSX elements so they can be reliably targeted by automated demo tools.
7850
+ var SYSTEM_PROMPT2 = `You add data-testid attributes to JSX elements so they can be reliably targeted by automated demo tools.
7098
7851
 
7099
7852
  Rules:
7100
7853
  - For each candidate, propose a stable kebab-case data-testid based on visible text, surrounding context, or the element's role. Examples: "login-submit", "new-article-button", "todo-item-toggle".
@@ -7121,7 +7874,7 @@ For each, return {file, line, original (exact source line), replacement (source
7121
7874
  });
7122
7875
  try {
7123
7876
  const result = await opts.provider.generateStructured({
7124
- systemPrompt: SYSTEM_PROMPT,
7877
+ systemPrompt: SYSTEM_PROMPT2,
7125
7878
  userPrompt,
7126
7879
  schema: suggestionSchema,
7127
7880
  schemaName: "instrument_suggestions",
@@ -7136,8 +7889,8 @@ For each, return {file, line, original (exact source line), replacement (source
7136
7889
  }
7137
7890
 
7138
7891
  // src/instrument/diff.ts
7139
- import { readFile as readFile12 } from "fs/promises";
7140
- import { relative } from "path";
7892
+ import { readFile as readFile14 } from "fs/promises";
7893
+ import { relative as relative2 } from "path";
7141
7894
  async function buildDiff(suggestions, opts = {}) {
7142
7895
  const byFile = /* @__PURE__ */ new Map();
7143
7896
  for (const s of suggestions) {
@@ -7150,7 +7903,7 @@ async function buildDiff(suggestions, opts = {}) {
7150
7903
  for (const [file, perFile] of byFile) {
7151
7904
  let source;
7152
7905
  try {
7153
- source = await readFile12(file, "utf8");
7906
+ source = await readFile14(file, "utf8");
7154
7907
  } catch (err) {
7155
7908
  for (const s of perFile) {
7156
7909
  skipped.push({
@@ -7186,7 +7939,7 @@ async function buildDiff(suggestions, opts = {}) {
7186
7939
  );
7187
7940
  }
7188
7941
  if (hunks.length === 0) continue;
7189
- const displayPath = opts.basePath ? relative(opts.basePath, file).replace(/\\/g, "/") : file;
7942
+ const displayPath = opts.basePath ? relative2(opts.basePath, file).replace(/\\/g, "/") : file;
7190
7943
  fileDiffs.push(`--- a/${displayPath}
7191
7944
  +++ b/${displayPath}
7192
7945
  ${hunks.join("\n")}`);
@@ -7206,7 +7959,7 @@ async function instrumentCommand(opts) {
7206
7959
  }
7207
7960
  throw err;
7208
7961
  }
7209
- const envFile = resolve25(loaded.configDir, ".env");
7962
+ const envFile = resolve26(loaded.configDir, ".env");
7210
7963
  try {
7211
7964
  process.loadEnvFile(envFile);
7212
7965
  } catch {
@@ -7221,13 +7974,13 @@ async function instrumentCommand(opts) {
7221
7974
  const filesToScan = /* @__PURE__ */ new Set();
7222
7975
  const IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
7223
7976
  async function walk(root, accept) {
7224
- const entries = await readdir3(root, { withFileTypes: true });
7977
+ const entries = await readdir4(root, { withFileTypes: true });
7225
7978
  for (const entry of entries) {
7226
- const abs = join14(root, entry.name);
7979
+ const abs = join15(root, entry.name);
7227
7980
  if (entry.isDirectory()) {
7228
7981
  if (IGNORED_DIRS.has(entry.name)) continue;
7229
7982
  await walk(abs, accept);
7230
- } else if (entry.isFile() && accept(relative2(root, abs))) {
7983
+ } else if (entry.isFile() && accept(relative3(root, abs))) {
7231
7984
  filesToScan.add(abs);
7232
7985
  }
7233
7986
  }
@@ -7238,7 +7991,7 @@ async function instrumentCommand(opts) {
7238
7991
  await walk(root, isJsxFile);
7239
7992
  } else {
7240
7993
  for (const src of codebaseSources) {
7241
- const root = isAbsolute13(src.path) ? src.path : resolve25(loaded.configDir, src.path);
7994
+ const root = isAbsolute14(src.path) ? src.path : resolve26(loaded.configDir, src.path);
7242
7995
  await walk(root, isJsxFile);
7243
7996
  }
7244
7997
  }
@@ -7279,9 +8032,9 @@ async function instrumentCommand(opts) {
7279
8032
  for (const s of skipped) {
7280
8033
  logger.warn(`Skipped suggestion at ${s.file}:${s.line} \u2014 ${s.reason}`);
7281
8034
  }
7282
- const outputPath = isAbsolute13(opts.output) ? opts.output : resolve25(loaded.configDir, opts.output);
8035
+ const outputPath = isAbsolute14(opts.output) ? opts.output : resolve26(loaded.configDir, opts.output);
7283
8036
  await mkdir16(dirname14(outputPath), { recursive: true });
7284
- await writeFile15(outputPath, patch, "utf8");
8037
+ await writeFile16(outputPath, patch, "utf8");
7285
8038
  logger.info("Wrote instrumentation patch", { path: outputPath, suggestions: suggestions.length });
7286
8039
  process.stdout.write(`
7287
8040
  Apply with: cd ${loaded.configDir} && git apply ${opts.output}
@@ -7291,8 +8044,8 @@ Apply with: cd ${loaded.configDir} && git apply ${opts.output}
7291
8044
 
7292
8045
  // src/commands/recordActions.ts
7293
8046
  import { createInterface as createInterface3 } from "readline/promises";
7294
- import { resolve as resolve26 } from "path";
7295
- import { stat as stat14 } from "fs/promises";
8047
+ import { resolve as resolve27 } from "path";
8048
+ import { stat as stat16 } from "fs/promises";
7296
8049
 
7297
8050
  // src/recording/captureEvents.ts
7298
8051
  async function installCaptureBinding(context, onEvent) {
@@ -7490,12 +8243,12 @@ async function recordActionsCommand(opts) {
7490
8243
  }
7491
8244
  throw err;
7492
8245
  }
7493
- const envFile = resolve26(loaded.configDir, ".env");
8246
+ const envFile = resolve27(loaded.configDir, ".env");
7494
8247
  try {
7495
8248
  process.loadEnvFile(envFile);
7496
8249
  } catch {
7497
8250
  }
7498
- const manifestPath = opts.output ? resolve26(loaded.configDir, opts.output) : resolve26(loaded.configDir, "./scripts/manifest.json");
8251
+ const manifestPath = opts.output ? resolve27(loaded.configDir, opts.output) : resolve27(loaded.configDir, "./scripts/manifest.json");
7499
8252
  logger.info("Launching headed browser for action capture", {
7500
8253
  target: loaded.config.recording.target_url
7501
8254
  });
@@ -7606,7 +8359,7 @@ async function mergeIntoManifest(opts) {
7606
8359
  }
7607
8360
  async function fileExists10(path) {
7608
8361
  try {
7609
- await stat14(path);
8362
+ await stat16(path);
7610
8363
  return true;
7611
8364
  } catch {
7612
8365
  return false;
@@ -7615,7 +8368,7 @@ async function fileExists10(path) {
7615
8368
 
7616
8369
  // src/cli.ts
7617
8370
  var program = new Command();
7618
- program.name("showrunner").description("Automated product demo recording & production tool").version("1.1.3").option("--json", "emit structured JSON logs to stdout").option("--log-level <level>", "log level (debug|info|warn|error)").hook("preAction", (thisCmd) => {
8371
+ program.name("showrunner").description("Automated product demo recording & production tool").version("1.1.5").option("--json", "emit structured JSON logs to stdout").option("--log-level <level>", "log level (debug|info|warn|error)").hook("preAction", (thisCmd) => {
7619
8372
  const opts = thisCmd.opts();
7620
8373
  if (opts.json) logger.setJson(true);
7621
8374
  if (opts.logLevel) logger.setLevel(opts.logLevel);
@@ -7635,7 +8388,7 @@ program.command("run").description("Run the full Showrunner pipeline").requiredO
7635
8388
  "--resolution <preset>",
7636
8389
  "override resolution for this run: low (854x480), standard (720p), high (1080p), extreme (4K)"
7637
8390
  ).action(runCommand2);
7638
- program.command("init").description("Scaffold a new Showrunner project").option("--name <name>", "project name", "showrunner-demo").option("--url <url>", "target URL of the product to demo", "http://localhost:3000").option("--dir <dir>", "parent directory in which to create the project", process.cwd()).option("--force", "overwrite an existing directory", false).option(
8391
+ program.command("init").description("Scaffold a new Showrunner project (interactive by default; use --yes to skip prompts)").option("--name <name>", "project name", "showrunner-demo").option("--url <url>", "target URL of the product to demo", "http://localhost:3000").option("--dir <dir>", "parent directory in which to create the project", process.cwd()).option("--force", "overwrite an existing directory", false).option("--yes", "skip interactive prompts and use defaults / passed flags", false).option(
7639
8392
  "--llm-provider <name>",
7640
8393
  "LLM provider for comprehension + script + instrument (anthropic | openai | agent_bridge)",
7641
8394
  "anthropic"
@@ -7648,6 +8401,7 @@ program.command("init").description("Scaffold a new Showrunner project").option(
7648
8401
  "scaffold resolution: low (854x480) | standard (720p) | high (1080p) | extreme (4K)",
7649
8402
  "standard"
7650
8403
  ).action(initCommand);
8404
+ program.command("set-target").description("Update demo.yaml's recording.target_url and re-probe it").requiredOption("-c, --config <path>", "path to demo.yaml").requiredOption("--url <url>", "new target URL (e.g. http://localhost:5173)").option("--force", "skip the reachability probe and update the URL anyway", false).action(setTargetCommand);
7651
8405
  program.command("install-browser").description("Install the Playwright browser binary (chromium by default) \u2014 wraps playwright-core install").option("--browser <name>", "browser to install: chromium | firefox | webkit", "chromium").action(installBrowserCommand);
7652
8406
  program.command("doctor").description("Run preflight checks on the current config + environment").requiredOption("-c, --config <path>", "path to demo.yaml").option("--json", "emit results as JSON instead of human-readable rows").action(doctorCommand);
7653
8407
  program.command("validate").description("Validate a demo.yaml config file").requiredOption("-c, --config <path>", "path to demo.yaml").option("--strict", "exit nonzero on any warning (e.g. missing provider env var)").action(validateCommand);
@@ -7666,17 +8420,17 @@ program.parseAsync(process.argv).catch((err) => {
7666
8420
  process.exit(1);
7667
8421
  });
7668
8422
  async function printWelcome() {
7669
- const { access: access4 } = await import("fs/promises");
7670
- const { resolve: resolve27 } = await import("path");
7671
- const demoYaml = resolve27(process.cwd(), "demo.yaml");
8423
+ const { access: access5 } = await import("fs/promises");
8424
+ const { resolve: resolve28 } = await import("path");
8425
+ const demoYaml = resolve28(process.cwd(), "demo.yaml");
7672
8426
  let inProject = false;
7673
8427
  try {
7674
- await access4(demoYaml);
8428
+ await access5(demoYaml);
7675
8429
  inProject = true;
7676
8430
  } catch {
7677
8431
  }
7678
8432
  const browserMissing = await isChromiumMissing();
7679
- const lines = ["", `Showrunner v1.1.3`, ""];
8433
+ const lines = ["", `Showrunner v1.1.5`, ""];
7680
8434
  if (browserMissing) {
7681
8435
  lines.push(`Showrunner records using Chromium. You haven't installed it yet.`);
7682
8436
  lines.push(``);
@@ -7704,8 +8458,8 @@ async function isChromiumMissing() {
7704
8458
  try {
7705
8459
  const { chromium: chromium6 } = await import("playwright-core");
7706
8460
  const exec = chromium6.executablePath();
7707
- const { stat: stat15 } = await import("fs/promises");
7708
- await stat15(exec);
8461
+ const { stat: stat17 } = await import("fs/promises");
8462
+ await stat17(exec);
7709
8463
  return false;
7710
8464
  } catch {
7711
8465
  return true;