@kadj-amoah/showrunner 1.1.4 → 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 +26 -0
- package/dist/cli.js +557 -145
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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((
|
|
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((
|
|
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)
|
|
@@ -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
|
|
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((
|
|
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
|
-
|
|
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
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
5955
|
-
await writeFile12(
|
|
5956
|
-
await writeFile12(
|
|
5957
|
-
await mkdir11(
|
|
5958
|
-
await writeFile12(
|
|
5959
|
-
await writeFile12(
|
|
5960
|
-
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(), {
|
|
5961
6321
|
mode: 493
|
|
5962
6322
|
});
|
|
5963
|
-
await writeFile12(
|
|
6323
|
+
await writeFile12(join11(projectRoot, "scripts/reset_demo_data.sh"), resetScript(), {
|
|
5964
6324
|
mode: 493
|
|
5965
6325
|
});
|
|
5966
|
-
await writeFile12(
|
|
6326
|
+
await writeFile12(join11(projectRoot, "scripts/teardown.sh"), teardownScript(), {
|
|
5967
6327
|
mode: 493
|
|
5968
6328
|
});
|
|
5969
|
-
await writeFile12(
|
|
6329
|
+
await writeFile12(join11(projectRoot, "README.md"), readmeTemplate(resolved), "utf8");
|
|
5970
6330
|
if (Object.keys(collectedKeys).length > 0) {
|
|
5971
|
-
await writeFile12(
|
|
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
|
|
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
|
|
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 =
|
|
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((
|
|
6845
|
+
const code = await new Promise((resolve28, reject) => {
|
|
6486
6846
|
child.on("error", reject);
|
|
6487
|
-
child.on("close", (c) =>
|
|
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 =
|
|
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/
|
|
6516
|
-
import {
|
|
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 =
|
|
6963
|
+
const abs = isAbsolute10(relPath) ? relPath : resolve17(configDir, relPath);
|
|
6553
6964
|
try {
|
|
6554
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
6611
|
-
import { resolve as
|
|
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 =
|
|
6624
|
-
const voScriptPath =
|
|
6625
|
-
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");
|
|
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
|
|
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
|
|
6667
|
-
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";
|
|
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 =
|
|
6684
|
-
const videoDir =
|
|
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 =
|
|
7195
|
+
const traceDir = resolve20(configDir, config.recording.trace_dir);
|
|
6785
7196
|
await mkdir12(traceDir, { recursive: true });
|
|
6786
|
-
await ctx.tracing.stopChunk({ path:
|
|
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 =
|
|
7205
|
+
const dest = join13(videoDir, `${segment.id}.webm`);
|
|
6795
7206
|
await rename3(original, dest);
|
|
6796
|
-
const metadataPath =
|
|
6797
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
6953
|
-
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";
|
|
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 =
|
|
6966
|
-
const videoDir =
|
|
6967
|
-
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");
|
|
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 &&
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
7043
|
-
import { resolve as
|
|
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 =
|
|
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
|
|
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
|
|
7090
|
-
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";
|
|
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.
|
|
@@ -7211,8 +7622,8 @@ var LineReader = class {
|
|
|
7211
7622
|
if (this.closed) {
|
|
7212
7623
|
return Promise.reject(new Error("stdin closed before answer was provided"));
|
|
7213
7624
|
}
|
|
7214
|
-
return new Promise((
|
|
7215
|
-
this.waiter =
|
|
7625
|
+
return new Promise((resolve28, reject) => {
|
|
7626
|
+
this.waiter = resolve28;
|
|
7216
7627
|
this.rejecter = reject;
|
|
7217
7628
|
});
|
|
7218
7629
|
}
|
|
@@ -7285,13 +7696,13 @@ async function understandCommand(opts) {
|
|
|
7285
7696
|
}
|
|
7286
7697
|
sources = loaded.config.comprehension.sources.map((s) => ({ path: s.path, type: s.type }));
|
|
7287
7698
|
llmConfig = loaded.config.llm;
|
|
7288
|
-
const envFile =
|
|
7699
|
+
const envFile = resolve25(loaded.configDir, ".env");
|
|
7289
7700
|
try {
|
|
7290
7701
|
process.loadEnvFile(envFile);
|
|
7291
7702
|
} catch {
|
|
7292
7703
|
}
|
|
7293
7704
|
}
|
|
7294
|
-
const outputPath =
|
|
7705
|
+
const outputPath = isAbsolute13(outputRel) ? outputRel : resolve25(configDir, outputRel);
|
|
7295
7706
|
await mkdir15(dirname13(outputPath), { recursive: true });
|
|
7296
7707
|
let productModel;
|
|
7297
7708
|
const provider = resolveDefaultLLMProvider({ configDir, llm: llmConfig });
|
|
@@ -7309,9 +7720,9 @@ async function understandCommand(opts) {
|
|
|
7309
7720
|
}
|
|
7310
7721
|
const docs = [];
|
|
7311
7722
|
for (const src of sources) {
|
|
7312
|
-
const abs =
|
|
7723
|
+
const abs = isAbsolute13(src.path) ? src.path : resolve25(configDir, src.path);
|
|
7313
7724
|
try {
|
|
7314
|
-
const content = await
|
|
7725
|
+
const content = await readFile12(abs, "utf8");
|
|
7315
7726
|
docs.push({ path: src.path, type: src.type, content });
|
|
7316
7727
|
} catch (err) {
|
|
7317
7728
|
const cause = err instanceof Error ? err.message : String(err);
|
|
@@ -7334,16 +7745,16 @@ async function understandCommand(opts) {
|
|
|
7334
7745
|
}
|
|
7335
7746
|
throw err;
|
|
7336
7747
|
}
|
|
7337
|
-
await
|
|
7748
|
+
await writeFile15(outputPath, JSON.stringify(productModel, null, 2) + "\n", "utf8");
|
|
7338
7749
|
logger.info("Wrote product_model.json", { path: outputPath });
|
|
7339
7750
|
}
|
|
7340
7751
|
|
|
7341
7752
|
// src/commands/instrument.ts
|
|
7342
|
-
import { mkdir as mkdir16, readdir as
|
|
7343
|
-
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";
|
|
7344
7755
|
|
|
7345
7756
|
// src/instrument/scan.ts
|
|
7346
|
-
import { readFile as
|
|
7757
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
7347
7758
|
import { parse } from "@babel/parser";
|
|
7348
7759
|
import _traverse from "@babel/traverse";
|
|
7349
7760
|
var traverse = _traverse.default ?? _traverse;
|
|
@@ -7361,7 +7772,7 @@ var TARGET_TAGS = /* @__PURE__ */ new Set([
|
|
|
7361
7772
|
"Form"
|
|
7362
7773
|
]);
|
|
7363
7774
|
async function scanFile(filePath) {
|
|
7364
|
-
const source = await
|
|
7775
|
+
const source = await readFile13(filePath, "utf8");
|
|
7365
7776
|
let ast;
|
|
7366
7777
|
try {
|
|
7367
7778
|
ast = parse(source, {
|
|
@@ -7420,23 +7831,23 @@ function pickAttr(node, attr) {
|
|
|
7420
7831
|
}
|
|
7421
7832
|
|
|
7422
7833
|
// src/instrument/suggest.ts
|
|
7423
|
-
import { z as
|
|
7834
|
+
import { z as z10 } from "zod";
|
|
7424
7835
|
var DEFAULT_MAX_TOKENS5 = 8e3;
|
|
7425
|
-
var suggestionSchema =
|
|
7426
|
-
suggestions:
|
|
7427
|
-
|
|
7428
|
-
file:
|
|
7429
|
-
line:
|
|
7430
|
-
original:
|
|
7431
|
-
replacement:
|
|
7432
|
-
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("")
|
|
7433
7844
|
})
|
|
7434
7845
|
)
|
|
7435
7846
|
});
|
|
7436
7847
|
var InstrumentSuggestionError = class extends Error {
|
|
7437
7848
|
name = "InstrumentSuggestionError";
|
|
7438
7849
|
};
|
|
7439
|
-
var
|
|
7850
|
+
var SYSTEM_PROMPT2 = `You add data-testid attributes to JSX elements so they can be reliably targeted by automated demo tools.
|
|
7440
7851
|
|
|
7441
7852
|
Rules:
|
|
7442
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".
|
|
@@ -7463,7 +7874,7 @@ For each, return {file, line, original (exact source line), replacement (source
|
|
|
7463
7874
|
});
|
|
7464
7875
|
try {
|
|
7465
7876
|
const result = await opts.provider.generateStructured({
|
|
7466
|
-
systemPrompt:
|
|
7877
|
+
systemPrompt: SYSTEM_PROMPT2,
|
|
7467
7878
|
userPrompt,
|
|
7468
7879
|
schema: suggestionSchema,
|
|
7469
7880
|
schemaName: "instrument_suggestions",
|
|
@@ -7478,8 +7889,8 @@ For each, return {file, line, original (exact source line), replacement (source
|
|
|
7478
7889
|
}
|
|
7479
7890
|
|
|
7480
7891
|
// src/instrument/diff.ts
|
|
7481
|
-
import { readFile as
|
|
7482
|
-
import { relative } from "path";
|
|
7892
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
7893
|
+
import { relative as relative2 } from "path";
|
|
7483
7894
|
async function buildDiff(suggestions, opts = {}) {
|
|
7484
7895
|
const byFile = /* @__PURE__ */ new Map();
|
|
7485
7896
|
for (const s of suggestions) {
|
|
@@ -7492,7 +7903,7 @@ async function buildDiff(suggestions, opts = {}) {
|
|
|
7492
7903
|
for (const [file, perFile] of byFile) {
|
|
7493
7904
|
let source;
|
|
7494
7905
|
try {
|
|
7495
|
-
source = await
|
|
7906
|
+
source = await readFile14(file, "utf8");
|
|
7496
7907
|
} catch (err) {
|
|
7497
7908
|
for (const s of perFile) {
|
|
7498
7909
|
skipped.push({
|
|
@@ -7528,7 +7939,7 @@ async function buildDiff(suggestions, opts = {}) {
|
|
|
7528
7939
|
);
|
|
7529
7940
|
}
|
|
7530
7941
|
if (hunks.length === 0) continue;
|
|
7531
|
-
const displayPath = opts.basePath ?
|
|
7942
|
+
const displayPath = opts.basePath ? relative2(opts.basePath, file).replace(/\\/g, "/") : file;
|
|
7532
7943
|
fileDiffs.push(`--- a/${displayPath}
|
|
7533
7944
|
+++ b/${displayPath}
|
|
7534
7945
|
${hunks.join("\n")}`);
|
|
@@ -7548,7 +7959,7 @@ async function instrumentCommand(opts) {
|
|
|
7548
7959
|
}
|
|
7549
7960
|
throw err;
|
|
7550
7961
|
}
|
|
7551
|
-
const envFile =
|
|
7962
|
+
const envFile = resolve26(loaded.configDir, ".env");
|
|
7552
7963
|
try {
|
|
7553
7964
|
process.loadEnvFile(envFile);
|
|
7554
7965
|
} catch {
|
|
@@ -7563,13 +7974,13 @@ async function instrumentCommand(opts) {
|
|
|
7563
7974
|
const filesToScan = /* @__PURE__ */ new Set();
|
|
7564
7975
|
const IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
7565
7976
|
async function walk(root, accept) {
|
|
7566
|
-
const entries = await
|
|
7977
|
+
const entries = await readdir4(root, { withFileTypes: true });
|
|
7567
7978
|
for (const entry of entries) {
|
|
7568
|
-
const abs =
|
|
7979
|
+
const abs = join15(root, entry.name);
|
|
7569
7980
|
if (entry.isDirectory()) {
|
|
7570
7981
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
7571
7982
|
await walk(abs, accept);
|
|
7572
|
-
} else if (entry.isFile() && accept(
|
|
7983
|
+
} else if (entry.isFile() && accept(relative3(root, abs))) {
|
|
7573
7984
|
filesToScan.add(abs);
|
|
7574
7985
|
}
|
|
7575
7986
|
}
|
|
@@ -7580,7 +7991,7 @@ async function instrumentCommand(opts) {
|
|
|
7580
7991
|
await walk(root, isJsxFile);
|
|
7581
7992
|
} else {
|
|
7582
7993
|
for (const src of codebaseSources) {
|
|
7583
|
-
const root =
|
|
7994
|
+
const root = isAbsolute14(src.path) ? src.path : resolve26(loaded.configDir, src.path);
|
|
7584
7995
|
await walk(root, isJsxFile);
|
|
7585
7996
|
}
|
|
7586
7997
|
}
|
|
@@ -7621,9 +8032,9 @@ async function instrumentCommand(opts) {
|
|
|
7621
8032
|
for (const s of skipped) {
|
|
7622
8033
|
logger.warn(`Skipped suggestion at ${s.file}:${s.line} \u2014 ${s.reason}`);
|
|
7623
8034
|
}
|
|
7624
|
-
const outputPath =
|
|
8035
|
+
const outputPath = isAbsolute14(opts.output) ? opts.output : resolve26(loaded.configDir, opts.output);
|
|
7625
8036
|
await mkdir16(dirname14(outputPath), { recursive: true });
|
|
7626
|
-
await
|
|
8037
|
+
await writeFile16(outputPath, patch, "utf8");
|
|
7627
8038
|
logger.info("Wrote instrumentation patch", { path: outputPath, suggestions: suggestions.length });
|
|
7628
8039
|
process.stdout.write(`
|
|
7629
8040
|
Apply with: cd ${loaded.configDir} && git apply ${opts.output}
|
|
@@ -7633,8 +8044,8 @@ Apply with: cd ${loaded.configDir} && git apply ${opts.output}
|
|
|
7633
8044
|
|
|
7634
8045
|
// src/commands/recordActions.ts
|
|
7635
8046
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
7636
|
-
import { resolve as
|
|
7637
|
-
import { stat as
|
|
8047
|
+
import { resolve as resolve27 } from "path";
|
|
8048
|
+
import { stat as stat16 } from "fs/promises";
|
|
7638
8049
|
|
|
7639
8050
|
// src/recording/captureEvents.ts
|
|
7640
8051
|
async function installCaptureBinding(context, onEvent) {
|
|
@@ -7832,12 +8243,12 @@ async function recordActionsCommand(opts) {
|
|
|
7832
8243
|
}
|
|
7833
8244
|
throw err;
|
|
7834
8245
|
}
|
|
7835
|
-
const envFile =
|
|
8246
|
+
const envFile = resolve27(loaded.configDir, ".env");
|
|
7836
8247
|
try {
|
|
7837
8248
|
process.loadEnvFile(envFile);
|
|
7838
8249
|
} catch {
|
|
7839
8250
|
}
|
|
7840
|
-
const manifestPath = opts.output ?
|
|
8251
|
+
const manifestPath = opts.output ? resolve27(loaded.configDir, opts.output) : resolve27(loaded.configDir, "./scripts/manifest.json");
|
|
7841
8252
|
logger.info("Launching headed browser for action capture", {
|
|
7842
8253
|
target: loaded.config.recording.target_url
|
|
7843
8254
|
});
|
|
@@ -7948,7 +8359,7 @@ async function mergeIntoManifest(opts) {
|
|
|
7948
8359
|
}
|
|
7949
8360
|
async function fileExists10(path) {
|
|
7950
8361
|
try {
|
|
7951
|
-
await
|
|
8362
|
+
await stat16(path);
|
|
7952
8363
|
return true;
|
|
7953
8364
|
} catch {
|
|
7954
8365
|
return false;
|
|
@@ -7957,7 +8368,7 @@ async function fileExists10(path) {
|
|
|
7957
8368
|
|
|
7958
8369
|
// src/cli.ts
|
|
7959
8370
|
var program = new Command();
|
|
7960
|
-
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) => {
|
|
7961
8372
|
const opts = thisCmd.opts();
|
|
7962
8373
|
if (opts.json) logger.setJson(true);
|
|
7963
8374
|
if (opts.logLevel) logger.setLevel(opts.logLevel);
|
|
@@ -7990,6 +8401,7 @@ program.command("init").description("Scaffold a new Showrunner project (interact
|
|
|
7990
8401
|
"scaffold resolution: low (854x480) | standard (720p) | high (1080p) | extreme (4K)",
|
|
7991
8402
|
"standard"
|
|
7992
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);
|
|
7993
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);
|
|
7994
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);
|
|
7995
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);
|
|
@@ -8009,8 +8421,8 @@ program.parseAsync(process.argv).catch((err) => {
|
|
|
8009
8421
|
});
|
|
8010
8422
|
async function printWelcome() {
|
|
8011
8423
|
const { access: access5 } = await import("fs/promises");
|
|
8012
|
-
const { resolve:
|
|
8013
|
-
const demoYaml =
|
|
8424
|
+
const { resolve: resolve28 } = await import("path");
|
|
8425
|
+
const demoYaml = resolve28(process.cwd(), "demo.yaml");
|
|
8014
8426
|
let inProject = false;
|
|
8015
8427
|
try {
|
|
8016
8428
|
await access5(demoYaml);
|
|
@@ -8018,7 +8430,7 @@ async function printWelcome() {
|
|
|
8018
8430
|
} catch {
|
|
8019
8431
|
}
|
|
8020
8432
|
const browserMissing = await isChromiumMissing();
|
|
8021
|
-
const lines = ["", `Showrunner v1.1.
|
|
8433
|
+
const lines = ["", `Showrunner v1.1.5`, ""];
|
|
8022
8434
|
if (browserMissing) {
|
|
8023
8435
|
lines.push(`Showrunner records using Chromium. You haven't installed it yet.`);
|
|
8024
8436
|
lines.push(``);
|
|
@@ -8046,8 +8458,8 @@ async function isChromiumMissing() {
|
|
|
8046
8458
|
try {
|
|
8047
8459
|
const { chromium: chromium6 } = await import("playwright-core");
|
|
8048
8460
|
const exec = chromium6.executablePath();
|
|
8049
|
-
const { stat:
|
|
8050
|
-
await
|
|
8461
|
+
const { stat: stat17 } = await import("fs/promises");
|
|
8462
|
+
await stat17(exec);
|
|
8051
8463
|
return false;
|
|
8052
8464
|
} catch {
|
|
8053
8465
|
return true;
|