Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/gittensory-miner/lib/laptop-init.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export function initLaptopState(env?: Record<string, string | undefined>): Lapto

export function checkLaptopStateSqlite(env?: Record<string, string | undefined>): DoctorCheck;

export function findExecutableOnPath(name: string, env?: Record<string, string | undefined>): string | null;

export function checkDockerPresent(options?: {
env?: Record<string, string | undefined>;
resolveDockerPath?: () => string | null;
Expand Down
4 changes: 3 additions & 1 deletion packages/gittensory-miner/lib/laptop-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ export function checkLaptopStateSqlite(env = process.env) {
}
}

function findExecutableOnPath(name, env = process.env) {
/** Exported so callers that only need a presence boolean (e.g. status.js's `driver` section, #5164) can reuse
* this PATH scan directly instead of duplicating it or parsing a DoctorCheck's detail string. */
export function findExecutableOnPath(name, env = process.env) {
const pathValue = typeof env.PATH === "string" ? env.PATH : "";
for (const pathEntry of pathValue.split(delimiter)) {
if (!pathEntry) continue;
Expand Down
7 changes: 7 additions & 0 deletions packages/gittensory-miner/lib/status.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
export type MinerDriverStatus = {
provider: string | null;
modelEnvVar: string | null;
cliPresent: boolean | null;
};

export type MinerStatus = {
package: { name: string; version: string | null };
engine: { name: string; version: string | null };
node: string;
stateDir: string;
configFile: string | null;
driver: MinerDriverStatus;
};

export type DoctorCheck = {
Expand Down
35 changes: 34 additions & 1 deletion packages/gittensory-miner/lib/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
import { createRequire } from "node:module";
import { homedir } from "node:os";
import { join } from "node:path";
import { checkClaudeCliPresent, checkCodexCliPresent, checkDockerPresent, checkLaptopStateSqlite } from "./laptop-init.js";
import { CODING_AGENT_DRIVER_CONFIG_ENV, resolveFirstConfiguredCodingAgentDriverName } from "@jsonbored/gittensory-engine";
import {
checkClaudeCliPresent,
checkCodexCliPresent,
checkDockerPresent,
checkLaptopStateSqlite,
findExecutableOnPath,
} from "./laptop-init.js";
import { resolveMinerVersion } from "./version.js";

// Slim laptop-mode CLI commands (#2288): `status` (what's installed + where local state lives) and `doctor` (is
Expand Down Expand Up @@ -193,6 +200,23 @@ function discoverConfigFile(cwd) {
return null;
}

// CLI names driver-factory.ts's resolved provider values that actually spawn a local subprocess -- "noop" and
// "agent-sdk" have no separate CLI binary to check presence for, so cliPresent is null (not applicable) for them.
const PROVIDER_CLI_BINARY = Object.freeze({ "claude-cli": "claude", "codex-cli": "codex" });

/** The `driver` section of `status`/`status --json` (#5164): which coding-agent provider is configured, the
* NAME (never the value) of its model env var, and whether its CLI binary is on PATH. Reuses
* `resolveFirstConfiguredCodingAgentDriverName`/`CODING_AGENT_DRIVER_CONFIG_ENV` (the same resolution
* driver-factory.ts uses) and `findExecutableOnPath` (the same PATH scan the doctor CLI-presence checks use)
* rather than duplicating either. Never reads or returns an env var's actual value. */
function resolveDriverStatus(env) {
const provider = resolveFirstConfiguredCodingAgentDriverName(env) ?? null;
const modelEnvVar = provider ? (CODING_AGENT_DRIVER_CONFIG_ENV[provider]?.model ?? null) : null;
const cliBinary = provider ? (PROVIDER_CLI_BINARY[provider] ?? null) : null;
const cliPresent = cliBinary ? Boolean(findExecutableOnPath(cliBinary, env)) : null;
return { provider, modelEnvVar, cliPresent };
}

/** Gather the read-only status snapshot. Pure w.r.t. its (env, cwd) inputs — no writes, no network. */
export function collectStatus(env = process.env, cwd = process.cwd()) {
const stateDir = resolveMinerStateDir(env);
Expand All @@ -202,15 +226,24 @@ export function collectStatus(env = process.env, cwd = process.cwd()) {
node: process.version,
stateDir,
configFile: discoverConfigFile(cwd),
driver: resolveDriverStatus(env),
};
}

function renderDriverLine(driver) {
if (!driver.provider) return "driver: none configured";
const cliText = driver.cliPresent === null ? "n/a" : driver.cliPresent ? "yes" : "no";
const modelText = driver.modelEnvVar ? `, model env: ${driver.modelEnvVar}` : "";
return `driver: ${driver.provider} (CLI present: ${cliText}${modelText})`;
}

function renderStatusText(status) {
return [
`${status.package.name} ${status.package.version ?? "unknown"} (node ${status.node})`,
`engine: ${status.engine.name} ${status.engine.version ?? "unresolved"}`,
`state dir: ${status.stateDir}`,
`config file: ${status.configFile ?? "none found"}`,
renderDriverLine(status.driver),
].join("\n");
}

Expand Down
120 changes: 119 additions & 1 deletion test/unit/miner-status.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
Expand Down Expand Up @@ -31,6 +31,14 @@ afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

/** Creates an executable file `name` in a fresh bin dir and returns that dir (usable directly as PATH). */
function fakeBinDir(name: string): string {
const dir = tempRoot();
writeFileSync(join(dir, name), "#!/bin/sh\n");
chmodSync(join(dir, name), 0o755);
return dir;
}

describe("gittensory-miner status/doctor (#2288)", () => {
it("resolves the state dir from the config-dir override, XDG, then the home default", () => {
expect(resolveMinerStateDir({ GITTENSORY_MINER_CONFIG_DIR: "/custom/state" })).toBe("/custom/state");
Expand Down Expand Up @@ -195,4 +203,114 @@ describe("gittensory-miner status/doctor (#2288)", () => {
runDoctor([], env);
expect(fetchStub).not.toHaveBeenCalled();
});

describe("driver section of status/status --json (#5164)", () => {
it("reports provider: null, modelEnvVar: null, cliPresent: null when no provider is configured", () => {
const status = collectStatus({ GITTENSORY_MINER_CONFIG_DIR: "/s", PATH: "" }, tempRoot());
expect(status.driver).toEqual({ provider: null, modelEnvVar: null, cliPresent: null });
});

it("reports the noop provider with no model env var and no CLI to check (cliPresent: null)", () => {
const status = collectStatus(
{ GITTENSORY_MINER_CONFIG_DIR: "/s", PATH: "", MINER_CODING_AGENT_PROVIDER: "noop" },
tempRoot(),
);
expect(status.driver).toEqual({ provider: "noop", modelEnvVar: null, cliPresent: null });
});

it("reports the agent-sdk provider with no model env var and no CLI to check (cliPresent: null)", () => {
const status = collectStatus(
{ GITTENSORY_MINER_CONFIG_DIR: "/s", PATH: "", MINER_CODING_AGENT_PROVIDER: "agent-sdk" },
tempRoot(),
);
expect(status.driver).toEqual({ provider: "agent-sdk", modelEnvVar: null, cliPresent: null });
});

it("claude-cli configured + CLI present on PATH: reports the model env-var name and cliPresent: true", () => {
const status = collectStatus(
{
GITTENSORY_MINER_CONFIG_DIR: "/s",
MINER_CODING_AGENT_PROVIDER: "claude-cli",
PATH: fakeBinDir("claude"),
},
tempRoot(),
);
expect(status.driver).toEqual({
provider: "claude-cli",
modelEnvVar: "MINER_CODING_AGENT_CLAUDE_MODEL",
cliPresent: true,
});
});

it("claude-cli configured + CLI absent from PATH: cliPresent: false", () => {
const status = collectStatus(
{ GITTENSORY_MINER_CONFIG_DIR: "/s", MINER_CODING_AGENT_PROVIDER: "claude-cli", PATH: tempRoot() },
tempRoot(),
);
expect(status.driver.cliPresent).toBe(false);
});

it("codex-cli configured + CLI present on PATH: reports the model env-var name and cliPresent: true", () => {
const status = collectStatus(
{
GITTENSORY_MINER_CONFIG_DIR: "/s",
MINER_CODING_AGENT_PROVIDER: "codex-cli",
PATH: fakeBinDir("codex"),
},
tempRoot(),
);
expect(status.driver).toEqual({
provider: "codex-cli",
modelEnvVar: "MINER_CODING_AGENT_CODEX_MODEL",
cliPresent: true,
});
});

it("codex-cli configured + CLI absent from PATH: cliPresent: false", () => {
const status = collectStatus(
{ GITTENSORY_MINER_CONFIG_DIR: "/s", MINER_CODING_AGENT_PROVIDER: "codex-cli", PATH: tempRoot() },
tempRoot(),
);
expect(status.driver.cliPresent).toBe(false);
});

it("human-readable status text renders the driver line for both configured and unconfigured cases", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
runStatus([], { GITTENSORY_MINER_CONFIG_DIR: "/s", PATH: "" }, tempRoot());
expect(String(log.mock.calls[0]?.[0])).toContain("driver: none configured");
log.mockClear();
runStatus(
[],
{ GITTENSORY_MINER_CONFIG_DIR: "/s", MINER_CODING_AGENT_PROVIDER: "codex-cli", PATH: fakeBinDir("codex") },
tempRoot(),
);
expect(String(log.mock.calls[0]?.[0])).toContain(
"driver: codex-cli (CLI present: yes, model env: MINER_CODING_AGENT_CODEX_MODEL)",
);
});

it("invariant: no env-var VALUE or secret-shaped string ever appears in status --json output across provider permutations", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const secretModelValue = "sk-ant-should-never-appear-in-output";
const providers = [undefined, "noop", "claude-cli", "codex-cli", "agent-sdk"];
for (const provider of providers) {
log.mockClear();
runStatus(
["--json"],
{
GITTENSORY_MINER_CONFIG_DIR: "/s",
...(provider ? { MINER_CODING_AGENT_PROVIDER: provider } : {}),
MINER_CODING_AGENT_CLAUDE_MODEL: secretModelValue,
MINER_CODING_AGENT_CODEX_MODEL: secretModelValue,
PATH: fakeBinDir("claude"),
},
tempRoot(),
);
const output = String(log.mock.calls[0]?.[0]);
expect(output).not.toContain(secretModelValue);
const parsed = JSON.parse(output);
expect(typeof parsed.driver.cliPresent === "boolean" || parsed.driver.cliPresent === null).toBe(true);
}
});
});
});