@canonical/pragma-cli 0.35.0 → 0.36.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/README.md +2 -2
- package/bundled-skills/add-standard/SKILL.md +338 -0
- package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
- package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
- package/bundled-skills/anatomy-author/SKILL.md +1226 -0
- package/bundled-skills/design-auditor/SKILL.md +455 -0
- package/bundled-skills/implement-component/SKILL.md +333 -0
- package/bundled-skills/specify-component/SKILL.md +331 -0
- package/bundled-skills/specify-pattern/SKILL.md +292 -0
- package/dist/package.json +16 -15
- package/dist/pragma.conf.js +236 -34
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/bin.js +1 -1
- package/dist/src/bin.js.map +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
- package/dist/src/capabilities/create/mount.js +4 -3
- package/dist/src/capabilities/create/mount.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/index.js +6 -6
- package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.render.js +43 -64
- package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
- package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
- package/dist/src/capabilities/doctor/runChecks.js +14 -11
- package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
- package/dist/src/capabilities/graph/query.render.js +2 -2
- package/dist/src/capabilities/graph/query.render.js.map +1 -1
- package/dist/src/capabilities/info/collectInfo.js +8 -2
- package/dist/src/capabilities/info/collectInfo.js.map +1 -1
- package/dist/src/capabilities/info/info.render.js +6 -2
- package/dist/src/capabilities/info/info.render.js.map +1 -1
- package/dist/src/capabilities/prompt/prompt.render.js +2 -2
- package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +43 -39
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/mode.js +2 -2
- package/dist/src/capabilities/setup/operations/index.js +4 -4
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +92 -45
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +88 -36
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +97 -51
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +325 -47
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/plan.render.js +144 -66
- package/dist/src/capabilities/setup/plan.render.js.map +1 -1
- package/dist/src/capabilities/setup/setup.render.js +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +86 -33
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +110 -45
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +6 -5
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/capabilities/shared/packageManager.js +300 -54
- package/dist/src/capabilities/shared/packageManager.js.map +1 -1
- package/dist/src/capabilities/skill/discover.js +97 -11
- package/dist/src/capabilities/skill/discover.js.map +1 -1
- package/dist/src/capabilities/skill/render.js +2 -2
- package/dist/src/capabilities/skill/render.js.map +1 -1
- package/dist/src/capabilities/sources/installSkills.js +145 -3
- package/dist/src/capabilities/sources/installSkills.js.map +1 -1
- package/dist/src/capabilities/sources/runUpdate.js +68 -2
- package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
- package/dist/src/capabilities/sources/update.verb.js +3 -3
- package/dist/src/capabilities/sources/update.verb.js.map +1 -1
- package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
- package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
- package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
- package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
- package/dist/src/constants.js +6 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/index.js +11 -2
- package/dist/src/kernel/index.js.map +1 -1
- package/dist/src/kernel/interactivity.js +68 -0
- package/dist/src/kernel/interactivity.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +3 -3
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +63 -12
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +57 -4
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +54 -2
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +306 -43
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +23 -23
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/globalFlags.js +4 -1
- package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
- package/dist/src/kernel/project/cli/helpFormat.js +1 -2
- package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +34 -4
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +1 -1
- package/dist/src/kernel/render/style.js +29 -9
- package/dist/src/kernel/render/style.js.map +1 -1
- package/dist/src/kernel/render/vocabulary.js +174 -0
- package/dist/src/kernel/render/vocabulary.js.map +1 -0
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +16 -15
- package/pragma.conf.ts +241 -34
- package/src/bin.ts +8 -1
- package/src/capabilities/block.parity.test.ts +25 -11
- package/src/capabilities/block.shipped.exec.test.ts +282 -0
- package/src/capabilities/block.tierRank.exec.test.ts +300 -0
- package/src/capabilities/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/create/createGrammar.test.ts +1 -1
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
- package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
- package/src/capabilities/doctor/checks/index.ts +6 -6
- package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
- package/src/capabilities/doctor/doctor.render.test.ts +108 -32
- package/src/capabilities/doctor/doctor.render.ts +56 -80
- package/src/capabilities/doctor/doctor.test.ts +247 -35
- package/src/capabilities/doctor/doctor.verb.ts +7 -2
- package/src/capabilities/doctor/runChecks.ts +14 -11
- package/src/capabilities/doctor/types.ts +15 -24
- package/src/capabilities/emptyRecovery.test.ts +10 -2
- package/src/capabilities/graph/query.render.ts +2 -2
- package/src/capabilities/info/collectInfo.ts +8 -1
- package/src/capabilities/info/info.render.ts +6 -2
- package/src/capabilities/info/info.test.ts +37 -1
- package/src/capabilities/info/types.ts +10 -2
- package/src/capabilities/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- package/src/capabilities/scopeTypeSync.test.ts +29 -15
- package/src/capabilities/setup/buildPlan.ts +45 -41
- package/src/capabilities/setup/index.ts +1 -1
- package/src/capabilities/setup/mode.ts +2 -2
- package/src/capabilities/setup/operations/index.ts +6 -1
- package/src/capabilities/setup/operations/setupGenerator.ts +144 -70
- package/src/capabilities/setup/operations/setupLsp.ts +129 -37
- package/src/capabilities/setup/operations/setupMcp.ts +132 -61
- package/src/capabilities/setup/operations/setupSkills.ts +392 -58
- package/src/capabilities/setup/plan.render.ts +171 -76
- package/src/capabilities/setup/plan.ts +17 -12
- package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
- package/src/capabilities/setup/setup.render.test.ts +50 -36
- package/src/capabilities/setup/setup.render.ts +1 -1
- package/src/capabilities/setup/setup.test.ts +881 -50
- package/src/capabilities/setup/setup.verb.ts +91 -33
- package/src/capabilities/setup/targets.ts +120 -52
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +17 -6
- package/src/capabilities/shared/packageManager.test.ts +421 -6
- package/src/capabilities/shared/packageManager.ts +394 -59
- package/src/capabilities/skill/bundledSkills.test.ts +347 -0
- package/src/capabilities/skill/discover.ts +100 -11
- package/src/capabilities/skill/render.ts +2 -2
- package/src/capabilities/skill/skill.test.ts +17 -4
- package/src/capabilities/sources/installSkills.ts +181 -3
- package/src/capabilities/sources/runUpdate.ts +78 -2
- package/src/capabilities/sources/sources.test.ts +228 -0
- package/src/capabilities/sources/update.verb.ts +3 -3
- package/src/capabilities/surface.test.ts +2 -2
- package/src/capabilities/upgrade/runUpgrade.ts +45 -6
- package/src/capabilities/upgrade/types.ts +10 -2
- package/src/capabilities/upgrade/upgrade.render.ts +9 -2
- package/src/capabilities/upgrade/upgrade.test.ts +72 -0
- package/src/constants.ts +6 -1
- package/src/identity.test.ts +18 -5
- package/src/kernel/completion/entitySource.test.ts +15 -2
- package/src/kernel/completion/safety.test.ts +16 -9
- package/src/kernel/config/defaults.test.ts +2 -1
- package/src/kernel/index.ts +11 -2
- package/src/kernel/interactivity.test.ts +76 -0
- package/src/kernel/interactivity.ts +69 -0
- package/src/kernel/packs/compile.test.ts +92 -0
- package/src/kernel/packs/renderPack.ts +3 -3
- package/src/kernel/packs/resolveEntity.test.ts +75 -14
- package/src/kernel/packs/resolveEntity.ts +88 -24
- package/src/kernel/packs/runBodies.ts +84 -19
- package/src/kernel/packs/schema.ts +60 -4
- package/src/kernel/packs/sparql/applyFilters.ts +125 -4
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +323 -45
- package/src/kernel/packs/toolDescription.test.ts +35 -1
- package/src/kernel/packs/types.ts +125 -2
- package/src/kernel/project/cli/buildProgram.test.ts +73 -2
- package/src/kernel/project/cli/buildProgram.ts +8 -1
- package/src/kernel/project/cli/dispatch.test.ts +5 -4
- package/src/kernel/project/cli/dispatch.ts +25 -24
- package/src/kernel/project/cli/globalFlags.ts +4 -1
- package/src/kernel/project/cli/helpFormat.ts +1 -2
- package/src/kernel/project/cli/rootHelp.ts +36 -3
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- package/src/kernel/render/renderers.ts +1 -1
- package/src/kernel/render/style.ts +32 -9
- package/src/kernel/render/vocabulary.ts +276 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/types.ts +35 -6
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/docExamples.test.ts +4 -2
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
- package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
- package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
- package/src/testing/eval/cases/readNouns.ts +245 -9
- package/src/testing/fixtures/blockGraph.ts +28 -13
- package/src/testing/fixtures/graph/canonical.ts +95 -12
- package/dist/src/capabilities/shared/bands.js +0 -16
- package/dist/src/capabilities/shared/bands.js.map +0 -1
- package/src/capabilities/shared/bands.ts +0 -16
|
@@ -10,58 +10,26 @@
|
|
|
10
10
|
* Glyphs are plain constants tinted at render time; nothing is baked at load.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import chalk from "chalk";
|
|
14
13
|
import { BIN_NAME } from "../../constants.js";
|
|
15
14
|
import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
|
|
15
|
+
import {
|
|
16
|
+
CHECK_GLYPHS,
|
|
17
|
+
checkRemedyWord,
|
|
18
|
+
FIX_ARROW,
|
|
19
|
+
SCOPE_LABELS,
|
|
20
|
+
SUB_BULLET,
|
|
21
|
+
SUB_INDENT,
|
|
22
|
+
} from "../../kernel/render/vocabulary.js";
|
|
16
23
|
import type { Formatters } from "../../kernel/spec/index.js";
|
|
17
|
-
import { BAND_LABELS } from "../shared/index.js";
|
|
18
24
|
import type { CheckResult, CheckStatus, DoctorData } from "./types.js";
|
|
19
25
|
|
|
20
|
-
/**
|
|
21
|
-
* Uncolored status glyphs — tinted at render time (never baked at module load).
|
|
22
|
-
* `available` gets its own glyph, distinct from ✗: an opt-in integration that
|
|
23
|
-
* is not set up is not a fault, and rendering it as one would train users to
|
|
24
|
-
* ignore the failure count. The glyph pairs with the word in the tally, so the
|
|
25
|
-
* distinction survives with color stripped.
|
|
26
|
-
*/
|
|
27
|
-
const STATUS_GLYPHS: Record<CheckStatus, string> = {
|
|
28
|
-
pass: "✓",
|
|
29
|
-
fail: "✗",
|
|
30
|
-
available: "◇",
|
|
31
|
-
skip: "○",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const SUB_BULLET = "·";
|
|
35
|
-
const FIX_ARROW = "↳";
|
|
36
|
-
const INDENT = " "; // aligns sub-lines under the check name
|
|
37
|
-
|
|
38
|
-
/** The TTY styler plus `red` — the one tint {@link RenderStyle} does not carry. */
|
|
39
|
-
interface DoctorStyle extends RenderStyle {
|
|
40
|
-
red(text: string): string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Build the doctor styler for this process's stdout: the shared TTY seam, plus a
|
|
45
|
-
* `red` gated on the SAME decision, so a fail tint appears only on a color TTY.
|
|
46
|
-
*
|
|
47
|
-
* @returns A colorizing styler on a color-capable TTY, else the identity styler.
|
|
48
|
-
* @note Impure — {@link defaultStyle} reads `process.stdout.isTTY` + chalk level.
|
|
49
|
-
*/
|
|
50
|
-
function doctorStyle(): DoctorStyle {
|
|
51
|
-
const style = defaultStyle();
|
|
52
|
-
return {
|
|
53
|
-
...style,
|
|
54
|
-
red: style.enabled ? (text) => chalk.red(text) : (text) => text,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
26
|
/**
|
|
59
27
|
* Tint a status glyph by meaning — green pass, red fail, cyan available
|
|
60
28
|
* (actionable-but-optional, the same tint as the `fix:` arrow it points at),
|
|
61
29
|
* yellow skip.
|
|
62
30
|
*/
|
|
63
|
-
function paintGlyph(status: CheckStatus, style:
|
|
64
|
-
const glyph =
|
|
31
|
+
function paintGlyph(status: CheckStatus, style: RenderStyle): string {
|
|
32
|
+
const glyph = CHECK_GLYPHS[status];
|
|
65
33
|
if (status === "pass") return style.green(glyph);
|
|
66
34
|
if (status === "fail") return style.red(glyph);
|
|
67
35
|
if (status === "available") return style.cyan(glyph);
|
|
@@ -70,13 +38,13 @@ function paintGlyph(status: CheckStatus, style: DoctorStyle): string {
|
|
|
70
38
|
|
|
71
39
|
/**
|
|
72
40
|
* Render one check as terminal lines: a headline row (icon, aligned name,
|
|
73
|
-
* detail), an optional indented breakdown of sub-items, and —
|
|
74
|
-
* inline
|
|
41
|
+
* detail), an optional indented breakdown of sub-items, and — whenever the
|
|
42
|
+
* check carries one — an inline instruction.
|
|
75
43
|
*/
|
|
76
44
|
function formatCheckPlain(
|
|
77
45
|
check: CheckResult,
|
|
78
46
|
nameWidth: number,
|
|
79
|
-
style:
|
|
47
|
+
style: RenderStyle,
|
|
80
48
|
): string[] {
|
|
81
49
|
const name = check.name.padEnd(nameWidth);
|
|
82
50
|
const label = check.status === "fail" ? style.red(name) : style.bold(name);
|
|
@@ -96,19 +64,23 @@ function formatCheckPlain(
|
|
|
96
64
|
const itemLabel = item.detail ? item.label.padEnd(itemWidth) : item.label;
|
|
97
65
|
const detail = item.detail ? ` ${style.dim(item.detail)}` : "";
|
|
98
66
|
lines.push(
|
|
99
|
-
`${
|
|
67
|
+
`${SUB_INDENT}${style.dim(SUB_BULLET)} ${icon}${itemLabel}${detail}`,
|
|
100
68
|
);
|
|
101
69
|
}
|
|
102
70
|
}
|
|
103
71
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
72
|
+
// Every row that HAS an instruction prints it. A fail's remedy is the
|
|
73
|
+
// repair, an available's is the setup command that enables it — and a skip's,
|
|
74
|
+
// when it authored one, is the only thing standing between the reader and a
|
|
75
|
+
// dead end. Skips were dropped here, so `skills` reported "no skills
|
|
76
|
+
// installed" with the next step (`sources update`) computed, carried through
|
|
77
|
+
// the check, published in `--format json`, and then thrown away one line
|
|
78
|
+
// before it reached the person reading. A skip never DERIVES a remedy
|
|
79
|
+
// (`scopedChecks` only passes through an authored one), so this prints
|
|
80
|
+
// nothing that was not deliberately written for this machine.
|
|
81
|
+
if (check.remedy) {
|
|
110
82
|
lines.push(
|
|
111
|
-
`${
|
|
83
|
+
`${SUB_INDENT}${style.cyan(FIX_ARROW)} ${style.cyan(`${checkRemedyWord(check.status)}:`)} ${check.remedy}`,
|
|
112
84
|
);
|
|
113
85
|
}
|
|
114
86
|
|
|
@@ -120,7 +92,7 @@ function formatCheckPlain(
|
|
|
120
92
|
* `available` is counted apart from `failed` so a healthy install with
|
|
121
93
|
* integrations left to opt into never reports failures it does not have.
|
|
122
94
|
*/
|
|
123
|
-
function formatSummary(data: DoctorData, style:
|
|
95
|
+
function formatSummary(data: DoctorData, style: RenderStyle): string {
|
|
124
96
|
const parts = [style.green(`${data.passed} passed`)];
|
|
125
97
|
parts.push(
|
|
126
98
|
data.failed > 0
|
|
@@ -141,28 +113,28 @@ function formatSummary(data: DoctorData, style: DoctorStyle): string {
|
|
|
141
113
|
}
|
|
142
114
|
|
|
143
115
|
/**
|
|
144
|
-
* Partition checks into ordered
|
|
145
|
-
* and project
|
|
146
|
-
* report stays deterministic.
|
|
116
|
+
* Partition checks into ordered sections: environment (no scope), then the
|
|
117
|
+
* global and local-project scopes. Declaration order is preserved within each,
|
|
118
|
+
* so the report stays deterministic.
|
|
147
119
|
*/
|
|
148
|
-
function
|
|
120
|
+
function partitionByScope(checks: readonly CheckResult[]): {
|
|
149
121
|
environment: CheckResult[];
|
|
150
|
-
|
|
122
|
+
global: CheckResult[];
|
|
151
123
|
project: CheckResult[];
|
|
152
124
|
} {
|
|
153
125
|
return {
|
|
154
|
-
environment: checks.filter((c) => c.
|
|
155
|
-
|
|
156
|
-
project: checks.filter((c) => c.
|
|
126
|
+
environment: checks.filter((c) => c.scope === undefined),
|
|
127
|
+
global: checks.filter((c) => c.scope === "global"),
|
|
128
|
+
project: checks.filter((c) => c.scope === "project"),
|
|
157
129
|
};
|
|
158
130
|
}
|
|
159
131
|
|
|
160
132
|
export const doctorFormatters: Formatters<DoctorData> = {
|
|
161
133
|
plain(data) {
|
|
162
|
-
const style =
|
|
134
|
+
const style = defaultStyle();
|
|
163
135
|
const nameWidth = Math.max(...data.checks.map((c) => c.name.length), 0);
|
|
164
136
|
const lines: string[] = [style.bold(`${BIN_NAME} doctor`), ""];
|
|
165
|
-
const { environment,
|
|
137
|
+
const { environment, global, project } = partitionByScope(data.checks);
|
|
166
138
|
const section = (heading: string, checks: CheckResult[]): void => {
|
|
167
139
|
if (checks.length === 0) return;
|
|
168
140
|
if (heading) lines.push(style.bold(heading));
|
|
@@ -170,11 +142,11 @@ export const doctorFormatters: Formatters<DoctorData> = {
|
|
|
170
142
|
lines.push(...formatCheckPlain(check, nameWidth, style));
|
|
171
143
|
lines.push("");
|
|
172
144
|
};
|
|
173
|
-
// Environment checks lead (no header);
|
|
174
|
-
// two
|
|
145
|
+
// Environment checks lead (no header); Global then Local project are the
|
|
146
|
+
// two scoped sections before the tally.
|
|
175
147
|
section("", environment);
|
|
176
|
-
section(
|
|
177
|
-
section(
|
|
148
|
+
section(SCOPE_LABELS.global, global);
|
|
149
|
+
section(SCOPE_LABELS.project, project);
|
|
178
150
|
lines.push(formatSummary(data, style));
|
|
179
151
|
return lines.join("\n");
|
|
180
152
|
},
|
|
@@ -182,34 +154,38 @@ export const doctorFormatters: Formatters<DoctorData> = {
|
|
|
182
154
|
llm(data) {
|
|
183
155
|
const renderCheck = (check: CheckResult): string[] => {
|
|
184
156
|
const out = [
|
|
185
|
-
`- ${
|
|
157
|
+
`- ${CHECK_GLYPHS[check.status]} **${check.name}**: ${check.detail}`,
|
|
186
158
|
];
|
|
187
159
|
for (const item of check.items ?? []) {
|
|
188
|
-
const itemIcon = item.status ? `${
|
|
160
|
+
const itemIcon = item.status ? `${CHECK_GLYPHS[item.status]} ` : "";
|
|
189
161
|
const detail = item.detail ? `: ${item.detail}` : "";
|
|
190
162
|
out.push(` - ${itemIcon}${item.label}${detail}`);
|
|
191
163
|
}
|
|
192
|
-
// Same rule as the plain path:
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
) {
|
|
198
|
-
out.push(
|
|
164
|
+
// Same rule as the plain path: every row that carries an instruction
|
|
165
|
+
// prints it, under the label its status earns. Only a `fix:` is code-set
|
|
166
|
+
// — a derived remedy is a bare command, while a skip's authored `next:`
|
|
167
|
+
// is a sentence that may quote a command of its own, and wrapping that in
|
|
168
|
+
// backticks nests one code span inside another.
|
|
169
|
+
if (check.remedy) {
|
|
170
|
+
out.push(
|
|
171
|
+
check.status === "skip"
|
|
172
|
+
? ` - _${checkRemedyWord(check.status)}:_ ${check.remedy}`
|
|
173
|
+
: ` - _${checkRemedyWord(check.status)}:_ \`${check.remedy}\``,
|
|
174
|
+
);
|
|
199
175
|
}
|
|
200
176
|
return out;
|
|
201
177
|
};
|
|
202
178
|
|
|
203
179
|
const lines: string[] = ["## Doctor", ""];
|
|
204
|
-
const { environment,
|
|
180
|
+
const { environment, global, project } = partitionByScope(data.checks);
|
|
205
181
|
const section = (heading: string, checks: CheckResult[]): void => {
|
|
206
182
|
if (checks.length === 0) return;
|
|
207
183
|
if (heading) lines.push(`### ${heading}`, "");
|
|
208
184
|
for (const check of checks) lines.push(...renderCheck(check));
|
|
209
185
|
};
|
|
210
186
|
section("", environment);
|
|
211
|
-
section(
|
|
212
|
-
section(
|
|
187
|
+
section(SCOPE_LABELS.global, global);
|
|
188
|
+
section(SCOPE_LABELS.project, project);
|
|
213
189
|
lines.push(
|
|
214
190
|
"",
|
|
215
191
|
`_${data.passed} passed, ${data.failed} failed, ${data.available} available, ${data.skipped} skipped_`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `pragma doctor` — the environment checks plus the
|
|
2
|
+
* `pragma doctor` — the environment checks plus the scoped target rows.
|
|
3
3
|
*
|
|
4
4
|
* Runs against isolated HOME/cwd/XDG so the harness/config/completion checks are
|
|
5
5
|
* deterministic (no harnesses, no config, no rc files). Covers the shape, a
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from "../../testing/fixtures/graph/canonical.js";
|
|
35
35
|
import { bootFixtureRuntime } from "../../testing/helpers/fixtureGraph.js";
|
|
36
36
|
import { projectMcp } from "../../testing/helpers/projectMcp.js";
|
|
37
|
-
import {
|
|
37
|
+
import { scopedChecks } from "./checks/targetHealth.js";
|
|
38
38
|
import { doctorModule } from "./index.js";
|
|
39
39
|
import { runChecks } from "./runChecks.js";
|
|
40
40
|
import type { DoctorData } from "./types.js";
|
|
@@ -118,12 +118,13 @@ const byName = (data: DoctorData, name: string) =>
|
|
|
118
118
|
data.checks.find((c) => c.name === name);
|
|
119
119
|
|
|
120
120
|
describe("doctor — shape & spread", () => {
|
|
121
|
-
it("returns the environment checks plus one row per
|
|
121
|
+
it("returns the environment checks plus one row per scoped target", async () => {
|
|
122
122
|
const data = await runChecks(bootRuntime(FLAGS, tmp("pragma-proj-")));
|
|
123
|
-
// Four
|
|
124
|
-
// all five targets are global, mcp and skills are also project
|
|
125
|
-
|
|
126
|
-
expect(data.
|
|
123
|
+
// Four unscoped environment checks, then the target table in both scopes
|
|
124
|
+
// (all five targets are global, mcp and skills are also project) = 11,
|
|
125
|
+
// plus the two `harnesses` inventory rows, one per scope = 13.
|
|
126
|
+
expect(data.checks).toHaveLength(13);
|
|
127
|
+
expect(data.passed + data.failed + data.available + data.skipped).toBe(13);
|
|
127
128
|
for (const check of data.checks) {
|
|
128
129
|
expect(["pass", "fail", "available", "skip"]).toContain(check.status);
|
|
129
130
|
}
|
|
@@ -133,14 +134,28 @@ describe("doctor — shape & spread", () => {
|
|
|
133
134
|
// snapshot answers reads and the check passes, naming what it is reading.
|
|
134
135
|
const pkgRefs = byName(data, "pack refs");
|
|
135
136
|
expect(pkgRefs?.status).toBe("pass");
|
|
136
|
-
expect(pkgRefs?.detail).toContain("
|
|
137
|
+
expect(pkgRefs?.detail).toContain("shipped with the CLI");
|
|
137
138
|
expect(pkgRefs?.remedy).toBeUndefined();
|
|
139
|
+
// Provenance is one ITEM PER PACK, not a comma-joined string in the
|
|
140
|
+
// headline: four packs and two forty-character SHAs on one line was
|
|
141
|
+
// unreadable, and every other multi-part check here already uses items.
|
|
142
|
+
// The headline counts them; the items say which revision each one is.
|
|
143
|
+
expect(pkgRefs?.detail).toContain("packs");
|
|
144
|
+
expect((pkgRefs?.items?.length ?? 0) > 1).toBe(true);
|
|
145
|
+
for (const item of pkgRefs?.items ?? []) {
|
|
146
|
+
expect(item.label).not.toContain(",");
|
|
147
|
+
// A git hash is cut to seven — the length every other tool in this
|
|
148
|
+
// workflow prints, and short enough that four rows stay scannable.
|
|
149
|
+
expect(item.detail ?? "").not.toMatch(/[0-9a-f]{40}/);
|
|
150
|
+
}
|
|
138
151
|
|
|
139
|
-
// The
|
|
152
|
+
// The scoped rows carry the TARGET IDS verbatim — `mcp`, not "MCP
|
|
140
153
|
// configured" — because the row name IS the fix command's argument. The
|
|
141
|
-
// environment checks stay
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
// environment checks stay unscoped. `harnesses` is the ONE scoped row that
|
|
155
|
+
// is not a target: an inventory of the machine, not something to set up, so
|
|
156
|
+
// it sits last in each scope and derives no `fix:`.
|
|
157
|
+
const scoped = data.checks.filter((c) => c.scope !== undefined);
|
|
158
|
+
expect(scoped.map((c) => `${c.scope}:${c.name}`)).toEqual([
|
|
144
159
|
"global:config",
|
|
145
160
|
"global:completions",
|
|
146
161
|
"global:lsp",
|
|
@@ -148,18 +163,27 @@ describe("doctor — shape & spread", () => {
|
|
|
148
163
|
"global:skills",
|
|
149
164
|
"project:mcp",
|
|
150
165
|
"project:skills",
|
|
166
|
+
"global:harnesses",
|
|
167
|
+
"project:harnesses",
|
|
151
168
|
]);
|
|
152
169
|
// Every row that wants action names the exact command that repairs it,
|
|
153
|
-
//
|
|
154
|
-
for (const check of
|
|
170
|
+
// scope included — the bijection, derived rather than authored.
|
|
171
|
+
for (const check of scoped) {
|
|
155
172
|
if (check.status === "fail" || check.status === "available") {
|
|
156
173
|
expect(check.remedy).toBeDefined();
|
|
157
174
|
}
|
|
158
175
|
}
|
|
176
|
+
// The inventory never inflates the failure or the action count: it is only
|
|
177
|
+
// ever `pass` (this scope holds harnesses) or `skip` (it holds none), and it
|
|
178
|
+
// proposes nothing — the `mcp`/`skills` rows own every harness action.
|
|
179
|
+
for (const check of scoped.filter((c) => c.name === "harnesses")) {
|
|
180
|
+
expect(["pass", "skip"]).toContain(check.status);
|
|
181
|
+
expect(check.remedy).toBeUndefined();
|
|
182
|
+
}
|
|
159
183
|
// No harnesses in an empty HOME/cwd, so both mcp rows skip; the project
|
|
160
|
-
//
|
|
184
|
+
// scope is opt-in, so its absence is never a fault.
|
|
161
185
|
expect(
|
|
162
|
-
|
|
186
|
+
scoped.find((c) => c.scope === "project" && c.name === "mcp")?.status,
|
|
163
187
|
).toBe("skip");
|
|
164
188
|
// No global config in the isolated XDG — an opt-in that is not set up yet.
|
|
165
189
|
expect(byName(data, "config")?.status).toBe("available");
|
|
@@ -190,8 +214,12 @@ describe("doctor — the pack-refs check", () => {
|
|
|
190
214
|
const data = await runChecks(fixture.runtime);
|
|
191
215
|
const pkgRefs = byName(data, "pack refs");
|
|
192
216
|
expect(pkgRefs?.status).toBe("pass");
|
|
193
|
-
// The fixture pack's own provenance label, not the embedded snapshot's
|
|
194
|
-
|
|
217
|
+
// The fixture pack's own provenance label, not the embedded snapshot's —
|
|
218
|
+
// now carried as an item rather than inline, and passed through WHOLE
|
|
219
|
+
// because it parses as no scheme. An unreadable provenance is still
|
|
220
|
+
// provenance; dropping it would be the one failure this check exists to
|
|
221
|
+
// prevent.
|
|
222
|
+
expect(pkgRefs?.items?.map((item) => item.label)).toContain("fixture");
|
|
195
223
|
expect(pkgRefs?.detail).not.toContain("embedded snapshot");
|
|
196
224
|
await fixture.dispose();
|
|
197
225
|
});
|
|
@@ -200,7 +228,7 @@ describe("doctor — the pack-refs check", () => {
|
|
|
200
228
|
describe("doctor — the store check", () => {
|
|
201
229
|
it("a store that fails to boot is an attributable fail, not a crash", async () => {
|
|
202
230
|
const data = await runChecks(throwingStoreRuntime(tmp("pragma-proj-")));
|
|
203
|
-
expect(data.checks).toHaveLength(
|
|
231
|
+
expect(data.checks).toHaveLength(13);
|
|
204
232
|
const store = byName(data, "store");
|
|
205
233
|
expect(store?.status).toBe("fail");
|
|
206
234
|
expect(store?.remedy).toBeTruthy();
|
|
@@ -242,23 +270,23 @@ describe("doctor — dispatch & MCP", () => {
|
|
|
242
270
|
const envelope = await mcp.callTool("doctor");
|
|
243
271
|
await mcp.cleanup();
|
|
244
272
|
expect(envelope.ok).toBe(true);
|
|
245
|
-
expect((envelope.data as DoctorData).checks).toHaveLength(
|
|
273
|
+
expect((envelope.data as DoctorData).checks).toHaveLength(13);
|
|
246
274
|
});
|
|
247
275
|
});
|
|
248
276
|
|
|
249
|
-
describe("doctor — the
|
|
250
|
-
it("reports a configured global-
|
|
277
|
+
describe("doctor — the scoped rows can see the GLOBAL scope", () => {
|
|
278
|
+
it("reports a configured global-scope entry as configured, not missing", async () => {
|
|
251
279
|
// The regression this closes: detection recorded only each harness's
|
|
252
|
-
// DEFAULT
|
|
280
|
+
// DEFAULT scope, so a server registered by `setup mcp --global` was invisible
|
|
253
281
|
// and doctor reported it not-configured forever. The rows now come from the
|
|
254
|
-
// same target table `setup` writes through, enumerated in BOTH
|
|
282
|
+
// same target table `setup` writes through, enumerated in BOTH scopes, so the
|
|
255
283
|
// two surfaces cannot disagree about what they looked at.
|
|
256
284
|
const cwd = tmp("pragma-doctor-proj-");
|
|
257
285
|
mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // ⇒ Windsurf detected
|
|
258
286
|
const home = process.env.HOME ?? "";
|
|
259
287
|
const wsDir = join(home, ".codeium", "windsurf");
|
|
260
288
|
mkdirSync(wsDir, { recursive: true });
|
|
261
|
-
// Exactly what a global-
|
|
289
|
+
// Exactly what a global-scope `setup mcp` writes: no `cwd`, so the entry is
|
|
262
290
|
// not pinned to whatever directory the registration ran from.
|
|
263
291
|
writeFileSync(
|
|
264
292
|
join(wsDir, "mcp_config.json"),
|
|
@@ -272,18 +300,175 @@ describe("doctor — the banded rows can see the GLOBAL band", () => {
|
|
|
272
300
|
writeFileSync(join(binDir, "pragma"), "#!/bin/sh\nexit 0\n");
|
|
273
301
|
chmodSync(join(binDir, "pragma"), 0o755);
|
|
274
302
|
|
|
275
|
-
const rows = await
|
|
276
|
-
const global = rows.find((r) => r.name === "mcp" && r.
|
|
303
|
+
const rows = await scopedChecks(bootRuntime(FLAGS, cwd), "pragma");
|
|
304
|
+
const global = rows.find((r) => r.name === "mcp" && r.scope === "global");
|
|
277
305
|
expect(global?.status).toBe("pass");
|
|
278
306
|
expect(
|
|
279
307
|
global?.items?.some((i) => i.label.includes("mcp_config.json")),
|
|
280
308
|
).toBe(true);
|
|
281
|
-
// ...and the project
|
|
282
|
-
const project = rows.find((r) => r.name === "mcp" && r.
|
|
309
|
+
// ...and the project scope stays an opt-in skip, never a fault.
|
|
310
|
+
const project = rows.find((r) => r.name === "mcp" && r.scope === "project");
|
|
283
311
|
expect(project?.status).toBe("skip");
|
|
284
312
|
});
|
|
285
313
|
});
|
|
286
314
|
|
|
315
|
+
describe("doctor — the harness inventory", () => {
|
|
316
|
+
const VERBOSE: GlobalFlags = { ...FLAGS, verbose: true };
|
|
317
|
+
const inventory = (rows: Awaited<ReturnType<typeof scopedChecks>>) => ({
|
|
318
|
+
global: rows.find((r) => r.name === "harnesses" && r.scope === "global"),
|
|
319
|
+
project: rows.find((r) => r.name === "harnesses" && r.scope === "project"),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("lists ONLY detected harnesses by default, by name and with their location", async () => {
|
|
323
|
+
const cwd = tmp("pragma-doctor-proj-");
|
|
324
|
+
mkdirSync(join(cwd, ".vscode"), { recursive: true }); // ⇒ VS Code detected
|
|
325
|
+
|
|
326
|
+
const rows = await scopedChecks(bootRuntime(FLAGS, cwd), "pragma");
|
|
327
|
+
const { global, project } = inventory(rows);
|
|
328
|
+
|
|
329
|
+
// The project scope holds the hit — by NAME, with the file it resolves to.
|
|
330
|
+
expect(project?.status).toBe("pass");
|
|
331
|
+
expect(project?.detail).toBe("1 detected · 0 registered");
|
|
332
|
+
expect(project?.items?.map((i) => i.label)).toEqual(["VS Code"]);
|
|
333
|
+
expect(project?.items?.[0]?.status).toBe("available");
|
|
334
|
+
expect(project?.items?.[0]?.detail).toContain("detected, not registered");
|
|
335
|
+
expect(project?.items?.[0]?.detail).toContain(".vscode/mcp.json");
|
|
336
|
+
|
|
337
|
+
// Nothing detected in the global scope, and that is a skip — not a fault.
|
|
338
|
+
expect(global?.status).toBe("skip");
|
|
339
|
+
expect(global?.detail).toBe("no harnesses detected");
|
|
340
|
+
expect(global?.items).toBeUndefined();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("--verbose lists every registry harness, undetected ones as a NON-failing skip", async () => {
|
|
344
|
+
const cwd = tmp("pragma-doctor-proj-");
|
|
345
|
+
mkdirSync(join(cwd, ".vscode"), { recursive: true });
|
|
346
|
+
|
|
347
|
+
const rows = await scopedChecks(bootRuntime(VERBOSE, cwd), "pragma");
|
|
348
|
+
const { global, project } = inventory(rows);
|
|
349
|
+
|
|
350
|
+
// Every harness the registry knows, in both scopes.
|
|
351
|
+
expect(project?.items).toHaveLength(13);
|
|
352
|
+
expect(global?.items).toHaveLength(13);
|
|
353
|
+
expect(project?.detail).toBe("1 detected · 0 registered · 13 known");
|
|
354
|
+
|
|
355
|
+
// `types.ts` forbids inflating the failure count: a machine that simply
|
|
356
|
+
// does not have Cursor is not a broken machine.
|
|
357
|
+
for (const check of [global, project]) {
|
|
358
|
+
expect(["pass", "skip"]).toContain(check?.status);
|
|
359
|
+
for (const item of check?.items ?? []) {
|
|
360
|
+
expect(item.status).not.toBe("fail");
|
|
361
|
+
expect(["pass", "available", "skip"]).toContain(item.status);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const cursor = project?.items?.find((i) => i.label === "Cursor");
|
|
366
|
+
expect(cursor?.status).toBe("skip");
|
|
367
|
+
expect(cursor?.detail).toBe("not detected");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("says where a harness DOES keep its config, rather than omitting it", async () => {
|
|
371
|
+
// `vscode` is `scope: "project"`, so it can NEVER carry a global entry. A
|
|
372
|
+
// verbose global listing that silently dropped it would read as a bug in
|
|
373
|
+
// the listing; the row says why instead. The mirror case is Windsurf, which
|
|
374
|
+
// is `scope: "global"` and keeps nothing per project.
|
|
375
|
+
//
|
|
376
|
+
// The sentence states the FACT, not the partition: `no Global band` named
|
|
377
|
+
// an internal word for the answer instead of giving it.
|
|
378
|
+
const rows = await scopedChecks(
|
|
379
|
+
bootRuntime(VERBOSE, tmp("pragma-doctor-proj-")),
|
|
380
|
+
"pragma",
|
|
381
|
+
);
|
|
382
|
+
const { global, project } = inventory(rows);
|
|
383
|
+
|
|
384
|
+
const vscodeGlobal = global?.items?.find((i) => i.label === "VS Code");
|
|
385
|
+
expect(vscodeGlobal?.detail).toBe(
|
|
386
|
+
"keeps no global config — it is per-project only",
|
|
387
|
+
);
|
|
388
|
+
expect(vscodeGlobal?.status).toBe("skip");
|
|
389
|
+
|
|
390
|
+
const windsurfProject = project?.items?.find((i) => i.label === "Windsurf");
|
|
391
|
+
expect(windsurfProject?.detail).toBe(
|
|
392
|
+
"keeps no per-project config — it is global only",
|
|
393
|
+
);
|
|
394
|
+
expect(windsurfProject?.status).toBe("skip");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("reports a harness whose pragma entry is current as registered", async () => {
|
|
398
|
+
const cwd = tmp("pragma-doctor-proj-");
|
|
399
|
+
mkdirSync(join(cwd, ".vscode"), { recursive: true });
|
|
400
|
+
// Byte-for-byte what a project-scope `setup mcp` writes for VS Code: its
|
|
401
|
+
// `servers` key, and the `cwd` a project registration pins.
|
|
402
|
+
writeFileSync(
|
|
403
|
+
join(cwd, ".vscode", "mcp.json"),
|
|
404
|
+
JSON.stringify({
|
|
405
|
+
servers: { pragma: { command: "pragma", args: ["mcp", "serve"], cwd } },
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const rows = await scopedChecks(bootRuntime(FLAGS, cwd), "pragma");
|
|
410
|
+
const { project } = inventory(rows);
|
|
411
|
+
expect(project?.detail).toBe("1 detected · 1 registered");
|
|
412
|
+
const vscode = project?.items?.find((i) => i.label === "VS Code");
|
|
413
|
+
expect(vscode?.status).toBe("pass");
|
|
414
|
+
expect(vscode?.detail).toContain("registered");
|
|
415
|
+
expect(vscode?.detail).not.toContain("not registered");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("reports two harnesses sharing ONE file by their own keys, not by the file", async () => {
|
|
419
|
+
// The co-detection case. `.vscode/mcp.json` is written under TWO keys —
|
|
420
|
+
// VS Code's `servers` and Cline's `mcpServers` — and registering pragma
|
|
421
|
+
// for VS Code alone leaves the FILE half-configured, which `detectMcp`
|
|
422
|
+
// aggregates to `drifted` (correctly: a write is still owed). That
|
|
423
|
+
// aggregate is not either harness's standing, and reporting it per harness
|
|
424
|
+
// said both had a stale entry when one was current and the other had none.
|
|
425
|
+
const cwd = tmp("pragma-doctor-proj-");
|
|
426
|
+
mkdirSync(join(cwd, ".vscode"), { recursive: true });
|
|
427
|
+
// Cline is detected ONLY by its installed extension, in the same directory
|
|
428
|
+
// that also proves VS Code is installed — so the two co-detect.
|
|
429
|
+
mkdirSync(
|
|
430
|
+
join(
|
|
431
|
+
process.env.HOME as string,
|
|
432
|
+
".vscode",
|
|
433
|
+
"extensions",
|
|
434
|
+
"saoudrizwan.claude-dev-3.20.0",
|
|
435
|
+
),
|
|
436
|
+
{ recursive: true },
|
|
437
|
+
);
|
|
438
|
+
writeFileSync(
|
|
439
|
+
join(
|
|
440
|
+
process.env.HOME as string,
|
|
441
|
+
".vscode",
|
|
442
|
+
"extensions",
|
|
443
|
+
"saoudrizwan.claude-dev-3.20.0",
|
|
444
|
+
"package.json",
|
|
445
|
+
),
|
|
446
|
+
"{}",
|
|
447
|
+
);
|
|
448
|
+
// Only VS Code's key is registered; Cline's `mcpServers` is absent.
|
|
449
|
+
writeFileSync(
|
|
450
|
+
join(cwd, ".vscode", "mcp.json"),
|
|
451
|
+
JSON.stringify({
|
|
452
|
+
servers: { pragma: { command: "pragma", args: ["mcp", "serve"], cwd } },
|
|
453
|
+
}),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const rows = await scopedChecks(bootRuntime(FLAGS, cwd), "pragma");
|
|
457
|
+
const { project } = inventory(rows);
|
|
458
|
+
expect(project?.detail).toBe("2 detected · 1 registered");
|
|
459
|
+
|
|
460
|
+
const vscode = project?.items?.find((i) => i.label === "VS Code");
|
|
461
|
+
expect(vscode?.status).toBe("pass");
|
|
462
|
+
expect(vscode?.detail).toContain("registered — ");
|
|
463
|
+
expect(vscode?.detail).not.toContain("differs");
|
|
464
|
+
|
|
465
|
+
const cline = project?.items?.find((i) => i.label === "Cline");
|
|
466
|
+
expect(cline?.status).toBe("available");
|
|
467
|
+
expect(cline?.detail).toContain("detected, not registered");
|
|
468
|
+
expect(cline?.detail).not.toContain("differs");
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
287
472
|
describe("doctor — a blocked skill link path is never reported as current", () => {
|
|
288
473
|
it("distinguishes a hand-placed real directory from a link that is correct", async () => {
|
|
289
474
|
// Detection marks both `skipped` — there is nothing to do to either — so
|
|
@@ -299,8 +484,8 @@ describe("doctor — a blocked skill link path is never reported as current", ()
|
|
|
299
484
|
// A hand-placed directory sitting exactly where the link would go.
|
|
300
485
|
mkdirSync(join(cwd, ".agents", "skills", "my-skill"), { recursive: true });
|
|
301
486
|
|
|
302
|
-
const rows = await
|
|
303
|
-
const row = rows.find((r) => r.name === "skills" && r.
|
|
487
|
+
const rows = await scopedChecks(bootRuntime(FLAGS, cwd), "pragma");
|
|
488
|
+
const row = rows.find((r) => r.name === "skills" && r.scope === "project");
|
|
304
489
|
expect(row?.status).not.toBe("pass");
|
|
305
490
|
expect(row?.detail).toContain("real directory");
|
|
306
491
|
// And the remedy is the one that settles it — rerunning setup skips it.
|
|
@@ -308,6 +493,33 @@ describe("doctor — a blocked skill link path is never reported as current", ()
|
|
|
308
493
|
});
|
|
309
494
|
});
|
|
310
495
|
|
|
496
|
+
describe("doctor — an EMPTY skills root is not diagnosed as an ABSENT one", () => {
|
|
497
|
+
it("reports an existing but empty project skills root as holding none", async () => {
|
|
498
|
+
// doctor publishes a DIAGNOSIS, so a wrong one is worse than a vague one:
|
|
499
|
+
// the shared skip line told the user `.pragma/skills` "is absent" while
|
|
500
|
+
// they were looking at the directory. Both surfaces read the same
|
|
501
|
+
// `rootExists` the detection already carries.
|
|
502
|
+
const cwd = tmp("pragma-doctor-proj-");
|
|
503
|
+
mkdirSync(join(cwd, ".pragma", "skills"), { recursive: true });
|
|
504
|
+
|
|
505
|
+
const rows = await scopedChecks(bootRuntime(FLAGS, cwd), "pragma");
|
|
506
|
+
const row = rows.find((r) => r.name === "skills" && r.scope === "project");
|
|
507
|
+
expect(row?.status).toBe("skip"); // still nothing to reconcile...
|
|
508
|
+
expect(row?.detail).toContain("is empty"); // ...for the true reason
|
|
509
|
+
expect(row?.detail).not.toContain("does not exist");
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("still reports a missing project skills root as absent", async () => {
|
|
513
|
+
const rows = await scopedChecks(
|
|
514
|
+
bootRuntime(FLAGS, tmp("pragma-doctor-proj-")),
|
|
515
|
+
"pragma",
|
|
516
|
+
);
|
|
517
|
+
const row = rows.find((r) => r.name === "skills" && r.scope === "project");
|
|
518
|
+
expect(row?.status).toBe("skip");
|
|
519
|
+
expect(row?.detail).toContain("does not exist");
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
311
523
|
describe("doctor — an unconfigured opt-in integration is available, not a fault", () => {
|
|
312
524
|
it("a detected harness with no MCP entry reports available with the setup command", async () => {
|
|
313
525
|
// Windsurf is detected (project signal) but nothing is configured. A fresh
|
|
@@ -317,11 +529,11 @@ describe("doctor — an unconfigured opt-in integration is available, not a faul
|
|
|
317
529
|
const cwd = tmp("pragma-doctor-proj-");
|
|
318
530
|
mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // ⇒ Windsurf detected
|
|
319
531
|
|
|
320
|
-
const rows = await
|
|
321
|
-
const check = rows.find((r) => r.name === "mcp" && r.
|
|
532
|
+
const rows = await scopedChecks(bootRuntime(FLAGS, cwd), "pragma");
|
|
533
|
+
const check = rows.find((r) => r.name === "mcp" && r.scope === "global");
|
|
322
534
|
expect(check?.status).toBe("available");
|
|
323
|
-
expect(check?.detail).toContain("not
|
|
324
|
-
// The fix is the target's own invocation, derived from the row's id +
|
|
535
|
+
expect(check?.detail).toContain("not registered");
|
|
536
|
+
// The fix is the target's own invocation, derived from the row's id + scope.
|
|
325
537
|
expect(check?.remedy).toBe("pragma setup mcp");
|
|
326
538
|
});
|
|
327
539
|
});
|
|
@@ -20,8 +20,13 @@ import type { DoctorData } from "./types.js";
|
|
|
20
20
|
|
|
21
21
|
const doctorVerb: VerbSpec<Record<string, unknown>, DoctorData> = {
|
|
22
22
|
path: ["doctor"],
|
|
23
|
-
summary:
|
|
24
|
-
|
|
23
|
+
summary:
|
|
24
|
+
"Check your environment and every setup target, globally and in this project.",
|
|
25
|
+
// Two house rules constrain this string and both are PROTECTED: it may not
|
|
26
|
+
// name the distribution (`identity.test.ts`) and it may not spell a CLI flag
|
|
27
|
+
// (`toolDescriptions.test.ts`), so the inventory's widening is described as
|
|
28
|
+
// "the verbose global flag" rather than by its spelling.
|
|
29
|
+
doc: "Reports the environment checks first, then one row per setup target — once for your home directory, once for this project. Each row is pass, fail, available (an optional integration you have not set up yet), or skip (nothing to do here, and the row says why), with the next step printed inline. Every row is named after the setup target that repairs it, except `harnesses`: a listing, per scope, of the AI harnesses found on this machine and whether this CLI's MCP server is registered in each — the ones actually found, or every harness it knows about under the verbose global flag. Needs no store; the store check boots lazily and never fails the run.",
|
|
25
30
|
params: [],
|
|
26
31
|
output: { formatters: doctorFormatters },
|
|
27
32
|
examples: [
|