@kadj-amoah/showrunner 1.1.4 → 1.1.6

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
@@ -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
@@ -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)
@@ -5596,7 +5596,7 @@ async function runCommand2(opts) {
5596
5596
 
5597
5597
  // src/commands/init.ts
5598
5598
  import { mkdir as mkdir11, writeFile as writeFile12, access as access3 } from "fs/promises";
5599
- import { dirname as dirname10, isAbsolute as isAbsolute8, join as join10, resolve as resolve15 } from "path";
5599
+ import { dirname as dirname10, isAbsolute as isAbsolute8, join as join11, resolve as resolve15 } from "path";
5600
5600
 
5601
5601
  // src/setup/detect.ts
5602
5602
  import { spawn as spawn4 } from "child_process";
@@ -5621,7 +5621,7 @@ async function detectEnvironment() {
5621
5621
  };
5622
5622
  }
5623
5623
  async function isOnPath(binary) {
5624
- return new Promise((resolve27) => {
5624
+ return new Promise((resolve28) => {
5625
5625
  const useShell = process.platform === "win32";
5626
5626
  const child = spawn4(binary, ["--version"], {
5627
5627
  stdio: ["ignore", "ignore", "ignore"],
@@ -5631,7 +5631,7 @@ async function isOnPath(binary) {
5631
5631
  const finish = (ok) => {
5632
5632
  if (settled) return;
5633
5633
  settled = true;
5634
- resolve27(ok);
5634
+ resolve28(ok);
5635
5635
  };
5636
5636
  child.on("error", () => finish(false));
5637
5637
  child.on("exit", (code) => finish(code === 0));
@@ -5696,6 +5696,234 @@ async function probeUrl(url, timeoutMs = DEFAULT_TIMEOUT_MS) {
5696
5696
  }
5697
5697
  }
5698
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
+
5699
5927
  // src/setup/wizard.ts
5700
5928
  var SLUG_RE = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/i;
5701
5929
  async function runWizard(env) {
@@ -5798,39 +6026,13 @@ async function runWizard(env) {
5798
6026
  if (key === null) return null;
5799
6027
  if (key.length > 0) collectedKeys["OPENAI_API_KEY"] = key;
5800
6028
  }
5801
- const url = await ask(
5802
- text({
5803
- message: "What URL is your product dev server on?",
5804
- placeholder: "http://localhost:3000",
5805
- defaultValue: "http://localhost:3000",
5806
- validate: (v) => {
5807
- if (!v) return void 0;
5808
- try {
5809
- new URL(v);
5810
- return void 0;
5811
- } catch {
5812
- return "Must be a valid URL (e.g. http://localhost:3000).";
5813
- }
5814
- }
5815
- })
5816
- );
5817
- if (url === null) return null;
5818
- const probeSpinner = spinner();
5819
- probeSpinner.start(`Probing ${url} ...`);
5820
- const probe = await probeUrl(url);
5821
- if (probe.reachable) {
5822
- probeSpinner.stop(`${url} reachable (HTTP ${probe.statusCode}, ${probe.elapsedMs}ms).`);
5823
- } else {
5824
- probeSpinner.stop(`${url} not reachable yet.`);
5825
- note(
5826
- `That's fine \u2014 you can start your dev server later. When it's up, run:
5827
-
5828
- showrunner set-target -c demo.yaml --url ${url}
5829
-
5830
- to re-probe and update the config.`,
5831
- "Heads up"
5832
- );
5833
- }
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;
5834
6036
  const proceed = await ask(
5835
6037
  confirm({
5836
6038
  message: `Scaffold ${projectName}/ with these choices?`,
@@ -5881,6 +6083,164 @@ function formatDetection(env) {
5881
6083
  lines.push(` ELEVENLABS_API_KEY: ${env.envVars.elevenlabs ? "set" : "unset"}`);
5882
6084
  return lines.join("\n");
5883
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
+ }
5884
6244
 
5885
6245
  // src/commands/init.ts
5886
6246
  var LLM_CHOICES = ["anthropic", "openai", "agent_bridge"];
@@ -5938,7 +6298,7 @@ async function initCommand(opts) {
5938
6298
  };
5939
6299
  }
5940
6300
  const parent = isAbsolute8(resolved.dir) ? resolved.dir : resolve15(process.cwd(), resolved.dir);
5941
- const projectRoot = join10(parent, resolved.name);
6301
+ const projectRoot = join11(parent, resolved.name);
5942
6302
  if (!resolved.force && await pathExists(projectRoot)) {
5943
6303
  logger.error(
5944
6304
  `Directory already exists: ${projectRoot}. Pass --force to overwrite, or choose a different --name/--dir.`
@@ -5947,28 +6307,28 @@ async function initCommand(opts) {
5947
6307
  }
5948
6308
  await mkdir11(projectRoot, { recursive: true });
5949
6309
  for (const rel of PLACEHOLDER_FILES) {
5950
- const dest = join10(projectRoot, rel);
6310
+ const dest = join11(projectRoot, rel);
5951
6311
  await mkdir11(dirname10(dest), { recursive: true });
5952
6312
  await writeFile12(dest, "", "utf8");
5953
6313
  }
5954
- await writeFile12(join10(projectRoot, "demo.yaml"), demoYamlTemplate(resolved), "utf8");
5955
- await writeFile12(join10(projectRoot, ".env.example"), envExampleTemplate(resolved), "utf8");
5956
- await writeFile12(join10(projectRoot, ".gitignore"), gitignoreTemplate(), "utf8");
5957
- await mkdir11(join10(projectRoot, "docs"), { recursive: true });
5958
- await writeFile12(join10(projectRoot, "docs/PRD.md"), prdStubTemplate(resolved), "utf8");
5959
- await writeFile12(join10(projectRoot, "scripts/manifest.json"), starterManifest(resolved), "utf8");
5960
- 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(), {
5961
6321
  mode: 493
5962
6322
  });
5963
- await writeFile12(join10(projectRoot, "scripts/reset_demo_data.sh"), resetScript(), {
6323
+ await writeFile12(join11(projectRoot, "scripts/reset_demo_data.sh"), resetScript(), {
5964
6324
  mode: 493
5965
6325
  });
5966
- await writeFile12(join10(projectRoot, "scripts/teardown.sh"), teardownScript(), {
6326
+ await writeFile12(join11(projectRoot, "scripts/teardown.sh"), teardownScript(), {
5967
6327
  mode: 493
5968
6328
  });
5969
- await writeFile12(join10(projectRoot, "README.md"), readmeTemplate(resolved), "utf8");
6329
+ await writeFile12(join11(projectRoot, "README.md"), readmeTemplate(resolved), "utf8");
5970
6330
  if (Object.keys(collectedKeys).length > 0) {
5971
- await writeFile12(join10(projectRoot, ".env"), buildEnvFile(collectedKeys, resolved), "utf8");
6331
+ await writeFile12(join11(projectRoot, ".env"), buildEnvFile(collectedKeys, resolved), "utf8");
5972
6332
  }
5973
6333
  logger.info(`Showrunner project scaffolded at ${projectRoot} (llm=${resolved.llm}, tts=${resolved.tts})`);
5974
6334
  printNextSteps(resolved.name, resolved, collectedKeys);
@@ -6469,22 +6829,22 @@ and \`at_word\` actions degrade to \`at\`).
6469
6829
  }
6470
6830
 
6471
6831
  // src/commands/installBrowser.ts
6472
- import { spawn as spawn5 } from "child_process";
6832
+ import { spawn as spawn6 } from "child_process";
6473
6833
  import { access as access4 } from "fs/promises";
6474
6834
  import { fileURLToPath } from "url";
6475
- import { dirname as dirname11, join as join11 } from "path";
6835
+ import { dirname as dirname11, join as join12 } from "path";
6476
6836
  var DEFAULT_BROWSER = "chromium";
6477
6837
  async function installBrowserCommand(opts) {
6478
6838
  const browser = opts.browser ?? DEFAULT_BROWSER;
6479
6839
  const cli = await resolvePlaywrightCoreCli();
6480
6840
  logger.info(`installing Playwright ${browser} (via bundled playwright-core, no project required)`);
6481
- const child = spawn5(process.execPath, [cli, "install", browser], {
6841
+ const child = spawn6(process.execPath, [cli, "install", browser], {
6482
6842
  stdio: "inherit",
6483
6843
  env: process.env
6484
6844
  });
6485
- const code = await new Promise((resolve27, reject) => {
6845
+ const code = await new Promise((resolve28, reject) => {
6486
6846
  child.on("error", reject);
6487
- child.on("close", (c) => resolve27(c ?? 0));
6847
+ child.on("close", (c) => resolve28(c ?? 0));
6488
6848
  });
6489
6849
  if (code !== 0) {
6490
6850
  logger.error(`playwright install exited with code ${code}`);
@@ -6497,7 +6857,7 @@ async function resolvePlaywrightCoreCli() {
6497
6857
  let dir = dirname11(here);
6498
6858
  const root = dir.split(/[\\/]/)[0] + "/";
6499
6859
  while (dir && dir !== root) {
6500
- const candidate = join11(dir, "node_modules", "playwright-core", "cli.js");
6860
+ const candidate = join12(dir, "node_modules", "playwright-core", "cli.js");
6501
6861
  try {
6502
6862
  await access4(candidate);
6503
6863
  return candidate;
@@ -6512,9 +6872,60 @@ async function resolvePlaywrightCoreCli() {
6512
6872
  );
6513
6873
  }
6514
6874
 
6515
- // src/commands/validate.ts
6516
- import { stat as stat11 } from "fs/promises";
6875
+ // src/commands/setTarget.ts
6876
+ import { readFile as readFile11, writeFile as writeFile13 } from "fs/promises";
6517
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";
6518
6929
  async function validateCommand(opts) {
6519
6930
  try {
6520
6931
  process.loadEnvFile?.();
@@ -6549,9 +6960,9 @@ async function checkReferencedPaths(config, configDir) {
6549
6960
  const warnings = [];
6550
6961
  const check = async (relPath, label) => {
6551
6962
  if (!relPath) return;
6552
- const abs = isAbsolute9(relPath) ? relPath : resolve16(configDir, relPath);
6963
+ const abs = isAbsolute10(relPath) ? relPath : resolve17(configDir, relPath);
6553
6964
  try {
6554
- await stat11(abs);
6965
+ await stat12(abs);
6555
6966
  } catch {
6556
6967
  warnings.push(`${label} not found: ${abs}`);
6557
6968
  }
@@ -6579,7 +6990,7 @@ async function checkReferencedPaths(config, configDir) {
6579
6990
  }
6580
6991
 
6581
6992
  // src/commands/printVo.ts
6582
- import { resolve as resolve17 } from "path";
6993
+ import { resolve as resolve18 } from "path";
6583
6994
  async function printVoCommand(opts) {
6584
6995
  let loaded;
6585
6996
  try {
@@ -6591,7 +7002,7 @@ async function printVoCommand(opts) {
6591
7002
  }
6592
7003
  throw err;
6593
7004
  }
6594
- const manifestPath = resolve17(loaded.configDir, "./scripts/manifest.json");
7005
+ const manifestPath = resolve18(loaded.configDir, "./scripts/manifest.json");
6595
7006
  let manifest;
6596
7007
  try {
6597
7008
  manifest = await readManifest(manifestPath);
@@ -6607,8 +7018,8 @@ async function printVoCommand(opts) {
6607
7018
  }
6608
7019
 
6609
7020
  // src/commands/approveVo.ts
6610
- import { rm as rm2, stat as stat12 } from "fs/promises";
6611
- import { resolve as resolve18 } from "path";
7021
+ import { rm as rm2, stat as stat13 } from "fs/promises";
7022
+ import { resolve as resolve19 } from "path";
6612
7023
  async function approveVoCommand(opts) {
6613
7024
  let loaded;
6614
7025
  try {
@@ -6620,9 +7031,9 @@ async function approveVoCommand(opts) {
6620
7031
  }
6621
7032
  throw err;
6622
7033
  }
6623
- const manifestPath = resolve18(loaded.configDir, "./scripts/manifest.json");
6624
- const voScriptPath = resolve18(loaded.configDir, "./scripts/vo_script.txt");
6625
- 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");
6626
7037
  let manifest;
6627
7038
  try {
6628
7039
  manifest = await readManifest(manifestPath);
@@ -6655,7 +7066,7 @@ async function approveVoCommand(opts) {
6655
7066
  }
6656
7067
  async function pathExists2(p) {
6657
7068
  try {
6658
- await stat12(p);
7069
+ await stat13(p);
6659
7070
  return true;
6660
7071
  } catch {
6661
7072
  return false;
@@ -6663,8 +7074,8 @@ async function pathExists2(p) {
6663
7074
  }
6664
7075
 
6665
7076
  // src/commands/rerunSegment.ts
6666
- import { mkdir as mkdir12, rename as rename3, writeFile as writeFile13 } from "fs/promises";
6667
- 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";
6668
7079
  import { chromium as chromium4, firefox as firefox4, webkit as webkit4 } from "playwright-core";
6669
7080
  var RERUN_BUFFER_MS = 500;
6670
7081
  var browserMap4 = { chromium: chromium4, firefox: firefox4, webkit: webkit4 };
@@ -6680,8 +7091,8 @@ async function rerunSegmentCommand(opts) {
6680
7091
  throw err;
6681
7092
  }
6682
7093
  const { config, configDir } = loaded;
6683
- const manifestPath = resolve19(configDir, "./scripts/manifest.json");
6684
- 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);
6685
7096
  let manifest;
6686
7097
  try {
6687
7098
  manifest = await readManifest(manifestPath);
@@ -6781,9 +7192,9 @@ async function rerunSegmentCommand(opts) {
6781
7192
  }
6782
7193
  }
6783
7194
  await page.waitForTimeout(config.recording.segment_buffer_ms);
6784
- const traceDir = resolve19(configDir, config.recording.trace_dir);
7195
+ const traceDir = resolve20(configDir, config.recording.trace_dir);
6785
7196
  await mkdir12(traceDir, { recursive: true });
6786
- await ctx.tracing.stopChunk({ path: join12(traceDir, `${segment.id}.zip`) });
7197
+ await ctx.tracing.stopChunk({ path: join13(traceDir, `${segment.id}.zip`) });
6787
7198
  const videoHandle = page.video();
6788
7199
  await ctx.close();
6789
7200
  if (!videoHandle) {
@@ -6791,10 +7202,10 @@ async function rerunSegmentCommand(opts) {
6791
7202
  process.exit(1);
6792
7203
  }
6793
7204
  const original = await videoHandle.path();
6794
- const dest = join12(videoDir, `${segment.id}.webm`);
7205
+ const dest = join13(videoDir, `${segment.id}.webm`);
6795
7206
  await rename3(original, dest);
6796
- const metadataPath = join12(videoDir, `${segment.id}.rerun.json`);
6797
- await writeFile13(
7207
+ const metadataPath = join13(videoDir, `${segment.id}.rerun.json`);
7208
+ await writeFile14(
6798
7209
  metadataPath,
6799
7210
  JSON.stringify(
6800
7211
  {
@@ -6822,12 +7233,12 @@ async function rerunSegmentCommand(opts) {
6822
7233
 
6823
7234
  // src/commands/captureAuth.ts
6824
7235
  import { mkdir as mkdir14 } from "fs/promises";
6825
- 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";
6826
7237
  import { createInterface } from "readline/promises";
6827
7238
 
6828
7239
  // src/recording/headed.ts
6829
7240
  import { mkdir as mkdir13 } from "fs/promises";
6830
- import { resolve as resolve20 } from "path";
7241
+ import { resolve as resolve21 } from "path";
6831
7242
  import {
6832
7243
  chromium as chromium5,
6833
7244
  firefox as firefox5,
@@ -6841,7 +7252,7 @@ async function launchHeadedSession(opts) {
6841
7252
  viewport: { width: recording.viewport.width, height: recording.viewport.height }
6842
7253
  };
6843
7254
  if (opts.recordVideo) {
6844
- const videoDir = resolve20(configDir, recording.output_dir);
7255
+ const videoDir = resolve21(configDir, recording.output_dir);
6845
7256
  await mkdir13(videoDir, { recursive: true });
6846
7257
  contextOptions.recordVideo = {
6847
7258
  dir: videoDir,
@@ -6902,13 +7313,13 @@ async function captureAuthCommand(opts) {
6902
7313
  }
6903
7314
  throw err;
6904
7315
  }
6905
- const envFile = resolve21(loaded.configDir, ".env");
7316
+ const envFile = resolve22(loaded.configDir, ".env");
6906
7317
  try {
6907
7318
  process.loadEnvFile(envFile);
6908
7319
  } catch {
6909
7320
  }
6910
7321
  const cookiesRel = opts.outputCookies ?? "./auth/session.json";
6911
- const cookiesPath = isAbsolute10(cookiesRel) ? cookiesRel : resolve21(loaded.configDir, cookiesRel);
7322
+ const cookiesPath = isAbsolute11(cookiesRel) ? cookiesRel : resolve22(loaded.configDir, cookiesRel);
6912
7323
  await mkdir14(dirname12(cookiesPath), { recursive: true });
6913
7324
  logger.info("Launching headed browser for auth capture", {
6914
7325
  target: loaded.config.recording.target_url,
@@ -6949,8 +7360,8 @@ Then re-run \`showrunner run --config <demo.yaml>\`.
6949
7360
  }
6950
7361
 
6951
7362
  // src/commands/trace.ts
6952
- import { readdir as readdir2, stat as stat13 } from "fs/promises";
6953
- 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";
6954
7365
  async function traceCommand(opts) {
6955
7366
  let loaded;
6956
7367
  try {
@@ -6962,9 +7373,9 @@ async function traceCommand(opts) {
6962
7373
  }
6963
7374
  throw err;
6964
7375
  }
6965
- const traceDir = resolve22(loaded.configDir, loaded.config.recording.trace_dir);
6966
- const videoDir = resolve22(loaded.configDir, loaded.config.recording.output_dir);
6967
- 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");
6968
7379
  if (opts.all) {
6969
7380
  let plan;
6970
7381
  try {
@@ -6975,14 +7386,14 @@ async function traceCommand(opts) {
6975
7386
  process.exit(1);
6976
7387
  }
6977
7388
  for (const seg of plan.segments) {
6978
- 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`);
6979
7390
  logger.info(`Opening trace for ${seg.id}`, { path: tracePath });
6980
7391
  await openTrace(tracePath);
6981
7392
  }
6982
7393
  return;
6983
7394
  }
6984
7395
  if (opts.segment) {
6985
- const tracePath = join13(traceDir, `${opts.segment}.zip`);
7396
+ const tracePath = join14(traceDir, `${opts.segment}.zip`);
6986
7397
  if (!await fileExists8(tracePath)) {
6987
7398
  logger.error(`No trace found at ${tracePath}`);
6988
7399
  process.exit(1);
@@ -7023,7 +7434,7 @@ async function openTrace(tracePath) {
7023
7434
  }
7024
7435
  async function listTraces(dir) {
7025
7436
  try {
7026
- const entries = await readdir2(dir);
7437
+ const entries = await readdir3(dir);
7027
7438
  return entries.filter((e) => e.endsWith(".zip")).sort();
7028
7439
  } catch {
7029
7440
  return [];
@@ -7031,7 +7442,7 @@ async function listTraces(dir) {
7031
7442
  }
7032
7443
  async function fileExists8(path) {
7033
7444
  try {
7034
- await stat13(path);
7445
+ await stat14(path);
7035
7446
  return true;
7036
7447
  } catch {
7037
7448
  return false;
@@ -7039,8 +7450,8 @@ async function fileExists8(path) {
7039
7450
  }
7040
7451
 
7041
7452
  // src/commands/preview.ts
7042
- import { stat as stat14 } from "fs/promises";
7043
- import { resolve as resolve23 } from "path";
7453
+ import { stat as stat15 } from "fs/promises";
7454
+ import { resolve as resolve24 } from "path";
7044
7455
  async function previewCommand(opts) {
7045
7456
  let loaded;
7046
7457
  try {
@@ -7052,7 +7463,7 @@ async function previewCommand(opts) {
7052
7463
  }
7053
7464
  throw err;
7054
7465
  }
7055
- const previewSpec = resolve23(loaded.configDir, "./scripts/playwright_demo.spec.ts");
7466
+ const previewSpec = resolve24(loaded.configDir, "./scripts/playwright_demo.spec.ts");
7056
7467
  if (!await fileExists9(previewSpec)) {
7057
7468
  logger.error(
7058
7469
  `No preview spec at ${previewSpec}. Run \`showrunner run --stages script\` first to generate it.`
@@ -7078,7 +7489,7 @@ async function previewCommand(opts) {
7078
7489
  }
7079
7490
  async function fileExists9(path) {
7080
7491
  try {
7081
- await stat14(path);
7492
+ await stat15(path);
7082
7493
  return true;
7083
7494
  } catch {
7084
7495
  return false;
@@ -7086,8 +7497,8 @@ async function fileExists9(path) {
7086
7497
  }
7087
7498
 
7088
7499
  // src/commands/understand.ts
7089
- import { mkdir as mkdir15, readFile as readFile10, writeFile as writeFile14 } from "fs/promises";
7090
- 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";
7091
7502
 
7092
7503
  // src/productModel/prompts.ts
7093
7504
  var PRODUCT_MODEL_SYSTEM_PROMPT = `You build product_model.json for Showrunner, an automated product demo recorder.
@@ -7176,6 +7587,71 @@ Regenerate the product_model strictly matching the schema.`
7176
7587
  }
7177
7588
  }
7178
7589
 
7590
+ // src/productModel/generateViaAgent.ts
7591
+ var DEFAULT_MAX_TOKENS5 = 8e3;
7592
+ var AGENT_SYSTEM_PROMPT = [
7593
+ "You are helping Showrunner build a product_model.json for a product demo recording.",
7594
+ "",
7595
+ "You have read-only filesystem tools (Read, Glob, Grep) and can explore the project freely.",
7596
+ "Use them \u2014 do NOT guess from the prompt alone.",
7597
+ "",
7598
+ "What to look for, in order:",
7599
+ " 1. package.json (or pyproject.toml, Cargo.toml, etc.) \u2014 name, description, scripts, deps",
7600
+ " 2. README.md / README.* at the project root \u2014 product description, primary user, features",
7601
+ " 3. docs/PRD.md or similar \u2014 explicit product brief if present",
7602
+ " 4. src/ tree (sample a few files) \u2014 what the product actually DOES, names of routes/pages/components",
7603
+ " 5. .env.example \u2014 hints at integrations and external services",
7604
+ " 6. Any framework config (next.config, vite.config, astro.config, etc.) \u2014 confirms what kind of app this is",
7605
+ "",
7606
+ "When you have enough signal, synthesize a product_model.json matching the requested schema.",
7607
+ "",
7608
+ "Rules:",
7609
+ ' - product_name: short, exact product name as branded. Take this from package.json "name" only if it looks human, else from README.',
7610
+ " - tagline: one sentence, 8\u201315 words, plain language. Derived from README/PRD, not invented.",
7611
+ ` - primary_user: one short phrase. Inferred from the README's "who is this for" framing if explicit, else from the product's shape.`,
7612
+ " - core_flows: 2\u20134 user flows. Each has id (kebab-case), name, 3\u20136 imperative steps from the user's POV, optional entry_url. Ground these in the actual routes/pages the codebase exposes.",
7613
+ " - key_features: 3\u20136 short bullets. No marketing fluff.",
7614
+ " - demo_recommendation.suggested_flows: ids from core_flows, in demo order.",
7615
+ " - demo_recommendation.suggested_duration_seconds: 60\u201390 typical.",
7616
+ " - confidence: 'high' if you found explicit, clear sources; 'medium' if you had to infer; 'low' if the codebase was sparse and you mostly guessed.",
7617
+ " - source: always 'documents' (this path counts as document-driven, just agentically discovered).",
7618
+ " - generated_at: ISO-8601 timestamp.",
7619
+ "",
7620
+ "Output exactly the structured JSON. No prose, no commentary, no markdown fence outside the JSON."
7621
+ ].join("\n");
7622
+ async function generateProductModelViaAgent(opts) {
7623
+ const provider = new AgentBridgeLLMProvider({
7624
+ mode: "spawn",
7625
+ command: opts.command ?? "claude",
7626
+ args: opts.args ?? ["-p", "--output-format", "json"],
7627
+ timeoutMs: opts.timeoutMs ?? 18e4,
7628
+ cwd: opts.projectDir
7629
+ });
7630
+ const userPrompt = [
7631
+ `Project directory: ${opts.projectDir}`,
7632
+ "",
7633
+ "Explore this directory with your filesystem tools, then synthesize a product_model.json."
7634
+ ].join("\n");
7635
+ logger.debug("Generating product_model via agent", { projectDir: opts.projectDir });
7636
+ try {
7637
+ return await generateWithRetry(provider, {
7638
+ systemPrompt: AGENT_SYSTEM_PROMPT,
7639
+ userPrompt,
7640
+ schema: productModelSchema,
7641
+ schemaName: "product_model",
7642
+ maxTokens: DEFAULT_MAX_TOKENS5,
7643
+ retryRenderer: (errorText, prevUserPrompt) => `${prevUserPrompt}
7644
+
7645
+ Your previous output failed validation with this error:
7646
+ ${errorText}
7647
+
7648
+ Regenerate the product_model strictly matching the schema.`
7649
+ });
7650
+ } catch (err) {
7651
+ throw new ProductModelGenerationError(err instanceof Error ? err.message : String(err));
7652
+ }
7653
+ }
7654
+
7179
7655
  // src/productModel/interactive.ts
7180
7656
  import { createInterface as createInterface2 } from "readline";
7181
7657
  var LineReader = class {
@@ -7211,8 +7687,8 @@ var LineReader = class {
7211
7687
  if (this.closed) {
7212
7688
  return Promise.reject(new Error("stdin closed before answer was provided"));
7213
7689
  }
7214
- return new Promise((resolve27, reject) => {
7215
- this.waiter = resolve27;
7690
+ return new Promise((resolve28, reject) => {
7691
+ this.waiter = resolve28;
7216
7692
  this.rejecter = reject;
7217
7693
  });
7218
7694
  }
@@ -7264,6 +7740,10 @@ function clamp(n, lo, hi) {
7264
7740
 
7265
7741
  // src/commands/understand.ts
7266
7742
  async function understandCommand(opts) {
7743
+ if (opts.interactive && opts.agent) {
7744
+ logger.error("--interactive and --agent are mutually exclusive. Pick one.");
7745
+ process.exit(2);
7746
+ }
7267
7747
  let configDir = process.cwd();
7268
7748
  let outputRel = opts.output ?? "./product_model.json";
7269
7749
  let sources = [];
@@ -7285,33 +7765,37 @@ async function understandCommand(opts) {
7285
7765
  }
7286
7766
  sources = loaded.config.comprehension.sources.map((s) => ({ path: s.path, type: s.type }));
7287
7767
  llmConfig = loaded.config.llm;
7288
- const envFile = resolve24(loaded.configDir, ".env");
7768
+ const envFile = resolve25(loaded.configDir, ".env");
7289
7769
  try {
7290
7770
  process.loadEnvFile(envFile);
7291
7771
  } catch {
7292
7772
  }
7293
7773
  }
7294
- const outputPath = isAbsolute12(outputRel) ? outputRel : resolve24(configDir, outputRel);
7774
+ const outputPath = isAbsolute13(outputRel) ? outputRel : resolve25(configDir, outputRel);
7295
7775
  await mkdir15(dirname13(outputPath), { recursive: true });
7296
7776
  let productModel;
7297
- const provider = resolveDefaultLLMProvider({ configDir, llm: llmConfig });
7298
7777
  try {
7299
- if (opts.interactive) {
7778
+ if (opts.agent) {
7779
+ const projectDir = process.cwd();
7780
+ logger.info("Generating product_model via local `claude` agent", { projectDir });
7781
+ productModel = await generateProductModelViaAgent({ projectDir });
7782
+ } else if (opts.interactive) {
7300
7783
  const answers = await runInteractiveQA();
7301
7784
  logger.info("Generating product_model from interactive answers");
7785
+ const provider = resolveDefaultLLMProvider({ configDir, llm: llmConfig });
7302
7786
  productModel = await generateProductModelFromInteractive({ answers, provider });
7303
7787
  } else {
7304
7788
  if (sources.length === 0) {
7305
7789
  logger.error(
7306
- "No `comprehension.sources` configured in demo.yaml. Add at least one source (prd, readme, codebase, etc.) or re-run with --interactive."
7790
+ "No `comprehension.sources` configured in demo.yaml. Add at least one source (prd, readme, codebase, etc.), re-run with --interactive (Q&A), or re-run with --agent (the local `claude` CLI explores your repo)."
7307
7791
  );
7308
7792
  process.exit(2);
7309
7793
  }
7310
7794
  const docs = [];
7311
7795
  for (const src of sources) {
7312
- const abs = isAbsolute12(src.path) ? src.path : resolve24(configDir, src.path);
7796
+ const abs = isAbsolute13(src.path) ? src.path : resolve25(configDir, src.path);
7313
7797
  try {
7314
- const content = await readFile10(abs, "utf8");
7798
+ const content = await readFile12(abs, "utf8");
7315
7799
  docs.push({ path: src.path, type: src.type, content });
7316
7800
  } catch (err) {
7317
7801
  const cause = err instanceof Error ? err.message : String(err);
@@ -7325,6 +7809,7 @@ async function understandCommand(opts) {
7325
7809
  logger.info("Generating product_model from documents", {
7326
7810
  sources: docs.map((d) => d.path)
7327
7811
  });
7812
+ const provider = resolveDefaultLLMProvider({ configDir, llm: llmConfig });
7328
7813
  productModel = await generateProductModelFromDocs({ sources: docs, provider });
7329
7814
  }
7330
7815
  } catch (err) {
@@ -7334,16 +7819,16 @@ async function understandCommand(opts) {
7334
7819
  }
7335
7820
  throw err;
7336
7821
  }
7337
- await writeFile14(outputPath, JSON.stringify(productModel, null, 2) + "\n", "utf8");
7822
+ await writeFile15(outputPath, JSON.stringify(productModel, null, 2) + "\n", "utf8");
7338
7823
  logger.info("Wrote product_model.json", { path: outputPath });
7339
7824
  }
7340
7825
 
7341
7826
  // src/commands/instrument.ts
7342
- import { mkdir as mkdir16, readdir as readdir3, writeFile as writeFile15 } from "fs/promises";
7343
- import { dirname as dirname14, isAbsolute as isAbsolute13, join as join14, resolve as resolve25, relative as relative2 } from "path";
7827
+ import { mkdir as mkdir16, readdir as readdir4, writeFile as writeFile16 } from "fs/promises";
7828
+ import { dirname as dirname14, isAbsolute as isAbsolute14, join as join15, resolve as resolve26, relative as relative3 } from "path";
7344
7829
 
7345
7830
  // src/instrument/scan.ts
7346
- import { readFile as readFile11 } from "fs/promises";
7831
+ import { readFile as readFile13 } from "fs/promises";
7347
7832
  import { parse } from "@babel/parser";
7348
7833
  import _traverse from "@babel/traverse";
7349
7834
  var traverse = _traverse.default ?? _traverse;
@@ -7361,7 +7846,7 @@ var TARGET_TAGS = /* @__PURE__ */ new Set([
7361
7846
  "Form"
7362
7847
  ]);
7363
7848
  async function scanFile(filePath) {
7364
- const source = await readFile11(filePath, "utf8");
7849
+ const source = await readFile13(filePath, "utf8");
7365
7850
  let ast;
7366
7851
  try {
7367
7852
  ast = parse(source, {
@@ -7420,23 +7905,23 @@ function pickAttr(node, attr) {
7420
7905
  }
7421
7906
 
7422
7907
  // src/instrument/suggest.ts
7423
- import { z as z9 } from "zod";
7424
- var DEFAULT_MAX_TOKENS5 = 8e3;
7425
- var suggestionSchema = z9.object({
7426
- suggestions: z9.array(
7427
- z9.object({
7428
- file: z9.string(),
7429
- line: z9.number().int().positive(),
7430
- original: z9.string(),
7431
- replacement: z9.string(),
7432
- reasoning: z9.string().default("")
7908
+ import { z as z10 } from "zod";
7909
+ var DEFAULT_MAX_TOKENS6 = 8e3;
7910
+ var suggestionSchema = z10.object({
7911
+ suggestions: z10.array(
7912
+ z10.object({
7913
+ file: z10.string(),
7914
+ line: z10.number().int().positive(),
7915
+ original: z10.string(),
7916
+ replacement: z10.string(),
7917
+ reasoning: z10.string().default("")
7433
7918
  })
7434
7919
  )
7435
7920
  });
7436
7921
  var InstrumentSuggestionError = class extends Error {
7437
7922
  name = "InstrumentSuggestionError";
7438
7923
  };
7439
- var SYSTEM_PROMPT = `You add data-testid attributes to JSX elements so they can be reliably targeted by automated demo tools.
7924
+ var SYSTEM_PROMPT2 = `You add data-testid attributes to JSX elements so they can be reliably targeted by automated demo tools.
7440
7925
 
7441
7926
  Rules:
7442
7927
  - 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".
@@ -7463,11 +7948,11 @@ For each, return {file, line, original (exact source line), replacement (source
7463
7948
  });
7464
7949
  try {
7465
7950
  const result = await opts.provider.generateStructured({
7466
- systemPrompt: SYSTEM_PROMPT,
7951
+ systemPrompt: SYSTEM_PROMPT2,
7467
7952
  userPrompt,
7468
7953
  schema: suggestionSchema,
7469
7954
  schemaName: "instrument_suggestions",
7470
- maxTokens: DEFAULT_MAX_TOKENS5
7955
+ maxTokens: DEFAULT_MAX_TOKENS6
7471
7956
  });
7472
7957
  return result.suggestions;
7473
7958
  } catch (err) {
@@ -7478,8 +7963,8 @@ For each, return {file, line, original (exact source line), replacement (source
7478
7963
  }
7479
7964
 
7480
7965
  // src/instrument/diff.ts
7481
- import { readFile as readFile12 } from "fs/promises";
7482
- import { relative } from "path";
7966
+ import { readFile as readFile14 } from "fs/promises";
7967
+ import { relative as relative2 } from "path";
7483
7968
  async function buildDiff(suggestions, opts = {}) {
7484
7969
  const byFile = /* @__PURE__ */ new Map();
7485
7970
  for (const s of suggestions) {
@@ -7492,7 +7977,7 @@ async function buildDiff(suggestions, opts = {}) {
7492
7977
  for (const [file, perFile] of byFile) {
7493
7978
  let source;
7494
7979
  try {
7495
- source = await readFile12(file, "utf8");
7980
+ source = await readFile14(file, "utf8");
7496
7981
  } catch (err) {
7497
7982
  for (const s of perFile) {
7498
7983
  skipped.push({
@@ -7528,7 +8013,7 @@ async function buildDiff(suggestions, opts = {}) {
7528
8013
  );
7529
8014
  }
7530
8015
  if (hunks.length === 0) continue;
7531
- const displayPath = opts.basePath ? relative(opts.basePath, file).replace(/\\/g, "/") : file;
8016
+ const displayPath = opts.basePath ? relative2(opts.basePath, file).replace(/\\/g, "/") : file;
7532
8017
  fileDiffs.push(`--- a/${displayPath}
7533
8018
  +++ b/${displayPath}
7534
8019
  ${hunks.join("\n")}`);
@@ -7548,7 +8033,7 @@ async function instrumentCommand(opts) {
7548
8033
  }
7549
8034
  throw err;
7550
8035
  }
7551
- const envFile = resolve25(loaded.configDir, ".env");
8036
+ const envFile = resolve26(loaded.configDir, ".env");
7552
8037
  try {
7553
8038
  process.loadEnvFile(envFile);
7554
8039
  } catch {
@@ -7563,13 +8048,13 @@ async function instrumentCommand(opts) {
7563
8048
  const filesToScan = /* @__PURE__ */ new Set();
7564
8049
  const IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
7565
8050
  async function walk(root, accept) {
7566
- const entries = await readdir3(root, { withFileTypes: true });
8051
+ const entries = await readdir4(root, { withFileTypes: true });
7567
8052
  for (const entry of entries) {
7568
- const abs = join14(root, entry.name);
8053
+ const abs = join15(root, entry.name);
7569
8054
  if (entry.isDirectory()) {
7570
8055
  if (IGNORED_DIRS.has(entry.name)) continue;
7571
8056
  await walk(abs, accept);
7572
- } else if (entry.isFile() && accept(relative2(root, abs))) {
8057
+ } else if (entry.isFile() && accept(relative3(root, abs))) {
7573
8058
  filesToScan.add(abs);
7574
8059
  }
7575
8060
  }
@@ -7580,7 +8065,7 @@ async function instrumentCommand(opts) {
7580
8065
  await walk(root, isJsxFile);
7581
8066
  } else {
7582
8067
  for (const src of codebaseSources) {
7583
- const root = isAbsolute13(src.path) ? src.path : resolve25(loaded.configDir, src.path);
8068
+ const root = isAbsolute14(src.path) ? src.path : resolve26(loaded.configDir, src.path);
7584
8069
  await walk(root, isJsxFile);
7585
8070
  }
7586
8071
  }
@@ -7621,9 +8106,9 @@ async function instrumentCommand(opts) {
7621
8106
  for (const s of skipped) {
7622
8107
  logger.warn(`Skipped suggestion at ${s.file}:${s.line} \u2014 ${s.reason}`);
7623
8108
  }
7624
- const outputPath = isAbsolute13(opts.output) ? opts.output : resolve25(loaded.configDir, opts.output);
8109
+ const outputPath = isAbsolute14(opts.output) ? opts.output : resolve26(loaded.configDir, opts.output);
7625
8110
  await mkdir16(dirname14(outputPath), { recursive: true });
7626
- await writeFile15(outputPath, patch, "utf8");
8111
+ await writeFile16(outputPath, patch, "utf8");
7627
8112
  logger.info("Wrote instrumentation patch", { path: outputPath, suggestions: suggestions.length });
7628
8113
  process.stdout.write(`
7629
8114
  Apply with: cd ${loaded.configDir} && git apply ${opts.output}
@@ -7633,8 +8118,8 @@ Apply with: cd ${loaded.configDir} && git apply ${opts.output}
7633
8118
 
7634
8119
  // src/commands/recordActions.ts
7635
8120
  import { createInterface as createInterface3 } from "readline/promises";
7636
- import { resolve as resolve26 } from "path";
7637
- import { stat as stat15 } from "fs/promises";
8121
+ import { resolve as resolve27 } from "path";
8122
+ import { stat as stat16 } from "fs/promises";
7638
8123
 
7639
8124
  // src/recording/captureEvents.ts
7640
8125
  async function installCaptureBinding(context, onEvent) {
@@ -7832,12 +8317,12 @@ async function recordActionsCommand(opts) {
7832
8317
  }
7833
8318
  throw err;
7834
8319
  }
7835
- const envFile = resolve26(loaded.configDir, ".env");
8320
+ const envFile = resolve27(loaded.configDir, ".env");
7836
8321
  try {
7837
8322
  process.loadEnvFile(envFile);
7838
8323
  } catch {
7839
8324
  }
7840
- const manifestPath = opts.output ? resolve26(loaded.configDir, opts.output) : resolve26(loaded.configDir, "./scripts/manifest.json");
8325
+ const manifestPath = opts.output ? resolve27(loaded.configDir, opts.output) : resolve27(loaded.configDir, "./scripts/manifest.json");
7841
8326
  logger.info("Launching headed browser for action capture", {
7842
8327
  target: loaded.config.recording.target_url
7843
8328
  });
@@ -7948,7 +8433,7 @@ async function mergeIntoManifest(opts) {
7948
8433
  }
7949
8434
  async function fileExists10(path) {
7950
8435
  try {
7951
- await stat15(path);
8436
+ await stat16(path);
7952
8437
  return true;
7953
8438
  } catch {
7954
8439
  return false;
@@ -7957,7 +8442,7 @@ async function fileExists10(path) {
7957
8442
 
7958
8443
  // src/cli.ts
7959
8444
  var program = new Command();
7960
- program.name("showrunner").description("Automated product demo recording & production tool").version("1.1.4").option("--json", "emit structured JSON logs to stdout").option("--log-level <level>", "log level (debug|info|warn|error)").hook("preAction", (thisCmd) => {
8445
+ program.name("showrunner").description("Automated product demo recording & production tool").version("1.1.6").option("--json", "emit structured JSON logs to stdout").option("--log-level <level>", "log level (debug|info|warn|error)").hook("preAction", (thisCmd) => {
7961
8446
  const opts = thisCmd.opts();
7962
8447
  if (opts.json) logger.setJson(true);
7963
8448
  if (opts.logLevel) logger.setLevel(opts.logLevel);
@@ -7990,10 +8475,14 @@ program.command("init").description("Scaffold a new Showrunner project (interact
7990
8475
  "scaffold resolution: low (854x480) | standard (720p) | high (1080p) | extreme (4K)",
7991
8476
  "standard"
7992
8477
  ).action(initCommand);
8478
+ 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);
7993
8479
  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);
7994
8480
  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);
7995
8481
  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);
7996
- program.command("understand").description("Build product_model.json from documents or interactive Q&A").option("-c, --config <path>", "path to demo.yaml").option("--interactive", "use interactive Q&A mode").option("--output <path>", "output path for product_model.json").action(understandCommand);
8482
+ program.command("understand").description("Build product_model.json from documents, interactive Q&A, or agent-driven repo exploration").option("-c, --config <path>", "path to demo.yaml").option("--interactive", "use interactive Q&A mode (5 prompts, no LLM)").option(
8483
+ "--agent",
8484
+ "delegate to the local `claude` CLI: it explores the current directory with its read tools and synthesizes the product model. Closes the type=codebase gap in demo.yaml sources."
8485
+ ).option("--output <path>", "output path for product_model.json").action(understandCommand);
7997
8486
  program.command("instrument").description("Suggest data-testid attributes for a codebase").requiredOption("-c, --config <path>", "path to demo.yaml").requiredOption("--output <path>", "unified diff output path").option("--glob <pattern>", "override comprehension.sources with an ad-hoc glob (relative to configDir)").action(instrumentCommand);
7998
8487
  program.command("record-actions").description("Author manifest actions by demonstrating them in a live browser").requiredOption("-c, --config <path>", "path to demo.yaml").option("--segment <id>", "replace actions for an existing segment id").option("--output <path>", "manifest output path (default ./scripts/manifest.json)").action(recordActionsCommand);
7999
8488
  program.command("preview").description("Preview the generated Playwright script in UI Mode").requiredOption("-c, --config <path>", "path to demo.yaml").action(previewCommand);
@@ -8009,8 +8498,8 @@ program.parseAsync(process.argv).catch((err) => {
8009
8498
  });
8010
8499
  async function printWelcome() {
8011
8500
  const { access: access5 } = await import("fs/promises");
8012
- const { resolve: resolve27 } = await import("path");
8013
- const demoYaml = resolve27(process.cwd(), "demo.yaml");
8501
+ const { resolve: resolve28 } = await import("path");
8502
+ const demoYaml = resolve28(process.cwd(), "demo.yaml");
8014
8503
  let inProject = false;
8015
8504
  try {
8016
8505
  await access5(demoYaml);
@@ -8018,7 +8507,7 @@ async function printWelcome() {
8018
8507
  } catch {
8019
8508
  }
8020
8509
  const browserMissing = await isChromiumMissing();
8021
- const lines = ["", `Showrunner v1.1.4`, ""];
8510
+ const lines = ["", `Showrunner v1.1.6`, ""];
8022
8511
  if (browserMissing) {
8023
8512
  lines.push(`Showrunner records using Chromium. You haven't installed it yet.`);
8024
8513
  lines.push(``);
@@ -8046,8 +8535,8 @@ async function isChromiumMissing() {
8046
8535
  try {
8047
8536
  const { chromium: chromium6 } = await import("playwright-core");
8048
8537
  const exec = chromium6.executablePath();
8049
- const { stat: stat16 } = await import("fs/promises");
8050
- await stat16(exec);
8538
+ const { stat: stat17 } = await import("fs/promises");
8539
+ await stat17(exec);
8051
8540
  return false;
8052
8541
  } catch {
8053
8542
  return true;