@canonical/pragma-cli 0.32.0 → 0.34.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 +72 -0
- package/dist/pragma +0 -0
- package/package.json +19 -15
- package/pragma.conf.ts +664 -0
- package/src/bin.ts +15 -6
- package/src/capabilities/block.parity.test.ts +250 -0
- package/src/capabilities/capabilities/capabilities.render.ts +3 -2
- package/src/capabilities/capabilities/capabilities.test.ts +10 -1
- package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
- package/src/capabilities/capabilities/catalog.ts +20 -8
- package/src/capabilities/capabilities/hints.ts +21 -36
- package/src/capabilities/capabilities/types.ts +2 -0
- package/src/capabilities/colophon/collectColophon.ts +34 -19
- package/src/capabilities/colophon/colophon.render.test.ts +31 -4
- package/src/capabilities/colophon/colophon.render.ts +13 -4
- package/src/capabilities/colophon/colophon.test.ts +3 -3
- package/src/capabilities/colophon/colophon.verb.ts +11 -4
- package/src/capabilities/colophon/markdownTerminal.ts +33 -14
- package/src/capabilities/colophon/types.ts +16 -7
- package/src/capabilities/config/collectConfigShow.test.ts +57 -0
- package/src/capabilities/config/collectConfigShow.ts +41 -4
- package/src/capabilities/config/fields.ts +10 -108
- package/src/capabilities/config/set.test.ts +6 -6
- package/src/capabilities/config/set.verb.ts +12 -8
- package/src/capabilities/config/show.render.test.ts +137 -0
- package/src/capabilities/config/show.render.ts +57 -16
- package/src/capabilities/config/show.verb.ts +9 -8
- package/src/capabilities/config/types.ts +8 -0
- package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
- package/src/capabilities/create/byteEquality.test.ts +82 -101
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
- package/src/capabilities/create/constants.ts +75 -0
- package/src/capabilities/create/create.mcp.test.ts +2 -2
- package/src/capabilities/create/create.test.ts +192 -16
- package/src/capabilities/create/create.verb.ts +208 -79
- package/src/capabilities/create/index.ts +2 -10
- package/src/capabilities/create/pickGenerator.ts +34 -28
- package/src/capabilities/create/templates.embedded.generated.ts +4 -29
- package/src/capabilities/create/types.ts +10 -6
- package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
- package/src/capabilities/distribution.test.ts +120 -0
- package/src/capabilities/distribution.ts +59 -0
- package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
- package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
- package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
- package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
- package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
- package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
- package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
- package/src/capabilities/doctor/doctor.render.test.ts +170 -0
- package/src/capabilities/doctor/doctor.render.ts +117 -34
- package/src/capabilities/doctor/doctor.test.ts +68 -9
- package/src/capabilities/doctor/doctor.verb.ts +6 -2
- package/src/capabilities/doctor/runChecks.ts +33 -9
- package/src/capabilities/doctor/types.ts +16 -1
- package/src/capabilities/emptyRecovery.test.ts +32 -15
- package/src/capabilities/graph/inspect.verb.ts +3 -2
- package/src/capabilities/graph/query.verb.ts +3 -2
- package/src/capabilities/graph/runQuery.ts +8 -5
- package/src/capabilities/index.ts +22 -18
- package/src/capabilities/info/collectInfo.ts +10 -6
- package/src/capabilities/info/info.render.ts +3 -2
- package/src/capabilities/info/info.verb.ts +3 -2
- package/src/capabilities/lazy.test.ts +93 -2
- package/src/capabilities/ontology/index.ts +10 -3
- package/src/capabilities/ontology/ontology.test.ts +157 -5
- package/src/capabilities/ontology/verbs.ts +203 -104
- package/src/capabilities/prompt/prompt.render.test.ts +94 -0
- package/src/capabilities/prompt/prompt.render.ts +23 -6
- package/src/capabilities/prompt/prompt.test.ts +68 -1
- package/src/capabilities/prompt/verbs.ts +24 -11
- package/src/capabilities/reference.test.ts +32 -0
- package/src/capabilities/resources/provider.ts +58 -25
- package/src/capabilities/resources/resources.test.ts +176 -21
- package/src/capabilities/scopeTypeSync.test.ts +37 -0
- package/src/capabilities/setup/index.ts +7 -1
- package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
- package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
- package/src/capabilities/setup/operations/setupLsp.ts +56 -12
- package/src/capabilities/setup/operations/setupMcp.ts +216 -39
- package/src/capabilities/setup/operations/setupSkills.ts +10 -4
- package/src/capabilities/setup/setup.render.test.ts +109 -0
- package/src/capabilities/setup/setup.render.ts +61 -14
- package/src/capabilities/setup/setup.test.ts +352 -1
- package/src/capabilities/setup/setup.verb.ts +68 -9
- package/src/capabilities/setup/shell.ts +29 -3
- package/src/capabilities/setup/types.ts +59 -2
- package/src/capabilities/shared/bands.ts +16 -0
- package/src/capabilities/skill/discover.ts +11 -5
- package/src/capabilities/skill/render.ts +4 -5
- package/src/capabilities/skill/verbs.ts +7 -7
- package/src/capabilities/sources/collectStatus.ts +69 -49
- package/src/capabilities/sources/index.ts +1 -1
- package/src/capabilities/sources/installSkills.ts +2 -2
- package/src/capabilities/sources/runUpdate.ts +146 -98
- package/src/capabilities/sources/sources.test.ts +264 -84
- package/src/capabilities/sources/status.render.test.ts +94 -0
- package/src/capabilities/sources/status.render.ts +55 -33
- package/src/capabilities/sources/status.verb.ts +15 -8
- package/src/capabilities/sources/types.ts +18 -25
- package/src/capabilities/sources/update.render.ts +9 -2
- package/src/capabilities/sources/update.verb.ts +12 -23
- package/src/capabilities/surface.test.ts +65 -17
- package/src/capabilities/tier.parity.test.ts +144 -0
- package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
- package/src/constants.test.ts +9 -7
- package/src/constants.ts +49 -23
- package/src/identity.test.ts +333 -0
- package/src/index.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
- package/src/kernel/completion/complete.test.ts +3 -5
- package/src/kernel/completion/emitScripts.test.ts +1 -1
- package/src/kernel/completion/emitScripts.ts +4 -2
- package/src/kernel/completion/entitySource.test.ts +185 -41
- package/src/kernel/completion/entitySource.ts +131 -113
- package/src/kernel/completion/model.test.ts +3 -3
- package/src/kernel/completion/model.ts +11 -1
- package/src/kernel/completion/parse.test.ts +2 -4
- package/src/kernel/completion/safety.test.ts +82 -36
- package/src/kernel/completion/shellDrive.test.ts +592 -0
- package/src/kernel/completion/templates/fish.ts +28 -6
- package/src/kernel/completion/types.ts +3 -3
- package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
- package/src/kernel/config/defaults.test.ts +85 -0
- package/src/kernel/config/defaults.ts +12 -23
- package/src/kernel/config/evaluateProjectConfig.ts +2 -3
- package/src/kernel/config/findProjectConfig.ts +4 -3
- package/src/kernel/config/firstRun.ts +12 -5
- package/src/kernel/config/globalConfig.ts +2 -2
- package/src/kernel/config/index.ts +4 -5
- package/src/kernel/config/paths.ts +17 -17
- package/src/kernel/config/readConfig.test.ts +242 -6
- package/src/kernel/config/readConfig.ts +8 -8
- package/src/kernel/config/schema.test.ts +50 -0
- package/src/kernel/config/schema.ts +97 -10
- package/src/kernel/config/types.ts +73 -19
- package/src/kernel/copy.test.ts +453 -0
- package/src/kernel/error/PragmaError.test.ts +5 -5
- package/src/kernel/error/constants.ts +3 -2
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +22 -45
- package/src/kernel/error/recovery.ts +22 -27
- package/src/kernel/error/renderError.ts +1 -1
- package/src/kernel/error/types.ts +5 -4
- package/src/kernel/packs/collect.test.ts +195 -11
- package/src/kernel/packs/collect.ts +213 -58
- package/src/kernel/packs/compile.test.ts +163 -7
- package/src/kernel/packs/compile.ts +15 -12
- package/src/kernel/packs/disclosure.test.ts +5 -2
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
- package/src/kernel/packs/graphqlSource.test.ts +6 -3
- package/src/kernel/packs/iri.ts +5 -7
- package/src/kernel/packs/renderPack.ts +5 -3
- package/src/kernel/packs/resolveEntity.ts +21 -17
- package/src/kernel/packs/runBodies.ts +15 -14
- package/src/kernel/packs/schema.ts +96 -10
- package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
- package/src/kernel/packs/sparql/runSelect.ts +73 -34
- package/src/kernel/packs/toolDescription.test.ts +10 -2
- package/src/kernel/packs/types.ts +39 -4
- package/src/kernel/packs/uniqueness.ts +4 -4
- package/src/kernel/project/cli/buildProgram.test.ts +60 -11
- package/src/kernel/project/cli/buildProgram.ts +11 -0
- package/src/kernel/project/cli/dispatch.test.ts +67 -0
- package/src/kernel/project/cli/dispatch.ts +34 -12
- package/src/kernel/project/cli/globalFlags.test.ts +12 -4
- package/src/kernel/project/cli/globalFlags.ts +14 -14
- package/src/kernel/project/cli/helpFormat.ts +1 -1
- package/src/kernel/project/cli/rootHelp.ts +109 -107
- package/src/kernel/project/mcp/buildServer.ts +11 -6
- package/src/kernel/project/mcp/instructions.ts +12 -7
- package/src/kernel/project/mcp/mcpError.ts +52 -0
- package/src/kernel/project/mcp/parity.test.ts +39 -1
- package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
- package/src/kernel/project/mcp/prompts/provider.ts +51 -7
- package/src/kernel/project/mcp/prompts/source.ts +68 -67
- package/src/kernel/project/mcp/registerVerb.ts +25 -6
- package/src/kernel/project/mcp/serve.ts +11 -3
- package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
- package/src/kernel/render/disclosure.ts +4 -2
- package/src/kernel/render/formatters.ts +5 -4
- package/src/kernel/render/prefixes.test.ts +36 -0
- package/src/kernel/render/prefixes.ts +40 -13
- package/src/kernel/render/renderers.test.ts +35 -1
- package/src/kernel/render/renderers.ts +25 -7
- package/src/kernel/render/style.test.ts +31 -0
- package/src/kernel/render/style.ts +74 -0
- package/src/kernel/runtime/graphpack/build.ts +57 -3
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
- package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +37 -11
- package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
- package/src/kernel/runtime/graphpack/hash.ts +3 -2
- package/src/kernel/runtime/graphpack/index.ts +1 -1
- package/src/kernel/runtime/graphpack/manifest.ts +114 -12
- package/src/kernel/runtime/graphpack/read.ts +32 -22
- package/src/kernel/runtime/graphpack/schemas.ts +74 -0
- package/src/kernel/runtime/graphpack/stories.ts +84 -0
- package/src/kernel/runtime/graphpack/types.ts +72 -45
- package/src/kernel/runtime/loadSession.ts +4 -12
- package/src/kernel/runtime/paths.ts +64 -15
- package/src/kernel/runtime/probe.test.ts +8 -4
- package/src/kernel/runtime/readEntity.ts +6 -5
- package/src/kernel/runtime/refs/gitOps.ts +1 -19
- package/src/kernel/runtime/refs/parseRef.ts +7 -7
- package/src/kernel/runtime/refs/resolve.ts +141 -65
- package/src/kernel/runtime/resolveSources.test.ts +63 -24
- package/src/kernel/runtime/resolveSources.ts +36 -25
- package/src/kernel/runtime/storeReadiness.ts +62 -0
- package/src/kernel/runtime/storeless.test.ts +1 -1
- package/src/kernel/runtime/types.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
- package/src/kernel/spec/emitReference.test.ts +196 -0
- package/src/kernel/spec/emitReference.ts +624 -0
- package/src/kernel/spec/emitSurface.ts +13 -7
- package/src/kernel/spec/index.ts +2 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/surfaceConformance.ts +8 -0
- package/src/kernel/spec/types.ts +29 -9
- package/src/kernel/spec/validate.ts +2 -1
- package/src/kernel/vocabulary.test.ts +164 -0
- package/src/kernel/vocabulary.ts +146 -0
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/README.md +3 -3
- package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
- package/src/testing/behavioral/completion.test.ts +21 -8
- package/src/testing/behavioral/disclosure.test.ts +12 -0
- package/src/testing/behavioral/docExamples.test.ts +283 -0
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
- package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
- package/src/testing/behavioral/harness.foundation.test.ts +14 -5
- package/src/testing/behavioral/journeys.cli.test.ts +116 -73
- package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
- package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
- package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
- package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
- package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
- package/src/testing/behavioral/liveReadSurface.ts +4 -3
- package/src/testing/behavioral/mcpSurface.test.ts +10 -5
- package/src/testing/behavioral/staleCommands.test.ts +86 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
- package/src/testing/eval/cases/assembly.ts +16 -9
- package/src/testing/eval/cases/effectful.ts +31 -16
- package/src/testing/eval/cases/readNouns.ts +23 -18
- package/src/testing/eval/cases/stable.ts +5 -1
- package/src/testing/eval/eval.test.ts +7 -1
- package/src/testing/fixtures/fixtureCapability.ts +201 -1
- package/src/testing/fixtures/graph/canonical.ts +6 -3
- package/src/testing/fixtures/graph/defaultPack.ts +179 -0
- package/src/testing/fixtures/graph/owlExport.ts +97 -0
- package/src/testing/helpers/extractSnippets.ts +29 -0
- package/src/testing/helpers/fixtureGraph.ts +33 -16
- package/src/testing/helpers/golden.ts +1 -1
- package/src/testing/helpers/packRuntime.ts +2 -3
- package/src/testing/helpers/projectCli.ts +2 -1
- package/src/testing/helpers/projectMcp.ts +6 -0
- package/src/testing/perf/budgets.test.ts +4 -0
- package/src/testing/perf/budgets.ts +22 -7
- package/src/testing/perf/globalSetup.ts +121 -6
- package/src/types.ts +2 -2
- package/src/capabilities/block/blockList.verb.ts +0 -150
- package/src/capabilities/block/index.ts +0 -25
- package/src/capabilities/block/pack.ts +0 -155
- package/src/capabilities/block/parity.test.ts +0 -180
- package/src/capabilities/block/tierChain.ts +0 -78
- package/src/capabilities/colophon/pragmaColophon.ts +0 -66
- package/src/capabilities/config/field.test.ts +0 -188
- package/src/capabilities/modifier/index.ts +0 -15
- package/src/capabilities/modifier/pack.ts +0 -64
- package/src/capabilities/standard/index.ts +0 -18
- package/src/capabilities/standard/pack.ts +0 -128
- package/src/capabilities/tier/index.ts +0 -19
- package/src/capabilities/tier/lookup.render.ts +0 -34
- package/src/capabilities/tier/lookup.test.ts +0 -68
- package/src/capabilities/tier/lookup.verb.ts +0 -44
- package/src/capabilities/tier/pack.ts +0 -28
- package/src/capabilities/tier/runLookup.ts +0 -59
- package/src/capabilities/token/addConfig.render.ts +0 -27
- package/src/capabilities/token/addConfig.test.ts +0 -62
- package/src/capabilities/token/addConfig.verb.ts +0 -40
- package/src/capabilities/token/index.ts +0 -19
- package/src/capabilities/token/pack.ts +0 -58
- package/src/capabilities/token/runAddConfig.ts +0 -95
- package/src/kernel/completion/bashDrive.test.ts +0 -73
- package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
- package/src/kernel/runtime/lock.ts +0 -90
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render goldens for `pragma doctor` — the band-grouped report AND the plain-path
|
|
3
|
+
* color gate (F1).
|
|
4
|
+
*
|
|
5
|
+
* The banded Global/Project grouping (`partitionByBand` → sections) is pinned for
|
|
6
|
+
* both the plain and llm formatters, locking the unified Global/Project
|
|
7
|
+
* vocabulary (the report no longer says MACHINE/PROJECT). Separately, the plain
|
|
8
|
+
* formatter tints ONLY on a color-capable TTY: `supports-color` reports a
|
|
9
|
+
* non-zero `chalk.level` off a TTY under `GITHUB_ACTIONS` / `FORCE_COLOR`, so the
|
|
10
|
+
* `isTTY` gate (via the shared style seam) keeps ANSI out of
|
|
11
|
+
* `doctor --format plain | tee`. `beforeEach` forces color OFF so the banded
|
|
12
|
+
* goldens stay deterministic; the color tests opt a level back in explicitly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
17
|
+
import { doctorFormatters } from "./doctor.render.js";
|
|
18
|
+
import type { DoctorData } from "./types.js";
|
|
19
|
+
|
|
20
|
+
/** A fixture spanning all three groups: environment (no band), global, project. */
|
|
21
|
+
const BANDED_DATA: DoctorData = {
|
|
22
|
+
checks: [
|
|
23
|
+
{ name: "Node version", status: "pass", detail: "v20 detected" },
|
|
24
|
+
{
|
|
25
|
+
name: "Shell completions",
|
|
26
|
+
status: "pass",
|
|
27
|
+
detail: "zsh up to date and resolving",
|
|
28
|
+
band: "global",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "MCP configured",
|
|
32
|
+
status: "fail",
|
|
33
|
+
detail: "detected Windsurf but pragma not configured",
|
|
34
|
+
remedy: "pragma setup mcp",
|
|
35
|
+
band: "global",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "Skills symlinked",
|
|
39
|
+
status: "skip",
|
|
40
|
+
detail: "no AI harnesses detected",
|
|
41
|
+
band: "project",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
passed: 2,
|
|
45
|
+
failed: 1,
|
|
46
|
+
skipped: 1,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A fixture spanning pass/fail/skip, sub-items, and a remedy — every color path. */
|
|
50
|
+
const COLOR_DATA: DoctorData = {
|
|
51
|
+
checks: [
|
|
52
|
+
{ name: "Node version", status: "pass", detail: "v24" },
|
|
53
|
+
{
|
|
54
|
+
name: "pack refs",
|
|
55
|
+
status: "fail",
|
|
56
|
+
detail: "packs are configured but the store has not been built",
|
|
57
|
+
items: [
|
|
58
|
+
{ label: "core", status: "fail", detail: "not built" },
|
|
59
|
+
{ label: "ui", status: "fail", detail: "not built" },
|
|
60
|
+
],
|
|
61
|
+
remedy: "pragma sources update",
|
|
62
|
+
},
|
|
63
|
+
{ name: "Skills symlinked", status: "skip", detail: "no harness" },
|
|
64
|
+
],
|
|
65
|
+
passed: 1,
|
|
66
|
+
failed: 1,
|
|
67
|
+
skipped: 1,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Run `body` with stdout's `isTTY` forced to `value`, then restore it. */
|
|
71
|
+
function withStdoutTty(value: boolean | undefined, body: () => void): void {
|
|
72
|
+
const stream = process.stdout as { isTTY?: boolean };
|
|
73
|
+
const saved = stream.isTTY;
|
|
74
|
+
stream.isTTY = value;
|
|
75
|
+
try {
|
|
76
|
+
body();
|
|
77
|
+
} finally {
|
|
78
|
+
stream.isTTY = saved;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let prevLevel: number;
|
|
83
|
+
beforeAll(() => {
|
|
84
|
+
prevLevel = chalk.level;
|
|
85
|
+
});
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
chalk.level = 0; // color-free ⇒ deterministic text (color tests opt back in)
|
|
88
|
+
});
|
|
89
|
+
afterAll(() => {
|
|
90
|
+
chalk.level = prevLevel;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("doctor render — banded plain report", () => {
|
|
94
|
+
it("groups checks under Global then Project headers (unified labels)", () => {
|
|
95
|
+
const out = doctorFormatters.plain(BANDED_DATA);
|
|
96
|
+
const lines = out.split("\n");
|
|
97
|
+
// The two banded section headers use the unified Global/Project vocabulary.
|
|
98
|
+
expect(lines).toContain("Global");
|
|
99
|
+
expect(lines).toContain("Project");
|
|
100
|
+
// The superseded MACHINE/PROJECT labels are gone.
|
|
101
|
+
expect(out).not.toContain("MACHINE");
|
|
102
|
+
expect(out).not.toMatch(/^PROJECT$/m);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("orders environment → Global → Project, banding each check correctly", () => {
|
|
106
|
+
const out = doctorFormatters.plain(BANDED_DATA);
|
|
107
|
+
// Environment check leads with no header; the two global-band checks sit
|
|
108
|
+
// under Global; the project-band check sits under Project.
|
|
109
|
+
const at = (needle: string): number => out.indexOf(needle);
|
|
110
|
+
expect(at("Node version")).toBeGreaterThanOrEqual(0);
|
|
111
|
+
expect(at("Node version")).toBeLessThan(at("Global"));
|
|
112
|
+
expect(at("Global")).toBeLessThan(at("Shell completions"));
|
|
113
|
+
expect(at("MCP configured")).toBeLessThan(at("Project"));
|
|
114
|
+
expect(at("Project")).toBeLessThan(at("Skills symlinked"));
|
|
115
|
+
// A failing banded check keeps its inline remedy under its band.
|
|
116
|
+
expect(out).toContain("fix: pragma setup mcp");
|
|
117
|
+
// The tally closes the report.
|
|
118
|
+
expect(out).toContain("2 passed");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("doctor render — banded llm report", () => {
|
|
123
|
+
it("groups checks under ### Global then ### Project headers", () => {
|
|
124
|
+
const out = doctorFormatters.llm(BANDED_DATA);
|
|
125
|
+
expect(out).toContain("### Global");
|
|
126
|
+
expect(out).toContain("### Project");
|
|
127
|
+
expect(out).not.toContain("### Machine");
|
|
128
|
+
const at = (needle: string): number => out.indexOf(needle);
|
|
129
|
+
// Environment leads (no section header), then the two banded sections.
|
|
130
|
+
expect(at("Node version")).toBeLessThan(at("### Global"));
|
|
131
|
+
expect(at("### Global")).toBeLessThan(at("Shell completions"));
|
|
132
|
+
expect(at("Shell completions")).toBeLessThan(at("### Project"));
|
|
133
|
+
expect(at("### Project")).toBeLessThan(at("Skills symlinked"));
|
|
134
|
+
expect(out).toContain("_2 passed, 1 failed, 1 skipped_");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("doctor render — json", () => {
|
|
139
|
+
it("is the exact DoctorData round-trip", () => {
|
|
140
|
+
expect(JSON.parse(doctorFormatters.json(BANDED_DATA))).toEqual(BANDED_DATA);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("doctor render — piped output is ANSI-free (F1)", () => {
|
|
145
|
+
it("plain emits ZERO ANSI off a TTY even when chalk reports color (CI/FORCE_COLOR)", () => {
|
|
146
|
+
chalk.level = 3;
|
|
147
|
+
withStdoutTty(undefined, () => {
|
|
148
|
+
const out = doctorFormatters.plain(COLOR_DATA);
|
|
149
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: asserting NO ESC byte survives
|
|
150
|
+
expect(out).not.toMatch(/\x1b\[/);
|
|
151
|
+
// The structural, color-free content still renders (glyphs, names, remedy).
|
|
152
|
+
expect(out).toContain("pragma doctor");
|
|
153
|
+
expect(out).toContain("✓ Node version");
|
|
154
|
+
expect(out).toContain("✗ pack refs");
|
|
155
|
+
expect(out).toContain("○ Skills symlinked");
|
|
156
|
+
expect(out).toContain("↳ fix: pragma sources update");
|
|
157
|
+
expect(out).toContain(" 1 passed · 1 failed · 1 skipped");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("doctor render — color ON (attended TTY)", () => {
|
|
163
|
+
it("plain tints the output on a color-capable TTY", () => {
|
|
164
|
+
chalk.level = 1;
|
|
165
|
+
withStdoutTty(true, () => {
|
|
166
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: asserting the literal ESC byte is the point
|
|
167
|
+
expect(doctorFormatters.plain(COLOR_DATA)).toMatch(/\x1b\[/);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -1,35 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Formatters for `pragma doctor` — plain (chalk), llm (Markdown), json.
|
|
2
|
+
* Formatters for `pragma doctor` — plain (chalk on a TTY), llm (Markdown), json.
|
|
3
3
|
*
|
|
4
4
|
* Ported from the old shell's `doctor/formatters/doctor.ts`, retargeted at the
|
|
5
|
-
* kernel `Formatters` contract.
|
|
5
|
+
* kernel `Formatters` contract. The plain path is colored ONLY on a color-capable
|
|
6
|
+
* TTY: it consults the shared {@link defaultStyle} seam (stdout `isTTY` AND a
|
|
7
|
+
* non-zero chalk level), so a piped / redirected / CI run — where `supports-color`
|
|
8
|
+
* can report a level with no TTY (`GITHUB_ACTIONS`, `FORCE_COLOR`) — renders the
|
|
9
|
+
* plain form byte-for-byte instead of leaking ANSI into `doctor --format plain`.
|
|
10
|
+
* Glyphs are plain constants tinted at render time; nothing is baked at load.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import chalk from "chalk";
|
|
14
|
+
import { BIN_NAME } from "../../constants.js";
|
|
15
|
+
import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
|
|
9
16
|
import type { Formatters } from "../../kernel/spec/types.js";
|
|
17
|
+
import { BAND_LABELS } from "../shared/bands.js";
|
|
10
18
|
import type { CheckResult, CheckStatus, DoctorData } from "./types.js";
|
|
11
19
|
|
|
12
|
-
/**
|
|
13
|
-
const
|
|
14
|
-
pass:
|
|
15
|
-
fail:
|
|
16
|
-
skip:
|
|
20
|
+
/** Uncolored status glyphs — tinted at render time (never baked at module load). */
|
|
21
|
+
const STATUS_GLYPHS: Record<CheckStatus, string> = {
|
|
22
|
+
pass: "✓",
|
|
23
|
+
fail: "✗",
|
|
24
|
+
skip: "○",
|
|
17
25
|
};
|
|
18
26
|
|
|
19
|
-
const SUB_BULLET =
|
|
20
|
-
const FIX_ARROW =
|
|
27
|
+
const SUB_BULLET = "·";
|
|
28
|
+
const FIX_ARROW = "↳";
|
|
21
29
|
const INDENT = " "; // aligns sub-lines under the check name
|
|
22
30
|
|
|
31
|
+
/** The TTY styler plus `red` — the one tint {@link RenderStyle} does not carry. */
|
|
32
|
+
interface DoctorStyle extends RenderStyle {
|
|
33
|
+
red(text: string): string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the doctor styler for this process's stdout: the shared TTY seam, plus a
|
|
38
|
+
* `red` gated on the SAME decision, so a fail tint appears only on a color TTY.
|
|
39
|
+
*
|
|
40
|
+
* @returns A colorizing styler on a color-capable TTY, else the identity styler.
|
|
41
|
+
* @note Impure — {@link defaultStyle} reads `process.stdout.isTTY` + chalk level.
|
|
42
|
+
*/
|
|
43
|
+
function doctorStyle(): DoctorStyle {
|
|
44
|
+
const style = defaultStyle();
|
|
45
|
+
return {
|
|
46
|
+
...style,
|
|
47
|
+
red: style.enabled ? (text) => chalk.red(text) : (text) => text,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Tint a status glyph by meaning — green pass, red fail, yellow skip. */
|
|
52
|
+
function paintGlyph(status: CheckStatus, style: DoctorStyle): string {
|
|
53
|
+
const glyph = STATUS_GLYPHS[status];
|
|
54
|
+
if (status === "pass") return style.green(glyph);
|
|
55
|
+
if (status === "fail") return style.red(glyph);
|
|
56
|
+
return style.yellow(glyph);
|
|
57
|
+
}
|
|
58
|
+
|
|
23
59
|
/**
|
|
24
60
|
* Render one check as terminal lines: a headline row (icon, aligned name,
|
|
25
61
|
* detail), an optional indented breakdown of sub-items, and — for failures — an
|
|
26
62
|
* inline remedial instruction.
|
|
27
63
|
*/
|
|
28
|
-
function formatCheckPlain(
|
|
64
|
+
function formatCheckPlain(
|
|
65
|
+
check: CheckResult,
|
|
66
|
+
nameWidth: number,
|
|
67
|
+
style: DoctorStyle,
|
|
68
|
+
): string[] {
|
|
29
69
|
const name = check.name.padEnd(nameWidth);
|
|
30
|
-
const label = check.status === "fail" ?
|
|
70
|
+
const label = check.status === "fail" ? style.red(name) : style.bold(name);
|
|
31
71
|
const lines = [
|
|
32
|
-
` ${
|
|
72
|
+
` ${paintGlyph(check.status, style)} ${label} ${style.dim(check.detail)}`,
|
|
33
73
|
];
|
|
34
74
|
|
|
35
75
|
if (check.items && check.items.length > 0) {
|
|
@@ -38,55 +78,86 @@ function formatCheckPlain(check: CheckResult, nameWidth: number): string[] {
|
|
|
38
78
|
for (const item of check.items) {
|
|
39
79
|
const icon = anyStatus
|
|
40
80
|
? item.status
|
|
41
|
-
? `${
|
|
81
|
+
? `${paintGlyph(item.status, style)} `
|
|
42
82
|
: " "
|
|
43
83
|
: "";
|
|
44
84
|
const itemLabel = item.detail ? item.label.padEnd(itemWidth) : item.label;
|
|
45
|
-
const detail = item.detail ? ` ${
|
|
46
|
-
lines.push(
|
|
85
|
+
const detail = item.detail ? ` ${style.dim(item.detail)}` : "";
|
|
86
|
+
lines.push(
|
|
87
|
+
`${INDENT}${style.dim(SUB_BULLET)} ${icon}${itemLabel}${detail}`,
|
|
88
|
+
);
|
|
47
89
|
}
|
|
48
90
|
}
|
|
49
91
|
|
|
50
92
|
if (check.status === "fail" && check.remedy) {
|
|
51
|
-
lines.push(
|
|
93
|
+
lines.push(
|
|
94
|
+
`${INDENT}${style.cyan(FIX_ARROW)} ${style.cyan("fix:")} ${check.remedy}`,
|
|
95
|
+
);
|
|
52
96
|
}
|
|
53
97
|
|
|
54
98
|
return lines;
|
|
55
99
|
}
|
|
56
100
|
|
|
57
101
|
/** Render the pass/fail/skip tally, coloring non-zero fail/skip. */
|
|
58
|
-
function formatSummary(data: DoctorData): string {
|
|
59
|
-
const parts = [
|
|
102
|
+
function formatSummary(data: DoctorData, style: DoctorStyle): string {
|
|
103
|
+
const parts = [style.green(`${data.passed} passed`)];
|
|
60
104
|
parts.push(
|
|
61
105
|
data.failed > 0
|
|
62
|
-
?
|
|
63
|
-
:
|
|
106
|
+
? style.red(`${data.failed} failed`)
|
|
107
|
+
: style.dim(`${data.failed} failed`),
|
|
64
108
|
);
|
|
65
109
|
parts.push(
|
|
66
110
|
data.skipped > 0
|
|
67
|
-
?
|
|
68
|
-
:
|
|
111
|
+
? style.yellow(`${data.skipped} skipped`)
|
|
112
|
+
: style.dim(`${data.skipped} skipped`),
|
|
69
113
|
);
|
|
70
|
-
return ` ${parts.join(
|
|
114
|
+
return ` ${parts.join(style.dim(" · "))}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Partition checks into ordered bands: environment (no band), then the global
|
|
119
|
+
* and project bands. Declaration order is preserved within each band, so the
|
|
120
|
+
* report stays deterministic.
|
|
121
|
+
*/
|
|
122
|
+
function partitionByBand(checks: readonly CheckResult[]): {
|
|
123
|
+
environment: CheckResult[];
|
|
124
|
+
machine: CheckResult[];
|
|
125
|
+
project: CheckResult[];
|
|
126
|
+
} {
|
|
127
|
+
return {
|
|
128
|
+
environment: checks.filter((c) => c.band === undefined),
|
|
129
|
+
machine: checks.filter((c) => c.band === "global"),
|
|
130
|
+
project: checks.filter((c) => c.band === "project"),
|
|
131
|
+
};
|
|
71
132
|
}
|
|
72
133
|
|
|
73
134
|
export const doctorFormatters: Formatters<DoctorData> = {
|
|
74
135
|
plain(data) {
|
|
136
|
+
const style = doctorStyle();
|
|
75
137
|
const nameWidth = Math.max(...data.checks.map((c) => c.name.length), 0);
|
|
76
|
-
const lines: string[] = [
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
138
|
+
const lines: string[] = [style.bold(`${BIN_NAME} doctor`), ""];
|
|
139
|
+
const { environment, machine, project } = partitionByBand(data.checks);
|
|
140
|
+
const section = (heading: string, checks: CheckResult[]): void => {
|
|
141
|
+
if (checks.length === 0) return;
|
|
142
|
+
if (heading) lines.push(style.bold(heading));
|
|
143
|
+
for (const check of checks)
|
|
144
|
+
lines.push(...formatCheckPlain(check, nameWidth, style));
|
|
145
|
+
lines.push("");
|
|
146
|
+
};
|
|
147
|
+
// Environment checks lead (no header); the global then project bands are the
|
|
148
|
+
// two banded sections before the tally.
|
|
149
|
+
section("", environment);
|
|
150
|
+
section(BAND_LABELS.global, machine);
|
|
151
|
+
section(BAND_LABELS.project, project);
|
|
152
|
+
lines.push(formatSummary(data, style));
|
|
81
153
|
return lines.join("\n");
|
|
82
154
|
},
|
|
83
155
|
|
|
84
156
|
llm(data) {
|
|
85
|
-
const
|
|
86
|
-
for (const check of data.checks) {
|
|
157
|
+
const renderCheck = (check: CheckResult): string[] => {
|
|
87
158
|
const icon =
|
|
88
159
|
check.status === "pass" ? "✓" : check.status === "fail" ? "✗" : "○";
|
|
89
|
-
|
|
160
|
+
const out = [`- ${icon} **${check.name}**: ${check.detail}`];
|
|
90
161
|
for (const item of check.items ?? []) {
|
|
91
162
|
const itemIcon =
|
|
92
163
|
item.status === "pass"
|
|
@@ -97,12 +168,24 @@ export const doctorFormatters: Formatters<DoctorData> = {
|
|
|
97
168
|
? "○ "
|
|
98
169
|
: "";
|
|
99
170
|
const detail = item.detail ? `: ${item.detail}` : "";
|
|
100
|
-
|
|
171
|
+
out.push(` - ${itemIcon}${item.label}${detail}`);
|
|
101
172
|
}
|
|
102
173
|
if (check.status === "fail" && check.remedy) {
|
|
103
|
-
|
|
174
|
+
out.push(` - _fix:_ \`${check.remedy}\``);
|
|
104
175
|
}
|
|
105
|
-
|
|
176
|
+
return out;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const lines: string[] = ["## Doctor", ""];
|
|
180
|
+
const { environment, machine, project } = partitionByBand(data.checks);
|
|
181
|
+
const section = (heading: string, checks: CheckResult[]): void => {
|
|
182
|
+
if (checks.length === 0) return;
|
|
183
|
+
if (heading) lines.push(`### ${heading}`, "");
|
|
184
|
+
for (const check of checks) lines.push(...renderCheck(check));
|
|
185
|
+
};
|
|
186
|
+
section("", environment);
|
|
187
|
+
section(BAND_LABELS.global, machine);
|
|
188
|
+
section(BAND_LABELS.project, project);
|
|
106
189
|
lines.push(
|
|
107
190
|
"",
|
|
108
191
|
`_${data.passed} passed, ${data.failed} failed, ${data.skipped} skipped_`,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the MCP read-only envelope.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "../../testing/fixtures/graph/canonical.js";
|
|
29
29
|
import { bootFixtureRuntime } from "../../testing/helpers/fixtureGraph.js";
|
|
30
30
|
import { projectMcp } from "../../testing/helpers/projectMcp.js";
|
|
31
|
+
import { checkMcpConfigured } from "./checks/checkMcpConfigured.js";
|
|
31
32
|
import { doctorModule } from "./index.js";
|
|
32
33
|
import { runChecks } from "./runChecks.js";
|
|
33
34
|
import type { DoctorData } from "./types.js";
|
|
@@ -50,17 +51,24 @@ const tmp = (prefix: string): string => {
|
|
|
50
51
|
|
|
51
52
|
let prevHome: string | undefined;
|
|
52
53
|
let prevXdg: string | undefined;
|
|
54
|
+
let prevPath: string | undefined;
|
|
53
55
|
beforeEach(() => {
|
|
54
56
|
prevHome = process.env.HOME;
|
|
55
57
|
prevXdg = process.env.XDG_CONFIG_HOME;
|
|
58
|
+
prevPath = process.env.PATH;
|
|
56
59
|
// Empty HOME/XDG so `~`-based harness signals, rc files, and the global config
|
|
57
60
|
// are all absent — the harness/config/completion checks become deterministic.
|
|
58
61
|
process.env.HOME = tmp("pragma-doctor-home-");
|
|
59
62
|
process.env.XDG_CONFIG_HOME = tmp("pragma-doctor-xdg-");
|
|
63
|
+
// Point PATH at an empty dir so no harness `process` signal fires off the
|
|
64
|
+
// ambient PATH (e.g. a `claude`/`codex` binary on the CI/dev host) — keeping
|
|
65
|
+
// "no harnesses detected" deterministic regardless of what is installed.
|
|
66
|
+
process.env.PATH = tmp("pragma-doctor-path-");
|
|
60
67
|
});
|
|
61
68
|
afterEach(() => {
|
|
62
69
|
process.env.HOME = prevHome;
|
|
63
70
|
process.env.XDG_CONFIG_HOME = prevXdg;
|
|
71
|
+
process.env.PATH = prevPath;
|
|
64
72
|
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
|
|
65
73
|
roots.length = 0;
|
|
66
74
|
});
|
|
@@ -71,10 +79,9 @@ const defaultLayers: ConfigLayers = {
|
|
|
71
79
|
tier: "default",
|
|
72
80
|
channel: "default",
|
|
73
81
|
detail: "default",
|
|
74
|
-
|
|
82
|
+
packs: "default",
|
|
75
83
|
stories: "default",
|
|
76
84
|
prefixes: "default",
|
|
77
|
-
prompts: "default",
|
|
78
85
|
},
|
|
79
86
|
global: { path: "", exists: false },
|
|
80
87
|
project: { exists: false },
|
|
@@ -114,12 +121,12 @@ describe("doctor — shape & spread", () => {
|
|
|
114
121
|
}
|
|
115
122
|
// Deterministic under the isolated env.
|
|
116
123
|
expect(byName(data, "Node version")?.status).toBe("pass");
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
const pkgRefs = byName(data, "
|
|
120
|
-
expect(pkgRefs?.status).toBe("
|
|
121
|
-
expect(pkgRefs?.
|
|
122
|
-
expect(pkgRefs?.
|
|
124
|
+
// Nothing built, but the packs are the distribution's own — so the embedded
|
|
125
|
+
// snapshot answers reads and the check passes, naming what it is reading.
|
|
126
|
+
const pkgRefs = byName(data, "pack refs");
|
|
127
|
+
expect(pkgRefs?.status).toBe("pass");
|
|
128
|
+
expect(pkgRefs?.detail).toContain("embedded snapshot @ ");
|
|
129
|
+
expect(pkgRefs?.remedy).toBeUndefined();
|
|
123
130
|
// No harnesses in an empty HOME/cwd — attributable fail + skips.
|
|
124
131
|
expect(byName(data, "MCP configured")?.status).toBe("fail");
|
|
125
132
|
expect(byName(data, "Skills symlinked")?.status).toBe("skip");
|
|
@@ -129,6 +136,36 @@ describe("doctor — shape & spread", () => {
|
|
|
129
136
|
});
|
|
130
137
|
});
|
|
131
138
|
|
|
139
|
+
describe("doctor — the pack-refs check", () => {
|
|
140
|
+
it("a project with its OWN packs and nothing built is an attributable fail", async () => {
|
|
141
|
+
// The install that must never read healthy: `origins.packs` is "project",
|
|
142
|
+
// so the embedded snapshot is a DIFFERENT graph and every read throws
|
|
143
|
+
// STORE_UNAVAILABLE. Doctor has to say so and name the fix, rather than
|
|
144
|
+
// pass by listing packs it is not actually reading from.
|
|
145
|
+
const cwd = tmp("pragma-unbuilt-");
|
|
146
|
+
writeFileSync(
|
|
147
|
+
join(cwd, "pragma.config.ts"),
|
|
148
|
+
'export default { packs: [{ name: "unbuilt", source: "file:///pragma-never-built" }] };\n',
|
|
149
|
+
);
|
|
150
|
+
const data = await runChecks(bootRuntime(FLAGS, cwd));
|
|
151
|
+
const pkgRefs = byName(data, "pack refs");
|
|
152
|
+
expect(pkgRefs?.status).toBe("fail");
|
|
153
|
+
expect(pkgRefs?.detail).toContain("the store has not been built");
|
|
154
|
+
expect(pkgRefs?.remedy).toBe("pragma sources update");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("a project pointed at its own built pack passes, naming that pack", async () => {
|
|
158
|
+
const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL });
|
|
159
|
+
const data = await runChecks(fixture.runtime);
|
|
160
|
+
const pkgRefs = byName(data, "pack refs");
|
|
161
|
+
expect(pkgRefs?.status).toBe("pass");
|
|
162
|
+
// The fixture pack's own provenance label, not the embedded snapshot's.
|
|
163
|
+
expect(pkgRefs?.detail).toContain("fixture");
|
|
164
|
+
expect(pkgRefs?.detail).not.toContain("embedded snapshot");
|
|
165
|
+
await fixture.dispose();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
132
169
|
describe("doctor — the store check", () => {
|
|
133
170
|
it("a store that fails to boot is an attributable fail, not a crash", async () => {
|
|
134
171
|
const data = await runChecks(throwingStoreRuntime(tmp("pragma-proj-")));
|
|
@@ -177,3 +214,25 @@ describe("doctor — dispatch & MCP", () => {
|
|
|
177
214
|
expect((envelope.data as DoctorData).checks).toHaveLength(9);
|
|
178
215
|
});
|
|
179
216
|
});
|
|
217
|
+
|
|
218
|
+
describe("doctor — MCP checks band by detected harness scope, not check name", () => {
|
|
219
|
+
it("bands a configured GLOBAL-scope harness (Windsurf) as global", async () => {
|
|
220
|
+
// Windsurf is a global-only harness whose MCP config lives in the home band.
|
|
221
|
+
// The old static map tagged every "MCP configured" result PROJECT; the fix
|
|
222
|
+
// derives the band from the harness's real scope, so this reports `global`.
|
|
223
|
+
const cwd = tmp("pragma-doctor-proj-");
|
|
224
|
+
mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // ⇒ Windsurf detected
|
|
225
|
+
const home = process.env.HOME ?? "";
|
|
226
|
+
const wsDir = join(home, ".codeium", "windsurf");
|
|
227
|
+
mkdirSync(wsDir, { recursive: true });
|
|
228
|
+
writeFileSync(
|
|
229
|
+
join(wsDir, "mcp_config.json"),
|
|
230
|
+
JSON.stringify({ mcpServers: { pragma: { command: "pragma" } } }),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const check = await checkMcpConfigured(cwd);
|
|
234
|
+
expect(check.status).toBe("pass");
|
|
235
|
+
expect(check.detail).toContain("Windsurf");
|
|
236
|
+
expect(check.band).toBe("global"); // NOT the old static "project"
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `doctor-exit-zero-with-failures`.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { BIN_NAME } from "../../constants.js";
|
|
15
16
|
import { asVerb } from "../../kernel/spec/asVerb.js";
|
|
16
17
|
import type { CapabilityModule, VerbSpec } from "../../kernel/spec/types.js";
|
|
17
18
|
import { doctorFormatters } from "./doctor.render.js";
|
|
@@ -24,8 +25,11 @@ const doctorVerb: VerbSpec<Record<string, unknown>, DoctorData> = {
|
|
|
24
25
|
params: [],
|
|
25
26
|
output: { formatters: doctorFormatters },
|
|
26
27
|
examples: [
|
|
27
|
-
{ cmd:
|
|
28
|
-
{
|
|
28
|
+
{ cmd: `${BIN_NAME} doctor` },
|
|
29
|
+
{
|
|
30
|
+
cmd: `${BIN_NAME} doctor --format json`,
|
|
31
|
+
note: "machine-readable checks",
|
|
32
|
+
},
|
|
29
33
|
],
|
|
30
34
|
capability: {
|
|
31
35
|
needsStore: false,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* neither the checks nor `@canonical/harnesses` land on the fast path.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { BIN_NAME } from "../../constants.js";
|
|
11
12
|
import type { PragmaRuntime } from "../../kernel/runtime/types.js";
|
|
12
13
|
import { checkConfigFile } from "./checks/checkConfigFile.js";
|
|
13
14
|
import { checkKeStore } from "./checks/checkKeStore.js";
|
|
@@ -18,7 +19,31 @@ import { checkPackageRefs } from "./checks/checkPackageRefs.js";
|
|
|
18
19
|
import { checkPragmaVersion } from "./checks/checkPragmaVersion.js";
|
|
19
20
|
import { checkShellCompletions } from "./checks/checkShellCompletions.js";
|
|
20
21
|
import { checkSkillsSymlinked } from "./checks/checkSkillsSymlinked.js";
|
|
21
|
-
import type { CheckResult, DoctorData } from "./types.js";
|
|
22
|
+
import type { CheckResult, DoctorData, ScopeBand } from "./types.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The band a check statically concerns, by name — for checks whose band is
|
|
26
|
+
* fixed by construction: skills always symlink into the per-repo project dir
|
|
27
|
+
* (`skillsPath` is project-rooted), and completions install into the user/home
|
|
28
|
+
* shell dir. The MCP checks are NOT here: they detect harnesses across BOTH
|
|
29
|
+
* bands, so each derives its own band from what it found (see `deriveBand`) and
|
|
30
|
+
* sets it on its own result — {@link attachBand} preserves that.
|
|
31
|
+
*/
|
|
32
|
+
const CHECK_BANDS: Record<string, ScopeBand> = {
|
|
33
|
+
"Skills symlinked": "project",
|
|
34
|
+
"Shell completions": "global",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Attach a band tag to a check result. A check that already set its own band
|
|
39
|
+
* (the harness-derived MCP checks) keeps it; otherwise the static
|
|
40
|
+
* {@link CHECK_BANDS} map fills one in by name, if defined.
|
|
41
|
+
*/
|
|
42
|
+
function attachBand(result: CheckResult): CheckResult {
|
|
43
|
+
if (result.band !== undefined) return result;
|
|
44
|
+
const band = CHECK_BANDS[result.name];
|
|
45
|
+
return band ? { ...result, band } : result;
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
/**
|
|
24
49
|
* Each check paired with the display name used if it rejects. The fallback name
|
|
@@ -31,9 +56,9 @@ function buildChecks(
|
|
|
31
56
|
): readonly [string, Promise<CheckResult>][] {
|
|
32
57
|
return [
|
|
33
58
|
["Node version", checkNodeVersion()],
|
|
34
|
-
[
|
|
35
|
-
[
|
|
36
|
-
["
|
|
59
|
+
[`${BIN_NAME} version`, checkPragmaVersion(rt)],
|
|
60
|
+
[`${BIN_NAME} config`, checkConfigFile(rt)],
|
|
61
|
+
["pack refs", checkPackageRefs(rt)],
|
|
37
62
|
["ke store", checkKeStore(rt)],
|
|
38
63
|
["Shell completions", checkShellCompletions(rt.cwd)],
|
|
39
64
|
["MCP configured", checkMcpConfigured(rt.cwd)],
|
|
@@ -53,16 +78,15 @@ export async function runChecks(rt: PragmaRuntime): Promise<DoctorData> {
|
|
|
53
78
|
const checks: CheckResult[] = await Promise.all(
|
|
54
79
|
buildChecks(rt).map(async ([name, promise]): Promise<CheckResult> => {
|
|
55
80
|
try {
|
|
56
|
-
return await promise;
|
|
81
|
+
return attachBand(await promise);
|
|
57
82
|
} catch (error) {
|
|
58
83
|
const reason = error instanceof Error ? error.message : String(error);
|
|
59
|
-
return {
|
|
84
|
+
return attachBand({
|
|
60
85
|
name,
|
|
61
86
|
status: "fail",
|
|
62
87
|
detail: `Check threw an unexpected error: ${reason}`,
|
|
63
|
-
remedy:
|
|
64
|
-
|
|
65
|
-
};
|
|
88
|
+
remedy: `Re-run \`${BIN_NAME} doctor\`; if it persists, report this as a bug.`,
|
|
89
|
+
});
|
|
66
90
|
}
|
|
67
91
|
}),
|
|
68
92
|
);
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* Data shapes for `pragma doctor` (ported verbatim from the old shell).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* One of the two config bands a check can concern. Mirrors the harnesses
|
|
7
|
+
* `ScopeBand` structurally but is redeclared here so this statically-reachable
|
|
8
|
+
* type module never pulls the harnesses runtime into the fast-path module graph.
|
|
9
|
+
*/
|
|
10
|
+
export type ScopeBand = "project" | "global";
|
|
11
|
+
|
|
5
12
|
/** Status of a doctor check or one of its sub-items. */
|
|
6
13
|
export type CheckStatus = "pass" | "fail" | "skip";
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
16
|
* A structured sub-item under a check — e.g. one resolved package under
|
|
10
|
-
* `
|
|
17
|
+
* `pack refs`, or one unresolvable server under `MCP commands`. Lets the
|
|
11
18
|
* formatter render an indented, aligned breakdown instead of cramming
|
|
12
19
|
* everything into a single `detail` string.
|
|
13
20
|
*/
|
|
@@ -30,6 +37,14 @@ export interface CheckResult {
|
|
|
30
37
|
readonly items?: readonly CheckItem[];
|
|
31
38
|
/** Remedial instruction shown inline under a failing check. */
|
|
32
39
|
readonly remedy?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Which config band the check concerns, if any: `global` for the user/home
|
|
42
|
+
* level (shell completions), `project` for per-repo config (skills). The MCP
|
|
43
|
+
* checks derive their band from the harnesses they found. The renderer groups
|
|
44
|
+
* banded checks into Global/Project sections; environment checks (Node,
|
|
45
|
+
* versions, store) carry no band.
|
|
46
|
+
*/
|
|
47
|
+
readonly band?: ScopeBand;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
/** Aggregated results from all doctor checks. */
|