portable-agent-layer 0.90.0 → 0.91.0
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/package.json +1 -1
- package/src/cli/doctor/agents.ts +41 -7
- package/src/cli/doctor/environment.ts +16 -8
- package/src/cli/doctor/finding.ts +7 -2
- package/src/cli/doctor/inference.ts +5 -1
- package/src/cli/doctor/render.ts +231 -50
- package/src/cli/doctor/run.ts +33 -15
- package/src/cli/index.ts +46 -165
- package/src/cli/install-flow.ts +331 -0
- package/src/cli/reinstall.ts +5 -1
- package/src/cli/setup-attribution.ts +9 -9
- package/src/cli/setup-auto-update.ts +8 -7
- package/src/cli/setup-identity.ts +10 -15
- package/src/cli/ui/banner.ts +50 -0
- package/src/cli/ui/live.ts +112 -0
- package/src/cli/ui/rail.ts +119 -0
- package/src/cli/ui/style.ts +132 -0
- package/src/cli/ui/terminal.ts +81 -0
- package/src/cli/ui/wrap.ts +19 -0
- package/src/cli/update-flow.ts +98 -0
- package/src/cli/whats-new.ts +108 -0
- package/src/targets/lib.ts +49 -6
package/package.json
CHANGED
package/src/cli/doctor/agents.ts
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
skillsDirOf,
|
|
9
9
|
} from "../../hooks/lib/agent-registry";
|
|
10
10
|
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
installedShippedAgentCount,
|
|
13
|
+
nativeAgentsDir,
|
|
14
|
+
staleShippedAgents,
|
|
15
|
+
} from "../../targets/lib";
|
|
12
16
|
import { NO_SESSION_AGENT_MESSAGE } from "../session-agent";
|
|
13
17
|
import { type Finding, type Fix, failing, optional, passed, warning } from "./finding";
|
|
14
18
|
|
|
@@ -231,7 +235,13 @@ function hookFindings(agent: AgentName, file: string, contextHook: string): Find
|
|
|
231
235
|
),
|
|
232
236
|
];
|
|
233
237
|
|
|
234
|
-
const findings = [
|
|
238
|
+
const findings = [
|
|
239
|
+
passed(
|
|
240
|
+
`${agent}.hooks`,
|
|
241
|
+
`${label}: ${pal.length} hooks registered`,
|
|
242
|
+
`${pal.length} hooks`
|
|
243
|
+
),
|
|
244
|
+
];
|
|
235
245
|
const undeclared = pal.filter((c) => !declaresAgent(c, agent));
|
|
236
246
|
if (undeclared.length > 0)
|
|
237
247
|
findings.push(
|
|
@@ -272,14 +282,20 @@ function opencodePluginFindings(): Finding[] {
|
|
|
272
282
|
reinstall("opencode")
|
|
273
283
|
),
|
|
274
284
|
];
|
|
275
|
-
return [
|
|
285
|
+
return [
|
|
286
|
+
passed("opencode.hooks", "opencode plugin installed and current", "plugin current"),
|
|
287
|
+
];
|
|
276
288
|
}
|
|
277
289
|
|
|
278
290
|
function subagentFinding(agent: AgentName): Finding {
|
|
279
291
|
const label = labelOf(agent);
|
|
280
292
|
const stale = staleShippedAgents(nativeAgentsDir(agent), agent);
|
|
281
293
|
if (stale.length === 0)
|
|
282
|
-
return passed(
|
|
294
|
+
return passed(
|
|
295
|
+
`${agent}.subagents`,
|
|
296
|
+
`${label}: subagents match this PAL version`,
|
|
297
|
+
"subagents current"
|
|
298
|
+
);
|
|
283
299
|
return warning(
|
|
284
300
|
`${agent}.subagents`,
|
|
285
301
|
`${label}: installed subagents differ from this PAL version — ${stale.join(", ")}`,
|
|
@@ -298,7 +314,7 @@ function oneAgentFindings(agent: AgentName): Finding[] {
|
|
|
298
314
|
const skills = countSkills(skillsDirOf(agent));
|
|
299
315
|
const findings = [
|
|
300
316
|
skills > 0
|
|
301
|
-
? passed(`${agent}.skills`, `${label}: ${skills} skills`)
|
|
317
|
+
? passed(`${agent}.skills`, `${label}: ${skills} skills`, `${skills} skills`)
|
|
302
318
|
: warning(`${agent}.skills`, `${label} has no PAL skills`, reinstall(agent)),
|
|
303
319
|
];
|
|
304
320
|
findings.push(
|
|
@@ -311,13 +327,31 @@ function oneAgentFindings(agent: AgentName): Finding[] {
|
|
|
311
327
|
const { file, name } = layout.instructions;
|
|
312
328
|
findings.push(
|
|
313
329
|
existsSync(file())
|
|
314
|
-
? passed(`${agent}.instructions`, `${name} present
|
|
330
|
+
? passed(`${agent}.instructions`, `${name} present`, name)
|
|
315
331
|
: failing(`${agent}.instructions`, `${name} is missing`, reinstall(agent))
|
|
316
332
|
);
|
|
317
333
|
}
|
|
318
334
|
return findings;
|
|
319
335
|
}
|
|
320
336
|
|
|
337
|
+
function palHookCount(agent: AgentName): number {
|
|
338
|
+
const file = WIRING[agent].hookFile?.();
|
|
339
|
+
const commands = file ? readHookCommands(file) : "missing";
|
|
340
|
+
return Array.isArray(commands) ? commands.filter((c) => isPalHook(c, agent)).length : 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** What PAL put into an agent, in the few words an install step reports it in. */
|
|
344
|
+
export function agentInventory(agent: AgentName): string[] {
|
|
345
|
+
const plural = (count: number, noun: string) =>
|
|
346
|
+
`${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
347
|
+
const wiring = WIRING[agent].hookFile ? plural(palHookCount(agent), "hook") : "plugin";
|
|
348
|
+
return [
|
|
349
|
+
plural(countSkills(skillsDirOf(agent)), "skill"),
|
|
350
|
+
wiring,
|
|
351
|
+
plural(installedShippedAgentCount(agent), "agent"),
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
|
|
321
355
|
export function agentFindings(agents: AgentName[]): Finding[] {
|
|
322
356
|
if (agents.length === 0) return [];
|
|
323
357
|
const agentsMd = resolve(platform.opencodeDir(), "AGENTS.md");
|
|
@@ -345,7 +379,7 @@ export function rosterFindings(agents: AgentName[]): Finding[] {
|
|
|
345
379
|
return [
|
|
346
380
|
optional(
|
|
347
381
|
"agents.others",
|
|
348
|
-
|
|
382
|
+
`${others.map(labelOf).join(", ")} — other agents PAL supports`,
|
|
349
383
|
{
|
|
350
384
|
say: `install it, then run pal cli install --${others.length === 1 ? others[0] : "<agent>"}`,
|
|
351
385
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
@@ -98,16 +98,24 @@ function thisBrowserHost(): BrowserHost {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function runInShell(command: string): boolean {
|
|
102
|
-
return
|
|
101
|
+
function runInShell(command: string): Promise<boolean> {
|
|
102
|
+
return new Promise((done) => {
|
|
103
|
+
const child = spawn(command, { cwd: palPkg(), shell: true, stdio: "ignore" });
|
|
104
|
+
child.on("error", () => done(false));
|
|
105
|
+
child.on("close", (code) => done(code === 0));
|
|
106
|
+
});
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Null when the right build is already there. A failure is left to the
|
|
111
|
+
* doctor's report that follows, which names the fix.
|
|
112
|
+
*/
|
|
113
|
+
export async function installChromium(
|
|
107
114
|
host: BrowserHost = thisBrowserHost(),
|
|
108
|
-
run: (command: string) => boolean = runInShell
|
|
109
|
-
):
|
|
110
|
-
if (
|
|
115
|
+
run: (command: string) => Promise<boolean> | boolean = runInShell
|
|
116
|
+
): Promise<boolean | null> {
|
|
117
|
+
if (hasChromium(host)) return null;
|
|
118
|
+
return run(chromiumInstallCommand(host));
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
interface PalInstall {
|
|
@@ -11,15 +11,20 @@ export type Fix =
|
|
|
11
11
|
| { say: string; command?: undefined }
|
|
12
12
|
| { say: string; command: string; external: boolean };
|
|
13
13
|
|
|
14
|
+
export type Group = "Environment" | "Agents" | "Inference" | "State";
|
|
15
|
+
|
|
14
16
|
export interface Finding {
|
|
15
17
|
id: string;
|
|
16
18
|
severity: Severity;
|
|
17
19
|
title: string;
|
|
18
20
|
fix?: Fix;
|
|
21
|
+
group?: Group;
|
|
22
|
+
/** The few words a passing check takes up when its agent's checks share one line. */
|
|
23
|
+
brief?: string;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
export function passed(id: string, title: string): Finding {
|
|
22
|
-
return { id, severity: "ok", title };
|
|
26
|
+
export function passed(id: string, title: string, brief?: string): Finding {
|
|
27
|
+
return brief ? { id, severity: "ok", title, brief } : { id, severity: "ok", title };
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export function failing(id: string, title: string, fix?: Fix): Finding {
|
|
@@ -183,7 +183,11 @@ export function cursorPlanFinding(about: string): Finding | null {
|
|
|
183
183
|
`Cursor is set to ${model}, but its free plan only runs Auto — background inference fails`,
|
|
184
184
|
{ say: "Switch Cursor's model to Auto, or upgrade the plan" }
|
|
185
185
|
);
|
|
186
|
-
return passed(
|
|
186
|
+
return passed(
|
|
187
|
+
"cursor.model",
|
|
188
|
+
`Cursor model ${model} runs on the ${tier} plan`,
|
|
189
|
+
`model ${model} on the ${tier} plan`
|
|
190
|
+
);
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
function cursorAbout(): string {
|
package/src/cli/doctor/render.ts
CHANGED
|
@@ -1,80 +1,261 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AGENT_REGISTRY, type AgentName } from "../../hooks/lib/agent-registry";
|
|
2
|
+
import { healthBadge, mark, seconds } from "../ui/rail";
|
|
3
|
+
import { homeShortened, type Style, visibleWidth } from "../ui/style";
|
|
4
|
+
import { wrapWords } from "../ui/wrap";
|
|
5
|
+
import type { Finding, Fix, Group, Severity } from "./finding";
|
|
2
6
|
|
|
3
7
|
export interface DoctorReport {
|
|
4
8
|
version: string;
|
|
5
9
|
agents: string[];
|
|
6
10
|
findings: Finding[];
|
|
11
|
+
elapsedMs?: number;
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
interface RenderOptions {
|
|
10
15
|
verbose: boolean;
|
|
11
|
-
|
|
16
|
+
style: Style;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
19
|
+
export interface Tally {
|
|
20
|
+
fails: Finding[];
|
|
21
|
+
warns: Finding[];
|
|
22
|
+
optionals: Finding[];
|
|
23
|
+
passes: Finding[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tally(findings: Finding[]): Tally {
|
|
27
|
+
const of = (severity: Severity) => findings.filter((f) => f.severity === severity);
|
|
28
|
+
return {
|
|
29
|
+
fails: of("fail"),
|
|
30
|
+
warns: of("warn"),
|
|
31
|
+
optionals: of("optional"),
|
|
32
|
+
passes: of("ok"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
20
35
|
|
|
21
36
|
function plural(count: number, noun: string): string {
|
|
22
37
|
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
23
38
|
}
|
|
24
39
|
|
|
40
|
+
export function plainSummary({ fails, warns, passes }: Tally): string {
|
|
41
|
+
return [
|
|
42
|
+
fails.length > 0 ? `${fails.length} failing` : "",
|
|
43
|
+
warns.length > 0 ? plural(warns.length, "warning") : "",
|
|
44
|
+
`${plural(passes.length, "check")} passed`,
|
|
45
|
+
]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(", ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PLAIN_LEVEL: Record<Severity, string> = {
|
|
51
|
+
fail: "fail",
|
|
52
|
+
warn: "warn",
|
|
53
|
+
optional: "optional",
|
|
54
|
+
ok: "ok ",
|
|
55
|
+
};
|
|
56
|
+
|
|
25
57
|
function fixText(fix: Fix): string {
|
|
26
58
|
return fix.command ? `${fix.say}: ${fix.command}` : fix.say;
|
|
27
59
|
}
|
|
28
60
|
|
|
29
|
-
function
|
|
61
|
+
function plainLine(finding: Finding): string {
|
|
62
|
+
const fix = finding.fix ? ` -> ${fixText(finding.fix)}` : "";
|
|
63
|
+
return `${PLAIN_LEVEL[finding.severity]} ${finding.title}${fix}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function plainProblemLines(t: Tally): string[] {
|
|
67
|
+
return [...t.fails, ...t.warns].map(plainLine);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function plainReport(report: DoctorReport, verbose: boolean): string[] {
|
|
71
|
+
const t = tally(report.findings);
|
|
30
72
|
const agents = report.agents.length > 0 ? report.agents.join(", ") : "no agent";
|
|
31
|
-
return
|
|
73
|
+
return [
|
|
74
|
+
`PAL ${report.version} · ${agents} · ${plainSummary(t)}`,
|
|
75
|
+
...(verbose ? t.passes.map(plainLine) : []),
|
|
76
|
+
...plainProblemLines(t),
|
|
77
|
+
...t.optionals.map(plainLine),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const TITLE_REST = " — ";
|
|
82
|
+
|
|
83
|
+
interface Segment {
|
|
84
|
+
text: string;
|
|
85
|
+
paint: (text: string) => string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const asIs = (text: string) => text;
|
|
89
|
+
|
|
90
|
+
/** Wraps the words of several differently coloured pieces as if they were one sentence. */
|
|
91
|
+
export function wrapSegments(segments: Segment[], width: number): string[] {
|
|
92
|
+
const text = segments.map((s) => s.text).join("");
|
|
93
|
+
let end = 0;
|
|
94
|
+
const bounds = segments.map((s) => {
|
|
95
|
+
end += s.text.length;
|
|
96
|
+
return end;
|
|
97
|
+
});
|
|
98
|
+
return wrapWords(text, width).map(({ text: chunk, start }) =>
|
|
99
|
+
segments
|
|
100
|
+
.map((segment, i) => {
|
|
101
|
+
const from = Math.max(start, bounds[i - 1] ?? 0);
|
|
102
|
+
const to = Math.min(start + chunk.length, bounds[i]);
|
|
103
|
+
return to > from ? segment.paint(text.slice(from, to)) : "";
|
|
104
|
+
})
|
|
105
|
+
.join("")
|
|
106
|
+
);
|
|
32
107
|
}
|
|
33
108
|
|
|
34
|
-
|
|
109
|
+
/** A title reads "what — more about it"; the part after the dash is quieter. */
|
|
110
|
+
function titleSegments(
|
|
111
|
+
style: Style,
|
|
112
|
+
title: string,
|
|
113
|
+
main: (text: string) => string,
|
|
114
|
+
separator = TITLE_REST
|
|
115
|
+
): Segment[] {
|
|
116
|
+
const text = homeShortened(title);
|
|
117
|
+
const split = text.indexOf(TITLE_REST);
|
|
118
|
+
if (split < 0) return [{ text, paint: main }];
|
|
35
119
|
return [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
120
|
+
{ text: text.slice(0, split), paint: main },
|
|
121
|
+
{ text: `${separator}${text.slice(split + TITLE_REST.length)}`, paint: style.dim },
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hanging(lines: string[], first: string, rest: string): string[] {
|
|
126
|
+
return lines.map((line, i) => `${i === 0 ? first : rest}${line}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function fixLines(style: Style, fix: Fix, width: number, indent: string): string[] {
|
|
130
|
+
const arrow = `${indent}${style.dim(style.glyph.arrow)} `;
|
|
131
|
+
const pad = " ".repeat(indent.length + style.glyph.arrow.length + 1);
|
|
132
|
+
const segment = fix.command
|
|
133
|
+
? { text: fix.command, paint: style.cmd }
|
|
134
|
+
: { text: homeShortened(fix.say), paint: asIs };
|
|
135
|
+
return hanging(wrapSegments([segment], width), arrow, pad);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const textWidth = (style: Style, indent: string) =>
|
|
139
|
+
Math.max(30, style.term.width - indent.length - 4);
|
|
140
|
+
|
|
141
|
+
export function problemLines(style: Style, t: Tally, indent = " "): string[] {
|
|
142
|
+
const width = textWidth(style, indent);
|
|
143
|
+
return [...t.fails, ...t.warns].flatMap((problem) => {
|
|
144
|
+
const kind = problem.severity === "fail" ? "fail" : "warn";
|
|
145
|
+
const main = kind === "fail" ? style.bold : asIs;
|
|
146
|
+
const head = `${indent}${mark(style, kind)} `;
|
|
147
|
+
return [
|
|
148
|
+
...hanging(
|
|
149
|
+
wrapSegments(titleSegments(style, problem.title, main), width),
|
|
150
|
+
head,
|
|
151
|
+
`${indent} `
|
|
152
|
+
),
|
|
153
|
+
...(problem.fix ? fixLines(style, problem.fix, width, `${indent} `) : []),
|
|
154
|
+
];
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function optionalLines(style: Style, item: Finding): string[] {
|
|
159
|
+
const name = homeShortened(item.title.split(TITLE_REST)[0]);
|
|
160
|
+
const how = item.fix?.command
|
|
161
|
+
? { text: item.fix.command, paint: style.cmd }
|
|
162
|
+
: { text: homeShortened(item.fix?.say ?? ""), paint: style.dim };
|
|
163
|
+
const segments = [
|
|
164
|
+
{ text: name, paint: style.soft },
|
|
165
|
+
{ text: ` ${style.glyph.dash} `, paint: style.dim },
|
|
166
|
+
how,
|
|
167
|
+
];
|
|
168
|
+
return hanging(
|
|
169
|
+
wrapSegments(segments, textWidth(style, " ")),
|
|
170
|
+
` ${style.dim(style.glyph.dot)} `,
|
|
171
|
+
" "
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const GROUP_ORDER: Group[] = ["Environment", "Agents", "Inference", "State"];
|
|
176
|
+
|
|
177
|
+
function agentOf(finding: Finding, agents: string[]): string | undefined {
|
|
178
|
+
return agents.find((agent) => finding.id.startsWith(`${agent}.`));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function passLines(style: Style, finding: Finding): string[] {
|
|
182
|
+
const segments = titleSegments(style, finding.title, asIs, ` ${style.glyph.dot} `);
|
|
183
|
+
return hanging(
|
|
184
|
+
wrapSegments(segments, textWidth(style, " ")),
|
|
185
|
+
` ${mark(style, "ok")} `,
|
|
186
|
+
" "
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function agentLines(style: Style, agent: string, passes: Finding[]): string[] {
|
|
191
|
+
const label = (AGENT_REGISTRY[agent as AgentName]?.label ?? agent).padEnd(12);
|
|
192
|
+
const briefs = passes.map((f) => f.brief ?? f.title).join(` ${style.glyph.dot} `);
|
|
193
|
+
const width = textWidth(style, " ") - label.length - 1;
|
|
194
|
+
return hanging(
|
|
195
|
+
wrapSegments([{ text: briefs, paint: style.dim }], width),
|
|
196
|
+
` ${mark(style, "ok")} ${label} `,
|
|
197
|
+
" ".repeat(label.length + 5)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function groupLines(style: Style, group: Group, passes: Finding[], agents: string[]) {
|
|
202
|
+
const inGroup = passes.filter((f) => (f.group ?? "State") === group);
|
|
203
|
+
if (inGroup.length === 0) return [];
|
|
204
|
+
const own = inGroup.filter((f) => !agentOf(f, agents));
|
|
205
|
+
const perAgent = agents
|
|
206
|
+
.map((agent) => [agent, inGroup.filter((f) => agentOf(f, agents) === agent)] as const)
|
|
207
|
+
.filter(([, found]) => found.length > 0);
|
|
208
|
+
return [
|
|
209
|
+
` ${style.bold(style.gradient(group))}`,
|
|
210
|
+
...own.flatMap((f) => passLines(style, f)),
|
|
211
|
+
...perAgent.flatMap(([agent, found]) => agentLines(style, agent, found)),
|
|
212
|
+
"",
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function headline(style: Style, t: Tally, report: DoctorReport): string[] {
|
|
217
|
+
const count = { fails: t.fails.length, warns: t.warns.length };
|
|
218
|
+
const agents = style.soft(
|
|
219
|
+
report.agents.length > 0 ? report.agents.join(` ${style.glyph.dot} `) : "no agent"
|
|
220
|
+
);
|
|
221
|
+
const pal = style.bold(`PAL ${report.version}`);
|
|
222
|
+
const head = ` ${healthBadge(style, count, true)} ${pal}`;
|
|
223
|
+
const oneLine = `${head} ${agents}`;
|
|
224
|
+
return visibleWidth(oneLine) <= style.term.width ? [oneLine] : [head, ` ${agents}`];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function richReport(report: DoctorReport, verbose: boolean, style: Style): string[] {
|
|
228
|
+
const t = tally(report.findings);
|
|
229
|
+
const elapsed =
|
|
230
|
+
report.elapsedMs === undefined
|
|
231
|
+
? ""
|
|
232
|
+
: ` ${style.glyph.dot} ${seconds(report.elapsedMs)}`;
|
|
233
|
+
const footer = `${plural(t.passes.length, "check")} passed${elapsed}`;
|
|
234
|
+
const problems = problemLines(style, t);
|
|
235
|
+
return [
|
|
236
|
+
"",
|
|
237
|
+
...headline(style, t, report),
|
|
238
|
+
"",
|
|
239
|
+
...(verbose
|
|
240
|
+
? GROUP_ORDER.flatMap((g) => groupLines(style, g, t.passes, report.agents))
|
|
241
|
+
: []),
|
|
242
|
+
...(problems.length > 0 ? [...problems, ""] : []),
|
|
243
|
+
...(t.optionals.length > 0
|
|
244
|
+
? [
|
|
245
|
+
` ${style.dim("Optional")}`,
|
|
246
|
+
...t.optionals.flatMap((o) => optionalLines(style, o)),
|
|
247
|
+
"",
|
|
248
|
+
]
|
|
249
|
+
: []),
|
|
250
|
+
` ${style.dim(footer)}`,
|
|
251
|
+
"",
|
|
252
|
+
];
|
|
42
253
|
}
|
|
43
254
|
|
|
44
255
|
export function renderReport(report: DoctorReport, options: RenderOptions): string[] {
|
|
45
|
-
|
|
46
|
-
options.
|
|
47
|
-
|
|
48
|
-
: MARK[severity].glyph;
|
|
49
|
-
const of = (severity: Severity) =>
|
|
50
|
-
report.findings.filter((finding) => finding.severity === severity);
|
|
51
|
-
const fails = of("fail");
|
|
52
|
-
const warns = of("warn");
|
|
53
|
-
const optionals = of("optional");
|
|
54
|
-
const passes = of("ok");
|
|
55
|
-
|
|
56
|
-
const lines = [headline(report, passes.length)];
|
|
57
|
-
if (options.verbose) {
|
|
58
|
-
lines.push("", ...passes.map((finding) => ` ${mark("ok")} ${finding.title}`));
|
|
59
|
-
}
|
|
60
|
-
const problems = [...fails, ...warns];
|
|
61
|
-
if (problems.length > 0) lines.push("");
|
|
62
|
-
for (const problem of problems) {
|
|
63
|
-
lines.push(` ${mark(problem.severity)} ${problem.title}`);
|
|
64
|
-
if (problem.fix) lines.push(` → ${fixText(problem.fix)}`);
|
|
65
|
-
}
|
|
66
|
-
if (optionals.length > 0) {
|
|
67
|
-
lines.push("", "Optional — not set up:");
|
|
68
|
-
for (const item of optionals) {
|
|
69
|
-
const how = item.fix?.command ?? item.fix?.say;
|
|
70
|
-
const suffix = how ? `: ${how}` : "";
|
|
71
|
-
lines.push(` ${mark("optional")} ${item.title}${suffix}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (problems.length > 0) {
|
|
75
|
-
lines.push("", summary(fails.length, warns.length, passes.length));
|
|
76
|
-
}
|
|
77
|
-
return lines;
|
|
256
|
+
return options.style.term.rich
|
|
257
|
+
? richReport(report, options.verbose, options.style)
|
|
258
|
+
: plainReport(report, options.verbose);
|
|
78
259
|
}
|
|
79
260
|
|
|
80
261
|
export function doctorExitCode(findings: Finding[]): 0 | 1 {
|
package/src/cli/doctor/run.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getInstalledVersion } from "../../hooks/handlers/update-check";
|
|
2
|
+
import { createStyle } from "../ui/style";
|
|
2
3
|
import {
|
|
3
4
|
agentFindings,
|
|
4
5
|
type DoctorResult,
|
|
@@ -7,38 +8,55 @@ import {
|
|
|
7
8
|
rosterFindings,
|
|
8
9
|
} from "./agents";
|
|
9
10
|
import { environmentFindings } from "./environment";
|
|
10
|
-
import type { Finding } from "./finding";
|
|
11
|
+
import type { Finding, Group } from "./finding";
|
|
11
12
|
import { healthFindings } from "./health";
|
|
12
13
|
import { inferenceFindings } from "./inference";
|
|
13
|
-
import { doctorExitCode, renderReport } from "./render";
|
|
14
|
+
import { type DoctorReport, doctorExitCode, renderReport } from "./render";
|
|
14
15
|
import { stateFindings } from "./state";
|
|
15
16
|
|
|
17
|
+
const inGroup = (group: Group, findings: Finding[]) =>
|
|
18
|
+
findings.map((finding) => ({ ...finding, group }));
|
|
19
|
+
|
|
20
|
+
const healthGroup = (finding: Finding): Group =>
|
|
21
|
+
finding.id.startsWith("hook-errors") ? "Inference" : "State";
|
|
22
|
+
|
|
16
23
|
function doctorFindings(health: DoctorResult): Finding[] {
|
|
17
24
|
const agents = installedAgents(health);
|
|
18
25
|
return [
|
|
19
|
-
...rosterFindings(agents),
|
|
20
|
-
...environmentFindings(health.rtk),
|
|
21
|
-
...stateFindings(),
|
|
22
|
-
...agentFindings(agents),
|
|
23
|
-
...inferenceFindings(agents),
|
|
24
|
-
...healthFindings(),
|
|
26
|
+
...inGroup("Agents", rosterFindings(agents)),
|
|
27
|
+
...inGroup("Environment", environmentFindings(health.rtk)),
|
|
28
|
+
...inGroup("State", stateFindings()),
|
|
29
|
+
...inGroup("Agents", agentFindings(agents)),
|
|
30
|
+
...inGroup("Inference", inferenceFindings(agents)),
|
|
31
|
+
...healthFindings().map((finding) => ({ ...finding, group: healthGroup(finding) })),
|
|
25
32
|
];
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
export function doctorReport(
|
|
36
|
+
health: DoctorResult,
|
|
37
|
+
started = performance.now()
|
|
38
|
+
): DoctorReport {
|
|
39
|
+
const findings = doctorFindings(health);
|
|
40
|
+
return {
|
|
32
41
|
version: getInstalledVersion(),
|
|
33
42
|
agents: installedAgents(health),
|
|
34
|
-
findings
|
|
43
|
+
findings,
|
|
44
|
+
elapsedMs: performance.now() - started,
|
|
35
45
|
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `pal cli doctor [--verbose] [--json]` — prints the report and returns the exit code. */
|
|
49
|
+
export function runDoctor(args: string[], health?: DoctorResult): number {
|
|
50
|
+
if (process.env.PAL_SKIP_DOCTOR === "1") return 0;
|
|
51
|
+
const started = performance.now();
|
|
52
|
+
const report = doctorReport(health ?? detectAgents(), started);
|
|
36
53
|
if (args.includes("--json")) {
|
|
37
|
-
|
|
54
|
+
const { elapsedMs: _elapsed, ...json } = report;
|
|
55
|
+
console.log(JSON.stringify(json, null, 2));
|
|
38
56
|
} else {
|
|
39
57
|
const lines = renderReport(report, {
|
|
40
58
|
verbose: args.includes("--verbose"),
|
|
41
|
-
|
|
59
|
+
style: createStyle(),
|
|
42
60
|
});
|
|
43
61
|
console.log(lines.join("\n"));
|
|
44
62
|
}
|