@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
|
@@ -523,11 +523,12 @@ export const createVerbs: Record<
|
|
|
523
523
|
),
|
|
524
524
|
application: createVerb(
|
|
525
525
|
"application",
|
|
526
|
-
"Scaffold a full React application with
|
|
526
|
+
"Scaffold a full React application with routing, and either server-side rendering or a client-only SPA.",
|
|
527
527
|
applicationParams,
|
|
528
528
|
[
|
|
529
529
|
{ cmd: `${BIN_NAME} create application react my-app` },
|
|
530
530
|
{ cmd: `${BIN_NAME} create application react my-app --relay` },
|
|
531
|
+
{ cmd: `${BIN_NAME} create application react my-app --rendering spa` },
|
|
531
532
|
],
|
|
532
533
|
),
|
|
533
534
|
};
|
|
@@ -655,7 +655,10 @@ describe("the mounted create grammar (subprocess)", () => {
|
|
|
655
655
|
"--type": "library",
|
|
656
656
|
"--description": "A replied library.",
|
|
657
657
|
},
|
|
658
|
-
"application/react": {
|
|
658
|
+
"application/react": {
|
|
659
|
+
"--app-path": "replied-app",
|
|
660
|
+
"--rendering": "spa",
|
|
661
|
+
},
|
|
659
662
|
};
|
|
660
663
|
for (const [commandPath, surface] of Object.entries(CREATE_SURFACE)) {
|
|
661
664
|
it(`${commandPath}: the refusal's own instruction WORKS — its Missing tokens supplied back (+ --dry-run) preview, exit 0`, () => {
|
|
@@ -768,7 +771,7 @@ describe("the refusal cell through dispatchPrepared (in-process)", () => {
|
|
|
768
771
|
describe("the mount's mode resolution — TTY driven through the seam", () => {
|
|
769
772
|
// No suite can hand a subprocess a real TTY, so the §L rows that depend on
|
|
770
773
|
// isTTY: true are pinned HERE, through the exported resolver the action
|
|
771
|
-
// itself calls (with
|
|
774
|
+
// itself calls (with canPrompt()). The OLD decision block routed TTY
|
|
772
775
|
// dry-run/undo into Ink; these rows keep that from silently returning.
|
|
773
776
|
const prompts = CREATE_SURFACE["component/react"]?.prompts ?? [];
|
|
774
777
|
const flags = (dryRun: boolean, undo: boolean, yes: boolean) => ({
|
|
@@ -17,7 +17,7 @@ export const CREATE_SURFACE: Readonly<Record<string, SurfaceCommand>> = {
|
|
|
17
17
|
"application",
|
|
18
18
|
"react"
|
|
19
19
|
],
|
|
20
|
-
"description": "Scaffold a complete React application with
|
|
20
|
+
"description": "Scaffold a complete React application with routing, and either server-side rendering or a client-only SPA",
|
|
21
21
|
"prompts": [
|
|
22
22
|
{
|
|
23
23
|
"name": "appPath",
|
|
@@ -41,6 +41,23 @@ export const CREATE_SURFACE: Readonly<Record<string, SurfaceCommand>> = {
|
|
|
41
41
|
"default": false,
|
|
42
42
|
"group": "Application"
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
"name": "rendering",
|
|
46
|
+
"type": "select",
|
|
47
|
+
"message": "Rendering — ssr keeps the server layer (SSR servers and sitemap), spa is client-only",
|
|
48
|
+
"default": "ssr",
|
|
49
|
+
"choices": [
|
|
50
|
+
{
|
|
51
|
+
"label": "ssr - server-side rendering (Express + Bun servers, sitemap)",
|
|
52
|
+
"value": "ssr"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"label": "spa - client-only single-page app (no server layer)",
|
|
56
|
+
"value": "spa"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"group": "Application"
|
|
60
|
+
},
|
|
44
61
|
{
|
|
45
62
|
"name": "relay",
|
|
46
63
|
"type": "confirm",
|
|
@@ -278,6 +295,11 @@ export const CREATE_CLI_SYNTAX: Readonly<
|
|
|
278
295
|
"takesValue": false,
|
|
279
296
|
"kebabName": "intl"
|
|
280
297
|
},
|
|
298
|
+
"rendering": {
|
|
299
|
+
"flag": "--rendering",
|
|
300
|
+
"takesValue": true,
|
|
301
|
+
"kebabName": "rendering"
|
|
302
|
+
},
|
|
281
303
|
"relay": {
|
|
282
304
|
"flag": "--relay",
|
|
283
305
|
"takesValue": false,
|
|
@@ -258,9 +258,16 @@ describe("cross-CLI conformance matrix (PROTECTED)", () => {
|
|
|
258
258
|
// here, which is the point).
|
|
259
259
|
it("every declared generator binding path is driven by a conformance fixture", () => {
|
|
260
260
|
expect(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
// Deduplicated: the assertion is that every declared path is COVERED, and
|
|
262
|
+
// one path may carry several fixtures — application/react is driven by
|
|
263
|
+
// both the SSR arm and the SPA arm.
|
|
264
|
+
[
|
|
265
|
+
...new Set(
|
|
266
|
+
CONFORMANCE_FIXTURES.map((fixture) =>
|
|
267
|
+
commandPathOf(fixture.generator),
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
].sort(),
|
|
264
271
|
).toEqual(
|
|
265
272
|
Object.values(CREATE_GENERATORS)
|
|
266
273
|
.flatMap((binding) => binding.paths as readonly string[])
|
|
@@ -47,9 +47,9 @@ import type { Task } from "@canonical/task";
|
|
|
47
47
|
import { type Command, CommanderError } from "commander";
|
|
48
48
|
import { PragmaError } from "../../kernel/error/index.js";
|
|
49
49
|
import { renderErrorForFormat } from "../../kernel/error/renderError.js";
|
|
50
|
+
import { canPrompt } from "../../kernel/interactivity.js";
|
|
50
51
|
import { MUTATION_FLAG_DOCS } from "../../kernel/project/cli/constants.js";
|
|
51
52
|
import {
|
|
52
|
-
cliIsTTY,
|
|
53
53
|
dispatchPrepared,
|
|
54
54
|
type MutationFlags,
|
|
55
55
|
} from "../../kernel/project/cli/dispatch.js";
|
|
@@ -177,7 +177,7 @@ export function explicitLeafAnswers(
|
|
|
177
177
|
* The mount's mode resolution — `decideInteraction` over the leaf's five
|
|
178
178
|
* inputs, with the TTY fact INJECTED so the resolution is testable with
|
|
179
179
|
* `tty: true` (no suite can drive a real TTY through a subprocess). The
|
|
180
|
-
* action calls it with {@link
|
|
180
|
+
* action calls it with {@link canPrompt} — the same exported H3 gate the
|
|
181
181
|
* kernel's interaction context reads — so the mount and `runCreate` can
|
|
182
182
|
* never disagree about interactivity.
|
|
183
183
|
*/
|
|
@@ -280,7 +280,7 @@ export function mountCreateTree(parent: Command, host: CliMountHost): void {
|
|
|
280
280
|
generator.prompts,
|
|
281
281
|
explicit,
|
|
282
282
|
mutation,
|
|
283
|
-
|
|
283
|
+
canPrompt(),
|
|
284
284
|
);
|
|
285
285
|
if (mode === "refuse") {
|
|
286
286
|
writeRefusal(
|
|
@@ -30,6 +30,10 @@ if (!standardPack) {
|
|
|
30
30
|
const CS = "http://pragma.canonical.com/codestandards#";
|
|
31
31
|
const PREFIXES = {
|
|
32
32
|
cs: CS,
|
|
33
|
+
// The store the fixture builds is queried with the story queries verbatim,
|
|
34
|
+
// and the category roll-up names `skos:broader` — a CORE prefix on the real
|
|
35
|
+
// build path (`buildPackPrefixes`), which this in-memory helper does not use.
|
|
36
|
+
skos: "http://www.w3.org/2004/02/skos/core#",
|
|
33
37
|
owl: "http://www.w3.org/2002/07/owl#",
|
|
34
38
|
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
35
39
|
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Covers the two things `checkNodeVersion` owns beyond the range comparison:
|
|
3
|
+
* the shape of the CheckResult, and that the failure path quotes the declared
|
|
4
|
+
* range rather than a number of its own. The range logic itself is pinned in
|
|
5
|
+
* `isSupportedNodeVersion.test.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { SUPPORTED_NODE_RANGE } from "../../../constants.js";
|
|
10
|
+
import { checkNodeVersion } from "./checkNodeVersion.js";
|
|
11
|
+
|
|
12
|
+
/** Swap `process.versions.node`, which is read-only in the normal way. */
|
|
13
|
+
function withNodeVersion(version: string) {
|
|
14
|
+
vi.spyOn(process, "versions", "get").mockReturnValue({
|
|
15
|
+
...process.versions,
|
|
16
|
+
node: version,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("checkNodeVersion — reports against the declared range", () => {
|
|
25
|
+
it("passes on a supported version and reports it", async () => {
|
|
26
|
+
withNodeVersion("24.18.1");
|
|
27
|
+
const result = await checkNodeVersion();
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
name: "Node version",
|
|
30
|
+
status: "pass",
|
|
31
|
+
detail: "v24.18.1",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("fails on an unsupported version and quotes the declared range", async () => {
|
|
36
|
+
withNodeVersion("20.19.0");
|
|
37
|
+
const result = await checkNodeVersion();
|
|
38
|
+
expect(result.status).toBe("fail");
|
|
39
|
+
expect(result.detail).toContain("v20.19.0");
|
|
40
|
+
// The range must come from the manifest, never from a literal in the check.
|
|
41
|
+
expect(result.detail).toContain(SUPPORTED_NODE_RANGE);
|
|
42
|
+
expect(result.remedy).toContain(SUPPORTED_NODE_RANGE);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("fails inside the 23.0-23.5 window the range excludes", async () => {
|
|
46
|
+
withNodeVersion("23.2.0");
|
|
47
|
+
expect((await checkNodeVersion()).status).toBe("fail");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -1,25 +1,29 @@
|
|
|
1
|
+
import { SUPPORTED_NODE_RANGE } from "../../../constants.js";
|
|
1
2
|
import type { CheckResult } from "../types.js";
|
|
2
|
-
|
|
3
|
-
const MIN_NODE_MAJOR = 20;
|
|
3
|
+
import { isSupportedNodeVersion } from "./isSupportedNodeVersion.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Check that the Node.js
|
|
6
|
+
* Check that the running Node.js version satisfies this package's declared
|
|
7
|
+
* `engines.node` range.
|
|
8
|
+
*
|
|
9
|
+
* The floor is a property of the manifest, not of this file: a check that
|
|
10
|
+
* hardcodes its own number can disagree with the range the package publishes,
|
|
11
|
+
* and a consumer then passes a check the install contract would have failed.
|
|
7
12
|
*
|
|
8
13
|
* @returns A CheckResult with the current version and pass/fail status.
|
|
9
14
|
* @note Impure — reads `process.versions`.
|
|
10
15
|
*/
|
|
11
16
|
export async function checkNodeVersion(): Promise<CheckResult> {
|
|
12
17
|
const version = process.versions.node;
|
|
13
|
-
const major = Number.parseInt(version.split(".")[0] ?? "0", 10);
|
|
14
18
|
|
|
15
|
-
if (
|
|
19
|
+
if (isSupportedNodeVersion(version)) {
|
|
16
20
|
return { name: "Node version", status: "pass", detail: `v${version}` };
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
return {
|
|
20
24
|
name: "Node version",
|
|
21
25
|
status: "fail",
|
|
22
|
-
detail: `v${version} (requires
|
|
23
|
-
remedy: `Install Node.js
|
|
26
|
+
detail: `v${version} (requires ${SUPPORTED_NODE_RANGE})`,
|
|
27
|
+
remedy: `Install Node.js ${SUPPORTED_NODE_RANGE}`,
|
|
24
28
|
};
|
|
25
29
|
}
|
|
@@ -60,6 +60,51 @@ function ignoredSuffix(items: readonly CheckItem[]): string {
|
|
|
60
60
|
return ` · ${items.length} ${items.length === 1 ? "story" : "stories"} ignored`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The answering pack's provenance, one item per pack it was built from.
|
|
65
|
+
*
|
|
66
|
+
* `sourceRef` is a single comma-joined string — `@canonical/design-system@git:
|
|
67
|
+
* d6d8a6c8268cf2bd103e956a2540d6e36bd08d72, @canonical/anatomy-dsl@npm:0.2.2,
|
|
68
|
+
* …` — and printing it whole put four packs and two forty-character SHAs on one
|
|
69
|
+
* line, in a report whose every other multi-part check (`mcp`, `harnesses`)
|
|
70
|
+
* uses the sub-item mechanism. Provenance is the thing a reader is here for
|
|
71
|
+
* when they read this line at all: which revision of which pack answered.
|
|
72
|
+
*
|
|
73
|
+
* The scheme (`git`/`npm`/`self`/`file`) is kept because it says how to move
|
|
74
|
+
* the pin, and a git hash is cut to the seven characters every other tool in
|
|
75
|
+
* this workflow shows. A ref that does not parse is passed through whole rather
|
|
76
|
+
* than dropped — an unreadable provenance is still provenance, and hiding it
|
|
77
|
+
* would be the one failure this check exists to prevent.
|
|
78
|
+
*
|
|
79
|
+
* @param sourceRef - The manifest's comma-joined provenance string.
|
|
80
|
+
* @returns One passing item per pack, in the order the pack records them.
|
|
81
|
+
*/
|
|
82
|
+
function packItems(sourceRef: string): CheckItem[] {
|
|
83
|
+
return sourceRef
|
|
84
|
+
.split(",")
|
|
85
|
+
.map((ref) => ref.trim())
|
|
86
|
+
.filter((ref) => ref !== "")
|
|
87
|
+
.map((ref) => {
|
|
88
|
+
const parsed = /^(.+)@(git|npm|self|file|link):(.+)$/.exec(ref);
|
|
89
|
+
if (parsed === null) return { label: ref, status: "pass" as const };
|
|
90
|
+
const [, pack, scheme, revision] = parsed as unknown as [
|
|
91
|
+
string,
|
|
92
|
+
string,
|
|
93
|
+
string,
|
|
94
|
+
string,
|
|
95
|
+
];
|
|
96
|
+
const short =
|
|
97
|
+
scheme === "git" && /^[0-9a-f]{40}$/.test(revision)
|
|
98
|
+
? revision.slice(0, 7)
|
|
99
|
+
: revision;
|
|
100
|
+
return {
|
|
101
|
+
label: pack,
|
|
102
|
+
detail: `${scheme} ${short}`,
|
|
103
|
+
status: "pass" as const,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
/**
|
|
64
109
|
* Report which pack answers reads, its provenance, and its entity total.
|
|
65
110
|
*
|
|
@@ -89,13 +134,30 @@ export async function checkPackageRefs(
|
|
|
89
134
|
// The pack DOES answer reads, so the check itself passes; the ignored stories
|
|
90
135
|
// are failing sub-items under it. (A `warn` status would ripple through the
|
|
91
136
|
// renderer and the DoctorData counts to buy an icon.)
|
|
92
|
-
const
|
|
93
|
-
const suffix = ignoredSuffix(
|
|
137
|
+
const ignored = await ignoredStoryItems(decision);
|
|
138
|
+
const suffix = ignoredSuffix(ignored);
|
|
139
|
+
const sourceRef =
|
|
140
|
+
decision.kind === "embedded"
|
|
141
|
+
? embeddedManifest().sourceRef
|
|
142
|
+
: (readManifest(decision.dir)?.sourceRef ?? "");
|
|
143
|
+
// Packs first, then anything wrong with them: the composition is the answer to
|
|
144
|
+
// "which revision am I reading", and a failing story is a note about one of
|
|
145
|
+
// the rows above it.
|
|
146
|
+
const packs = packItems(sourceRef);
|
|
147
|
+
const items = [...packs, ...ignored];
|
|
148
|
+
const composition = `${packs.length} ${packs.length === 1 ? "pack" : "packs"}, ${entities} entities${suffix}`;
|
|
149
|
+
|
|
94
150
|
if (decision.kind === "embedded") {
|
|
95
151
|
return {
|
|
96
152
|
name,
|
|
97
153
|
status: "pass",
|
|
98
|
-
|
|
154
|
+
// The update hint stays in the DETAIL rather than moving to `remedy`:
|
|
155
|
+
// the renderer prints a remedy only where there is something to fix or
|
|
156
|
+
// to finish, and a snapshot that is answering reads correctly is
|
|
157
|
+
// neither. "shipped with the CLI" rather than "embedded snapshot" — the
|
|
158
|
+
// reader needs to know where these packs came from, not what the build
|
|
159
|
+
// calls the artefact.
|
|
160
|
+
detail: `shipped with the CLI — ${composition} · run \`${BIN_NAME} sources update\` to build from your own configured packs instead`,
|
|
99
161
|
...(items.length > 0 ? { items } : {}),
|
|
100
162
|
};
|
|
101
163
|
}
|
|
@@ -103,7 +165,10 @@ export async function checkPackageRefs(
|
|
|
103
165
|
return {
|
|
104
166
|
name,
|
|
105
167
|
status: "pass",
|
|
106
|
-
detail:
|
|
168
|
+
detail:
|
|
169
|
+
packs.length > 0
|
|
170
|
+
? `built ${manifest?.createdAt ?? "?"} — ${composition}`
|
|
171
|
+
: `${decision.contentHash.slice(0, 12)} — ${composition}, built ${manifest?.createdAt ?? "?"}`,
|
|
107
172
|
...(items.length > 0 ? { items } : {}),
|
|
108
173
|
};
|
|
109
174
|
}
|
|
@@ -94,10 +94,12 @@ function wireZfunc(): void {
|
|
|
94
94
|
describe("checkShellCompletions — effect test (gate 1)", () => {
|
|
95
95
|
it("passes the resolver gate: `pragma <TAB>` resolves nouns", async () => {
|
|
96
96
|
// No shell → skip, but only AFTER the resolver answered — the detail proves
|
|
97
|
-
// the effect test ran and succeeded.
|
|
97
|
+
// the effect test ran and succeeded. The proof is the same fact worded for
|
|
98
|
+
// a reader ("pragma answers `<TAB>`") rather than as "resolver OK": the
|
|
99
|
+
// gate it pins is unchanged, only the sentence that reports it.
|
|
98
100
|
const result = await check(tmp(), null);
|
|
99
101
|
expect(result.status).toBe("skip");
|
|
100
|
-
expect(result.detail).toMatch(/
|
|
102
|
+
expect(result.detail).toMatch(/answers `<TAB>`/);
|
|
101
103
|
});
|
|
102
104
|
});
|
|
103
105
|
|
|
@@ -137,7 +139,7 @@ describe("checkShellCompletions — install probe (gate 2)", () => {
|
|
|
137
139
|
const result = await check(tmp(), "bash");
|
|
138
140
|
expect(result.status).toBe("pass");
|
|
139
141
|
expect(result.detail).toMatch(
|
|
140
|
-
/bash \(the shell in
|
|
142
|
+
/bash \(the shell you are in\) — installed, up to date, and completing/,
|
|
141
143
|
);
|
|
142
144
|
});
|
|
143
145
|
|
|
@@ -200,7 +202,7 @@ describe("checkShellCompletions — zsh fpath activation (gate 3)", () => {
|
|
|
200
202
|
const result = await check(tmp(), "zsh");
|
|
201
203
|
expect(result.status).toBe("pass");
|
|
202
204
|
expect(result.detail).toMatch(
|
|
203
|
-
/zsh \(the shell in
|
|
205
|
+
/zsh \(the shell you are in\) — installed, up to date, and completing/,
|
|
204
206
|
);
|
|
205
207
|
});
|
|
206
208
|
});
|
|
@@ -126,7 +126,7 @@ export async function checkShellCompletions(
|
|
|
126
126
|
return {
|
|
127
127
|
name: NAME,
|
|
128
128
|
status: "fail",
|
|
129
|
-
detail:
|
|
129
|
+
detail: `${BIN_NAME} cannot answer \`<TAB>\`: ${reason}`,
|
|
130
130
|
remedy: `Report this as a bug — \`${BIN_NAME} __complete\` should never throw.`,
|
|
131
131
|
};
|
|
132
132
|
}
|
|
@@ -134,7 +134,7 @@ export async function checkShellCompletions(
|
|
|
134
134
|
return {
|
|
135
135
|
name: NAME,
|
|
136
136
|
status: "fail",
|
|
137
|
-
detail:
|
|
137
|
+
detail: `${BIN_NAME} answered \`<TAB>\` with nothing to complete`,
|
|
138
138
|
remedy: "Report this as a bug — the noun context is always non-empty.",
|
|
139
139
|
};
|
|
140
140
|
}
|
|
@@ -147,7 +147,7 @@ export async function checkShellCompletions(
|
|
|
147
147
|
return {
|
|
148
148
|
name: NAME,
|
|
149
149
|
status: "skip",
|
|
150
|
-
detail:
|
|
150
|
+
detail: `${BIN_NAME} answers \`<TAB>\`, but the running shell cannot be identified — SHELL names ${d.detection.login}, which is your login shell`,
|
|
151
151
|
remedy: `Run \`${INSTALL_REMEDY}\` from the shell you want completions in.`,
|
|
152
152
|
};
|
|
153
153
|
}
|
|
@@ -155,7 +155,7 @@ export async function checkShellCompletions(
|
|
|
155
155
|
return {
|
|
156
156
|
name: NAME,
|
|
157
157
|
status: "skip",
|
|
158
|
-
detail:
|
|
158
|
+
detail: `${BIN_NAME} answers \`<TAB>\`, but no bash, zsh, or fish was found in this process tree`,
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
// The installed script delegates every name context to the binary. A script
|
|
@@ -174,7 +174,7 @@ export async function checkShellCompletions(
|
|
|
174
174
|
status: installed ? "fail" : "available",
|
|
175
175
|
detail: installed
|
|
176
176
|
? `${shell} script installed, but \`${BIN_NAME}\` is not on PATH — the script cannot run it`
|
|
177
|
-
:
|
|
177
|
+
: `${BIN_NAME} answers \`<TAB>\`, but is not on PATH — an installed script cannot run it`,
|
|
178
178
|
remedy: `Put \`${BIN_NAME}\` on your PATH.`,
|
|
179
179
|
};
|
|
180
180
|
}
|
|
@@ -186,7 +186,7 @@ export async function checkShellCompletions(
|
|
|
186
186
|
return {
|
|
187
187
|
name: NAME,
|
|
188
188
|
status: "available",
|
|
189
|
-
detail:
|
|
189
|
+
detail: `${BIN_NAME} answers \`<TAB>\`; the ${shell} script is not installed`,
|
|
190
190
|
remedy: INSTALL_REMEDY,
|
|
191
191
|
};
|
|
192
192
|
}
|
|
@@ -194,7 +194,7 @@ export async function checkShellCompletions(
|
|
|
194
194
|
return {
|
|
195
195
|
name: NAME,
|
|
196
196
|
status: "fail",
|
|
197
|
-
detail:
|
|
197
|
+
detail: `${BIN_NAME} answers \`<TAB>\`; the ${shell} script at ${path} is out of date`,
|
|
198
198
|
remedy: INSTALL_REMEDY,
|
|
199
199
|
};
|
|
200
200
|
}
|
|
@@ -212,6 +212,6 @@ export async function checkShellCompletions(
|
|
|
212
212
|
return {
|
|
213
213
|
name: NAME,
|
|
214
214
|
status: "pass",
|
|
215
|
-
detail: `${shell} (the shell in
|
|
215
|
+
detail: `${shell} (the shell you are in) — installed, up to date, and completing ${candidates} commands`,
|
|
216
216
|
};
|
|
217
217
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness inventory roll-up — the pure half, driven by fixtures.
|
|
3
|
+
*
|
|
4
|
+
* `doctor.test.ts` covers the wiring (both scopes, `--verbose`, the real
|
|
5
|
+
* registry). What is unit-tested here is the classification itself: the
|
|
6
|
+
* registry supplies the universe so the NEGATIVE is representable, the groups
|
|
7
|
+
* supply the state, and no combination of the two may ever produce a `fail`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import {
|
|
12
|
+
harnessInventory,
|
|
13
|
+
type InventoryGroup,
|
|
14
|
+
type InventoryHarness,
|
|
15
|
+
type InventoryMember,
|
|
16
|
+
type InventoryRow,
|
|
17
|
+
inventoryHealth,
|
|
18
|
+
inventoryItems,
|
|
19
|
+
isDetectedState,
|
|
20
|
+
type WriteState,
|
|
21
|
+
} from "./harnessInventory.js";
|
|
22
|
+
|
|
23
|
+
const REGISTRY: InventoryHarness[] = [
|
|
24
|
+
{ id: "claude-code", name: "Claude Code", inScope: true },
|
|
25
|
+
{ id: "cursor", name: "Cursor", inScope: true },
|
|
26
|
+
{ id: "vscode", name: "VS Code", inScope: false },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** One harness's share of a file — its OWN write's state, never the file's. */
|
|
30
|
+
const named = (name: string, state: WriteState): InventoryMember => ({
|
|
31
|
+
name,
|
|
32
|
+
state,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const roll = (groups: InventoryGroup[]): InventoryRow[] =>
|
|
36
|
+
harnessInventory(REGISTRY, groups, "global");
|
|
37
|
+
|
|
38
|
+
describe("harnessInventory — the roll-up", () => {
|
|
39
|
+
it("classifies each registry harness against the scope's groups", () => {
|
|
40
|
+
const rows = roll([
|
|
41
|
+
{
|
|
42
|
+
path: "~/.claude.json",
|
|
43
|
+
harnesses: [named("Claude Code", "registered")],
|
|
44
|
+
},
|
|
45
|
+
{ path: "~/.cursor/mcp.json", harnesses: [named("Cursor", "absent")] },
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
expect(rows.map((r) => `${r.harnessId}:${r.state}`)).toEqual([
|
|
49
|
+
"claude-code:registered",
|
|
50
|
+
"cursor:detected",
|
|
51
|
+
// Not detected AND has no location in this scope at all — the two are
|
|
52
|
+
// different facts and the row says which one applies.
|
|
53
|
+
"vscode:unscoped",
|
|
54
|
+
]);
|
|
55
|
+
expect(rows[0]?.locations).toEqual(["~/.claude.json"]);
|
|
56
|
+
expect(rows[2]?.locations).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("keeps 'undetected' and 'unscoped' apart", () => {
|
|
60
|
+
// Same absence, two different reasons: Cursor could be here and is not,
|
|
61
|
+
// VS Code could never be here at all.
|
|
62
|
+
const rows = roll([]);
|
|
63
|
+
expect(rows.find((r) => r.harnessId === "cursor")?.state).toBe(
|
|
64
|
+
"undetected",
|
|
65
|
+
);
|
|
66
|
+
expect(rows.find((r) => r.harnessId === "vscode")?.state).toBe("unscoped");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("reports a drifted group as drifted, not as registered", () => {
|
|
70
|
+
const rows = roll([
|
|
71
|
+
{ path: "~/.claude.json", harnesses: [named("Claude Code", "drifted")] },
|
|
72
|
+
]);
|
|
73
|
+
expect(rows[0]?.state).toBe("drifted");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("a harness sharing two files is registered only when BOTH carry the entry", () => {
|
|
77
|
+
const both = harnessInventory(
|
|
78
|
+
REGISTRY,
|
|
79
|
+
[
|
|
80
|
+
{ path: "a.json", harnesses: [named("Cursor", "registered")] },
|
|
81
|
+
{ path: "b.json", harnesses: [named("Cursor", "registered")] },
|
|
82
|
+
],
|
|
83
|
+
"global",
|
|
84
|
+
);
|
|
85
|
+
expect(both[1]?.state).toBe("registered");
|
|
86
|
+
expect(both[1]?.locations).toEqual(["a.json", "b.json"]);
|
|
87
|
+
|
|
88
|
+
const mixed = harnessInventory(
|
|
89
|
+
REGISTRY,
|
|
90
|
+
[
|
|
91
|
+
{ path: "a.json", harnesses: [named("Cursor", "registered")] },
|
|
92
|
+
{ path: "b.json", harnesses: [named("Cursor", "absent")] },
|
|
93
|
+
],
|
|
94
|
+
"global",
|
|
95
|
+
);
|
|
96
|
+
expect(mixed[1]?.state).toBe("drifted");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("gives each harness sharing ONE file its own state, not the file's", () => {
|
|
100
|
+
// The co-detection case, and the reason a group carries per-harness state:
|
|
101
|
+
// `.vscode/mcp.json` holds VS Code's `servers` and Cline's `mcpServers` as
|
|
102
|
+
// two independent entries. Register pragma for VS Code alone and the FILE
|
|
103
|
+
// classifies as drifted — one key current, one absent — but neither
|
|
104
|
+
// harness is drifted: VS Code is registered and Cline is merely detected.
|
|
105
|
+
// A fixture with one harness per file cannot see this.
|
|
106
|
+
const shared: InventoryHarness[] = [
|
|
107
|
+
{ id: "cline", name: "Cline", inScope: true },
|
|
108
|
+
{ id: "vscode", name: "VS Code", inScope: true },
|
|
109
|
+
];
|
|
110
|
+
const rows = harnessInventory(
|
|
111
|
+
shared,
|
|
112
|
+
[
|
|
113
|
+
{
|
|
114
|
+
path: ".vscode/mcp.json",
|
|
115
|
+
harnesses: [named("Cline", "absent"), named("VS Code", "registered")],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
"project",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(rows.map((r) => `${r.harnessId}:${r.state}`)).toEqual([
|
|
122
|
+
"cline:detected",
|
|
123
|
+
"vscode:registered",
|
|
124
|
+
]);
|
|
125
|
+
// Both are located in the same file — the sharing itself is unchanged.
|
|
126
|
+
for (const row of rows) expect(row.locations).toEqual([".vscode/mcp.json"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("ignores a group naming a harness the registry does not know", () => {
|
|
130
|
+
// The inventory reports on the REGISTRY; a group is evidence, not a source
|
|
131
|
+
// of rows, so an unknown name adds nothing rather than inventing an entry.
|
|
132
|
+
const rows = roll([
|
|
133
|
+
{ path: "x.json", harnesses: [named("Ghost Editor", "registered")] },
|
|
134
|
+
]);
|
|
135
|
+
expect(rows).toHaveLength(REGISTRY.length);
|
|
136
|
+
expect(rows.map((r) => r.harnessName)).not.toContain("Ghost Editor");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("names every row with the scope it was rolled up for", () => {
|
|
140
|
+
const rows = harnessInventory(REGISTRY, [], "project");
|
|
141
|
+
expect(rows.every((r) => r.scope === "project")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("harnessInventory — the listing", () => {
|
|
146
|
+
const rows = roll([
|
|
147
|
+
{ path: "~/.claude.json", harnesses: [named("Claude Code", "registered")] },
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
it("shows detected harnesses only by default", () => {
|
|
151
|
+
const items = inventoryItems(rows, false);
|
|
152
|
+
expect(items.map((i) => i.label)).toEqual(["Claude Code"]);
|
|
153
|
+
expect(items[0]?.detail).toBe("registered — ~/.claude.json");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("--verbose widens the same rows to the whole registry", () => {
|
|
157
|
+
const items = inventoryItems(rows, true);
|
|
158
|
+
expect(items.map((i) => i.label)).toEqual([
|
|
159
|
+
"Claude Code",
|
|
160
|
+
"Cursor",
|
|
161
|
+
"VS Code",
|
|
162
|
+
]);
|
|
163
|
+
expect(items[1]?.detail).toBe("not detected");
|
|
164
|
+
// Scope-aware, and it states the FACT rather than naming a partition:
|
|
165
|
+
// VS Code keeps its MCP config per project, so there is no global file for
|
|
166
|
+
// pragma to be in. `no Global band` named an internal word for that.
|
|
167
|
+
expect(items[2]?.detail).toBe(
|
|
168
|
+
"keeps no global config — it is per-project only",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("never renders any state as a failure", () => {
|
|
173
|
+
for (const item of inventoryItems(rows, true)) {
|
|
174
|
+
expect(item.status).not.toBe("fail");
|
|
175
|
+
}
|
|
176
|
+
const every = harnessInventory(REGISTRY, [], "project");
|
|
177
|
+
for (const item of inventoryItems(every, true)) {
|
|
178
|
+
expect(item.status).not.toBe("fail");
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("is a listing, not a verdict: pass when the scope holds harnesses, skip when it does not", () => {
|
|
183
|
+
expect(inventoryHealth(rows, false).status).toBe("pass");
|
|
184
|
+
expect(inventoryHealth(rows, false).detail).toBe(
|
|
185
|
+
"1 detected · 1 registered",
|
|
186
|
+
);
|
|
187
|
+
expect(inventoryHealth(rows, true).detail).toBe(
|
|
188
|
+
"1 detected · 1 registered · 3 known",
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const empty = roll([]);
|
|
192
|
+
expect(inventoryHealth(empty, false).status).toBe("skip");
|
|
193
|
+
expect(inventoryHealth(empty, false).detail).toBe("no harnesses detected");
|
|
194
|
+
expect(inventoryHealth(empty, false).items).toEqual([]);
|
|
195
|
+
expect(inventoryHealth(empty, true).detail).toBe(
|
|
196
|
+
"none of 3 known harnesses detected",
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("counts a drifted harness as detected but not as registered", () => {
|
|
201
|
+
const drifted = roll([
|
|
202
|
+
{ path: "~/.claude.json", harnesses: [named("Claude Code", "drifted")] },
|
|
203
|
+
]);
|
|
204
|
+
expect(inventoryHealth(drifted, false).detail).toBe(
|
|
205
|
+
"1 detected · 0 registered",
|
|
206
|
+
);
|
|
207
|
+
expect(inventoryItems(drifted, false)[0]?.status).toBe("available");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("isDetectedState", () => {
|
|
212
|
+
it("counts every state that means the harness is on the machine", () => {
|
|
213
|
+
expect(isDetectedState("registered")).toBe(true);
|
|
214
|
+
expect(isDetectedState("drifted")).toBe(true);
|
|
215
|
+
expect(isDetectedState("detected")).toBe(true);
|
|
216
|
+
expect(isDetectedState("undetected")).toBe(false);
|
|
217
|
+
expect(isDetectedState("unscoped")).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
});
|