@beastmode-develeap/beastmode 0.1.146 → 0.1.148
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 +504 -61
- package/dist/index.js.map +1 -1
- package/dist/web/board.html +1 -1
- package/dist/web/build-commit.txt +1 -1
- package/dist/web/build-stamp.txt +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3366,7 +3366,7 @@ async function createEpicsOnBoard(factoryDir, productName, boardUrl = "http://12
|
|
|
3366
3366
|
});
|
|
3367
3367
|
let itemId = "";
|
|
3368
3368
|
try {
|
|
3369
|
-
const itemResult = await new Promise((
|
|
3369
|
+
const itemResult = await new Promise((resolve21, reject) => {
|
|
3370
3370
|
const url = new URL("/api/items", boardUrl);
|
|
3371
3371
|
const req = http4.default.request(url, {
|
|
3372
3372
|
method: "POST",
|
|
@@ -3378,7 +3378,7 @@ async function createEpicsOnBoard(factoryDir, productName, boardUrl = "http://12
|
|
|
3378
3378
|
});
|
|
3379
3379
|
res.on("end", () => {
|
|
3380
3380
|
try {
|
|
3381
|
-
|
|
3381
|
+
resolve21(JSON.parse(data));
|
|
3382
3382
|
} catch {
|
|
3383
3383
|
reject(new Error(data));
|
|
3384
3384
|
}
|
|
@@ -3394,13 +3394,13 @@ async function createEpicsOnBoard(factoryDir, productName, boardUrl = "http://12
|
|
|
3394
3394
|
if (itemId) {
|
|
3395
3395
|
createdIds.push(itemId);
|
|
3396
3396
|
const updatePayload = JSON.stringify({ body: detailContent, creator_name: "beastmode-inception" });
|
|
3397
|
-
await new Promise((
|
|
3397
|
+
await new Promise((resolve21) => {
|
|
3398
3398
|
const url = new URL(`/api/items/${itemId}/updates`, boardUrl);
|
|
3399
3399
|
const req = http4.default.request(url, {
|
|
3400
3400
|
method: "POST",
|
|
3401
3401
|
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(updatePayload).toString() }
|
|
3402
|
-
}, () =>
|
|
3403
|
-
req.on("error", () =>
|
|
3402
|
+
}, () => resolve21());
|
|
3403
|
+
req.on("error", () => resolve21());
|
|
3404
3404
|
req.end(updatePayload);
|
|
3405
3405
|
});
|
|
3406
3406
|
}
|
|
@@ -3620,19 +3620,19 @@ function getRecentTokenUsage() {
|
|
|
3620
3620
|
return recentTokenUsage.slice();
|
|
3621
3621
|
}
|
|
3622
3622
|
function boardGet(path, boardUrl) {
|
|
3623
|
-
return new Promise((
|
|
3623
|
+
return new Promise((resolve21) => {
|
|
3624
3624
|
const url = new URL(path, boardUrl);
|
|
3625
3625
|
const req = http.request(url, { method: "GET", timeout: 5e3 }, (res) => {
|
|
3626
3626
|
let data = "";
|
|
3627
3627
|
res.on("data", (chunk) => {
|
|
3628
3628
|
data += chunk.toString();
|
|
3629
3629
|
});
|
|
3630
|
-
res.on("end", () =>
|
|
3630
|
+
res.on("end", () => resolve21(data));
|
|
3631
3631
|
});
|
|
3632
|
-
req.on("error", (err) =>
|
|
3632
|
+
req.on("error", (err) => resolve21(`Board API error: ${err.message}`));
|
|
3633
3633
|
req.on("timeout", () => {
|
|
3634
3634
|
req.destroy();
|
|
3635
|
-
|
|
3635
|
+
resolve21("Board API timeout");
|
|
3636
3636
|
});
|
|
3637
3637
|
req.end();
|
|
3638
3638
|
});
|
|
@@ -4343,7 +4343,7 @@ You are currently scoped to project "${scope}". Focus your answers on this proje
|
|
|
4343
4343
|
async function runViaCli(session, content, scope = "factory") {
|
|
4344
4344
|
session.busy = true;
|
|
4345
4345
|
try {
|
|
4346
|
-
const { spawn } = await import("child_process");
|
|
4346
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
4347
4347
|
let boardContext = "";
|
|
4348
4348
|
try {
|
|
4349
4349
|
const boardUrl = getBoardUrl(session.factoryPath);
|
|
@@ -4435,7 +4435,7 @@ Respond concisely. Continue the conversation naturally.`;
|
|
|
4435
4435
|
spawnCmd = "claude";
|
|
4436
4436
|
spawnArgs = claudeArgs;
|
|
4437
4437
|
}
|
|
4438
|
-
const child =
|
|
4438
|
+
const child = spawn3(spawnCmd, spawnArgs, {
|
|
4439
4439
|
cwd: session.factoryPath,
|
|
4440
4440
|
env: {
|
|
4441
4441
|
...process.env,
|
|
@@ -4582,7 +4582,7 @@ function getBoardUrl2(factoryDir) {
|
|
|
4582
4582
|
return "http://127.0.0.1:8080";
|
|
4583
4583
|
}
|
|
4584
4584
|
function proxyToBoard(boardUrl, method, path, body, query) {
|
|
4585
|
-
return new Promise((
|
|
4585
|
+
return new Promise((resolve21, reject) => {
|
|
4586
4586
|
const url = new URL(path, boardUrl);
|
|
4587
4587
|
if (query) {
|
|
4588
4588
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -4614,7 +4614,7 @@ function proxyToBoard(boardUrl, method, path, body, query) {
|
|
|
4614
4614
|
if (statusCode >= 400) {
|
|
4615
4615
|
reject(new HttpError(statusCode, parsed));
|
|
4616
4616
|
} else {
|
|
4617
|
-
|
|
4617
|
+
resolve21(parsed);
|
|
4618
4618
|
}
|
|
4619
4619
|
});
|
|
4620
4620
|
});
|
|
@@ -4624,7 +4624,7 @@ function proxyToBoard(boardUrl, method, path, body, query) {
|
|
|
4624
4624
|
});
|
|
4625
4625
|
}
|
|
4626
4626
|
function proxyBinaryToBoard(boardUrl, path, query) {
|
|
4627
|
-
return new Promise((
|
|
4627
|
+
return new Promise((resolve21, reject) => {
|
|
4628
4628
|
const url = new URL(path, boardUrl);
|
|
4629
4629
|
if (query) {
|
|
4630
4630
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -4653,7 +4653,7 @@ function proxyBinaryToBoard(boardUrl, path, query) {
|
|
|
4653
4653
|
const cd = res.headers["content-disposition"] || "";
|
|
4654
4654
|
const match = /filename="([^"]+)"/.exec(cd);
|
|
4655
4655
|
const filename = match ? match[1] : void 0;
|
|
4656
|
-
|
|
4656
|
+
resolve21(new BinaryResponse(body, contentType, filename));
|
|
4657
4657
|
});
|
|
4658
4658
|
}
|
|
4659
4659
|
);
|
|
@@ -4665,7 +4665,7 @@ function proxyBinaryToBoard(boardUrl, path, query) {
|
|
|
4665
4665
|
});
|
|
4666
4666
|
}
|
|
4667
4667
|
function transparentProxyToBoard(boardUrl, method, path, body, query) {
|
|
4668
|
-
return new Promise((
|
|
4668
|
+
return new Promise((resolve21, reject) => {
|
|
4669
4669
|
const url = new URL(path, boardUrl);
|
|
4670
4670
|
if (query) {
|
|
4671
4671
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -4688,7 +4688,7 @@ function transparentProxyToBoard(boardUrl, method, path, body, query) {
|
|
|
4688
4688
|
const chunks = [];
|
|
4689
4689
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
4690
4690
|
res.on("end", () => {
|
|
4691
|
-
|
|
4691
|
+
resolve21({
|
|
4692
4692
|
statusCode: res.statusCode || 502,
|
|
4693
4693
|
contentType: res.headers["content-type"] || "application/json",
|
|
4694
4694
|
body: Buffer.concat(chunks)
|
|
@@ -6420,17 +6420,17 @@ function resolveStaticDir() {
|
|
|
6420
6420
|
);
|
|
6421
6421
|
}
|
|
6422
6422
|
function parseBody(req) {
|
|
6423
|
-
return new Promise((
|
|
6423
|
+
return new Promise((resolve21, reject) => {
|
|
6424
6424
|
const chunks = [];
|
|
6425
6425
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
6426
6426
|
req.on("end", () => {
|
|
6427
6427
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
6428
6428
|
if (!raw) {
|
|
6429
|
-
|
|
6429
|
+
resolve21({});
|
|
6430
6430
|
return;
|
|
6431
6431
|
}
|
|
6432
6432
|
try {
|
|
6433
|
-
|
|
6433
|
+
resolve21(JSON.parse(raw));
|
|
6434
6434
|
} catch {
|
|
6435
6435
|
reject(new Error("Invalid JSON body"));
|
|
6436
6436
|
}
|
|
@@ -6492,11 +6492,11 @@ function clearSessionCookie(res) {
|
|
|
6492
6492
|
}
|
|
6493
6493
|
async function findAvailablePort(start) {
|
|
6494
6494
|
for (let port = start; port < start + 100; port++) {
|
|
6495
|
-
const available = await new Promise((
|
|
6495
|
+
const available = await new Promise((resolve21) => {
|
|
6496
6496
|
const testServer = createServer();
|
|
6497
|
-
testServer.once("error", () =>
|
|
6497
|
+
testServer.once("error", () => resolve21(false));
|
|
6498
6498
|
testServer.listen(port, "127.0.0.1", () => {
|
|
6499
|
-
testServer.close(() =>
|
|
6499
|
+
testServer.close(() => resolve21(true));
|
|
6500
6500
|
});
|
|
6501
6501
|
});
|
|
6502
6502
|
if (available) return port;
|
|
@@ -6817,23 +6817,23 @@ async function startServer(options = {}) {
|
|
|
6817
6817
|
if (chatManager) {
|
|
6818
6818
|
chatManager.shutdown();
|
|
6819
6819
|
}
|
|
6820
|
-
return new Promise((
|
|
6820
|
+
return new Promise((resolve21) => {
|
|
6821
6821
|
server.close(() => {
|
|
6822
6822
|
if (shutdownResolve) shutdownResolve();
|
|
6823
|
-
|
|
6823
|
+
resolve21();
|
|
6824
6824
|
});
|
|
6825
6825
|
setTimeout(() => {
|
|
6826
6826
|
if (shutdownResolve) shutdownResolve();
|
|
6827
|
-
|
|
6827
|
+
resolve21();
|
|
6828
6828
|
}, 2e3);
|
|
6829
6829
|
});
|
|
6830
6830
|
}
|
|
6831
|
-
return new Promise((
|
|
6831
|
+
return new Promise((resolve21, reject) => {
|
|
6832
6832
|
server.once("error", reject);
|
|
6833
6833
|
const host = options.host || "127.0.0.1";
|
|
6834
6834
|
server.listen(port, host, () => {
|
|
6835
6835
|
resetInactivityTimer();
|
|
6836
|
-
|
|
6836
|
+
resolve21({
|
|
6837
6837
|
server,
|
|
6838
6838
|
port,
|
|
6839
6839
|
url: `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`,
|
|
@@ -8425,12 +8425,12 @@ async function runInit(name, opts) {
|
|
|
8425
8425
|
} catch {
|
|
8426
8426
|
info(`Open ${uiServer.url} in your browser to continue.`);
|
|
8427
8427
|
}
|
|
8428
|
-
await new Promise((
|
|
8429
|
-
uiServer.server.on("close",
|
|
8428
|
+
await new Promise((resolve21) => {
|
|
8429
|
+
uiServer.server.on("close", resolve21);
|
|
8430
8430
|
process.on("SIGINT", async () => {
|
|
8431
8431
|
info("\nShutting down wizard server...");
|
|
8432
8432
|
await uiServer.shutdown();
|
|
8433
|
-
|
|
8433
|
+
resolve21();
|
|
8434
8434
|
});
|
|
8435
8435
|
});
|
|
8436
8436
|
return;
|
|
@@ -8447,8 +8447,8 @@ async function runInit(name, opts) {
|
|
|
8447
8447
|
throw new Error(`Factory already exists at ./${factoryName}. Use 'beastmode config' to modify.`);
|
|
8448
8448
|
}
|
|
8449
8449
|
if (opts.from) {
|
|
8450
|
-
const { readFileSync:
|
|
8451
|
-
const templateContent =
|
|
8450
|
+
const { readFileSync: readFileSync32 } = await import("fs");
|
|
8451
|
+
const templateContent = readFileSync32(resolve6(opts.from), "utf-8");
|
|
8452
8452
|
const { parseTemplate: parseTemplate2 } = await Promise.resolve().then(() => (init_template_importer(), template_importer_exports));
|
|
8453
8453
|
const template = parseTemplate2(templateContent);
|
|
8454
8454
|
info(`Importing from template: ${opts.from}`);
|
|
@@ -9660,16 +9660,16 @@ function tryExec(cmd, timeout = 8e3) {
|
|
|
9660
9660
|
}
|
|
9661
9661
|
}
|
|
9662
9662
|
function httpGet(url, timeoutMs = 5e3) {
|
|
9663
|
-
return new Promise((
|
|
9663
|
+
return new Promise((resolve21) => {
|
|
9664
9664
|
const lib = url.startsWith("https") ? https : http3;
|
|
9665
9665
|
const req = lib.get(url, { timeout: timeoutMs }, (res) => {
|
|
9666
9666
|
res.resume();
|
|
9667
|
-
|
|
9667
|
+
resolve21(res.statusCode ?? null);
|
|
9668
9668
|
});
|
|
9669
|
-
req.on("error", () =>
|
|
9669
|
+
req.on("error", () => resolve21(null));
|
|
9670
9670
|
req.on("timeout", () => {
|
|
9671
9671
|
req.destroy();
|
|
9672
|
-
|
|
9672
|
+
resolve21(null);
|
|
9673
9673
|
});
|
|
9674
9674
|
});
|
|
9675
9675
|
}
|
|
@@ -9717,7 +9717,7 @@ async function checkClaudeAuth(key) {
|
|
|
9717
9717
|
max_tokens: 1,
|
|
9718
9718
|
messages: [{ role: "user", content: "hi" }]
|
|
9719
9719
|
});
|
|
9720
|
-
const statusCode = await new Promise((
|
|
9720
|
+
const statusCode = await new Promise((resolve21) => {
|
|
9721
9721
|
const req = https.request(
|
|
9722
9722
|
{
|
|
9723
9723
|
hostname: "api.anthropic.com",
|
|
@@ -9733,13 +9733,13 @@ async function checkClaudeAuth(key) {
|
|
|
9733
9733
|
},
|
|
9734
9734
|
(res) => {
|
|
9735
9735
|
res.resume();
|
|
9736
|
-
|
|
9736
|
+
resolve21(res.statusCode ?? null);
|
|
9737
9737
|
}
|
|
9738
9738
|
);
|
|
9739
|
-
req.on("error", () =>
|
|
9739
|
+
req.on("error", () => resolve21(null));
|
|
9740
9740
|
req.on("timeout", () => {
|
|
9741
9741
|
req.destroy();
|
|
9742
|
-
|
|
9742
|
+
resolve21(null);
|
|
9743
9743
|
});
|
|
9744
9744
|
req.write(payload);
|
|
9745
9745
|
req.end();
|
|
@@ -11401,8 +11401,8 @@ async function runMigrate(opts) {
|
|
|
11401
11401
|
}
|
|
11402
11402
|
let worktreeOutput = "";
|
|
11403
11403
|
try {
|
|
11404
|
-
const { execSync:
|
|
11405
|
-
worktreeOutput =
|
|
11404
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
11405
|
+
worktreeOutput = execSync11("git worktree list", {
|
|
11406
11406
|
cwd,
|
|
11407
11407
|
encoding: "utf-8",
|
|
11408
11408
|
timeout: 5e3
|
|
@@ -11575,14 +11575,14 @@ async function runPipeline(projectName, opts) {
|
|
|
11575
11575
|
const daemonConfig = generateDaemonConfig(factoryConfig, projectConfig, factoryDir);
|
|
11576
11576
|
writeFileSync22(daemonConfigPath, JSON.stringify(daemonConfig, null, 2), "utf-8");
|
|
11577
11577
|
info(`Generated daemon config at: ${daemonConfigPath}`);
|
|
11578
|
-
const { execSync:
|
|
11578
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
11579
11579
|
let pythonAvailable = false;
|
|
11580
11580
|
try {
|
|
11581
|
-
|
|
11581
|
+
execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11582
11582
|
pythonAvailable = true;
|
|
11583
11583
|
} catch {
|
|
11584
11584
|
try {
|
|
11585
|
-
|
|
11585
|
+
execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11586
11586
|
pythonAvailable = true;
|
|
11587
11587
|
} catch {
|
|
11588
11588
|
}
|
|
@@ -11610,8 +11610,8 @@ async function runPipeline(projectName, opts) {
|
|
|
11610
11610
|
}
|
|
11611
11611
|
const cmd = buildDaemonCommand(null, daemonConfigPath);
|
|
11612
11612
|
info(`Spawning daemon: ${cmd.command} ${cmd.args.join(" ")}`);
|
|
11613
|
-
const { spawn } = await import("child_process");
|
|
11614
|
-
const child =
|
|
11613
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
11614
|
+
const child = spawn3(cmd.command, cmd.args, {
|
|
11615
11615
|
stdio: "inherit",
|
|
11616
11616
|
cwd: factoryDir,
|
|
11617
11617
|
env: {
|
|
@@ -11735,16 +11735,16 @@ async function runDaemon(opts) {
|
|
|
11735
11735
|
console.log(JSON.stringify(daemonConfig, null, 2));
|
|
11736
11736
|
return;
|
|
11737
11737
|
}
|
|
11738
|
-
const { execSync:
|
|
11738
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
11739
11739
|
let pythonCmd = "python";
|
|
11740
11740
|
let pythonAvailable = false;
|
|
11741
11741
|
try {
|
|
11742
|
-
|
|
11742
|
+
execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11743
11743
|
pythonAvailable = true;
|
|
11744
11744
|
pythonCmd = "python";
|
|
11745
11745
|
} catch {
|
|
11746
11746
|
try {
|
|
11747
|
-
|
|
11747
|
+
execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11748
11748
|
pythonAvailable = true;
|
|
11749
11749
|
pythonCmd = "python3";
|
|
11750
11750
|
} catch {
|
|
@@ -11762,8 +11762,8 @@ async function runDaemon(opts) {
|
|
|
11762
11762
|
info(`Starting daemon: ${pythonCmd} ${cmd.args.join(" ")}`);
|
|
11763
11763
|
console.log();
|
|
11764
11764
|
const pidFile = join28(bmDir, "daemon.pid");
|
|
11765
|
-
const { spawn } = await import("child_process");
|
|
11766
|
-
const child =
|
|
11765
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
11766
|
+
const child = spawn3(pythonCmd, cmd.args, {
|
|
11767
11767
|
stdio: "inherit",
|
|
11768
11768
|
cwd: factoryDir,
|
|
11769
11769
|
env: {
|
|
@@ -11783,8 +11783,8 @@ async function runDaemon(opts) {
|
|
|
11783
11783
|
const exitCode = await new Promise((resolvePromise) => {
|
|
11784
11784
|
child.on("exit", (code) => {
|
|
11785
11785
|
try {
|
|
11786
|
-
const { unlinkSync:
|
|
11787
|
-
|
|
11786
|
+
const { unlinkSync: unlinkSync6 } = __require("fs");
|
|
11787
|
+
unlinkSync6(pidFile);
|
|
11788
11788
|
} catch {
|
|
11789
11789
|
}
|
|
11790
11790
|
resolvePromise(code ?? 1);
|
|
@@ -12422,9 +12422,422 @@ var updateCommand = new Command22("update").description("Pull latest BeastMode i
|
|
|
12422
12422
|
// src/cli/commands/runner-cmd.ts
|
|
12423
12423
|
init_display();
|
|
12424
12424
|
import { Command as Command23 } from "commander";
|
|
12425
|
+
import { spawn as spawn2 } from "child_process";
|
|
12426
|
+
import { resolve as resolve20 } from "path";
|
|
12427
|
+
|
|
12428
|
+
// src/cli/github-runners.ts
|
|
12429
|
+
var GitHubRunnerApiError = class extends Error {
|
|
12430
|
+
status;
|
|
12431
|
+
endpoint;
|
|
12432
|
+
constructor(status, endpoint, message) {
|
|
12433
|
+
super(message);
|
|
12434
|
+
this.name = "GitHubRunnerApiError";
|
|
12435
|
+
this.status = status;
|
|
12436
|
+
this.endpoint = endpoint;
|
|
12437
|
+
}
|
|
12438
|
+
};
|
|
12439
|
+
var GITHUB_API_BASE = "https://api.github.com";
|
|
12440
|
+
function buildHeaders(token) {
|
|
12441
|
+
return {
|
|
12442
|
+
Authorization: `token ${token}`,
|
|
12443
|
+
Accept: "application/vnd.github.v3+json",
|
|
12444
|
+
"User-Agent": "beastmode-cli"
|
|
12445
|
+
};
|
|
12446
|
+
}
|
|
12447
|
+
function runnersUrl(owner, repo) {
|
|
12448
|
+
return `${GITHUB_API_BASE}/repos/${owner}/${repo}/actions/runners`;
|
|
12449
|
+
}
|
|
12450
|
+
async function githubFetch(url, token, method = "GET", body) {
|
|
12451
|
+
const resp = await fetch(url, {
|
|
12452
|
+
method,
|
|
12453
|
+
headers: buildHeaders(token),
|
|
12454
|
+
body: body ? JSON.stringify(body) : void 0
|
|
12455
|
+
});
|
|
12456
|
+
if (!resp.ok) {
|
|
12457
|
+
const text = await resp.text().catch(() => "");
|
|
12458
|
+
const endpoint = url.replace(GITHUB_API_BASE, "");
|
|
12459
|
+
throw new GitHubRunnerApiError(
|
|
12460
|
+
resp.status,
|
|
12461
|
+
endpoint,
|
|
12462
|
+
`GitHub API ${method} ${endpoint} \u2192 ${resp.status}: ${text}`
|
|
12463
|
+
);
|
|
12464
|
+
}
|
|
12465
|
+
if (resp.status === 204) return void 0;
|
|
12466
|
+
return await resp.json();
|
|
12467
|
+
}
|
|
12468
|
+
async function createRegistrationToken(config) {
|
|
12469
|
+
const url = `${runnersUrl(config.owner, config.repo)}/registration-token`;
|
|
12470
|
+
return githubFetch(url, config.token, "POST");
|
|
12471
|
+
}
|
|
12472
|
+
async function listRunners(config) {
|
|
12473
|
+
const url = runnersUrl(config.owner, config.repo);
|
|
12474
|
+
return githubFetch(url, config.token);
|
|
12475
|
+
}
|
|
12476
|
+
function resolveGitHubConfig() {
|
|
12477
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
12478
|
+
if (!token) {
|
|
12479
|
+
throw new Error(
|
|
12480
|
+
"GITHUB_TOKEN (or GH_TOKEN) not set. Add it to .env or export it."
|
|
12481
|
+
);
|
|
12482
|
+
}
|
|
12483
|
+
const repo = process.env.PROJECT_REPO;
|
|
12484
|
+
if (!repo || !repo.includes("/")) {
|
|
12485
|
+
throw new Error(
|
|
12486
|
+
"PROJECT_REPO not set or invalid (expected 'owner/repo'). Add it to .env."
|
|
12487
|
+
);
|
|
12488
|
+
}
|
|
12489
|
+
const [owner, repoName] = repo.split("/", 2);
|
|
12490
|
+
return { owner, repo: repoName, token };
|
|
12491
|
+
}
|
|
12492
|
+
|
|
12493
|
+
// src/cli/runner-helpers.ts
|
|
12494
|
+
import { execSync as execSync10, spawn, spawnSync as spawnSync5 } from "child_process";
|
|
12495
|
+
import { promises as fs } from "fs";
|
|
12496
|
+
init_display();
|
|
12497
|
+
function resolveRepoSlug() {
|
|
12498
|
+
let rawUrl;
|
|
12499
|
+
try {
|
|
12500
|
+
rawUrl = execSync10("git remote get-url origin", {
|
|
12501
|
+
encoding: "utf-8",
|
|
12502
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
12503
|
+
}).trim();
|
|
12504
|
+
} catch {
|
|
12505
|
+
throw new Error(
|
|
12506
|
+
"Could not determine repo from git remote. Use --repo owner/repo."
|
|
12507
|
+
);
|
|
12508
|
+
}
|
|
12509
|
+
const match = rawUrl.match(/github\.com[:/](.+?)(?:\.git)?$/) || rawUrl.match(/github\.com\/(.+?)(?:\.git)?$/);
|
|
12510
|
+
if (!match) {
|
|
12511
|
+
throw new Error(
|
|
12512
|
+
`Could not parse GitHub repo from remote URL: ${rawUrl}`
|
|
12513
|
+
);
|
|
12514
|
+
}
|
|
12515
|
+
return match[1];
|
|
12516
|
+
}
|
|
12517
|
+
async function pullImageIfNeeded(image) {
|
|
12518
|
+
const inspect = spawnSync5("docker", ["image", "inspect", image], {
|
|
12519
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
12520
|
+
});
|
|
12521
|
+
if (inspect.error && inspect.error.code === "ENOENT") {
|
|
12522
|
+
throw new Error(
|
|
12523
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12524
|
+
);
|
|
12525
|
+
}
|
|
12526
|
+
if (inspect.status === 0) return;
|
|
12527
|
+
await new Promise((resolve21, reject) => {
|
|
12528
|
+
const child = spawn("docker", ["pull", image], {
|
|
12529
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
12530
|
+
});
|
|
12531
|
+
child.on("error", (err) => {
|
|
12532
|
+
const e = err;
|
|
12533
|
+
if (e.code === "ENOENT") {
|
|
12534
|
+
reject(
|
|
12535
|
+
new Error(
|
|
12536
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12537
|
+
)
|
|
12538
|
+
);
|
|
12539
|
+
} else {
|
|
12540
|
+
reject(err);
|
|
12541
|
+
}
|
|
12542
|
+
});
|
|
12543
|
+
child.on("exit", (code) => {
|
|
12544
|
+
if (code === 0) resolve21();
|
|
12545
|
+
else reject(new Error(`docker pull ${image} exited with code ${code}`));
|
|
12546
|
+
});
|
|
12547
|
+
});
|
|
12548
|
+
}
|
|
12549
|
+
async function findContainerByName(name) {
|
|
12550
|
+
const result = spawnSync5(
|
|
12551
|
+
"docker",
|
|
12552
|
+
["inspect", name, "--format", "{{.State.Status}}"],
|
|
12553
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
|
|
12554
|
+
);
|
|
12555
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
12556
|
+
throw new Error(
|
|
12557
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12558
|
+
);
|
|
12559
|
+
}
|
|
12560
|
+
if (result.status !== 0) return null;
|
|
12561
|
+
const status = (result.stdout || "").trim();
|
|
12562
|
+
if (!status) return null;
|
|
12563
|
+
return { name, status };
|
|
12564
|
+
}
|
|
12565
|
+
function containerIsHealthy(infoArg) {
|
|
12566
|
+
return infoArg.status === "running";
|
|
12567
|
+
}
|
|
12568
|
+
async function removeContainer(name) {
|
|
12569
|
+
const result = spawnSync5("docker", ["rm", "-f", name], {
|
|
12570
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
12571
|
+
encoding: "utf-8"
|
|
12572
|
+
});
|
|
12573
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
12574
|
+
throw new Error(
|
|
12575
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12576
|
+
);
|
|
12577
|
+
}
|
|
12578
|
+
if (result.status !== 0) {
|
|
12579
|
+
throw new Error(
|
|
12580
|
+
`docker rm -f ${name} failed (exit ${result.status}): ${result.stderr || ""}`
|
|
12581
|
+
);
|
|
12582
|
+
}
|
|
12583
|
+
}
|
|
12584
|
+
async function startRunnerContainer(opts) {
|
|
12585
|
+
const labelStr = opts.labels.join(",");
|
|
12586
|
+
const args = [
|
|
12587
|
+
"run",
|
|
12588
|
+
"-d",
|
|
12589
|
+
"--restart=unless-stopped",
|
|
12590
|
+
"--name",
|
|
12591
|
+
opts.name,
|
|
12592
|
+
"-e",
|
|
12593
|
+
`REPO_URL=${opts.repoUrl}`,
|
|
12594
|
+
"-e",
|
|
12595
|
+
`RUNNER_TOKEN=${opts.token}`,
|
|
12596
|
+
"-e",
|
|
12597
|
+
`LABELS=${labelStr}`,
|
|
12598
|
+
"-e",
|
|
12599
|
+
`RUNNER_NAME=${opts.name}`,
|
|
12600
|
+
"-v",
|
|
12601
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
12602
|
+
"myoung34/github-runner:latest"
|
|
12603
|
+
];
|
|
12604
|
+
const result = spawnSync5("docker", args, {
|
|
12605
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
12606
|
+
encoding: "utf-8"
|
|
12607
|
+
});
|
|
12608
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
12609
|
+
throw new Error(
|
|
12610
|
+
"Docker is required for the default runner. Install Docker, or use --native."
|
|
12611
|
+
);
|
|
12612
|
+
}
|
|
12613
|
+
if (result.status !== 0) {
|
|
12614
|
+
const stderr = (result.stderr || "").replace(opts.token, "<REDACTED>");
|
|
12615
|
+
throw new Error(
|
|
12616
|
+
`docker run failed (exit ${result.status}): ${stderr}`
|
|
12617
|
+
);
|
|
12618
|
+
}
|
|
12619
|
+
}
|
|
12620
|
+
async function writeEnvEntries(entries, envPath = ".env") {
|
|
12621
|
+
let existing = "";
|
|
12622
|
+
try {
|
|
12623
|
+
existing = await fs.readFile(envPath, "utf-8");
|
|
12624
|
+
} catch (err) {
|
|
12625
|
+
if (err.code !== "ENOENT") throw err;
|
|
12626
|
+
}
|
|
12627
|
+
const lines = existing === "" ? [] : existing.split("\n");
|
|
12628
|
+
const remainingKeys = new Set(Object.keys(entries));
|
|
12629
|
+
const updated = lines.map((line) => {
|
|
12630
|
+
for (const key of remainingKeys) {
|
|
12631
|
+
if (line.startsWith(`${key}=`)) {
|
|
12632
|
+
remainingKeys.delete(key);
|
|
12633
|
+
return `${key}=${entries[key]}`;
|
|
12634
|
+
}
|
|
12635
|
+
}
|
|
12636
|
+
return line;
|
|
12637
|
+
});
|
|
12638
|
+
for (const key of remainingKeys) {
|
|
12639
|
+
updated.push(`${key}=${entries[key]}`);
|
|
12640
|
+
}
|
|
12641
|
+
let output = updated.join("\n");
|
|
12642
|
+
if (existing !== "" && !existing.endsWith("\n") && !output.endsWith("\n")) {
|
|
12643
|
+
output += "\n";
|
|
12644
|
+
} else if (existing === "" && !output.endsWith("\n")) {
|
|
12645
|
+
output += "\n";
|
|
12646
|
+
}
|
|
12647
|
+
await fs.writeFile(envPath, output, "utf-8");
|
|
12648
|
+
}
|
|
12649
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12650
|
+
async function pollUntilOnline(config, name, timeoutMs) {
|
|
12651
|
+
const deadline = Date.now() + timeoutMs;
|
|
12652
|
+
while (Date.now() < deadline) {
|
|
12653
|
+
try {
|
|
12654
|
+
const list = await listRunners(config);
|
|
12655
|
+
const runner = list.runners.find((r) => r.name === name);
|
|
12656
|
+
if (runner && runner.status === "online") return;
|
|
12657
|
+
} catch {
|
|
12658
|
+
}
|
|
12659
|
+
if (Date.now() + 3e3 >= deadline) break;
|
|
12660
|
+
await sleep(3e3);
|
|
12661
|
+
}
|
|
12662
|
+
throw new Error(
|
|
12663
|
+
`Runner '${name}' did not appear online within ${Math.floor(
|
|
12664
|
+
timeoutMs / 1e3
|
|
12665
|
+
)}s \u2014 check container logs with: docker logs ${name}`
|
|
12666
|
+
);
|
|
12667
|
+
}
|
|
12668
|
+
function setupStep(text) {
|
|
12669
|
+
info(text);
|
|
12670
|
+
}
|
|
12671
|
+
|
|
12672
|
+
// src/cli/workflow-switcher.ts
|
|
12673
|
+
import {
|
|
12674
|
+
existsSync as existsSync33,
|
|
12675
|
+
mkdirSync as mkdirSync20,
|
|
12676
|
+
readFileSync as readFileSync31,
|
|
12677
|
+
unlinkSync as unlinkSync5,
|
|
12678
|
+
writeFileSync as writeFileSync27
|
|
12679
|
+
} from "fs";
|
|
12680
|
+
import { dirname as dirname8, join as join31 } from "path";
|
|
12681
|
+
var TARGET_LABEL = "[self-hosted, beastmode]";
|
|
12682
|
+
var TARGET_WORKFLOWS = [
|
|
12683
|
+
".github/workflows/test.yml",
|
|
12684
|
+
".github/workflows/dogfood-e2e.yml"
|
|
12685
|
+
];
|
|
12686
|
+
var STATE_FILE = ".beastmode/runner-workflow-state.json";
|
|
12687
|
+
var RUNS_ON_REGEX = /^(\s+runs-on:\s*)(.+)$/;
|
|
12688
|
+
function replaceRunsOn(content, targetLabel) {
|
|
12689
|
+
const lines = content.split("\n");
|
|
12690
|
+
const originals = [];
|
|
12691
|
+
const newLines = lines.map((line) => {
|
|
12692
|
+
const match = line.match(RUNS_ON_REGEX);
|
|
12693
|
+
if (match) {
|
|
12694
|
+
originals.push({ line });
|
|
12695
|
+
return `${match[1]}${targetLabel}`;
|
|
12696
|
+
}
|
|
12697
|
+
return line;
|
|
12698
|
+
});
|
|
12699
|
+
return {
|
|
12700
|
+
newContent: newLines.join("\n"),
|
|
12701
|
+
originals
|
|
12702
|
+
};
|
|
12703
|
+
}
|
|
12704
|
+
function restoreRunsOn(content, originals) {
|
|
12705
|
+
const lines = content.split("\n");
|
|
12706
|
+
let idx = 0;
|
|
12707
|
+
const newLines = lines.map((line) => {
|
|
12708
|
+
const match = line.match(RUNS_ON_REGEX);
|
|
12709
|
+
if (match && idx < originals.length) {
|
|
12710
|
+
const restored = originals[idx].line;
|
|
12711
|
+
idx++;
|
|
12712
|
+
return restored;
|
|
12713
|
+
}
|
|
12714
|
+
return line;
|
|
12715
|
+
});
|
|
12716
|
+
return newLines.join("\n");
|
|
12717
|
+
}
|
|
12718
|
+
async function switchWorkflows(projectDir) {
|
|
12719
|
+
const statePath = join31(projectDir, STATE_FILE);
|
|
12720
|
+
if (existsSync33(statePath)) {
|
|
12721
|
+
return { alreadySwitched: true, files: [] };
|
|
12722
|
+
}
|
|
12723
|
+
const state = {
|
|
12724
|
+
switched_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12725
|
+
target_label: TARGET_LABEL,
|
|
12726
|
+
files: []
|
|
12727
|
+
};
|
|
12728
|
+
const resultFiles = [];
|
|
12729
|
+
for (const relPath of TARGET_WORKFLOWS) {
|
|
12730
|
+
const absPath = join31(projectDir, relPath);
|
|
12731
|
+
if (!existsSync33(absPath)) {
|
|
12732
|
+
throw new Error(`Workflow file not found: ${relPath}`);
|
|
12733
|
+
}
|
|
12734
|
+
const content = readFileSync31(absPath, "utf-8");
|
|
12735
|
+
const { newContent, originals } = replaceRunsOn(content, TARGET_LABEL);
|
|
12736
|
+
if (originals.length === 0) {
|
|
12737
|
+
throw new Error(`No runs-on found in ${relPath}`);
|
|
12738
|
+
}
|
|
12739
|
+
writeFileSync27(absPath, newContent, "utf-8");
|
|
12740
|
+
state.files.push({ relativePath: relPath, originals });
|
|
12741
|
+
resultFiles.push({ relativePath: relPath, jobCount: originals.length });
|
|
12742
|
+
}
|
|
12743
|
+
mkdirSync20(dirname8(statePath), { recursive: true });
|
|
12744
|
+
writeFileSync27(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
12745
|
+
return { alreadySwitched: false, files: resultFiles };
|
|
12746
|
+
}
|
|
12747
|
+
async function restoreWorkflows(projectDir) {
|
|
12748
|
+
const statePath = join31(projectDir, STATE_FILE);
|
|
12749
|
+
if (!existsSync33(statePath)) {
|
|
12750
|
+
return { nothingToRestore: true, files: [] };
|
|
12751
|
+
}
|
|
12752
|
+
const state = JSON.parse(
|
|
12753
|
+
readFileSync31(statePath, "utf-8")
|
|
12754
|
+
);
|
|
12755
|
+
const resultFiles = [];
|
|
12756
|
+
for (const fileState of state.files) {
|
|
12757
|
+
const absPath = join31(projectDir, fileState.relativePath);
|
|
12758
|
+
if (!existsSync33(absPath)) {
|
|
12759
|
+
throw new Error(`Workflow file not found: ${fileState.relativePath}`);
|
|
12760
|
+
}
|
|
12761
|
+
const content = readFileSync31(absPath, "utf-8");
|
|
12762
|
+
const newContent = restoreRunsOn(content, fileState.originals);
|
|
12763
|
+
writeFileSync27(absPath, newContent, "utf-8");
|
|
12764
|
+
resultFiles.push({
|
|
12765
|
+
relativePath: fileState.relativePath,
|
|
12766
|
+
jobCount: fileState.originals.length
|
|
12767
|
+
});
|
|
12768
|
+
}
|
|
12769
|
+
unlinkSync5(statePath);
|
|
12770
|
+
return { nothingToRestore: false, files: resultFiles };
|
|
12771
|
+
}
|
|
12772
|
+
|
|
12773
|
+
// src/cli/commands/runner-cmd.ts
|
|
12425
12774
|
var runnerCommand = new Command23("runner").description("Manage self-hosted GitHub Actions runners");
|
|
12426
|
-
|
|
12427
|
-
|
|
12775
|
+
async function runnerSetupAction(opts) {
|
|
12776
|
+
if (opts.native) {
|
|
12777
|
+
warn("--native is not implemented yet (Story 5)");
|
|
12778
|
+
return;
|
|
12779
|
+
}
|
|
12780
|
+
const ghConfig = resolveGitHubConfig();
|
|
12781
|
+
const repoSlug = opts.repo ?? resolveRepoSlug();
|
|
12782
|
+
setupStep("Generating registration token via GitHub API...");
|
|
12783
|
+
if (opts.dryRun) {
|
|
12784
|
+
info(`[dry-run] Would start container '${opts.name}' for repo ${repoSlug}`);
|
|
12785
|
+
return;
|
|
12786
|
+
}
|
|
12787
|
+
const { token: regToken } = await createRegistrationToken(ghConfig);
|
|
12788
|
+
setupStep("Pulling myoung34/github-runner:latest...");
|
|
12789
|
+
await pullImageIfNeeded("myoung34/github-runner:latest");
|
|
12790
|
+
const existing = await findContainerByName(opts.name);
|
|
12791
|
+
if (existing) {
|
|
12792
|
+
if (containerIsHealthy(existing)) {
|
|
12793
|
+
success(`Runner container '${opts.name}' already running \u2014 reusing.`);
|
|
12794
|
+
return;
|
|
12795
|
+
}
|
|
12796
|
+
setupStep(
|
|
12797
|
+
`Container '${opts.name}' exists but is dead \u2014 removing and recreating...`
|
|
12798
|
+
);
|
|
12799
|
+
await removeContainer(opts.name);
|
|
12800
|
+
}
|
|
12801
|
+
const repoUrl = `https://github.com/${ghConfig.owner}/${ghConfig.repo}`;
|
|
12802
|
+
setupStep("Starting runner container...");
|
|
12803
|
+
await startRunnerContainer({
|
|
12804
|
+
name: opts.name,
|
|
12805
|
+
repoUrl,
|
|
12806
|
+
token: regToken,
|
|
12807
|
+
labels: ["self-hosted", opts.label]
|
|
12808
|
+
});
|
|
12809
|
+
await writeEnvEntries({
|
|
12810
|
+
RUNNER_REPO_URL: repoUrl,
|
|
12811
|
+
RUNNER_TOKEN: regToken,
|
|
12812
|
+
RUNNER_NAME: opts.name
|
|
12813
|
+
});
|
|
12814
|
+
setupStep("Waiting for runner to appear online on GitHub (timeout 60s)...");
|
|
12815
|
+
await pollUntilOnline(ghConfig, opts.name, 6e4);
|
|
12816
|
+
setupStep("Adding runner to docker-compose...");
|
|
12817
|
+
await composeUpRunner();
|
|
12818
|
+
success(`Runner '${opts.name}' registered and online.`);
|
|
12819
|
+
}
|
|
12820
|
+
async function composeUpRunner() {
|
|
12821
|
+
await new Promise((resolve21, reject) => {
|
|
12822
|
+
const child = spawn2(
|
|
12823
|
+
"docker",
|
|
12824
|
+
["compose", "--profile", "runner", "up", "-d", "runner"],
|
|
12825
|
+
{ stdio: ["ignore", "inherit", "inherit"] }
|
|
12826
|
+
);
|
|
12827
|
+
child.on("error", (err) => reject(err));
|
|
12828
|
+
child.on("exit", (code) => {
|
|
12829
|
+
if (code === 0) resolve21();
|
|
12830
|
+
else
|
|
12831
|
+
reject(
|
|
12832
|
+
new Error(
|
|
12833
|
+
`docker compose --profile runner up -d runner exited with code ${code}`
|
|
12834
|
+
)
|
|
12835
|
+
);
|
|
12836
|
+
});
|
|
12837
|
+
});
|
|
12838
|
+
}
|
|
12839
|
+
runnerCommand.command("setup").description("Set up a self-hosted GitHub Actions runner").option("--repo <owner/repo>", "GitHub repo for runner registration").option("--name <name>", "Container + runner name", "beastmode-runner").option("--label <label>", "Additional runner label", "beastmode").option("--dry-run", "Print what would happen without mutating state").option("--native", "Use native install instead of Docker").action(async (opts) => {
|
|
12840
|
+
await runnerSetupAction(opts);
|
|
12428
12841
|
});
|
|
12429
12842
|
runnerCommand.command("status").description("Show runner status (container + GitHub registration)").action(() => {
|
|
12430
12843
|
warn("runner status is not implemented yet (Story 3)");
|
|
@@ -12432,11 +12845,41 @@ runnerCommand.command("status").description("Show runner status (container + Git
|
|
|
12432
12845
|
runnerCommand.command("remove").description("Remove the runner (container + GitHub deregistration)").action(() => {
|
|
12433
12846
|
warn("runner remove is not implemented yet (Story 3)");
|
|
12434
12847
|
});
|
|
12435
|
-
runnerCommand.command("switch-workflows").description("Switch workflow runs-on to self-hosted runner").action(() => {
|
|
12436
|
-
|
|
12848
|
+
runnerCommand.command("switch-workflows").description("Switch workflow runs-on to self-hosted runner").option("--project-dir <path>", "Project root directory", process.cwd()).action(async (opts) => {
|
|
12849
|
+
const projectDir = resolve20(opts.projectDir);
|
|
12850
|
+
try {
|
|
12851
|
+
const result = await switchWorkflows(projectDir);
|
|
12852
|
+
if (result.alreadySwitched) {
|
|
12853
|
+
info("Workflows already switched \u2014 nothing to do.");
|
|
12854
|
+
return;
|
|
12855
|
+
}
|
|
12856
|
+
header("Switched workflows to self-hosted runner");
|
|
12857
|
+
for (const file of result.files) {
|
|
12858
|
+
success(`${file.relativePath}: ${file.jobCount} job(s) switched`);
|
|
12859
|
+
}
|
|
12860
|
+
info("State saved to .beastmode/runner-workflow-state.json");
|
|
12861
|
+
} catch (err) {
|
|
12862
|
+
error(err.message);
|
|
12863
|
+
process.exitCode = 1;
|
|
12864
|
+
}
|
|
12437
12865
|
});
|
|
12438
|
-
runnerCommand.command("restore-workflows").description("Restore workflows to original runs-on values").action(() => {
|
|
12439
|
-
|
|
12866
|
+
runnerCommand.command("restore-workflows").description("Restore workflows to original runs-on values").option("--project-dir <path>", "Project root directory", process.cwd()).action(async (opts) => {
|
|
12867
|
+
const projectDir = resolve20(opts.projectDir);
|
|
12868
|
+
try {
|
|
12869
|
+
const result = await restoreWorkflows(projectDir);
|
|
12870
|
+
if (result.nothingToRestore) {
|
|
12871
|
+
info("Nothing to restore.");
|
|
12872
|
+
return;
|
|
12873
|
+
}
|
|
12874
|
+
header("Restored workflows to original runs-on values");
|
|
12875
|
+
for (const file of result.files) {
|
|
12876
|
+
success(`${file.relativePath}: ${file.jobCount} job(s) restored`);
|
|
12877
|
+
}
|
|
12878
|
+
info("State file removed.");
|
|
12879
|
+
} catch (err) {
|
|
12880
|
+
error(err.message);
|
|
12881
|
+
process.exitCode = 1;
|
|
12882
|
+
}
|
|
12440
12883
|
});
|
|
12441
12884
|
|
|
12442
12885
|
// src/index.ts
|