@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/CHANGELOG.md +49 -0
- package/dist/cli.js +922 -168
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
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
|
|
1416
|
+
const text2 = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
|
|
1417
1417
|
throw new LLMProviderError(
|
|
1418
|
-
|
|
1419
|
-
${
|
|
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((
|
|
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
|
|
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 (!
|
|
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,
|
|
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
|
|
2723
|
-
if (
|
|
2724
|
-
logger.debug(`browser: ${
|
|
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(
|
|
4606
|
-
return
|
|
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
|
|
4684
|
-
if (
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
5336
|
+
resolve28({ status: "PASS", label: `${name} present`, detail: firstLine });
|
|
5337
5337
|
} else {
|
|
5338
|
-
|
|
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
|
|
5599
|
-
import { dirname as dirname10, isAbsolute as isAbsolute8, join as
|
|
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
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
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 =
|
|
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(
|
|
5639
|
-
await writeFile12(
|
|
5640
|
-
await writeFile12(
|
|
5641
|
-
await mkdir11(
|
|
5642
|
-
await writeFile12(
|
|
5643
|
-
await writeFile12(
|
|
5644
|
-
await writeFile12(
|
|
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(
|
|
6323
|
+
await writeFile12(join11(projectRoot, "scripts/reset_demo_data.sh"), resetScript(), {
|
|
5648
6324
|
mode: 493
|
|
5649
6325
|
});
|
|
5650
|
-
await writeFile12(
|
|
6326
|
+
await writeFile12(join11(projectRoot, "scripts/teardown.sh"), teardownScript(), {
|
|
5651
6327
|
mode: 493
|
|
5652
6328
|
});
|
|
5653
|
-
await writeFile12(
|
|
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(
|
|
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
|
|
5663
|
-
lines.push(
|
|
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
|
|
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
|
|
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
|
|
6131
|
-
import { access as
|
|
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
|
|
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 =
|
|
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((
|
|
6845
|
+
const code = await new Promise((resolve28, reject) => {
|
|
6144
6846
|
child.on("error", reject);
|
|
6145
|
-
child.on("close", (c) =>
|
|
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 =
|
|
6860
|
+
const candidate = join12(dir, "node_modules", "playwright-core", "cli.js");
|
|
6159
6861
|
try {
|
|
6160
|
-
await
|
|
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/
|
|
6174
|
-
import {
|
|
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 =
|
|
6963
|
+
const abs = isAbsolute10(relPath) ? relPath : resolve17(configDir, relPath);
|
|
6211
6964
|
try {
|
|
6212
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
6264
|
-
process.stdout.write(
|
|
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
|
|
6269
|
-
import { resolve as
|
|
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 =
|
|
6282
|
-
const voScriptPath =
|
|
6283
|
-
const lockPath =
|
|
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
|
|
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
|
|
6325
|
-
import { join as
|
|
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 =
|
|
6342
|
-
const videoDir =
|
|
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 =
|
|
7195
|
+
const traceDir = resolve20(configDir, config.recording.trace_dir);
|
|
6443
7196
|
await mkdir12(traceDir, { recursive: true });
|
|
6444
|
-
await ctx.tracing.stopChunk({ path:
|
|
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 =
|
|
7205
|
+
const dest = join13(videoDir, `${segment.id}.webm`);
|
|
6453
7206
|
await rename3(original, dest);
|
|
6454
|
-
const metadataPath =
|
|
6455
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
6535
|
-
if (
|
|
6536
|
-
logger.debug(`browser: ${
|
|
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 =
|
|
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 =
|
|
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
|
|
6611
|
-
import { isAbsolute as
|
|
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 =
|
|
6624
|
-
const videoDir =
|
|
6625
|
-
const slicePlanPath =
|
|
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 &&
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
6701
|
-
import { resolve as
|
|
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 =
|
|
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
|
|
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
|
|
6748
|
-
import { dirname as dirname13, isAbsolute as
|
|
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((
|
|
6873
|
-
this.waiter =
|
|
7625
|
+
return new Promise((resolve28, reject) => {
|
|
7626
|
+
this.waiter = resolve28;
|
|
6874
7627
|
this.rejecter = reject;
|
|
6875
7628
|
});
|
|
6876
7629
|
}
|
|
6877
7630
|
};
|
|
6878
|
-
async function
|
|
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
|
|
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
|
|
6892
|
-
const primaryUser = (await
|
|
6893
|
-
const featuresLine = (await
|
|
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
|
|
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
|
|
6900
|
-
const stepsLine = (await
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
7723
|
+
const abs = isAbsolute13(src.path) ? src.path : resolve25(configDir, src.path);
|
|
6971
7724
|
try {
|
|
6972
|
-
const content = await
|
|
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
|
|
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
|
|
7001
|
-
import { dirname as dirname14, isAbsolute as
|
|
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
|
|
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
|
|
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
|
|
7834
|
+
import { z as z10 } from "zod";
|
|
7082
7835
|
var DEFAULT_MAX_TOKENS5 = 8e3;
|
|
7083
|
-
var suggestionSchema =
|
|
7084
|
-
suggestions:
|
|
7085
|
-
|
|
7086
|
-
file:
|
|
7087
|
-
line:
|
|
7088
|
-
original:
|
|
7089
|
-
replacement:
|
|
7090
|
-
reasoning:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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 ?
|
|
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 =
|
|
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
|
|
7977
|
+
const entries = await readdir4(root, { withFileTypes: true });
|
|
7225
7978
|
for (const entry of entries) {
|
|
7226
|
-
const abs =
|
|
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(
|
|
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 =
|
|
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 =
|
|
8035
|
+
const outputPath = isAbsolute14(opts.output) ? opts.output : resolve26(loaded.configDir, opts.output);
|
|
7283
8036
|
await mkdir16(dirname14(outputPath), { recursive: true });
|
|
7284
|
-
await
|
|
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
|
|
7295
|
-
import { stat as
|
|
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 =
|
|
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 ?
|
|
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
|
|
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.
|
|
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:
|
|
7670
|
-
const { resolve:
|
|
7671
|
-
const demoYaml =
|
|
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
|
|
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.
|
|
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:
|
|
7708
|
-
await
|
|
8461
|
+
const { stat: stat17 } = await import("fs/promises");
|
|
8462
|
+
await stat17(exec);
|
|
7709
8463
|
return false;
|
|
7710
8464
|
} catch {
|
|
7711
8465
|
return true;
|