@beastmode-develeap/beastmode 0.1.145 → 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 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 = spawn(spawnCmd, spawnArgs, {
4438
+ const child = spawn3(spawnCmd, spawnArgs, {
4439
4439
  cwd: session.factoryPath,
4440
4440
  env: {
4441
4441
  ...process.env,
@@ -6959,28 +6959,214 @@ var init_server = __esm({
6959
6959
  }
6960
6960
  });
6961
6961
 
6962
+ // src/cli/commands/board.ts
6963
+ import { Command } from "commander";
6964
+ import { resolve as resolve5, join as join15 } from "path";
6965
+ import { existsSync as existsSync17, readFileSync as readFileSync14, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13, readdirSync as readdirSync7 } from "fs";
6966
+ import { execSync as execSync4 } from "child_process";
6967
+ function findFactoryDir(startDir) {
6968
+ let dir = startDir || process.cwd();
6969
+ const root = resolve5("/");
6970
+ while (dir !== root) {
6971
+ if (existsSync17(join15(dir, ".beastmode", "factory.json"))) {
6972
+ return dir;
6973
+ }
6974
+ const parent = resolve5(dir, "..");
6975
+ if (parent === dir) break;
6976
+ dir = parent;
6977
+ }
6978
+ if (existsSync17(join15(dir, ".beastmode", "factory.json"))) {
6979
+ return dir;
6980
+ }
6981
+ return null;
6982
+ }
6983
+ function inferProjectName(factoryDir) {
6984
+ const projectsDir = join15(factoryDir, ".beastmode", "projects");
6985
+ if (!existsSync17(projectsDir)) return null;
6986
+ const files = readdirSync7(projectsDir).filter((f) => f.endsWith(".json"));
6987
+ if (files.length === 1) return files[0].replace(/\.json$/, "");
6988
+ return null;
6989
+ }
6990
+ function tryExecSync(cmd, timeoutMs = 15e3) {
6991
+ try {
6992
+ return execSync4(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs }).trim();
6993
+ } catch {
6994
+ return null;
6995
+ }
6996
+ }
6997
+ async function setItemStatus(itemId, status, opts) {
6998
+ if (!itemId || !/^\d+$/.test(itemId)) {
6999
+ throw new Error(`Invalid item_id: ${itemId} (expected numeric id)`);
7000
+ }
7001
+ const trimmedStatus = status.trim();
7002
+ if (!trimmedStatus) {
7003
+ throw new Error("status cannot be empty");
7004
+ }
7005
+ let project = opts.project;
7006
+ if (!project) {
7007
+ const factoryDir = findFactoryDir();
7008
+ if (!factoryDir) {
7009
+ throw new Error("No .beastmode factory found in cwd tree. Pass --project <name> explicitly.");
7010
+ }
7011
+ const inferred = inferProjectName(factoryDir);
7012
+ if (!inferred) {
7013
+ throw new Error(
7014
+ "Could not determine project name from factory (0 or >1 projects configured). Pass --project <name>."
7015
+ );
7016
+ }
7017
+ project = inferred;
7018
+ }
7019
+ const payload = JSON.stringify({ status: trimmedStatus });
7020
+ if (opts.boardUrl) {
7021
+ const url2 = `${opts.boardUrl.replace(/\/+$/, "")}/api/items/${itemId}?board=${encodeURIComponent(project)}`;
7022
+ const cmd2 = `curl -sS -X PATCH '${url2}' -H 'Content-Type: application/json' -d '${payload.replace(/'/g, "'\\''")}'`;
7023
+ const out2 = tryExecSync(cmd2);
7024
+ if (!out2) throw new Error(`curl failed hitting ${url2}`);
7025
+ info(out2);
7026
+ return;
7027
+ }
7028
+ const container = tryExecSync(`docker ps --filter 'label=com.docker.compose.service=daemon' --format '{{.Names}}' | head -n1`) || tryExecSync(`docker ps --filter 'label=com.docker.compose.service=ui' --format '{{.Names}}' | head -n1`);
7029
+ if (!container) {
7030
+ throw new Error(
7031
+ "No running daemon or ui container found. Start the factory with `docker compose up -d`, or pass --board-url."
7032
+ );
7033
+ }
7034
+ const escapedPayload = payload.replace(/'/g, "'\\''");
7035
+ const url = `http://board:8080/api/items/${itemId}?board=${encodeURIComponent(project)}`;
7036
+ const cmd = `docker exec ${container} curl -sS -X PATCH '${url}' -H 'Content-Type: application/json' -d '${escapedPayload}'`;
7037
+ const out = tryExecSync(cmd);
7038
+ if (!out) {
7039
+ throw new Error(`docker exec curl failed hitting ${url} via ${container}`);
7040
+ }
7041
+ try {
7042
+ const parsed = JSON.parse(out);
7043
+ if (parsed.detail) {
7044
+ throw new Error(`Board rejected update: ${parsed.detail}`);
7045
+ }
7046
+ if (parsed.id && parsed.status) {
7047
+ info(`Item ${parsed.id} \u2192 ${parsed.status} (project=${project})`);
7048
+ return;
7049
+ }
7050
+ } catch (e) {
7051
+ if (e instanceof Error && e.message.startsWith("Board rejected update:")) throw e;
7052
+ }
7053
+ info(out);
7054
+ }
7055
+ async function runBoard(opts) {
7056
+ let factoryDir = findFactoryDir();
7057
+ if (!factoryDir) {
7058
+ factoryDir = process.cwd();
7059
+ const bmDir = join15(factoryDir, ".beastmode");
7060
+ if (!existsSync17(bmDir)) {
7061
+ mkdirSync12(bmDir, { recursive: true });
7062
+ }
7063
+ const factoryJsonPath2 = join15(bmDir, "factory.json");
7064
+ if (!existsSync17(factoryJsonPath2)) {
7065
+ writeFileSync13(
7066
+ factoryJsonPath2,
7067
+ JSON.stringify({ factory_name: "BeastMode", created_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
7068
+ "utf-8"
7069
+ );
7070
+ info("No factory found \u2014 created minimal stub at .beastmode/factory.json");
7071
+ }
7072
+ }
7073
+ const factoryJsonPath = join15(factoryDir, ".beastmode", "factory.json");
7074
+ const factoryJson = JSON.parse(readFileSync14(factoryJsonPath, "utf-8"));
7075
+ const factoryName = factoryJson.factory_name || "BeastMode Factory";
7076
+ header(`BeastMode Board \u2014 ${factoryName}`);
7077
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
7078
+ const port = parseInt(opts.port, 10) || 7669;
7079
+ const uiServer = await startServer2({
7080
+ port,
7081
+ host: opts.host,
7082
+ factoryName,
7083
+ factoryPath: factoryDir,
7084
+ mode: "board"
7085
+ });
7086
+ info(`Board running at ${uiServer.url}/board`);
7087
+ info("Press Ctrl+C to stop.\n");
7088
+ const skipOpen = process.env.BEASTMODE_NO_OPEN === "1" || !!process.env.DOCKER_CONTAINER;
7089
+ if (!skipOpen) {
7090
+ try {
7091
+ const open = (await import("open")).default;
7092
+ await open(`${uiServer.url}/board`);
7093
+ } catch {
7094
+ info(`Open ${uiServer.url}/board in your browser.`);
7095
+ }
7096
+ } else {
7097
+ info(`Open ${uiServer.url}/board in your browser.`);
7098
+ }
7099
+ await new Promise((resolvePromise) => {
7100
+ uiServer.server.on("close", resolvePromise);
7101
+ process.on("SIGINT", async () => {
7102
+ info("\nShutting down board server...");
7103
+ await uiServer.shutdown();
7104
+ resolvePromise();
7105
+ });
7106
+ });
7107
+ }
7108
+ var boardCommand;
7109
+ var init_board = __esm({
7110
+ "src/cli/commands/board.ts"() {
7111
+ "use strict";
7112
+ init_display();
7113
+ boardCommand = new Command("board").description("Launch the BeastMode Board web UI").option("--port <number>", "Port to serve on", "7669").option("--host <host>", "Host to bind to (use 0.0.0.0 for external access)", "127.0.0.1").action(async (opts) => {
7114
+ try {
7115
+ await runBoard(opts);
7116
+ } catch (err) {
7117
+ error(err.message);
7118
+ process.exit(1);
7119
+ }
7120
+ });
7121
+ boardCommand.command("set-status <item_id> <status>").description(
7122
+ "Safely change a board item's status via the board HTTP API (never sqlite3 against the live DB \u2014 Gap 24)"
7123
+ ).option("--project <name>", "Project/board name (defaults to the factory's only project, or errors if ambiguous)").option("--board-url <url>", "Override board HTTP URL (default: use running board container via docker exec)").action(async (itemId, status, opts) => {
7124
+ try {
7125
+ await setItemStatus(itemId, status, opts);
7126
+ } catch (err) {
7127
+ error(err.message);
7128
+ process.exit(1);
7129
+ }
7130
+ });
7131
+ }
7132
+ });
7133
+
6962
7134
  // src/cli/commands/sync-claude-creds.ts
6963
7135
  var sync_claude_creds_exports = {};
6964
7136
  __export(sync_claude_creds_exports, {
6965
7137
  installAgent: () => installAgent,
7138
+ installAgentLinux: () => installAgentLinux,
6966
7139
  readKeychainTokenSafe: () => readKeychainTokenSafe,
6967
7140
  syncClaudeCredsCommand: () => syncClaudeCredsCommand,
7141
+ syncClaudeCredsLinux: () => syncClaudeCredsLinux,
6968
7142
  syncClaudeCredsOnce: () => syncClaudeCredsOnce
6969
7143
  });
6970
- import { Command } from "commander";
6971
- import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
6972
- import { writeFileSync as writeFileSync13, chmodSync, mkdirSync as mkdirSync12, existsSync as existsSync17, unlinkSync as unlinkSync4 } from "fs";
6973
- import { join as join15 } from "path";
7144
+ import { Command as Command2 } from "commander";
7145
+ import { execSync as execSync5, spawnSync as spawnSync2 } from "child_process";
7146
+ import { writeFileSync as writeFileSync14, readFileSync as readFileSync15, chmodSync, mkdirSync as mkdirSync13, existsSync as existsSync18, unlinkSync as unlinkSync4 } from "fs";
7147
+ import { join as join16 } from "path";
6974
7148
  import { homedir as homedir2, platform } from "os";
7149
+ function systemdUserDir() {
7150
+ return join16(homedir2(), ".config", "systemd", "user");
7151
+ }
7152
+ function systemdServicePath() {
7153
+ return join16(systemdUserDir(), `${SYSTEMD_UNIT_NAME}.service`);
7154
+ }
7155
+ function systemdTimerPath() {
7156
+ return join16(systemdUserDir(), `${SYSTEMD_UNIT_NAME}.timer`);
7157
+ }
7158
+ function linuxCredsPath() {
7159
+ return join16(homedir2(), ".claude", ".credentials.json");
7160
+ }
6975
7161
  function plistPath() {
6976
- return join15(homedir2(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`);
7162
+ return join16(homedir2(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`);
6977
7163
  }
6978
7164
  function agentLogPath() {
6979
- return join15(homedir2(), ".beastmode", "logs", "sync-claude-creds.log");
7165
+ return join16(homedir2(), ".beastmode", "logs", "sync-claude-creds.log");
6980
7166
  }
6981
7167
  function readKeychainTokenSafe() {
6982
7168
  try {
6983
- return execSync4(
7169
+ return execSync5(
6984
7170
  `security find-generic-password -s "Claude Code-credentials" -w`,
6985
7171
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
6986
7172
  ).trim();
@@ -7008,10 +7194,10 @@ function writeCredentialsFile(rawJson) {
7008
7194
  if (!oauth?.accessToken) {
7009
7195
  throw new Error("Keychain entry missing claudeAiOauth.accessToken. Fix: run `claude login` again to reset the credential.");
7010
7196
  }
7011
- const claudeDir = join15(homedir2(), ".claude");
7012
- if (!existsSync17(claudeDir)) mkdirSync12(claudeDir, { recursive: true });
7013
- const credsPath = join15(claudeDir, ".credentials.json");
7014
- writeFileSync13(credsPath, rawJson + "\n", "utf-8");
7197
+ const claudeDir = join16(homedir2(), ".claude");
7198
+ if (!existsSync18(claudeDir)) mkdirSync13(claudeDir, { recursive: true });
7199
+ const credsPath = join16(claudeDir, ".credentials.json");
7200
+ writeFileSync14(credsPath, rawJson + "\n", "utf-8");
7015
7201
  chmodSync(credsPath, 384);
7016
7202
  if (oauth.expiresAt) {
7017
7203
  const hoursLeft = Math.round((oauth.expiresAt - Date.now()) / 36e5);
@@ -7023,10 +7209,31 @@ function writeCredentialsFile(rawJson) {
7023
7209
  }
7024
7210
  return credsPath;
7025
7211
  }
7026
- function buildPlist(intervalSeconds) {
7212
+ function urgencyMarkerHostPath(factoryDir) {
7213
+ return join16(factoryDir, "daemon", "logs", ".cred-refresh-needed");
7214
+ }
7215
+ function removeUrgencyMarker(factoryDir) {
7216
+ if (!factoryDir) return;
7217
+ const markerPath = urgencyMarkerHostPath(factoryDir);
7218
+ try {
7219
+ unlinkSync4(markerPath);
7220
+ } catch {
7221
+ }
7222
+ }
7223
+ function buildPlist(intervalSeconds, factoryDir) {
7027
7224
  const nodePath = process.execPath;
7028
7225
  const cliEntry = process.argv[1];
7029
7226
  const logPath = agentLogPath();
7227
+ const keychainPath = join16(homedir2(), "Library", "Keychains", "login.keychain-db");
7228
+ const watchPaths = [keychainPath];
7229
+ if (factoryDir) {
7230
+ watchPaths.push(urgencyMarkerHostPath(factoryDir));
7231
+ }
7232
+ const watchPathsXml = ` <key>WatchPaths</key>
7233
+ <array>
7234
+ ${watchPaths.map((p) => ` <string>${p}</string>`).join("\n")}
7235
+ </array>
7236
+ `;
7030
7237
  return `<?xml version="1.0" encoding="UTF-8"?>
7031
7238
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7032
7239
  <plist version="1.0">
@@ -7043,7 +7250,7 @@ function buildPlist(intervalSeconds) {
7043
7250
  <integer>${intervalSeconds}</integer>
7044
7251
  <key>RunAtLoad</key>
7045
7252
  <true/>
7046
- <key>StandardOutPath</key>
7253
+ ${watchPathsXml} <key>StandardOutPath</key>
7047
7254
  <string>${logPath}</string>
7048
7255
  <key>StandardErrorPath</key>
7049
7256
  <string>${logPath}</string>
@@ -7051,17 +7258,21 @@ function buildPlist(intervalSeconds) {
7051
7258
  </plist>
7052
7259
  `;
7053
7260
  }
7054
- function installAgent(intervalSeconds, { throwOnError = false } = {}) {
7261
+ function installAgent(intervalSeconds, {
7262
+ throwOnError = false,
7263
+ factoryDir
7264
+ } = {}) {
7055
7265
  const plist = plistPath();
7056
7266
  const logPath = agentLogPath();
7057
- mkdirSync12(join15(homedir2(), "Library", "LaunchAgents"), { recursive: true });
7058
- mkdirSync12(join15(homedir2(), ".beastmode", "logs"), { recursive: true });
7267
+ mkdirSync13(join16(homedir2(), "Library", "LaunchAgents"), { recursive: true });
7268
+ mkdirSync13(join16(homedir2(), ".beastmode", "logs"), { recursive: true });
7059
7269
  const uid = process.getuid?.();
7060
- if (existsSync17(plist)) {
7270
+ if (existsSync18(plist)) {
7061
7271
  spawnSync2("launchctl", ["bootout", `gui/${uid}`, plist], { stdio: "pipe" });
7062
7272
  }
7063
- writeFileSync13(plist, buildPlist(intervalSeconds), "utf-8");
7064
- writeFileSync13(logPath, "", { flag: "a" });
7273
+ const resolvedFactory = factoryDir ?? findFactoryDir() ?? void 0;
7274
+ writeFileSync14(plist, buildPlist(intervalSeconds, resolvedFactory), "utf-8");
7275
+ writeFileSync14(logPath, "", { flag: "a" });
7065
7276
  const result = spawnSync2("launchctl", ["bootstrap", `gui/${uid}`, plist], {
7066
7277
  stdio: "pipe",
7067
7278
  encoding: "utf-8"
@@ -7080,7 +7291,7 @@ function installAgent(intervalSeconds, { throwOnError = false } = {}) {
7080
7291
  info(` Plist: ${plist}`);
7081
7292
  info(` Interval: every ${Math.round(intervalSeconds / 60)} minutes`);
7082
7293
  info(` Logs: ${logPath}`);
7083
- info(` Runs at: load + every ${Math.round(intervalSeconds / 60)}min (while you're logged in)`);
7294
+ info(` Triggers: load + every ${Math.round(intervalSeconds / 60)}min + Keychain change`);
7084
7295
  console.log();
7085
7296
  info("The agent also runs once right now (RunAtLoad=true) \u2014 credentials synced.");
7086
7297
  }
@@ -7099,7 +7310,7 @@ function syncClaudeCredsOnce() {
7099
7310
  function uninstallAgent() {
7100
7311
  const plist = plistPath();
7101
7312
  const uid = process.getuid?.();
7102
- if (!existsSync17(plist)) {
7313
+ if (!existsSync18(plist)) {
7103
7314
  info("No LaunchAgent installed \u2014 nothing to remove.");
7104
7315
  return;
7105
7316
  }
@@ -7119,7 +7330,7 @@ function uninstallAgent() {
7119
7330
  function showStatus() {
7120
7331
  const plist = plistPath();
7121
7332
  const uid = process.getuid?.();
7122
- if (!existsSync17(plist)) {
7333
+ if (!existsSync18(plist)) {
7123
7334
  info("LaunchAgent not installed.");
7124
7335
  info("Install with: beastmode sync-claude-creds --install");
7125
7336
  return;
@@ -7141,18 +7352,257 @@ function showStatus() {
7141
7352
  warn(`Plist exists but agent not loaded. Re-install with: beastmode sync-claude-creds --install`);
7142
7353
  }
7143
7354
  }
7144
- var LAUNCH_AGENT_LABEL, syncClaudeCredsCommand;
7355
+ function syncClaudeCredsLinux() {
7356
+ const credsPath = linuxCredsPath();
7357
+ if (!existsSync18(credsPath)) {
7358
+ return {
7359
+ error: `${credsPath} not found \u2014 run \`claude login\` first`
7360
+ };
7361
+ }
7362
+ let parsed;
7363
+ try {
7364
+ const raw = readFileSync15(credsPath, "utf-8");
7365
+ parsed = JSON.parse(raw);
7366
+ } catch {
7367
+ return {
7368
+ error: `${credsPath} exists but is not valid JSON \u2014 run \`claude login\` to reset`
7369
+ };
7370
+ }
7371
+ const oauth = parsed.claudeAiOauth;
7372
+ if (!oauth?.accessToken) {
7373
+ return {
7374
+ error: `${credsPath} missing claudeAiOauth.accessToken \u2014 run \`claude login\``
7375
+ };
7376
+ }
7377
+ if (oauth.expiresAt) {
7378
+ const minutesLeft = (oauth.expiresAt - Date.now()) / 6e4;
7379
+ if (minutesLeft < 60) {
7380
+ info(
7381
+ `Token expiring in ${Math.round(minutesLeft)}min \u2014 triggering refresh via \`claude --print ping\`...`
7382
+ );
7383
+ const result = spawnSync2("claude", ["--print", "ping"], {
7384
+ stdio: "pipe",
7385
+ timeout: 15e3
7386
+ });
7387
+ if (result.status !== 0) {
7388
+ warn(
7389
+ `claude --print ping failed (exit ${result.status ?? "?"}) \u2014 token may still be valid`
7390
+ );
7391
+ }
7392
+ try {
7393
+ const raw2 = readFileSync15(credsPath, "utf-8");
7394
+ const parsed2 = JSON.parse(raw2);
7395
+ if (parsed2.claudeAiOauth?.expiresAt) {
7396
+ const newMinutes = (parsed2.claudeAiOauth.expiresAt - Date.now()) / 6e4;
7397
+ info(`Token now valid for ${Math.round(newMinutes)} more minutes`);
7398
+ }
7399
+ } catch {
7400
+ }
7401
+ }
7402
+ }
7403
+ return { path: credsPath };
7404
+ }
7405
+ function buildServiceUnit(nodePath, cliEntry, logPath) {
7406
+ return `[Unit]
7407
+ Description=BeastMode Claude credential sync
7408
+
7409
+ [Service]
7410
+ Type=oneshot
7411
+ ExecStart=${nodePath} ${cliEntry} sync-claude-creds
7412
+ StandardOutput=append:${logPath}
7413
+ StandardError=append:${logPath}
7414
+ `;
7415
+ }
7416
+ function buildTimerUnit(intervalSeconds) {
7417
+ return `[Unit]
7418
+ Description=BeastMode Claude credential sync timer
7419
+
7420
+ [Timer]
7421
+ OnBootSec=30s
7422
+ OnUnitActiveSec=${intervalSeconds}s
7423
+ Persistent=true
7424
+
7425
+ [Install]
7426
+ WantedBy=timers.target
7427
+ `;
7428
+ }
7429
+ function ensureSystemctlAvailable() {
7430
+ const result = spawnSync2("systemctl", ["--version"], {
7431
+ stdio: "pipe",
7432
+ encoding: "utf-8"
7433
+ });
7434
+ if (result.status !== 0) {
7435
+ return "systemctl not available \u2014 systemd user units not supported on this system";
7436
+ }
7437
+ return null;
7438
+ }
7439
+ function installAgentLinux(intervalSeconds, { throwOnError = false } = {}) {
7440
+ const unavail = ensureSystemctlAvailable();
7441
+ if (unavail) {
7442
+ if (throwOnError) throw new Error(unavail);
7443
+ error(unavail);
7444
+ process.exit(1);
7445
+ }
7446
+ const unitDir = systemdUserDir();
7447
+ const logDir = join16(homedir2(), ".beastmode", "logs");
7448
+ mkdirSync13(unitDir, { recursive: true });
7449
+ mkdirSync13(logDir, { recursive: true });
7450
+ const logPath = join16(logDir, "sync-claude-creds.log");
7451
+ const nodePath = process.execPath;
7452
+ const cliEntry = process.argv[1];
7453
+ writeFileSync14(
7454
+ systemdServicePath(),
7455
+ buildServiceUnit(nodePath, cliEntry, logPath),
7456
+ "utf-8"
7457
+ );
7458
+ writeFileSync14(
7459
+ systemdTimerPath(),
7460
+ buildTimerUnit(intervalSeconds),
7461
+ "utf-8"
7462
+ );
7463
+ const reload = spawnSync2("systemctl", ["--user", "daemon-reload"], {
7464
+ stdio: "pipe",
7465
+ encoding: "utf-8"
7466
+ });
7467
+ if (reload.status !== 0) {
7468
+ const msg = `systemctl --user daemon-reload failed: ${reload.stderr || reload.stdout}`;
7469
+ if (throwOnError) throw new Error(msg);
7470
+ error(msg);
7471
+ process.exit(1);
7472
+ }
7473
+ const enable = spawnSync2(
7474
+ "systemctl",
7475
+ ["--user", "enable", "--now", `${SYSTEMD_UNIT_NAME}.timer`],
7476
+ { stdio: "pipe", encoding: "utf-8" }
7477
+ );
7478
+ if (enable.status !== 0) {
7479
+ const msg = `systemctl --user enable --now failed: ${enable.stderr || enable.stdout}`;
7480
+ if (throwOnError) throw new Error(msg);
7481
+ error(msg);
7482
+ info(`Service: ${systemdServicePath()}`);
7483
+ info(`Timer: ${systemdTimerPath()}`);
7484
+ process.exit(1);
7485
+ }
7486
+ success(`Systemd timer installed: ${SYSTEMD_UNIT_NAME}.timer`);
7487
+ info(` Service: ${systemdServicePath()}`);
7488
+ info(` Timer: ${systemdTimerPath()}`);
7489
+ info(` Interval: every ${Math.round(intervalSeconds / 60)} minutes`);
7490
+ info(` Logs: ${logPath}`);
7491
+ }
7492
+ function uninstallAgentLinux() {
7493
+ const unavail = ensureSystemctlAvailable();
7494
+ if (unavail) {
7495
+ error(unavail);
7496
+ process.exit(1);
7497
+ }
7498
+ spawnSync2("systemctl", ["--user", "stop", `${SYSTEMD_UNIT_NAME}.timer`], {
7499
+ stdio: "pipe"
7500
+ });
7501
+ spawnSync2(
7502
+ "systemctl",
7503
+ ["--user", "disable", `${SYSTEMD_UNIT_NAME}.timer`],
7504
+ { stdio: "pipe" }
7505
+ );
7506
+ const servicePath = systemdServicePath();
7507
+ const timerPath = systemdTimerPath();
7508
+ let removed = false;
7509
+ if (existsSync18(servicePath)) {
7510
+ try {
7511
+ unlinkSync4(servicePath);
7512
+ removed = true;
7513
+ } catch {
7514
+ }
7515
+ }
7516
+ if (existsSync18(timerPath)) {
7517
+ try {
7518
+ unlinkSync4(timerPath);
7519
+ removed = true;
7520
+ } catch {
7521
+ }
7522
+ }
7523
+ spawnSync2("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
7524
+ if (removed) {
7525
+ success(`Systemd timer uninstalled: ${SYSTEMD_UNIT_NAME}.timer`);
7526
+ } else {
7527
+ info("No systemd unit files found \u2014 nothing to remove.");
7528
+ }
7529
+ }
7530
+ function statusAgentLinux() {
7531
+ const unavail = ensureSystemctlAvailable();
7532
+ if (unavail) {
7533
+ error(unavail);
7534
+ return;
7535
+ }
7536
+ if (!existsSync18(systemdTimerPath())) {
7537
+ info("Systemd timer not installed.");
7538
+ info("Install with: beastmode sync-claude-creds --install");
7539
+ return;
7540
+ }
7541
+ const isActive = spawnSync2(
7542
+ "systemctl",
7543
+ ["--user", "is-active", `${SYSTEMD_UNIT_NAME}.timer`],
7544
+ { stdio: "pipe", encoding: "utf-8" }
7545
+ );
7546
+ const active = (isActive.stdout || "").trim();
7547
+ if (active === "active") {
7548
+ success(`Systemd timer active: ${SYSTEMD_UNIT_NAME}.timer`);
7549
+ } else {
7550
+ warn(`Systemd timer not active (state: ${active || "unknown"})`);
7551
+ }
7552
+ const show = spawnSync2(
7553
+ "systemctl",
7554
+ [
7555
+ "--user",
7556
+ "show",
7557
+ `${SYSTEMD_UNIT_NAME}.service`,
7558
+ "--property=ExecMainStartTimestamp,ExecMainStatus"
7559
+ ],
7560
+ { stdio: "pipe", encoding: "utf-8" }
7561
+ );
7562
+ if (show.status === 0 && show.stdout) {
7563
+ for (const line of show.stdout.split("\n")) {
7564
+ const trimmed = line.trim();
7565
+ if (trimmed) info(` ${trimmed}`);
7566
+ }
7567
+ }
7568
+ info(` Service: ${systemdServicePath()}`);
7569
+ info(` Timer: ${systemdTimerPath()}`);
7570
+ }
7571
+ function restartAgentLinux() {
7572
+ const unavail = ensureSystemctlAvailable();
7573
+ if (unavail) {
7574
+ error(unavail);
7575
+ process.exit(1);
7576
+ }
7577
+ const result = spawnSync2(
7578
+ "systemctl",
7579
+ ["--user", "restart", `${SYSTEMD_UNIT_NAME}.timer`],
7580
+ { stdio: "pipe", encoding: "utf-8" }
7581
+ );
7582
+ if (result.status !== 0) {
7583
+ error(
7584
+ `systemctl --user restart failed: ${result.stderr || result.stdout}`
7585
+ );
7586
+ process.exit(1);
7587
+ }
7588
+ success(`Restarted: ${SYSTEMD_UNIT_NAME}.timer`);
7589
+ }
7590
+ var LAUNCH_AGENT_LABEL, SYSTEMD_UNIT_NAME, syncClaudeCredsCommand;
7145
7591
  var init_sync_claude_creds = __esm({
7146
7592
  "src/cli/commands/sync-claude-creds.ts"() {
7147
7593
  "use strict";
7148
7594
  init_display();
7595
+ init_board();
7149
7596
  LAUNCH_AGENT_LABEL = "com.develeap.beastmode.claude-creds";
7150
- syncClaudeCredsCommand = new Command("sync-claude-creds").description(
7151
- "Sync Claude Code credentials from macOS Keychain to ~/.claude/.credentials.json (for Docker)"
7152
- ).option("--restart", "Restart the daemon container after syncing (one-shot only)").option("--install", "Install a launchd agent that re-syncs every 30 minutes").option("--uninstall", "Remove the launchd agent").option("--status", "Show launchd agent status").option("--interval <seconds>", "Sync interval for --install (default: 1800)", "1800").action(async (opts) => {
7153
- if (platform() !== "darwin") {
7154
- info("Not macOS \u2014 Docker mounts ~/.claude/.credentials.json directly. Nothing to sync.");
7155
- return;
7597
+ SYSTEMD_UNIT_NAME = "beastmode-claude-creds";
7598
+ syncClaudeCredsCommand = new Command2("sync-claude-creds").description(
7599
+ "Sync/validate Claude credentials for Docker (macOS: extract from Keychain; Linux: validate ~/.claude/.credentials.json + refresh)"
7600
+ ).option("--restart", "Restart the daemon container after syncing (one-shot only)").option("--install", "Install a watcher (launchd on macOS, systemd user timer on Linux) that re-syncs periodically").option("--uninstall", "Remove the watcher").option("--status", "Show watcher status").option("--interval <seconds>", "Sync interval for --install (default: 1800)", "1800").action(async (opts) => {
7601
+ const plat = platform();
7602
+ if (plat !== "darwin" && plat !== "linux") {
7603
+ error(`Unsupported platform: ${plat}`);
7604
+ info("Supported: macOS (darwin), Linux");
7605
+ process.exit(1);
7156
7606
  }
7157
7607
  if (opts.install) {
7158
7608
  header("Install Claude Credential Sync Agent");
@@ -7162,45 +7612,78 @@ var init_sync_claude_creds = __esm({
7162
7612
  error(`Invalid --interval: ${opts.interval} (minimum 60 seconds)`);
7163
7613
  process.exit(1);
7164
7614
  }
7165
- installAgent(seconds);
7615
+ if (plat === "darwin") {
7616
+ installAgent(seconds);
7617
+ } else {
7618
+ installAgentLinux(seconds);
7619
+ }
7166
7620
  return;
7167
7621
  }
7168
7622
  if (opts.uninstall) {
7169
7623
  header("Remove Claude Credential Sync Agent");
7170
7624
  console.log();
7171
- uninstallAgent();
7625
+ if (plat === "darwin") {
7626
+ uninstallAgent();
7627
+ } else {
7628
+ uninstallAgentLinux();
7629
+ }
7172
7630
  return;
7173
7631
  }
7174
7632
  if (opts.status) {
7175
7633
  header("Claude Credential Sync Agent Status");
7176
7634
  console.log();
7177
- showStatus();
7635
+ if (plat === "darwin") {
7636
+ showStatus();
7637
+ } else {
7638
+ statusAgentLinux();
7639
+ }
7178
7640
  return;
7179
7641
  }
7180
- header("Sync Claude Code Credentials");
7181
- console.log();
7182
- const rawJson = readKeychainToken();
7183
- let credsPath;
7184
- try {
7185
- credsPath = writeCredentialsFile(rawJson);
7186
- } catch (e) {
7187
- error(e instanceof Error ? e.message : String(e));
7188
- process.exit(1);
7642
+ if (opts.restart && plat === "linux") {
7643
+ header("Restart Claude Credential Sync Timer");
7644
+ console.log();
7645
+ restartAgentLinux();
7646
+ return;
7189
7647
  }
7190
- success(`Wrote ${credsPath}`);
7191
- if (opts.restart) {
7648
+ if (plat === "darwin") {
7649
+ header("Sync Claude Code Credentials");
7192
7650
  console.log();
7193
- info("Restarting daemon container...");
7194
- const result = spawnSync2("docker", ["compose", "restart", "daemon"], { stdio: "inherit" });
7195
- if (result.status !== 0) {
7196
- warn("docker compose restart daemon failed \u2014 run it manually.");
7197
- process.exit(result.status ?? 1);
7651
+ const rawJson = readKeychainToken();
7652
+ let credsPath;
7653
+ try {
7654
+ credsPath = writeCredentialsFile(rawJson);
7655
+ } catch (e) {
7656
+ error(e instanceof Error ? e.message : String(e));
7657
+ process.exit(1);
7198
7658
  }
7199
- success("Daemon restarted.");
7200
- } else {
7201
- console.log();
7202
- info("Automate: beastmode sync-claude-creds --install");
7659
+ success(`Wrote ${credsPath}`);
7660
+ removeUrgencyMarker(findFactoryDir() ?? void 0);
7661
+ if (opts.restart) {
7662
+ console.log();
7663
+ info("Restarting daemon container...");
7664
+ const result2 = spawnSync2("docker", ["compose", "restart", "daemon"], { stdio: "inherit" });
7665
+ if (result2.status !== 0) {
7666
+ warn("docker compose restart daemon failed \u2014 run it manually.");
7667
+ process.exit(result2.status ?? 1);
7668
+ }
7669
+ success("Daemon restarted.");
7670
+ } else {
7671
+ console.log();
7672
+ info("Automate: beastmode sync-claude-creds --install");
7673
+ }
7674
+ return;
7203
7675
  }
7676
+ header("Validate Claude Credentials");
7677
+ console.log();
7678
+ const result = syncClaudeCredsLinux();
7679
+ if ("error" in result) {
7680
+ error(result.error);
7681
+ process.exit(1);
7682
+ }
7683
+ success(`Credentials valid: ${result.path}`);
7684
+ removeUrgencyMarker(findFactoryDir() ?? void 0);
7685
+ console.log();
7686
+ info("Automate: beastmode sync-claude-creds --install");
7204
7687
  });
7205
7688
  }
7206
7689
  });
@@ -7209,13 +7692,13 @@ var init_sync_claude_creds = __esm({
7209
7692
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7210
7693
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7211
7694
  import { z as z2 } from "zod";
7212
- import { readFileSync as readFileSync27, writeFileSync as writeFileSync24, existsSync as existsSync31, readdirSync as readdirSync11, mkdirSync as mkdirSync19 } from "fs";
7695
+ import { readFileSync as readFileSync28, writeFileSync as writeFileSync24, existsSync as existsSync31, readdirSync as readdirSync11, mkdirSync as mkdirSync19 } from "fs";
7213
7696
  import { join as join29, resolve as resolve18, basename as basename5 } from "path";
7214
7697
  import { randomUUID as randomUUID3 } from "crypto";
7215
7698
  function readJsonFile2(filePath) {
7216
7699
  if (!existsSync31(filePath)) return null;
7217
7700
  try {
7218
- return JSON.parse(readFileSync27(filePath, "utf-8"));
7701
+ return JSON.parse(readFileSync28(filePath, "utf-8"));
7219
7702
  } catch {
7220
7703
  return null;
7221
7704
  }
@@ -7242,7 +7725,7 @@ function getFactoryPath() {
7242
7725
  function readFactoryStatus(factoryDir) {
7243
7726
  const bmDir = join29(factoryDir, ".beastmode");
7244
7727
  const factoryIdentity = FactoryIdentitySchema.parse(
7245
- JSON.parse(readFileSync27(join29(bmDir, "factory.json"), "utf-8"))
7728
+ JSON.parse(readFileSync28(join29(bmDir, "factory.json"), "utf-8"))
7246
7729
  );
7247
7730
  const projectsDir = join29(bmDir, "projects");
7248
7731
  const projectCount = existsSync31(projectsDir) ? readdirSync11(projectsDir).filter((f) => f.endsWith(".json")).length : 0;
@@ -7250,7 +7733,7 @@ function readFactoryStatus(factoryDir) {
7250
7733
  let pluginNames = [];
7251
7734
  if (existsSync31(lockPath)) {
7252
7735
  try {
7253
- const lock = JSON.parse(readFileSync27(lockPath, "utf-8"));
7736
+ const lock = JSON.parse(readFileSync28(lockPath, "utf-8"));
7254
7737
  pluginNames = Object.keys(lock.plugins || {});
7255
7738
  } catch {
7256
7739
  }
@@ -7286,7 +7769,7 @@ function readFactoryStatus(factoryDir) {
7286
7769
  let pidAlive = false;
7287
7770
  if (existsSync31(pidFile)) {
7288
7771
  try {
7289
- daemonPid = parseInt(readFileSync27(pidFile, "utf-8").trim(), 10);
7772
+ daemonPid = parseInt(readFileSync28(pidFile, "utf-8").trim(), 10);
7290
7773
  process.kill(daemonPid, 0);
7291
7774
  pidAlive = true;
7292
7775
  } catch {
@@ -7310,7 +7793,7 @@ function readBoardItems(factoryDir) {
7310
7793
  const filePath = join29(factoryDir, ".beastmode", "board.json");
7311
7794
  if (!existsSync31(filePath)) return [];
7312
7795
  try {
7313
- const raw = JSON.parse(readFileSync27(filePath, "utf-8"));
7796
+ const raw = JSON.parse(readFileSync28(filePath, "utf-8"));
7314
7797
  return Array.isArray(raw.items) ? raw.items : [];
7315
7798
  } catch {
7316
7799
  return [];
@@ -7342,7 +7825,7 @@ function createMcpServer() {
7342
7825
  async ({ key_path }) => {
7343
7826
  const factoryDir = getFactoryPath();
7344
7827
  const configPath = join29(factoryDir, ".beastmode", "config.json");
7345
- const config = existsSync31(configPath) ? JSON.parse(readFileSync27(configPath, "utf-8")) : generateDefaults();
7828
+ const config = existsSync31(configPath) ? JSON.parse(readFileSync28(configPath, "utf-8")) : generateDefaults();
7346
7829
  try {
7347
7830
  const value = configGet(config, key_path);
7348
7831
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
@@ -7361,7 +7844,7 @@ function createMcpServer() {
7361
7844
  async ({ key_path, value }) => {
7362
7845
  const factoryDir = getFactoryPath();
7363
7846
  const configPath = join29(factoryDir, ".beastmode", "config.json");
7364
- const config = existsSync31(configPath) ? JSON.parse(readFileSync27(configPath, "utf-8")) : generateDefaults();
7847
+ const config = existsSync31(configPath) ? JSON.parse(readFileSync28(configPath, "utf-8")) : generateDefaults();
7365
7848
  const coerced = coerceValue(value);
7366
7849
  const updated = configSet(config, key_path, coerced);
7367
7850
  writeFileSync24(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
@@ -7480,7 +7963,7 @@ function createMcpServer() {
7480
7963
  const lockPath = join29(bmDir, "extensions.lock");
7481
7964
  if (existsSync31(lockPath)) {
7482
7965
  try {
7483
- const lock = JSON.parse(readFileSync27(lockPath, "utf-8"));
7966
+ const lock = JSON.parse(readFileSync28(lockPath, "utf-8"));
7484
7967
  plugins = lock.plugins || {};
7485
7968
  } catch {
7486
7969
  }
@@ -7520,7 +8003,7 @@ function createMcpServer() {
7520
8003
  }
7521
8004
  const projects = readdirSync11(projectsDir).filter((f) => f.endsWith(".json")).map((f) => {
7522
8005
  try {
7523
- return JSON.parse(readFileSync27(join29(projectsDir, f), "utf-8"));
8006
+ return JSON.parse(readFileSync28(join29(projectsDir, f), "utf-8"));
7524
8007
  } catch {
7525
8008
  return null;
7526
8009
  }
@@ -7658,15 +8141,15 @@ var init_mcp = __esm({
7658
8141
  });
7659
8142
 
7660
8143
  // src/index.ts
7661
- import { Command as Command23 } from "commander";
8144
+ import { Command as Command24 } from "commander";
7662
8145
 
7663
8146
  // src/cli/commands/init.ts
7664
8147
  init_engine();
7665
8148
  init_file_writer();
7666
- import { Command as Command2 } from "commander";
8149
+ import { Command as Command3 } from "commander";
7667
8150
  import inquirer from "inquirer";
7668
- import { resolve as resolve5, basename as basename4, join as join16 } from "path";
7669
- import { existsSync as existsSync18, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13, readFileSync as readFileSync14 } from "fs";
8151
+ import { resolve as resolve6, basename as basename4, join as join17 } from "path";
8152
+ import { existsSync as existsSync19, writeFileSync as writeFileSync15, mkdirSync as mkdirSync14, readFileSync as readFileSync16 } from "fs";
7670
8153
 
7671
8154
  // src/cli/utils/docker.ts
7672
8155
  import { existsSync as existsSync9 } from "fs";
@@ -7887,7 +8370,7 @@ function generateComposeYaml(tag) {
7887
8370
 
7888
8371
  // src/cli/commands/init.ts
7889
8372
  init_display();
7890
- var initCommand = new Command2("init").description("Create a new BeastMode factory").argument("[name]", "Factory name (default: current directory name)").option("--project <path>", "Path to target project").option("--preset <preset>", "Pipeline preset (full, lean, prototype, infra, docs)").option("--backend <backend>", "Task backend (beastmode-board, github-issues)").option("--deploy <target>", "Deploy target (pr-only, vercel, aws-ecs, ...)").option("--methodology <name>", "Development methodology plugin").option("--plugin <name>", "Install plugin (repeatable)", collect, []).option("--from <template>", "Import config from template file").option("--ui", "Open web wizard instead of CLI").option("--yes", "Accept all defaults (non-interactive)").option(
8373
+ var initCommand = new Command3("init").description("Create a new BeastMode factory").argument("[name]", "Factory name (default: current directory name)").option("--project <path>", "Path to target project").option("--preset <preset>", "Pipeline preset (full, lean, prototype, infra, docs)").option("--backend <backend>", "Task backend (beastmode-board, github-issues)").option("--deploy <target>", "Deploy target (pr-only, vercel, aws-ecs, ...)").option("--methodology <name>", "Development methodology plugin").option("--plugin <name>", "Install plugin (repeatable)", collect, []).option("--from <template>", "Import config from template file").option("--ui", "Open web wizard instead of CLI").option("--yes", "Accept all defaults (non-interactive)").option(
7891
8374
  "--project-github-token <token>",
7892
8375
  "GitHub PAT for the daemon's project operations (commit/push/PR/merge against PROJECT_DIR's git remote \u2014 needs `repo` scope). Alias for the legacy --github-token flag."
7893
8376
  ).option(
@@ -7918,16 +8401,16 @@ function collect(val, acc) {
7918
8401
  return acc;
7919
8402
  }
7920
8403
  function readSecretFile(filePath) {
7921
- return readFileSync14(resolve5(filePath), "utf-8").trim();
8404
+ return readFileSync16(resolve6(filePath), "utf-8").trim();
7922
8405
  }
7923
8406
  function isSourceRepo(dir) {
7924
- return existsSync18(resolve5(dir, "daemon")) && existsSync18(resolve5(dir, "board")) && existsSync18(resolve5(dir, "cli"));
8407
+ return existsSync19(resolve6(dir, "daemon")) && existsSync19(resolve6(dir, "board")) && existsSync19(resolve6(dir, "cli"));
7925
8408
  }
7926
8409
  async function runInit(name, opts) {
7927
8410
  if (opts.ui) {
7928
8411
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
7929
- const factoryName2 = name || basename4(resolve5("."));
7930
- const projectPath2 = opts.project ? resolve5(opts.project) : void 0;
8412
+ const factoryName2 = name || basename4(resolve6("."));
8413
+ const projectPath2 = opts.project ? resolve6(opts.project) : void 0;
7931
8414
  info("Starting init wizard...");
7932
8415
  const uiServer = await startServer2({
7933
8416
  port: 7669,
@@ -7952,20 +8435,20 @@ async function runInit(name, opts) {
7952
8435
  });
7953
8436
  return;
7954
8437
  }
7955
- if (!isSourceRepo(resolve5("."))) {
8438
+ if (!isSourceRepo(resolve6("."))) {
7956
8439
  await runImageModeInit(name, opts);
7957
8440
  return;
7958
8441
  }
7959
8442
  const totalSteps = 5;
7960
8443
  header("BeastMode \u2014 Dark Factory Init");
7961
8444
  info("Creating your Custom Dark Factory\n");
7962
- const factoryName = name || basename4(resolve5("."));
7963
- if (existsSync18(factoryName) && existsSync18(resolve5(factoryName, ".beastmode"))) {
8445
+ const factoryName = name || basename4(resolve6("."));
8446
+ if (existsSync19(factoryName) && existsSync19(resolve6(factoryName, ".beastmode"))) {
7964
8447
  throw new Error(`Factory already exists at ./${factoryName}. Use 'beastmode config' to modify.`);
7965
8448
  }
7966
8449
  if (opts.from) {
7967
- const { readFileSync: readFileSync30 } = await import("fs");
7968
- const templateContent = readFileSync30(resolve5(opts.from), "utf-8");
8450
+ const { readFileSync: readFileSync31 } = await import("fs");
8451
+ const templateContent = readFileSync31(resolve6(opts.from), "utf-8");
7969
8452
  const { parseTemplate: parseTemplate2 } = await Promise.resolve().then(() => (init_template_importer(), template_importer_exports));
7970
8453
  const template = parseTemplate2(templateContent);
7971
8454
  info(`Importing from template: ${opts.from}`);
@@ -7973,7 +8456,7 @@ async function runInit(name, opts) {
7973
8456
  if (!opts.project) {
7974
8457
  throw new Error("--project is required when using --from");
7975
8458
  }
7976
- const templateProjectPath = resolve5(opts.project);
8459
+ const templateProjectPath = resolve6(opts.project);
7977
8460
  const templateStack = detectStack(templateProjectPath);
7978
8461
  const templateProjectName = basename4(templateProjectPath);
7979
8462
  const actions2 = scaffoldFactory(factoryName, template.config, {
@@ -7999,9 +8482,9 @@ async function runInit(name, opts) {
7999
8482
  step(1, totalSteps, "Detect");
8000
8483
  let projectPath;
8001
8484
  if (opts.project) {
8002
- projectPath = resolve5(opts.project);
8485
+ projectPath = resolve6(opts.project);
8003
8486
  } else if (opts.yes) {
8004
- projectPath = resolve5(".");
8487
+ projectPath = resolve6(".");
8005
8488
  info("No --project specified, using current directory");
8006
8489
  } else {
8007
8490
  const answer = await inquirer.prompt([
@@ -8010,10 +8493,10 @@ async function runInit(name, opts) {
8010
8493
  name: "project",
8011
8494
  message: "Path to your project:",
8012
8495
  default: ".",
8013
- validate: (input) => existsSync18(resolve5(input)) || `Directory not found: ${input}`
8496
+ validate: (input) => existsSync19(resolve6(input)) || `Directory not found: ${input}`
8014
8497
  }
8015
8498
  ]);
8016
- projectPath = resolve5(answer.project);
8499
+ projectPath = resolve6(answer.project);
8017
8500
  }
8018
8501
  const stack = detectStack(projectPath);
8019
8502
  success(`Detected: ${stack.framework} (${stack.language}) with ${stack.package_manager || "no package manager"}`);
@@ -8195,7 +8678,7 @@ async function runInit(name, opts) {
8195
8678
  collectedSecrets.push(`BEASTMODE_UI_PASSWORD=${uiPassword}`);
8196
8679
  }
8197
8680
  if (collectedSecrets.length > 0) {
8198
- const secretsPath = resolve5(factoryName, ".beastmode", "secrets.env.local");
8681
+ const secretsPath = resolve6(factoryName, ".beastmode", "secrets.env.local");
8199
8682
  const secretsContent = "# BeastMode secrets \u2014 DO NOT COMMIT\n" + collectedSecrets.join("\n") + "\n";
8200
8683
  const { writeFileSync: writeSecrets } = await import("fs");
8201
8684
  writeSecrets(secretsPath, secretsContent, "utf-8");
@@ -8219,6 +8702,25 @@ async function runInit(name, opts) {
8219
8702
  } catch (e) {
8220
8703
  warn(`Claude-creds setup skipped: ${e instanceof Error ? e.message : String(e)}`);
8221
8704
  }
8705
+ } else if (process.platform === "linux") {
8706
+ try {
8707
+ const { syncClaudeCredsLinux: syncClaudeCredsLinux2, installAgentLinux: installAgentLinux2 } = await Promise.resolve().then(() => (init_sync_claude_creds(), sync_claude_creds_exports));
8708
+ const sync = syncClaudeCredsLinux2();
8709
+ if ("error" in sync) {
8710
+ warn(`Claude creds check skipped: ${sync.error}`);
8711
+ info(" Run `claude login` then `beastmode sync-claude-creds --install`.");
8712
+ } else {
8713
+ success(`Claude creds validated: ${sync.path}`);
8714
+ try {
8715
+ installAgentLinux2(1800, { throwOnError: true });
8716
+ } catch (e) {
8717
+ warn(`Claude-creds timer install failed: ${e instanceof Error ? e.message : String(e)}`);
8718
+ info(" You can retry with: beastmode sync-claude-creds --install");
8719
+ }
8720
+ }
8721
+ } catch (e) {
8722
+ warn(`Claude-creds setup skipped: ${e instanceof Error ? e.message : String(e)}`);
8723
+ }
8222
8724
  }
8223
8725
  console.log();
8224
8726
  header(`Factory "${factoryName}" is ready!`);
@@ -8238,7 +8740,7 @@ async function runImageModeInit(name, opts) {
8238
8740
  header("BeastMode \u2014 Factory Init (Image Mode)");
8239
8741
  info("Setting up BeastMode from pre-built Docker images\n");
8240
8742
  const factoryName = name || ".";
8241
- const targetDir = resolve5(factoryName === "." ? "." : factoryName);
8743
+ const targetDir = resolve6(factoryName === "." ? "." : factoryName);
8242
8744
  step(1, totalSteps, "Credentials");
8243
8745
  let projectGithubToken = opts.projectGithubToken || (opts.projectGithubTokenFile ? readSecretFile(opts.projectGithubTokenFile) : "") || opts.githubToken || (opts.githubTokenFile ? readSecretFile(opts.githubTokenFile) : "") || process.env.PROJECT_GITHUB_TOKEN || process.env.GITHUB_TOKEN || "";
8244
8746
  if (!projectGithubToken && !opts.yes) {
@@ -8296,15 +8798,15 @@ async function runImageModeInit(name, opts) {
8296
8798
  }
8297
8799
  let projectPath;
8298
8800
  if (opts.project) {
8299
- projectPath = resolve5(opts.project);
8300
- if (!existsSync18(projectPath)) {
8801
+ projectPath = resolve6(opts.project);
8802
+ if (!existsSync19(projectPath)) {
8301
8803
  error(`--project path does not exist: ${projectPath}`);
8302
8804
  process.exit(1);
8303
8805
  }
8304
8806
  success(`Project path: ${projectPath}`);
8305
8807
  } else if (opts.yes) {
8306
- const cwd = resolve5(".");
8307
- if (!existsSync18(join16(cwd, ".git"))) {
8808
+ const cwd = resolve6(".");
8809
+ if (!existsSync19(join17(cwd, ".git"))) {
8308
8810
  error(
8309
8811
  "--project is required in non-interactive mode (--yes) unless you run from inside a git repository. Pass --project <path> or cd into your project clone first."
8310
8812
  );
@@ -8320,16 +8822,16 @@ async function runImageModeInit(name, opts) {
8320
8822
  message: "Path to your project (must be a git clone with an origin remote):",
8321
8823
  default: ".",
8322
8824
  validate: (input) => {
8323
- const p = resolve5(input);
8324
- if (!existsSync18(p)) return `Directory not found: ${p}`;
8325
- if (!existsSync18(join16(p, ".git"))) {
8825
+ const p = resolve6(input);
8826
+ if (!existsSync19(p)) return `Directory not found: ${p}`;
8827
+ if (!existsSync19(join17(p, ".git"))) {
8326
8828
  return `Not a git repo: ${p} (run 'git init' first, or pick a different path)`;
8327
8829
  }
8328
8830
  return true;
8329
8831
  }
8330
8832
  }
8331
8833
  ]);
8332
- projectPath = resolve5(answer.project);
8834
+ projectPath = resolve6(answer.project);
8333
8835
  success(`Project path: ${projectPath}`);
8334
8836
  }
8335
8837
  step(2, totalSteps, "Docker Registry");
@@ -8361,9 +8863,9 @@ async function runImageModeInit(name, opts) {
8361
8863
  success("Authenticated to ghcr.io");
8362
8864
  }
8363
8865
  step(3, totalSteps, "Generate");
8364
- mkdirSync13(targetDir, { recursive: true });
8866
+ mkdirSync14(targetDir, { recursive: true });
8365
8867
  const composeContent = generateComposeYaml("latest");
8366
- writeFileSync14(join16(targetDir, "docker-compose.yml"), composeContent, "utf-8");
8868
+ writeFileSync15(join17(targetDir, "docker-compose.yml"), composeContent, "utf-8");
8367
8869
  success("docker-compose.yml");
8368
8870
  const envLines = [
8369
8871
  "# BeastMode environment \u2014 DO NOT COMMIT",
@@ -8403,10 +8905,10 @@ async function runImageModeInit(name, opts) {
8403
8905
  "# PROJECT_REPO=owner/repo",
8404
8906
  ""
8405
8907
  ];
8406
- writeFileSync14(join16(targetDir, ".env"), envLines.join("\n"), "utf-8");
8908
+ writeFileSync15(join17(targetDir, ".env"), envLines.join("\n"), "utf-8");
8407
8909
  success(`.env (PROJECT_DIR=${projectPath}, two-PAT model)`);
8408
8910
  for (const dir of ["data", "runs", "daemon/logs", ".beastmode", "config"]) {
8409
- mkdirSync13(join16(targetDir, dir), { recursive: true });
8911
+ mkdirSync14(join17(targetDir, dir), { recursive: true });
8410
8912
  }
8411
8913
  step(4, totalSteps, "Pull Images");
8412
8914
  try {
@@ -8456,6 +8958,25 @@ async function runImageModeInit(name, opts) {
8456
8958
  } catch (e) {
8457
8959
  warn(`Claude-creds setup skipped: ${e instanceof Error ? e.message : String(e)}`);
8458
8960
  }
8961
+ } else if (process.platform === "linux") {
8962
+ try {
8963
+ const { syncClaudeCredsLinux: syncClaudeCredsLinux2, installAgentLinux: installAgentLinux2 } = await Promise.resolve().then(() => (init_sync_claude_creds(), sync_claude_creds_exports));
8964
+ const sync = syncClaudeCredsLinux2();
8965
+ if ("error" in sync) {
8966
+ warn(`Claude creds check skipped: ${sync.error}`);
8967
+ info(" Run `claude login` then `beastmode sync-claude-creds --install`.");
8968
+ } else {
8969
+ success(`Claude creds validated: ${sync.path}`);
8970
+ try {
8971
+ installAgentLinux2(1800, { throwOnError: true });
8972
+ } catch (e) {
8973
+ warn(`Claude-creds timer install failed: ${e instanceof Error ? e.message : String(e)}`);
8974
+ info(" You can retry with: beastmode sync-claude-creds --install");
8975
+ }
8976
+ }
8977
+ } catch (e) {
8978
+ warn(`Claude-creds setup skipped: ${e instanceof Error ? e.message : String(e)}`);
8979
+ }
8459
8980
  }
8460
8981
  console.log();
8461
8982
  header("BeastMode is ready!");
@@ -8475,21 +8996,21 @@ async function runImageModeInit(name, opts) {
8475
8996
  // src/cli/commands/export-config.ts
8476
8997
  init_export_adapter();
8477
8998
  init_display();
8478
- import { Command as Command3 } from "commander";
8479
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync15, existsSync as existsSync19, readdirSync as readdirSync7 } from "fs";
8480
- import { resolve as resolve6, join as join17 } from "path";
8999
+ import { Command as Command4 } from "commander";
9000
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync16, existsSync as existsSync20, readdirSync as readdirSync8 } from "fs";
9001
+ import { resolve as resolve7, join as join18 } from "path";
8481
9002
  function exportFactory(factoryDir, outputPath) {
8482
- const bmDir = join17(factoryDir, ".beastmode");
8483
- const configPath = join17(bmDir, "config.json");
8484
- if (!existsSync19(configPath)) {
9003
+ const bmDir = join18(factoryDir, ".beastmode");
9004
+ const configPath = join18(bmDir, "config.json");
9005
+ if (!existsSync20(configPath)) {
8485
9006
  throw new Error(`No factory found at ${factoryDir}`);
8486
9007
  }
8487
- const config = JSON.parse(readFileSync15(configPath, "utf-8"));
8488
- const identity = JSON.parse(readFileSync15(join17(bmDir, "factory.json"), "utf-8"));
9008
+ const config = JSON.parse(readFileSync17(configPath, "utf-8"));
9009
+ const identity = JSON.parse(readFileSync17(join18(bmDir, "factory.json"), "utf-8"));
8489
9010
  let plugins = [];
8490
- const lockPath = join17(bmDir, "extensions.lock");
8491
- if (existsSync19(lockPath)) {
8492
- const lock = JSON.parse(readFileSync15(lockPath, "utf-8"));
9011
+ const lockPath = join18(bmDir, "extensions.lock");
9012
+ if (existsSync20(lockPath)) {
9013
+ const lock = JSON.parse(readFileSync17(lockPath, "utf-8"));
8493
9014
  plugins = Object.keys(lock.plugins || {});
8494
9015
  }
8495
9016
  const template = {
@@ -8498,15 +9019,15 @@ function exportFactory(factoryDir, outputPath) {
8498
9019
  config,
8499
9020
  plugins
8500
9021
  };
8501
- writeFileSync15(outputPath, JSON.stringify(template, null, 2) + "\n", "utf-8");
9022
+ writeFileSync16(outputPath, JSON.stringify(template, null, 2) + "\n", "utf-8");
8502
9023
  }
8503
9024
  function findRunDir(runId) {
8504
9025
  const candidates = [
8505
- resolve6(".", "runs", runId),
8506
- resolve6(".", runId)
9026
+ resolve7(".", "runs", runId),
9027
+ resolve7(".", runId)
8507
9028
  ];
8508
9029
  for (const candidate of candidates) {
8509
- if (existsSync19(candidate)) {
9030
+ if (existsSync20(candidate)) {
8510
9031
  return candidate;
8511
9032
  }
8512
9033
  }
@@ -8515,28 +9036,28 @@ function findRunDir(runId) {
8515
9036
  );
8516
9037
  }
8517
9038
  function createArtifactExportCommand(artifact) {
8518
- return new Command3(artifact).description(`Export ${artifact} from a pipeline run`).requiredOption("--run <id>", "Run ID to export from").option("--adapter <id>", "Export adapter (default: generic:markdown)", "generic:markdown").option("--output <path>", "Output file path (default: stdout)").action((opts) => {
9039
+ return new Command4(artifact).description(`Export ${artifact} from a pipeline run`).requiredOption("--run <id>", "Run ID to export from").option("--adapter <id>", "Export adapter (default: generic:markdown)", "generic:markdown").option("--output <path>", "Output file path (default: stdout)").action((opts) => {
8519
9040
  try {
8520
9041
  const runDir = findRunDir(opts.run);
8521
9042
  let sourceContent;
8522
9043
  if (artifact === "scenarios") {
8523
- const scenariosDir = join17(runDir, "scenarios");
8524
- if (!existsSync19(scenariosDir)) {
9044
+ const scenariosDir = join18(runDir, "scenarios");
9045
+ if (!existsSync20(scenariosDir)) {
8525
9046
  throw new Error(`No scenarios directory found in run ${opts.run}`);
8526
9047
  }
8527
- const files = readdirSync7(scenariosDir).filter((f) => f.endsWith(".md")).sort();
8528
- sourceContent = files.map((f) => readFileSync15(join17(scenariosDir, f), "utf-8")).join("\n\n---\n\n");
9048
+ const files = readdirSync8(scenariosDir).filter((f) => f.endsWith(".md")).sort();
9049
+ sourceContent = files.map((f) => readFileSync17(join18(scenariosDir, f), "utf-8")).join("\n\n---\n\n");
8529
9050
  } else {
8530
9051
  const artifactFile = artifact === "nlspec" ? "nlspec.md" : "plan.md";
8531
- const artifactPath = join17(runDir, artifactFile);
8532
- if (!existsSync19(artifactPath)) {
9052
+ const artifactPath = join18(runDir, artifactFile);
9053
+ if (!existsSync20(artifactPath)) {
8533
9054
  throw new Error(`${artifactFile} not found in run ${opts.run}`);
8534
9055
  }
8535
- sourceContent = readFileSync15(artifactPath, "utf-8");
9056
+ sourceContent = readFileSync17(artifactPath, "utf-8");
8536
9057
  }
8537
9058
  const result = runExportAdapter(opts.adapter, sourceContent);
8538
9059
  if (opts.output) {
8539
- writeFileSync15(resolve6(opts.output), result, "utf-8");
9060
+ writeFileSync16(resolve7(opts.output), result, "utf-8");
8540
9061
  header(`Exported ${artifact}`);
8541
9062
  success(`Written to: ${opts.output}`);
8542
9063
  } else {
@@ -8548,12 +9069,12 @@ function createArtifactExportCommand(artifact) {
8548
9069
  }
8549
9070
  });
8550
9071
  }
8551
- var exportCommand = new Command3("export").description("Export factory config or pipeline artifacts");
9072
+ var exportCommand = new Command4("export").description("Export factory config or pipeline artifacts");
8552
9073
  exportCommand.addCommand(
8553
- new Command3("config").description("Export factory configuration template (secrets excluded)").option("--output <path>", "Output file path", "beastmode-template.json").action((opts) => {
9074
+ new Command4("config").description("Export factory configuration template (secrets excluded)").option("--output <path>", "Output file path", "beastmode-template.json").action((opts) => {
8554
9075
  try {
8555
- const factoryDir = resolve6(".");
8556
- exportFactory(factoryDir, resolve6(opts.output));
9076
+ const factoryDir = resolve7(".");
9077
+ exportFactory(factoryDir, resolve7(opts.output));
8557
9078
  header("Factory template exported");
8558
9079
  success(`Written to: ${opts.output}`);
8559
9080
  info("Share with teammates: beastmode init my-factory --from " + opts.output);
@@ -8570,10 +9091,10 @@ exportCommand.addCommand(createArtifactExportCommand("scenarios"));
8570
9091
  // src/cli/commands/validate.ts
8571
9092
  init_config_validator();
8572
9093
  init_display();
8573
- import { Command as Command4 } from "commander";
8574
- import { resolve as resolve7 } from "path";
8575
- var validateCommand = new Command4("validate").description("Validate factory health").option("--verbose", "Show all checks").action((opts) => {
8576
- const factoryDir = resolve7(".");
9094
+ import { Command as Command5 } from "commander";
9095
+ import { resolve as resolve8 } from "path";
9096
+ var validateCommand = new Command5("validate").description("Validate factory health").option("--verbose", "Show all checks").action((opts) => {
9097
+ const factoryDir = resolve8(".");
8577
9098
  const result = validateFactory(factoryDir, process.env);
8578
9099
  header("Factory Validation");
8579
9100
  if (result.errors.length > 0) {
@@ -8602,8 +9123,8 @@ init_mcp_manager();
8602
9123
  init_hook_manager();
8603
9124
  init_skill_manager();
8604
9125
  init_display();
8605
- import { Command as Command5 } from "commander";
8606
- import { resolve as resolve8 } from "path";
9126
+ import { Command as Command6 } from "commander";
9127
+ import { resolve as resolve9 } from "path";
8607
9128
  async function addPluginAction(factoryDir, source, options) {
8608
9129
  await installPlugin(factoryDir, source);
8609
9130
  }
@@ -8617,7 +9138,7 @@ function addMcpAction(factoryDir, name, command, args, config) {
8617
9138
  });
8618
9139
  }
8619
9140
  function addSkillAction(factoryDir, sourcePath) {
8620
- addSkill(factoryDir, resolve8(sourcePath));
9141
+ addSkill(factoryDir, resolve9(sourcePath));
8621
9142
  }
8622
9143
  function addHookAction(factoryDir, event, name, options) {
8623
9144
  addHook(factoryDir, event, {
@@ -8629,10 +9150,10 @@ function addHookAction(factoryDir, event, name, options) {
8629
9150
  source: "user"
8630
9151
  });
8631
9152
  }
8632
- var addCommand = new Command5("add").description("Add extensions to the factory").addCommand(
8633
- new Command5("plugin").description("Install a plugin (from path, URL, or registry name)").argument("<source>", "Plugin source: local path, git URL, or registry name").option("--version <ver>", "Specific version to install").action(async (source, opts) => {
9153
+ var addCommand = new Command6("add").description("Add extensions to the factory").addCommand(
9154
+ new Command6("plugin").description("Install a plugin (from path, URL, or registry name)").argument("<source>", "Plugin source: local path, git URL, or registry name").option("--version <ver>", "Specific version to install").action(async (source, opts) => {
8634
9155
  try {
8635
- const factoryDir = resolve8(".");
9156
+ const factoryDir = resolve9(".");
8636
9157
  await addPluginAction(factoryDir, source, opts);
8637
9158
  success(`Plugin installed from ${source}`);
8638
9159
  } catch (err) {
@@ -8641,10 +9162,10 @@ var addCommand = new Command5("add").description("Add extensions to the factory"
8641
9162
  }
8642
9163
  })
8643
9164
  ).addCommand(
8644
- new Command5("mcp").description("Add an MCP server").argument("<name>", "Server name").requiredOption("--command <cmd>", "Command to run").option("--args <args...>", "Command arguments", []).option("--config <json>", "Configuration as JSON string").action(
9165
+ new Command6("mcp").description("Add an MCP server").argument("<name>", "Server name").requiredOption("--command <cmd>", "Command to run").option("--args <args...>", "Command arguments", []).option("--config <json>", "Configuration as JSON string").action(
8645
9166
  (name, opts) => {
8646
9167
  try {
8647
- const factoryDir = resolve8(".");
9168
+ const factoryDir = resolve9(".");
8648
9169
  const config = opts.config ? JSON.parse(opts.config) : void 0;
8649
9170
  addMcpAction(factoryDir, name, opts.command, opts.args, config);
8650
9171
  success(`MCP server '${name}' added`);
@@ -8655,9 +9176,9 @@ var addCommand = new Command5("add").description("Add extensions to the factory"
8655
9176
  }
8656
9177
  )
8657
9178
  ).addCommand(
8658
- new Command5("skill").description("Add a custom skill from a directory").argument("<path>", "Path to skill directory (must contain SKILL.md)").action((path) => {
9179
+ new Command6("skill").description("Add a custom skill from a directory").argument("<path>", "Path to skill directory (must contain SKILL.md)").action((path) => {
8659
9180
  try {
8660
- const factoryDir = resolve8(".");
9181
+ const factoryDir = resolve9(".");
8661
9182
  addSkillAction(factoryDir, path);
8662
9183
  success(`Skill added from ${path}`);
8663
9184
  } catch (err) {
@@ -8666,10 +9187,10 @@ var addCommand = new Command5("add").description("Add extensions to the factory"
8666
9187
  }
8667
9188
  })
8668
9189
  ).addCommand(
8669
- new Command5("hook").description("Add a pipeline hook").argument("<event>", "Hook event (e.g., pre-build, post-verify)").argument("<name>", "Hook name").option("--command <cmd>", "Shell command to run").option("--skill <name>", "Skill to invoke").option("--webhook <url>", "Webhook URL to POST to").option("--config <json>", "Configuration as JSON string").action(
9190
+ new Command6("hook").description("Add a pipeline hook").argument("<event>", "Hook event (e.g., pre-build, post-verify)").argument("<name>", "Hook name").option("--command <cmd>", "Shell command to run").option("--skill <name>", "Skill to invoke").option("--webhook <url>", "Webhook URL to POST to").option("--config <json>", "Configuration as JSON string").action(
8670
9191
  (event, name, opts) => {
8671
9192
  try {
8672
- const factoryDir = resolve8(".");
9193
+ const factoryDir = resolve9(".");
8673
9194
  const config = opts.config ? JSON.parse(opts.config) : void 0;
8674
9195
  addHookAction(factoryDir, event, name, {
8675
9196
  command: opts.command,
@@ -8692,8 +9213,8 @@ init_mcp_manager();
8692
9213
  init_hook_manager();
8693
9214
  init_skill_manager();
8694
9215
  init_display();
8695
- import { Command as Command6 } from "commander";
8696
- import { resolve as resolve9 } from "path";
9216
+ import { Command as Command7 } from "commander";
9217
+ import { resolve as resolve10 } from "path";
8697
9218
  function removePluginAction(factoryDir, name, options) {
8698
9219
  removePlugin(factoryDir, name, options);
8699
9220
  }
@@ -8706,10 +9227,10 @@ function removeSkillAction(factoryDir, name) {
8706
9227
  function removeHookAction(factoryDir, event, name) {
8707
9228
  removeHook(factoryDir, event, name);
8708
9229
  }
8709
- var removeCommand = new Command6("remove").description("Remove extensions from the factory").addCommand(
8710
- new Command6("plugin").description("Remove an installed plugin").argument("<name>", "Plugin name").option("--force", "Force removal even if other plugins depend on it").action((name, opts) => {
9230
+ var removeCommand = new Command7("remove").description("Remove extensions from the factory").addCommand(
9231
+ new Command7("plugin").description("Remove an installed plugin").argument("<name>", "Plugin name").option("--force", "Force removal even if other plugins depend on it").action((name, opts) => {
8711
9232
  try {
8712
- const factoryDir = resolve9(".");
9233
+ const factoryDir = resolve10(".");
8713
9234
  removePluginAction(factoryDir, name, opts);
8714
9235
  success(`Plugin '${name}' removed`);
8715
9236
  } catch (err) {
@@ -8718,9 +9239,9 @@ var removeCommand = new Command6("remove").description("Remove extensions from t
8718
9239
  }
8719
9240
  })
8720
9241
  ).addCommand(
8721
- new Command6("mcp").description("Remove an MCP server").argument("<name>", "Server name").action((name) => {
9242
+ new Command7("mcp").description("Remove an MCP server").argument("<name>", "Server name").action((name) => {
8722
9243
  try {
8723
- const factoryDir = resolve9(".");
9244
+ const factoryDir = resolve10(".");
8724
9245
  removeMcpAction(factoryDir, name);
8725
9246
  success(`MCP server '${name}' removed`);
8726
9247
  } catch (err) {
@@ -8729,9 +9250,9 @@ var removeCommand = new Command6("remove").description("Remove extensions from t
8729
9250
  }
8730
9251
  })
8731
9252
  ).addCommand(
8732
- new Command6("skill").description("Remove a custom skill").argument("<name>", "Skill name").action((name) => {
9253
+ new Command7("skill").description("Remove a custom skill").argument("<name>", "Skill name").action((name) => {
8733
9254
  try {
8734
- const factoryDir = resolve9(".");
9255
+ const factoryDir = resolve10(".");
8735
9256
  removeSkillAction(factoryDir, name);
8736
9257
  success(`Skill '${name}' removed`);
8737
9258
  } catch (err) {
@@ -8740,9 +9261,9 @@ var removeCommand = new Command6("remove").description("Remove extensions from t
8740
9261
  }
8741
9262
  })
8742
9263
  ).addCommand(
8743
- new Command6("hook").description("Remove a pipeline hook").argument("<event>", "Hook event (e.g., pre-build, post-verify)").argument("<name>", "Hook name").action((event, name) => {
9264
+ new Command7("hook").description("Remove a pipeline hook").argument("<event>", "Hook event (e.g., pre-build, post-verify)").argument("<name>", "Hook name").action((event, name) => {
8744
9265
  try {
8745
- const factoryDir = resolve9(".");
9266
+ const factoryDir = resolve10(".");
8746
9267
  removeHookAction(factoryDir, event, name);
8747
9268
  success(`Hook '${name}' removed from ${event}`);
8748
9269
  } catch (err) {
@@ -8758,17 +9279,17 @@ init_hook_manager();
8758
9279
  init_skill_manager();
8759
9280
  init_presets();
8760
9281
  init_display();
8761
- import { Command as Command7 } from "commander";
8762
- import { resolve as resolve10 } from "path";
8763
- import { readFileSync as readFileSync16, existsSync as existsSync20 } from "fs";
8764
- import { join as join18 } from "path";
9282
+ import { Command as Command8 } from "commander";
9283
+ import { resolve as resolve11 } from "path";
9284
+ import { readFileSync as readFileSync18, existsSync as existsSync21 } from "fs";
9285
+ import { join as join19 } from "path";
8765
9286
  function listPluginsAction(factoryDir) {
8766
- const lockPath = join18(factoryDir, ".beastmode", "extensions.lock");
8767
- if (!existsSync20(lockPath)) {
9287
+ const lockPath = join19(factoryDir, ".beastmode", "extensions.lock");
9288
+ if (!existsSync21(lockPath)) {
8768
9289
  console.log(" No plugins installed (extensions.lock not found).");
8769
9290
  return;
8770
9291
  }
8771
- const lock = JSON.parse(readFileSync16(lockPath, "utf-8"));
9292
+ const lock = JSON.parse(readFileSync18(lockPath, "utf-8"));
8772
9293
  const plugins = lock.plugins || {};
8773
9294
  const names = Object.keys(plugins);
8774
9295
  if (names.length === 0) {
@@ -8841,24 +9362,24 @@ function listPresetsAction() {
8841
9362
  console.log(` ${name} \u2014 ${preset.description}`);
8842
9363
  }
8843
9364
  }
8844
- var listCommand = new Command7("list").description("List installed extensions and resources").addCommand(
8845
- new Command7("plugins").description("List installed plugins").action(() => {
8846
- listPluginsAction(resolve10("."));
9365
+ var listCommand = new Command8("list").description("List installed extensions and resources").addCommand(
9366
+ new Command8("plugins").description("List installed plugins").action(() => {
9367
+ listPluginsAction(resolve11("."));
8847
9368
  })
8848
9369
  ).addCommand(
8849
- new Command7("mcps").description("List configured MCP servers").action(() => {
8850
- listMcpsAction(resolve10("."));
9370
+ new Command8("mcps").description("List configured MCP servers").action(() => {
9371
+ listMcpsAction(resolve11("."));
8851
9372
  })
8852
9373
  ).addCommand(
8853
- new Command7("skills").description("List available skills").action(() => {
8854
- listSkillsAction(resolve10("."));
9374
+ new Command8("skills").description("List available skills").action(() => {
9375
+ listSkillsAction(resolve11("."));
8855
9376
  })
8856
9377
  ).addCommand(
8857
- new Command7("hooks").description("List configured hooks").action(() => {
8858
- listHooksAction(resolve10("."));
9378
+ new Command8("hooks").description("List configured hooks").action(() => {
9379
+ listHooksAction(resolve11("."));
8859
9380
  })
8860
9381
  ).addCommand(
8861
- new Command7("presets").description("List available pipeline presets").action(() => {
9382
+ new Command8("presets").description("List available pipeline presets").action(() => {
8862
9383
  listPresetsAction();
8863
9384
  })
8864
9385
  );
@@ -8866,32 +9387,32 @@ var listCommand = new Command7("list").description("List installed extensions an
8866
9387
  // src/cli/commands/import-cmd.ts
8867
9388
  init_import_adapter();
8868
9389
  init_display();
8869
- import { Command as Command8 } from "commander";
8870
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync16, mkdirSync as mkdirSync14, existsSync as existsSync21 } from "fs";
8871
- import { resolve as resolve11, join as join19 } from "path";
9390
+ import { Command as Command9 } from "commander";
9391
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, existsSync as existsSync22 } from "fs";
9392
+ import { resolve as resolve12, join as join20 } from "path";
8872
9393
  var VALID_ARTIFACTS = ["nlspec", "plan", "scenarios"];
8873
9394
  function createArtifactCommand(artifact) {
8874
- return new Command8(artifact).description(`Import external document as ${artifact}`).requiredOption("--from <path>", "Path to source file").requiredOption("--adapter <id>", "Adapter ID (e.g., generic:prd, bmad:brainstorm)").option("--output <path>", "Output path (default: stdout)").action((opts) => {
9395
+ return new Command9(artifact).description(`Import external document as ${artifact}`).requiredOption("--from <path>", "Path to source file").requiredOption("--adapter <id>", "Adapter ID (e.g., generic:prd, bmad:brainstorm)").option("--output <path>", "Output path (default: stdout)").action((opts) => {
8875
9396
  try {
8876
- const sourcePath = resolve11(opts.from);
8877
- if (!existsSync21(sourcePath)) {
9397
+ const sourcePath = resolve12(opts.from);
9398
+ if (!existsSync22(sourcePath)) {
8878
9399
  throw new Error(`Source file not found: ${sourcePath}`);
8879
9400
  }
8880
- const sourceContent = readFileSync17(sourcePath, "utf-8");
9401
+ const sourceContent = readFileSync19(sourcePath, "utf-8");
8881
9402
  const result = runImportAdapter(opts.adapter, sourceContent);
8882
9403
  if (opts.output) {
8883
- const outputPath = resolve11(opts.output);
9404
+ const outputPath = resolve12(opts.output);
8884
9405
  if (artifact === "scenarios" && opts.adapter.endsWith(":test-cases")) {
8885
9406
  const scenarios = JSON.parse(result);
8886
- mkdirSync14(outputPath, { recursive: true });
9407
+ mkdirSync15(outputPath, { recursive: true });
8887
9408
  for (const scenario of scenarios) {
8888
- const filePath = join19(outputPath, `${scenario.name}.md`);
8889
- writeFileSync16(filePath, scenario.content, "utf-8");
9409
+ const filePath = join20(outputPath, `${scenario.name}.md`);
9410
+ writeFileSync17(filePath, scenario.content, "utf-8");
8890
9411
  success(` ${scenario.name}.md`);
8891
9412
  }
8892
9413
  header(`Imported ${scenarios.length} scenarios to ${opts.output}`);
8893
9414
  } else {
8894
- writeFileSync16(outputPath, result, "utf-8");
9415
+ writeFileSync17(outputPath, result, "utf-8");
8895
9416
  header(`Imported ${artifact}`);
8896
9417
  success(`Written to: ${opts.output}`);
8897
9418
  }
@@ -8904,7 +9425,7 @@ function createArtifactCommand(artifact) {
8904
9425
  }
8905
9426
  });
8906
9427
  }
8907
- var importCommand = new Command8("import").description("Import external artifacts into BeastMode format");
9428
+ var importCommand = new Command9("import").description("Import external artifacts into BeastMode format");
8908
9429
  for (const artifact of VALID_ARTIFACTS) {
8909
9430
  importCommand.addCommand(createArtifactCommand(artifact));
8910
9431
  }
@@ -8913,53 +9434,53 @@ for (const artifact of VALID_ARTIFACTS) {
8913
9434
  init_status_checker();
8914
9435
  init_schemas();
8915
9436
  init_display();
8916
- import { Command as Command9 } from "commander";
8917
- import { existsSync as existsSync22, readFileSync as readFileSync18, readdirSync as readdirSync8 } from "fs";
8918
- import { execSync as execSync5 } from "child_process";
8919
- import { join as join20, resolve as resolve12 } from "path";
9437
+ import { Command as Command10 } from "commander";
9438
+ import { existsSync as existsSync23, readFileSync as readFileSync20, readdirSync as readdirSync9 } from "fs";
9439
+ import { execSync as execSync6 } from "child_process";
9440
+ import { join as join21, resolve as resolve13 } from "path";
8920
9441
  function statusAction(factoryDir, opts) {
8921
- const bmDir = join20(factoryDir, ".beastmode");
8922
- if (!existsSync22(bmDir)) {
9442
+ const bmDir = join21(factoryDir, ".beastmode");
9443
+ if (!existsSync23(bmDir)) {
8923
9444
  throw new Error(`No factory found at ${factoryDir}`);
8924
9445
  }
8925
- const factoryJsonPath = join20(bmDir, "factory.json");
8926
- const rawIdentity = JSON.parse(readFileSync18(factoryJsonPath, "utf-8"));
9446
+ const factoryJsonPath = join21(bmDir, "factory.json");
9447
+ const rawIdentity = JSON.parse(readFileSync20(factoryJsonPath, "utf-8"));
8927
9448
  const factoryIdentity = FactoryIdentitySchema.parse(rawIdentity);
8928
- const projectsDir = join20(bmDir, "projects");
8929
- const projectCount = existsSync22(projectsDir) ? readdirSync8(projectsDir).filter((f) => f.endsWith(".json")).length : 0;
8930
- const pluginsDir = join20(bmDir, "plugins");
8931
- const pluginNames = existsSync22(pluginsDir) ? readdirSync8(pluginsDir) : [];
8932
- const mcpPath = join20(bmDir, "mcp-servers.json");
9449
+ const projectsDir = join21(bmDir, "projects");
9450
+ const projectCount = existsSync23(projectsDir) ? readdirSync9(projectsDir).filter((f) => f.endsWith(".json")).length : 0;
9451
+ const pluginsDir = join21(bmDir, "plugins");
9452
+ const pluginNames = existsSync23(pluginsDir) ? readdirSync9(pluginsDir) : [];
9453
+ const mcpPath = join21(bmDir, "mcp-servers.json");
8933
9454
  let mcpServers = {};
8934
- if (existsSync22(mcpPath)) {
9455
+ if (existsSync23(mcpPath)) {
8935
9456
  try {
8936
- const raw = JSON.parse(readFileSync18(mcpPath, "utf-8"));
9457
+ const raw = JSON.parse(readFileSync20(mcpPath, "utf-8"));
8937
9458
  mcpServers = raw.servers || {};
8938
9459
  } catch {
8939
9460
  }
8940
9461
  }
8941
- const hooksPath = join20(bmDir, "hooks.json");
9462
+ const hooksPath = join21(bmDir, "hooks.json");
8942
9463
  let hooks = {};
8943
- if (existsSync22(hooksPath)) {
9464
+ if (existsSync23(hooksPath)) {
8944
9465
  try {
8945
- const raw = JSON.parse(readFileSync18(hooksPath, "utf-8"));
9466
+ const raw = JSON.parse(readFileSync20(hooksPath, "utf-8"));
8946
9467
  hooks = raw.hooks || {};
8947
9468
  } catch {
8948
9469
  }
8949
9470
  }
8950
- const skillsDir = join20(bmDir, "skills");
8951
- const skillCount = existsSync22(skillsDir) ? readdirSync8(skillsDir).length : 0;
8952
- const runsDir = join20(factoryDir, "runs");
9471
+ const skillsDir = join21(bmDir, "skills");
9472
+ const skillCount = existsSync23(skillsDir) ? readdirSync9(skillsDir).length : 0;
9473
+ const runsDir = join21(factoryDir, "runs");
8953
9474
  let runDirs = [];
8954
- if (existsSync22(runsDir)) {
8955
- runDirs = readdirSync8(runsDir).filter((d) => d.startsWith("run-")).sort();
9475
+ if (existsSync23(runsDir)) {
9476
+ runDirs = readdirSync9(runsDir).filter((d) => d.startsWith("run-")).sort();
8956
9477
  }
8957
- const pidPath = join20(bmDir, "daemon.pid");
9478
+ const pidPath = join21(bmDir, "daemon.pid");
8958
9479
  let daemonPid = null;
8959
9480
  let pidAlive = false;
8960
- if (existsSync22(pidPath)) {
9481
+ if (existsSync23(pidPath)) {
8961
9482
  try {
8962
- daemonPid = parseInt(readFileSync18(pidPath, "utf-8").trim(), 10);
9483
+ daemonPid = parseInt(readFileSync20(pidPath, "utf-8").trim(), 10);
8963
9484
  process.kill(daemonPid, 0);
8964
9485
  pidAlive = true;
8965
9486
  } catch {
@@ -8968,7 +9489,7 @@ function statusAction(factoryDir, opts) {
8968
9489
  }
8969
9490
  if (!pidAlive) {
8970
9491
  try {
8971
- const out = execSync5("pgrep -f 'beastmode_daemon' 2>/dev/null || true", {
9492
+ const out = execSync6("pgrep -f 'beastmode_daemon' 2>/dev/null || true", {
8972
9493
  encoding: "utf-8",
8973
9494
  timeout: 3e3
8974
9495
  }).trim();
@@ -8995,8 +9516,8 @@ function statusAction(factoryDir, opts) {
8995
9516
  };
8996
9517
  return collectStatus(input);
8997
9518
  }
8998
- var statusCommand = new Command9("status").description("Show factory status overview").option("--json", "Output as JSON").option("--watch", "Enable watch mode (display layer)").action((opts) => {
8999
- const factoryDir = resolve12(".");
9519
+ var statusCommand = new Command10("status").description("Show factory status overview").option("--json", "Output as JSON").option("--watch", "Enable watch mode (display layer)").action((opts) => {
9520
+ const factoryDir = resolve13(".");
9000
9521
  try {
9001
9522
  const status = statusAction(factoryDir, { json: !!opts.json });
9002
9523
  if (opts.json) {
@@ -9032,20 +9553,20 @@ var statusCommand = new Command9("status").description("Show factory status over
9032
9553
  // src/cli/commands/config-cmd.ts
9033
9554
  init_config_manager();
9034
9555
  init_display();
9035
- import { Command as Command10 } from "commander";
9036
- import { existsSync as existsSync23, readFileSync as readFileSync19, writeFileSync as writeFileSync17 } from "fs";
9037
- import { join as join21, resolve as resolve13 } from "path";
9038
- import { execSync as execSync6 } from "child_process";
9556
+ import { Command as Command11 } from "commander";
9557
+ import { existsSync as existsSync24, readFileSync as readFileSync21, writeFileSync as writeFileSync18 } from "fs";
9558
+ import { join as join22, resolve as resolve14 } from "path";
9559
+ import { execSync as execSync7 } from "child_process";
9039
9560
  function readConfig2(factoryDir) {
9040
- const configPath = join21(factoryDir, ".beastmode", "config.json");
9041
- if (!existsSync23(configPath)) {
9561
+ const configPath = join22(factoryDir, ".beastmode", "config.json");
9562
+ if (!existsSync24(configPath)) {
9042
9563
  throw new Error("No config.json found. Run beastmode init first.");
9043
9564
  }
9044
- return JSON.parse(readFileSync19(configPath, "utf-8"));
9565
+ return JSON.parse(readFileSync21(configPath, "utf-8"));
9045
9566
  }
9046
9567
  function writeConfig2(factoryDir, config) {
9047
- const configPath = join21(factoryDir, ".beastmode", "config.json");
9048
- writeFileSync17(configPath, JSON.stringify(config, null, 2) + "\n");
9568
+ const configPath = join22(factoryDir, ".beastmode", "config.json");
9569
+ writeFileSync18(configPath, JSON.stringify(config, null, 2) + "\n");
9049
9570
  }
9050
9571
  function configGetAction(factoryDir, key) {
9051
9572
  const config = readConfig2(factoryDir);
@@ -9062,8 +9583,8 @@ function configResetAction(factoryDir, key) {
9062
9583
  const updated = configReset(config, key);
9063
9584
  writeConfig2(factoryDir, updated);
9064
9585
  }
9065
- var configGetCmd = new Command10("get").description("Get a config value by dot-notation key").argument("<key>", "Config key (e.g., pipeline.preset)").action((key) => {
9066
- const factoryDir = resolve13(".");
9586
+ var configGetCmd = new Command11("get").description("Get a config value by dot-notation key").argument("<key>", "Config key (e.g., pipeline.preset)").action((key) => {
9587
+ const factoryDir = resolve14(".");
9067
9588
  try {
9068
9589
  const value = configGetAction(factoryDir, key);
9069
9590
  if (typeof value === "object" && value !== null) {
@@ -9076,8 +9597,8 @@ var configGetCmd = new Command10("get").description("Get a config value by dot-n
9076
9597
  process.exit(1);
9077
9598
  }
9078
9599
  });
9079
- var configSetCmd = new Command10("set").description("Set a config value by dot-notation key").argument("<key>", "Config key (e.g., pipeline.preset)").argument("<value>", "Value to set (auto-coerces types)").action((key, value) => {
9080
- const factoryDir = resolve13(".");
9600
+ var configSetCmd = new Command11("set").description("Set a config value by dot-notation key").argument("<key>", "Config key (e.g., pipeline.preset)").argument("<value>", "Value to set (auto-coerces types)").action((key, value) => {
9601
+ const factoryDir = resolve14(".");
9081
9602
  try {
9082
9603
  configSetAction(factoryDir, key, value);
9083
9604
  success(`Set ${key} = ${value}`);
@@ -9086,23 +9607,23 @@ var configSetCmd = new Command10("set").description("Set a config value by dot-n
9086
9607
  process.exit(1);
9087
9608
  }
9088
9609
  });
9089
- var configEditCmd = new Command10("edit").description("Open config.json in $EDITOR").action(() => {
9090
- const factoryDir = resolve13(".");
9091
- const configPath = join21(factoryDir, ".beastmode", "config.json");
9092
- if (!existsSync23(configPath)) {
9610
+ var configEditCmd = new Command11("edit").description("Open config.json in $EDITOR").action(() => {
9611
+ const factoryDir = resolve14(".");
9612
+ const configPath = join22(factoryDir, ".beastmode", "config.json");
9613
+ if (!existsSync24(configPath)) {
9093
9614
  error("No config.json found. Run beastmode init first.");
9094
9615
  process.exit(1);
9095
9616
  }
9096
9617
  const editor = process.env.EDITOR || "vi";
9097
9618
  try {
9098
- execSync6(`${editor} ${configPath}`, { stdio: "inherit" });
9619
+ execSync7(`${editor} ${configPath}`, { stdio: "inherit" });
9099
9620
  } catch {
9100
9621
  error(`Failed to open editor: ${editor}`);
9101
9622
  process.exit(1);
9102
9623
  }
9103
9624
  });
9104
- var configResetCmd = new Command10("reset").description("Reset config to defaults (all or specific key)").argument("[key]", "Optional key to reset (resets all if omitted)").action((key) => {
9105
- const factoryDir = resolve13(".");
9625
+ var configResetCmd = new Command11("reset").description("Reset config to defaults (all or specific key)").argument("[key]", "Optional key to reset (resets all if omitted)").action((key) => {
9626
+ const factoryDir = resolve14(".");
9106
9627
  try {
9107
9628
  configResetAction(factoryDir, key);
9108
9629
  if (key) {
@@ -9115,7 +9636,7 @@ var configResetCmd = new Command10("reset").description("Reset config to default
9115
9636
  process.exit(1);
9116
9637
  }
9117
9638
  });
9118
- var configCommand = new Command10("config").description("Manage factory configuration").addCommand(configGetCmd).addCommand(configSetCmd).addCommand(configEditCmd).addCommand(configResetCmd);
9639
+ var configCommand = new Command11("config").description("Manage factory configuration").addCommand(configGetCmd).addCommand(configSetCmd).addCommand(configEditCmd).addCommand(configResetCmd);
9119
9640
 
9120
9641
  // src/cli/commands/doctor.ts
9121
9642
  init_doctor();
@@ -9124,180 +9645,13 @@ init_version();
9124
9645
  init_plugin_resolver();
9125
9646
  init_display();
9126
9647
  import { Command as Command12 } from "commander";
9127
- import { existsSync as existsSync25, readFileSync as readFileSync21, readdirSync as readdirSync10 } from "fs";
9648
+ import { existsSync as existsSync25, readFileSync as readFileSync22, readdirSync as readdirSync10 } from "fs";
9128
9649
  import { join as join23, resolve as resolve15 } from "path";
9129
- import { execSync as execSync8 } from "child_process";
9650
+ import { execSync as execSync8, spawnSync as spawnSync3 } from "child_process";
9130
9651
  import { homedir as homedir3, platform as platform2 } from "os";
9131
9652
  import * as http3 from "http";
9132
9653
  import * as https from "https";
9133
-
9134
- // src/cli/commands/board.ts
9135
- init_display();
9136
- import { Command as Command11 } from "commander";
9137
- import { resolve as resolve14, join as join22 } from "path";
9138
- import { existsSync as existsSync24, readFileSync as readFileSync20, mkdirSync as mkdirSync15, writeFileSync as writeFileSync18, readdirSync as readdirSync9 } from "fs";
9139
- import { execSync as execSync7 } from "child_process";
9140
- var boardCommand = new Command11("board").description("Launch the BeastMode Board web UI").option("--port <number>", "Port to serve on", "7669").option("--host <host>", "Host to bind to (use 0.0.0.0 for external access)", "127.0.0.1").action(async (opts) => {
9141
- try {
9142
- await runBoard(opts);
9143
- } catch (err) {
9144
- error(err.message);
9145
- process.exit(1);
9146
- }
9147
- });
9148
- boardCommand.command("set-status <item_id> <status>").description(
9149
- "Safely change a board item's status via the board HTTP API (never sqlite3 against the live DB \u2014 Gap 24)"
9150
- ).option("--project <name>", "Project/board name (defaults to the factory's only project, or errors if ambiguous)").option("--board-url <url>", "Override board HTTP URL (default: use running board container via docker exec)").action(async (itemId, status, opts) => {
9151
- try {
9152
- await setItemStatus(itemId, status, opts);
9153
- } catch (err) {
9154
- error(err.message);
9155
- process.exit(1);
9156
- }
9157
- });
9158
- function findFactoryDir(startDir) {
9159
- let dir = startDir || process.cwd();
9160
- const root = resolve14("/");
9161
- while (dir !== root) {
9162
- if (existsSync24(join22(dir, ".beastmode", "factory.json"))) {
9163
- return dir;
9164
- }
9165
- const parent = resolve14(dir, "..");
9166
- if (parent === dir) break;
9167
- dir = parent;
9168
- }
9169
- if (existsSync24(join22(dir, ".beastmode", "factory.json"))) {
9170
- return dir;
9171
- }
9172
- return null;
9173
- }
9174
- function inferProjectName(factoryDir) {
9175
- const projectsDir = join22(factoryDir, ".beastmode", "projects");
9176
- if (!existsSync24(projectsDir)) return null;
9177
- const files = readdirSync9(projectsDir).filter((f) => f.endsWith(".json"));
9178
- if (files.length === 1) return files[0].replace(/\.json$/, "");
9179
- return null;
9180
- }
9181
- function tryExecSync(cmd, timeoutMs = 15e3) {
9182
- try {
9183
- return execSync7(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs }).trim();
9184
- } catch {
9185
- return null;
9186
- }
9187
- }
9188
- async function setItemStatus(itemId, status, opts) {
9189
- if (!itemId || !/^\d+$/.test(itemId)) {
9190
- throw new Error(`Invalid item_id: ${itemId} (expected numeric id)`);
9191
- }
9192
- const trimmedStatus = status.trim();
9193
- if (!trimmedStatus) {
9194
- throw new Error("status cannot be empty");
9195
- }
9196
- let project = opts.project;
9197
- if (!project) {
9198
- const factoryDir = findFactoryDir();
9199
- if (!factoryDir) {
9200
- throw new Error("No .beastmode factory found in cwd tree. Pass --project <name> explicitly.");
9201
- }
9202
- const inferred = inferProjectName(factoryDir);
9203
- if (!inferred) {
9204
- throw new Error(
9205
- "Could not determine project name from factory (0 or >1 projects configured). Pass --project <name>."
9206
- );
9207
- }
9208
- project = inferred;
9209
- }
9210
- const payload = JSON.stringify({ status: trimmedStatus });
9211
- if (opts.boardUrl) {
9212
- const url2 = `${opts.boardUrl.replace(/\/+$/, "")}/api/items/${itemId}?board=${encodeURIComponent(project)}`;
9213
- const cmd2 = `curl -sS -X PATCH '${url2}' -H 'Content-Type: application/json' -d '${payload.replace(/'/g, "'\\''")}'`;
9214
- const out2 = tryExecSync(cmd2);
9215
- if (!out2) throw new Error(`curl failed hitting ${url2}`);
9216
- info(out2);
9217
- return;
9218
- }
9219
- const container = tryExecSync(`docker ps --filter 'label=com.docker.compose.service=daemon' --format '{{.Names}}' | head -n1`) || tryExecSync(`docker ps --filter 'label=com.docker.compose.service=ui' --format '{{.Names}}' | head -n1`);
9220
- if (!container) {
9221
- throw new Error(
9222
- "No running daemon or ui container found. Start the factory with `docker compose up -d`, or pass --board-url."
9223
- );
9224
- }
9225
- const escapedPayload = payload.replace(/'/g, "'\\''");
9226
- const url = `http://board:8080/api/items/${itemId}?board=${encodeURIComponent(project)}`;
9227
- const cmd = `docker exec ${container} curl -sS -X PATCH '${url}' -H 'Content-Type: application/json' -d '${escapedPayload}'`;
9228
- const out = tryExecSync(cmd);
9229
- if (!out) {
9230
- throw new Error(`docker exec curl failed hitting ${url} via ${container}`);
9231
- }
9232
- try {
9233
- const parsed = JSON.parse(out);
9234
- if (parsed.detail) {
9235
- throw new Error(`Board rejected update: ${parsed.detail}`);
9236
- }
9237
- if (parsed.id && parsed.status) {
9238
- info(`Item ${parsed.id} \u2192 ${parsed.status} (project=${project})`);
9239
- return;
9240
- }
9241
- } catch (e) {
9242
- if (e instanceof Error && e.message.startsWith("Board rejected update:")) throw e;
9243
- }
9244
- info(out);
9245
- }
9246
- async function runBoard(opts) {
9247
- let factoryDir = findFactoryDir();
9248
- if (!factoryDir) {
9249
- factoryDir = process.cwd();
9250
- const bmDir = join22(factoryDir, ".beastmode");
9251
- if (!existsSync24(bmDir)) {
9252
- mkdirSync15(bmDir, { recursive: true });
9253
- }
9254
- const factoryJsonPath2 = join22(bmDir, "factory.json");
9255
- if (!existsSync24(factoryJsonPath2)) {
9256
- writeFileSync18(
9257
- factoryJsonPath2,
9258
- JSON.stringify({ factory_name: "BeastMode", created_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
9259
- "utf-8"
9260
- );
9261
- info("No factory found \u2014 created minimal stub at .beastmode/factory.json");
9262
- }
9263
- }
9264
- const factoryJsonPath = join22(factoryDir, ".beastmode", "factory.json");
9265
- const factoryJson = JSON.parse(readFileSync20(factoryJsonPath, "utf-8"));
9266
- const factoryName = factoryJson.factory_name || "BeastMode Factory";
9267
- header(`BeastMode Board \u2014 ${factoryName}`);
9268
- const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
9269
- const port = parseInt(opts.port, 10) || 7669;
9270
- const uiServer = await startServer2({
9271
- port,
9272
- host: opts.host,
9273
- factoryName,
9274
- factoryPath: factoryDir,
9275
- mode: "board"
9276
- });
9277
- info(`Board running at ${uiServer.url}/board`);
9278
- info("Press Ctrl+C to stop.\n");
9279
- const skipOpen = process.env.BEASTMODE_NO_OPEN === "1" || !!process.env.DOCKER_CONTAINER;
9280
- if (!skipOpen) {
9281
- try {
9282
- const open = (await import("open")).default;
9283
- await open(`${uiServer.url}/board`);
9284
- } catch {
9285
- info(`Open ${uiServer.url}/board in your browser.`);
9286
- }
9287
- } else {
9288
- info(`Open ${uiServer.url}/board in your browser.`);
9289
- }
9290
- await new Promise((resolvePromise) => {
9291
- uiServer.server.on("close", resolvePromise);
9292
- process.on("SIGINT", async () => {
9293
- info("\nShutting down board server...");
9294
- await uiServer.shutdown();
9295
- resolvePromise();
9296
- });
9297
- });
9298
- }
9299
-
9300
- // src/cli/commands/doctor.ts
9654
+ init_board();
9301
9655
  function tryExec(cmd, timeout = 8e3) {
9302
9656
  try {
9303
9657
  return execSync8(cmd, { encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }).trim();
@@ -9439,7 +9793,7 @@ async function checkProjectGithubToken(env, factoryDir) {
9439
9793
  const envPath = join23(factoryDir, ".env");
9440
9794
  if (existsSync25(envPath)) {
9441
9795
  try {
9442
- const content = readFileSync21(envPath, "utf-8");
9796
+ const content = readFileSync22(envPath, "utf-8");
9443
9797
  const dirLine = content.split("\n").find(
9444
9798
  (l) => l.trim().startsWith("PROJECT_DIR=") && !l.trim().startsWith("#")
9445
9799
  );
@@ -9672,7 +10026,7 @@ function checkProjectDirEnv(factoryDir) {
9672
10026
  }
9673
10027
  let content;
9674
10028
  try {
9675
- content = readFileSync21(envPath, "utf-8");
10029
+ content = readFileSync22(envPath, "utf-8");
9676
10030
  } catch {
9677
10031
  return {
9678
10032
  label: "PROJECT_DIR (.env)",
@@ -10046,7 +10400,7 @@ function checkProjectDirectory(factoryDir) {
10046
10400
  let anyFail = false;
10047
10401
  for (const file of projectFiles) {
10048
10402
  try {
10049
- const proj = JSON.parse(readFileSync21(join23(projectsDir, file), "utf-8"));
10403
+ const proj = JSON.parse(readFileSync22(join23(projectsDir, file), "utf-8"));
10050
10404
  const projPath = proj.path || "";
10051
10405
  const projName = proj.name || file.replace(".json", "");
10052
10406
  if (!projPath || !existsSync25(projPath)) {
@@ -10130,7 +10484,7 @@ function checkStack(factoryDir) {
10130
10484
  const stacks = [];
10131
10485
  for (const file of projectFiles) {
10132
10486
  try {
10133
- const proj = JSON.parse(readFileSync21(join23(projectsDir, file), "utf-8"));
10487
+ const proj = JSON.parse(readFileSync22(join23(projectsDir, file), "utf-8"));
10134
10488
  const name = proj.name || file.replace(".json", "");
10135
10489
  const detected = proj.stack?.detected || "unknown";
10136
10490
  const build = proj.stack?.build_command || "";
@@ -10179,7 +10533,7 @@ function checkBoardPassword(env, factoryDir) {
10179
10533
  for (const filePath of [dotEnv, secretsEnv]) {
10180
10534
  if (existsSync25(filePath)) {
10181
10535
  try {
10182
- const content = readFileSync21(filePath, "utf-8");
10536
+ const content = readFileSync22(filePath, "utf-8");
10183
10537
  const lines = content.split("\n");
10184
10538
  const line = lines.find((l) => l.startsWith("BEASTMODE_UI_PASSWORD="));
10185
10539
  if (line) {
@@ -10206,7 +10560,7 @@ function doctorAction(factoryDir, env) {
10206
10560
  let factoryIdentity = null;
10207
10561
  if (factoryDirExists) {
10208
10562
  try {
10209
- const raw = JSON.parse(readFileSync21(join23(bmDir, "factory.json"), "utf-8"));
10563
+ const raw = JSON.parse(readFileSync22(join23(bmDir, "factory.json"), "utf-8"));
10210
10564
  factoryIdentity = FactoryIdentitySchema.parse(raw);
10211
10565
  } catch {
10212
10566
  }
@@ -10217,7 +10571,7 @@ function doctorAction(factoryDir, env) {
10217
10571
  const configPath = join23(bmDir, "config.json");
10218
10572
  if (existsSync25(configPath)) {
10219
10573
  try {
10220
- const raw = JSON.parse(readFileSync21(configPath, "utf-8"));
10574
+ const raw = JSON.parse(readFileSync22(configPath, "utf-8"));
10221
10575
  const result = FactoryConfigSchema.safeParse(raw);
10222
10576
  if (result.success) {
10223
10577
  config = raw;
@@ -10238,7 +10592,7 @@ function doctorAction(factoryDir, env) {
10238
10592
  for (const file of readdirSync10(projectsDir)) {
10239
10593
  if (file.endsWith(".json")) {
10240
10594
  try {
10241
- const proj = JSON.parse(readFileSync21(join23(projectsDir, file), "utf-8"));
10595
+ const proj = JSON.parse(readFileSync22(join23(projectsDir, file), "utf-8"));
10242
10596
  if (proj.path) {
10243
10597
  projectPaths.push({
10244
10598
  name: proj.name || file.replace(".json", ""),
@@ -10252,42 +10606,357 @@ function doctorAction(factoryDir, env) {
10252
10606
  }
10253
10607
  }
10254
10608
  }
10255
- const installedPlugins = [];
10256
- if (factoryDirExists) {
10257
- const pluginsDir = join23(bmDir, "plugins");
10258
- if (existsSync25(pluginsDir)) {
10259
- for (const pluginName of readdirSync10(pluginsDir)) {
10260
- const manifestPath = join23(pluginsDir, pluginName, "manifest.json");
10261
- if (existsSync25(manifestPath)) {
10262
- try {
10263
- const manifest = JSON.parse(readFileSync21(manifestPath, "utf-8"));
10264
- installedPlugins.push({
10265
- name: pluginName,
10266
- engine_version: manifest.engine_version || "*"
10609
+ const installedPlugins = [];
10610
+ if (factoryDirExists) {
10611
+ const pluginsDir = join23(bmDir, "plugins");
10612
+ if (existsSync25(pluginsDir)) {
10613
+ for (const pluginName of readdirSync10(pluginsDir)) {
10614
+ const manifestPath = join23(pluginsDir, pluginName, "manifest.json");
10615
+ if (existsSync25(manifestPath)) {
10616
+ try {
10617
+ const manifest = JSON.parse(readFileSync22(manifestPath, "utf-8"));
10618
+ installedPlugins.push({
10619
+ name: pluginName,
10620
+ engine_version: manifest.engine_version || "*"
10621
+ });
10622
+ } catch {
10623
+ }
10624
+ }
10625
+ }
10626
+ }
10627
+ }
10628
+ const input = {
10629
+ factoryDirExists,
10630
+ factoryIdentity,
10631
+ config,
10632
+ configParseError,
10633
+ env,
10634
+ projectPaths,
10635
+ installedPlugins,
10636
+ engineVersion: ENGINE_VERSION,
10637
+ portsInUse: []
10638
+ };
10639
+ return runDiagnostics(input);
10640
+ }
10641
+ var SYSTEMD_CREDS_TIMER = "beastmode-claude-creds.timer";
10642
+ var LAUNCH_AGENT_LABEL2 = "com.develeap.beastmode.claude-creds";
10643
+ function checkCredentialSyncAgent() {
10644
+ const results = [];
10645
+ const credsPath = join23(homedir3(), ".claude", ".credentials.json");
10646
+ if (!existsSync25(credsPath)) {
10647
+ results.push({
10648
+ label: "Claude credentials file",
10649
+ status: "fail",
10650
+ detail: "~/.claude/.credentials.json not found",
10651
+ fix: platform2() === "darwin" ? "Run `claude login` then `beastmode sync-claude-creds --install`" : "Run `claude login`"
10652
+ });
10653
+ return results;
10654
+ }
10655
+ results.push({
10656
+ label: "Claude credentials file",
10657
+ status: "pass",
10658
+ detail: credsPath
10659
+ });
10660
+ try {
10661
+ const creds = JSON.parse(readFileSync22(credsPath, "utf-8"));
10662
+ const expiresAt = creds?.claudeAiOauth?.expiresAt;
10663
+ if (typeof expiresAt === "number") {
10664
+ const hoursLeft = Math.round((expiresAt - Date.now()) / 36e5);
10665
+ if (hoursLeft < 0) {
10666
+ results.push({
10667
+ label: "Claude token expiry",
10668
+ status: "fail",
10669
+ detail: `Token expired ${Math.abs(hoursLeft)}h ago`,
10670
+ fix: "Run `claude login` (then `beastmode sync-claude-creds` on macOS)"
10671
+ });
10672
+ } else if (hoursLeft < 2) {
10673
+ results.push({
10674
+ label: "Claude token expiry",
10675
+ status: "warn",
10676
+ detail: `Token expires in ${hoursLeft}h`,
10677
+ fix: "Token will auto-refresh if the sync agent is running"
10678
+ });
10679
+ } else {
10680
+ results.push({
10681
+ label: "Claude token expiry",
10682
+ status: "pass",
10683
+ detail: `Token valid (${hoursLeft}h remaining)`
10684
+ });
10685
+ }
10686
+ } else {
10687
+ results.push({
10688
+ label: "Claude token expiry",
10689
+ status: "warn",
10690
+ detail: "No expiry timestamp in credentials \u2014 cannot check freshness"
10691
+ });
10692
+ }
10693
+ } catch {
10694
+ results.push({
10695
+ label: "Claude token expiry",
10696
+ status: "warn",
10697
+ detail: "credentials file exists but could not be parsed"
10698
+ });
10699
+ }
10700
+ if (platform2() === "darwin") {
10701
+ const uid = process.getuid?.();
10702
+ const result = spawnSync3(
10703
+ "launchctl",
10704
+ ["print", `gui/${uid}/${LAUNCH_AGENT_LABEL2}`],
10705
+ { stdio: "pipe", encoding: "utf-8" }
10706
+ );
10707
+ if (result.status === 0) {
10708
+ results.push({
10709
+ label: "Credential sync agent",
10710
+ status: "pass",
10711
+ detail: "launchd LaunchAgent loaded"
10712
+ });
10713
+ } else {
10714
+ results.push({
10715
+ label: "Credential sync agent",
10716
+ status: "warn",
10717
+ detail: "launchd LaunchAgent not loaded",
10718
+ fix: "Run `beastmode sync-claude-creds --install`"
10719
+ });
10720
+ }
10721
+ } else if (platform2() === "linux") {
10722
+ const result = spawnSync3(
10723
+ "systemctl",
10724
+ ["--user", "is-active", SYSTEMD_CREDS_TIMER],
10725
+ { stdio: "pipe", encoding: "utf-8" }
10726
+ );
10727
+ if ((result.stdout || "").trim() === "active") {
10728
+ results.push({
10729
+ label: "Credential sync agent",
10730
+ status: "pass",
10731
+ detail: `${SYSTEMD_CREDS_TIMER} active`
10732
+ });
10733
+ } else {
10734
+ results.push({
10735
+ label: "Credential sync agent",
10736
+ status: "warn",
10737
+ detail: `${SYSTEMD_CREDS_TIMER} not active`,
10738
+ fix: "Run `beastmode sync-claude-creds --install`"
10739
+ });
10740
+ }
10741
+ }
10742
+ return results;
10743
+ }
10744
+ async function checkDaemonCredentialHealth() {
10745
+ const ports = [8420, 8080];
10746
+ let parsed = null;
10747
+ let lastStatus = null;
10748
+ for (const port of ports) {
10749
+ const data2 = await new Promise(
10750
+ (resolveProm) => {
10751
+ const req = http3.get(
10752
+ {
10753
+ host: "127.0.0.1",
10754
+ port,
10755
+ path: "/api/credentials/status",
10756
+ timeout: 5e3
10757
+ },
10758
+ (res) => {
10759
+ let body = "";
10760
+ res.on("data", (chunk) => {
10761
+ body += chunk;
10762
+ });
10763
+ res.on("end", () => {
10764
+ resolveProm({ status: res.statusCode ?? null, body });
10765
+ });
10766
+ }
10767
+ );
10768
+ req.on("error", () => resolveProm({ status: null, body: "" }));
10769
+ req.on("timeout", () => {
10770
+ req.destroy();
10771
+ resolveProm({ status: null, body: "" });
10772
+ });
10773
+ }
10774
+ );
10775
+ if (data2.status !== null) {
10776
+ lastStatus = data2.status;
10777
+ if (data2.status >= 200 && data2.status < 300) {
10778
+ try {
10779
+ parsed = JSON.parse(data2.body);
10780
+ } catch {
10781
+ parsed = null;
10782
+ }
10783
+ break;
10784
+ }
10785
+ }
10786
+ }
10787
+ if (parsed === null) {
10788
+ return [
10789
+ {
10790
+ label: "Daemon credential broker",
10791
+ status: "info",
10792
+ detail: lastStatus === null ? "Could not reach board API \u2014 Docker Compose may not be running (skipped)" : `Board API returned HTTP ${lastStatus}`
10793
+ }
10794
+ ];
10795
+ }
10796
+ const data = parsed;
10797
+ if (data.healthy === null) {
10798
+ return [
10799
+ {
10800
+ label: "Daemon credential broker",
10801
+ status: "warn",
10802
+ detail: "No credential status reported \u2014 daemon may not be running"
10803
+ }
10804
+ ];
10805
+ }
10806
+ if (data.stale === true) {
10807
+ return [
10808
+ {
10809
+ label: "Daemon credential broker",
10810
+ status: "warn",
10811
+ detail: `Last status report is stale (reported at ${data.reported_at})`
10812
+ }
10813
+ ];
10814
+ }
10815
+ if (data.degraded === true) {
10816
+ return [
10817
+ {
10818
+ label: "Daemon credential broker",
10819
+ status: "fail",
10820
+ detail: `Broker DEGRADED \u2014 ${data.consecutive_failures ?? "?"} consecutive auth failures`,
10821
+ fix: "Run `claude login` then `beastmode sync-claude-creds`"
10822
+ }
10823
+ ];
10824
+ }
10825
+ if (data.token_expired === true) {
10826
+ return [
10827
+ {
10828
+ label: "Daemon credential broker",
10829
+ status: "fail",
10830
+ detail: "Broker reports token is expired",
10831
+ fix: "Run `claude login` (then `beastmode sync-claude-creds` on macOS)"
10832
+ }
10833
+ ];
10834
+ }
10835
+ return [
10836
+ {
10837
+ label: "Daemon credential broker",
10838
+ status: "pass",
10839
+ detail: `Healthy \u2014 token expires ${data.token_expires_at ?? "unknown"}`
10840
+ }
10841
+ ];
10842
+ }
10843
+ async function checkDaemonCredHealth() {
10844
+ const ports = [8420, 8080];
10845
+ let parsed = null;
10846
+ let lastStatus = null;
10847
+ for (const port of ports) {
10848
+ const data2 = await new Promise(
10849
+ (resolveProm) => {
10850
+ const req = http3.get(
10851
+ {
10852
+ host: "127.0.0.1",
10853
+ port,
10854
+ path: "/api/daemon/cred-health",
10855
+ timeout: 5e3
10856
+ },
10857
+ (res) => {
10858
+ let body = "";
10859
+ res.on("data", (chunk) => {
10860
+ body += chunk;
10861
+ });
10862
+ res.on("end", () => {
10863
+ resolveProm({ status: res.statusCode ?? null, body });
10267
10864
  });
10268
- } catch {
10269
10865
  }
10270
- }
10866
+ );
10867
+ req.on("error", () => resolveProm({ status: null, body: "" }));
10868
+ req.on("timeout", () => {
10869
+ req.destroy();
10870
+ resolveProm({ status: null, body: "" });
10871
+ });
10872
+ }
10873
+ );
10874
+ if (data2.status !== null) {
10875
+ lastStatus = data2.status;
10876
+ try {
10877
+ parsed = JSON.parse(data2.body);
10878
+ } catch {
10879
+ parsed = null;
10271
10880
  }
10881
+ break;
10272
10882
  }
10273
10883
  }
10274
- const input = {
10275
- factoryDirExists,
10276
- factoryIdentity,
10277
- config,
10278
- configParseError,
10279
- env,
10280
- projectPaths,
10281
- installedPlugins,
10282
- engineVersion: ENGINE_VERSION,
10283
- portsInUse: []
10284
- };
10285
- return runDiagnostics(input);
10884
+ if (lastStatus === null) {
10885
+ return [
10886
+ {
10887
+ label: "Daemon cred-health endpoint",
10888
+ status: "info",
10889
+ detail: "Board API not reachable \u2014 Docker Compose may not be running (skipped)"
10890
+ }
10891
+ ];
10892
+ }
10893
+ if (lastStatus === 503) {
10894
+ const detail = parsed && typeof parsed.detail === "string" ? String(parsed.detail) : "Daemon hasn't reported credential state yet";
10895
+ return [
10896
+ {
10897
+ label: "Daemon cred-health endpoint",
10898
+ status: "warn",
10899
+ detail
10900
+ }
10901
+ ];
10902
+ }
10903
+ if (lastStatus !== 200 || parsed === null) {
10904
+ return [
10905
+ {
10906
+ label: "Daemon cred-health endpoint",
10907
+ status: "warn",
10908
+ detail: `Unexpected HTTP ${lastStatus} from /api/daemon/cred-health`
10909
+ }
10910
+ ];
10911
+ }
10912
+ const data = parsed;
10913
+ const status = String(data.status ?? "unknown");
10914
+ const urgent = data.urgency_marker_present === true;
10915
+ if (status === "failed") {
10916
+ return [
10917
+ {
10918
+ label: "Daemon cred-health endpoint",
10919
+ status: "fail",
10920
+ detail: `Broker FAILED${urgent ? " (urgency marker present)" : ""}`,
10921
+ fix: "Run `claude login` then `beastmode sync-claude-creds`"
10922
+ }
10923
+ ];
10924
+ }
10925
+ if (status === "degraded") {
10926
+ return [
10927
+ {
10928
+ label: "Daemon cred-health endpoint",
10929
+ status: "warn",
10930
+ detail: `Broker degraded${urgent ? " \u2014 urgency marker present" : ""}`,
10931
+ fix: "Ensure the credential sync agent is running on the host"
10932
+ }
10933
+ ];
10934
+ }
10935
+ if (status === "healthy") {
10936
+ const hours = data.token_hours_remaining;
10937
+ const detail = typeof hours === "number" ? `Healthy \u2014 token has ${hours.toFixed(1)}h remaining` : "Healthy";
10938
+ return [
10939
+ {
10940
+ label: "Daemon cred-health endpoint",
10941
+ status: "pass",
10942
+ detail
10943
+ }
10944
+ ];
10945
+ }
10946
+ return [
10947
+ {
10948
+ label: "Daemon cred-health endpoint",
10949
+ status: "warn",
10950
+ detail: `Unknown broker status: ${status}`
10951
+ }
10952
+ ];
10286
10953
  }
10287
10954
  function printCheck(check) {
10288
10955
  const labelPad = check.label.padEnd(22);
10289
10956
  if (check.status === "pass") {
10290
10957
  success(`${labelPad} ${check.detail}`);
10958
+ } else if (check.status === "info") {
10959
+ info(`${labelPad} ${check.detail}`);
10291
10960
  } else if (check.status === "warn") {
10292
10961
  warn(`${labelPad} ${check.detail}`);
10293
10962
  if (check.fix) {
@@ -10308,6 +10977,9 @@ var doctorCommand = new Command12("doctor").description("Health check \u2014 val
10308
10977
  const hasFactory = existsSync25(join23(factoryDir, ".beastmode"));
10309
10978
  const checks = [];
10310
10979
  checks.push(...await checkClaudeAuth(env.ANTHROPIC_API_KEY));
10980
+ checks.push(...checkCredentialSyncAgent());
10981
+ checks.push(...await checkDaemonCredentialHealth());
10982
+ checks.push(...await checkDaemonCredHealth());
10311
10983
  checks.push(checkGithubToken(env));
10312
10984
  checks.push(await checkProjectGithubToken(env, hasFactory ? factoryDir : null));
10313
10985
  checks.push(checkGhcrPullToken(env));
@@ -10378,11 +11050,11 @@ init_schemas();
10378
11050
  init_version();
10379
11051
  init_display();
10380
11052
  import { Command as Command13 } from "commander";
10381
- import { existsSync as existsSync27, readFileSync as readFileSync23, writeFileSync as writeFileSync20 } from "fs";
11053
+ import { existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "fs";
10382
11054
  import { join as join25, resolve as resolve16 } from "path";
10383
11055
 
10384
11056
  // src/cli/utils/regenerate.ts
10385
- import { existsSync as existsSync26, readFileSync as readFileSync22, writeFileSync as writeFileSync19, copyFileSync } from "fs";
11057
+ import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync19, copyFileSync } from "fs";
10386
11058
  import { join as join24 } from "path";
10387
11059
  var RECOGNIZED_KEYS = /* @__PURE__ */ new Set([
10388
11060
  "PROJECT_DIR",
@@ -10509,7 +11181,7 @@ function regenerateFactoryFiles(factoryDir, now = /* @__PURE__ */ new Date()) {
10509
11181
  `docker-compose.yml not found at ${composePath}. This does not look like a beastmode factory \u2014 run 'beastmode init' first.`
10510
11182
  );
10511
11183
  }
10512
- const existingEnv = readFileSync22(envPath, "utf-8");
11184
+ const existingEnv = readFileSync23(envPath, "utf-8");
10513
11185
  const values = parseExistingEnv(existingEnv);
10514
11186
  const missing = [];
10515
11187
  if (!values.projectDir) missing.push("PROJECT_DIR");
@@ -10527,7 +11199,7 @@ function regenerateFactoryFiles(factoryDir, now = /* @__PURE__ */ new Date()) {
10527
11199
  }
10528
11200
  const newEnv = buildNewEnv(values);
10529
11201
  const newCompose = generateComposeYaml("latest");
10530
- const existingCompose = readFileSync22(composePath, "utf-8");
11202
+ const existingCompose = readFileSync23(composePath, "utf-8");
10531
11203
  const envChanged = newEnv !== existingEnv;
10532
11204
  const composeChanged = newCompose !== existingCompose;
10533
11205
  if (!envChanged && !composeChanged) {
@@ -10567,14 +11239,14 @@ function readIdentity(factoryDir) {
10567
11239
  if (!existsSync27(path)) {
10568
11240
  throw new Error("No factory.json found. Run beastmode init first.");
10569
11241
  }
10570
- return FactoryIdentitySchema.parse(JSON.parse(readFileSync23(path, "utf-8")));
11242
+ return FactoryIdentitySchema.parse(JSON.parse(readFileSync24(path, "utf-8")));
10571
11243
  }
10572
11244
  function readConfig3(factoryDir) {
10573
11245
  const path = join25(factoryDir, ".beastmode", "config.json");
10574
11246
  if (!existsSync27(path)) {
10575
11247
  return {};
10576
11248
  }
10577
- return JSON.parse(readFileSync23(path, "utf-8"));
11249
+ return JSON.parse(readFileSync24(path, "utf-8"));
10578
11250
  }
10579
11251
  function upgradeCheckAction(factoryDir) {
10580
11252
  const identity = readIdentity(factoryDir);
@@ -10674,12 +11346,15 @@ var upgradeCommand = new Command13("upgrade").description("Upgrade engine versio
10674
11346
  }
10675
11347
  });
10676
11348
 
11349
+ // src/index.ts
11350
+ init_board();
11351
+
10677
11352
  // src/cli/commands/migrate.ts
10678
11353
  init_display();
10679
11354
  init_migrator();
10680
11355
  import { Command as Command14 } from "commander";
10681
11356
  import { resolve as resolve17, join as join26 } from "path";
10682
- import { existsSync as existsSync28, readFileSync as readFileSync24, mkdirSync as mkdirSync16, writeFileSync as writeFileSync21 } from "fs";
11357
+ import { existsSync as existsSync28, readFileSync as readFileSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync21 } from "fs";
10683
11358
  var migrateCommand = new Command14("migrate").description("Migrate a daemon config into a .beastmode/ factory").option("--config <path>", "Path to beastmode.daemon.json").option("--dry-run", "Show what would be created without writing files").action(async (opts) => {
10684
11359
  try {
10685
11360
  await runMigrate(opts);
@@ -10699,7 +11374,7 @@ async function runMigrate(opts) {
10699
11374
  }
10700
11375
  header("BeastMode Migrate");
10701
11376
  info(`Reading daemon config from: ${configPath}`);
10702
- const configContent = readFileSync24(configPath, "utf-8");
11377
+ const configContent = readFileSync25(configPath, "utf-8");
10703
11378
  const daemonConfig = parseDaemonConfig(configContent);
10704
11379
  const runsDir = join26(cwd, "runs");
10705
11380
  let runDirs = [];
@@ -10717,7 +11392,7 @@ async function runMigrate(opts) {
10717
11392
  const cpPath = join26(runsDir, dir, "checkpoint.json");
10718
11393
  if (existsSync28(cpPath)) {
10719
11394
  try {
10720
- const cp = JSON.parse(readFileSync24(cpPath, "utf-8"));
11395
+ const cp = JSON.parse(readFileSync25(cpPath, "utf-8"));
10721
11396
  checkpoints.set(dir, cp);
10722
11397
  } catch {
10723
11398
  }
@@ -10726,8 +11401,8 @@ async function runMigrate(opts) {
10726
11401
  }
10727
11402
  let worktreeOutput = "";
10728
11403
  try {
10729
- const { execSync: execSync10 } = await import("child_process");
10730
- worktreeOutput = execSync10("git worktree list", {
11404
+ const { execSync: execSync11 } = await import("child_process");
11405
+ worktreeOutput = execSync11("git worktree list", {
10731
11406
  cwd,
10732
11407
  encoding: "utf-8",
10733
11408
  timeout: 5e3
@@ -10813,12 +11488,13 @@ async function runMigrate(opts) {
10813
11488
 
10814
11489
  // src/cli/commands/run.ts
10815
11490
  init_display();
11491
+ init_board();
11492
+ init_bridge();
11493
+ init_schemas();
10816
11494
  import { Command as Command15 } from "commander";
10817
11495
  import { join as join27 } from "path";
10818
- import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync22, mkdirSync as mkdirSync17 } from "fs";
11496
+ import { existsSync as existsSync29, readFileSync as readFileSync26, writeFileSync as writeFileSync22, mkdirSync as mkdirSync17 } from "fs";
10819
11497
  import { randomUUID as randomUUID2 } from "crypto";
10820
- init_bridge();
10821
- init_schemas();
10822
11498
  var runCommand = new Command15("run").description("Run a single pipeline task").argument("[project]", "Project name (defaults to first project)").option("--task <description>", "Task description").action(async (project, opts) => {
10823
11499
  try {
10824
11500
  await runPipeline(project, opts);
@@ -10844,7 +11520,7 @@ async function runPipeline(projectName, opts) {
10844
11520
  throw new Error("Factory config not found. Run 'beastmode init' first.");
10845
11521
  }
10846
11522
  const factoryConfig = FactoryConfigSchema.parse(
10847
- JSON.parse(readFileSync25(configPath, "utf-8"))
11523
+ JSON.parse(readFileSync26(configPath, "utf-8"))
10848
11524
  );
10849
11525
  let projectConfig = null;
10850
11526
  const projectsDir = join27(bmDir, "projects");
@@ -10861,11 +11537,11 @@ async function runPipeline(projectName, opts) {
10861
11537
  throw new Error(`Project not found: ${projectName}`);
10862
11538
  }
10863
11539
  projectConfig = ProjectConfigSchema.parse(
10864
- JSON.parse(readFileSync25(join27(projectsDir, file), "utf-8"))
11540
+ JSON.parse(readFileSync26(join27(projectsDir, file), "utf-8"))
10865
11541
  );
10866
11542
  } else if (projectFiles.length > 0) {
10867
11543
  projectConfig = ProjectConfigSchema.parse(
10868
- JSON.parse(readFileSync25(join27(projectsDir, projectFiles[0]), "utf-8"))
11544
+ JSON.parse(readFileSync26(join27(projectsDir, projectFiles[0]), "utf-8"))
10869
11545
  );
10870
11546
  info(`Using project: ${projectConfig.name}`);
10871
11547
  }
@@ -10874,7 +11550,7 @@ async function runPipeline(projectName, opts) {
10874
11550
  let boardItems = [];
10875
11551
  if (existsSync29(boardPath)) {
10876
11552
  try {
10877
- const raw = JSON.parse(readFileSync25(boardPath, "utf-8"));
11553
+ const raw = JSON.parse(readFileSync26(boardPath, "utf-8"));
10878
11554
  boardItems = Array.isArray(raw.items) ? raw.items : [];
10879
11555
  } catch {
10880
11556
  boardItems = [];
@@ -10899,14 +11575,14 @@ async function runPipeline(projectName, opts) {
10899
11575
  const daemonConfig = generateDaemonConfig(factoryConfig, projectConfig, factoryDir);
10900
11576
  writeFileSync22(daemonConfigPath, JSON.stringify(daemonConfig, null, 2), "utf-8");
10901
11577
  info(`Generated daemon config at: ${daemonConfigPath}`);
10902
- const { execSync: execSync10 } = await import("child_process");
11578
+ const { execSync: execSync11 } = await import("child_process");
10903
11579
  let pythonAvailable = false;
10904
11580
  try {
10905
- execSync10("python --version", { timeout: 5e3, encoding: "utf-8" });
11581
+ execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
10906
11582
  pythonAvailable = true;
10907
11583
  } catch {
10908
11584
  try {
10909
- execSync10("python3 --version", { timeout: 5e3, encoding: "utf-8" });
11585
+ execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
10910
11586
  pythonAvailable = true;
10911
11587
  } catch {
10912
11588
  }
@@ -10934,8 +11610,8 @@ async function runPipeline(projectName, opts) {
10934
11610
  }
10935
11611
  const cmd = buildDaemonCommand(null, daemonConfigPath);
10936
11612
  info(`Spawning daemon: ${cmd.command} ${cmd.args.join(" ")}`);
10937
- const { spawn } = await import("child_process");
10938
- const child = spawn(cmd.command, cmd.args, {
11613
+ const { spawn: spawn3 } = await import("child_process");
11614
+ const child = spawn3(cmd.command, cmd.args, {
10939
11615
  stdio: "inherit",
10940
11616
  cwd: factoryDir,
10941
11617
  env: {
@@ -10948,7 +11624,7 @@ async function runPipeline(projectName, opts) {
10948
11624
  const startTime = Date.now();
10949
11625
  const pollInterval = setInterval(() => {
10950
11626
  try {
10951
- const board = JSON.parse(readFileSync25(boardPath, "utf-8"));
11627
+ const board = JSON.parse(readFileSync26(boardPath, "utf-8"));
10952
11628
  const items = Array.isArray(board.items) ? board.items : [];
10953
11629
  const taskItem = items.find((i) => i.id === taskId);
10954
11630
  if (taskItem) {
@@ -10990,11 +11666,12 @@ async function runPipeline(projectName, opts) {
10990
11666
 
10991
11667
  // src/cli/commands/daemon-cmd.ts
10992
11668
  init_display();
10993
- import { Command as Command16 } from "commander";
10994
- import { join as join28 } from "path";
10995
- import { existsSync as existsSync30, readFileSync as readFileSync26, writeFileSync as writeFileSync23, mkdirSync as mkdirSync18 } from "fs";
11669
+ init_board();
10996
11670
  init_bridge();
10997
11671
  init_schemas();
11672
+ import { Command as Command16 } from "commander";
11673
+ import { join as join28 } from "path";
11674
+ import { existsSync as existsSync30, readFileSync as readFileSync27, writeFileSync as writeFileSync23, mkdirSync as mkdirSync18 } from "fs";
10998
11675
  var daemonCommand = new Command16("daemon").description("Start the BeastMode daemon via bridge").option("--dry-run", "Generate config but don't start daemon").option(
10999
11676
  "--log-level <level>",
11000
11677
  "Log level (DEBUG, INFO, WARNING, ERROR)",
@@ -11021,7 +11698,7 @@ async function runDaemon(opts) {
11021
11698
  throw new Error("Factory config not found. Run 'beastmode init' first.");
11022
11699
  }
11023
11700
  const factoryConfig = FactoryConfigSchema.parse(
11024
- JSON.parse(readFileSync26(configPath, "utf-8"))
11701
+ JSON.parse(readFileSync27(configPath, "utf-8"))
11025
11702
  );
11026
11703
  let projectConfig = null;
11027
11704
  const projectsDir = join28(bmDir, "projects");
@@ -11032,7 +11709,7 @@ async function runDaemon(opts) {
11032
11709
  );
11033
11710
  if (projectFiles.length > 0) {
11034
11711
  projectConfig = ProjectConfigSchema.parse(
11035
- JSON.parse(readFileSync26(join28(projectsDir, projectFiles[0]), "utf-8"))
11712
+ JSON.parse(readFileSync27(join28(projectsDir, projectFiles[0]), "utf-8"))
11036
11713
  );
11037
11714
  info(`Using project: ${projectConfig.name}`);
11038
11715
  }
@@ -11058,16 +11735,16 @@ async function runDaemon(opts) {
11058
11735
  console.log(JSON.stringify(daemonConfig, null, 2));
11059
11736
  return;
11060
11737
  }
11061
- const { execSync: execSync10 } = await import("child_process");
11738
+ const { execSync: execSync11 } = await import("child_process");
11062
11739
  let pythonCmd = "python";
11063
11740
  let pythonAvailable = false;
11064
11741
  try {
11065
- execSync10("python --version", { timeout: 5e3, encoding: "utf-8" });
11742
+ execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
11066
11743
  pythonAvailable = true;
11067
11744
  pythonCmd = "python";
11068
11745
  } catch {
11069
11746
  try {
11070
- execSync10("python3 --version", { timeout: 5e3, encoding: "utf-8" });
11747
+ execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
11071
11748
  pythonAvailable = true;
11072
11749
  pythonCmd = "python3";
11073
11750
  } catch {
@@ -11085,8 +11762,8 @@ async function runDaemon(opts) {
11085
11762
  info(`Starting daemon: ${pythonCmd} ${cmd.args.join(" ")}`);
11086
11763
  console.log();
11087
11764
  const pidFile = join28(bmDir, "daemon.pid");
11088
- const { spawn } = await import("child_process");
11089
- const child = spawn(pythonCmd, cmd.args, {
11765
+ const { spawn: spawn3 } = await import("child_process");
11766
+ const child = spawn3(pythonCmd, cmd.args, {
11090
11767
  stdio: "inherit",
11091
11768
  cwd: factoryDir,
11092
11769
  env: {
@@ -11135,7 +11812,7 @@ var mcpCommand = new Command17("mcp").description("Start the BeastMode MCP serve
11135
11812
  init_display();
11136
11813
  import { Command as Command18 } from "commander";
11137
11814
  import { resolve as resolve19, join as join30 } from "path";
11138
- import { existsSync as existsSync32, writeFileSync as writeFileSync25, readFileSync as readFileSync28 } from "fs";
11815
+ import { existsSync as existsSync32, writeFileSync as writeFileSync25, readFileSync as readFileSync29 } from "fs";
11139
11816
  import { execSync as execSync9 } from "child_process";
11140
11817
  import { randomBytes } from "crypto";
11141
11818
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -11225,8 +11902,8 @@ async function runDeploy(opts) {
11225
11902
  const host = opts.host;
11226
11903
  const dotEnv = join30(factoryDir, ".env");
11227
11904
  const secretsEnv = join30(bmDir, "secrets.env.local");
11228
- const envContent = existsSync32(dotEnv) ? readFileSync28(dotEnv, "utf-8") : "";
11229
- const secretsContent = existsSync32(secretsEnv) ? readFileSync28(secretsEnv, "utf-8") : "";
11905
+ const envContent = existsSync32(dotEnv) ? readFileSync29(dotEnv, "utf-8") : "";
11906
+ const secretsContent = existsSync32(secretsEnv) ? readFileSync29(secretsEnv, "utf-8") : "";
11230
11907
  const hasPassword = envContent.includes("BEASTMODE_UI_PASSWORD=") && !envContent.includes("BEASTMODE_UI_PASSWORD=\n") || secretsContent.includes("BEASTMODE_UI_PASSWORD=") && !secretsContent.includes("BEASTMODE_UI_PASSWORD=\n") || !!process.env.BEASTMODE_UI_PASSWORD;
11231
11908
  if (!hasPassword && opts.host === "0.0.0.0") {
11232
11909
  const generated = randomBytes(18).toString("base64url");
@@ -11235,7 +11912,7 @@ async function runDeploy(opts) {
11235
11912
  # Auto-generated board UI password (deploy)
11236
11913
  BEASTMODE_UI_PASSWORD=${generated}
11237
11914
  `;
11238
- writeFileSync25(target, (existsSync32(target) ? readFileSync28(target, "utf-8") : "") + append, "utf-8");
11915
+ writeFileSync25(target, (existsSync32(target) ? readFileSync29(target, "utf-8") : "") + append, "utf-8");
11239
11916
  info(`Board UI password auto-generated and saved to ${target}`);
11240
11917
  success(`Password: ${generated}`);
11241
11918
  info("Save this password \u2014 you'll need it to access the board UI.");
@@ -11561,7 +12238,63 @@ init_sync_claude_creds();
11561
12238
 
11562
12239
  // src/cli/commands/up.ts
11563
12240
  import { Command as Command19 } from "commander";
12241
+ import { spawnSync as spawnSync4 } from "child_process";
11564
12242
  init_display();
12243
+ var LAUNCH_AGENT_LABEL3 = "com.develeap.beastmode.claude-creds";
12244
+ var SYSTEMD_CREDS_TIMER2 = "beastmode-claude-creds.timer";
12245
+ async function ensureCredWatcher(cwd) {
12246
+ const plat = process.platform;
12247
+ if (plat !== "darwin" && plat !== "linux") return;
12248
+ if (plat === "darwin") {
12249
+ const uid = process.getuid?.();
12250
+ const probe = spawnSync4(
12251
+ "launchctl",
12252
+ ["print", `gui/${uid}/${LAUNCH_AGENT_LABEL3}`],
12253
+ { stdio: "pipe", encoding: "utf-8" }
12254
+ );
12255
+ if (probe.status === 0) return;
12256
+ } else {
12257
+ const probe = spawnSync4(
12258
+ "systemctl",
12259
+ ["--user", "is-active", SYSTEMD_CREDS_TIMER2],
12260
+ { stdio: "pipe", encoding: "utf-8" }
12261
+ );
12262
+ if ((probe.stdout || "").trim() === "active") return;
12263
+ }
12264
+ info("Installing credential sync watcher for Docker daemon...");
12265
+ try {
12266
+ if (plat === "darwin") {
12267
+ const { syncClaudeCredsOnce: syncClaudeCredsOnce2, installAgent: installAgent2 } = await Promise.resolve().then(() => (init_sync_claude_creds(), sync_claude_creds_exports));
12268
+ const sync = syncClaudeCredsOnce2();
12269
+ if ("error" in sync) {
12270
+ warn(`Credential sync skipped: ${sync.error}`);
12271
+ info(
12272
+ " Run `beastmode sync-claude-creds --install` after `claude login`."
12273
+ );
12274
+ return;
12275
+ }
12276
+ success(`Credentials synced: ${sync.path}`);
12277
+ installAgent2(1800, { throwOnError: true, factoryDir: cwd });
12278
+ } else {
12279
+ const { syncClaudeCredsLinux: syncClaudeCredsLinux2, installAgentLinux: installAgentLinux2 } = await Promise.resolve().then(() => (init_sync_claude_creds(), sync_claude_creds_exports));
12280
+ const sync = syncClaudeCredsLinux2();
12281
+ if ("error" in sync) {
12282
+ warn(`Credential sync skipped: ${sync.error}`);
12283
+ info(
12284
+ " Run `beastmode sync-claude-creds --install` after `claude login`."
12285
+ );
12286
+ return;
12287
+ }
12288
+ success(`Credentials valid: ${sync.path}`);
12289
+ installAgentLinux2(1800, { throwOnError: true });
12290
+ }
12291
+ } catch (e) {
12292
+ warn(
12293
+ `Credential watcher install failed: ${e instanceof Error ? e.message : String(e)}`
12294
+ );
12295
+ info(" Service startup continues \u2014 re-try with `beastmode sync-claude-creds --install`.");
12296
+ }
12297
+ }
11565
12298
  async function runUp(opts) {
11566
12299
  const cwd = opts.cwd ?? process.cwd();
11567
12300
  requireComposeFile(cwd);
@@ -11569,6 +12302,7 @@ async function runUp(opts) {
11569
12302
  runCompose(["pull"], { cwd, inherit: true });
11570
12303
  }
11571
12304
  runCompose(["up", "-d"], { cwd, inherit: true });
12305
+ await ensureCredWatcher(cwd);
11572
12306
  }
11573
12307
  var upCommand = new Command19("up").description("Start BeastMode services").option("--pull", "Pull latest images before starting").action(async (opts) => {
11574
12308
  try {
@@ -11652,13 +12386,13 @@ var logsCommand = new Command21("logs").description("Stream BeastMode service lo
11652
12386
 
11653
12387
  // src/cli/commands/update.ts
11654
12388
  import { Command as Command22 } from "commander";
11655
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync26 } from "fs";
12389
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync26 } from "fs";
11656
12390
  init_display();
11657
12391
  async function runUpdate(opts) {
11658
12392
  const cwd = opts.cwd ?? process.cwd();
11659
12393
  const composePath = requireComposeFile(cwd);
11660
12394
  if (opts.tag) {
11661
- let content = readFileSync29(composePath, "utf-8");
12395
+ let content = readFileSync30(composePath, "utf-8");
11662
12396
  const tagPattern = new RegExp(
11663
12397
  `(${GHCR_IMAGE_PREFIX.replace(/[/]/g, "\\/")}\\/(?:board|daemon|ui)):([\\w.\\-]+)`,
11664
12398
  "g"
@@ -11685,9 +12419,340 @@ var updateCommand = new Command22("update").description("Pull latest BeastMode i
11685
12419
  }
11686
12420
  });
11687
12421
 
12422
+ // src/cli/commands/runner-cmd.ts
12423
+ init_display();
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
12672
+ var runnerCommand = new Command23("runner").description("Manage self-hosted GitHub Actions runners");
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);
12739
+ });
12740
+ runnerCommand.command("status").description("Show runner status (container + GitHub registration)").action(() => {
12741
+ warn("runner status is not implemented yet (Story 3)");
12742
+ });
12743
+ runnerCommand.command("remove").description("Remove the runner (container + GitHub deregistration)").action(() => {
12744
+ warn("runner remove is not implemented yet (Story 3)");
12745
+ });
12746
+ runnerCommand.command("switch-workflows").description("Switch workflow runs-on to self-hosted runner").action(() => {
12747
+ warn("runner switch-workflows is not implemented yet (Story 4)");
12748
+ });
12749
+ runnerCommand.command("restore-workflows").description("Restore workflows to original runs-on values").action(() => {
12750
+ warn("runner restore-workflows is not implemented yet (Story 4)");
12751
+ });
12752
+
11688
12753
  // src/index.ts
11689
12754
  init_version();
11690
- var program = new Command23();
12755
+ var program = new Command24();
11691
12756
  program.name("beastmode").description("BeastMode Dark Factory \u2014 turn intent into verified software").version(ENGINE_VERSION);
11692
12757
  program.addCommand(initCommand);
11693
12758
  program.addCommand(boardCommand);
@@ -11711,5 +12776,6 @@ program.addCommand(upCommand);
11711
12776
  program.addCommand(downCommand);
11712
12777
  program.addCommand(logsCommand);
11713
12778
  program.addCommand(updateCommand);
12779
+ program.addCommand(runnerCommand);
11714
12780
  program.parse();
11715
12781
  //# sourceMappingURL=index.js.map