@beastmode-develeap/beastmode 0.1.33 → 0.1.35
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 +355 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7250,7 +7250,13 @@ function generateComposeYaml(tag) {
|
|
|
7250
7250
|
- ./runs:/app/runs
|
|
7251
7251
|
- ./daemon/logs:/app/daemon/logs
|
|
7252
7252
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
7253
|
-
|
|
7253
|
+
# PROJECT_DIR is REQUIRED \u2014 the daemon targets its git remote for
|
|
7254
|
+
# all merge/push/PR operations. Writing without a fallback so
|
|
7255
|
+
# 'docker compose up' fails loudly if the user has deleted the
|
|
7256
|
+
# PROJECT_DIR line from .env instead of silently mounting the
|
|
7257
|
+
# beatmode runtime dir (which has no source code). See
|
|
7258
|
+
# docs/zero-to-productive-readiness.md Gap 1.
|
|
7259
|
+
- \${PROJECT_DIR:?PROJECT_DIR must be set in .env \u2014 run 'beastmode init' to regenerate}:/app/project
|
|
7254
7260
|
- \${HOME}/.claude:/home/appuser/.claude
|
|
7255
7261
|
depends_on:
|
|
7256
7262
|
board:
|
|
@@ -7632,6 +7638,44 @@ async function runImageModeInit(name, opts) {
|
|
|
7632
7638
|
} else if (uiPassword) {
|
|
7633
7639
|
success("Board UI password set");
|
|
7634
7640
|
}
|
|
7641
|
+
let projectPath;
|
|
7642
|
+
if (opts.project) {
|
|
7643
|
+
projectPath = resolve5(opts.project);
|
|
7644
|
+
if (!existsSync17(projectPath)) {
|
|
7645
|
+
error(`--project path does not exist: ${projectPath}`);
|
|
7646
|
+
process.exit(1);
|
|
7647
|
+
}
|
|
7648
|
+
success(`Project path: ${projectPath}`);
|
|
7649
|
+
} else if (opts.yes) {
|
|
7650
|
+
const cwd = resolve5(".");
|
|
7651
|
+
if (!existsSync17(join15(cwd, ".git"))) {
|
|
7652
|
+
error(
|
|
7653
|
+
"--project is required in non-interactive mode (--yes) unless you run from inside a git repository. Pass --project <path> or cd into your project clone first."
|
|
7654
|
+
);
|
|
7655
|
+
process.exit(1);
|
|
7656
|
+
}
|
|
7657
|
+
projectPath = cwd;
|
|
7658
|
+
info(`No --project specified, using current directory: ${projectPath}`);
|
|
7659
|
+
} else {
|
|
7660
|
+
const answer = await inquirer.prompt([
|
|
7661
|
+
{
|
|
7662
|
+
type: "input",
|
|
7663
|
+
name: "project",
|
|
7664
|
+
message: "Path to your project (must be a git clone with an origin remote):",
|
|
7665
|
+
default: ".",
|
|
7666
|
+
validate: (input) => {
|
|
7667
|
+
const p = resolve5(input);
|
|
7668
|
+
if (!existsSync17(p)) return `Directory not found: ${p}`;
|
|
7669
|
+
if (!existsSync17(join15(p, ".git"))) {
|
|
7670
|
+
return `Not a git repo: ${p} (run 'git init' first, or pick a different path)`;
|
|
7671
|
+
}
|
|
7672
|
+
return true;
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
]);
|
|
7676
|
+
projectPath = resolve5(answer.project);
|
|
7677
|
+
success(`Project path: ${projectPath}`);
|
|
7678
|
+
}
|
|
7635
7679
|
step(2, totalSteps, "Docker Registry");
|
|
7636
7680
|
if (!isGhcrAuthenticated()) {
|
|
7637
7681
|
if (githubToken && loginToGhcr(githubToken)) {
|
|
@@ -7666,14 +7710,21 @@ async function runImageModeInit(name, opts) {
|
|
|
7666
7710
|
"# BeastMode environment \u2014 DO NOT COMMIT",
|
|
7667
7711
|
`GITHUB_TOKEN=${githubToken}`,
|
|
7668
7712
|
`BEASTMODE_UI_PASSWORD=${uiPassword}`,
|
|
7713
|
+
"",
|
|
7714
|
+
"# Project repo path \u2014 the daemon's git operations target the git remote of this path.",
|
|
7715
|
+
`PROJECT_DIR=${projectPath}`,
|
|
7716
|
+
"",
|
|
7669
7717
|
"# Optional: uncomment for faster direct API calls",
|
|
7670
7718
|
"# ANTHROPIC_API_KEY=sk-ant-...",
|
|
7671
|
-
"
|
|
7672
|
-
"#
|
|
7719
|
+
"",
|
|
7720
|
+
"# Optional: last-resort override if the daemon's auto-resolution",
|
|
7721
|
+
"# (git remote get-url origin) doesn't work for your setup.",
|
|
7722
|
+
"# The daemon handles this automatically in >99% of cases.",
|
|
7723
|
+
"# PROJECT_REPO=owner/repo",
|
|
7673
7724
|
""
|
|
7674
7725
|
];
|
|
7675
7726
|
writeFileSync13(join15(targetDir, ".env"), envLines.join("\n"), "utf-8");
|
|
7676
|
-
success(
|
|
7727
|
+
success(`.env (PROJECT_DIR=${projectPath})`);
|
|
7677
7728
|
for (const dir of ["data", "runs", "daemon/logs", ".beastmode", "config"]) {
|
|
7678
7729
|
mkdirSync12(join15(targetDir, dir), { recursive: true });
|
|
7679
7730
|
}
|
|
@@ -8596,6 +8647,301 @@ function checkDocker() {
|
|
|
8596
8647
|
fix: "Install Docker Desktop or start the Docker daemon"
|
|
8597
8648
|
};
|
|
8598
8649
|
}
|
|
8650
|
+
function checkDockerComposeVersion() {
|
|
8651
|
+
const v2 = tryExec("docker compose version --short 2>/dev/null");
|
|
8652
|
+
if (v2) {
|
|
8653
|
+
const major = parseInt(v2.split(".")[0], 10);
|
|
8654
|
+
if (!isNaN(major) && major >= 2) {
|
|
8655
|
+
return {
|
|
8656
|
+
label: "Docker Compose",
|
|
8657
|
+
status: "pass",
|
|
8658
|
+
detail: `v${v2} (plugin)`
|
|
8659
|
+
};
|
|
8660
|
+
}
|
|
8661
|
+
return {
|
|
8662
|
+
label: "Docker Compose",
|
|
8663
|
+
status: "fail",
|
|
8664
|
+
detail: `v${v2} \u2014 need v2.0 or newer`,
|
|
8665
|
+
fix: "Update Docker Desktop to the latest version"
|
|
8666
|
+
};
|
|
8667
|
+
}
|
|
8668
|
+
const v1 = tryExec("docker-compose version --short 2>/dev/null");
|
|
8669
|
+
if (v1) {
|
|
8670
|
+
return {
|
|
8671
|
+
label: "Docker Compose",
|
|
8672
|
+
status: "fail",
|
|
8673
|
+
detail: `legacy v${v1} \u2014 beastmode requires v2.0+ (plugin form)`,
|
|
8674
|
+
fix: "Install Docker Compose v2 (bundled with modern Docker Desktop)"
|
|
8675
|
+
};
|
|
8676
|
+
}
|
|
8677
|
+
return {
|
|
8678
|
+
label: "Docker Compose",
|
|
8679
|
+
status: "fail",
|
|
8680
|
+
detail: "not installed",
|
|
8681
|
+
fix: "Install Docker Compose v2 (bundled with Docker Desktop)"
|
|
8682
|
+
};
|
|
8683
|
+
}
|
|
8684
|
+
function checkProjectDirEnv(factoryDir) {
|
|
8685
|
+
if (!factoryDir) {
|
|
8686
|
+
return {
|
|
8687
|
+
label: "PROJECT_DIR (.env)",
|
|
8688
|
+
status: "warn",
|
|
8689
|
+
detail: "no factory in scope"
|
|
8690
|
+
};
|
|
8691
|
+
}
|
|
8692
|
+
const envPath = join22(factoryDir, ".env");
|
|
8693
|
+
if (!existsSync24(envPath)) {
|
|
8694
|
+
return {
|
|
8695
|
+
label: "PROJECT_DIR (.env)",
|
|
8696
|
+
status: "fail",
|
|
8697
|
+
detail: `.env not found at ${envPath}`,
|
|
8698
|
+
fix: "Run `beastmode init` in this directory to generate .env"
|
|
8699
|
+
};
|
|
8700
|
+
}
|
|
8701
|
+
let content;
|
|
8702
|
+
try {
|
|
8703
|
+
content = readFileSync21(envPath, "utf-8");
|
|
8704
|
+
} catch {
|
|
8705
|
+
return {
|
|
8706
|
+
label: "PROJECT_DIR (.env)",
|
|
8707
|
+
status: "fail",
|
|
8708
|
+
detail: ".env unreadable"
|
|
8709
|
+
};
|
|
8710
|
+
}
|
|
8711
|
+
const lines = content.split("\n");
|
|
8712
|
+
const line = lines.find(
|
|
8713
|
+
(l) => l.trim().startsWith("PROJECT_DIR=") && !l.trim().startsWith("#")
|
|
8714
|
+
);
|
|
8715
|
+
if (!line) {
|
|
8716
|
+
return {
|
|
8717
|
+
label: "PROJECT_DIR (.env)",
|
|
8718
|
+
status: "fail",
|
|
8719
|
+
detail: "PROJECT_DIR not set (or commented out)",
|
|
8720
|
+
fix: "Re-run `beastmode init --project <path>` to populate it"
|
|
8721
|
+
};
|
|
8722
|
+
}
|
|
8723
|
+
const value = line.split("=").slice(1).join("=").trim();
|
|
8724
|
+
if (!value) {
|
|
8725
|
+
return {
|
|
8726
|
+
label: "PROJECT_DIR (.env)",
|
|
8727
|
+
status: "fail",
|
|
8728
|
+
detail: "PROJECT_DIR is empty",
|
|
8729
|
+
fix: "Re-run `beastmode init --project <path>` to populate it"
|
|
8730
|
+
};
|
|
8731
|
+
}
|
|
8732
|
+
if (!existsSync24(value)) {
|
|
8733
|
+
return {
|
|
8734
|
+
label: "PROJECT_DIR (.env)",
|
|
8735
|
+
status: "fail",
|
|
8736
|
+
detail: `PROJECT_DIR=${value} does not exist`,
|
|
8737
|
+
fix: `Clone your project to ${value} or re-run 'beastmode init --project <path>'`
|
|
8738
|
+
};
|
|
8739
|
+
}
|
|
8740
|
+
if (!existsSync24(join22(value, ".git"))) {
|
|
8741
|
+
return {
|
|
8742
|
+
label: "PROJECT_DIR (.env)",
|
|
8743
|
+
status: "fail",
|
|
8744
|
+
detail: `PROJECT_DIR=${value} is not a git repo`,
|
|
8745
|
+
fix: "Run `git init` in the project dir, or point PROJECT_DIR at an existing git clone"
|
|
8746
|
+
};
|
|
8747
|
+
}
|
|
8748
|
+
const remote = tryExec(`git -C "${value}" remote get-url origin 2>/dev/null`);
|
|
8749
|
+
if (!remote) {
|
|
8750
|
+
return {
|
|
8751
|
+
label: "PROJECT_DIR (.env)",
|
|
8752
|
+
status: "warn",
|
|
8753
|
+
detail: `${value} has no origin remote \u2014 daemon can't auto-resolve github repo (Gap 2)`,
|
|
8754
|
+
fix: `cd ${value} && git remote add origin <url>, or set PROJECT_REPO in .env as a last-resort override`
|
|
8755
|
+
};
|
|
8756
|
+
}
|
|
8757
|
+
return {
|
|
8758
|
+
label: "PROJECT_DIR (.env)",
|
|
8759
|
+
status: "pass",
|
|
8760
|
+
detail: `${value} (origin: ${remote})`
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
async function checkFactoryContainers(factoryDir) {
|
|
8764
|
+
if (!factoryDir) {
|
|
8765
|
+
return {
|
|
8766
|
+
label: "Factory containers",
|
|
8767
|
+
status: "warn",
|
|
8768
|
+
detail: "no factory in scope"
|
|
8769
|
+
};
|
|
8770
|
+
}
|
|
8771
|
+
const composePath = join22(factoryDir, "docker-compose.yml");
|
|
8772
|
+
if (!existsSync24(composePath)) {
|
|
8773
|
+
return {
|
|
8774
|
+
label: "Factory containers",
|
|
8775
|
+
status: "warn",
|
|
8776
|
+
detail: "no docker-compose.yml in factory",
|
|
8777
|
+
fix: "Run `beastmode init` to generate one"
|
|
8778
|
+
};
|
|
8779
|
+
}
|
|
8780
|
+
const out = tryExec(
|
|
8781
|
+
`docker compose -f "${composePath}" --project-directory "${factoryDir}" ps --format json 2>/dev/null`,
|
|
8782
|
+
15e3
|
|
8783
|
+
);
|
|
8784
|
+
if (out === null) {
|
|
8785
|
+
return {
|
|
8786
|
+
label: "Factory containers",
|
|
8787
|
+
status: "warn",
|
|
8788
|
+
detail: "docker compose ps failed \u2014 factory may be down",
|
|
8789
|
+
fix: "Run `docker compose up -d` in the factory directory"
|
|
8790
|
+
};
|
|
8791
|
+
}
|
|
8792
|
+
if (!out.trim()) {
|
|
8793
|
+
return {
|
|
8794
|
+
label: "Factory containers",
|
|
8795
|
+
status: "warn",
|
|
8796
|
+
detail: "no containers running",
|
|
8797
|
+
fix: "Run `docker compose up -d` in the factory directory"
|
|
8798
|
+
};
|
|
8799
|
+
}
|
|
8800
|
+
const rows = [];
|
|
8801
|
+
for (const line of out.split("\n")) {
|
|
8802
|
+
const t = line.trim();
|
|
8803
|
+
if (!t) continue;
|
|
8804
|
+
try {
|
|
8805
|
+
const parsed = JSON.parse(t);
|
|
8806
|
+
if (Array.isArray(parsed)) {
|
|
8807
|
+
for (const p of parsed) rows.push(p);
|
|
8808
|
+
} else {
|
|
8809
|
+
rows.push(parsed);
|
|
8810
|
+
}
|
|
8811
|
+
} catch {
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
if (rows.length === 0) {
|
|
8815
|
+
return {
|
|
8816
|
+
label: "Factory containers",
|
|
8817
|
+
status: "warn",
|
|
8818
|
+
detail: "docker compose ps output not parseable"
|
|
8819
|
+
};
|
|
8820
|
+
}
|
|
8821
|
+
const required = ["board", "ui", "daemon"];
|
|
8822
|
+
const byService = /* @__PURE__ */ new Map();
|
|
8823
|
+
for (const r of rows) {
|
|
8824
|
+
if (r.Service) {
|
|
8825
|
+
byService.set(r.Service, {
|
|
8826
|
+
state: r.State,
|
|
8827
|
+
health: r.Health,
|
|
8828
|
+
name: r.Name
|
|
8829
|
+
});
|
|
8830
|
+
}
|
|
8831
|
+
}
|
|
8832
|
+
const missing = required.filter((s) => !byService.has(s));
|
|
8833
|
+
if (missing.length > 0) {
|
|
8834
|
+
return {
|
|
8835
|
+
label: "Factory containers",
|
|
8836
|
+
status: "fail",
|
|
8837
|
+
detail: `missing services: ${missing.join(", ")}`,
|
|
8838
|
+
fix: "Run `docker compose up -d` in the factory directory"
|
|
8839
|
+
};
|
|
8840
|
+
}
|
|
8841
|
+
const unhealthy = [];
|
|
8842
|
+
for (const svc of required) {
|
|
8843
|
+
const info5 = byService.get(svc);
|
|
8844
|
+
if (!info5) continue;
|
|
8845
|
+
if (info5.state !== "running") {
|
|
8846
|
+
unhealthy.push(`${svc}(${info5.state})`);
|
|
8847
|
+
} else if (info5.health && info5.health !== "healthy" && info5.health !== "") {
|
|
8848
|
+
unhealthy.push(`${svc}(${info5.health})`);
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
if (unhealthy.length > 0) {
|
|
8852
|
+
return {
|
|
8853
|
+
label: "Factory containers",
|
|
8854
|
+
status: "fail",
|
|
8855
|
+
detail: `unhealthy: ${unhealthy.join(", ")}`,
|
|
8856
|
+
fix: "Check `docker compose logs <service>` for the unhealthy container"
|
|
8857
|
+
};
|
|
8858
|
+
}
|
|
8859
|
+
return {
|
|
8860
|
+
label: "Factory containers",
|
|
8861
|
+
status: "pass",
|
|
8862
|
+
detail: `${required.join(", ")} running & healthy`
|
|
8863
|
+
};
|
|
8864
|
+
}
|
|
8865
|
+
async function checkUiServesBoard() {
|
|
8866
|
+
const ports = [8420, 8080, 3e3];
|
|
8867
|
+
for (const port of ports) {
|
|
8868
|
+
const code = await httpGet(`http://127.0.0.1:${port}/board`, 3e3);
|
|
8869
|
+
if (code !== null && code >= 200 && code < 400) {
|
|
8870
|
+
return {
|
|
8871
|
+
label: "UI /board endpoint",
|
|
8872
|
+
status: "pass",
|
|
8873
|
+
detail: `http://127.0.0.1:${port}/board \u2192 ${code}`
|
|
8874
|
+
};
|
|
8875
|
+
}
|
|
8876
|
+
}
|
|
8877
|
+
return {
|
|
8878
|
+
label: "UI /board endpoint",
|
|
8879
|
+
status: "warn",
|
|
8880
|
+
detail: "no UI server responding to /board on 8420, 8080, or 3000",
|
|
8881
|
+
fix: "Check `docker compose logs ui` \u2014 the Node process may have crashed after startup"
|
|
8882
|
+
};
|
|
8883
|
+
}
|
|
8884
|
+
function checkDaemonConfigLoad() {
|
|
8885
|
+
const container = tryExec(
|
|
8886
|
+
`docker ps --filter 'label=com.docker.compose.service=daemon' --format '{{.Names}}' 2>/dev/null | head -n1`
|
|
8887
|
+
);
|
|
8888
|
+
if (!container) {
|
|
8889
|
+
return {
|
|
8890
|
+
label: "Daemon config",
|
|
8891
|
+
status: "warn",
|
|
8892
|
+
detail: "no daemon container running",
|
|
8893
|
+
fix: "Run `docker compose up -d` in the factory directory"
|
|
8894
|
+
};
|
|
8895
|
+
}
|
|
8896
|
+
const pyScript = `
|
|
8897
|
+
from beastmode_daemon.config import DaemonConfig
|
|
8898
|
+
try:
|
|
8899
|
+
c = DaemonConfig.load()
|
|
8900
|
+
errors = c.validate_for_startup()
|
|
8901
|
+
print('REPO=' + (c.github.project_repo or ''))
|
|
8902
|
+
print('DIR=' + str(c.project_dir or ''))
|
|
8903
|
+
print('ERRORS=' + ('; '.join(errors) if errors else 'none'))
|
|
8904
|
+
except Exception as e:
|
|
8905
|
+
print('EXC=' + type(e).__name__ + ': ' + str(e))
|
|
8906
|
+
`.trim();
|
|
8907
|
+
const out = tryExec(
|
|
8908
|
+
`docker exec ${container} python -c "${pyScript.replace(/"/g, '\\"')}" 2>/dev/null`,
|
|
8909
|
+
12e3
|
|
8910
|
+
);
|
|
8911
|
+
if (!out) {
|
|
8912
|
+
return {
|
|
8913
|
+
label: "Daemon config",
|
|
8914
|
+
status: "warn",
|
|
8915
|
+
detail: `could not execute python inside ${container}`
|
|
8916
|
+
};
|
|
8917
|
+
}
|
|
8918
|
+
const lines = out.split("\n");
|
|
8919
|
+
const repo = lines.find((l) => l.startsWith("REPO="))?.slice(5) ?? "";
|
|
8920
|
+
const dir = lines.find((l) => l.startsWith("DIR="))?.slice(4) ?? "";
|
|
8921
|
+
const errors = lines.find((l) => l.startsWith("ERRORS="))?.slice(7) ?? "";
|
|
8922
|
+
const exc = lines.find((l) => l.startsWith("EXC="))?.slice(4) ?? "";
|
|
8923
|
+
if (exc) {
|
|
8924
|
+
return {
|
|
8925
|
+
label: "Daemon config",
|
|
8926
|
+
status: "fail",
|
|
8927
|
+
detail: `exception: ${exc}`,
|
|
8928
|
+
fix: "Check docker logs + daemon/beastmode_daemon/config.py"
|
|
8929
|
+
};
|
|
8930
|
+
}
|
|
8931
|
+
if (errors && errors !== "none") {
|
|
8932
|
+
return {
|
|
8933
|
+
label: "Daemon config",
|
|
8934
|
+
status: "fail",
|
|
8935
|
+
detail: errors,
|
|
8936
|
+
fix: "See docs/zero-to-productive-readiness.md Gap 2"
|
|
8937
|
+
};
|
|
8938
|
+
}
|
|
8939
|
+
return {
|
|
8940
|
+
label: "Daemon config",
|
|
8941
|
+
status: "pass",
|
|
8942
|
+
detail: `repo=${repo || "<empty>"}, dir=${dir || "<empty>"}`
|
|
8943
|
+
};
|
|
8944
|
+
}
|
|
8599
8945
|
function checkGhcrAuth() {
|
|
8600
8946
|
if (isGhcrAuthenticated()) {
|
|
8601
8947
|
return {
|
|
@@ -8920,10 +9266,15 @@ var doctorCommand = new Command11("doctor").description("Health check \u2014 val
|
|
|
8920
9266
|
checks.push(...await checkClaudeAuth(env.ANTHROPIC_API_KEY));
|
|
8921
9267
|
checks.push(checkGithubToken(env));
|
|
8922
9268
|
checks.push(checkDocker());
|
|
9269
|
+
checks.push(checkDockerComposeVersion());
|
|
8923
9270
|
checks.push(checkGhcrAuth());
|
|
8924
9271
|
checks.push(checkGitHubCli());
|
|
9272
|
+
checks.push(checkProjectDirEnv(hasFactory ? factoryDir : null));
|
|
8925
9273
|
checks.push(checkProjectDirectory(hasFactory ? factoryDir : null));
|
|
8926
9274
|
checks.push(await checkBoardServer(hasFactory ? factoryDir : null));
|
|
9275
|
+
checks.push(await checkFactoryContainers(hasFactory ? factoryDir : null));
|
|
9276
|
+
checks.push(await checkUiServesBoard());
|
|
9277
|
+
checks.push(checkDaemonConfigLoad());
|
|
8927
9278
|
checks.push(checkStack(hasFactory ? factoryDir : null));
|
|
8928
9279
|
checks.push(checkPlaywright());
|
|
8929
9280
|
checks.push(checkBoardPassword(env, hasFactory ? factoryDir : null));
|