@beastmode-develeap/beastmode 0.1.198 → 0.1.199

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
@@ -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: readFileSync33 } = await import("fs");
8494
- const templateContent = readFileSync33(resolve6(opts.from), "utf-8");
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: readdirSync13 } = await import("fs");
11427
- runDirs = readdirSync13(runsDir).filter((d) => {
11426
+ const { readdirSync: readdirSync14 } = await import("fs");
11427
+ runDirs = readdirSync14(runsDir).filter((d) => {
11428
11428
  try {
11429
- return readdirSync13(join26(runsDir, d)).length > 0;
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: execSync13 } = await import("child_process");
11448
- worktreeOutput = execSync13("git worktree list", {
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: readdirSync13 } = await import("fs");
11572
- const projectFiles = readdirSync13(projectsDir).filter(
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: execSync13 } = await import("child_process");
11621
+ const { execSync: execSync14 } = await import("child_process");
11622
11622
  let pythonAvailable = false;
11623
11623
  try {
11624
- execSync13("python --version", { timeout: 5e3, encoding: "utf-8" });
11624
+ execSync14("python --version", { timeout: 5e3, encoding: "utf-8" });
11625
11625
  pythonAvailable = true;
11626
11626
  } catch {
11627
11627
  try {
11628
- execSync13("python3 --version", { timeout: 5e3, encoding: "utf-8" });
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: readdirSync13 } = await import("fs");
11750
- const projectFiles = readdirSync13(projectsDir).filter(
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: execSync13 } = await import("child_process");
11781
+ const { execSync: execSync14 } = await import("child_process");
11782
11782
  let pythonCmd = "python";
11783
11783
  let pythonAvailable = false;
11784
11784
  try {
11785
- execSync13("python --version", { timeout: 5e3, encoding: "utf-8" });
11785
+ execSync14("python --version", { timeout: 5e3, encoding: "utf-8" });
11786
11786
  pythonAvailable = true;
11787
11787
  pythonCmd = "python";
11788
11788
  } catch {
11789
11789
  try {
11790
- execSync13("python3 --version", { timeout: 5e3, encoding: "utf-8" });
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 { resolve as resolve20 } from "path";
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 execSync10, spawn, spawnSync as spawnSync5 } from "child_process";
12847
+ import { execSync as execSync11, spawn, spawnSync as spawnSync5 } from "child_process";
12542
12848
  import { promises as fs } from "fs";
12543
- import { join as join31 } from "path";
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 = join31(dir, "runner-meta.json");
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 = execSync10("git remote get-url origin", {
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
- "myoung34/github-runner:latest"
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 execSync11,
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 existsSync33,
12748
- mkdirSync as mkdirSync20,
13054
+ existsSync as existsSync35,
13055
+ mkdirSync as mkdirSync21,
12749
13056
  unlinkSync as unlinkSync5,
12750
- writeFileSync as writeFileSync27
13057
+ writeFileSync as writeFileSync28
12751
13058
  } from "fs";
12752
13059
  import { homedir as homedir4 } from "os";
12753
- import { join as join32, dirname as dirname8 } from "path";
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 = join32(installDir, "run.sh");
12784
- if (existsSync33(runShPath)) {
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 = join32(installDir, "runner.tar.gz");
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
- execSync11("tar -xzf runner.tar.gz", { cwd: installDir, stdio: "ignore" });
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(join32(installDir, "config.sh"), args, {
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(join32(installDir, "run.sh"), [], {
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 = join32(homedir4(), ".config", "systemd", "user");
12899
- mkdirSync20(unitDir, { recursive: true });
12900
- writeFileSync27(
12901
- join32(unitDir, unitName),
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
- execSync11("systemctl --user daemon-reload", { stdio: "inherit" });
12907
- execSync11(`systemctl --user enable --now ${unitName}`, {
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 = join32(
13262
+ const plistPath2 = join34(
12956
13263
  homedir4(),
12957
13264
  "Library",
12958
13265
  "LaunchAgents",
12959
13266
  `${label}.plist`
12960
13267
  );
12961
- mkdirSync20(dirname8(plistPath2), { recursive: true });
12962
- mkdirSync20(join32(installDir, "logs"), { recursive: true });
12963
- writeFileSync27(plistPath2, launchdPlistContent(installDir, name), "utf-8");
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
- execSync11(`launchctl load ${plistPath2}`, { stdio: "inherit" });
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 = join32(homedir4(), ".beastmode", "runners", opts.name);
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
- mkdirSync20(installDir, { recursive: true });
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 existsSync34,
13053
- mkdirSync as mkdirSync21,
13054
- readFileSync as readFileSync31,
13359
+ existsSync as existsSync36,
13360
+ mkdirSync as mkdirSync22,
13361
+ readFileSync as readFileSync33,
13055
13362
  unlinkSync as unlinkSync6,
13056
- writeFileSync as writeFileSync28
13363
+ writeFileSync as writeFileSync29
13057
13364
  } from "fs";
13058
- import { dirname as dirname9, join as join33 } from "path";
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 = join33(projectDir, STATE_FILE);
13098
- if (existsSync34(statePath)) {
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 = join33(projectDir, relPath);
13109
- if (!existsSync34(absPath)) {
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 = readFileSync31(absPath, "utf-8");
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
- writeFileSync28(absPath, newContent, "utf-8");
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
- mkdirSync21(dirname9(statePath), { recursive: true });
13122
- writeFileSync28(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
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 = join33(projectDir, STATE_FILE);
13127
- if (!existsSync34(statePath)) {
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
- readFileSync31(statePath, "utf-8")
13438
+ readFileSync33(statePath, "utf-8")
13132
13439
  );
13133
13440
  const resultFiles = [];
13134
13441
  for (const fileState of state.files) {
13135
- const absPath = join33(projectDir, fileState.relativePath);
13136
- if (!existsSync34(absPath)) {
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 = readFileSync31(absPath, "utf-8");
13446
+ const content = readFileSync33(absPath, "utf-8");
13140
13447
  const newContent = restoreRunsOn(content, fileState.originals);
13141
- writeFileSync28(absPath, newContent, "utf-8");
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 existsSync35, mkdirSync as mkdirSync22, writeFileSync as writeFileSync29, readFileSync as readFileSync32, readdirSync as readdirSync12, renameSync as renameSync2 } from "fs";
13426
- import { join as join34, resolve as resolve21, basename as basename6 } from "path";
13427
- import { execSync as execSync12 } from "child_process";
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 = join34(factoryDir, ".beastmode", "projects");
13432
- if (!existsSync35(projectsDir)) return 0;
13860
+ const projectsDir = join37(factoryDir, ".beastmode", "projects");
13861
+ if (!existsSync38(projectsDir)) return 0;
13433
13862
  let count = 0;
13434
- for (const entry of readdirSync12(projectsDir)) {
13863
+ for (const entry of readdirSync13(projectsDir)) {
13435
13864
  if (entry.startsWith(".")) continue;
13436
- if (existsSync35(join34(projectsDir, entry, "project.json"))) count++;
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 = join34(factoryDir, ".beastmode", "config.json");
13443
- if (existsSync35(configPath)) {
13871
+ const configPath = join37(factoryDir, ".beastmode", "config.json");
13872
+ if (existsSync38(configPath)) {
13444
13873
  try {
13445
- const config = JSON.parse(readFileSync32(configPath, "utf-8"));
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 = execSync12("df -k / | tail -1", { encoding: "utf-8", timeout: 3e3 });
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 (!existsSync35(resolvedPath)) throw new Error(`Directory not found: ${resolvedPath}`);
13465
- const projectName = opts.name || basename6(resolvedPath);
13466
- const projectsDir = join34(factoryDir, ".beastmode", "projects", projectName);
13467
- if (existsSync35(projectsDir)) throw new Error(`Project already exists: ${projectName}`);
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
- mkdirSync22(projectsDir, { recursive: true });
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 = join34(factoryDir, ".beastmode", "projects");
13489
- if (existsSync35(allProjectsDir)) {
13490
- for (const d of readdirSync12(allProjectsDir)) {
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 = join34(allProjectsDir, d, "project.json");
13494
- if (existsSync35(pf)) {
13922
+ const pf = join37(allProjectsDir, d, "project.json");
13923
+ if (existsSync38(pf)) {
13495
13924
  try {
13496
- const pc = JSON.parse(readFileSync32(pf, "utf-8"));
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
- writeFileSync29(join34(projectsDir, "project.json"), JSON.stringify(projectConfig, null, 2) + "\n");
13523
- writeFileSync29(join34(projectsDir, "extensions.json"), JSON.stringify({
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 = join34(factoryDir, "runs", projectName);
13529
- if (!existsSync35(runsDir)) mkdirSync22(runsDir, { recursive: true });
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 = join34(factoryDir, ".beastmode", "projects");
13570
- if (!existsSync35(projectsDir)) return [];
13571
- return readdirSync12(projectsDir).filter((d) => !d.startsWith(".") && existsSync35(join34(projectsDir, d, "project.json"))).map((d) => {
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(readFileSync32(join34(projectsDir, d, "project.json"), "utf-8"));
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 = join34(factoryDir, ".beastmode", "projects", name);
13581
- if (!existsSync35(projectDir)) throw new Error(`Project not found: ${name}`);
13582
- const archiveDir = join34(factoryDir, ".beastmode", "projects", ".archived", name);
13583
- mkdirSync22(join34(factoryDir, ".beastmode", "projects", ".archived"), { recursive: true });
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 || basename6(resolve21(path))}`);
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("."));