@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 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 pipeline = factoryConfig.pipeline;
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: pipeline.satisfaction_threshold,
2723
+ satisfaction_threshold: pipeline2.satisfaction_threshold,
2724
2724
  timeout_minutes: 30,
2725
- prod_accept_floor: pipeline.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: pipeline.max_iterations,
2749
- fail_fast_threshold: pipeline.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: spawn3 } = await import("child_process");
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 = spawn3(spawnCmd, spawnArgs, {
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: execSync11 } = await import("child_process");
11405
- worktreeOutput = execSync11("git worktree list", {
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: execSync11 } = await import("child_process");
11578
+ const { execSync: execSync12 } = await import("child_process");
11579
11579
  let pythonAvailable = false;
11580
11580
  try {
11581
- execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
11581
+ execSync12("python --version", { timeout: 5e3, encoding: "utf-8" });
11582
11582
  pythonAvailable = true;
11583
11583
  } catch {
11584
11584
  try {
11585
- execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
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: spawn3 } = await import("child_process");
11614
- const child = spawn3(cmd.command, cmd.args, {
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: execSync11 } = await import("child_process");
11738
+ const { execSync: execSync12 } = await import("child_process");
11739
11739
  let pythonCmd = "python";
11740
11740
  let pythonAvailable = false;
11741
11741
  try {
11742
- execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
11742
+ execSync12("python --version", { timeout: 5e3, encoding: "utf-8" });
11743
11743
  pythonAvailable = true;
11744
11744
  pythonCmd = "python";
11745
11745
  } catch {
11746
11746
  try {
11747
- execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
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: spawn3 } = await import("child_process");
11766
- const child = spawn3(pythonCmd, cmd.args, {
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: unlinkSync6 } = __require("fs");
11787
- unlinkSync6(pidFile);
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 spawn2 } from "child_process";
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/workflow-switcher.ts
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 { dirname as dirname8, join as join31 } from "path";
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 = join31(projectDir, STATE_FILE);
12720
- if (existsSync33(statePath)) {
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 = join31(projectDir, relPath);
12731
- if (!existsSync33(absPath)) {
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
- writeFileSync27(absPath, newContent, "utf-8");
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
- mkdirSync20(dirname8(statePath), { recursive: true });
12744
- writeFileSync27(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
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 = join31(projectDir, STATE_FILE);
12749
- if (!existsSync33(statePath)) {
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 = join31(projectDir, fileState.relativePath);
12758
- if (!existsSync33(absPath)) {
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
- writeFileSync27(absPath, newContent, "utf-8");
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
- unlinkSync5(statePath);
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
- warn("--native is not implemented yet (Story 5)");
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 = spawn2(
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("--native", "Use native install instead of Docker").action(async (opts) => {
12840
- await runnerSetupAction(opts);
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
- runnerCommand.command("status").description("Show runner status (container + GitHub registration)").action(() => {
12843
- warn("runner status is not implemented yet (Story 3)");
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
- warn("runner remove is not implemented yet (Story 3)");
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);