@on-par/factory-cli 0.9.3 → 0.9.4
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/README.md +1 -1
- package/dist/cli/doctor.d.ts +75 -3
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +332 -33
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/first-run.d.ts +6 -1
- package/dist/cli/first-run.d.ts.map +1 -1
- package/dist/cli/first-run.js +9 -2
- package/dist/cli/first-run.js.map +1 -1
- package/dist/cli/index.d.ts +10 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +141 -91
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/stop-sentinel.d.ts.map +1 -1
- package/dist/cli/stop-sentinel.js +4 -3
- package/dist/cli/stop-sentinel.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ factory --version
|
|
|
16
16
|
factory doctor
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Also requires `git`,
|
|
19
|
+
Also requires `git`, one supported coding agent CLI (see [docs/agents.md](https://github.com/on-par/software-factory/blob/main/docs/agents.md)), signed in, and the GitHub CLI signed in with `gh auth login`. The factory reads `GITHUB_TOKEN`, then `GH_TOKEN`, and falls back to `gh auth token`. `factory doctor` checks all of this and prints a fix for anything missing. It also checks your git identity and the sandbox runtime, and it runs outside a repo.
|
|
20
20
|
|
|
21
21
|
`factory init` creates a local-only `.factory/` (git-excluded) with a `default-code` constitution. See the main README quick start.
|
|
22
22
|
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -7,12 +7,22 @@ export interface DoctorCheck {
|
|
|
7
7
|
fix?: string;
|
|
8
8
|
/** Optional checks never fail the doctor exit code. */
|
|
9
9
|
optional?: boolean;
|
|
10
|
+
/** Setup section the row renders under (#2555); unsectioned rows render first. */
|
|
11
|
+
section?: string;
|
|
12
|
+
/** Exactly one shell command to run next (#2557); rendered as the last line of a non-ok row. */
|
|
13
|
+
next?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DoctorExecOptions {
|
|
16
|
+
/** Extra env vars merged over process.env for this command. */
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
/** Kill the command after this many ms; the probe then returns null. */
|
|
19
|
+
timeoutMs?: number;
|
|
10
20
|
}
|
|
11
21
|
export interface DoctorEnvProbes {
|
|
12
22
|
commandAvailable: (cmd: string) => boolean;
|
|
13
23
|
envPresent: (key: string) => boolean;
|
|
14
|
-
/** Run a command, return trimmed stdout, or null if it fails. */
|
|
15
|
-
tryExec: (cmd: string) => string | null;
|
|
24
|
+
/** Run a command, return trimmed stdout, or null if it fails. `opts` adds env vars and a timeout. */
|
|
25
|
+
tryExec: (cmd: string, opts?: DoctorExecOptions) => string | null;
|
|
16
26
|
pathExists: (p: string) => boolean;
|
|
17
27
|
/** Host platform; defaults to process.platform. */
|
|
18
28
|
platform?: NodeJS.Platform;
|
|
@@ -20,8 +30,18 @@ export interface DoctorEnvProbes {
|
|
|
20
30
|
fresh: boolean;
|
|
21
31
|
detail: string;
|
|
22
32
|
};
|
|
33
|
+
/** Stored Codex ChatGPT session state (#2509), read locally; never refreshes. */
|
|
34
|
+
codexSessionState?: () => 'ok' | 'refresh-pending' | 'unusable' | 'unknown';
|
|
35
|
+
/** Whether a stored Claude Code login exists (#2556); reads local state only, never runs inference. */
|
|
36
|
+
claudeLoggedIn?: () => boolean;
|
|
37
|
+
/** Whether a stored opencode auth login exists (#2556); reads local state only, never runs inference. */
|
|
38
|
+
opencodeLoggedIn?: () => boolean;
|
|
23
39
|
}
|
|
40
|
+
/** Repo wired-up rows, sectioned only inside a git repo (#2557). */
|
|
41
|
+
export declare const REPO_SECTION = "Repo";
|
|
24
42
|
export declare const NOT_IN_REPO_HINT = "cd into your repo, then run factory doctor again";
|
|
43
|
+
/** Detail for every repo wired-up row when doctor runs outside a git repository (#2554). */
|
|
44
|
+
export declare const NOT_IN_REPO_SKIP_DETAIL = "skipped \u2014 not inside a git repository; cd into your repo, then run factory doctor again";
|
|
25
45
|
export declare function hasSandboxRuntime(platform: NodeJS.Platform, commandAvailable: (cmd: string) => boolean): boolean;
|
|
26
46
|
/** Reason the sandboxed claude auth probe is skipped before a sandbox policy is resolved,
|
|
27
47
|
* or null when cmdDoctor should resolve the policy as before (#2383). */
|
|
@@ -30,8 +50,51 @@ export declare function sandboxAuthSkipDetail(opts: {
|
|
|
30
50
|
hostOk: boolean;
|
|
31
51
|
inRepo: boolean;
|
|
32
52
|
}): string | null;
|
|
53
|
+
/** Sign-in method reported by `codex login status`, or null when it shows no login. Never returns
|
|
54
|
+
* any part of a credential: only a fixed label is derived from the status text. */
|
|
55
|
+
export declare function codexAuthMethod(statusOutput: string | null): string | null;
|
|
56
|
+
/** Non-interactive read-only probe of origin. `credential.interactive=never` turns off Git
|
|
57
|
+
* Credential Manager UI and an empty `core.askPass` turns off configured askpass programs;
|
|
58
|
+
* stored credentials from any helper still work. */
|
|
59
|
+
export declare const GIT_AUTH_PROBE_CMD = "git -c credential.interactive=never -c core.askPass= ls-remote --quiet origin HEAD";
|
|
60
|
+
/** GIT_TERMINAL_PROMPT=0 makes git fail instead of asking on the terminal; empty GIT_ASKPASS
|
|
61
|
+
* and SSH_ASKPASS turn off askpass programs and ssh's GUI askpass; GCM_INTERACTIVE=never
|
|
62
|
+
* turns off Git Credential Manager prompts. */
|
|
63
|
+
export declare const GIT_AUTH_PROBE_ENV: Readonly<Record<string, string>>;
|
|
64
|
+
/** Bounds the probe, e.g. an ssh key passphrase prompt that waits on /dev/tty. */
|
|
65
|
+
export declare const GIT_AUTH_PROBE_TIMEOUT_MS = 15000;
|
|
66
|
+
/** Remedy shown only when the git-auth probe fails: wires git's credential helper to gh. */
|
|
67
|
+
export declare const GIT_AUTH_FIX = "run `gh auth setup-git` so git uses your gh credentials, then re-run `factory doctor`";
|
|
68
|
+
/** Git-auth check (#2482, #2483): skipped, never failing, outside a git repository or without
|
|
69
|
+
* an origin remote. Otherwise runs a non-interactive `git ls-remote` against origin and fails
|
|
70
|
+
* when it cannot authenticate. The detail never echoes the origin URL, which may hold a token. */
|
|
71
|
+
export declare function gitAuthCheck(repoRoot: string | null, tryExec: (cmd: string, opts?: DoctorExecOptions) => string | null): DoctorCheck;
|
|
72
|
+
export interface HarnessSetup {
|
|
73
|
+
section: string;
|
|
74
|
+
cli: string;
|
|
75
|
+
install: string;
|
|
76
|
+
/** Exact shell command that signs the harness in (#2556); also the row's `next` command. */
|
|
77
|
+
login: string;
|
|
78
|
+
/** Env vars that count as sign-in evidence (#2556). */
|
|
79
|
+
envKeys: readonly string[];
|
|
80
|
+
}
|
|
81
|
+
/** One doctor section per built-in harness (#2555), in display order. */
|
|
82
|
+
export declare const HARNESS_SETUP: readonly HarnessSetup[];
|
|
83
|
+
/** One optional `<cli> installed` row per harness, with the CLI version when found and the install command when not (#2555). */
|
|
84
|
+
export declare function harnessInstallChecks(commandAvailable: (cmd: string) => boolean, tryExec: (cmd: string) => string | null): DoctorCheck[];
|
|
85
|
+
/** One `<cli> signed in` row per harness (#2556), judged only from local evidence: an env key,
|
|
86
|
+
* stored credentials, or `codex login status`. Never runs inference and never prints a secret. */
|
|
87
|
+
export declare function harnessSignInChecks(probes: DoctorEnvProbes): DoctorCheck[];
|
|
88
|
+
/** Coding agent prerequisites: at least one agent CLI is required; each CLI is then reported
|
|
89
|
+
* on its own optional line, with sign-in reported separately from installation (#1269, #2422, #2555, #2556). */
|
|
90
|
+
export declare function agentCliChecks(probes: DoctorEnvProbes): DoctorCheck[];
|
|
33
91
|
export declare function runDoctorChecks(probes: DoctorEnvProbes): DoctorCheck[];
|
|
34
|
-
|
|
92
|
+
/** Row icon (#2550): ❌ only for a failure (counted in "N failed"), ➖ for a skipped row, ⚠️ for an optional warning. */
|
|
93
|
+
export declare function doctorCheckIcon(check: DoctorCheck): string;
|
|
94
|
+
/** Outside a repo the models hint is skipped because `factory models --doctor` needs a repo (#2472). */
|
|
95
|
+
export declare function formatDoctorChecks(checks: DoctorCheck[], opts?: {
|
|
96
|
+
inRepo?: boolean;
|
|
97
|
+
}): string;
|
|
35
98
|
export declare function doctorFailed(checks: DoctorCheck[]): boolean;
|
|
36
99
|
/** Which Claude profile a keychain probe checked: the keychain service name and, for a
|
|
37
100
|
* non-default profile, the CLAUDE_CONFIG_DIR it belongs to. Absent = default profile. */
|
|
@@ -65,6 +128,10 @@ export interface ClaudeAuthProbeResult {
|
|
|
65
128
|
* differential — host ok, sandboxed failed — is a hard doctor failure; that is the one
|
|
66
129
|
* combination that proves the sandbox, not the credentials, is at fault. */
|
|
67
130
|
export declare function sandboxClaudeAuthChecks(result: ClaudeAuthProbeResult): DoctorCheck[];
|
|
131
|
+
/** #2496: doctor fails on agent sign-in only when no installed agent is signed in. A logged-out
|
|
132
|
+
* agent is a warning while another one is signed in, and a hard failure when none is. Skipped
|
|
133
|
+
* probes (optional passes, e.g. a `<cli> signed in` row for a CLI not on PATH) never count as signed in. */
|
|
134
|
+
export declare function reconcileAgentAuthChecks(checks: DoctorCheck[]): DoctorCheck[];
|
|
68
135
|
export interface LeaseHealthRow {
|
|
69
136
|
worktreeId: string;
|
|
70
137
|
branch: string;
|
|
@@ -138,4 +205,9 @@ export type GreenPrScanResult = {
|
|
|
138
205
|
export declare function unmergedGreenPrChecks(result: GreenPrScanResult): DoctorCheck[];
|
|
139
206
|
/** One doctor check per user-declared harness, from its `probe` result. */
|
|
140
207
|
export declare function declaredHarnessChecks(probes: DeclaredHarnessProbe[]): DoctorCheck[];
|
|
208
|
+
/** One failed check per models.registry entry whose harness is missing or unknown;
|
|
209
|
+
* a single ok row when every entry is valid; nothing when there is no registry. */
|
|
210
|
+
export declare function modelsRegistryChecks(configPath: string, registry: Readonly<Record<string, {
|
|
211
|
+
harness?: string;
|
|
212
|
+
}>> | undefined, validHarnesses: readonly string[]): DoctorCheck[];
|
|
141
213
|
//# sourceMappingURL=doctor.d.ts.map
|
package/dist/cli/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAMjE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC,
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { detectSandboxRuntime } from '@on-par/factory-core';
|
|
4
4
|
import { MIN_NODE_MAJOR } from '../node-version.js';
|
|
5
|
-
import { CLAUDE_CODE_URL, GITHUB_TOKENS_URL } from './first-run.js';
|
|
5
|
+
import { availableAgentClis, CLAUDE_CODE_URL, CODEX_CLI_URL, GITHUB_TOKENS_URL } from './first-run.js';
|
|
6
|
+
/** Repo wired-up rows, sectioned only inside a git repo (#2557). */
|
|
7
|
+
export const REPO_SECTION = 'Repo';
|
|
6
8
|
export const NOT_IN_REPO_HINT = 'cd into your repo, then run factory doctor again';
|
|
9
|
+
/** Detail for every repo wired-up row when doctor runs outside a git repository (#2554). */
|
|
10
|
+
export const NOT_IN_REPO_SKIP_DETAIL = `skipped — not inside a git repository; ${NOT_IN_REPO_HINT}`;
|
|
7
11
|
export function hasSandboxRuntime(platform, commandAvailable) {
|
|
8
12
|
return detectSandboxRuntime(platform, commandAvailable) !== 'none';
|
|
9
13
|
}
|
|
@@ -15,9 +19,225 @@ export function sandboxAuthSkipDetail(opts) {
|
|
|
15
19
|
if (!opts.hostOk)
|
|
16
20
|
return 'skipped — host claude auth was not verified';
|
|
17
21
|
if (!opts.inRepo)
|
|
18
|
-
return
|
|
22
|
+
return NOT_IN_REPO_SKIP_DETAIL;
|
|
19
23
|
return null;
|
|
20
24
|
}
|
|
25
|
+
/** Sign-in method reported by `codex login status`, or null when it shows no login. Never returns
|
|
26
|
+
* any part of a credential: only a fixed label is derived from the status text. */
|
|
27
|
+
export function codexAuthMethod(statusOutput) {
|
|
28
|
+
if (statusOutput === null)
|
|
29
|
+
return null;
|
|
30
|
+
const text = statusOutput.toLowerCase();
|
|
31
|
+
if (!text.includes('logged in') || text.includes('not logged in'))
|
|
32
|
+
return null;
|
|
33
|
+
if (text.includes('chatgpt'))
|
|
34
|
+
return 'ChatGPT';
|
|
35
|
+
if (text.includes('api key'))
|
|
36
|
+
return 'API key';
|
|
37
|
+
if (text.includes('access token'))
|
|
38
|
+
return 'access token';
|
|
39
|
+
return 'unknown method';
|
|
40
|
+
}
|
|
41
|
+
const GIT_AUTH_CHECK = 'git auth';
|
|
42
|
+
/** Non-interactive read-only probe of origin. `credential.interactive=never` turns off Git
|
|
43
|
+
* Credential Manager UI and an empty `core.askPass` turns off configured askpass programs;
|
|
44
|
+
* stored credentials from any helper still work. */
|
|
45
|
+
export const GIT_AUTH_PROBE_CMD = 'git -c credential.interactive=never -c core.askPass= ls-remote --quiet origin HEAD';
|
|
46
|
+
/** GIT_TERMINAL_PROMPT=0 makes git fail instead of asking on the terminal; empty GIT_ASKPASS
|
|
47
|
+
* and SSH_ASKPASS turn off askpass programs and ssh's GUI askpass; GCM_INTERACTIVE=never
|
|
48
|
+
* turns off Git Credential Manager prompts. */
|
|
49
|
+
export const GIT_AUTH_PROBE_ENV = {
|
|
50
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
51
|
+
GIT_ASKPASS: '',
|
|
52
|
+
SSH_ASKPASS: '',
|
|
53
|
+
GCM_INTERACTIVE: 'never',
|
|
54
|
+
};
|
|
55
|
+
/** Bounds the probe, e.g. an ssh key passphrase prompt that waits on /dev/tty. */
|
|
56
|
+
export const GIT_AUTH_PROBE_TIMEOUT_MS = 15_000;
|
|
57
|
+
/** Remedy shown only when the git-auth probe fails: wires git's credential helper to gh. */
|
|
58
|
+
export const GIT_AUTH_FIX = 'run `gh auth setup-git` so git uses your gh credentials, then re-run `factory doctor`';
|
|
59
|
+
/** Git-auth check (#2482, #2483): skipped, never failing, outside a git repository or without
|
|
60
|
+
* an origin remote. Otherwise runs a non-interactive `git ls-remote` against origin and fails
|
|
61
|
+
* when it cannot authenticate. The detail never echoes the origin URL, which may hold a token. */
|
|
62
|
+
export function gitAuthCheck(repoRoot, tryExec) {
|
|
63
|
+
if (repoRoot === null) {
|
|
64
|
+
return {
|
|
65
|
+
name: GIT_AUTH_CHECK,
|
|
66
|
+
ok: true,
|
|
67
|
+
optional: true,
|
|
68
|
+
detail: NOT_IN_REPO_SKIP_DETAIL,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const origin = tryExec('git remote get-url origin');
|
|
72
|
+
if (!origin) {
|
|
73
|
+
return { name: GIT_AUTH_CHECK, ok: true, optional: true, detail: 'skipped — no origin remote configured' };
|
|
74
|
+
}
|
|
75
|
+
// An empty string is exit 0 with no HEAD ref (an empty repo): still authenticated.
|
|
76
|
+
const probe = tryExec(GIT_AUTH_PROBE_CMD, { env: { ...GIT_AUTH_PROBE_ENV }, timeoutMs: GIT_AUTH_PROBE_TIMEOUT_MS });
|
|
77
|
+
if (probe === null) {
|
|
78
|
+
return {
|
|
79
|
+
name: GIT_AUTH_CHECK,
|
|
80
|
+
ok: false,
|
|
81
|
+
detail: 'git could not authenticate to origin non-interactively (prompting disabled)',
|
|
82
|
+
fix: GIT_AUTH_FIX,
|
|
83
|
+
next: 'gh auth setup-git',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { name: GIT_AUTH_CHECK, ok: true, detail: 'git can authenticate to origin non-interactively' };
|
|
87
|
+
}
|
|
88
|
+
/** One doctor section per built-in harness (#2555), in display order. */
|
|
89
|
+
export const HARNESS_SETUP = [
|
|
90
|
+
{
|
|
91
|
+
section: 'Claude Code',
|
|
92
|
+
cli: 'claude',
|
|
93
|
+
install: 'npm install -g @anthropic-ai/claude-code',
|
|
94
|
+
login: 'claude /login',
|
|
95
|
+
envKeys: ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
section: 'Codex',
|
|
99
|
+
cli: 'codex',
|
|
100
|
+
install: 'npm install -g @openai/codex',
|
|
101
|
+
login: 'codex login',
|
|
102
|
+
envKeys: ['CODEX_API_KEY'],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
section: 'OpenCode',
|
|
106
|
+
cli: 'opencode',
|
|
107
|
+
install: 'npm install -g opencode-ai',
|
|
108
|
+
login: 'opencode auth login',
|
|
109
|
+
envKeys: ['OPENCODE_API_KEY'],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
/** One optional `<cli> installed` row per harness, with the CLI version when found and the install command when not (#2555). */
|
|
113
|
+
export function harnessInstallChecks(commandAvailable, tryExec) {
|
|
114
|
+
return HARNESS_SETUP.map(({ section, cli, install }) => {
|
|
115
|
+
const name = `${cli} installed`;
|
|
116
|
+
if (!commandAvailable(cli)) {
|
|
117
|
+
return {
|
|
118
|
+
name,
|
|
119
|
+
section,
|
|
120
|
+
ok: false,
|
|
121
|
+
optional: true,
|
|
122
|
+
detail: `${cli} not found on PATH, ${cli} routes unavailable`,
|
|
123
|
+
fix: `install ${section}: ${install}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const version = tryExec(`${cli} --version`)?.trim().split('\n')[0]?.trim();
|
|
127
|
+
return {
|
|
128
|
+
name,
|
|
129
|
+
section,
|
|
130
|
+
ok: true,
|
|
131
|
+
optional: true,
|
|
132
|
+
detail: version ? `found on PATH (${version})` : 'found on PATH (version unknown)',
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const SIGN_IN_EVIDENCE = {
|
|
137
|
+
claude: 'no ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN or stored login',
|
|
138
|
+
codex: 'no CODEX_API_KEY or codex login; API billing: codex login --with-api-key; headless: codex login --device-auth',
|
|
139
|
+
opencode: 'no OPENCODE_API_KEY or stored auth login',
|
|
140
|
+
};
|
|
141
|
+
function safe(fn) {
|
|
142
|
+
if (!fn)
|
|
143
|
+
return false;
|
|
144
|
+
try {
|
|
145
|
+
return fn();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** One `<cli> signed in` row per harness (#2556), judged only from local evidence: an env key,
|
|
152
|
+
* stored credentials, or `codex login status`. Never runs inference and never prints a secret. */
|
|
153
|
+
export function harnessSignInChecks(probes) {
|
|
154
|
+
const { commandAvailable, envPresent, tryExec } = probes;
|
|
155
|
+
const found = availableAgentClis(commandAvailable);
|
|
156
|
+
return HARNESS_SETUP.map(({ section, cli, login, envKeys }) => {
|
|
157
|
+
const name = `${cli} signed in`;
|
|
158
|
+
if (!commandAvailable(cli)) {
|
|
159
|
+
return { name, section, ok: true, optional: true, detail: `skipped — ${cli} not installed` };
|
|
160
|
+
}
|
|
161
|
+
const fix = `sign in to ${section}: ${login}`;
|
|
162
|
+
if (cli === 'codex') {
|
|
163
|
+
const key = envKeys.find((k) => envPresent(k));
|
|
164
|
+
if (key)
|
|
165
|
+
return { name, section, ok: true, detail: `signed in (${key} set)` };
|
|
166
|
+
const method = codexAuthMethod(tryExec('codex login status 2>&1'));
|
|
167
|
+
const session = method === 'ChatGPT' ? (probes.codexSessionState?.() ?? 'unknown') : 'unknown';
|
|
168
|
+
if (session === 'unusable') {
|
|
169
|
+
return {
|
|
170
|
+
name,
|
|
171
|
+
section,
|
|
172
|
+
ok: false,
|
|
173
|
+
optional: found.length > 1,
|
|
174
|
+
detail: 'ChatGPT session in auth.json is unusable (unreadable, or no valid access token and no refresh token)',
|
|
175
|
+
fix,
|
|
176
|
+
next: login,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (session === 'refresh-pending') {
|
|
180
|
+
return {
|
|
181
|
+
name,
|
|
182
|
+
section,
|
|
183
|
+
ok: true,
|
|
184
|
+
detail: 'logged in with ChatGPT; access token expired, codex refreshes it on the next run (run `codex login` if that run fails with 401)',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (method) {
|
|
188
|
+
return {
|
|
189
|
+
name,
|
|
190
|
+
section,
|
|
191
|
+
ok: true,
|
|
192
|
+
detail: method === 'ChatGPT'
|
|
193
|
+
? 'logged in with ChatGPT; runs draw on your ChatGPT plan limits (manage usage in ChatGPT settings)'
|
|
194
|
+
: `logged in with ${method}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const key = envKeys.find((k) => envPresent(k));
|
|
200
|
+
if (key)
|
|
201
|
+
return { name, section, ok: true, detail: `signed in (${key} set)` };
|
|
202
|
+
if (cli === 'claude' && safe(probes.claudeLoggedIn)) {
|
|
203
|
+
return { name, section, ok: true, detail: 'signed in (stored Claude Code login found)' };
|
|
204
|
+
}
|
|
205
|
+
if (cli === 'opencode' && safe(probes.opencodeLoggedIn)) {
|
|
206
|
+
return { name, section, ok: true, detail: 'signed in (stored opencode auth login found)' };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
name,
|
|
211
|
+
section,
|
|
212
|
+
ok: false,
|
|
213
|
+
// Required only when this is the sole agent: then nothing else can run.
|
|
214
|
+
optional: found.length > 1,
|
|
215
|
+
detail: `unverified — no sign-in evidence (${SIGN_IN_EVIDENCE[cli]})`,
|
|
216
|
+
fix,
|
|
217
|
+
next: login,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** Coding agent prerequisites: at least one agent CLI is required; each CLI is then reported
|
|
222
|
+
* on its own optional line, with sign-in reported separately from installation (#1269, #2422, #2555, #2556). */
|
|
223
|
+
export function agentCliChecks(probes) {
|
|
224
|
+
const { commandAvailable, tryExec } = probes;
|
|
225
|
+
const found = availableAgentClis(commandAvailable);
|
|
226
|
+
const checks = [
|
|
227
|
+
found.length > 0
|
|
228
|
+
? { name: 'coding agent', ok: true, detail: `found ${found.join(', ')}` }
|
|
229
|
+
: {
|
|
230
|
+
name: 'coding agent',
|
|
231
|
+
ok: false,
|
|
232
|
+
detail: 'no coding agent CLI on PATH (claude, codex or opencode)',
|
|
233
|
+
fix: `install Claude Code (${CLAUDE_CODE_URL}) or the Codex CLI (${CODEX_CLI_URL})`,
|
|
234
|
+
next: HARNESS_SETUP[0].install,
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
checks.push(...harnessInstallChecks(commandAvailable, tryExec));
|
|
238
|
+
checks.push(...harnessSignInChecks(probes));
|
|
239
|
+
return checks;
|
|
240
|
+
}
|
|
21
241
|
export function runDoctorChecks(probes) {
|
|
22
242
|
const { commandAvailable, envPresent, tryExec, pathExists, distFreshness } = probes;
|
|
23
243
|
const checks = [];
|
|
@@ -31,34 +251,37 @@ export function runDoctorChecks(probes) {
|
|
|
31
251
|
detail: 'not inside a git repository — repo checks skipped',
|
|
32
252
|
fix: NOT_IN_REPO_HINT,
|
|
33
253
|
});
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
checks.push({ name: 'git identity', ok: true, detail: `${userName} <${userEmail}>` });
|
|
254
|
+
const repoSection = repoRoot === null ? {} : { section: REPO_SECTION };
|
|
255
|
+
if (repoRoot === null) {
|
|
256
|
+
checks.push({ name: 'git identity', ok: false, optional: true, detail: NOT_IN_REPO_SKIP_DETAIL });
|
|
38
257
|
}
|
|
39
258
|
else {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ok:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
259
|
+
const userName = tryExec('git config user.name');
|
|
260
|
+
const userEmail = tryExec('git config user.email');
|
|
261
|
+
if (userName && userEmail) {
|
|
262
|
+
checks.push({ name: 'git identity', ok: true, detail: `${userName} <${userEmail}>`, ...repoSection });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const missing = [!userName && 'user.name', !userEmail && 'user.email'].filter(Boolean).join(' and ');
|
|
266
|
+
checks.push({
|
|
267
|
+
name: 'git identity',
|
|
268
|
+
ok: false,
|
|
269
|
+
detail: `${missing} not set`,
|
|
270
|
+
next: !userName
|
|
271
|
+
? 'git config --global user.name "Your Name"'
|
|
272
|
+
: 'git config --global user.email you@example.com',
|
|
273
|
+
...repoSection,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
47
276
|
}
|
|
48
|
-
checks.push(
|
|
49
|
-
? { name: 'claude CLI', ok: true, detail: 'found on PATH' }
|
|
50
|
-
: {
|
|
51
|
-
name: 'claude CLI',
|
|
52
|
-
ok: false,
|
|
53
|
-
detail: 'not found on PATH',
|
|
54
|
-
fix: `install Claude Code first: ${CLAUDE_CODE_URL}`,
|
|
55
|
-
});
|
|
277
|
+
checks.push(...agentCliChecks(probes));
|
|
56
278
|
if (!commandAvailable('gh')) {
|
|
57
279
|
checks.push({
|
|
58
280
|
name: 'gh auth',
|
|
59
281
|
ok: false,
|
|
60
282
|
detail: 'gh CLI not found on PATH',
|
|
61
|
-
fix: 'install GitHub CLI
|
|
283
|
+
fix: 'install GitHub CLI: https://cli.github.com',
|
|
284
|
+
next: 'gh auth login',
|
|
62
285
|
});
|
|
63
286
|
}
|
|
64
287
|
else if (tryExec('gh auth status') === null) {
|
|
@@ -66,12 +289,13 @@ export function runDoctorChecks(probes) {
|
|
|
66
289
|
name: 'gh auth',
|
|
67
290
|
ok: false,
|
|
68
291
|
detail: 'gh is not authenticated',
|
|
69
|
-
|
|
292
|
+
next: 'gh auth login',
|
|
70
293
|
});
|
|
71
294
|
}
|
|
72
295
|
else {
|
|
73
296
|
checks.push({ name: 'gh auth', ok: true, detail: 'authenticated' });
|
|
74
297
|
}
|
|
298
|
+
checks.push({ ...gitAuthCheck(repoRoot, tryExec), ...repoSection });
|
|
75
299
|
if (envPresent('GITHUB_TOKEN')) {
|
|
76
300
|
checks.push({ name: 'GitHub token', ok: true, detail: 'GITHUB_TOKEN set' });
|
|
77
301
|
}
|
|
@@ -89,6 +313,7 @@ export function runDoctorChecks(probes) {
|
|
|
89
313
|
ok: false,
|
|
90
314
|
detail: 'no GITHUB_TOKEN, GH_TOKEN, or gh auth token',
|
|
91
315
|
fix: `create a token at ${GITHUB_TOKENS_URL} and export it`,
|
|
316
|
+
next: 'gh auth login',
|
|
92
317
|
});
|
|
93
318
|
}
|
|
94
319
|
}
|
|
@@ -105,11 +330,11 @@ export function runDoctorChecks(probes) {
|
|
|
105
330
|
name: 'git worktree clean',
|
|
106
331
|
ok: false,
|
|
107
332
|
optional: true,
|
|
108
|
-
detail:
|
|
333
|
+
detail: NOT_IN_REPO_SKIP_DETAIL,
|
|
109
334
|
});
|
|
110
335
|
}
|
|
111
336
|
else if (tryExec('git status --porcelain') === '') {
|
|
112
|
-
checks.push({ name: 'git worktree clean', ok: true, detail: 'clean' });
|
|
337
|
+
checks.push({ name: 'git worktree clean', ok: true, detail: 'clean', ...repoSection });
|
|
113
338
|
}
|
|
114
339
|
else {
|
|
115
340
|
checks.push({
|
|
@@ -118,6 +343,7 @@ export function runDoctorChecks(probes) {
|
|
|
118
343
|
optional: true,
|
|
119
344
|
detail: 'uncommitted changes present',
|
|
120
345
|
fix: 'commit or stash changes before running the factory',
|
|
346
|
+
...repoSection,
|
|
121
347
|
});
|
|
122
348
|
}
|
|
123
349
|
const sandboxRuntime = detectSandboxRuntime(probes.platform ?? process.platform, commandAvailable);
|
|
@@ -151,19 +377,19 @@ export function runDoctorChecks(probes) {
|
|
|
151
377
|
name: '.factory initialized',
|
|
152
378
|
ok: false,
|
|
153
379
|
optional: true,
|
|
154
|
-
detail:
|
|
380
|
+
detail: NOT_IN_REPO_SKIP_DETAIL,
|
|
155
381
|
});
|
|
156
382
|
}
|
|
157
383
|
else if (pathExists(resolve(repoRoot, '.factory'))) {
|
|
158
|
-
checks.push({ name: '.factory initialized', ok: true,
|
|
384
|
+
checks.push({ name: '.factory initialized', ok: true, detail: '.factory/ present', ...repoSection });
|
|
159
385
|
}
|
|
160
386
|
else {
|
|
161
387
|
checks.push({
|
|
162
388
|
name: '.factory initialized',
|
|
163
389
|
ok: false,
|
|
164
|
-
optional: true,
|
|
165
390
|
detail: '.factory/ missing',
|
|
166
|
-
|
|
391
|
+
next: 'factory init',
|
|
392
|
+
...repoSection,
|
|
167
393
|
});
|
|
168
394
|
}
|
|
169
395
|
if (distFreshness) {
|
|
@@ -175,16 +401,32 @@ export function runDoctorChecks(probes) {
|
|
|
175
401
|
ok: false,
|
|
176
402
|
detail,
|
|
177
403
|
fix: "run 'npm run build' at the monorepo root",
|
|
404
|
+
next: 'npm run build',
|
|
178
405
|
});
|
|
179
406
|
}
|
|
180
407
|
return checks;
|
|
181
408
|
}
|
|
182
|
-
|
|
409
|
+
/** Row icon (#2550): ❌ only for a failure (counted in "N failed"), ➖ for a skipped row, ⚠️ for an optional warning. */
|
|
410
|
+
export function doctorCheckIcon(check) {
|
|
411
|
+
if (!check.ok && !check.optional)
|
|
412
|
+
return '❌';
|
|
413
|
+
if (/^skipped\b/.test(check.detail))
|
|
414
|
+
return '➖';
|
|
415
|
+
return check.ok ? '✅' : '⚠️';
|
|
416
|
+
}
|
|
417
|
+
/** Outside a repo the models hint is skipped because `factory models --doctor` needs a repo (#2472). */
|
|
418
|
+
export function formatDoctorChecks(checks, opts = {}) {
|
|
183
419
|
const lines = ['== factory doctor =='];
|
|
184
420
|
let passed = 0;
|
|
185
421
|
let failed = 0;
|
|
186
422
|
let warnings = 0;
|
|
187
|
-
|
|
423
|
+
const sections = [...new Set(checks.flatMap((c) => (c.section === undefined ? [] : [c.section])))];
|
|
424
|
+
const ordered = [
|
|
425
|
+
...checks.filter((c) => c.section === undefined),
|
|
426
|
+
...sections.flatMap((s) => checks.filter((c) => c.section === s)),
|
|
427
|
+
];
|
|
428
|
+
let currentSection;
|
|
429
|
+
for (const check of ordered) {
|
|
188
430
|
if (check.ok) {
|
|
189
431
|
passed++;
|
|
190
432
|
}
|
|
@@ -194,15 +436,24 @@ export function formatDoctorChecks(checks) {
|
|
|
194
436
|
else {
|
|
195
437
|
failed++;
|
|
196
438
|
}
|
|
197
|
-
|
|
439
|
+
if (check.section !== undefined && check.section !== currentSection) {
|
|
440
|
+
lines.push(`-- ${check.section} --`);
|
|
441
|
+
}
|
|
442
|
+
currentSection = check.section;
|
|
443
|
+
const icon = doctorCheckIcon(check);
|
|
198
444
|
lines.push(` ${icon} ${check.name} — ${check.detail}`);
|
|
199
445
|
if (!check.ok && check.fix) {
|
|
200
446
|
lines.push(` fix: ${check.fix}`);
|
|
201
447
|
}
|
|
448
|
+
if (!check.ok && check.next) {
|
|
449
|
+
lines.push(` next: ${check.next}`);
|
|
450
|
+
}
|
|
202
451
|
}
|
|
203
452
|
lines.push(`${passed} passed, ${failed} failed, ${warnings} warnings`);
|
|
204
453
|
lines.push('');
|
|
205
|
-
lines.push(
|
|
454
|
+
lines.push(opts.inRepo === false
|
|
455
|
+
? `Model reachability: ${NOT_IN_REPO_SKIP_DETAIL}`
|
|
456
|
+
: 'Model reachability: factory models --doctor');
|
|
206
457
|
return lines.join('\n');
|
|
207
458
|
}
|
|
208
459
|
export function doctorFailed(checks) {
|
|
@@ -292,6 +543,21 @@ export function sandboxClaudeAuthChecks(result) {
|
|
|
292
543
|
}
|
|
293
544
|
return [host, sandboxed];
|
|
294
545
|
}
|
|
546
|
+
const AGENT_AUTH_CHECKS = new Set([...HARNESS_SETUP.map(({ cli }) => `${cli} signed in`), HOST_AUTH_CHECK]);
|
|
547
|
+
/** #2496: doctor fails on agent sign-in only when no installed agent is signed in. A logged-out
|
|
548
|
+
* agent is a warning while another one is signed in, and a hard failure when none is. Skipped
|
|
549
|
+
* probes (optional passes, e.g. a `<cli> signed in` row for a CLI not on PATH) never count as signed in. */
|
|
550
|
+
export function reconcileAgentAuthChecks(checks) {
|
|
551
|
+
const auth = checks.filter((c) => AGENT_AUTH_CHECKS.has(c.name));
|
|
552
|
+
const anySignedIn = auth.some((c) => c.ok && c.optional !== true);
|
|
553
|
+
return checks.map((c) => {
|
|
554
|
+
if (!AGENT_AUTH_CHECKS.has(c.name) || c.ok)
|
|
555
|
+
return c;
|
|
556
|
+
return anySignedIn
|
|
557
|
+
? { ...c, optional: true, detail: `${c.detail} (another agent is signed in, so runs can still proceed)` }
|
|
558
|
+
: { ...c, optional: false };
|
|
559
|
+
});
|
|
560
|
+
}
|
|
295
561
|
export function leaseChecks(rows) {
|
|
296
562
|
if (rows.length === 0) {
|
|
297
563
|
return [{ name: 'port leases', ok: true, optional: true, detail: 'no active leases' }];
|
|
@@ -469,4 +735,37 @@ export function declaredHarnessChecks(probes) {
|
|
|
469
735
|
};
|
|
470
736
|
});
|
|
471
737
|
}
|
|
738
|
+
const REGISTRY_CHECK = 'models.registry';
|
|
739
|
+
/** One failed check per models.registry entry whose harness is missing or unknown;
|
|
740
|
+
* a single ok row when every entry is valid; nothing when there is no registry. */
|
|
741
|
+
export function modelsRegistryChecks(configPath, registry, validHarnesses) {
|
|
742
|
+
const entries = Object.entries(registry ?? {});
|
|
743
|
+
if (entries.length === 0)
|
|
744
|
+
return [];
|
|
745
|
+
const failed = [];
|
|
746
|
+
for (const [id, def] of entries) {
|
|
747
|
+
const field = `models.registry.${id}.harness`;
|
|
748
|
+
const name = `${REGISTRY_CHECK} ${id}`;
|
|
749
|
+
if (def.harness === undefined || def.harness.trim() === '') {
|
|
750
|
+
failed.push({
|
|
751
|
+
name,
|
|
752
|
+
ok: false,
|
|
753
|
+
detail: `${configPath}: ${field} is missing`,
|
|
754
|
+
fix: `set "harness" to one of: ${validHarnesses.join(', ')}`,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
else if (!validHarnesses.includes(def.harness)) {
|
|
758
|
+
failed.push({
|
|
759
|
+
name,
|
|
760
|
+
ok: false,
|
|
761
|
+
detail: `${configPath}: ${field} '${def.harness}' is not a known harness (valid harnesses: ${validHarnesses.join(', ')})`,
|
|
762
|
+
fix: 'use a built-in harness id or declare it under "harnesses" in the models config',
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (failed.length > 0)
|
|
767
|
+
return failed;
|
|
768
|
+
const n = entries.length;
|
|
769
|
+
return [{ name: REGISTRY_CHECK, ok: true, detail: `${n} entr${n === 1 ? 'y' : 'ies'} valid (${configPath})` }];
|
|
770
|
+
}
|
|
472
771
|
//# sourceMappingURL=doctor.js.map
|