@canonical/pragma-cli 0.35.0 → 0.37.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 +24 -20
- package/dist/pragma.conf.js +266 -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/capabilities/hints.js +1 -1
- package/dist/src/capabilities/capabilities/hints.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/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.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/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.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/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- 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/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
- 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 +165 -63
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +122 -51
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +7 -6
- 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 +13 -2
- 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 +310 -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 +46 -24
- 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 +42 -9
- 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/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +14 -9
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +9 -3
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +24 -20
- package/pragma.conf.ts +272 -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/capabilities/hints.ts +1 -1
- package/src/capabilities/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +5 -2
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- 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/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- 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/lazy.test.ts +26 -2
- package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
- package/src/capabilities/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- 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/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
- package/src/capabilities/setup/operations/setupLsp.ts +203 -38
- package/src/capabilities/setup/operations/setupMcp.ts +139 -62
- package/src/capabilities/setup/operations/setupSkills.ts +399 -60
- 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 +1126 -50
- package/src/capabilities/setup/setup.verb.ts +174 -68
- package/src/capabilities/setup/targets.ts +138 -60
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +18 -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 +14 -6
- 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 +14 -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 +19 -3
- package/src/kernel/config/readConfig.test.ts +1 -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 +327 -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 +48 -25
- 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 +44 -8
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- package/src/kernel/render/prefixes.test.ts +2 -0
- 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/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +14 -12
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +9 -3
- package/src/kernel/runtime/types.ts +17 -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/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The scoped doctor rows, derived from the setup target table.
|
|
3
3
|
*
|
|
4
|
-
* `doctor` and `setup` are the SAME LIST. Every
|
|
4
|
+
* `doctor` and `setup` are the SAME LIST. Every scoped row here carries a
|
|
5
5
|
* target id verbatim — `completions`, not "Shell completions"; `mcp`, not "MCP
|
|
6
6
|
* configured" — because the row name IS the fix command's argument, and a user
|
|
7
7
|
* who reads `✗ mcp` beside `fix: pragma setup mcp` can see that they are the
|
|
@@ -11,11 +11,18 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Every row's detection comes from the target table's own `detect`, so the two
|
|
13
13
|
* surfaces cannot disagree about what they looked at, and every `fix:` is
|
|
14
|
-
* derived from the row's id and
|
|
14
|
+
* derived from the row's id and scope rather than authored per check.
|
|
15
15
|
*
|
|
16
|
-
* Doctor's
|
|
16
|
+
* Doctor's UNSCOPED environment checks (Node version, the CLI's own version,
|
|
17
17
|
* pack refs, the store) diagnose things setup cannot install and stay outside
|
|
18
18
|
* this list.
|
|
19
|
+
*
|
|
20
|
+
* ONE scoped row is not a target: `harnesses`, the per-scope inventory. It is an
|
|
21
|
+
* inventory of the machine the targets are measured against, not a target that
|
|
22
|
+
* can be set up, so it deliberately carries no `fix:` and can never be `fail`
|
|
23
|
+
* or `available` — see {@link inventoryChecks}. The bijection between the other
|
|
24
|
+
* rows and the setup table is untouched: `harnesses` is not a `TargetId`, and
|
|
25
|
+
* nothing derives a command from it.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
import { MCP_SERVER_NAME } from "../../../constants.js";
|
|
@@ -36,15 +43,24 @@ import {
|
|
|
36
43
|
lspSkipReason,
|
|
37
44
|
type McpDetection,
|
|
38
45
|
mcpGroupState,
|
|
46
|
+
mcpWriteState,
|
|
39
47
|
type SkillsDetection,
|
|
40
48
|
skillsSkipReason,
|
|
49
|
+
skillsSkipRemedy,
|
|
41
50
|
} from "../../setup/operations/index.js";
|
|
42
51
|
import { shortenPath, TARGET_IDS } from "../../setup/plan.js";
|
|
43
|
-
import
|
|
52
|
+
import { MCP_NO_LOCATION } from "../../setup/targets.js";
|
|
53
|
+
import type { CheckItem, CheckResult, Scope } from "../types.js";
|
|
44
54
|
import { checkShellCompletions } from "./checkShellCompletions.js";
|
|
55
|
+
import {
|
|
56
|
+
harnessInventory,
|
|
57
|
+
type InventoryGroup,
|
|
58
|
+
type InventoryHarness,
|
|
59
|
+
inventoryHealth,
|
|
60
|
+
} from "./harnessInventory.js";
|
|
45
61
|
import { commandResolves } from "./mcpCommand.js";
|
|
46
62
|
|
|
47
|
-
/** What a row reports before its name,
|
|
63
|
+
/** What a row reports before its name, scope and `fix:` line are attached. */
|
|
48
64
|
interface Health {
|
|
49
65
|
readonly status: CheckResult["status"];
|
|
50
66
|
readonly detail: string;
|
|
@@ -64,7 +80,8 @@ const configHealth = (
|
|
|
64
80
|
roots: { global: string; project: string },
|
|
65
81
|
): Health => {
|
|
66
82
|
const path = shortenPath(d.path, roots);
|
|
67
|
-
if (!d.exists)
|
|
83
|
+
if (!d.exists)
|
|
84
|
+
return { status: "available", detail: `${path} does not exist yet` };
|
|
68
85
|
if (!d.parses) {
|
|
69
86
|
return { status: "fail", detail: `${path} cannot be parsed as JSON` };
|
|
70
87
|
}
|
|
@@ -78,8 +95,8 @@ const lspHealth = (d: LspDetection): Health => {
|
|
|
78
95
|
}
|
|
79
96
|
const editors = lspEditorNames(d).join(", ");
|
|
80
97
|
return d.state === "installed"
|
|
81
|
-
? { status: "pass", detail: `installed
|
|
82
|
-
: { status: "available", detail: `not installed
|
|
98
|
+
? { status: "pass", detail: `installed in ${editors}` }
|
|
99
|
+
: { status: "available", detail: `not installed in ${editors}` };
|
|
83
100
|
};
|
|
84
101
|
|
|
85
102
|
/**
|
|
@@ -90,22 +107,19 @@ const lspHealth = (d: LspDetection): Health => {
|
|
|
90
107
|
*/
|
|
91
108
|
async function mcpHealth(
|
|
92
109
|
d: McpDetection,
|
|
93
|
-
|
|
110
|
+
scope: Scope,
|
|
94
111
|
cwd: string,
|
|
95
112
|
roots: { global: string; project: string },
|
|
96
113
|
): Promise<Health> {
|
|
97
114
|
if (d.groups.length === 0) {
|
|
98
|
-
return {
|
|
99
|
-
status: "skip",
|
|
100
|
-
detail: "no harness config location in this band",
|
|
101
|
-
};
|
|
115
|
+
return { status: "skip", detail: MCP_NO_LOCATION[scope] };
|
|
102
116
|
}
|
|
103
117
|
const resolves = await commandResolves(MCP_SERVER_NAME, cwd);
|
|
104
118
|
const items: CheckItem[] = d.groups.map((group) => {
|
|
105
119
|
const state = mcpGroupState(d, group.path);
|
|
106
120
|
const label = shortenPath(group.path, roots);
|
|
107
121
|
if (state === "absent") {
|
|
108
|
-
return { label, status: "available", detail: "not
|
|
122
|
+
return { label, status: "available", detail: "not registered" };
|
|
109
123
|
}
|
|
110
124
|
if (state === "drifted") {
|
|
111
125
|
return { label, status: "fail", detail: "entry differs from current" };
|
|
@@ -114,8 +128,8 @@ async function mcpHealth(
|
|
|
114
128
|
label,
|
|
115
129
|
status: resolves ? "pass" : "fail",
|
|
116
130
|
detail: resolves
|
|
117
|
-
? `
|
|
118
|
-
: `
|
|
131
|
+
? `registered, and \`${MCP_SERVER_NAME}\` is on PATH`
|
|
132
|
+
: `registered, but \`${MCP_SERVER_NAME}\` is not on PATH`,
|
|
119
133
|
};
|
|
120
134
|
});
|
|
121
135
|
const configured = items.filter((item) => item.status === "pass").length;
|
|
@@ -123,24 +137,29 @@ async function mcpHealth(
|
|
|
123
137
|
if (failing > 0) {
|
|
124
138
|
return {
|
|
125
139
|
status: "fail",
|
|
126
|
-
detail: `${failing} of ${items.length} need attention`,
|
|
140
|
+
detail: `${failing} of ${items.length} config files need attention`,
|
|
127
141
|
items,
|
|
128
142
|
};
|
|
129
143
|
}
|
|
130
144
|
if (configured === 0) {
|
|
131
|
-
//
|
|
132
|
-
// repository with no checked-in MCP config is
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
// Absence in the LOCAL PROJECT is not a failure: registering there is
|
|
146
|
+
// opt-in, so a repository with no checked-in MCP config is healthy. The
|
|
147
|
+
// line says so rather than leaving "(opt-in)" to be decoded.
|
|
148
|
+
return scope === "project"
|
|
149
|
+
? {
|
|
150
|
+
status: "skip",
|
|
151
|
+
detail:
|
|
152
|
+
"not registered for this project — per-project registration is opt-in",
|
|
153
|
+
}
|
|
135
154
|
: {
|
|
136
155
|
status: "available",
|
|
137
|
-
detail:
|
|
156
|
+
detail: `not registered in any of ${items.length} config files`,
|
|
138
157
|
items,
|
|
139
158
|
};
|
|
140
159
|
}
|
|
141
160
|
return {
|
|
142
161
|
status: configured === items.length ? "pass" : "available",
|
|
143
|
-
detail:
|
|
162
|
+
detail: `registered in ${configured} of ${items.length} config files`,
|
|
144
163
|
items,
|
|
145
164
|
};
|
|
146
165
|
}
|
|
@@ -148,13 +167,27 @@ async function mcpHealth(
|
|
|
148
167
|
/** The `skills` row: every expected link present, lstat-verified, none stale. */
|
|
149
168
|
function skillsHealth(
|
|
150
169
|
d: SkillsDetection,
|
|
151
|
-
|
|
170
|
+
scope: Scope,
|
|
152
171
|
roots: { global: string; project: string },
|
|
153
172
|
): Health {
|
|
154
|
-
|
|
173
|
+
const short = shortenPath(d.sourceRoot, roots);
|
|
174
|
+
// Stale links this scope owns are work even when the root holds no skill, so
|
|
175
|
+
// the row is only a skip once there is nothing left to reconcile either.
|
|
176
|
+
const orphans = d.rootExists ? d.orphans.length : 0;
|
|
177
|
+
if (!d.available && orphans === 0) {
|
|
178
|
+
// The skip carries a remedy — `setup`'s row and this row are the same
|
|
179
|
+
// finding, and the dead-end version of it ("no skills installed", no next
|
|
180
|
+
// step) was reported identically by both surfaces.
|
|
155
181
|
return {
|
|
156
182
|
status: "skip",
|
|
157
|
-
detail: skillsSkipReason(
|
|
183
|
+
detail: skillsSkipReason(short, scope, d.rootExists),
|
|
184
|
+
remedy: skillsSkipRemedy(short, scope),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (orphans > 0) {
|
|
188
|
+
return {
|
|
189
|
+
status: "available",
|
|
190
|
+
detail: `${orphans} link${orphans === 1 ? "" : "s"} point at a skill that is gone`,
|
|
158
191
|
};
|
|
159
192
|
}
|
|
160
193
|
const stale = d.actions.filter((a) => a.action === "replaced").length;
|
|
@@ -169,6 +202,22 @@ function skillsHealth(
|
|
|
169
202
|
// and reporting the two the same way made this row say "links current" where
|
|
170
203
|
// no link exists at all. Setup will never clear such a path — it is not this
|
|
171
204
|
// command's to delete — so the row carries the only remedy that settles it.
|
|
205
|
+
// A symlink pointing outside every root pragma owns is `skipped` so setup
|
|
206
|
+
// will never delete it — which would make this row claim "links current" over
|
|
207
|
+
// a path where the user's own link shadows a shipped skill. `skipped` also
|
|
208
|
+
// covers an already-correct link (owned) and a real directory (blocked), so
|
|
209
|
+
// the foreign case is what is left.
|
|
210
|
+
const foreign = d.actions.filter(
|
|
211
|
+
(a) => a.action === "skipped" && !a.owned && !a.blocked,
|
|
212
|
+
).length;
|
|
213
|
+
if (foreign > 0) {
|
|
214
|
+
return {
|
|
215
|
+
status: "available",
|
|
216
|
+
detail: `${foreign} of ${d.actions.length} link paths hold a symlink pragma does not own`,
|
|
217
|
+
remedy:
|
|
218
|
+
"Move or delete the symlink at that path, then link the skills again.",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
172
221
|
const blocked = d.actions.filter((a) => a.blocked).length;
|
|
173
222
|
if (blocked > 0) {
|
|
174
223
|
return {
|
|
@@ -198,7 +247,7 @@ async function healthOf(
|
|
|
198
247
|
roots: { global: string; project: string },
|
|
199
248
|
): Promise<Health> {
|
|
200
249
|
// A detection that threw is reported as ITS OWN failing row. Letting the
|
|
201
|
-
// rejection escape took the whole
|
|
250
|
+
// rejection escape took the whole scoped section of the report with it, so a
|
|
202
251
|
// single unreadable config file left the user with no rows at all — the one
|
|
203
252
|
// moment the report is most worth having.
|
|
204
253
|
const failure = detectionFailure(row);
|
|
@@ -220,55 +269,176 @@ async function healthOf(
|
|
|
220
269
|
case "lsp":
|
|
221
270
|
return lspHealth(row.detection as LspDetection);
|
|
222
271
|
case "mcp":
|
|
223
|
-
return mcpHealth(row.detection as McpDetection, row.
|
|
272
|
+
return mcpHealth(row.detection as McpDetection, row.scope, rt.cwd, roots);
|
|
224
273
|
default:
|
|
225
|
-
return skillsHealth(row.detection as SkillsDetection, row.
|
|
274
|
+
return skillsHealth(row.detection as SkillsDetection, row.scope, roots);
|
|
226
275
|
}
|
|
227
276
|
}
|
|
228
277
|
|
|
278
|
+
/** The scoped inventory row's name — a LISTING, not a setup target id. */
|
|
279
|
+
export const INVENTORY_CHECK = "harnesses";
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The `harnesses` row for one scope: what this machine has, and where pragma
|
|
283
|
+
* stands in each.
|
|
284
|
+
*
|
|
285
|
+
* It is the one scoped row that is NOT a target, and it earns that by never
|
|
286
|
+
* competing with the ones that are. Its status is only `pass` or `skip`, so it
|
|
287
|
+
* adds no failure and no `available` to the tally, and it derives no `fix:` —
|
|
288
|
+
* the `mcp` and `skills` rows beside it already name every command a harness
|
|
289
|
+
* needs, and repeating them here would be the same finding seen twice.
|
|
290
|
+
*
|
|
291
|
+
* The registry is the only thing detection could not supply: `detectHarnesses`
|
|
292
|
+
* filters to hits, so "Windsurf is not on this machine" was unrepresentable
|
|
293
|
+
* until the universe came from somewhere. Everything else — which harnesses
|
|
294
|
+
* share which file, and whether each one's own entry in it is current — is read
|
|
295
|
+
* straight off the `mcp` detection this report already ran. The projection to
|
|
296
|
+
* {@link InventoryGroup} is deliberate: `McpDetection` holds
|
|
297
|
+
* `writeMcpConfigTargets`/`removeMcpConfigFrom` as live functions, and a
|
|
298
|
+
* `mutates: false` command should be unable to reach a writer, not merely
|
|
299
|
+
* trusted not to.
|
|
300
|
+
*
|
|
301
|
+
* This is also where the harness↔`mcpKey` association is REJOINED, because it
|
|
302
|
+
* is the one place that holds both halves: `TargetGroup` records the names
|
|
303
|
+
* sharing a file and, separately, one write per distinct `mcpKey`, and the
|
|
304
|
+
* registry is what says which key a given harness writes. Without that join the
|
|
305
|
+
* only state available per harness is the file's aggregate — and a
|
|
306
|
+
* `.vscode/mcp.json` where VS Code's `servers` entry is current while Cline's
|
|
307
|
+
* `mcpServers` is absent aggregates to `drifted`, which would report BOTH
|
|
308
|
+
* harnesses as drifted when neither one is.
|
|
309
|
+
*
|
|
310
|
+
* @param rt - The per-invocation runtime (read for `--verbose`).
|
|
311
|
+
* @param rows - Every detected row, both scopes, as `scopedChecks` has them.
|
|
312
|
+
* @param roots - The roots every path renders relative to.
|
|
313
|
+
* @returns One inventory {@link CheckResult} per scope, global then project.
|
|
314
|
+
* @note Impure — dynamically imports the harness registry.
|
|
315
|
+
*/
|
|
316
|
+
async function inventoryChecks(
|
|
317
|
+
rt: PragmaRuntime,
|
|
318
|
+
rows: readonly DetectedRow[],
|
|
319
|
+
roots: { global: string; project: string },
|
|
320
|
+
): Promise<CheckResult[]> {
|
|
321
|
+
const { harnesses, isHarnessInBand } = await import("@canonical/harnesses");
|
|
322
|
+
const verbose = rt.globalFlags.verbose;
|
|
323
|
+
const scopes: readonly Scope[] = ["global", "project"];
|
|
324
|
+
// The key each harness NAME writes under — the half of the association
|
|
325
|
+
// `groupConfigTargets` consumed and did not record. Names are the only
|
|
326
|
+
// identity a group carries, and the registry's are unique.
|
|
327
|
+
const keyByName = new Map(harnesses.map((h) => [h.name, h.mcpKey]));
|
|
328
|
+
|
|
329
|
+
return scopes.map((scope): CheckResult => {
|
|
330
|
+
// `isHarnessInBand(harness.scope, scope, scope)` — the scope asked for as
|
|
331
|
+
// its OWN selection ("band" is the harnesses package's name for it) — is
|
|
332
|
+
// exactly "does this harness have a config location here", which is the
|
|
333
|
+
// question `detectMcp` already answers for that scope. Asking it under the
|
|
334
|
+
// `both` selection would instead answer "who writes here when both scopes
|
|
335
|
+
// run", and report every dual-scope harness as having no global location.
|
|
336
|
+
const registry: InventoryHarness[] = harnesses.map((harness) => ({
|
|
337
|
+
id: harness.id,
|
|
338
|
+
name: harness.name,
|
|
339
|
+
inScope: isHarnessInBand(harness.scope, scope, scope),
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
const mcpRow = rows.find(
|
|
343
|
+
(row) => row.target.id === "mcp" && row.scope === scope,
|
|
344
|
+
);
|
|
345
|
+
if (mcpRow === undefined || detectionFailure(mcpRow) !== undefined) {
|
|
346
|
+
return {
|
|
347
|
+
name: INVENTORY_CHECK,
|
|
348
|
+
status: "skip",
|
|
349
|
+
detail: "harness detection did not settle — see the `mcp` row",
|
|
350
|
+
scope,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const detection = mcpRow.detection as McpDetection;
|
|
355
|
+
const groups: InventoryGroup[] = detection.groups.map((group) => ({
|
|
356
|
+
path: shortenPath(group.path, roots),
|
|
357
|
+
harnesses: group.harnessNames.map((name) => {
|
|
358
|
+
// The harness's OWN write in this file. A name the registry does not
|
|
359
|
+
// know (or a key no write carries — impossible while the group was
|
|
360
|
+
// built from the same registry) falls back to the file's aggregate:
|
|
361
|
+
// strictly no worse than the state before this join existed.
|
|
362
|
+
const write = group.writes.find(
|
|
363
|
+
(w) => w.mcpKey === keyByName.get(name),
|
|
364
|
+
);
|
|
365
|
+
return {
|
|
366
|
+
name,
|
|
367
|
+
state:
|
|
368
|
+
write === undefined
|
|
369
|
+
? mcpGroupState(detection, group.path)
|
|
370
|
+
: mcpWriteState(detection, write),
|
|
371
|
+
};
|
|
372
|
+
}),
|
|
373
|
+
}));
|
|
374
|
+
|
|
375
|
+
const health = inventoryHealth(
|
|
376
|
+
harnessInventory(registry, groups, scope),
|
|
377
|
+
verbose,
|
|
378
|
+
);
|
|
379
|
+
return {
|
|
380
|
+
name: INVENTORY_CHECK,
|
|
381
|
+
status: health.status,
|
|
382
|
+
detail: health.detail,
|
|
383
|
+
scope,
|
|
384
|
+
...(health.items.length === 0 ? {} : { items: health.items }),
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
229
389
|
/**
|
|
230
|
-
* The command that repairs a row — derived from its id and
|
|
390
|
+
* The command that repairs a row — derived from its id and scope, never
|
|
231
391
|
* authored. This is the bijection made mechanical: a row exists because a
|
|
232
392
|
* target exists, and its fix is that target's own invocation.
|
|
233
393
|
*/
|
|
234
|
-
export const fixCommandFor = (
|
|
235
|
-
id:
|
|
236
|
-
band: ScopeBand,
|
|
237
|
-
bin: string,
|
|
238
|
-
): string => `${bin} setup ${id}${band === "project" ? " --local" : ""}`;
|
|
394
|
+
export const fixCommandFor = (id: string, scope: Scope, bin: string): string =>
|
|
395
|
+
`${bin} setup ${id}${scope === "project" ? " --local" : ""}`;
|
|
239
396
|
|
|
240
397
|
/**
|
|
241
|
-
* Run every
|
|
398
|
+
* Run every scoped check: the target table, both scopes, in table order, then
|
|
399
|
+
* each scope's harness inventory.
|
|
400
|
+
*
|
|
401
|
+
* The inventory rows come LAST within the array, and the renderer partitions by
|
|
402
|
+
* scope while preserving order, so each scope's section ends with the listing of
|
|
403
|
+
* what that scope actually holds — the targets first, then the machine they were
|
|
404
|
+
* measured against. They ride on the SAME detection pass: no target is probed a
|
|
405
|
+
* second time to produce them.
|
|
242
406
|
*
|
|
243
407
|
* @param rt - The per-invocation runtime.
|
|
244
408
|
* @param bin - The binary name the `fix:` lines are derived from.
|
|
245
|
-
* @returns One {@link CheckResult} per (target,
|
|
409
|
+
* @returns One {@link CheckResult} per (target, scope), plus one per scope.
|
|
246
410
|
* @note Impure — every target's detection reads the real filesystem.
|
|
247
411
|
*/
|
|
248
|
-
export async function
|
|
412
|
+
export async function scopedChecks(
|
|
249
413
|
rt: PragmaRuntime,
|
|
250
414
|
bin: string,
|
|
251
415
|
): Promise<CheckResult[]> {
|
|
252
416
|
const roots = await resolveRoots(rt);
|
|
253
417
|
const detected = await detectTargets(rt, [...TARGET_IDS], "both");
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
418
|
+
const [targetRows, inventoryRows] = await Promise.all([
|
|
419
|
+
Promise.all(
|
|
420
|
+
detected.map(async (row): Promise<CheckResult> => {
|
|
421
|
+
const health = await healthOf(row, rt, roots);
|
|
422
|
+
const needsFix =
|
|
423
|
+
health.status === "fail" || health.status === "available";
|
|
424
|
+
// A `skip` gets no DERIVED fix — re-running the target's own setup command
|
|
425
|
+
// would reproduce the skip. But a skip that AUTHORED a remedy has found a
|
|
426
|
+
// real next step on this machine (fill the scope's skill root, say), and
|
|
427
|
+
// dropping it is what made the skip a dead end on both surfaces.
|
|
428
|
+
const remedy = needsFix
|
|
429
|
+
? (health.remedy ?? fixCommandFor(row.target.id, row.scope, bin))
|
|
430
|
+
: health.remedy;
|
|
431
|
+
return {
|
|
432
|
+
name: row.target.id,
|
|
433
|
+
status: health.status,
|
|
434
|
+
detail: health.detail,
|
|
435
|
+
scope: row.scope,
|
|
436
|
+
...(health.items === undefined ? {} : { items: health.items }),
|
|
437
|
+
...(remedy === undefined ? {} : { remedy }),
|
|
438
|
+
};
|
|
439
|
+
}),
|
|
440
|
+
),
|
|
441
|
+
inventoryChecks(rt, detected, roots),
|
|
442
|
+
]);
|
|
443
|
+
return [...targetRows, ...inventoryRows];
|
|
274
444
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render goldens for `pragma doctor` — the
|
|
3
|
-
* color gate (F1).
|
|
2
|
+
* Render goldens for `pragma doctor` — the scope-grouped report AND the
|
|
3
|
+
* plain-path color gate (F1).
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
* both the plain and llm formatters, locking the
|
|
7
|
-
*
|
|
5
|
+
* The Global / Local project grouping (`partitionByScope` → sections) is pinned
|
|
6
|
+
* for both the plain and llm formatters, locking the vocabulary the flags
|
|
7
|
+
* already use — `--global` and `--local`. It is neither MACHINE/PROJECT (the
|
|
8
|
+
* first spelling) nor "band" (the second): "band" was this repository's word for
|
|
9
|
+
* the partition and nobody else's. Separately, the plain
|
|
8
10
|
* formatter tints ONLY on a color-capable TTY: `supports-color` reports a
|
|
9
11
|
* non-zero `chalk.level` off a TTY under `GITHUB_ACTIONS` / `FORCE_COLOR`, so the
|
|
10
12
|
* `isTTY` gate (via the shared style seam) keeps ANSI out of
|
|
11
|
-
* `doctor --format plain | tee`. `beforeEach` forces color OFF so the
|
|
13
|
+
* `doctor --format plain | tee`. `beforeEach` forces color OFF so the section
|
|
12
14
|
* goldens stay deterministic; the color tests opt a level back in explicitly.
|
|
13
15
|
*/
|
|
14
16
|
|
|
@@ -17,28 +19,28 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
|
17
19
|
import { doctorFormatters } from "./doctor.render.js";
|
|
18
20
|
import type { DoctorData } from "./types.js";
|
|
19
21
|
|
|
20
|
-
/** A fixture spanning all three groups: environment (no
|
|
21
|
-
const
|
|
22
|
+
/** A fixture spanning all three groups: environment (no scope), global, project. */
|
|
23
|
+
const SCOPED_DATA: DoctorData = {
|
|
22
24
|
checks: [
|
|
23
25
|
{ name: "Node version", status: "pass", detail: "v20 detected" },
|
|
24
26
|
{
|
|
25
27
|
name: "Shell completions",
|
|
26
28
|
status: "pass",
|
|
27
29
|
detail: "zsh up to date and resolving",
|
|
28
|
-
|
|
30
|
+
scope: "global",
|
|
29
31
|
},
|
|
30
32
|
{
|
|
31
33
|
name: "MCP configured",
|
|
32
34
|
status: "available",
|
|
33
35
|
detail: "not set up for Windsurf",
|
|
34
36
|
remedy: "pragma setup mcp",
|
|
35
|
-
|
|
37
|
+
scope: "global",
|
|
36
38
|
},
|
|
37
39
|
{
|
|
38
40
|
name: "Skills symlinked",
|
|
39
41
|
status: "skip",
|
|
40
42
|
detail: "no AI harnesses detected",
|
|
41
|
-
|
|
43
|
+
scope: "project",
|
|
42
44
|
},
|
|
43
45
|
],
|
|
44
46
|
passed: 2,
|
|
@@ -98,54 +100,59 @@ afterAll(() => {
|
|
|
98
100
|
chalk.level = prevLevel;
|
|
99
101
|
});
|
|
100
102
|
|
|
101
|
-
describe("doctor render —
|
|
102
|
-
it("groups checks under Global then
|
|
103
|
-
const out = doctorFormatters.plain(
|
|
103
|
+
describe("doctor render — scoped plain report", () => {
|
|
104
|
+
it("groups checks under Global then Local project headers", () => {
|
|
105
|
+
const out = doctorFormatters.plain(SCOPED_DATA);
|
|
104
106
|
const lines = out.split("\n");
|
|
105
|
-
// The two
|
|
107
|
+
// The two section headers use the words the flags use.
|
|
106
108
|
expect(lines).toContain("Global");
|
|
107
|
-
expect(lines).toContain("
|
|
108
|
-
// The superseded MACHINE/PROJECT labels are gone
|
|
109
|
+
expect(lines).toContain("Local project");
|
|
110
|
+
// The superseded MACHINE/PROJECT labels are gone — and so is "band", which
|
|
111
|
+
// no user-facing string in this report may say.
|
|
109
112
|
expect(out).not.toContain("MACHINE");
|
|
110
113
|
expect(out).not.toMatch(/^PROJECT$/m);
|
|
114
|
+
expect(out).not.toMatch(/\bbands?\b/);
|
|
111
115
|
});
|
|
112
116
|
|
|
113
|
-
it("orders environment → Global →
|
|
114
|
-
const out = doctorFormatters.plain(
|
|
115
|
-
// Environment check leads with no header; the two global
|
|
116
|
-
//
|
|
117
|
+
it("orders environment → Global → Local project, placing each check correctly", () => {
|
|
118
|
+
const out = doctorFormatters.plain(SCOPED_DATA);
|
|
119
|
+
// Environment check leads with no header; the two global checks sit under
|
|
120
|
+
// Global; the per-project check sits under Local project.
|
|
117
121
|
const at = (needle: string): number => out.indexOf(needle);
|
|
118
122
|
expect(at("Node version")).toBeGreaterThanOrEqual(0);
|
|
119
123
|
expect(at("Node version")).toBeLessThan(at("Global"));
|
|
120
124
|
expect(at("Global")).toBeLessThan(at("Shell completions"));
|
|
121
|
-
expect(at("MCP configured")).toBeLessThan(at("
|
|
122
|
-
expect(at("
|
|
123
|
-
// An available
|
|
125
|
+
expect(at("MCP configured")).toBeLessThan(at("Local project"));
|
|
126
|
+
expect(at("Local project")).toBeLessThan(at("Skills symlinked"));
|
|
127
|
+
// An available scoped check keeps its inline setup command under its scope.
|
|
124
128
|
expect(out).toContain("fix: pragma setup mcp");
|
|
125
129
|
// The tally closes the report.
|
|
126
130
|
expect(out).toContain("2 passed");
|
|
127
131
|
});
|
|
128
132
|
});
|
|
129
133
|
|
|
130
|
-
describe("doctor render —
|
|
131
|
-
it("groups checks under ### Global then ###
|
|
132
|
-
const out = doctorFormatters.llm(
|
|
134
|
+
describe("doctor render — scoped llm report", () => {
|
|
135
|
+
it("groups checks under ### Global then ### Local project headers", () => {
|
|
136
|
+
const out = doctorFormatters.llm(SCOPED_DATA);
|
|
133
137
|
expect(out).toContain("### Global");
|
|
134
|
-
expect(out).toContain("###
|
|
138
|
+
expect(out).toContain("### Local project");
|
|
135
139
|
expect(out).not.toContain("### Machine");
|
|
136
140
|
const at = (needle: string): number => out.indexOf(needle);
|
|
137
|
-
// Environment leads (no section header), then the two
|
|
141
|
+
// Environment leads (no section header), then the two scoped sections.
|
|
138
142
|
expect(at("Node version")).toBeLessThan(at("### Global"));
|
|
139
143
|
expect(at("### Global")).toBeLessThan(at("Shell completions"));
|
|
140
|
-
expect(at("Shell completions")).toBeLessThan(at("###
|
|
141
|
-
expect(at("###
|
|
144
|
+
expect(at("Shell completions")).toBeLessThan(at("### Local project"));
|
|
145
|
+
expect(at("### Local project")).toBeLessThan(at("Skills symlinked"));
|
|
142
146
|
expect(out).toContain("_2 passed, 0 failed, 1 available, 1 skipped_");
|
|
143
147
|
});
|
|
144
148
|
});
|
|
145
149
|
|
|
146
150
|
describe("doctor render — json", () => {
|
|
147
151
|
it("is the exact DoctorData round-trip", () => {
|
|
148
|
-
expect(JSON.parse(doctorFormatters.json(
|
|
152
|
+
expect(JSON.parse(doctorFormatters.json(SCOPED_DATA))).toEqual(SCOPED_DATA);
|
|
153
|
+
// The machine surface uses the scope vocabulary too — the retired word
|
|
154
|
+
// cannot survive as a JSON key.
|
|
155
|
+
expect(doctorFormatters.json(SCOPED_DATA)).not.toMatch(/\bbands?\b/);
|
|
149
156
|
});
|
|
150
157
|
});
|
|
151
158
|
|
|
@@ -179,3 +186,72 @@ describe("doctor render — color ON (attended TTY)", () => {
|
|
|
179
186
|
});
|
|
180
187
|
});
|
|
181
188
|
});
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The harness inventory renders through the EXISTING check-with-items
|
|
192
|
+
* primitive — a listing needs no new machinery, only a check whose `items` are
|
|
193
|
+
* harnesses. This pins that it reads as an inventory in both formats: named
|
|
194
|
+
* harnesses under their scope, mixed per-item glyphs, and NO `fix:` line (the
|
|
195
|
+
* `mcp`/`skills` rows own every action a harness can need).
|
|
196
|
+
*/
|
|
197
|
+
const INVENTORY_DATA: DoctorData = {
|
|
198
|
+
checks: [
|
|
199
|
+
{
|
|
200
|
+
name: "harnesses",
|
|
201
|
+
status: "pass",
|
|
202
|
+
detail: "1 detected · 1 registered · 3 known",
|
|
203
|
+
scope: "global",
|
|
204
|
+
items: [
|
|
205
|
+
{
|
|
206
|
+
label: "Claude Code",
|
|
207
|
+
status: "pass",
|
|
208
|
+
detail: "registered — ~/.claude.json",
|
|
209
|
+
},
|
|
210
|
+
{ label: "Cursor", status: "skip", detail: "not detected" },
|
|
211
|
+
{
|
|
212
|
+
label: "VS Code",
|
|
213
|
+
status: "skip",
|
|
214
|
+
detail: "keeps no global config",
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
passed: 1,
|
|
220
|
+
failed: 0,
|
|
221
|
+
available: 0,
|
|
222
|
+
skipped: 0,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
describe("doctor render — the harness inventory", () => {
|
|
226
|
+
it("renders one aligned sub-item per harness, with no fix line", () => {
|
|
227
|
+
const out = doctorFormatters.plain(INVENTORY_DATA);
|
|
228
|
+
expect(out).toContain("Global");
|
|
229
|
+
expect(out).toContain("harnesses");
|
|
230
|
+
expect(out).toContain("1 detected · 1 registered · 3 known");
|
|
231
|
+
expect(out).toContain("· ✓ Claude Code registered — ~/.claude.json");
|
|
232
|
+
expect(out).toContain("· ○ Cursor not detected");
|
|
233
|
+
expect(out).toContain("· ○ VS Code keeps no global config");
|
|
234
|
+
// A listing proposes nothing — and never renders a failure glyph.
|
|
235
|
+
expect(out).not.toContain("fix:");
|
|
236
|
+
expect(out).not.toContain("✗");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("nests the harnesses under the check in the llm format", () => {
|
|
240
|
+
const out = doctorFormatters.llm(INVENTORY_DATA);
|
|
241
|
+
expect(out).toContain("### Global");
|
|
242
|
+
expect(out).toContain(
|
|
243
|
+
"- ✓ **harnesses**: 1 detected · 1 registered · 3 known",
|
|
244
|
+
);
|
|
245
|
+
expect(out).toContain(" - ✓ Claude Code: registered — ~/.claude.json");
|
|
246
|
+
expect(out).toContain(" - ○ VS Code: keeps no global config");
|
|
247
|
+
expect(out).not.toContain("_fix:_");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("round-trips through json with every row intact", () => {
|
|
251
|
+
const parsed = JSON.parse(
|
|
252
|
+
doctorFormatters.json(INVENTORY_DATA),
|
|
253
|
+
) as DoctorData;
|
|
254
|
+
expect(parsed).toEqual(INVENTORY_DATA);
|
|
255
|
+
expect(parsed.checks[0]?.items).toHaveLength(3);
|
|
256
|
+
});
|
|
257
|
+
});
|