@beastmode-develeap/beastmode 0.1.146 → 0.1.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +327 -16
- package/dist/index.js.map +1 -1
- package/dist/web/board.html +1 -1
- package/dist/web/build-commit.txt +1 -1
- package/dist/web/build-stamp.txt +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4343,7 +4343,7 @@ You are currently scoped to project "${scope}". Focus your answers on this proje
|
|
|
4343
4343
|
async function runViaCli(session, content, scope = "factory") {
|
|
4344
4344
|
session.busy = true;
|
|
4345
4345
|
try {
|
|
4346
|
-
const { spawn } = await import("child_process");
|
|
4346
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
4347
4347
|
let boardContext = "";
|
|
4348
4348
|
try {
|
|
4349
4349
|
const boardUrl = getBoardUrl(session.factoryPath);
|
|
@@ -4435,7 +4435,7 @@ Respond concisely. Continue the conversation naturally.`;
|
|
|
4435
4435
|
spawnCmd = "claude";
|
|
4436
4436
|
spawnArgs = claudeArgs;
|
|
4437
4437
|
}
|
|
4438
|
-
const child =
|
|
4438
|
+
const child = spawn3(spawnCmd, spawnArgs, {
|
|
4439
4439
|
cwd: session.factoryPath,
|
|
4440
4440
|
env: {
|
|
4441
4441
|
...process.env,
|
|
@@ -11401,8 +11401,8 @@ async function runMigrate(opts) {
|
|
|
11401
11401
|
}
|
|
11402
11402
|
let worktreeOutput = "";
|
|
11403
11403
|
try {
|
|
11404
|
-
const { execSync:
|
|
11405
|
-
worktreeOutput =
|
|
11404
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
11405
|
+
worktreeOutput = execSync11("git worktree list", {
|
|
11406
11406
|
cwd,
|
|
11407
11407
|
encoding: "utf-8",
|
|
11408
11408
|
timeout: 5e3
|
|
@@ -11575,14 +11575,14 @@ async function runPipeline(projectName, opts) {
|
|
|
11575
11575
|
const daemonConfig = generateDaemonConfig(factoryConfig, projectConfig, factoryDir);
|
|
11576
11576
|
writeFileSync22(daemonConfigPath, JSON.stringify(daemonConfig, null, 2), "utf-8");
|
|
11577
11577
|
info(`Generated daemon config at: ${daemonConfigPath}`);
|
|
11578
|
-
const { execSync:
|
|
11578
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
11579
11579
|
let pythonAvailable = false;
|
|
11580
11580
|
try {
|
|
11581
|
-
|
|
11581
|
+
execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11582
11582
|
pythonAvailable = true;
|
|
11583
11583
|
} catch {
|
|
11584
11584
|
try {
|
|
11585
|
-
|
|
11585
|
+
execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11586
11586
|
pythonAvailable = true;
|
|
11587
11587
|
} catch {
|
|
11588
11588
|
}
|
|
@@ -11610,8 +11610,8 @@ async function runPipeline(projectName, opts) {
|
|
|
11610
11610
|
}
|
|
11611
11611
|
const cmd = buildDaemonCommand(null, daemonConfigPath);
|
|
11612
11612
|
info(`Spawning daemon: ${cmd.command} ${cmd.args.join(" ")}`);
|
|
11613
|
-
const { spawn } = await import("child_process");
|
|
11614
|
-
const child =
|
|
11613
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
11614
|
+
const child = spawn3(cmd.command, cmd.args, {
|
|
11615
11615
|
stdio: "inherit",
|
|
11616
11616
|
cwd: factoryDir,
|
|
11617
11617
|
env: {
|
|
@@ -11735,16 +11735,16 @@ async function runDaemon(opts) {
|
|
|
11735
11735
|
console.log(JSON.stringify(daemonConfig, null, 2));
|
|
11736
11736
|
return;
|
|
11737
11737
|
}
|
|
11738
|
-
const { execSync:
|
|
11738
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
11739
11739
|
let pythonCmd = "python";
|
|
11740
11740
|
let pythonAvailable = false;
|
|
11741
11741
|
try {
|
|
11742
|
-
|
|
11742
|
+
execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11743
11743
|
pythonAvailable = true;
|
|
11744
11744
|
pythonCmd = "python";
|
|
11745
11745
|
} catch {
|
|
11746
11746
|
try {
|
|
11747
|
-
|
|
11747
|
+
execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11748
11748
|
pythonAvailable = true;
|
|
11749
11749
|
pythonCmd = "python3";
|
|
11750
11750
|
} catch {
|
|
@@ -11762,8 +11762,8 @@ async function runDaemon(opts) {
|
|
|
11762
11762
|
info(`Starting daemon: ${pythonCmd} ${cmd.args.join(" ")}`);
|
|
11763
11763
|
console.log();
|
|
11764
11764
|
const pidFile = join28(bmDir, "daemon.pid");
|
|
11765
|
-
const { spawn } = await import("child_process");
|
|
11766
|
-
const child =
|
|
11765
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
11766
|
+
const child = spawn3(pythonCmd, cmd.args, {
|
|
11767
11767
|
stdio: "inherit",
|
|
11768
11768
|
cwd: factoryDir,
|
|
11769
11769
|
env: {
|
|
@@ -12422,9 +12422,320 @@ var updateCommand = new Command22("update").description("Pull latest BeastMode i
|
|
|
12422
12422
|
// src/cli/commands/runner-cmd.ts
|
|
12423
12423
|
init_display();
|
|
12424
12424
|
import { Command as Command23 } from "commander";
|
|
12425
|
+
import { spawn as spawn2 } from "child_process";
|
|
12426
|
+
|
|
12427
|
+
// src/cli/github-runners.ts
|
|
12428
|
+
var GitHubRunnerApiError = class extends Error {
|
|
12429
|
+
status;
|
|
12430
|
+
endpoint;
|
|
12431
|
+
constructor(status, endpoint, message) {
|
|
12432
|
+
super(message);
|
|
12433
|
+
this.name = "GitHubRunnerApiError";
|
|
12434
|
+
this.status = status;
|
|
12435
|
+
this.endpoint = endpoint;
|
|
12436
|
+
}
|
|
12437
|
+
};
|
|
12438
|
+
var GITHUB_API_BASE = "https://api.github.com";
|
|
12439
|
+
function buildHeaders(token) {
|
|
12440
|
+
return {
|
|
12441
|
+
Authorization: `token ${token}`,
|
|
12442
|
+
Accept: "application/vnd.github.v3+json",
|
|
12443
|
+
"User-Agent": "beastmode-cli"
|
|
12444
|
+
};
|
|
12445
|
+
}
|
|
12446
|
+
function runnersUrl(owner, repo) {
|
|
12447
|
+
return `${GITHUB_API_BASE}/repos/${owner}/${repo}/actions/runners`;
|
|
12448
|
+
}
|
|
12449
|
+
async function githubFetch(url, token, method = "GET", body) {
|
|
12450
|
+
const resp = await fetch(url, {
|
|
12451
|
+
method,
|
|
12452
|
+
headers: buildHeaders(token),
|
|
12453
|
+
body: body ? JSON.stringify(body) : void 0
|
|
12454
|
+
});
|
|
12455
|
+
if (!resp.ok) {
|
|
12456
|
+
const text = await resp.text().catch(() => "");
|
|
12457
|
+
const endpoint = url.replace(GITHUB_API_BASE, "");
|
|
12458
|
+
throw new GitHubRunnerApiError(
|
|
12459
|
+
resp.status,
|
|
12460
|
+
endpoint,
|
|
12461
|
+
`GitHub API ${method} ${endpoint} \u2192 ${resp.status}: ${text}`
|
|
12462
|
+
);
|
|
12463
|
+
}
|
|
12464
|
+
if (resp.status === 204) return void 0;
|
|
12465
|
+
return await resp.json();
|
|
12466
|
+
}
|
|
12467
|
+
async function createRegistrationToken(config) {
|
|
12468
|
+
const url = `${runnersUrl(config.owner, config.repo)}/registration-token`;
|
|
12469
|
+
return githubFetch(url, config.token, "POST");
|
|
12470
|
+
}
|
|
12471
|
+
async function listRunners(config) {
|
|
12472
|
+
const url = runnersUrl(config.owner, config.repo);
|
|
12473
|
+
return githubFetch(url, config.token);
|
|
12474
|
+
}
|
|
12475
|
+
function resolveGitHubConfig() {
|
|
12476
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
12477
|
+
if (!token) {
|
|
12478
|
+
throw new Error(
|
|
12479
|
+
"GITHUB_TOKEN (or GH_TOKEN) not set. Add it to .env or export it."
|
|
12480
|
+
);
|
|
12481
|
+
}
|
|
12482
|
+
const repo = process.env.PROJECT_REPO;
|
|
12483
|
+
if (!repo || !repo.includes("/")) {
|
|
12484
|
+
throw new Error(
|
|
12485
|
+
"PROJECT_REPO not set or invalid (expected 'owner/repo'). Add it to .env."
|
|
12486
|
+
);
|
|
12487
|
+
}
|
|
12488
|
+
const [owner, repoName] = repo.split("/", 2);
|
|
12489
|
+
return { owner, repo: repoName, token };
|
|
12490
|
+
}
|
|
12491
|
+
|
|
12492
|
+
// src/cli/runner-helpers.ts
|
|
12493
|
+
import { execSync as execSync10, spawn, spawnSync as spawnSync5 } from "child_process";
|
|
12494
|
+
import { promises as fs } from "fs";
|
|
12495
|
+
init_display();
|
|
12496
|
+
function resolveRepoSlug() {
|
|
12497
|
+
let rawUrl;
|
|
12498
|
+
try {
|
|
12499
|
+
rawUrl = execSync10("git remote get-url origin", {
|
|
12500
|
+
encoding: "utf-8",
|
|
12501
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
12502
|
+
}).trim();
|
|
12503
|
+
} catch {
|
|
12504
|
+
throw new Error(
|
|
12505
|
+
"Could not determine repo from git remote. Use --repo owner/repo."
|
|
12506
|
+
);
|
|
12507
|
+
}
|
|
12508
|
+
const match = rawUrl.match(/github\.com[:/](.+?)(?:\.git)?$/) || rawUrl.match(/github\.com\/(.+?)(?:\.git)?$/);
|
|
12509
|
+
if (!match) {
|
|
12510
|
+
throw new Error(
|
|
12511
|
+
`Could not parse GitHub repo from remote URL: ${rawUrl}`
|
|
12512
|
+
);
|
|
12513
|
+
}
|
|
12514
|
+
return match[1];
|
|
12515
|
+
}
|
|
12516
|
+
async function pullImageIfNeeded(image) {
|
|
12517
|
+
const inspect = spawnSync5("docker", ["image", "inspect", image], {
|
|
12518
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
12519
|
+
});
|
|
12520
|
+
if (inspect.error && inspect.error.code === "ENOENT") {
|
|
12521
|
+
throw new Error(
|
|
12522
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12523
|
+
);
|
|
12524
|
+
}
|
|
12525
|
+
if (inspect.status === 0) return;
|
|
12526
|
+
await new Promise((resolve20, reject) => {
|
|
12527
|
+
const child = spawn("docker", ["pull", image], {
|
|
12528
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
12529
|
+
});
|
|
12530
|
+
child.on("error", (err) => {
|
|
12531
|
+
const e = err;
|
|
12532
|
+
if (e.code === "ENOENT") {
|
|
12533
|
+
reject(
|
|
12534
|
+
new Error(
|
|
12535
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12536
|
+
)
|
|
12537
|
+
);
|
|
12538
|
+
} else {
|
|
12539
|
+
reject(err);
|
|
12540
|
+
}
|
|
12541
|
+
});
|
|
12542
|
+
child.on("exit", (code) => {
|
|
12543
|
+
if (code === 0) resolve20();
|
|
12544
|
+
else reject(new Error(`docker pull ${image} exited with code ${code}`));
|
|
12545
|
+
});
|
|
12546
|
+
});
|
|
12547
|
+
}
|
|
12548
|
+
async function findContainerByName(name) {
|
|
12549
|
+
const result = spawnSync5(
|
|
12550
|
+
"docker",
|
|
12551
|
+
["inspect", name, "--format", "{{.State.Status}}"],
|
|
12552
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
|
|
12553
|
+
);
|
|
12554
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
12555
|
+
throw new Error(
|
|
12556
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12557
|
+
);
|
|
12558
|
+
}
|
|
12559
|
+
if (result.status !== 0) return null;
|
|
12560
|
+
const status = (result.stdout || "").trim();
|
|
12561
|
+
if (!status) return null;
|
|
12562
|
+
return { name, status };
|
|
12563
|
+
}
|
|
12564
|
+
function containerIsHealthy(infoArg) {
|
|
12565
|
+
return infoArg.status === "running";
|
|
12566
|
+
}
|
|
12567
|
+
async function removeContainer(name) {
|
|
12568
|
+
const result = spawnSync5("docker", ["rm", "-f", name], {
|
|
12569
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
12570
|
+
encoding: "utf-8"
|
|
12571
|
+
});
|
|
12572
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
12573
|
+
throw new Error(
|
|
12574
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12575
|
+
);
|
|
12576
|
+
}
|
|
12577
|
+
if (result.status !== 0) {
|
|
12578
|
+
throw new Error(
|
|
12579
|
+
`docker rm -f ${name} failed (exit ${result.status}): ${result.stderr || ""}`
|
|
12580
|
+
);
|
|
12581
|
+
}
|
|
12582
|
+
}
|
|
12583
|
+
async function startRunnerContainer(opts) {
|
|
12584
|
+
const labelStr = opts.labels.join(",");
|
|
12585
|
+
const args = [
|
|
12586
|
+
"run",
|
|
12587
|
+
"-d",
|
|
12588
|
+
"--restart=unless-stopped",
|
|
12589
|
+
"--name",
|
|
12590
|
+
opts.name,
|
|
12591
|
+
"-e",
|
|
12592
|
+
`REPO_URL=${opts.repoUrl}`,
|
|
12593
|
+
"-e",
|
|
12594
|
+
`RUNNER_TOKEN=${opts.token}`,
|
|
12595
|
+
"-e",
|
|
12596
|
+
`LABELS=${labelStr}`,
|
|
12597
|
+
"-e",
|
|
12598
|
+
`RUNNER_NAME=${opts.name}`,
|
|
12599
|
+
"-v",
|
|
12600
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
12601
|
+
"myoung34/github-runner:latest"
|
|
12602
|
+
];
|
|
12603
|
+
const result = spawnSync5("docker", args, {
|
|
12604
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
12605
|
+
encoding: "utf-8"
|
|
12606
|
+
});
|
|
12607
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
12608
|
+
throw new Error(
|
|
12609
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12610
|
+
);
|
|
12611
|
+
}
|
|
12612
|
+
if (result.status !== 0) {
|
|
12613
|
+
const stderr = (result.stderr || "").replace(opts.token, "<REDACTED>");
|
|
12614
|
+
throw new Error(
|
|
12615
|
+
`docker run failed (exit ${result.status}): ${stderr}`
|
|
12616
|
+
);
|
|
12617
|
+
}
|
|
12618
|
+
}
|
|
12619
|
+
async function writeEnvEntries(entries, envPath = ".env") {
|
|
12620
|
+
let existing = "";
|
|
12621
|
+
try {
|
|
12622
|
+
existing = await fs.readFile(envPath, "utf-8");
|
|
12623
|
+
} catch (err) {
|
|
12624
|
+
if (err.code !== "ENOENT") throw err;
|
|
12625
|
+
}
|
|
12626
|
+
const lines = existing === "" ? [] : existing.split("\n");
|
|
12627
|
+
const remainingKeys = new Set(Object.keys(entries));
|
|
12628
|
+
const updated = lines.map((line) => {
|
|
12629
|
+
for (const key of remainingKeys) {
|
|
12630
|
+
if (line.startsWith(`${key}=`)) {
|
|
12631
|
+
remainingKeys.delete(key);
|
|
12632
|
+
return `${key}=${entries[key]}`;
|
|
12633
|
+
}
|
|
12634
|
+
}
|
|
12635
|
+
return line;
|
|
12636
|
+
});
|
|
12637
|
+
for (const key of remainingKeys) {
|
|
12638
|
+
updated.push(`${key}=${entries[key]}`);
|
|
12639
|
+
}
|
|
12640
|
+
let output = updated.join("\n");
|
|
12641
|
+
if (existing !== "" && !existing.endsWith("\n") && !output.endsWith("\n")) {
|
|
12642
|
+
output += "\n";
|
|
12643
|
+
} else if (existing === "" && !output.endsWith("\n")) {
|
|
12644
|
+
output += "\n";
|
|
12645
|
+
}
|
|
12646
|
+
await fs.writeFile(envPath, output, "utf-8");
|
|
12647
|
+
}
|
|
12648
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12649
|
+
async function pollUntilOnline(config, name, timeoutMs) {
|
|
12650
|
+
const deadline = Date.now() + timeoutMs;
|
|
12651
|
+
while (Date.now() < deadline) {
|
|
12652
|
+
try {
|
|
12653
|
+
const list = await listRunners(config);
|
|
12654
|
+
const runner = list.runners.find((r) => r.name === name);
|
|
12655
|
+
if (runner && runner.status === "online") return;
|
|
12656
|
+
} catch {
|
|
12657
|
+
}
|
|
12658
|
+
if (Date.now() + 3e3 >= deadline) break;
|
|
12659
|
+
await sleep(3e3);
|
|
12660
|
+
}
|
|
12661
|
+
throw new Error(
|
|
12662
|
+
`Runner '${name}' did not appear online within ${Math.floor(
|
|
12663
|
+
timeoutMs / 1e3
|
|
12664
|
+
)}s \u2014 check container logs with: docker logs ${name}`
|
|
12665
|
+
);
|
|
12666
|
+
}
|
|
12667
|
+
function setupStep(text) {
|
|
12668
|
+
info(text);
|
|
12669
|
+
}
|
|
12670
|
+
|
|
12671
|
+
// src/cli/commands/runner-cmd.ts
|
|
12425
12672
|
var runnerCommand = new Command23("runner").description("Manage self-hosted GitHub Actions runners");
|
|
12426
|
-
|
|
12427
|
-
|
|
12673
|
+
async function runnerSetupAction(opts) {
|
|
12674
|
+
if (opts.native) {
|
|
12675
|
+
warn("--native is not implemented yet (Story 5)");
|
|
12676
|
+
return;
|
|
12677
|
+
}
|
|
12678
|
+
const ghConfig = resolveGitHubConfig();
|
|
12679
|
+
const repoSlug = opts.repo ?? resolveRepoSlug();
|
|
12680
|
+
setupStep("Generating registration token via GitHub API...");
|
|
12681
|
+
if (opts.dryRun) {
|
|
12682
|
+
info(`[dry-run] Would start container '${opts.name}' for repo ${repoSlug}`);
|
|
12683
|
+
return;
|
|
12684
|
+
}
|
|
12685
|
+
const { token: regToken } = await createRegistrationToken(ghConfig);
|
|
12686
|
+
setupStep("Pulling myoung34/github-runner:latest...");
|
|
12687
|
+
await pullImageIfNeeded("myoung34/github-runner:latest");
|
|
12688
|
+
const existing = await findContainerByName(opts.name);
|
|
12689
|
+
if (existing) {
|
|
12690
|
+
if (containerIsHealthy(existing)) {
|
|
12691
|
+
success(`Runner container '${opts.name}' already running \u2014 reusing.`);
|
|
12692
|
+
return;
|
|
12693
|
+
}
|
|
12694
|
+
setupStep(
|
|
12695
|
+
`Container '${opts.name}' exists but is dead \u2014 removing and recreating...`
|
|
12696
|
+
);
|
|
12697
|
+
await removeContainer(opts.name);
|
|
12698
|
+
}
|
|
12699
|
+
const repoUrl = `https://github.com/${ghConfig.owner}/${ghConfig.repo}`;
|
|
12700
|
+
setupStep("Starting runner container...");
|
|
12701
|
+
await startRunnerContainer({
|
|
12702
|
+
name: opts.name,
|
|
12703
|
+
repoUrl,
|
|
12704
|
+
token: regToken,
|
|
12705
|
+
labels: ["self-hosted", opts.label]
|
|
12706
|
+
});
|
|
12707
|
+
await writeEnvEntries({
|
|
12708
|
+
RUNNER_REPO_URL: repoUrl,
|
|
12709
|
+
RUNNER_TOKEN: regToken,
|
|
12710
|
+
RUNNER_NAME: opts.name
|
|
12711
|
+
});
|
|
12712
|
+
setupStep("Waiting for runner to appear online on GitHub (timeout 60s)...");
|
|
12713
|
+
await pollUntilOnline(ghConfig, opts.name, 6e4);
|
|
12714
|
+
setupStep("Adding runner to docker-compose...");
|
|
12715
|
+
await composeUpRunner();
|
|
12716
|
+
success(`Runner '${opts.name}' registered and online.`);
|
|
12717
|
+
}
|
|
12718
|
+
async function composeUpRunner() {
|
|
12719
|
+
await new Promise((resolve20, reject) => {
|
|
12720
|
+
const child = spawn2(
|
|
12721
|
+
"docker",
|
|
12722
|
+
["compose", "--profile", "runner", "up", "-d", "runner"],
|
|
12723
|
+
{ stdio: ["ignore", "inherit", "inherit"] }
|
|
12724
|
+
);
|
|
12725
|
+
child.on("error", (err) => reject(err));
|
|
12726
|
+
child.on("exit", (code) => {
|
|
12727
|
+
if (code === 0) resolve20();
|
|
12728
|
+
else
|
|
12729
|
+
reject(
|
|
12730
|
+
new Error(
|
|
12731
|
+
`docker compose --profile runner up -d runner exited with code ${code}`
|
|
12732
|
+
)
|
|
12733
|
+
);
|
|
12734
|
+
});
|
|
12735
|
+
});
|
|
12736
|
+
}
|
|
12737
|
+
runnerCommand.command("setup").description("Set up a self-hosted GitHub Actions runner").option("--repo <owner/repo>", "GitHub repo for runner registration").option("--name <name>", "Container + runner name", "beastmode-runner").option("--label <label>", "Additional runner label", "beastmode").option("--dry-run", "Print what would happen without mutating state").option("--native", "Use native install instead of Docker").action(async (opts) => {
|
|
12738
|
+
await runnerSetupAction(opts);
|
|
12428
12739
|
});
|
|
12429
12740
|
runnerCommand.command("status").description("Show runner status (container + GitHub registration)").action(() => {
|
|
12430
12741
|
warn("runner status is not implemented yet (Story 3)");
|