@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
|
@@ -60,6 +60,51 @@ function ignoredSuffix(items: readonly CheckItem[]): string {
|
|
|
60
60
|
return ` · ${items.length} ${items.length === 1 ? "story" : "stories"} ignored`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The answering pack's provenance, one item per pack it was built from.
|
|
65
|
+
*
|
|
66
|
+
* `sourceRef` is a single comma-joined string — `@canonical/design-system@git:
|
|
67
|
+
* d6d8a6c8268cf2bd103e956a2540d6e36bd08d72, @canonical/anatomy-dsl@npm:0.2.2,
|
|
68
|
+
* …` — and printing it whole put four packs and two forty-character SHAs on one
|
|
69
|
+
* line, in a report whose every other multi-part check (`mcp`, `harnesses`)
|
|
70
|
+
* uses the sub-item mechanism. Provenance is the thing a reader is here for
|
|
71
|
+
* when they read this line at all: which revision of which pack answered.
|
|
72
|
+
*
|
|
73
|
+
* The scheme (`git`/`npm`/`self`/`file`) is kept because it says how to move
|
|
74
|
+
* the pin, and a git hash is cut to the seven characters every other tool in
|
|
75
|
+
* this workflow shows. A ref that does not parse is passed through whole rather
|
|
76
|
+
* than dropped — an unreadable provenance is still provenance, and hiding it
|
|
77
|
+
* would be the one failure this check exists to prevent.
|
|
78
|
+
*
|
|
79
|
+
* @param sourceRef - The manifest's comma-joined provenance string.
|
|
80
|
+
* @returns One passing item per pack, in the order the pack records them.
|
|
81
|
+
*/
|
|
82
|
+
function packItems(sourceRef: string): CheckItem[] {
|
|
83
|
+
return sourceRef
|
|
84
|
+
.split(",")
|
|
85
|
+
.map((ref) => ref.trim())
|
|
86
|
+
.filter((ref) => ref !== "")
|
|
87
|
+
.map((ref) => {
|
|
88
|
+
const parsed = /^(.+)@(git|npm|self|file|link):(.+)$/.exec(ref);
|
|
89
|
+
if (parsed === null) return { label: ref, status: "pass" as const };
|
|
90
|
+
const [, pack, scheme, revision] = parsed as unknown as [
|
|
91
|
+
string,
|
|
92
|
+
string,
|
|
93
|
+
string,
|
|
94
|
+
string,
|
|
95
|
+
];
|
|
96
|
+
const short =
|
|
97
|
+
scheme === "git" && /^[0-9a-f]{40}$/.test(revision)
|
|
98
|
+
? revision.slice(0, 7)
|
|
99
|
+
: revision;
|
|
100
|
+
return {
|
|
101
|
+
label: pack,
|
|
102
|
+
detail: `${scheme} ${short}`,
|
|
103
|
+
status: "pass" as const,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
/**
|
|
64
109
|
* Report which pack answers reads, its provenance, and its entity total.
|
|
65
110
|
*
|
|
@@ -89,13 +134,30 @@ export async function checkPackageRefs(
|
|
|
89
134
|
// The pack DOES answer reads, so the check itself passes; the ignored stories
|
|
90
135
|
// are failing sub-items under it. (A `warn` status would ripple through the
|
|
91
136
|
// renderer and the DoctorData counts to buy an icon.)
|
|
92
|
-
const
|
|
93
|
-
const suffix = ignoredSuffix(
|
|
137
|
+
const ignored = await ignoredStoryItems(decision);
|
|
138
|
+
const suffix = ignoredSuffix(ignored);
|
|
139
|
+
const sourceRef =
|
|
140
|
+
decision.kind === "embedded"
|
|
141
|
+
? embeddedManifest().sourceRef
|
|
142
|
+
: (readManifest(decision.dir)?.sourceRef ?? "");
|
|
143
|
+
// Packs first, then anything wrong with them: the composition is the answer to
|
|
144
|
+
// "which revision am I reading", and a failing story is a note about one of
|
|
145
|
+
// the rows above it.
|
|
146
|
+
const packs = packItems(sourceRef);
|
|
147
|
+
const items = [...packs, ...ignored];
|
|
148
|
+
const composition = `${packs.length} ${packs.length === 1 ? "pack" : "packs"}, ${entities} entities${suffix}`;
|
|
149
|
+
|
|
94
150
|
if (decision.kind === "embedded") {
|
|
95
151
|
return {
|
|
96
152
|
name,
|
|
97
153
|
status: "pass",
|
|
98
|
-
|
|
154
|
+
// The update hint stays in the DETAIL rather than moving to `remedy`:
|
|
155
|
+
// the renderer prints a remedy only where there is something to fix or
|
|
156
|
+
// to finish, and a snapshot that is answering reads correctly is
|
|
157
|
+
// neither. "shipped with the CLI" rather than "embedded snapshot" — the
|
|
158
|
+
// reader needs to know where these packs came from, not what the build
|
|
159
|
+
// calls the artefact.
|
|
160
|
+
detail: `shipped with the CLI — ${composition} · run \`${BIN_NAME} sources update\` to build from your own configured packs instead`,
|
|
99
161
|
...(items.length > 0 ? { items } : {}),
|
|
100
162
|
};
|
|
101
163
|
}
|
|
@@ -103,7 +165,10 @@ export async function checkPackageRefs(
|
|
|
103
165
|
return {
|
|
104
166
|
name,
|
|
105
167
|
status: "pass",
|
|
106
|
-
detail:
|
|
168
|
+
detail:
|
|
169
|
+
packs.length > 0
|
|
170
|
+
? `built ${manifest?.createdAt ?? "?"} — ${composition}`
|
|
171
|
+
: `${decision.contentHash.slice(0, 12)} — ${composition}, built ${manifest?.createdAt ?? "?"}`,
|
|
107
172
|
...(items.length > 0 ? { items } : {}),
|
|
108
173
|
};
|
|
109
174
|
}
|
|
@@ -94,10 +94,12 @@ function wireZfunc(): void {
|
|
|
94
94
|
describe("checkShellCompletions — effect test (gate 1)", () => {
|
|
95
95
|
it("passes the resolver gate: `pragma <TAB>` resolves nouns", async () => {
|
|
96
96
|
// No shell → skip, but only AFTER the resolver answered — the detail proves
|
|
97
|
-
// the effect test ran and succeeded.
|
|
97
|
+
// the effect test ran and succeeded. The proof is the same fact worded for
|
|
98
|
+
// a reader ("pragma answers `<TAB>`") rather than as "resolver OK": the
|
|
99
|
+
// gate it pins is unchanged, only the sentence that reports it.
|
|
98
100
|
const result = await check(tmp(), null);
|
|
99
101
|
expect(result.status).toBe("skip");
|
|
100
|
-
expect(result.detail).toMatch(/
|
|
102
|
+
expect(result.detail).toMatch(/answers `<TAB>`/);
|
|
101
103
|
});
|
|
102
104
|
});
|
|
103
105
|
|
|
@@ -137,7 +139,7 @@ describe("checkShellCompletions — install probe (gate 2)", () => {
|
|
|
137
139
|
const result = await check(tmp(), "bash");
|
|
138
140
|
expect(result.status).toBe("pass");
|
|
139
141
|
expect(result.detail).toMatch(
|
|
140
|
-
/bash \(the shell in
|
|
142
|
+
/bash \(the shell you are in\) — installed, up to date, and completing/,
|
|
141
143
|
);
|
|
142
144
|
});
|
|
143
145
|
|
|
@@ -200,7 +202,7 @@ describe("checkShellCompletions — zsh fpath activation (gate 3)", () => {
|
|
|
200
202
|
const result = await check(tmp(), "zsh");
|
|
201
203
|
expect(result.status).toBe("pass");
|
|
202
204
|
expect(result.detail).toMatch(
|
|
203
|
-
/zsh \(the shell in
|
|
205
|
+
/zsh \(the shell you are in\) — installed, up to date, and completing/,
|
|
204
206
|
);
|
|
205
207
|
});
|
|
206
208
|
});
|
|
@@ -126,7 +126,7 @@ export async function checkShellCompletions(
|
|
|
126
126
|
return {
|
|
127
127
|
name: NAME,
|
|
128
128
|
status: "fail",
|
|
129
|
-
detail:
|
|
129
|
+
detail: `${BIN_NAME} cannot answer \`<TAB>\`: ${reason}`,
|
|
130
130
|
remedy: `Report this as a bug — \`${BIN_NAME} __complete\` should never throw.`,
|
|
131
131
|
};
|
|
132
132
|
}
|
|
@@ -134,7 +134,7 @@ export async function checkShellCompletions(
|
|
|
134
134
|
return {
|
|
135
135
|
name: NAME,
|
|
136
136
|
status: "fail",
|
|
137
|
-
detail:
|
|
137
|
+
detail: `${BIN_NAME} answered \`<TAB>\` with nothing to complete`,
|
|
138
138
|
remedy: "Report this as a bug — the noun context is always non-empty.",
|
|
139
139
|
};
|
|
140
140
|
}
|
|
@@ -147,7 +147,7 @@ export async function checkShellCompletions(
|
|
|
147
147
|
return {
|
|
148
148
|
name: NAME,
|
|
149
149
|
status: "skip",
|
|
150
|
-
detail:
|
|
150
|
+
detail: `${BIN_NAME} answers \`<TAB>\`, but the running shell cannot be identified — SHELL names ${d.detection.login}, which is your login shell`,
|
|
151
151
|
remedy: `Run \`${INSTALL_REMEDY}\` from the shell you want completions in.`,
|
|
152
152
|
};
|
|
153
153
|
}
|
|
@@ -155,7 +155,7 @@ export async function checkShellCompletions(
|
|
|
155
155
|
return {
|
|
156
156
|
name: NAME,
|
|
157
157
|
status: "skip",
|
|
158
|
-
detail:
|
|
158
|
+
detail: `${BIN_NAME} answers \`<TAB>\`, but no bash, zsh, or fish was found in this process tree`,
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
// The installed script delegates every name context to the binary. A script
|
|
@@ -174,7 +174,7 @@ export async function checkShellCompletions(
|
|
|
174
174
|
status: installed ? "fail" : "available",
|
|
175
175
|
detail: installed
|
|
176
176
|
? `${shell} script installed, but \`${BIN_NAME}\` is not on PATH — the script cannot run it`
|
|
177
|
-
:
|
|
177
|
+
: `${BIN_NAME} answers \`<TAB>\`, but is not on PATH — an installed script cannot run it`,
|
|
178
178
|
remedy: `Put \`${BIN_NAME}\` on your PATH.`,
|
|
179
179
|
};
|
|
180
180
|
}
|
|
@@ -186,7 +186,7 @@ export async function checkShellCompletions(
|
|
|
186
186
|
return {
|
|
187
187
|
name: NAME,
|
|
188
188
|
status: "available",
|
|
189
|
-
detail:
|
|
189
|
+
detail: `${BIN_NAME} answers \`<TAB>\`; the ${shell} script is not installed`,
|
|
190
190
|
remedy: INSTALL_REMEDY,
|
|
191
191
|
};
|
|
192
192
|
}
|
|
@@ -194,7 +194,7 @@ export async function checkShellCompletions(
|
|
|
194
194
|
return {
|
|
195
195
|
name: NAME,
|
|
196
196
|
status: "fail",
|
|
197
|
-
detail:
|
|
197
|
+
detail: `${BIN_NAME} answers \`<TAB>\`; the ${shell} script at ${path} is out of date`,
|
|
198
198
|
remedy: INSTALL_REMEDY,
|
|
199
199
|
};
|
|
200
200
|
}
|
|
@@ -212,6 +212,6 @@ export async function checkShellCompletions(
|
|
|
212
212
|
return {
|
|
213
213
|
name: NAME,
|
|
214
214
|
status: "pass",
|
|
215
|
-
detail: `${shell} (the shell in
|
|
215
|
+
detail: `${shell} (the shell you are in) — installed, up to date, and completing ${candidates} commands`,
|
|
216
216
|
};
|
|
217
217
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness inventory roll-up — the pure half, driven by fixtures.
|
|
3
|
+
*
|
|
4
|
+
* `doctor.test.ts` covers the wiring (both scopes, `--verbose`, the real
|
|
5
|
+
* registry). What is unit-tested here is the classification itself: the
|
|
6
|
+
* registry supplies the universe so the NEGATIVE is representable, the groups
|
|
7
|
+
* supply the state, and no combination of the two may ever produce a `fail`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import {
|
|
12
|
+
harnessInventory,
|
|
13
|
+
type InventoryGroup,
|
|
14
|
+
type InventoryHarness,
|
|
15
|
+
type InventoryMember,
|
|
16
|
+
type InventoryRow,
|
|
17
|
+
inventoryHealth,
|
|
18
|
+
inventoryItems,
|
|
19
|
+
isDetectedState,
|
|
20
|
+
type WriteState,
|
|
21
|
+
} from "./harnessInventory.js";
|
|
22
|
+
|
|
23
|
+
const REGISTRY: InventoryHarness[] = [
|
|
24
|
+
{ id: "claude-code", name: "Claude Code", inScope: true },
|
|
25
|
+
{ id: "cursor", name: "Cursor", inScope: true },
|
|
26
|
+
{ id: "vscode", name: "VS Code", inScope: false },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** One harness's share of a file — its OWN write's state, never the file's. */
|
|
30
|
+
const named = (name: string, state: WriteState): InventoryMember => ({
|
|
31
|
+
name,
|
|
32
|
+
state,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const roll = (groups: InventoryGroup[]): InventoryRow[] =>
|
|
36
|
+
harnessInventory(REGISTRY, groups, "global");
|
|
37
|
+
|
|
38
|
+
describe("harnessInventory — the roll-up", () => {
|
|
39
|
+
it("classifies each registry harness against the scope's groups", () => {
|
|
40
|
+
const rows = roll([
|
|
41
|
+
{
|
|
42
|
+
path: "~/.claude.json",
|
|
43
|
+
harnesses: [named("Claude Code", "registered")],
|
|
44
|
+
},
|
|
45
|
+
{ path: "~/.cursor/mcp.json", harnesses: [named("Cursor", "absent")] },
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
expect(rows.map((r) => `${r.harnessId}:${r.state}`)).toEqual([
|
|
49
|
+
"claude-code:registered",
|
|
50
|
+
"cursor:detected",
|
|
51
|
+
// Not detected AND has no location in this scope at all — the two are
|
|
52
|
+
// different facts and the row says which one applies.
|
|
53
|
+
"vscode:unscoped",
|
|
54
|
+
]);
|
|
55
|
+
expect(rows[0]?.locations).toEqual(["~/.claude.json"]);
|
|
56
|
+
expect(rows[2]?.locations).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("keeps 'undetected' and 'unscoped' apart", () => {
|
|
60
|
+
// Same absence, two different reasons: Cursor could be here and is not,
|
|
61
|
+
// VS Code could never be here at all.
|
|
62
|
+
const rows = roll([]);
|
|
63
|
+
expect(rows.find((r) => r.harnessId === "cursor")?.state).toBe(
|
|
64
|
+
"undetected",
|
|
65
|
+
);
|
|
66
|
+
expect(rows.find((r) => r.harnessId === "vscode")?.state).toBe("unscoped");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("reports a drifted group as drifted, not as registered", () => {
|
|
70
|
+
const rows = roll([
|
|
71
|
+
{ path: "~/.claude.json", harnesses: [named("Claude Code", "drifted")] },
|
|
72
|
+
]);
|
|
73
|
+
expect(rows[0]?.state).toBe("drifted");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("a harness sharing two files is registered only when BOTH carry the entry", () => {
|
|
77
|
+
const both = harnessInventory(
|
|
78
|
+
REGISTRY,
|
|
79
|
+
[
|
|
80
|
+
{ path: "a.json", harnesses: [named("Cursor", "registered")] },
|
|
81
|
+
{ path: "b.json", harnesses: [named("Cursor", "registered")] },
|
|
82
|
+
],
|
|
83
|
+
"global",
|
|
84
|
+
);
|
|
85
|
+
expect(both[1]?.state).toBe("registered");
|
|
86
|
+
expect(both[1]?.locations).toEqual(["a.json", "b.json"]);
|
|
87
|
+
|
|
88
|
+
const mixed = harnessInventory(
|
|
89
|
+
REGISTRY,
|
|
90
|
+
[
|
|
91
|
+
{ path: "a.json", harnesses: [named("Cursor", "registered")] },
|
|
92
|
+
{ path: "b.json", harnesses: [named("Cursor", "absent")] },
|
|
93
|
+
],
|
|
94
|
+
"global",
|
|
95
|
+
);
|
|
96
|
+
expect(mixed[1]?.state).toBe("drifted");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("gives each harness sharing ONE file its own state, not the file's", () => {
|
|
100
|
+
// The co-detection case, and the reason a group carries per-harness state:
|
|
101
|
+
// `.vscode/mcp.json` holds VS Code's `servers` and Cline's `mcpServers` as
|
|
102
|
+
// two independent entries. Register pragma for VS Code alone and the FILE
|
|
103
|
+
// classifies as drifted — one key current, one absent — but neither
|
|
104
|
+
// harness is drifted: VS Code is registered and Cline is merely detected.
|
|
105
|
+
// A fixture with one harness per file cannot see this.
|
|
106
|
+
const shared: InventoryHarness[] = [
|
|
107
|
+
{ id: "cline", name: "Cline", inScope: true },
|
|
108
|
+
{ id: "vscode", name: "VS Code", inScope: true },
|
|
109
|
+
];
|
|
110
|
+
const rows = harnessInventory(
|
|
111
|
+
shared,
|
|
112
|
+
[
|
|
113
|
+
{
|
|
114
|
+
path: ".vscode/mcp.json",
|
|
115
|
+
harnesses: [named("Cline", "absent"), named("VS Code", "registered")],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
"project",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(rows.map((r) => `${r.harnessId}:${r.state}`)).toEqual([
|
|
122
|
+
"cline:detected",
|
|
123
|
+
"vscode:registered",
|
|
124
|
+
]);
|
|
125
|
+
// Both are located in the same file — the sharing itself is unchanged.
|
|
126
|
+
for (const row of rows) expect(row.locations).toEqual([".vscode/mcp.json"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("ignores a group naming a harness the registry does not know", () => {
|
|
130
|
+
// The inventory reports on the REGISTRY; a group is evidence, not a source
|
|
131
|
+
// of rows, so an unknown name adds nothing rather than inventing an entry.
|
|
132
|
+
const rows = roll([
|
|
133
|
+
{ path: "x.json", harnesses: [named("Ghost Editor", "registered")] },
|
|
134
|
+
]);
|
|
135
|
+
expect(rows).toHaveLength(REGISTRY.length);
|
|
136
|
+
expect(rows.map((r) => r.harnessName)).not.toContain("Ghost Editor");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("names every row with the scope it was rolled up for", () => {
|
|
140
|
+
const rows = harnessInventory(REGISTRY, [], "project");
|
|
141
|
+
expect(rows.every((r) => r.scope === "project")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("harnessInventory — the listing", () => {
|
|
146
|
+
const rows = roll([
|
|
147
|
+
{ path: "~/.claude.json", harnesses: [named("Claude Code", "registered")] },
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
it("shows detected harnesses only by default", () => {
|
|
151
|
+
const items = inventoryItems(rows, false);
|
|
152
|
+
expect(items.map((i) => i.label)).toEqual(["Claude Code"]);
|
|
153
|
+
expect(items[0]?.detail).toBe("registered — ~/.claude.json");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("--verbose widens the same rows to the whole registry", () => {
|
|
157
|
+
const items = inventoryItems(rows, true);
|
|
158
|
+
expect(items.map((i) => i.label)).toEqual([
|
|
159
|
+
"Claude Code",
|
|
160
|
+
"Cursor",
|
|
161
|
+
"VS Code",
|
|
162
|
+
]);
|
|
163
|
+
expect(items[1]?.detail).toBe("not detected");
|
|
164
|
+
// Scope-aware, and it states the FACT rather than naming a partition:
|
|
165
|
+
// VS Code keeps its MCP config per project, so there is no global file for
|
|
166
|
+
// pragma to be in. `no Global band` named an internal word for that.
|
|
167
|
+
expect(items[2]?.detail).toBe(
|
|
168
|
+
"keeps no global config — it is per-project only",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("never renders any state as a failure", () => {
|
|
173
|
+
for (const item of inventoryItems(rows, true)) {
|
|
174
|
+
expect(item.status).not.toBe("fail");
|
|
175
|
+
}
|
|
176
|
+
const every = harnessInventory(REGISTRY, [], "project");
|
|
177
|
+
for (const item of inventoryItems(every, true)) {
|
|
178
|
+
expect(item.status).not.toBe("fail");
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("is a listing, not a verdict: pass when the scope holds harnesses, skip when it does not", () => {
|
|
183
|
+
expect(inventoryHealth(rows, false).status).toBe("pass");
|
|
184
|
+
expect(inventoryHealth(rows, false).detail).toBe(
|
|
185
|
+
"1 detected · 1 registered",
|
|
186
|
+
);
|
|
187
|
+
expect(inventoryHealth(rows, true).detail).toBe(
|
|
188
|
+
"1 detected · 1 registered · 3 known",
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const empty = roll([]);
|
|
192
|
+
expect(inventoryHealth(empty, false).status).toBe("skip");
|
|
193
|
+
expect(inventoryHealth(empty, false).detail).toBe("no harnesses detected");
|
|
194
|
+
expect(inventoryHealth(empty, false).items).toEqual([]);
|
|
195
|
+
expect(inventoryHealth(empty, true).detail).toBe(
|
|
196
|
+
"none of 3 known harnesses detected",
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("counts a drifted harness as detected but not as registered", () => {
|
|
201
|
+
const drifted = roll([
|
|
202
|
+
{ path: "~/.claude.json", harnesses: [named("Claude Code", "drifted")] },
|
|
203
|
+
]);
|
|
204
|
+
expect(inventoryHealth(drifted, false).detail).toBe(
|
|
205
|
+
"1 detected · 0 registered",
|
|
206
|
+
);
|
|
207
|
+
expect(inventoryItems(drifted, false)[0]?.status).toBe("available");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("isDetectedState", () => {
|
|
212
|
+
it("counts every state that means the harness is on the machine", () => {
|
|
213
|
+
expect(isDetectedState("registered")).toBe(true);
|
|
214
|
+
expect(isDetectedState("drifted")).toBe(true);
|
|
215
|
+
expect(isDetectedState("detected")).toBe(true);
|
|
216
|
+
expect(isDetectedState("undetected")).toBe(false);
|
|
217
|
+
expect(isDetectedState("unscoped")).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
});
|