@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,32 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { emitReference } from "../kernel/spec/emitReference.js";
|
|
5
|
+
import { capabilities } from "./index.js";
|
|
6
|
+
|
|
7
|
+
/** Read a committed reference page exactly as a consumer (or lychee) would. */
|
|
8
|
+
function readCommitted(relPath: string): string {
|
|
9
|
+
return readFileSync(
|
|
10
|
+
fileURLToPath(new URL(`../../docs/reference/${relPath}`, import.meta.url)),
|
|
11
|
+
"utf-8",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("reference docs drift-guard — emitReference == committed (PROTECTED)", () => {
|
|
16
|
+
const emitted = emitReference(capabilities);
|
|
17
|
+
|
|
18
|
+
it("every generated page is byte-identical to the committed file", () => {
|
|
19
|
+
for (const [relPath, expected] of emitted) {
|
|
20
|
+
expect(readCommitted(relPath)).toBe(expected);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("the committed file set matches the generated key set (no orphans, none missing)", () => {
|
|
25
|
+
const committed = readdirSync(
|
|
26
|
+
fileURLToPath(new URL("../../docs/reference/", import.meta.url)),
|
|
27
|
+
)
|
|
28
|
+
.filter((name) => name.endsWith(".md"))
|
|
29
|
+
.sort();
|
|
30
|
+
expect(committed).toEqual([...emitted.keys()].sort());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Listing and autocomplete are STORELESS: they read the enriched `index.json`
|
|
5
5
|
* (readPackIndex) off disk, so the resource surface costs nothing until an agent
|
|
6
|
-
* reads one.
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
6
|
+
* reads one. They read it for the pack the BOOT DECISION names, so the listing
|
|
7
|
+
* can never advertise entities whose reads then fail STORE_UNAVAILABLE. A read
|
|
8
|
+
* is STORE-BACKED through the SHARED {@link readEntity} — the same reader
|
|
9
|
+
* `graph inspect` uses — so a resource read and a `graph inspect` of the same
|
|
10
|
+
* URI return identical content (the mirror contract).
|
|
9
11
|
*
|
|
10
12
|
* Degradation (Risk4): on a missing or legacy (pre-v2) index the listing returns
|
|
11
13
|
* a single "run `pragma sources update`" hint — never a live re-index of the
|
|
@@ -16,10 +18,14 @@
|
|
|
16
18
|
* the `--help`/`__complete` fast path.
|
|
17
19
|
*/
|
|
18
20
|
|
|
21
|
+
import { BIN_NAME } from "../../constants.js";
|
|
19
22
|
import { readPackIndex } from "../../kernel/completion/entitySource.js";
|
|
23
|
+
import { asPragmaError } from "../../kernel/error/fromTaskError.js";
|
|
20
24
|
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
25
|
+
import { mcpErrorFrom } from "../../kernel/project/mcp/mcpError.js";
|
|
21
26
|
import type { PackIndex } from "../../kernel/runtime/graphpack/types.js";
|
|
22
27
|
import { readEntity } from "../../kernel/runtime/readEntity.js";
|
|
28
|
+
import { resolveSources } from "../../kernel/runtime/resolveSources.js";
|
|
23
29
|
import type { McpResourceProvider } from "../../kernel/spec/types.js";
|
|
24
30
|
|
|
25
31
|
/** Max autocomplete suggestions returned for a partial URI. */
|
|
@@ -30,13 +36,25 @@ const CLASS_PRIORITY = 0.9;
|
|
|
30
36
|
/** MCP annotation priority for an individual (ABox) entry. */
|
|
31
37
|
const INDIVIDUAL_PRIORITY = 0.3;
|
|
32
38
|
|
|
33
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The `pragma:` URI scheme + the single reserved-expansion template variable.
|
|
41
|
+
*
|
|
42
|
+
* A LITERAL, and it must stay one until the wire identifiers move together.
|
|
43
|
+
* `surface/surface.v2.json` freezes `pragma:{+uri}` as protocol identity, and
|
|
44
|
+
* `buildResourceList` mints `pragma:<prefixed>` for every entry it lists —
|
|
45
|
+
* three writings of one decision. Deriving only this one made a fork advertise
|
|
46
|
+
* `recipes:{+uri}` over a list of 653 `pragma:` URIs: every resource the server
|
|
47
|
+
* offered became unreadable, and the readable form was never advertised.
|
|
48
|
+
* `resources.test.ts` pins the pair through {@link resourceProvider}'s declared
|
|
49
|
+
* surface; `identity.test.ts` masks this template out of its leak scan for the
|
|
50
|
+
* same reason.
|
|
51
|
+
*/
|
|
34
52
|
const URI_TEMPLATE = "pragma:{+uri}";
|
|
35
53
|
|
|
36
54
|
const TEMPLATE_DESCRIPTION =
|
|
37
55
|
"Knowledge-graph entities from the local pack. Read any entry by its prefixed " +
|
|
38
56
|
"URI (e.g. pragma:ds:button); autocomplete matches URI or label substrings. " +
|
|
39
|
-
|
|
57
|
+
`Content mirrors \`${BIN_NAME} graph inspect <uri>\`.`;
|
|
40
58
|
|
|
41
59
|
/** Agent-navigability annotations mirrored onto a listed resource. */
|
|
42
60
|
export interface ResourceAnnotations {
|
|
@@ -78,8 +96,7 @@ export function buildResourceList(
|
|
|
78
96
|
{
|
|
79
97
|
uri: "pragma:sources",
|
|
80
98
|
name: "Store not indexed",
|
|
81
|
-
description:
|
|
82
|
-
"No enriched entity index. Run `pragma sources update` to build it.",
|
|
99
|
+
description: `No enriched entity index. Run \`${BIN_NAME} sources update\` to build it.`,
|
|
83
100
|
mimeType: "application/json",
|
|
84
101
|
},
|
|
85
102
|
];
|
|
@@ -88,7 +105,18 @@ export function buildResourceList(
|
|
|
88
105
|
const boxRank = (e: typeof a) => (e.box === "tbox" ? 0 : 1);
|
|
89
106
|
return boxRank(a) - boxRank(b) || a.name.localeCompare(b.name);
|
|
90
107
|
});
|
|
91
|
-
|
|
108
|
+
// Dedup by resource URI: an OWL-punned subject (a class/property IRI ALSO
|
|
109
|
+
// asserted as a domain individual) is indexed as TWO entities — a tbox and an
|
|
110
|
+
// abox facet — that compact to the SAME `pragma:<uri>`. Mapping both would
|
|
111
|
+
// list one URI twice (A8). The sort above puts the tbox facet first, so
|
|
112
|
+
// keeping the first occurrence per URI surfaces the schema facet (its higher
|
|
113
|
+
// priority + instance count) and drops the punned duplicate.
|
|
114
|
+
const seen = new Set<string>();
|
|
115
|
+
const resources: ListedResource[] = [];
|
|
116
|
+
for (const entity of ordered) {
|
|
117
|
+
const uri = `pragma:${entity.prefixed ?? entity.name}`;
|
|
118
|
+
if (seen.has(uri)) continue;
|
|
119
|
+
seen.add(uri);
|
|
92
120
|
const isTbox = entity.box === "tbox";
|
|
93
121
|
// Port of the old shell's `_meta` taxonomy: `pragma/box` + a priority that
|
|
94
122
|
// ranks schema (classes/properties) above individuals, so an agent browsing
|
|
@@ -104,8 +132,8 @@ export function buildResourceList(
|
|
|
104
132
|
if (typeof instanceCount === "number") {
|
|
105
133
|
meta["pragma/instanceCount"] = instanceCount;
|
|
106
134
|
}
|
|
107
|
-
|
|
108
|
-
uri
|
|
135
|
+
resources.push({
|
|
136
|
+
uri,
|
|
109
137
|
name: entity.label || entity.name,
|
|
110
138
|
...(entity.description ? { description: entity.description } : {}),
|
|
111
139
|
mimeType: "application/json" as const,
|
|
@@ -114,8 +142,9 @@ export function buildResourceList(
|
|
|
114
142
|
priority: isTbox ? CLASS_PRIORITY : INDIVIDUAL_PRIORITY,
|
|
115
143
|
},
|
|
116
144
|
_meta: meta,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return resources;
|
|
119
148
|
}
|
|
120
149
|
|
|
121
150
|
/**
|
|
@@ -180,14 +209,21 @@ export const resourceProvider: McpResourceProvider = {
|
|
|
180
209
|
const { ResourceTemplate } = await import(
|
|
181
210
|
"@modelcontextprotocol/sdk/server/mcp.js"
|
|
182
211
|
);
|
|
212
|
+
const { McpError, ErrorCode } = await import(
|
|
213
|
+
"@modelcontextprotocol/sdk/types.js"
|
|
214
|
+
);
|
|
215
|
+
// The index of the pack the boot decision names — `undefined` when the
|
|
216
|
+
// store is unavailable, which `buildResourceList` renders as the recovery
|
|
217
|
+
// entry rather than a catalogue of unreadable URIs.
|
|
218
|
+
const activeIndex = async (): Promise<PackIndex | undefined> =>
|
|
219
|
+
readPackIndex(resolveSources(await runtime.loadConfig(), runtime.cwd));
|
|
220
|
+
|
|
183
221
|
const template = new ResourceTemplate(URI_TEMPLATE, {
|
|
184
|
-
list: async () => ({
|
|
185
|
-
resources: buildResourceList(readPackIndex(runtime.cwd)),
|
|
186
|
-
}),
|
|
222
|
+
list: async () => ({ resources: buildResourceList(await activeIndex()) }),
|
|
187
223
|
complete: {
|
|
188
224
|
uri: async (value: string) =>
|
|
189
225
|
rankUriCompletions(
|
|
190
|
-
|
|
226
|
+
(await activeIndex())?.entities ?? [],
|
|
191
227
|
value,
|
|
192
228
|
COMPLETION_LIMIT,
|
|
193
229
|
),
|
|
@@ -215,15 +251,12 @@ export const resourceProvider: McpResourceProvider = {
|
|
|
215
251
|
],
|
|
216
252
|
};
|
|
217
253
|
} catch (error) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{ uri: url.href, mimeType: "text/plain", text: message },
|
|
225
|
-
],
|
|
226
|
-
};
|
|
254
|
+
// Surface the failure as a JSON-RPC error (the resource-read analogue
|
|
255
|
+
// of a tool result's `isError`), preserving the machine code AND the
|
|
256
|
+
// recovery in `data` — never swallow it into `text/plain` content an
|
|
257
|
+
// agent reads as a successful entity (which dropped the recovery and
|
|
258
|
+
// masked a cold store as though the entity itself were malformed).
|
|
259
|
+
throw mcpErrorFrom(asPragmaError(error), { McpError, ErrorCode });
|
|
227
260
|
}
|
|
228
261
|
},
|
|
229
262
|
);
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* degrades to a `pragma sources update` hint (never a live re-index).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
10
14
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
11
15
|
import { readPackIndex } from "../../kernel/completion/entitySource.js";
|
|
12
16
|
import { verbKey } from "../../kernel/packs/uniqueness.js";
|
|
@@ -17,7 +21,35 @@ import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
|
17
21
|
import { TEST_FLAGS } from "../../testing/helpers/projectCli.js";
|
|
18
22
|
import { projectMcp } from "../../testing/helpers/projectMcp.js";
|
|
19
23
|
import { graphModule } from "../graph/index.js";
|
|
20
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
buildResourceList,
|
|
26
|
+
rankUriCompletions,
|
|
27
|
+
resourceProvider,
|
|
28
|
+
} from "./provider.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Two entities of the committed embedded pack, chosen for what they PROVE: an
|
|
32
|
+
* abox individual and the tbox class it instantiates, so the schema-before-
|
|
33
|
+
* individual ordering has both sides. Named, not counted — the roster moves
|
|
34
|
+
* whenever the design system does.
|
|
35
|
+
*/
|
|
36
|
+
const BUTTON_NAME = "ds:global.component.button";
|
|
37
|
+
const BUTTON_URI = `pragma:${BUTTON_NAME}`;
|
|
38
|
+
const COMPONENT_URI = "pragma:ds:Component";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Blank out blank-node labels.
|
|
42
|
+
*
|
|
43
|
+
* Oxigraph re-mints blank-node identifiers every time it loads a store, so two
|
|
44
|
+
* independently booted sessions over the SAME pack name the same blank node
|
|
45
|
+
* differently. Those labels are store-local handles, not content — and this
|
|
46
|
+
* test's subject is that both surfaces return the same CONTENT for the same
|
|
47
|
+
* entity. (That the labels are not stable across processes is a real property
|
|
48
|
+
* of exposing blank nodes through `graph inspect`; the toy pack this suite used
|
|
49
|
+
* to run against simply had none.)
|
|
50
|
+
*/
|
|
51
|
+
const withoutBlankNodeLabels = (value: InspectResult): unknown =>
|
|
52
|
+
JSON.parse(JSON.stringify(value).replace(/_:[0-9a-f]+/g, "_:b"));
|
|
21
53
|
|
|
22
54
|
describe("resource listing (storeless, over the pack index)", () => {
|
|
23
55
|
it("degrades to a recovery entry on a missing or legacy index", () => {
|
|
@@ -32,37 +64,99 @@ describe("resource listing (storeless, over the pack index)", () => {
|
|
|
32
64
|
});
|
|
33
65
|
|
|
34
66
|
it("lists the enriched (v2) embedded index, schema entries first", () => {
|
|
35
|
-
const index = readPackIndex(
|
|
67
|
+
const index = readPackIndex({ kind: "embedded" });
|
|
36
68
|
expect(index?.version).toBe(2);
|
|
37
69
|
const resources = buildResourceList(index);
|
|
38
|
-
expect(resources.some((r) => r.uri ===
|
|
70
|
+
expect(resources.some((r) => r.uri === BUTTON_URI)).toBe(true);
|
|
39
71
|
// A class (tbox) sorts before an individual (abox).
|
|
40
|
-
const componentIdx = resources.findIndex(
|
|
41
|
-
|
|
42
|
-
);
|
|
43
|
-
const buttonIdx = resources.findIndex((r) => r.uri === "pragma:ex:Button");
|
|
72
|
+
const componentIdx = resources.findIndex((r) => r.uri === COMPONENT_URI);
|
|
73
|
+
const buttonIdx = resources.findIndex((r) => r.uri === BUTTON_URI);
|
|
44
74
|
expect(componentIdx).toBeLessThan(buttonIdx);
|
|
45
75
|
});
|
|
46
76
|
|
|
77
|
+
it("emits ONE resource per URI for an OWL-punned subject (A8)", () => {
|
|
78
|
+
// A punned subject (a class IRI also asserted as an individual) is indexed
|
|
79
|
+
// as TWO facets — tbox + abox — sharing one prefixed URI. The listing must
|
|
80
|
+
// dedup them so the MCP resource surface carries no duplicate URI. The abox
|
|
81
|
+
// facet is listed first here to prove the dedup relies on the schema-first
|
|
82
|
+
// SORT, not input order.
|
|
83
|
+
const punned: PackIndex = {
|
|
84
|
+
version: 2,
|
|
85
|
+
contentHash: "test",
|
|
86
|
+
prefixes: {},
|
|
87
|
+
entities: [
|
|
88
|
+
{
|
|
89
|
+
name: "ex:Slider",
|
|
90
|
+
type: "ex:Category",
|
|
91
|
+
uri: "https://ex.test/#Slider",
|
|
92
|
+
prefixed: "ex:Slider",
|
|
93
|
+
types: ["owl:Class", "ex:Category"],
|
|
94
|
+
label: "Slider",
|
|
95
|
+
box: "abox",
|
|
96
|
+
description: null,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "ex:Slider",
|
|
100
|
+
type: "owl:Class",
|
|
101
|
+
uri: "https://ex.test/#Slider",
|
|
102
|
+
prefixed: "ex:Slider",
|
|
103
|
+
types: ["owl:Class", "ex:Category"],
|
|
104
|
+
label: "Slider",
|
|
105
|
+
box: "tbox",
|
|
106
|
+
description: null,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
instanceCountByType: {},
|
|
110
|
+
};
|
|
111
|
+
const slider = buildResourceList(punned).filter(
|
|
112
|
+
(r) => r.uri === "pragma:ex:Slider",
|
|
113
|
+
);
|
|
114
|
+
expect(slider).toHaveLength(1);
|
|
115
|
+
// The retained facet is the schema (tbox) one — surfaced above individuals.
|
|
116
|
+
expect(slider.at(0)?._meta?.["pragma/box"]).toBe("tbox");
|
|
117
|
+
});
|
|
118
|
+
|
|
47
119
|
it("enriches each entry with the _meta taxonomy (pragma/box + priority)", () => {
|
|
48
|
-
const resources = buildResourceList(readPackIndex(
|
|
49
|
-
const component = resources.find((r) => r.uri ===
|
|
50
|
-
const button = resources.find((r) => r.uri ===
|
|
120
|
+
const resources = buildResourceList(readPackIndex({ kind: "embedded" }));
|
|
121
|
+
const component = resources.find((r) => r.uri === COMPONENT_URI);
|
|
122
|
+
const button = resources.find((r) => r.uri === BUTTON_URI);
|
|
51
123
|
// A schema class: tbox, higher priority, carries its instance count.
|
|
52
124
|
expect(component?._meta?.["pragma/box"]).toBe("tbox");
|
|
53
125
|
expect(component?.annotations?.audience).toEqual(["assistant"]);
|
|
54
126
|
expect(component?.annotations?.priority).toBe(0.9);
|
|
55
|
-
expect(component?._meta?.["pragma/instanceCount"]).
|
|
127
|
+
expect(component?._meta?.["pragma/instanceCount"]).toBeGreaterThan(0);
|
|
56
128
|
// An individual: abox, lower priority, no instance count.
|
|
57
129
|
expect(button?._meta?.["pragma/box"]).toBe("abox");
|
|
58
130
|
expect(button?.annotations?.priority).toBe(0.3);
|
|
59
131
|
expect(button?._meta?.["pragma/instanceCount"]).toBeUndefined();
|
|
60
132
|
});
|
|
61
133
|
|
|
134
|
+
it("mints every URI in the scheme the declared template routes", () => {
|
|
135
|
+
// Two writings of one decision: the template `register` installs (and the
|
|
136
|
+
// covenant publishes as `mcpSurface.resources`) and the scheme every listed
|
|
137
|
+
// URI carries. Nothing compared them, and a half-derivation left a fork
|
|
138
|
+
// advertising `recipes:{+uri}` over a list of `pragma:` URIs — every
|
|
139
|
+
// resource offered was unreadable and the readable form was unadvertised.
|
|
140
|
+
// DERIVED from the declared surface, so this cannot be satisfied by two
|
|
141
|
+
// literals that agree today.
|
|
142
|
+
const declared = resourceProvider.surface?.templates?.[0];
|
|
143
|
+
const scheme = `${declared?.split(":")[0]}:`;
|
|
144
|
+
const listed = [
|
|
145
|
+
...buildResourceList(readPackIndex({ kind: "embedded" })),
|
|
146
|
+
...buildResourceList(undefined),
|
|
147
|
+
];
|
|
148
|
+
expect(listed.length).toBeGreaterThan(1);
|
|
149
|
+
expect(listed.filter((r) => !r.uri.startsWith(scheme))).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
62
152
|
it("ranks autocomplete over prefixed URI and label", () => {
|
|
63
|
-
const index = readPackIndex(
|
|
64
|
-
const hits = rankUriCompletions(
|
|
65
|
-
|
|
153
|
+
const index = readPackIndex({ kind: "embedded" });
|
|
154
|
+
const hits = rankUriCompletions(
|
|
155
|
+
index?.entities ?? [],
|
|
156
|
+
"global.component.button",
|
|
157
|
+
10,
|
|
158
|
+
);
|
|
159
|
+
expect(hits).toContain(BUTTON_NAME);
|
|
66
160
|
});
|
|
67
161
|
});
|
|
68
162
|
|
|
@@ -77,7 +171,7 @@ describe("resource surface over the server (embedded pack)", () => {
|
|
|
77
171
|
|
|
78
172
|
it("registers the {+uri} template and lists resources (not tools)", async () => {
|
|
79
173
|
const resources = await harness.listResources();
|
|
80
|
-
expect(resources.some((r) => r.uri ===
|
|
174
|
+
expect(resources.some((r) => r.uri === BUTTON_URI)).toBe(true);
|
|
81
175
|
// Resources are NOT tools — the graph module's tools (inspect + the PR6
|
|
82
176
|
// SPARQL escape hatch) are what appear in the tool surface.
|
|
83
177
|
const tools = await harness.listTools();
|
|
@@ -88,12 +182,12 @@ describe("resource surface over the server (embedded pack)", () => {
|
|
|
88
182
|
});
|
|
89
183
|
|
|
90
184
|
it("autocompletes a partial URI through the template", async () => {
|
|
91
|
-
const values = await harness.completeResource("
|
|
92
|
-
expect(values).toContain(
|
|
185
|
+
const values = await harness.completeResource("global.component.butt");
|
|
186
|
+
expect(values).toContain(BUTTON_NAME);
|
|
93
187
|
});
|
|
94
188
|
|
|
95
189
|
it("reads an entity, mirroring `graph inspect` content", async () => {
|
|
96
|
-
const read = await harness.readResource(
|
|
190
|
+
const read = await harness.readResource(BUTTON_URI);
|
|
97
191
|
expect(read.mimeType).toBe("application/json");
|
|
98
192
|
const fromResource = JSON.parse(read.text) as InspectResult;
|
|
99
193
|
|
|
@@ -103,12 +197,73 @@ describe("resource surface over the server (embedded pack)", () => {
|
|
|
103
197
|
(v) => verbKey(v.path) === "graph inspect",
|
|
104
198
|
) as VerbSpec;
|
|
105
199
|
const fromCli = (await inspect.run(
|
|
106
|
-
{ uri:
|
|
200
|
+
{ uri: BUTTON_NAME },
|
|
107
201
|
rt,
|
|
108
202
|
)) as InspectResult;
|
|
109
203
|
(await rt.store.get()).store.dispose();
|
|
110
204
|
|
|
111
|
-
expect(fromResource).toEqual(
|
|
112
|
-
|
|
205
|
+
expect(withoutBlankNodeLabels(fromResource)).toEqual(
|
|
206
|
+
withoutBlankNodeLabels(fromCli),
|
|
207
|
+
);
|
|
208
|
+
expect(fromResource.uri).toBe(
|
|
209
|
+
"https://ds.canonical.com/global.component.button",
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("rejects a read of an absent entity as InvalidParams carrying ENTITY_NOT_FOUND", async () => {
|
|
214
|
+
// The warm embedded pack resolves the `ds:` prefix but holds no `Nonexistent`
|
|
215
|
+
// subject, so the read fails ENTITY_NOT_FOUND — the caller's fault, which
|
|
216
|
+
// `mcpErrorFrom` maps to JSON-RPC InvalidParams, NOT the InternalError a cold
|
|
217
|
+
// store (STORE_UNAVAILABLE) earns.
|
|
218
|
+
const error = await harness.readResource("pragma:ds:Nonexistent").then(
|
|
219
|
+
() => undefined,
|
|
220
|
+
(caught: unknown) => caught,
|
|
221
|
+
);
|
|
222
|
+
expect(error, "the read must throw, not swallow to content").toBeDefined();
|
|
223
|
+
expect((error as Error).message).toMatch(/not found/i);
|
|
224
|
+
const failure = error as { code?: number; data?: { code?: string } };
|
|
225
|
+
expect(failure.data?.code).toBe("ENTITY_NOT_FOUND");
|
|
226
|
+
expect(failure.code).toBe(ErrorCode.InvalidParams);
|
|
227
|
+
expect(failure.code).not.toBe(ErrorCode.InternalError);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* D3 — a resource-read failure surfaces as a JSON-RPC error (the read analogue of
|
|
233
|
+
* a tool result's `isError`) carrying the recovery, NOT swallowed into a
|
|
234
|
+
* `text/plain` "success" body that drops the recovery and reads as though the
|
|
235
|
+
* entity itself were malformed. A project that declares its own packs and has
|
|
236
|
+
* never built them is the cold store — it must NOT be served the distribution's
|
|
237
|
+
* embedded graph; the read boots the store, so it refuses with
|
|
238
|
+
* STORE_UNAVAILABLE.
|
|
239
|
+
*/
|
|
240
|
+
describe("resource read — cold-store failure surfaces isError + recovery (D3)", () => {
|
|
241
|
+
let harness: Awaited<ReturnType<typeof projectMcp>>;
|
|
242
|
+
beforeAll(async () => {
|
|
243
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-resource-cold-"));
|
|
244
|
+
writeFileSync(
|
|
245
|
+
join(cwd, "pragma.config.ts"),
|
|
246
|
+
`export default { packs: [{ name: "unbuilt", source: "file:///pragma-never-built" }] };\n`,
|
|
247
|
+
);
|
|
248
|
+
harness = await projectMcp([graphModule], cwd);
|
|
249
|
+
});
|
|
250
|
+
afterAll(async () => {
|
|
251
|
+
await harness.cleanup();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("rejects (does not return text/plain content) with the STORE_UNAVAILABLE recovery", async () => {
|
|
255
|
+
const error = await harness.readResource(BUTTON_URI).then(
|
|
256
|
+
() => undefined,
|
|
257
|
+
(caught: unknown) => caught,
|
|
258
|
+
);
|
|
259
|
+
expect(error, "the read must throw, not swallow to content").toBeDefined();
|
|
260
|
+
expect((error as Error).message).toMatch(/store unavailable/i);
|
|
261
|
+
const data = (
|
|
262
|
+
error as {
|
|
263
|
+
data?: { code?: string; recovery?: { mcp?: { tool?: string } } };
|
|
264
|
+
}
|
|
265
|
+
).data;
|
|
266
|
+
expect(data?.code).toBe("STORE_UNAVAILABLE");
|
|
267
|
+
expect(data?.recovery?.mcp?.tool).toBe("sources_update");
|
|
113
268
|
});
|
|
114
269
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-sync pin (PROTECTED): the CLI's redeclared `ScopeBand`/`ScopeSelection`
|
|
3
|
+
* mirrors must stay structurally EQUAL to the `@canonical/harnesses` types.
|
|
4
|
+
*
|
|
5
|
+
* The CLI redeclares these in `doctor/types.ts` and `setup/types.ts` on purpose
|
|
6
|
+
* — so those statically-reachable type modules never pull the harnesses runtime
|
|
7
|
+
* onto the fast-path module graph (the lazy-dispatch invariant). That freedom is
|
|
8
|
+
* only safe if the mirrors do not DRIFT from the source of truth, which this
|
|
9
|
+
* `expectTypeOf` pin enforces at `tsc` time.
|
|
10
|
+
*
|
|
11
|
+
* Every harnesses import here is `import type` (type-only, erased at emit), so
|
|
12
|
+
* this file loads no harnesses runtime — the `lazy.test` no-runtime-import guard
|
|
13
|
+
* stays green (it also never reaches this test file: it walks the graph from
|
|
14
|
+
* `capabilities/index`, which imports no test).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ScopeBand as HarnessScopeBand,
|
|
19
|
+
ScopeSelection as HarnessScopeSelection,
|
|
20
|
+
} from "@canonical/harnesses";
|
|
21
|
+
import { describe, expectTypeOf, it } from "vitest";
|
|
22
|
+
import type { ScopeBand as DoctorScopeBand } from "./doctor/types.js";
|
|
23
|
+
import type {
|
|
24
|
+
ScopeBand as SetupScopeBand,
|
|
25
|
+
ScopeSelection as SetupScopeSelection,
|
|
26
|
+
} from "./setup/types.js";
|
|
27
|
+
|
|
28
|
+
describe("scope type-sync — CLI mirrors == @canonical/harnesses", () => {
|
|
29
|
+
it("both ScopeBand mirrors equal the harnesses ScopeBand", () => {
|
|
30
|
+
expectTypeOf<DoctorScopeBand>().toEqualTypeOf<HarnessScopeBand>();
|
|
31
|
+
expectTypeOf<SetupScopeBand>().toEqualTypeOf<HarnessScopeBand>();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("the setup ScopeSelection mirror equals the harnesses ScopeSelection", () => {
|
|
35
|
+
expectTypeOf<SetupScopeSelection>().toEqualTypeOf<HarnessScopeSelection>();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -3,4 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { setupModule } from "./setup.verb.js";
|
|
6
|
-
export type {
|
|
6
|
+
export type {
|
|
7
|
+
ConfiguredTarget,
|
|
8
|
+
ScopeSelection,
|
|
9
|
+
SetupResult,
|
|
10
|
+
SetupSkillsResult,
|
|
11
|
+
SymlinkAction,
|
|
12
|
+
} from "./types.js";
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* `setup completions`").
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
14
|
import { dirname } from "node:path";
|
|
14
15
|
import {
|
|
15
16
|
deleteFile,
|
|
@@ -26,15 +27,39 @@ import {
|
|
|
26
27
|
detectShell,
|
|
27
28
|
type ShellId,
|
|
28
29
|
} from "../shell.js";
|
|
30
|
+
import type { CompletionsState } from "../types.js";
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* The detected completion install target: the shell, the absolute script path,
|
|
32
|
-
*
|
|
34
|
+
* the emitted script body — all `null` when no shell could be detected — and the
|
|
35
|
+
* prior on-disk {@link CompletionsState} (whether an up-to-date/stale script is
|
|
36
|
+
* already installed), read up front so the recap and a `--dry-run` are accurate
|
|
37
|
+
* and an identical rewrite is skipped.
|
|
33
38
|
*/
|
|
34
39
|
export interface CompletionsDetection {
|
|
35
40
|
readonly shell: ShellId | null;
|
|
36
41
|
readonly path: string | null;
|
|
37
42
|
readonly script: string | null;
|
|
43
|
+
readonly state: CompletionsState;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Classify the completion script already at `path` against the body we would
|
|
48
|
+
* write: `installed` when byte-identical (a re-run skips it), `stale` when a
|
|
49
|
+
* different script is present, `absent` when no file exists.
|
|
50
|
+
*
|
|
51
|
+
* @param path - The install path.
|
|
52
|
+
* @param script - The body a write would emit.
|
|
53
|
+
* @returns The prior {@link CompletionsState}.
|
|
54
|
+
* @note Impure — reads the install path.
|
|
55
|
+
*/
|
|
56
|
+
function classifyCompletions(path: string, script: string): CompletionsState {
|
|
57
|
+
if (!existsSync(path)) return "absent";
|
|
58
|
+
try {
|
|
59
|
+
return readFileSync(path, "utf8") === script ? "installed" : "stale";
|
|
60
|
+
} catch {
|
|
61
|
+
return "stale";
|
|
62
|
+
}
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
/**
|
|
@@ -52,7 +77,7 @@ export async function detectCompletions(
|
|
|
52
77
|
cwd: string,
|
|
53
78
|
): Promise<CompletionsDetection> {
|
|
54
79
|
const shell = detectShell();
|
|
55
|
-
if (!shell) return { shell: null, path: null, script: null };
|
|
80
|
+
if (!shell) return { shell: null, path: null, script: null, state: "absent" };
|
|
56
81
|
|
|
57
82
|
const [{ capabilities }, { emitScripts }, { readConfig }] = await Promise.all(
|
|
58
83
|
[
|
|
@@ -68,18 +93,16 @@ export async function detectCompletions(
|
|
|
68
93
|
.filter(([, enabled]) => enabled === false)
|
|
69
94
|
.map(([family]) => family)
|
|
70
95
|
: undefined;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
})[shell],
|
|
82
|
-
};
|
|
96
|
+
const path = completionScriptPath(shell);
|
|
97
|
+
const script = emitScripts(capabilities, {
|
|
98
|
+
...(completion?.minChars !== undefined
|
|
99
|
+
? { minChars: completion.minChars }
|
|
100
|
+
: {}),
|
|
101
|
+
...(disabledFamilies && disabledFamilies.length > 0
|
|
102
|
+
? { disabledFamilies }
|
|
103
|
+
: {}),
|
|
104
|
+
})[shell];
|
|
105
|
+
return { shell, path, script, state: classifyCompletions(path, script) };
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
/**
|
|
@@ -89,6 +112,12 @@ export async function detectCompletions(
|
|
|
89
112
|
* interprets a generator's task TWICE — once for the confirm-gate preview and
|
|
90
113
|
* once to perform it — so the composed task must survive a second drive.
|
|
91
114
|
*
|
|
115
|
+
* The prior `state` drives the MESSAGE only ("already up to date" / "Updating" /
|
|
116
|
+
* "Installing"), NOT whether the write happens: the (idempotent) write is always
|
|
117
|
+
* composed so it carries its `undo` — `--undo` reverses the SAME task and must
|
|
118
|
+
* find the reversible effect, and `execute` interprets the task twice. A
|
|
119
|
+
* byte-identical rewrite in the `installed` case is harmless.
|
|
120
|
+
*
|
|
92
121
|
* @param d - The detection gathered up front.
|
|
93
122
|
* @returns A Task that writes the script (with an undo), or warns if no shell.
|
|
94
123
|
*/
|
|
@@ -98,9 +127,13 @@ export function composeCompletions(d: CompletionsDetection): Task<void> {
|
|
|
98
127
|
"Could not detect your shell — set $SHELL to zsh, bash, or fish.",
|
|
99
128
|
);
|
|
100
129
|
}
|
|
101
|
-
const { path, script, shell } = d;
|
|
130
|
+
const { path, script, shell, state } = d;
|
|
131
|
+
const opening =
|
|
132
|
+
state === "installed"
|
|
133
|
+
? `${shell} completions already up to date at ${path}.`
|
|
134
|
+
: `${state === "stale" ? "Updating" : "Installing"} ${shell} completions to ${path}...`;
|
|
102
135
|
return sequence_([
|
|
103
|
-
info(
|
|
136
|
+
info(opening),
|
|
104
137
|
mkdir(dirname(path), true),
|
|
105
138
|
writeFile(path, script, { undo: deleteFile(path) }),
|
|
106
139
|
info(activationHint(shell)),
|