@beastmode-develeap/beastmode 0.1.198 → 0.1.200
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 +531 -102
- 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
|
@@ -8490,8 +8490,8 @@ async function runInit(name, opts) {
|
|
|
8490
8490
|
throw new Error(`Factory already exists at ./${factoryName}. Use 'beastmode config' to modify.`);
|
|
8491
8491
|
}
|
|
8492
8492
|
if (opts.from) {
|
|
8493
|
-
const { readFileSync:
|
|
8494
|
-
const templateContent =
|
|
8493
|
+
const { readFileSync: readFileSync36 } = await import("fs");
|
|
8494
|
+
const templateContent = readFileSync36(resolve6(opts.from), "utf-8");
|
|
8495
8495
|
const { parseTemplate: parseTemplate2 } = await Promise.resolve().then(() => (init_template_importer(), template_importer_exports));
|
|
8496
8496
|
const template = parseTemplate2(templateContent);
|
|
8497
8497
|
info(`Importing from template: ${opts.from}`);
|
|
@@ -11423,10 +11423,10 @@ async function runMigrate(opts) {
|
|
|
11423
11423
|
let runDirs = [];
|
|
11424
11424
|
const checkpoints = /* @__PURE__ */ new Map();
|
|
11425
11425
|
if (existsSync28(runsDir)) {
|
|
11426
|
-
const { readdirSync:
|
|
11427
|
-
runDirs =
|
|
11426
|
+
const { readdirSync: readdirSync14 } = await import("fs");
|
|
11427
|
+
runDirs = readdirSync14(runsDir).filter((d) => {
|
|
11428
11428
|
try {
|
|
11429
|
-
return
|
|
11429
|
+
return readdirSync14(join26(runsDir, d)).length > 0;
|
|
11430
11430
|
} catch {
|
|
11431
11431
|
return false;
|
|
11432
11432
|
}
|
|
@@ -11444,8 +11444,8 @@ async function runMigrate(opts) {
|
|
|
11444
11444
|
}
|
|
11445
11445
|
let worktreeOutput = "";
|
|
11446
11446
|
try {
|
|
11447
|
-
const { execSync:
|
|
11448
|
-
worktreeOutput =
|
|
11447
|
+
const { execSync: execSync14 } = await import("child_process");
|
|
11448
|
+
worktreeOutput = execSync14("git worktree list", {
|
|
11449
11449
|
cwd,
|
|
11450
11450
|
encoding: "utf-8",
|
|
11451
11451
|
timeout: 5e3
|
|
@@ -11568,8 +11568,8 @@ async function runPipeline(projectName, opts) {
|
|
|
11568
11568
|
let projectConfig = null;
|
|
11569
11569
|
const projectsDir = join27(bmDir, "projects");
|
|
11570
11570
|
if (existsSync29(projectsDir)) {
|
|
11571
|
-
const { readdirSync:
|
|
11572
|
-
const projectFiles =
|
|
11571
|
+
const { readdirSync: readdirSync14 } = await import("fs");
|
|
11572
|
+
const projectFiles = readdirSync14(projectsDir).filter(
|
|
11573
11573
|
(f) => f.endsWith(".json")
|
|
11574
11574
|
);
|
|
11575
11575
|
if (projectName) {
|
|
@@ -11618,14 +11618,14 @@ async function runPipeline(projectName, opts) {
|
|
|
11618
11618
|
const daemonConfig = generateDaemonConfig(factoryConfig, projectConfig, factoryDir);
|
|
11619
11619
|
writeFileSync22(daemonConfigPath, JSON.stringify(daemonConfig, null, 2), "utf-8");
|
|
11620
11620
|
info(`Generated daemon config at: ${daemonConfigPath}`);
|
|
11621
|
-
const { execSync:
|
|
11621
|
+
const { execSync: execSync14 } = await import("child_process");
|
|
11622
11622
|
let pythonAvailable = false;
|
|
11623
11623
|
try {
|
|
11624
|
-
|
|
11624
|
+
execSync14("python --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11625
11625
|
pythonAvailable = true;
|
|
11626
11626
|
} catch {
|
|
11627
11627
|
try {
|
|
11628
|
-
|
|
11628
|
+
execSync14("python3 --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11629
11629
|
pythonAvailable = true;
|
|
11630
11630
|
} catch {
|
|
11631
11631
|
}
|
|
@@ -11746,8 +11746,8 @@ async function runDaemon(opts) {
|
|
|
11746
11746
|
let projectConfig = null;
|
|
11747
11747
|
const projectsDir = join28(bmDir, "projects");
|
|
11748
11748
|
if (existsSync30(projectsDir)) {
|
|
11749
|
-
const { readdirSync:
|
|
11750
|
-
const projectFiles =
|
|
11749
|
+
const { readdirSync: readdirSync14 } = await import("fs");
|
|
11750
|
+
const projectFiles = readdirSync14(projectsDir).filter(
|
|
11751
11751
|
(f) => f.endsWith(".json")
|
|
11752
11752
|
);
|
|
11753
11753
|
if (projectFiles.length > 0) {
|
|
@@ -11778,16 +11778,16 @@ async function runDaemon(opts) {
|
|
|
11778
11778
|
console.log(JSON.stringify(daemonConfig, null, 2));
|
|
11779
11779
|
return;
|
|
11780
11780
|
}
|
|
11781
|
-
const { execSync:
|
|
11781
|
+
const { execSync: execSync14 } = await import("child_process");
|
|
11782
11782
|
let pythonCmd = "python";
|
|
11783
11783
|
let pythonAvailable = false;
|
|
11784
11784
|
try {
|
|
11785
|
-
|
|
11785
|
+
execSync14("python --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11786
11786
|
pythonAvailable = true;
|
|
11787
11787
|
pythonCmd = "python";
|
|
11788
11788
|
} catch {
|
|
11789
11789
|
try {
|
|
11790
|
-
|
|
11790
|
+
execSync14("python3 --version", { timeout: 5e3, encoding: "utf-8" });
|
|
11791
11791
|
pythonAvailable = true;
|
|
11792
11792
|
pythonCmd = "python3";
|
|
11793
11793
|
} catch {
|
|
@@ -12466,7 +12466,313 @@ var updateCommand = new Command22("update").description("Pull latest BeastMode i
|
|
|
12466
12466
|
init_display();
|
|
12467
12467
|
import { Command as Command23 } from "commander";
|
|
12468
12468
|
import { spawn as spawn3 } from "child_process";
|
|
12469
|
-
import {
|
|
12469
|
+
import { existsSync as existsSync37, readdirSync as readdirSync12, readFileSync as readFileSync34 } from "fs";
|
|
12470
|
+
import { basename as basename6, join as join36, resolve as resolve20 } from "path";
|
|
12471
|
+
|
|
12472
|
+
// src/cli/runner-image-builder.ts
|
|
12473
|
+
import { execSync as execSync10 } from "child_process";
|
|
12474
|
+
import { createHash } from "crypto";
|
|
12475
|
+
import {
|
|
12476
|
+
existsSync as existsSync34,
|
|
12477
|
+
mkdirSync as mkdirSync20,
|
|
12478
|
+
readFileSync as readFileSync32,
|
|
12479
|
+
writeFileSync as writeFileSync27
|
|
12480
|
+
} from "fs";
|
|
12481
|
+
import { join as join32 } from "path";
|
|
12482
|
+
|
|
12483
|
+
// src/cli/stack-detect.ts
|
|
12484
|
+
import { existsSync as existsSync33, readFileSync as readFileSync31 } from "fs";
|
|
12485
|
+
import { join as join31 } from "path";
|
|
12486
|
+
var NODE_LOCKFILES = [
|
|
12487
|
+
"package-lock.json",
|
|
12488
|
+
"pnpm-lock.yaml",
|
|
12489
|
+
"yarn.lock",
|
|
12490
|
+
"bun.lockb",
|
|
12491
|
+
"package.json"
|
|
12492
|
+
];
|
|
12493
|
+
var PYTHON_LOCKFILES = [
|
|
12494
|
+
"poetry.lock",
|
|
12495
|
+
"uv.lock",
|
|
12496
|
+
"Pipfile.lock",
|
|
12497
|
+
"requirements.txt",
|
|
12498
|
+
"pyproject.toml"
|
|
12499
|
+
];
|
|
12500
|
+
var GO_LOCKFILES = ["go.mod", "go.sum"];
|
|
12501
|
+
var RUST_LOCKFILES = ["Cargo.lock", "Cargo.toml"];
|
|
12502
|
+
var JAVA_MAVEN_LOCKFILES = ["pom.xml"];
|
|
12503
|
+
var JAVA_GRADLE_LOCKFILES = [
|
|
12504
|
+
"build.gradle",
|
|
12505
|
+
"build.gradle.kts",
|
|
12506
|
+
"gradle.lockfile"
|
|
12507
|
+
];
|
|
12508
|
+
var STACK_LOCKFILES = {
|
|
12509
|
+
nextjs: NODE_LOCKFILES,
|
|
12510
|
+
vite: NODE_LOCKFILES,
|
|
12511
|
+
react: NODE_LOCKFILES,
|
|
12512
|
+
node: NODE_LOCKFILES,
|
|
12513
|
+
fastapi: PYTHON_LOCKFILES,
|
|
12514
|
+
django: PYTHON_LOCKFILES,
|
|
12515
|
+
python: PYTHON_LOCKFILES,
|
|
12516
|
+
go: GO_LOCKFILES,
|
|
12517
|
+
rust: RUST_LOCKFILES,
|
|
12518
|
+
"java-maven": JAVA_MAVEN_LOCKFILES,
|
|
12519
|
+
"java-gradle": JAVA_GRADLE_LOCKFILES
|
|
12520
|
+
};
|
|
12521
|
+
function readFileSafe2(path) {
|
|
12522
|
+
try {
|
|
12523
|
+
return readFileSync31(path, "utf-8");
|
|
12524
|
+
} catch {
|
|
12525
|
+
return null;
|
|
12526
|
+
}
|
|
12527
|
+
}
|
|
12528
|
+
function parseJsonSafe2(content) {
|
|
12529
|
+
try {
|
|
12530
|
+
return JSON.parse(content);
|
|
12531
|
+
} catch {
|
|
12532
|
+
return null;
|
|
12533
|
+
}
|
|
12534
|
+
}
|
|
12535
|
+
function detectRunnerStack(projectDir) {
|
|
12536
|
+
const pkgContent = readFileSafe2(join31(projectDir, "package.json"));
|
|
12537
|
+
if (pkgContent) {
|
|
12538
|
+
const pkg = parseJsonSafe2(pkgContent);
|
|
12539
|
+
if (pkg) {
|
|
12540
|
+
const deps = {
|
|
12541
|
+
...pkg.dependencies,
|
|
12542
|
+
...pkg.devDependencies
|
|
12543
|
+
};
|
|
12544
|
+
if (deps.next) return { name: "nextjs", language: "node" };
|
|
12545
|
+
if (deps.vite) return { name: "vite", language: "node" };
|
|
12546
|
+
if (deps["react-scripts"]) return { name: "react", language: "node" };
|
|
12547
|
+
}
|
|
12548
|
+
return { name: "node", language: "node" };
|
|
12549
|
+
}
|
|
12550
|
+
if (existsSync33(join31(projectDir, "manage.py"))) {
|
|
12551
|
+
return { name: "django", language: "python" };
|
|
12552
|
+
}
|
|
12553
|
+
const pyproject = readFileSafe2(join31(projectDir, "pyproject.toml"));
|
|
12554
|
+
if (pyproject) {
|
|
12555
|
+
if (pyproject.toLowerCase().includes("fastapi")) {
|
|
12556
|
+
return { name: "fastapi", language: "python" };
|
|
12557
|
+
}
|
|
12558
|
+
return { name: "python", language: "python" };
|
|
12559
|
+
}
|
|
12560
|
+
if (existsSync33(join31(projectDir, "requirements.txt"))) {
|
|
12561
|
+
return { name: "python", language: "python" };
|
|
12562
|
+
}
|
|
12563
|
+
if (existsSync33(join31(projectDir, "go.mod"))) {
|
|
12564
|
+
return { name: "go", language: "go" };
|
|
12565
|
+
}
|
|
12566
|
+
if (existsSync33(join31(projectDir, "Cargo.toml"))) {
|
|
12567
|
+
return { name: "rust", language: "rust" };
|
|
12568
|
+
}
|
|
12569
|
+
if (existsSync33(join31(projectDir, "pom.xml"))) {
|
|
12570
|
+
return { name: "java-maven", language: "java" };
|
|
12571
|
+
}
|
|
12572
|
+
if (existsSync33(join31(projectDir, "build.gradle")) || existsSync33(join31(projectDir, "build.gradle.kts"))) {
|
|
12573
|
+
return { name: "java-gradle", language: "java" };
|
|
12574
|
+
}
|
|
12575
|
+
return { name: "node", language: "node" };
|
|
12576
|
+
}
|
|
12577
|
+
function findLockfiles(projectDir, stackName) {
|
|
12578
|
+
const candidates = STACK_LOCKFILES[stackName] ?? [];
|
|
12579
|
+
return candidates.filter((f) => existsSync33(join31(projectDir, f)));
|
|
12580
|
+
}
|
|
12581
|
+
|
|
12582
|
+
// src/cli/runner-image-builder.ts
|
|
12583
|
+
var RUNNER_STATE_DIR = ".beastmode/runner";
|
|
12584
|
+
var LAST_BUILD_FILE = "last-build.json";
|
|
12585
|
+
var LAYERS_FILE = "runner-layers.json";
|
|
12586
|
+
function getDependencyInstallCommand(stack, lockfiles) {
|
|
12587
|
+
if (lockfiles.length === 0) return null;
|
|
12588
|
+
const has = (name) => lockfiles.includes(name);
|
|
12589
|
+
if (stack.language === "node") {
|
|
12590
|
+
if (has("pnpm-lock.yaml")) return "pnpm install --frozen-lockfile";
|
|
12591
|
+
if (has("yarn.lock")) return "yarn install --frozen-lockfile";
|
|
12592
|
+
if (has("bun.lockb")) return "bun install --frozen-lockfile";
|
|
12593
|
+
if (has("package-lock.json")) return "npm ci";
|
|
12594
|
+
if (has("package.json")) return "npm install";
|
|
12595
|
+
return null;
|
|
12596
|
+
}
|
|
12597
|
+
if (stack.language === "python") {
|
|
12598
|
+
if (has("poetry.lock")) return "poetry install --no-interaction --no-root";
|
|
12599
|
+
if (has("uv.lock")) return "uv sync --frozen";
|
|
12600
|
+
if (has("Pipfile.lock")) return "pipenv install --deploy";
|
|
12601
|
+
if (has("requirements.txt")) return "pip install -r requirements.txt";
|
|
12602
|
+
if (has("pyproject.toml")) return "pip install .";
|
|
12603
|
+
return null;
|
|
12604
|
+
}
|
|
12605
|
+
if (stack.language === "go") {
|
|
12606
|
+
if (has("go.mod")) return "go mod download";
|
|
12607
|
+
return null;
|
|
12608
|
+
}
|
|
12609
|
+
if (stack.language === "rust") {
|
|
12610
|
+
if (has("Cargo.lock") || has("Cargo.toml")) return "cargo fetch";
|
|
12611
|
+
return null;
|
|
12612
|
+
}
|
|
12613
|
+
if (stack.language === "java") {
|
|
12614
|
+
if (stack.name === "java-maven" && has("pom.xml")) {
|
|
12615
|
+
return "mvn -B -q dependency:go-offline";
|
|
12616
|
+
}
|
|
12617
|
+
if (stack.name === "java-gradle" && (has("build.gradle") || has("build.gradle.kts"))) {
|
|
12618
|
+
return "./gradlew --no-daemon dependencies || true";
|
|
12619
|
+
}
|
|
12620
|
+
return null;
|
|
12621
|
+
}
|
|
12622
|
+
return null;
|
|
12623
|
+
}
|
|
12624
|
+
function runtimeInstallBlock(stack) {
|
|
12625
|
+
switch (stack.language) {
|
|
12626
|
+
case "node":
|
|
12627
|
+
return [
|
|
12628
|
+
"RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \\",
|
|
12629
|
+
" && apt-get install -y --no-install-recommends nodejs \\",
|
|
12630
|
+
" && npm install -g pnpm yarn \\",
|
|
12631
|
+
" && rm -rf /var/lib/apt/lists/*"
|
|
12632
|
+
].join("\n");
|
|
12633
|
+
case "python":
|
|
12634
|
+
return [
|
|
12635
|
+
"RUN apt-get update \\",
|
|
12636
|
+
" && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \\",
|
|
12637
|
+
" && rm -rf /var/lib/apt/lists/*"
|
|
12638
|
+
].join("\n");
|
|
12639
|
+
case "go":
|
|
12640
|
+
return [
|
|
12641
|
+
"RUN curl -fsSL https://go.dev/dl/go1.22.0.linux-amd64.tar.gz \\",
|
|
12642
|
+
" | tar -C /usr/local -xz",
|
|
12643
|
+
'ENV PATH="/usr/local/go/bin:${PATH}"'
|
|
12644
|
+
].join("\n");
|
|
12645
|
+
case "rust":
|
|
12646
|
+
return [
|
|
12647
|
+
"RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \\",
|
|
12648
|
+
" | sh -s -- -y --default-toolchain stable",
|
|
12649
|
+
'ENV PATH="/root/.cargo/bin:${PATH}"'
|
|
12650
|
+
].join("\n");
|
|
12651
|
+
case "java":
|
|
12652
|
+
return [
|
|
12653
|
+
"RUN apt-get update \\",
|
|
12654
|
+
" && apt-get install -y --no-install-recommends openjdk-17-jdk-headless maven gradle \\",
|
|
12655
|
+
" && rm -rf /var/lib/apt/lists/*"
|
|
12656
|
+
].join("\n");
|
|
12657
|
+
default:
|
|
12658
|
+
return "# no runtime block for unknown language";
|
|
12659
|
+
}
|
|
12660
|
+
}
|
|
12661
|
+
function generateDockerfile(stack, lockfiles) {
|
|
12662
|
+
const lines = [];
|
|
12663
|
+
lines.push("FROM myoung34/github-runner:latest");
|
|
12664
|
+
lines.push("USER root");
|
|
12665
|
+
lines.push("WORKDIR /opt/runner-deps");
|
|
12666
|
+
lines.push(runtimeInstallBlock(stack));
|
|
12667
|
+
if (lockfiles.length > 0) {
|
|
12668
|
+
for (const lf of lockfiles) {
|
|
12669
|
+
lines.push(`COPY ${lf} ./${lf}`);
|
|
12670
|
+
}
|
|
12671
|
+
const installCmd = getDependencyInstallCommand(stack, lockfiles);
|
|
12672
|
+
if (installCmd) {
|
|
12673
|
+
lines.push(`RUN ${installCmd}`);
|
|
12674
|
+
}
|
|
12675
|
+
}
|
|
12676
|
+
return lines.join("\n") + "\n";
|
|
12677
|
+
}
|
|
12678
|
+
function computeLockfileHash(projectDir, lockfiles) {
|
|
12679
|
+
const hash = createHash("sha256");
|
|
12680
|
+
for (const lf of lockfiles) {
|
|
12681
|
+
const path = join32(projectDir, lf);
|
|
12682
|
+
hash.update(lf);
|
|
12683
|
+
hash.update("\0");
|
|
12684
|
+
if (existsSync34(path)) {
|
|
12685
|
+
try {
|
|
12686
|
+
hash.update(readFileSync32(path));
|
|
12687
|
+
} catch {
|
|
12688
|
+
hash.update("<unreadable>");
|
|
12689
|
+
}
|
|
12690
|
+
} else {
|
|
12691
|
+
hash.update("<missing>");
|
|
12692
|
+
}
|
|
12693
|
+
hash.update("\0");
|
|
12694
|
+
}
|
|
12695
|
+
return hash.digest("hex").slice(0, 16);
|
|
12696
|
+
}
|
|
12697
|
+
function readLastBuild(projectDir) {
|
|
12698
|
+
const path = join32(projectDir, RUNNER_STATE_DIR, LAST_BUILD_FILE);
|
|
12699
|
+
if (!existsSync34(path)) return null;
|
|
12700
|
+
try {
|
|
12701
|
+
const data = JSON.parse(readFileSync32(path, "utf-8"));
|
|
12702
|
+
if (data && typeof data === "object" && typeof data.lockfileHash === "string" && typeof data.imageTag === "string" && typeof data.builtAt === "string") {
|
|
12703
|
+
return data;
|
|
12704
|
+
}
|
|
12705
|
+
return null;
|
|
12706
|
+
} catch {
|
|
12707
|
+
return null;
|
|
12708
|
+
}
|
|
12709
|
+
}
|
|
12710
|
+
function shouldRebuild(projectDir, currentHash) {
|
|
12711
|
+
const last = readLastBuild(projectDir);
|
|
12712
|
+
if (!last) return true;
|
|
12713
|
+
return last.lockfileHash !== currentHash;
|
|
12714
|
+
}
|
|
12715
|
+
function imageTagFor(projectName, hash) {
|
|
12716
|
+
const safeName = projectName.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
12717
|
+
return `beastmode-runner-${safeName}:${hash}`;
|
|
12718
|
+
}
|
|
12719
|
+
function writeStateFile(projectDir, fileName, payload) {
|
|
12720
|
+
const dir = join32(projectDir, RUNNER_STATE_DIR);
|
|
12721
|
+
mkdirSync20(dir, { recursive: true });
|
|
12722
|
+
writeFileSync27(
|
|
12723
|
+
join32(dir, fileName),
|
|
12724
|
+
JSON.stringify(payload, null, 2) + "\n",
|
|
12725
|
+
"utf-8"
|
|
12726
|
+
);
|
|
12727
|
+
}
|
|
12728
|
+
function buildRunnerImage(opts) {
|
|
12729
|
+
const { projectDir, projectName, stack, lockfiles, force, dryRun } = opts;
|
|
12730
|
+
if (!existsSync34(projectDir)) {
|
|
12731
|
+
throw new Error(`Project directory not found: ${projectDir}`);
|
|
12732
|
+
}
|
|
12733
|
+
const lockfileHash = computeLockfileHash(projectDir, lockfiles);
|
|
12734
|
+
const imageTag = imageTagFor(projectName, lockfileHash);
|
|
12735
|
+
const dockerfile = generateDockerfile(stack, lockfiles);
|
|
12736
|
+
if (dryRun) {
|
|
12737
|
+
return { imageTag, lockfileHash, dockerfile, built: false };
|
|
12738
|
+
}
|
|
12739
|
+
if (!force && !shouldRebuild(projectDir, lockfileHash)) {
|
|
12740
|
+
return { imageTag, lockfileHash, dockerfile, built: false };
|
|
12741
|
+
}
|
|
12742
|
+
try {
|
|
12743
|
+
execSync10(
|
|
12744
|
+
`docker build -t ${imageTag} -f - ${projectDir}`,
|
|
12745
|
+
{
|
|
12746
|
+
input: dockerfile,
|
|
12747
|
+
stdio: ["pipe", "inherit", "inherit"]
|
|
12748
|
+
}
|
|
12749
|
+
);
|
|
12750
|
+
} catch (err) {
|
|
12751
|
+
const e = err;
|
|
12752
|
+
if (e && e.code === "ENOENT") {
|
|
12753
|
+
throw new Error(
|
|
12754
|
+
"Docker is required for runner image builds. Install Docker and retry."
|
|
12755
|
+
);
|
|
12756
|
+
}
|
|
12757
|
+
const msg = err?.message ?? String(err);
|
|
12758
|
+
throw new Error(`docker build failed: ${msg}`);
|
|
12759
|
+
}
|
|
12760
|
+
const builtAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12761
|
+
writeStateFile(projectDir, LAST_BUILD_FILE, {
|
|
12762
|
+
lockfileHash,
|
|
12763
|
+
imageTag,
|
|
12764
|
+
builtAt
|
|
12765
|
+
});
|
|
12766
|
+
writeStateFile(projectDir, LAYERS_FILE, {
|
|
12767
|
+
project: projectName,
|
|
12768
|
+
stack: stack.name,
|
|
12769
|
+
lockfile_hash: lockfileHash,
|
|
12770
|
+
image_tag: imageTag,
|
|
12771
|
+
lockfiles,
|
|
12772
|
+
built_at: builtAt
|
|
12773
|
+
});
|
|
12774
|
+
return { imageTag, lockfileHash, dockerfile, built: true };
|
|
12775
|
+
}
|
|
12470
12776
|
|
|
12471
12777
|
// src/cli/github-runners.ts
|
|
12472
12778
|
var GitHubRunnerApiError = class extends Error {
|
|
@@ -12538,19 +12844,19 @@ function resolveGitHubConfig() {
|
|
|
12538
12844
|
}
|
|
12539
12845
|
|
|
12540
12846
|
// src/cli/runner-helpers.ts
|
|
12541
|
-
import { execSync as
|
|
12847
|
+
import { execSync as execSync11, spawn, spawnSync as spawnSync5 } from "child_process";
|
|
12542
12848
|
import { promises as fs } from "fs";
|
|
12543
|
-
import { join as
|
|
12849
|
+
import { join as join33 } from "path";
|
|
12544
12850
|
init_display();
|
|
12545
12851
|
async function writeRunnerMeta(dir, meta) {
|
|
12546
12852
|
await fs.mkdir(dir, { recursive: true });
|
|
12547
|
-
const path =
|
|
12853
|
+
const path = join33(dir, "runner-meta.json");
|
|
12548
12854
|
await fs.writeFile(path, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
12549
12855
|
}
|
|
12550
12856
|
function resolveRepoSlug() {
|
|
12551
12857
|
let rawUrl;
|
|
12552
12858
|
try {
|
|
12553
|
-
rawUrl =
|
|
12859
|
+
rawUrl = execSync11("git remote get-url origin", {
|
|
12554
12860
|
encoding: "utf-8",
|
|
12555
12861
|
stdio: ["ignore", "pipe", "pipe"]
|
|
12556
12862
|
}).trim();
|
|
@@ -12636,6 +12942,7 @@ async function removeContainer(name) {
|
|
|
12636
12942
|
}
|
|
12637
12943
|
async function startRunnerContainer(opts) {
|
|
12638
12944
|
const labelStr = opts.labels.join(",");
|
|
12945
|
+
const image = opts.image ?? "myoung34/github-runner:latest";
|
|
12639
12946
|
const args = [
|
|
12640
12947
|
"run",
|
|
12641
12948
|
"-d",
|
|
@@ -12652,7 +12959,7 @@ async function startRunnerContainer(opts) {
|
|
|
12652
12959
|
`RUNNER_NAME=${opts.name}`,
|
|
12653
12960
|
"-v",
|
|
12654
12961
|
"/var/run/docker.sock:/var/run/docker.sock",
|
|
12655
|
-
|
|
12962
|
+
image
|
|
12656
12963
|
];
|
|
12657
12964
|
const result = spawnSync5("docker", args, {
|
|
12658
12965
|
stdio: ["ignore", "ignore", "pipe"],
|
|
@@ -12738,19 +13045,19 @@ function setupStep(text) {
|
|
|
12738
13045
|
|
|
12739
13046
|
// src/cli/native-runner.ts
|
|
12740
13047
|
import {
|
|
12741
|
-
execSync as
|
|
13048
|
+
execSync as execSync12,
|
|
12742
13049
|
spawn as spawn2,
|
|
12743
13050
|
spawnSync as spawnSync6
|
|
12744
13051
|
} from "child_process";
|
|
12745
13052
|
import {
|
|
12746
13053
|
createWriteStream,
|
|
12747
|
-
existsSync as
|
|
12748
|
-
mkdirSync as
|
|
13054
|
+
existsSync as existsSync35,
|
|
13055
|
+
mkdirSync as mkdirSync21,
|
|
12749
13056
|
unlinkSync as unlinkSync5,
|
|
12750
|
-
writeFileSync as
|
|
13057
|
+
writeFileSync as writeFileSync28
|
|
12751
13058
|
} from "fs";
|
|
12752
13059
|
import { homedir as homedir4 } from "os";
|
|
12753
|
-
import { join as
|
|
13060
|
+
import { join as join34, dirname as dirname8 } from "path";
|
|
12754
13061
|
import { Readable } from "stream";
|
|
12755
13062
|
import { pipeline } from "stream/promises";
|
|
12756
13063
|
init_display();
|
|
@@ -12780,8 +13087,8 @@ function runnerDownloadUrl(os, arch) {
|
|
|
12780
13087
|
return `https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-${ghOs}-${arch}-${RUNNER_VERSION}.tar.gz`;
|
|
12781
13088
|
}
|
|
12782
13089
|
async function downloadAndExtractRunner(installDir, os, arch) {
|
|
12783
|
-
const runShPath =
|
|
12784
|
-
if (
|
|
13090
|
+
const runShPath = join34(installDir, "run.sh");
|
|
13091
|
+
if (existsSync35(runShPath)) {
|
|
12785
13092
|
info(
|
|
12786
13093
|
`Runner binary already present at ${runShPath} \u2014 skipping download.`
|
|
12787
13094
|
);
|
|
@@ -12791,7 +13098,7 @@ async function downloadAndExtractRunner(installDir, os, arch) {
|
|
|
12791
13098
|
return;
|
|
12792
13099
|
}
|
|
12793
13100
|
const url = runnerDownloadUrl(os, arch);
|
|
12794
|
-
const tarball =
|
|
13101
|
+
const tarball = join34(installDir, "runner.tar.gz");
|
|
12795
13102
|
setupStep(
|
|
12796
13103
|
`Downloading GitHub Actions runner v${RUNNER_VERSION} (${os}/${arch})...`
|
|
12797
13104
|
);
|
|
@@ -12807,7 +13114,7 @@ async function downloadAndExtractRunner(installDir, os, arch) {
|
|
|
12807
13114
|
);
|
|
12808
13115
|
setupStep("Extracting runner...");
|
|
12809
13116
|
try {
|
|
12810
|
-
|
|
13117
|
+
execSync12("tar -xzf runner.tar.gz", { cwd: installDir, stdio: "ignore" });
|
|
12811
13118
|
} catch (err) {
|
|
12812
13119
|
throw new Error(
|
|
12813
13120
|
`Failed to extract runner archive: ${err?.message ?? String(err)}`
|
|
@@ -12842,7 +13149,7 @@ async function configureRunner(installDir, repoUrl, token, name, labels) {
|
|
|
12842
13149
|
"--unattended",
|
|
12843
13150
|
"--replace"
|
|
12844
13151
|
];
|
|
12845
|
-
const result = spawnSync6(
|
|
13152
|
+
const result = spawnSync6(join34(installDir, "config.sh"), args, {
|
|
12846
13153
|
cwd: installDir,
|
|
12847
13154
|
stdio: ["ignore", "inherit", "pipe"],
|
|
12848
13155
|
encoding: "utf-8"
|
|
@@ -12870,7 +13177,7 @@ function startRunnerForeground(installDir) {
|
|
|
12870
13177
|
"Use --service to install as a launchd agent for persistent operation."
|
|
12871
13178
|
);
|
|
12872
13179
|
}
|
|
12873
|
-
spawn2(
|
|
13180
|
+
spawn2(join34(installDir, "run.sh"), [], {
|
|
12874
13181
|
cwd: installDir,
|
|
12875
13182
|
stdio: "inherit"
|
|
12876
13183
|
});
|
|
@@ -12895,16 +13202,16 @@ WantedBy=default.target
|
|
|
12895
13202
|
}
|
|
12896
13203
|
async function installSystemdService(installDir, name) {
|
|
12897
13204
|
const unitName = `beastmode-runner-${name}.service`;
|
|
12898
|
-
const unitDir =
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
13205
|
+
const unitDir = join34(homedir4(), ".config", "systemd", "user");
|
|
13206
|
+
mkdirSync21(unitDir, { recursive: true });
|
|
13207
|
+
writeFileSync28(
|
|
13208
|
+
join34(unitDir, unitName),
|
|
12902
13209
|
systemdUnitContent(installDir, name),
|
|
12903
13210
|
"utf-8"
|
|
12904
13211
|
);
|
|
12905
13212
|
try {
|
|
12906
|
-
|
|
12907
|
-
|
|
13213
|
+
execSync12("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
13214
|
+
execSync12(`systemctl --user enable --now ${unitName}`, {
|
|
12908
13215
|
stdio: "inherit"
|
|
12909
13216
|
});
|
|
12910
13217
|
} catch (err) {
|
|
@@ -12952,17 +13259,17 @@ function launchdPlistContent(installDir, name) {
|
|
|
12952
13259
|
}
|
|
12953
13260
|
async function installLaunchdService(installDir, name) {
|
|
12954
13261
|
const label = `com.beastmode.runner.${name}`;
|
|
12955
|
-
const plistPath2 =
|
|
13262
|
+
const plistPath2 = join34(
|
|
12956
13263
|
homedir4(),
|
|
12957
13264
|
"Library",
|
|
12958
13265
|
"LaunchAgents",
|
|
12959
13266
|
`${label}.plist`
|
|
12960
13267
|
);
|
|
12961
|
-
|
|
12962
|
-
|
|
12963
|
-
|
|
13268
|
+
mkdirSync21(dirname8(plistPath2), { recursive: true });
|
|
13269
|
+
mkdirSync21(join34(installDir, "logs"), { recursive: true });
|
|
13270
|
+
writeFileSync28(plistPath2, launchdPlistContent(installDir, name), "utf-8");
|
|
12964
13271
|
try {
|
|
12965
|
-
|
|
13272
|
+
execSync12(`launchctl load ${plistPath2}`, { stdio: "inherit" });
|
|
12966
13273
|
} catch (err) {
|
|
12967
13274
|
throw new Error(
|
|
12968
13275
|
`launchd agent install failed: ${err?.message ?? String(err)}.`
|
|
@@ -13001,7 +13308,7 @@ async function nativeRunnerSetup(opts) {
|
|
|
13001
13308
|
}
|
|
13002
13309
|
const ghConfig = resolveGitHubConfig();
|
|
13003
13310
|
const repoSlug = opts.repo ?? resolveRepoSlug();
|
|
13004
|
-
const installDir =
|
|
13311
|
+
const installDir = join34(homedir4(), ".beastmode", "runners", opts.name);
|
|
13005
13312
|
if (opts.dryRun) {
|
|
13006
13313
|
info(
|
|
13007
13314
|
`[dry-run] Would install native runner '${opts.name}' for repo ${repoSlug}`
|
|
@@ -13011,7 +13318,7 @@ async function nativeRunnerSetup(opts) {
|
|
|
13011
13318
|
info(`[dry-run] Mode: ${opts.service ? "service" : "foreground"}`);
|
|
13012
13319
|
return;
|
|
13013
13320
|
}
|
|
13014
|
-
|
|
13321
|
+
mkdirSync21(installDir, { recursive: true });
|
|
13015
13322
|
setupStep("Generating registration token via GitHub API...");
|
|
13016
13323
|
const { token: regToken } = await createRegistrationToken(ghConfig);
|
|
13017
13324
|
await downloadAndExtractRunner(installDir, platformOs, arch);
|
|
@@ -13049,13 +13356,13 @@ async function nativeRunnerSetup(opts) {
|
|
|
13049
13356
|
|
|
13050
13357
|
// src/cli/workflow-switcher.ts
|
|
13051
13358
|
import {
|
|
13052
|
-
existsSync as
|
|
13053
|
-
mkdirSync as
|
|
13054
|
-
readFileSync as
|
|
13359
|
+
existsSync as existsSync36,
|
|
13360
|
+
mkdirSync as mkdirSync22,
|
|
13361
|
+
readFileSync as readFileSync33,
|
|
13055
13362
|
unlinkSync as unlinkSync6,
|
|
13056
|
-
writeFileSync as
|
|
13363
|
+
writeFileSync as writeFileSync29
|
|
13057
13364
|
} from "fs";
|
|
13058
|
-
import { dirname as dirname9, join as
|
|
13365
|
+
import { dirname as dirname9, join as join35 } from "path";
|
|
13059
13366
|
var TARGET_LABEL = "[self-hosted, beastmode]";
|
|
13060
13367
|
var TARGET_WORKFLOWS = [
|
|
13061
13368
|
".github/workflows/test.yml",
|
|
@@ -13094,8 +13401,8 @@ function restoreRunsOn(content, originals) {
|
|
|
13094
13401
|
return newLines.join("\n");
|
|
13095
13402
|
}
|
|
13096
13403
|
async function switchWorkflows(projectDir) {
|
|
13097
|
-
const statePath =
|
|
13098
|
-
if (
|
|
13404
|
+
const statePath = join35(projectDir, STATE_FILE);
|
|
13405
|
+
if (existsSync36(statePath)) {
|
|
13099
13406
|
return { alreadySwitched: true, files: [] };
|
|
13100
13407
|
}
|
|
13101
13408
|
const state = {
|
|
@@ -13105,40 +13412,40 @@ async function switchWorkflows(projectDir) {
|
|
|
13105
13412
|
};
|
|
13106
13413
|
const resultFiles = [];
|
|
13107
13414
|
for (const relPath of TARGET_WORKFLOWS) {
|
|
13108
|
-
const absPath =
|
|
13109
|
-
if (!
|
|
13415
|
+
const absPath = join35(projectDir, relPath);
|
|
13416
|
+
if (!existsSync36(absPath)) {
|
|
13110
13417
|
throw new Error(`Workflow file not found: ${relPath}`);
|
|
13111
13418
|
}
|
|
13112
|
-
const content =
|
|
13419
|
+
const content = readFileSync33(absPath, "utf-8");
|
|
13113
13420
|
const { newContent, originals } = replaceRunsOn(content, TARGET_LABEL);
|
|
13114
13421
|
if (originals.length === 0) {
|
|
13115
13422
|
throw new Error(`No runs-on found in ${relPath}`);
|
|
13116
13423
|
}
|
|
13117
|
-
|
|
13424
|
+
writeFileSync29(absPath, newContent, "utf-8");
|
|
13118
13425
|
state.files.push({ relativePath: relPath, originals });
|
|
13119
13426
|
resultFiles.push({ relativePath: relPath, jobCount: originals.length });
|
|
13120
13427
|
}
|
|
13121
|
-
|
|
13122
|
-
|
|
13428
|
+
mkdirSync22(dirname9(statePath), { recursive: true });
|
|
13429
|
+
writeFileSync29(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
13123
13430
|
return { alreadySwitched: false, files: resultFiles };
|
|
13124
13431
|
}
|
|
13125
13432
|
async function restoreWorkflows(projectDir) {
|
|
13126
|
-
const statePath =
|
|
13127
|
-
if (!
|
|
13433
|
+
const statePath = join35(projectDir, STATE_FILE);
|
|
13434
|
+
if (!existsSync36(statePath)) {
|
|
13128
13435
|
return { nothingToRestore: true, files: [] };
|
|
13129
13436
|
}
|
|
13130
13437
|
const state = JSON.parse(
|
|
13131
|
-
|
|
13438
|
+
readFileSync33(statePath, "utf-8")
|
|
13132
13439
|
);
|
|
13133
13440
|
const resultFiles = [];
|
|
13134
13441
|
for (const fileState of state.files) {
|
|
13135
|
-
const absPath =
|
|
13136
|
-
if (!
|
|
13442
|
+
const absPath = join35(projectDir, fileState.relativePath);
|
|
13443
|
+
if (!existsSync36(absPath)) {
|
|
13137
13444
|
throw new Error(`Workflow file not found: ${fileState.relativePath}`);
|
|
13138
13445
|
}
|
|
13139
|
-
const content =
|
|
13446
|
+
const content = readFileSync33(absPath, "utf-8");
|
|
13140
13447
|
const newContent = restoreRunsOn(content, fileState.originals);
|
|
13141
|
-
|
|
13448
|
+
writeFileSync29(absPath, newContent, "utf-8");
|
|
13142
13449
|
resultFiles.push({
|
|
13143
13450
|
relativePath: fileState.relativePath,
|
|
13144
13451
|
jobCount: fileState.originals.length
|
|
@@ -13150,12 +13457,38 @@ async function restoreWorkflows(projectDir) {
|
|
|
13150
13457
|
|
|
13151
13458
|
// src/cli/commands/runner-cmd.ts
|
|
13152
13459
|
var runnerCommand = new Command23("runner").description("Manage self-hosted GitHub Actions runners");
|
|
13460
|
+
function resolveProjectName(projectDir) {
|
|
13461
|
+
const projectsDir = join36(projectDir, ".beastmode", "projects");
|
|
13462
|
+
if (existsSync37(projectsDir)) {
|
|
13463
|
+
try {
|
|
13464
|
+
for (const entry of readdirSync12(projectsDir)) {
|
|
13465
|
+
if (!entry.endsWith(".json")) continue;
|
|
13466
|
+
const file = join36(projectsDir, entry);
|
|
13467
|
+
try {
|
|
13468
|
+
const raw = readFileSync34(file, "utf-8");
|
|
13469
|
+
const data = JSON.parse(raw);
|
|
13470
|
+
if (typeof data.path === "string" && resolve20(data.path) === resolve20(projectDir) && typeof data.name === "string" && data.name.length > 0) {
|
|
13471
|
+
return data.name;
|
|
13472
|
+
}
|
|
13473
|
+
} catch {
|
|
13474
|
+
}
|
|
13475
|
+
}
|
|
13476
|
+
} catch {
|
|
13477
|
+
}
|
|
13478
|
+
}
|
|
13479
|
+
return basename6(resolve20(projectDir));
|
|
13480
|
+
}
|
|
13153
13481
|
async function runnerSetupAction(opts) {
|
|
13154
13482
|
if (opts.service !== void 0 && !opts.native) {
|
|
13155
13483
|
throw new Error(
|
|
13156
13484
|
"--service requires --native. The Docker runner always runs as a container service."
|
|
13157
13485
|
);
|
|
13158
13486
|
}
|
|
13487
|
+
if (opts.buildImage && opts.native) {
|
|
13488
|
+
throw new Error(
|
|
13489
|
+
"Custom image builds require Docker mode. --build-image is incompatible with --native."
|
|
13490
|
+
);
|
|
13491
|
+
}
|
|
13159
13492
|
if (opts.native) {
|
|
13160
13493
|
await nativeRunnerSetup(opts);
|
|
13161
13494
|
return;
|
|
@@ -13196,6 +13529,46 @@ async function runnerSetupAction(opts) {
|
|
|
13196
13529
|
});
|
|
13197
13530
|
setupStep("Waiting for runner to appear online on GitHub (timeout 60s)...");
|
|
13198
13531
|
await pollUntilOnline(ghConfig, opts.name, 6e4);
|
|
13532
|
+
if (opts.buildImage) {
|
|
13533
|
+
try {
|
|
13534
|
+
const projectDir = resolve20(opts.projectDir ?? process.cwd());
|
|
13535
|
+
const stack = detectRunnerStack(projectDir);
|
|
13536
|
+
const lockfiles = findLockfiles(projectDir, stack.name);
|
|
13537
|
+
if (lockfiles.length === 0) {
|
|
13538
|
+
info(
|
|
13539
|
+
`No lockfiles detected for stack "${stack.name}" \u2014 skipping custom image build.`
|
|
13540
|
+
);
|
|
13541
|
+
} else {
|
|
13542
|
+
setupStep(
|
|
13543
|
+
`Building custom runner image (stack: ${stack.name}, lockfiles: ${lockfiles.join(", ")})...`
|
|
13544
|
+
);
|
|
13545
|
+
const projectName = resolveProjectName(projectDir);
|
|
13546
|
+
const result = buildRunnerImage({
|
|
13547
|
+
projectDir,
|
|
13548
|
+
projectName,
|
|
13549
|
+
stack,
|
|
13550
|
+
lockfiles
|
|
13551
|
+
});
|
|
13552
|
+
setupStep(
|
|
13553
|
+
`Restarting container '${opts.name}' on custom image ${result.imageTag}...`
|
|
13554
|
+
);
|
|
13555
|
+
await removeContainer(opts.name);
|
|
13556
|
+
await startRunnerContainer({
|
|
13557
|
+
name: opts.name,
|
|
13558
|
+
repoUrl,
|
|
13559
|
+
token: regToken,
|
|
13560
|
+
labels: ["self-hosted", opts.label],
|
|
13561
|
+
image: result.imageTag
|
|
13562
|
+
});
|
|
13563
|
+
await pollUntilOnline(ghConfig, opts.name, 6e4);
|
|
13564
|
+
success(`Runner now using custom image: ${result.imageTag}`);
|
|
13565
|
+
}
|
|
13566
|
+
} catch (err) {
|
|
13567
|
+
warn(
|
|
13568
|
+
`Custom image build failed \u2014 runner staying on base image. Reason: ${err?.message ? String(err.message) : String(err)}`
|
|
13569
|
+
);
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
13199
13572
|
setupStep("Adding runner to docker-compose...");
|
|
13200
13573
|
await composeUpRunner();
|
|
13201
13574
|
await writeRunnerMeta(".beastmode", {
|
|
@@ -13233,6 +13606,14 @@ runnerCommand.command("setup").description("Set up a self-hosted GitHub Actions
|
|
|
13233
13606
|
).option(
|
|
13234
13607
|
"--service",
|
|
13235
13608
|
"Install as background service (systemd/launchd). Requires --native."
|
|
13609
|
+
).option(
|
|
13610
|
+
"--build-image",
|
|
13611
|
+
"Build a custom runner image with pre-installed project dependencies",
|
|
13612
|
+
false
|
|
13613
|
+
).option(
|
|
13614
|
+
"--project-dir <path>",
|
|
13615
|
+
"Project root used to detect stack for --build-image",
|
|
13616
|
+
process.cwd()
|
|
13236
13617
|
).action(async (opts) => {
|
|
13237
13618
|
try {
|
|
13238
13619
|
await runnerSetupAction(opts);
|
|
@@ -13418,31 +13799,79 @@ runnerCommand.command("restore-workflows").description("Restore workflows to ori
|
|
|
13418
13799
|
process.exitCode = 1;
|
|
13419
13800
|
}
|
|
13420
13801
|
});
|
|
13802
|
+
async function runnerBuildImageAction(opts) {
|
|
13803
|
+
const projectDir = resolve20(opts.projectDir);
|
|
13804
|
+
if (!existsSync37(projectDir)) {
|
|
13805
|
+
throw new Error(`Project directory not found: ${projectDir}`);
|
|
13806
|
+
}
|
|
13807
|
+
const stack = detectRunnerStack(projectDir);
|
|
13808
|
+
const lockfiles = findLockfiles(projectDir, stack.name);
|
|
13809
|
+
if (lockfiles.length === 0) {
|
|
13810
|
+
info(
|
|
13811
|
+
`No lockfiles detected for stack "${stack.name}" \u2014 skipping dependency layer.`
|
|
13812
|
+
);
|
|
13813
|
+
return;
|
|
13814
|
+
}
|
|
13815
|
+
const projectName = resolveProjectName(projectDir);
|
|
13816
|
+
const result = buildRunnerImage({
|
|
13817
|
+
projectDir,
|
|
13818
|
+
projectName,
|
|
13819
|
+
stack,
|
|
13820
|
+
lockfiles,
|
|
13821
|
+
force: opts.force,
|
|
13822
|
+
dryRun: opts.dryRun
|
|
13823
|
+
});
|
|
13824
|
+
if (opts.dryRun) {
|
|
13825
|
+
header("Dry run: Dockerfile");
|
|
13826
|
+
process.stdout.write(result.dockerfile);
|
|
13827
|
+
info(`Image would be tagged: ${result.imageTag}`);
|
|
13828
|
+
return;
|
|
13829
|
+
}
|
|
13830
|
+
success(
|
|
13831
|
+
`Stack detected: ${stack.name} (lockfiles: ${lockfiles.join(", ")})`
|
|
13832
|
+
);
|
|
13833
|
+
if (result.built) {
|
|
13834
|
+
success(`Runner image built: ${result.imageTag}`);
|
|
13835
|
+
} else {
|
|
13836
|
+
info(`Runner image already up-to-date: ${result.imageTag}`);
|
|
13837
|
+
}
|
|
13838
|
+
info(`Lockfile hash: ${result.lockfileHash}`);
|
|
13839
|
+
}
|
|
13840
|
+
runnerCommand.command("build-image").description(
|
|
13841
|
+
"Build a custom runner image with pre-installed project dependencies"
|
|
13842
|
+
).option("--project-dir <path>", "Project root directory", process.cwd()).option("--force", "Rebuild even if lockfiles unchanged", false).option("--dry-run", "Print Dockerfile without building", false).action(async (opts) => {
|
|
13843
|
+
try {
|
|
13844
|
+
await runnerBuildImageAction(opts);
|
|
13845
|
+
} catch (err) {
|
|
13846
|
+
error(err?.message ? String(err.message) : String(err));
|
|
13847
|
+
process.exitCode = 1;
|
|
13848
|
+
}
|
|
13849
|
+
});
|
|
13421
13850
|
|
|
13422
13851
|
// src/cli/commands/project-cmd.ts
|
|
13423
13852
|
init_engine();
|
|
13424
13853
|
import { Command as Command24 } from "commander";
|
|
13425
|
-
import { existsSync as
|
|
13426
|
-
import { join as
|
|
13427
|
-
import { execSync as
|
|
13854
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync23, writeFileSync as writeFileSync30, readFileSync as readFileSync35, readdirSync as readdirSync13, renameSync as renameSync2 } from "fs";
|
|
13855
|
+
import { join as join37, resolve as resolve21, basename as basename7 } from "path";
|
|
13856
|
+
import { execSync as execSync13 } from "child_process";
|
|
13428
13857
|
var DEFAULT_MAX_PROJECTS = 5;
|
|
13429
13858
|
var MIN_DISK_WARNING_GB = 10;
|
|
13430
13859
|
function countExistingProjects(factoryDir) {
|
|
13431
|
-
const projectsDir =
|
|
13432
|
-
if (!
|
|
13860
|
+
const projectsDir = join37(factoryDir, ".beastmode", "projects");
|
|
13861
|
+
if (!existsSync38(projectsDir)) return 0;
|
|
13433
13862
|
let count = 0;
|
|
13434
|
-
for (const entry of
|
|
13863
|
+
for (const entry of readdirSync13(projectsDir)) {
|
|
13435
13864
|
if (entry.startsWith(".")) continue;
|
|
13436
|
-
if (
|
|
13865
|
+
if (existsSync38(join37(projectsDir, entry, "project.json"))) count++;
|
|
13437
13866
|
else if (entry.endsWith(".json")) count++;
|
|
13438
13867
|
}
|
|
13439
13868
|
return count;
|
|
13440
13869
|
}
|
|
13441
13870
|
function getMaxProjects(factoryDir) {
|
|
13442
|
-
const configPath =
|
|
13443
|
-
if (
|
|
13871
|
+
const configPath = join37(factoryDir, ".beastmode", "config.json");
|
|
13872
|
+
if (existsSync38(configPath)) {
|
|
13444
13873
|
try {
|
|
13445
|
-
const config = JSON.parse(
|
|
13874
|
+
const config = JSON.parse(readFileSync35(configPath, "utf-8"));
|
|
13446
13875
|
if (typeof config.max_projects === "number") return config.max_projects;
|
|
13447
13876
|
} catch {
|
|
13448
13877
|
}
|
|
@@ -13451,7 +13880,7 @@ function getMaxProjects(factoryDir) {
|
|
|
13451
13880
|
}
|
|
13452
13881
|
function getFreeDiskGB() {
|
|
13453
13882
|
try {
|
|
13454
|
-
const output =
|
|
13883
|
+
const output = execSync13("df -k / | tail -1", { encoding: "utf-8", timeout: 3e3 });
|
|
13455
13884
|
const parts = output.trim().split(/\s+/);
|
|
13456
13885
|
const availKB = parseInt(parts[3], 10);
|
|
13457
13886
|
if (!isNaN(availKB)) return Math.floor(availKB / (1024 * 1024));
|
|
@@ -13461,10 +13890,10 @@ function getFreeDiskGB() {
|
|
|
13461
13890
|
}
|
|
13462
13891
|
function projectAddAction(factoryDir, projectPath, opts) {
|
|
13463
13892
|
const resolvedPath = resolve21(projectPath);
|
|
13464
|
-
if (!
|
|
13465
|
-
const projectName = opts.name ||
|
|
13466
|
-
const projectsDir =
|
|
13467
|
-
if (
|
|
13893
|
+
if (!existsSync38(resolvedPath)) throw new Error(`Directory not found: ${resolvedPath}`);
|
|
13894
|
+
const projectName = opts.name || basename7(resolvedPath);
|
|
13895
|
+
const projectsDir = join37(factoryDir, ".beastmode", "projects", projectName);
|
|
13896
|
+
if (existsSync38(projectsDir)) throw new Error(`Project already exists: ${projectName}`);
|
|
13468
13897
|
const currentCount = countExistingProjects(factoryDir);
|
|
13469
13898
|
const maxProjects = getMaxProjects(factoryDir);
|
|
13470
13899
|
if (currentCount >= maxProjects) {
|
|
@@ -13478,22 +13907,22 @@ function projectAddAction(factoryDir, projectPath, opts) {
|
|
|
13478
13907
|
`Warning: only ${freeGB}GB free disk space. Each project with worktrees may use 1-2GB. Consider freeing space.`
|
|
13479
13908
|
);
|
|
13480
13909
|
}
|
|
13481
|
-
|
|
13910
|
+
mkdirSync23(projectsDir, { recursive: true });
|
|
13482
13911
|
let stack = {};
|
|
13483
13912
|
try {
|
|
13484
13913
|
stack = detectStack(resolvedPath);
|
|
13485
13914
|
} catch {
|
|
13486
13915
|
}
|
|
13487
13916
|
let verifyPort = 3001;
|
|
13488
|
-
const allProjectsDir =
|
|
13489
|
-
if (
|
|
13490
|
-
for (const d of
|
|
13917
|
+
const allProjectsDir = join37(factoryDir, ".beastmode", "projects");
|
|
13918
|
+
if (existsSync38(allProjectsDir)) {
|
|
13919
|
+
for (const d of readdirSync13(allProjectsDir)) {
|
|
13491
13920
|
if (d.startsWith(".")) continue;
|
|
13492
13921
|
if (d === projectName) continue;
|
|
13493
|
-
const pf =
|
|
13494
|
-
if (
|
|
13922
|
+
const pf = join37(allProjectsDir, d, "project.json");
|
|
13923
|
+
if (existsSync38(pf)) {
|
|
13495
13924
|
try {
|
|
13496
|
-
const pc = JSON.parse(
|
|
13925
|
+
const pc = JSON.parse(readFileSync35(pf, "utf-8"));
|
|
13497
13926
|
const port = pc?.deploy?.verify_port;
|
|
13498
13927
|
if (typeof port === "number" && port >= verifyPort) verifyPort = port + 1;
|
|
13499
13928
|
} catch {
|
|
@@ -13519,14 +13948,14 @@ function projectAddAction(factoryDir, projectPath, opts) {
|
|
|
13519
13948
|
slots: { max: null },
|
|
13520
13949
|
registered_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
13521
13950
|
};
|
|
13522
|
-
|
|
13523
|
-
|
|
13951
|
+
writeFileSync30(join37(projectsDir, "project.json"), JSON.stringify(projectConfig, null, 2) + "\n");
|
|
13952
|
+
writeFileSync30(join37(projectsDir, "extensions.json"), JSON.stringify({
|
|
13524
13953
|
plugins: { add: [], remove: [] },
|
|
13525
13954
|
mcps: { add: {}, remove: [] },
|
|
13526
13955
|
skills: { add: [], remove: [] }
|
|
13527
13956
|
}, null, 2) + "\n");
|
|
13528
|
-
const runsDir =
|
|
13529
|
-
if (!
|
|
13957
|
+
const runsDir = join37(factoryDir, "runs", projectName);
|
|
13958
|
+
if (!existsSync38(runsDir)) mkdirSync23(runsDir, { recursive: true });
|
|
13530
13959
|
if (!opts.boardId) {
|
|
13531
13960
|
void (async () => {
|
|
13532
13961
|
try {
|
|
@@ -13566,28 +13995,28 @@ function projectAddAction(factoryDir, projectPath, opts) {
|
|
|
13566
13995
|
}
|
|
13567
13996
|
}
|
|
13568
13997
|
function projectListAction(factoryDir) {
|
|
13569
|
-
const projectsDir =
|
|
13570
|
-
if (!
|
|
13571
|
-
return
|
|
13998
|
+
const projectsDir = join37(factoryDir, ".beastmode", "projects");
|
|
13999
|
+
if (!existsSync38(projectsDir)) return [];
|
|
14000
|
+
return readdirSync13(projectsDir).filter((d) => !d.startsWith(".") && existsSync38(join37(projectsDir, d, "project.json"))).map((d) => {
|
|
13572
14001
|
try {
|
|
13573
|
-
return JSON.parse(
|
|
14002
|
+
return JSON.parse(readFileSync35(join37(projectsDir, d, "project.json"), "utf-8"));
|
|
13574
14003
|
} catch {
|
|
13575
14004
|
return null;
|
|
13576
14005
|
}
|
|
13577
14006
|
}).filter(Boolean);
|
|
13578
14007
|
}
|
|
13579
14008
|
function projectRemoveAction(factoryDir, name) {
|
|
13580
|
-
const projectDir =
|
|
13581
|
-
if (!
|
|
13582
|
-
const archiveDir =
|
|
13583
|
-
|
|
14009
|
+
const projectDir = join37(factoryDir, ".beastmode", "projects", name);
|
|
14010
|
+
if (!existsSync38(projectDir)) throw new Error(`Project not found: ${name}`);
|
|
14011
|
+
const archiveDir = join37(factoryDir, ".beastmode", "projects", ".archived", name);
|
|
14012
|
+
mkdirSync23(join37(factoryDir, ".beastmode", "projects", ".archived"), { recursive: true });
|
|
13584
14013
|
renameSync2(projectDir, archiveDir);
|
|
13585
14014
|
}
|
|
13586
14015
|
var projectCommand = new Command24("project").description("Manage projects in this factory");
|
|
13587
14016
|
projectCommand.command("add <path>").description("Register a project").option("--name <name>", "Override project name").option("--board-id <id>", "Link to existing board ID").action((path, opts) => {
|
|
13588
14017
|
const factoryDir = resolve21(".");
|
|
13589
14018
|
projectAddAction(factoryDir, path, opts);
|
|
13590
|
-
console.log(`Project registered: ${opts.name ||
|
|
14019
|
+
console.log(`Project registered: ${opts.name || basename7(resolve21(path))}`);
|
|
13591
14020
|
});
|
|
13592
14021
|
projectCommand.command("list").description("List registered projects").action(() => {
|
|
13593
14022
|
const projects = projectListAction(resolve21("."));
|