@beastmode-develeap/beastmode 0.1.150 → 0.1.152
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 +540 -46
- 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
|
@@ -2652,7 +2652,7 @@ var init_migrator = __esm({
|
|
|
2652
2652
|
|
|
2653
2653
|
// src/engine/bridge.ts
|
|
2654
2654
|
function generateDaemonConfig(factoryConfig, projectConfig, factoryPath) {
|
|
2655
|
-
const
|
|
2655
|
+
const pipeline2 = factoryConfig.pipeline;
|
|
2656
2656
|
const models = factoryConfig.models;
|
|
2657
2657
|
const resilience = factoryConfig.resilience;
|
|
2658
2658
|
const cost = factoryConfig.cost;
|
|
@@ -2720,9 +2720,9 @@ function generateDaemonConfig(factoryConfig, projectConfig, factoryPath) {
|
|
|
2720
2720
|
},
|
|
2721
2721
|
verification: {
|
|
2722
2722
|
enabled: true,
|
|
2723
|
-
satisfaction_threshold:
|
|
2723
|
+
satisfaction_threshold: pipeline2.satisfaction_threshold,
|
|
2724
2724
|
timeout_minutes: 30,
|
|
2725
|
-
prod_accept_floor:
|
|
2725
|
+
prod_accept_floor: pipeline2.prod_accept_floor
|
|
2726
2726
|
},
|
|
2727
2727
|
review: {
|
|
2728
2728
|
enabled: humanGates.pr_review !== "disabled",
|
|
@@ -2745,8 +2745,8 @@ function generateDaemonConfig(factoryConfig, projectConfig, factoryPath) {
|
|
|
2745
2745
|
build_timeout_seconds: 600
|
|
2746
2746
|
},
|
|
2747
2747
|
convergence: {
|
|
2748
|
-
max_iterations:
|
|
2749
|
-
fail_fast_threshold:
|
|
2748
|
+
max_iterations: pipeline2.max_iterations,
|
|
2749
|
+
fail_fast_threshold: pipeline2.fail_fast_threshold,
|
|
2750
2750
|
infra_retry_max: 3,
|
|
2751
2751
|
max_precheck_retries: 2
|
|
2752
2752
|
},
|
|
@@ -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:
|
|
4346
|
+
const { spawn: spawn4 } = 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 = spawn4(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: execSync12 } = await import("child_process");
|
|
11405
|
+
worktreeOutput = execSync12("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: execSync12 } = await import("child_process");
|
|
11579
11579
|
let pythonAvailable = false;
|
|
11580
11580
|
try {
|
|
11581
|
-
|
|
11581
|
+
execSync12("python --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11582
11582
|
pythonAvailable = true;
|
|
11583
11583
|
} catch {
|
|
11584
11584
|
try {
|
|
11585
|
-
|
|
11585
|
+
execSync12("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:
|
|
11614
|
-
const child =
|
|
11613
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
11614
|
+
const child = spawn4(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: execSync12 } = await import("child_process");
|
|
11739
11739
|
let pythonCmd = "python";
|
|
11740
11740
|
let pythonAvailable = false;
|
|
11741
11741
|
try {
|
|
11742
|
-
|
|
11742
|
+
execSync12("python --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11743
11743
|
pythonAvailable = true;
|
|
11744
11744
|
pythonCmd = "python";
|
|
11745
11745
|
} catch {
|
|
11746
11746
|
try {
|
|
11747
|
-
|
|
11747
|
+
execSync12("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:
|
|
11766
|
-
const child =
|
|
11765
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
11766
|
+
const child = spawn4(pythonCmd, cmd.args, {
|
|
11767
11767
|
stdio: "inherit",
|
|
11768
11768
|
cwd: factoryDir,
|
|
11769
11769
|
env: {
|
|
@@ -11783,8 +11783,8 @@ async function runDaemon(opts) {
|
|
|
11783
11783
|
const exitCode = await new Promise((resolvePromise) => {
|
|
11784
11784
|
child.on("exit", (code) => {
|
|
11785
11785
|
try {
|
|
11786
|
-
const { unlinkSync:
|
|
11787
|
-
|
|
11786
|
+
const { unlinkSync: unlinkSync7 } = __require("fs");
|
|
11787
|
+
unlinkSync7(pidFile);
|
|
11788
11788
|
} catch {
|
|
11789
11789
|
}
|
|
11790
11790
|
resolvePromise(code ?? 1);
|
|
@@ -12422,7 +12422,7 @@ 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
|
|
12425
|
+
import { spawn as spawn3 } from "child_process";
|
|
12426
12426
|
import { resolve as resolve20 } from "path";
|
|
12427
12427
|
|
|
12428
12428
|
// src/cli/github-runners.ts
|
|
@@ -12473,6 +12473,10 @@ async function listRunners(config) {
|
|
|
12473
12473
|
const url = runnersUrl(config.owner, config.repo);
|
|
12474
12474
|
return githubFetch(url, config.token);
|
|
12475
12475
|
}
|
|
12476
|
+
async function deleteRunner(config, runnerId) {
|
|
12477
|
+
const url = `${runnersUrl(config.owner, config.repo)}/${runnerId}`;
|
|
12478
|
+
await githubFetch(url, config.token, "DELETE");
|
|
12479
|
+
}
|
|
12476
12480
|
function resolveGitHubConfig() {
|
|
12477
12481
|
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
12478
12482
|
if (!token) {
|
|
@@ -12493,7 +12497,13 @@ function resolveGitHubConfig() {
|
|
|
12493
12497
|
// src/cli/runner-helpers.ts
|
|
12494
12498
|
import { execSync as execSync10, spawn, spawnSync as spawnSync5 } from "child_process";
|
|
12495
12499
|
import { promises as fs } from "fs";
|
|
12500
|
+
import { join as join31 } from "path";
|
|
12496
12501
|
init_display();
|
|
12502
|
+
async function writeRunnerMeta(dir, meta) {
|
|
12503
|
+
await fs.mkdir(dir, { recursive: true });
|
|
12504
|
+
const path = join31(dir, "runner-meta.json");
|
|
12505
|
+
await fs.writeFile(path, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
12506
|
+
}
|
|
12497
12507
|
function resolveRepoSlug() {
|
|
12498
12508
|
let rawUrl;
|
|
12499
12509
|
try {
|
|
@@ -12617,6 +12627,20 @@ async function startRunnerContainer(opts) {
|
|
|
12617
12627
|
);
|
|
12618
12628
|
}
|
|
12619
12629
|
}
|
|
12630
|
+
async function removeEnvEntries(keys, envPath = ".env") {
|
|
12631
|
+
let existing = "";
|
|
12632
|
+
try {
|
|
12633
|
+
existing = await fs.readFile(envPath, "utf-8");
|
|
12634
|
+
} catch (err) {
|
|
12635
|
+
if (err.code !== "ENOENT") throw err;
|
|
12636
|
+
return;
|
|
12637
|
+
}
|
|
12638
|
+
const lines = existing.split("\n");
|
|
12639
|
+
const filtered = lines.filter(
|
|
12640
|
+
(line) => !keys.some((key) => line.startsWith(`${key}=`))
|
|
12641
|
+
);
|
|
12642
|
+
await fs.writeFile(envPath, filtered.join("\n"), "utf-8");
|
|
12643
|
+
}
|
|
12620
12644
|
async function writeEnvEntries(entries, envPath = ".env") {
|
|
12621
12645
|
let existing = "";
|
|
12622
12646
|
try {
|
|
@@ -12669,15 +12693,326 @@ function setupStep(text) {
|
|
|
12669
12693
|
info(text);
|
|
12670
12694
|
}
|
|
12671
12695
|
|
|
12672
|
-
// src/cli/
|
|
12696
|
+
// src/cli/native-runner.ts
|
|
12697
|
+
import {
|
|
12698
|
+
execSync as execSync11,
|
|
12699
|
+
spawn as spawn2,
|
|
12700
|
+
spawnSync as spawnSync6
|
|
12701
|
+
} from "child_process";
|
|
12673
12702
|
import {
|
|
12703
|
+
createWriteStream,
|
|
12674
12704
|
existsSync as existsSync33,
|
|
12675
12705
|
mkdirSync as mkdirSync20,
|
|
12676
|
-
readFileSync as readFileSync31,
|
|
12677
12706
|
unlinkSync as unlinkSync5,
|
|
12678
12707
|
writeFileSync as writeFileSync27
|
|
12679
12708
|
} from "fs";
|
|
12680
|
-
import {
|
|
12709
|
+
import { homedir as homedir4 } from "os";
|
|
12710
|
+
import { join as join32, dirname as dirname8 } from "path";
|
|
12711
|
+
import { Readable } from "stream";
|
|
12712
|
+
import { pipeline } from "stream/promises";
|
|
12713
|
+
init_display();
|
|
12714
|
+
var RUNNER_VERSION = "2.322.0";
|
|
12715
|
+
function detectPlatform() {
|
|
12716
|
+
const platform3 = process.platform;
|
|
12717
|
+
if (platform3 === "win32") {
|
|
12718
|
+
throw new Error(
|
|
12719
|
+
"--native is not supported on Windows. Use Docker or WSL."
|
|
12720
|
+
);
|
|
12721
|
+
}
|
|
12722
|
+
if (platform3 !== "linux" && platform3 !== "darwin") {
|
|
12723
|
+
throw new Error(
|
|
12724
|
+
`--native is not supported on ${platform3}. Use Docker instead.`
|
|
12725
|
+
);
|
|
12726
|
+
}
|
|
12727
|
+
const arch = process.arch;
|
|
12728
|
+
if (arch !== "x64" && arch !== "arm64") {
|
|
12729
|
+
throw new Error(
|
|
12730
|
+
`Unsupported architecture: ${arch}. GitHub runners support x64 and arm64.`
|
|
12731
|
+
);
|
|
12732
|
+
}
|
|
12733
|
+
return { os: platform3, arch };
|
|
12734
|
+
}
|
|
12735
|
+
function runnerDownloadUrl(os, arch) {
|
|
12736
|
+
const ghOs = os === "darwin" ? "osx" : "linux";
|
|
12737
|
+
return `https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-${ghOs}-${arch}-${RUNNER_VERSION}.tar.gz`;
|
|
12738
|
+
}
|
|
12739
|
+
async function downloadAndExtractRunner(installDir, os, arch) {
|
|
12740
|
+
const runShPath = join32(installDir, "run.sh");
|
|
12741
|
+
if (existsSync33(runShPath)) {
|
|
12742
|
+
info(
|
|
12743
|
+
`Runner binary already present at ${runShPath} \u2014 skipping download.`
|
|
12744
|
+
);
|
|
12745
|
+
if (os === "darwin") {
|
|
12746
|
+
spawnSync6("xattr", ["-c", "-r", installDir], { stdio: "ignore" });
|
|
12747
|
+
}
|
|
12748
|
+
return;
|
|
12749
|
+
}
|
|
12750
|
+
const url = runnerDownloadUrl(os, arch);
|
|
12751
|
+
const tarball = join32(installDir, "runner.tar.gz");
|
|
12752
|
+
setupStep(
|
|
12753
|
+
`Downloading GitHub Actions runner v${RUNNER_VERSION} (${os}/${arch})...`
|
|
12754
|
+
);
|
|
12755
|
+
const resp = await fetch(url);
|
|
12756
|
+
if (!resp.ok || !resp.body) {
|
|
12757
|
+
throw new Error(
|
|
12758
|
+
`Failed to download runner: HTTP ${resp.status} from ${url}`
|
|
12759
|
+
);
|
|
12760
|
+
}
|
|
12761
|
+
await pipeline(
|
|
12762
|
+
Readable.fromWeb(resp.body),
|
|
12763
|
+
createWriteStream(tarball)
|
|
12764
|
+
);
|
|
12765
|
+
setupStep("Extracting runner...");
|
|
12766
|
+
try {
|
|
12767
|
+
execSync11("tar -xzf runner.tar.gz", { cwd: installDir, stdio: "ignore" });
|
|
12768
|
+
} catch (err) {
|
|
12769
|
+
throw new Error(
|
|
12770
|
+
`Failed to extract runner archive: ${err?.message ?? String(err)}`
|
|
12771
|
+
);
|
|
12772
|
+
}
|
|
12773
|
+
try {
|
|
12774
|
+
unlinkSync5(tarball);
|
|
12775
|
+
} catch {
|
|
12776
|
+
}
|
|
12777
|
+
if (os === "darwin") {
|
|
12778
|
+
setupStep("Removing macOS Gatekeeper quarantine attribute...");
|
|
12779
|
+
warn(
|
|
12780
|
+
"macOS Gatekeeper quarantine: running xattr -c -r on runner binaries."
|
|
12781
|
+
);
|
|
12782
|
+
warn(
|
|
12783
|
+
"This is required because macOS quarantines downloaded executables."
|
|
12784
|
+
);
|
|
12785
|
+
spawnSync6("xattr", ["-c", "-r", installDir], { stdio: "ignore" });
|
|
12786
|
+
}
|
|
12787
|
+
}
|
|
12788
|
+
async function configureRunner(installDir, repoUrl, token, name, labels) {
|
|
12789
|
+
setupStep("Configuring runner...");
|
|
12790
|
+
const args = [
|
|
12791
|
+
"--url",
|
|
12792
|
+
repoUrl,
|
|
12793
|
+
"--token",
|
|
12794
|
+
token,
|
|
12795
|
+
"--name",
|
|
12796
|
+
name,
|
|
12797
|
+
"--labels",
|
|
12798
|
+
labels.join(","),
|
|
12799
|
+
"--unattended",
|
|
12800
|
+
"--replace"
|
|
12801
|
+
];
|
|
12802
|
+
const result = spawnSync6(join32(installDir, "config.sh"), args, {
|
|
12803
|
+
cwd: installDir,
|
|
12804
|
+
stdio: ["ignore", "inherit", "pipe"],
|
|
12805
|
+
encoding: "utf-8"
|
|
12806
|
+
});
|
|
12807
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
12808
|
+
throw new Error(
|
|
12809
|
+
`Runner configuration failed: config.sh not found at ${installDir}/config.sh.`
|
|
12810
|
+
);
|
|
12811
|
+
}
|
|
12812
|
+
if (result.status !== 0) {
|
|
12813
|
+
const stderr = (result.stderr || "").replace(token, "<REDACTED>");
|
|
12814
|
+
throw new Error(
|
|
12815
|
+
`Runner configuration failed (exit ${result.status}): ${stderr}`
|
|
12816
|
+
);
|
|
12817
|
+
}
|
|
12818
|
+
}
|
|
12819
|
+
function startRunnerForeground(installDir) {
|
|
12820
|
+
setupStep("Starting runner in foreground mode (Ctrl+C to stop)...");
|
|
12821
|
+
info("The runner will stop when this terminal closes.");
|
|
12822
|
+
if (process.platform === "darwin") {
|
|
12823
|
+
warn(
|
|
12824
|
+
"macOS pitfall: if the terminal session disconnects, the runner dies."
|
|
12825
|
+
);
|
|
12826
|
+
warn(
|
|
12827
|
+
"Use --service to install as a launchd agent for persistent operation."
|
|
12828
|
+
);
|
|
12829
|
+
}
|
|
12830
|
+
spawn2(join32(installDir, "run.sh"), [], {
|
|
12831
|
+
cwd: installDir,
|
|
12832
|
+
stdio: "inherit"
|
|
12833
|
+
});
|
|
12834
|
+
}
|
|
12835
|
+
function systemdUnitContent(installDir, name) {
|
|
12836
|
+
return `[Unit]
|
|
12837
|
+
Description=GitHub Actions Runner (${name})
|
|
12838
|
+
After=network.target
|
|
12839
|
+
|
|
12840
|
+
[Service]
|
|
12841
|
+
Type=simple
|
|
12842
|
+
WorkingDirectory=${installDir}
|
|
12843
|
+
ExecStart=${installDir}/run.sh
|
|
12844
|
+
Restart=on-failure
|
|
12845
|
+
RestartSec=5
|
|
12846
|
+
KillSignal=SIGTERM
|
|
12847
|
+
TimeoutStopSec=30
|
|
12848
|
+
|
|
12849
|
+
[Install]
|
|
12850
|
+
WantedBy=default.target
|
|
12851
|
+
`;
|
|
12852
|
+
}
|
|
12853
|
+
async function installSystemdService(installDir, name) {
|
|
12854
|
+
const unitName = `beastmode-runner-${name}.service`;
|
|
12855
|
+
const unitDir = join32(homedir4(), ".config", "systemd", "user");
|
|
12856
|
+
mkdirSync20(unitDir, { recursive: true });
|
|
12857
|
+
writeFileSync27(
|
|
12858
|
+
join32(unitDir, unitName),
|
|
12859
|
+
systemdUnitContent(installDir, name),
|
|
12860
|
+
"utf-8"
|
|
12861
|
+
);
|
|
12862
|
+
try {
|
|
12863
|
+
execSync11("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
12864
|
+
execSync11(`systemctl --user enable --now ${unitName}`, {
|
|
12865
|
+
stdio: "inherit"
|
|
12866
|
+
});
|
|
12867
|
+
} catch (err) {
|
|
12868
|
+
throw new Error(
|
|
12869
|
+
`systemd user-service install failed: ${err?.message ?? String(err)}. Ensure systemd --user is available (some minimal Linux containers don't ship it).`
|
|
12870
|
+
);
|
|
12871
|
+
}
|
|
12872
|
+
success(`Installed and started systemd user service: ${unitName}`);
|
|
12873
|
+
info(` Check status: systemctl --user status ${unitName}`);
|
|
12874
|
+
info(` View logs: journalctl --user -u ${unitName} -f`);
|
|
12875
|
+
}
|
|
12876
|
+
function launchdPlistContent(installDir, name) {
|
|
12877
|
+
const label = `com.beastmode.runner.${name}`;
|
|
12878
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
12879
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
12880
|
+
<plist version="1.0">
|
|
12881
|
+
<dict>
|
|
12882
|
+
<key>Label</key>
|
|
12883
|
+
<string>${label}</string>
|
|
12884
|
+
<key>WorkingDirectory</key>
|
|
12885
|
+
<string>${installDir}</string>
|
|
12886
|
+
<key>ProgramArguments</key>
|
|
12887
|
+
<array>
|
|
12888
|
+
<string>${installDir}/run.sh</string>
|
|
12889
|
+
</array>
|
|
12890
|
+
<key>RunAtLoad</key>
|
|
12891
|
+
<true/>
|
|
12892
|
+
<key>KeepAlive</key>
|
|
12893
|
+
<dict>
|
|
12894
|
+
<key>SuccessfulExit</key>
|
|
12895
|
+
<false/>
|
|
12896
|
+
</dict>
|
|
12897
|
+
<key>StandardOutPath</key>
|
|
12898
|
+
<string>${installDir}/logs/stdout.log</string>
|
|
12899
|
+
<key>StandardErrorPath</key>
|
|
12900
|
+
<string>${installDir}/logs/stderr.log</string>
|
|
12901
|
+
<key>EnvironmentVariables</key>
|
|
12902
|
+
<dict>
|
|
12903
|
+
<key>PATH</key>
|
|
12904
|
+
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin</string>
|
|
12905
|
+
</dict>
|
|
12906
|
+
</dict>
|
|
12907
|
+
</plist>
|
|
12908
|
+
`;
|
|
12909
|
+
}
|
|
12910
|
+
async function installLaunchdService(installDir, name) {
|
|
12911
|
+
const label = `com.beastmode.runner.${name}`;
|
|
12912
|
+
const plistPath2 = join32(
|
|
12913
|
+
homedir4(),
|
|
12914
|
+
"Library",
|
|
12915
|
+
"LaunchAgents",
|
|
12916
|
+
`${label}.plist`
|
|
12917
|
+
);
|
|
12918
|
+
mkdirSync20(dirname8(plistPath2), { recursive: true });
|
|
12919
|
+
mkdirSync20(join32(installDir, "logs"), { recursive: true });
|
|
12920
|
+
writeFileSync27(plistPath2, launchdPlistContent(installDir, name), "utf-8");
|
|
12921
|
+
try {
|
|
12922
|
+
execSync11(`launchctl load ${plistPath2}`, { stdio: "inherit" });
|
|
12923
|
+
} catch (err) {
|
|
12924
|
+
throw new Error(
|
|
12925
|
+
`launchd agent install failed: ${err?.message ?? String(err)}.`
|
|
12926
|
+
);
|
|
12927
|
+
}
|
|
12928
|
+
success(`Installed and loaded launchd agent: ${label}`);
|
|
12929
|
+
info(` Check status: launchctl list | grep ${label}`);
|
|
12930
|
+
info(` View logs: tail -f ${installDir}/logs/stdout.log`);
|
|
12931
|
+
warn("macOS pitfall: launchd agents only run while the user is logged in.");
|
|
12932
|
+
warn(
|
|
12933
|
+
"If you reboot without auto-login, the runner won't start until you log in."
|
|
12934
|
+
);
|
|
12935
|
+
warn("For headless servers, use a LaunchDaemon (requires root) instead.");
|
|
12936
|
+
}
|
|
12937
|
+
async function installRunnerService(installDir, name, os) {
|
|
12938
|
+
if (os === "linux") {
|
|
12939
|
+
await installSystemdService(installDir, name);
|
|
12940
|
+
} else {
|
|
12941
|
+
await installLaunchdService(installDir, name);
|
|
12942
|
+
}
|
|
12943
|
+
}
|
|
12944
|
+
async function nativeRunnerSetup(opts) {
|
|
12945
|
+
if (opts.service !== void 0 && !opts.native) {
|
|
12946
|
+
throw new Error(
|
|
12947
|
+
"--service requires --native. The Docker runner always runs as a container service."
|
|
12948
|
+
);
|
|
12949
|
+
}
|
|
12950
|
+
const { os: platformOs, arch } = detectPlatform();
|
|
12951
|
+
if (platformOs === "darwin") {
|
|
12952
|
+
header("Native Runner Setup (macOS)");
|
|
12953
|
+
warn(
|
|
12954
|
+
"macOS has several pitfalls for self-hosted runners. Read the warnings below carefully."
|
|
12955
|
+
);
|
|
12956
|
+
} else {
|
|
12957
|
+
header("Native Runner Setup (Linux)");
|
|
12958
|
+
}
|
|
12959
|
+
const ghConfig = resolveGitHubConfig();
|
|
12960
|
+
const repoSlug = opts.repo ?? resolveRepoSlug();
|
|
12961
|
+
const installDir = join32(homedir4(), ".beastmode", "runners", opts.name);
|
|
12962
|
+
if (opts.dryRun) {
|
|
12963
|
+
info(
|
|
12964
|
+
`[dry-run] Would install native runner '${opts.name}' for repo ${repoSlug}`
|
|
12965
|
+
);
|
|
12966
|
+
info(`[dry-run] Platform: ${platformOs}/${arch}`);
|
|
12967
|
+
info(`[dry-run] Install dir: ${installDir}`);
|
|
12968
|
+
info(`[dry-run] Mode: ${opts.service ? "service" : "foreground"}`);
|
|
12969
|
+
return;
|
|
12970
|
+
}
|
|
12971
|
+
mkdirSync20(installDir, { recursive: true });
|
|
12972
|
+
setupStep("Generating registration token via GitHub API...");
|
|
12973
|
+
const { token: regToken } = await createRegistrationToken(ghConfig);
|
|
12974
|
+
await downloadAndExtractRunner(installDir, platformOs, arch);
|
|
12975
|
+
const repoUrl = `https://github.com/${ghConfig.owner}/${ghConfig.repo}`;
|
|
12976
|
+
await configureRunner(installDir, repoUrl, regToken, opts.name, [
|
|
12977
|
+
"self-hosted",
|
|
12978
|
+
opts.label
|
|
12979
|
+
]);
|
|
12980
|
+
await writeEnvEntries({
|
|
12981
|
+
RUNNER_REPO_URL: repoUrl,
|
|
12982
|
+
RUNNER_TOKEN: regToken,
|
|
12983
|
+
RUNNER_NAME: opts.name,
|
|
12984
|
+
RUNNER_MODE: "native"
|
|
12985
|
+
});
|
|
12986
|
+
await writeRunnerMeta(installDir, {
|
|
12987
|
+
name: opts.name,
|
|
12988
|
+
mode: "native",
|
|
12989
|
+
platform: platformOs,
|
|
12990
|
+
arch,
|
|
12991
|
+
installDir,
|
|
12992
|
+
service: opts.service ?? false,
|
|
12993
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12994
|
+
});
|
|
12995
|
+
if (opts.service) {
|
|
12996
|
+
await installRunnerService(installDir, opts.name, platformOs);
|
|
12997
|
+
} else {
|
|
12998
|
+
startRunnerForeground(installDir);
|
|
12999
|
+
}
|
|
13000
|
+
setupStep("Waiting for runner to appear online on GitHub (timeout 60s)...");
|
|
13001
|
+
await pollUntilOnline(ghConfig, opts.name, 6e4);
|
|
13002
|
+
success(
|
|
13003
|
+
`Runner '${opts.name}' registered and online (native/${platformOs}).`
|
|
13004
|
+
);
|
|
13005
|
+
}
|
|
13006
|
+
|
|
13007
|
+
// src/cli/workflow-switcher.ts
|
|
13008
|
+
import {
|
|
13009
|
+
existsSync as existsSync34,
|
|
13010
|
+
mkdirSync as mkdirSync21,
|
|
13011
|
+
readFileSync as readFileSync31,
|
|
13012
|
+
unlinkSync as unlinkSync6,
|
|
13013
|
+
writeFileSync as writeFileSync28
|
|
13014
|
+
} from "fs";
|
|
13015
|
+
import { dirname as dirname9, join as join33 } from "path";
|
|
12681
13016
|
var TARGET_LABEL = "[self-hosted, beastmode]";
|
|
12682
13017
|
var TARGET_WORKFLOWS = [
|
|
12683
13018
|
".github/workflows/test.yml",
|
|
@@ -12716,8 +13051,8 @@ function restoreRunsOn(content, originals) {
|
|
|
12716
13051
|
return newLines.join("\n");
|
|
12717
13052
|
}
|
|
12718
13053
|
async function switchWorkflows(projectDir) {
|
|
12719
|
-
const statePath =
|
|
12720
|
-
if (
|
|
13054
|
+
const statePath = join33(projectDir, STATE_FILE);
|
|
13055
|
+
if (existsSync34(statePath)) {
|
|
12721
13056
|
return { alreadySwitched: true, files: [] };
|
|
12722
13057
|
}
|
|
12723
13058
|
const state = {
|
|
@@ -12727,8 +13062,8 @@ async function switchWorkflows(projectDir) {
|
|
|
12727
13062
|
};
|
|
12728
13063
|
const resultFiles = [];
|
|
12729
13064
|
for (const relPath of TARGET_WORKFLOWS) {
|
|
12730
|
-
const absPath =
|
|
12731
|
-
if (!
|
|
13065
|
+
const absPath = join33(projectDir, relPath);
|
|
13066
|
+
if (!existsSync34(absPath)) {
|
|
12732
13067
|
throw new Error(`Workflow file not found: ${relPath}`);
|
|
12733
13068
|
}
|
|
12734
13069
|
const content = readFileSync31(absPath, "utf-8");
|
|
@@ -12736,17 +13071,17 @@ async function switchWorkflows(projectDir) {
|
|
|
12736
13071
|
if (originals.length === 0) {
|
|
12737
13072
|
throw new Error(`No runs-on found in ${relPath}`);
|
|
12738
13073
|
}
|
|
12739
|
-
|
|
13074
|
+
writeFileSync28(absPath, newContent, "utf-8");
|
|
12740
13075
|
state.files.push({ relativePath: relPath, originals });
|
|
12741
13076
|
resultFiles.push({ relativePath: relPath, jobCount: originals.length });
|
|
12742
13077
|
}
|
|
12743
|
-
|
|
12744
|
-
|
|
13078
|
+
mkdirSync21(dirname9(statePath), { recursive: true });
|
|
13079
|
+
writeFileSync28(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
12745
13080
|
return { alreadySwitched: false, files: resultFiles };
|
|
12746
13081
|
}
|
|
12747
13082
|
async function restoreWorkflows(projectDir) {
|
|
12748
|
-
const statePath =
|
|
12749
|
-
if (!
|
|
13083
|
+
const statePath = join33(projectDir, STATE_FILE);
|
|
13084
|
+
if (!existsSync34(statePath)) {
|
|
12750
13085
|
return { nothingToRestore: true, files: [] };
|
|
12751
13086
|
}
|
|
12752
13087
|
const state = JSON.parse(
|
|
@@ -12754,27 +13089,32 @@ async function restoreWorkflows(projectDir) {
|
|
|
12754
13089
|
);
|
|
12755
13090
|
const resultFiles = [];
|
|
12756
13091
|
for (const fileState of state.files) {
|
|
12757
|
-
const absPath =
|
|
12758
|
-
if (!
|
|
13092
|
+
const absPath = join33(projectDir, fileState.relativePath);
|
|
13093
|
+
if (!existsSync34(absPath)) {
|
|
12759
13094
|
throw new Error(`Workflow file not found: ${fileState.relativePath}`);
|
|
12760
13095
|
}
|
|
12761
13096
|
const content = readFileSync31(absPath, "utf-8");
|
|
12762
13097
|
const newContent = restoreRunsOn(content, fileState.originals);
|
|
12763
|
-
|
|
13098
|
+
writeFileSync28(absPath, newContent, "utf-8");
|
|
12764
13099
|
resultFiles.push({
|
|
12765
13100
|
relativePath: fileState.relativePath,
|
|
12766
13101
|
jobCount: fileState.originals.length
|
|
12767
13102
|
});
|
|
12768
13103
|
}
|
|
12769
|
-
|
|
13104
|
+
unlinkSync6(statePath);
|
|
12770
13105
|
return { nothingToRestore: false, files: resultFiles };
|
|
12771
13106
|
}
|
|
12772
13107
|
|
|
12773
13108
|
// src/cli/commands/runner-cmd.ts
|
|
12774
13109
|
var runnerCommand = new Command23("runner").description("Manage self-hosted GitHub Actions runners");
|
|
12775
13110
|
async function runnerSetupAction(opts) {
|
|
13111
|
+
if (opts.service !== void 0 && !opts.native) {
|
|
13112
|
+
throw new Error(
|
|
13113
|
+
"--service requires --native. The Docker runner always runs as a container service."
|
|
13114
|
+
);
|
|
13115
|
+
}
|
|
12776
13116
|
if (opts.native) {
|
|
12777
|
-
|
|
13117
|
+
await nativeRunnerSetup(opts);
|
|
12778
13118
|
return;
|
|
12779
13119
|
}
|
|
12780
13120
|
const ghConfig = resolveGitHubConfig();
|
|
@@ -12815,11 +13155,19 @@ async function runnerSetupAction(opts) {
|
|
|
12815
13155
|
await pollUntilOnline(ghConfig, opts.name, 6e4);
|
|
12816
13156
|
setupStep("Adding runner to docker-compose...");
|
|
12817
13157
|
await composeUpRunner();
|
|
13158
|
+
await writeRunnerMeta(".beastmode", {
|
|
13159
|
+
name: opts.name,
|
|
13160
|
+
mode: "docker",
|
|
13161
|
+
platform: process.platform,
|
|
13162
|
+
arch: process.arch,
|
|
13163
|
+
service: true,
|
|
13164
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13165
|
+
});
|
|
12818
13166
|
success(`Runner '${opts.name}' registered and online.`);
|
|
12819
13167
|
}
|
|
12820
13168
|
async function composeUpRunner() {
|
|
12821
13169
|
await new Promise((resolve21, reject) => {
|
|
12822
|
-
const child =
|
|
13170
|
+
const child = spawn3(
|
|
12823
13171
|
"docker",
|
|
12824
13172
|
["compose", "--profile", "runner", "up", "-d", "runner"],
|
|
12825
13173
|
{ stdio: ["ignore", "inherit", "inherit"] }
|
|
@@ -12836,14 +13184,160 @@ async function composeUpRunner() {
|
|
|
12836
13184
|
});
|
|
12837
13185
|
});
|
|
12838
13186
|
}
|
|
12839
|
-
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(
|
|
12840
|
-
|
|
13187
|
+
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(
|
|
13188
|
+
"--native",
|
|
13189
|
+
"Use native runner binary instead of Docker (downloads actions/runner)"
|
|
13190
|
+
).option(
|
|
13191
|
+
"--service",
|
|
13192
|
+
"Install as background service (systemd/launchd). Requires --native."
|
|
13193
|
+
).action(async (opts) => {
|
|
13194
|
+
try {
|
|
13195
|
+
await runnerSetupAction(opts);
|
|
13196
|
+
} catch (err) {
|
|
13197
|
+
error(err?.message ? String(err.message) : String(err));
|
|
13198
|
+
process.exitCode = 1;
|
|
13199
|
+
}
|
|
12841
13200
|
});
|
|
12842
|
-
|
|
12843
|
-
|
|
13201
|
+
async function runnerStatusAction(opts) {
|
|
13202
|
+
const container = await findContainerByName(opts.name);
|
|
13203
|
+
const containerStatus = container ? container.status : null;
|
|
13204
|
+
let ghRunner = null;
|
|
13205
|
+
let ghError = null;
|
|
13206
|
+
try {
|
|
13207
|
+
const ghConfig = resolveGitHubConfig();
|
|
13208
|
+
const list = await listRunners(ghConfig);
|
|
13209
|
+
ghRunner = list.runners.find((r) => r.name === opts.name) ?? null;
|
|
13210
|
+
} catch (err) {
|
|
13211
|
+
ghError = err?.message ? String(err.message) : String(err);
|
|
13212
|
+
}
|
|
13213
|
+
if (opts.json) {
|
|
13214
|
+
const result = {
|
|
13215
|
+
name: opts.name,
|
|
13216
|
+
container: { found: !!container, status: containerStatus }
|
|
13217
|
+
};
|
|
13218
|
+
if (ghRunner) {
|
|
13219
|
+
result.github = {
|
|
13220
|
+
registered: true,
|
|
13221
|
+
id: ghRunner.id,
|
|
13222
|
+
status: ghRunner.status,
|
|
13223
|
+
busy: ghRunner.busy,
|
|
13224
|
+
os: ghRunner.os,
|
|
13225
|
+
labels: ghRunner.labels.map((l) => l.name)
|
|
13226
|
+
};
|
|
13227
|
+
} else if (ghError) {
|
|
13228
|
+
result.github = { registered: null, error: ghError };
|
|
13229
|
+
} else {
|
|
13230
|
+
result.github = {
|
|
13231
|
+
registered: false,
|
|
13232
|
+
id: null,
|
|
13233
|
+
status: null,
|
|
13234
|
+
busy: null,
|
|
13235
|
+
os: null,
|
|
13236
|
+
labels: []
|
|
13237
|
+
};
|
|
13238
|
+
}
|
|
13239
|
+
console.log(JSON.stringify(result, null, 2));
|
|
13240
|
+
return;
|
|
13241
|
+
}
|
|
13242
|
+
header(`Runner Status: ${opts.name}`);
|
|
13243
|
+
if (container && containerIsHealthy(container)) {
|
|
13244
|
+
success(`Container: ${container.status}`);
|
|
13245
|
+
} else if (container) {
|
|
13246
|
+
error(`Container: ${container.status}`);
|
|
13247
|
+
} else {
|
|
13248
|
+
error("Container: not found");
|
|
13249
|
+
}
|
|
13250
|
+
if (ghRunner) {
|
|
13251
|
+
const busySuffix = ghRunner.busy ? " \u2014 busy" : "";
|
|
13252
|
+
if (ghRunner.status === "online") {
|
|
13253
|
+
success(`GitHub: online (id: ${ghRunner.id})${busySuffix}`);
|
|
13254
|
+
} else {
|
|
13255
|
+
warn(`GitHub: ${ghRunner.status} (id: ${ghRunner.id})${busySuffix}`);
|
|
13256
|
+
}
|
|
13257
|
+
info(`Labels: ${ghRunner.labels.map((l) => l.name).join(", ")}`);
|
|
13258
|
+
info(`OS: ${ghRunner.os}`);
|
|
13259
|
+
info(`Busy: ${ghRunner.busy ? "yes" : "no"}`);
|
|
13260
|
+
} else if (ghError) {
|
|
13261
|
+
warn(`GitHub: could not reach API (${ghError})`);
|
|
13262
|
+
} else {
|
|
13263
|
+
error("GitHub: not registered");
|
|
13264
|
+
}
|
|
13265
|
+
}
|
|
13266
|
+
async function runnerRemoveAction(opts) {
|
|
13267
|
+
header(`Removing runner: ${opts.name}`);
|
|
13268
|
+
let ghDeregistrationFailed = false;
|
|
13269
|
+
let ghWarning = null;
|
|
13270
|
+
try {
|
|
13271
|
+
const ghConfig = resolveGitHubConfig();
|
|
13272
|
+
const list = await listRunners(ghConfig);
|
|
13273
|
+
const runner = list.runners.find((r) => r.name === opts.name);
|
|
13274
|
+
if (!runner) {
|
|
13275
|
+
ghWarning = "not registered (nothing to deregister)";
|
|
13276
|
+
} else {
|
|
13277
|
+
if (runner.busy && !opts.force) {
|
|
13278
|
+
warn(`Runner '${opts.name}' is currently busy (running a job).`);
|
|
13279
|
+
info("Use --force to remove anyway.");
|
|
13280
|
+
return;
|
|
13281
|
+
}
|
|
13282
|
+
try {
|
|
13283
|
+
await deleteRunner(ghConfig, runner.id);
|
|
13284
|
+
success(`Deregistered from GitHub (runner id: ${runner.id})`);
|
|
13285
|
+
} catch (err) {
|
|
13286
|
+
ghDeregistrationFailed = true;
|
|
13287
|
+
ghWarning = `deregistration failed: ${err?.message ?? String(err)}`;
|
|
13288
|
+
}
|
|
13289
|
+
}
|
|
13290
|
+
} catch (err) {
|
|
13291
|
+
ghDeregistrationFailed = true;
|
|
13292
|
+
ghWarning = err?.message ? String(err.message) : String(err);
|
|
13293
|
+
}
|
|
13294
|
+
if (ghWarning) {
|
|
13295
|
+
warn(`GitHub: ${ghWarning}`);
|
|
13296
|
+
}
|
|
13297
|
+
let containerWarning = null;
|
|
13298
|
+
try {
|
|
13299
|
+
const container = await findContainerByName(opts.name);
|
|
13300
|
+
if (!container) {
|
|
13301
|
+
containerWarning = "not found (nothing to remove)";
|
|
13302
|
+
} else {
|
|
13303
|
+
try {
|
|
13304
|
+
await removeContainer(opts.name);
|
|
13305
|
+
success("Container removed");
|
|
13306
|
+
} catch (err) {
|
|
13307
|
+
containerWarning = err?.message ? String(err.message) : String(err);
|
|
13308
|
+
}
|
|
13309
|
+
}
|
|
13310
|
+
} catch (err) {
|
|
13311
|
+
containerWarning = err?.message ? String(err.message) : String(err);
|
|
13312
|
+
}
|
|
13313
|
+
if (containerWarning) {
|
|
13314
|
+
warn(`Container: ${containerWarning}`);
|
|
13315
|
+
}
|
|
13316
|
+
await removeEnvEntries(["RUNNER_REPO_URL", "RUNNER_TOKEN", "RUNNER_NAME"]);
|
|
13317
|
+
success(".env entries cleaned (RUNNER_REPO_URL, RUNNER_TOKEN, RUNNER_NAME)");
|
|
13318
|
+
if (ghDeregistrationFailed) {
|
|
13319
|
+
warn(
|
|
13320
|
+
`Runner '${opts.name}' partially removed (GitHub deregistration failed).`
|
|
13321
|
+
);
|
|
13322
|
+
} else {
|
|
13323
|
+
success(`Runner '${opts.name}' fully removed.`);
|
|
13324
|
+
}
|
|
13325
|
+
}
|
|
13326
|
+
runnerCommand.command("status").description("Show runner status (container + GitHub registration)").option("--name <name>", "Container + runner name", "beastmode-runner").option("--json", "Output machine-readable JSON").action(async (opts) => {
|
|
13327
|
+
try {
|
|
13328
|
+
await runnerStatusAction(opts);
|
|
13329
|
+
} catch (err) {
|
|
13330
|
+
error(err.message);
|
|
13331
|
+
process.exitCode = 1;
|
|
13332
|
+
}
|
|
12844
13333
|
});
|
|
12845
|
-
runnerCommand.command("remove").description("Remove the runner (container + GitHub deregistration)").action(() => {
|
|
12846
|
-
|
|
13334
|
+
runnerCommand.command("remove").description("Remove the runner (container + GitHub deregistration)").option("--name <name>", "Container + runner name", "beastmode-runner").option("--force", "Remove even if the runner is busy").action(async (opts) => {
|
|
13335
|
+
try {
|
|
13336
|
+
await runnerRemoveAction(opts);
|
|
13337
|
+
} catch (err) {
|
|
13338
|
+
error(err.message);
|
|
13339
|
+
process.exitCode = 1;
|
|
13340
|
+
}
|
|
12847
13341
|
});
|
|
12848
13342
|
runnerCommand.command("switch-workflows").description("Switch workflow runs-on to self-hosted runner").option("--project-dir <path>", "Project root directory", process.cwd()).action(async (opts) => {
|
|
12849
13343
|
const projectDir = resolve20(opts.projectDir);
|