@canonical/pragma-cli 0.29.0 → 0.30.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 +67 -15
- package/dist/pragma +0 -0
- package/package.json +13 -13
- package/src/config/defaultConfig.test.ts +33 -0
- package/src/config/defaultConfig.ts +59 -0
- package/src/config/defaults.json +17 -0
- package/src/config/findProjectConfigPath.test.ts +53 -0
- package/src/config/findProjectConfigPath.ts +36 -0
- package/src/config/index.ts +17 -2
- package/src/config/mergeConfigFileUpdate.ts +58 -0
- package/src/config/parseConfigValues.ts +215 -0
- package/src/config/readConfig.test.ts +104 -13
- package/src/config/readConfig.ts +12 -153
- package/src/config/readConfigLayers.test.ts +107 -0
- package/src/config/readConfigLayers.ts +115 -0
- package/src/config/resolveGlobalConfigPath.ts +13 -0
- package/src/config/resolveWriteConfigPath.test.ts +53 -0
- package/src/config/resolveWriteConfigPath.ts +31 -0
- package/src/config/types.ts +76 -1
- package/src/config/writeConfig.ts +25 -55
- package/src/domains/block/commands/list.ts +5 -90
- package/src/domains/block/commands/lookup.ts +8 -167
- package/src/domains/block/mcp/specs.ts +16 -196
- package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
- package/src/domains/block/stories.test.ts +28 -0
- package/src/domains/block/stories.ts +254 -0
- package/src/domains/config/commands/channel.test.ts +14 -3
- package/src/domains/config/commands/channel.ts +11 -7
- package/src/domains/config/commands/configScope.ts +46 -0
- package/src/domains/config/commands/framework.test.ts +18 -1
- package/src/domains/config/commands/framework.ts +13 -7
- package/src/domains/config/commands/show.ts +5 -45
- package/src/domains/config/commands/tier.test.ts +13 -2
- package/src/domains/config/commands/tier.ts +11 -7
- package/src/domains/config/commands/trace.test.ts +19 -2
- package/src/domains/config/commands/trace.ts +6 -3
- package/src/domains/config/formatters/channel.test.ts +20 -6
- package/src/domains/config/formatters/channel.ts +14 -11
- package/src/domains/config/formatters/framework.ts +14 -11
- package/src/domains/config/formatters/show.test.ts +27 -0
- package/src/domains/config/formatters/show.ts +27 -4
- package/src/domains/config/formatters/tier.test.ts +26 -10
- package/src/domains/config/formatters/tier.ts +19 -11
- package/src/domains/config/formatters/trace.ts +7 -7
- package/src/domains/config/mcp/specs.ts +33 -53
- package/src/domains/config/operations/show.test.ts +18 -0
- package/src/domains/config/operations/show.ts +7 -1
- package/src/domains/config/operations/types.ts +7 -0
- package/src/domains/config/stories.ts +42 -0
- package/src/domains/config/tasks/index.ts +1 -0
- package/src/domains/config/tasks/setChannel.ts +13 -28
- package/src/domains/config/tasks/setFramework.ts +11 -30
- package/src/domains/config/tasks/setTier.ts +10 -29
- package/src/domains/config/tasks/setTrace.ts +10 -28
- package/src/domains/config/tasks/writeConfigField.ts +41 -0
- package/src/domains/create/commands/component.ts +5 -5
- package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
- package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +2 -14
- package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
- package/src/domains/doctor/operations/checks/index.ts +1 -0
- package/src/domains/doctor/operations/checks.test.ts +158 -7
- package/src/domains/doctor/operations/runChecks.test.ts +15 -6
- package/src/domains/doctor/operations/runChecks.ts +2 -0
- package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
- package/src/domains/graph/helpers/resolveUri.ts +28 -21
- package/src/domains/graphql/commands/build.test.ts +5 -3
- package/src/domains/graphql/commands/build.ts +8 -16
- package/src/domains/graphql/commands/check.test.ts +5 -3
- package/src/domains/graphql/commands/check.ts +7 -13
- package/src/domains/graphql/commands/index.ts +1 -0
- package/src/domains/graphql/commands/serve.test.ts +40 -0
- package/src/domains/graphql/commands/serve.ts +131 -0
- package/src/domains/graphql/constants.ts +10 -0
- package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
- package/src/domains/graphql/helpers/gatherSources.ts +58 -0
- package/src/domains/graphql/helpers/index.ts +2 -0
- package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
- package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
- package/src/domains/graphql/index.ts +9 -6
- package/src/domains/graphql/operations/compileSchema.test.ts +21 -45
- package/src/domains/graphql/operations/compileSchema.ts +23 -25
- package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
- package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
- package/src/domains/graphql/operations/index.ts +5 -0
- package/src/domains/info/operations/collectInfo.test.ts +14 -3
- package/src/domains/info/operations/collectInfo.ts +12 -15
- package/src/domains/llm/capabilities.test.ts +4 -4
- package/src/domains/llm/data/toolCatalog.ts +6 -0
- package/src/domains/modifier/commands/list.ts +5 -38
- package/src/domains/modifier/commands/lookup.ts +8 -79
- package/src/domains/modifier/mcp/specs.ts +15 -94
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
- package/src/domains/modifier/stories.test.ts +20 -0
- package/src/domains/modifier/stories.ts +77 -0
- package/src/domains/ontology/commands/list.ts +5 -58
- package/src/domains/ontology/commands/show.ts +6 -58
- package/src/domains/ontology/formatters/show.ts +7 -1
- package/src/domains/ontology/mcp/specs.ts +7 -84
- package/src/domains/ontology/stories.test.ts +36 -0
- package/src/domains/ontology/stories.ts +108 -0
- package/src/domains/refs/operations/updateRefs.ts +2 -37
- package/src/domains/setup/commands/skills.test.ts +6 -2
- package/src/domains/setup/commands/skills.ts +6 -2
- package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
- package/src/domains/setup/helpers/runSetupTask.ts +62 -70
- package/src/domains/setup/operations/setupMcp.test.ts +6 -1
- package/src/domains/shared/bootStore.ts +6 -3
- package/src/domains/shared/compactUri.test.ts +11 -0
- package/src/domains/shared/compactUri.ts +10 -5
- package/src/domains/shared/context.ts +2 -2
- package/src/domains/shared/contracts.ts +11 -0
- package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
- package/src/domains/shared/createInteractivePromptSession.ts +236 -0
- package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
- package/src/domains/shared/loaders/bundledLoader.ts +110 -12
- package/src/domains/shared/loaders/gitLoader.ts +2 -2
- package/src/domains/shared/loaders/index.ts +4 -1
- package/src/domains/shared/loaders/localLoader.ts +18 -4
- package/src/domains/shared/loaders/readPackageDir.ts +36 -1
- package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
- package/src/domains/shared/lookupMany.test.ts +120 -0
- package/src/domains/shared/lookupMany.ts +34 -2
- package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
- package/src/domains/shared/mergeAndParseRefs.ts +44 -0
- package/src/domains/shared/packages.ts +10 -17
- package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
- package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
- package/src/domains/shared/runtime.ts +4 -63
- package/src/domains/shared/semanticPackage.ts +17 -1
- package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
- package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
- package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
- package/src/domains/shared/stories/compileLookupTool.ts +96 -0
- package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
- package/src/domains/shared/stories/compileReadCommand.ts +48 -0
- package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
- package/src/domains/shared/stories/compileReadTool.ts +38 -0
- package/src/domains/shared/stories/condense.test.ts +18 -0
- package/src/domains/shared/stories/condense.ts +15 -0
- package/src/domains/shared/stories/index.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.ts +22 -0
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
- package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
- package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
- package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
- package/src/domains/shared/stories/pack/index.ts +28 -0
- package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
- package/src/domains/shared/stories/pack/types.ts +104 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
- package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
- package/src/domains/shared/stories/projectCliParameters.ts +57 -0
- package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
- package/src/domains/shared/stories/projectToolParams.ts +32 -0
- package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
- package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
- package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
- package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
- package/src/domains/shared/stories/types.ts +171 -0
- package/src/domains/shared/suggestions/expandGlob.ts +1 -0
- package/src/domains/shared/suggestions/suggestNames.ts +43 -10
- package/src/domains/skill/commands/index.ts +3 -1
- package/src/domains/skill/commands/list.test.ts +6 -2
- package/src/domains/skill/commands/list.ts +5 -63
- package/src/domains/skill/commands/lookup.test.ts +118 -0
- package/src/domains/skill/commands/lookup.ts +14 -0
- package/src/domains/skill/formatters/index.ts +1 -0
- package/src/domains/skill/formatters/lookup.test.ts +46 -0
- package/src/domains/skill/formatters/lookup.ts +57 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +0 -1
- package/src/domains/skill/index.ts +7 -3
- package/src/domains/skill/mcp/specs.ts +10 -29
- package/src/domains/skill/operations/discover.test.ts +61 -30
- package/src/domains/skill/operations/discover.ts +28 -37
- package/src/domains/skill/operations/index.ts +2 -1
- package/src/domains/skill/operations/list.test.ts +15 -7
- package/src/domains/skill/operations/lookup.test.ts +91 -0
- package/src/domains/skill/operations/lookup.ts +70 -0
- package/src/domains/skill/stories.test.ts +43 -0
- package/src/domains/skill/stories.ts +103 -0
- package/src/domains/skill/types.ts +8 -0
- package/src/domains/standard/commands/list.ts +5 -84
- package/src/domains/standard/commands/lookup.ts +8 -93
- package/src/domains/standard/mcp/specs.ts +16 -171
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +2 -1
- package/src/domains/standard/stories.test.ts +24 -0
- package/src/domains/standard/stories.ts +151 -0
- package/src/domains/tier/commands/list.ts +5 -55
- package/src/domains/tier/mcp/specs.ts +5 -31
- package/src/domains/tier/stories.test.ts +22 -0
- package/src/domains/tier/stories.ts +52 -0
- package/src/domains/token/mcp/specs.ts +5 -1
- package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
- package/src/domains/trace/tracePlugin.test.ts +1 -1
- package/src/mcp/registerResources.test.ts +382 -74
- package/src/mcp/registerResources.ts +5 -348
- package/src/mcp/resources/buildGraphIndex.ts +167 -0
- package/src/mcp/resources/buildResourceList.test.ts +97 -0
- package/src/mcp/resources/buildResourceList.ts +176 -0
- package/src/mcp/resources/classifyEntity.test.ts +40 -0
- package/src/mcp/resources/classifyEntity.ts +27 -0
- package/src/mcp/resources/constants.ts +105 -0
- package/src/mcp/resources/displayPredicates.ts +62 -0
- package/src/mcp/resources/pickFirstValue.test.ts +29 -0
- package/src/mcp/resources/pickFirstValue.ts +26 -0
- package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
- package/src/mcp/resources/pickPrimaryType.ts +26 -0
- package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
- package/src/mcp/resources/rankUriCompletions.ts +64 -0
- package/src/mcp/resources/readEntity.ts +169 -0
- package/src/mcp/resources/registerResources.ts +126 -0
- package/src/mcp/resources/types.ts +128 -0
- package/src/mcp/tools/index.ts +9 -0
- package/src/mcp/tools/registerTools.test.ts +65 -8
- package/src/mcp/types.ts +4 -0
- package/src/pipeline/collectCommands.ts +8 -1
- package/src/pipeline/runCli.ts +40 -6
- package/src/testing/createTestMcpClient.ts +4 -1
- package/src/testing/index.ts +10 -0
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
- package/src/testing/integration/executor-execution.test.ts +101 -0
- package/src/testing/integration/executor-golden.test.ts +145 -0
- package/src/testing/integration/mcp-surface.test.ts +1 -1
- package/src/testing/integration/parity.test.ts +17 -12
- package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
- package/src/testing/setupXdgIsolation.ts +15 -0
- package/src/testing/standardParityFixtures.ts +92 -0
- package/src/testing/storyFixtures.ts +63 -0
- package/src/pipeline/runInteractiveCommand.ts +0 -198
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PragmaError } from "#error";
|
|
3
|
+
import lookupMany, { lookupToolMeta } from "./lookupMany.js";
|
|
4
|
+
|
|
5
|
+
describe("lookupMany", () => {
|
|
6
|
+
it("returns all results when every query resolves", async () => {
|
|
7
|
+
const result = await lookupMany(["a", "b"], async (q) => q.toUpperCase());
|
|
8
|
+
|
|
9
|
+
expect(result.results).toEqual(["A", "B"]);
|
|
10
|
+
expect(result.errors).toEqual([]);
|
|
11
|
+
expect(result.meta).toEqual({ internalErrorCount: 0 });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("collects PragmaError failures as structured error entries", async () => {
|
|
15
|
+
const result = await lookupMany(["Button", "Buton"], async (q) => {
|
|
16
|
+
if (q === "Buton") {
|
|
17
|
+
throw PragmaError.notFound("block", q, { suggestions: ["Button"] });
|
|
18
|
+
}
|
|
19
|
+
return q;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(result.results).toEqual(["Button"]);
|
|
23
|
+
expect(result.errors).toEqual([
|
|
24
|
+
{
|
|
25
|
+
query: "Buton",
|
|
26
|
+
code: "ENTITY_NOT_FOUND",
|
|
27
|
+
message: 'block "Buton" not found.',
|
|
28
|
+
suggestions: ["Button"],
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
expect(result.meta).toEqual({ internalErrorCount: 0 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps N-1 results when one query throws a non-PragmaError", async () => {
|
|
35
|
+
const result = await lookupMany(
|
|
36
|
+
["a", "poisoned", "c"],
|
|
37
|
+
async (q): Promise<string> => {
|
|
38
|
+
if (q === "poisoned") {
|
|
39
|
+
throw new TypeError("cannot read properties of undefined");
|
|
40
|
+
}
|
|
41
|
+
return q.toUpperCase();
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(result.results).toEqual(["A", "C"]);
|
|
46
|
+
expect(result.errors).toEqual([
|
|
47
|
+
{
|
|
48
|
+
query: "poisoned",
|
|
49
|
+
code: "INTERNAL_ERROR",
|
|
50
|
+
message: "Internal error: cannot read properties of undefined",
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
expect(result.meta).toEqual({ internalErrorCount: 1 });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("stringifies non-Error rejection values", async () => {
|
|
57
|
+
const result = await lookupMany(["a"], async () => {
|
|
58
|
+
throw "string failure";
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.results).toEqual([]);
|
|
62
|
+
expect(result.errors).toEqual([
|
|
63
|
+
{
|
|
64
|
+
query: "a",
|
|
65
|
+
code: "INTERNAL_ERROR",
|
|
66
|
+
message: "Internal error: string failure",
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
expect(result.meta).toEqual({ internalErrorCount: 1 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("counts each internal error and mixes with PragmaError entries", async () => {
|
|
73
|
+
const result = await lookupMany(
|
|
74
|
+
["ok", "missing", "boom-1", "boom-2"],
|
|
75
|
+
async (q): Promise<string> => {
|
|
76
|
+
if (q === "missing") throw PragmaError.notFound("token", q);
|
|
77
|
+
if (q.startsWith("boom")) throw new Error(q);
|
|
78
|
+
return q;
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result.results).toEqual(["ok"]);
|
|
83
|
+
expect(result.errors).toHaveLength(3);
|
|
84
|
+
expect(result.errors.map((e) => e.code)).toEqual([
|
|
85
|
+
"ENTITY_NOT_FOUND",
|
|
86
|
+
"INTERNAL_ERROR",
|
|
87
|
+
"INTERNAL_ERROR",
|
|
88
|
+
]);
|
|
89
|
+
expect(result.meta).toEqual({ internalErrorCount: 2 });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("lookupToolMeta", () => {
|
|
94
|
+
it("reports count and omits internalErrorCount when zero", async () => {
|
|
95
|
+
const result = await lookupMany(["a", "b"], async (q) => q);
|
|
96
|
+
|
|
97
|
+
expect(lookupToolMeta(result)).toEqual({ count: 2 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("includes internalErrorCount when at least one query failed unexpectedly", async () => {
|
|
101
|
+
const result = await lookupMany(
|
|
102
|
+
["a", "boom"],
|
|
103
|
+
async (q): Promise<string> => {
|
|
104
|
+
if (q === "boom") throw new Error("boom");
|
|
105
|
+
return q;
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(lookupToolMeta(result)).toEqual({
|
|
110
|
+
count: 1,
|
|
111
|
+
internalErrorCount: 1,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("tolerates hand-built results without meta", () => {
|
|
116
|
+
expect(lookupToolMeta({ results: ["x"], errors: [] })).toEqual({
|
|
117
|
+
count: 1,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -6,6 +6,11 @@ import type { LookupResult } from "./contracts.js";
|
|
|
6
6
|
*
|
|
7
7
|
* Used by MCP lookup tools so a single call can return multiple results
|
|
8
8
|
* without failing the entire request when one query is missing.
|
|
9
|
+
*
|
|
10
|
+
* The collection loop never rejects: a `PragmaError` becomes an error entry
|
|
11
|
+
* with its own code, and any other thrown value becomes an `INTERNAL_ERROR`
|
|
12
|
+
* entry — so one poisoned query cannot discard the other queries' results.
|
|
13
|
+
* `meta.internalErrorCount` reports how many entries are internal errors.
|
|
9
14
|
*/
|
|
10
15
|
export default async function lookupMany<TResult>(
|
|
11
16
|
queries: readonly string[],
|
|
@@ -21,6 +26,7 @@ export default async function lookupMany<TResult>(
|
|
|
21
26
|
queries.map((query) => lookup(query)),
|
|
22
27
|
);
|
|
23
28
|
const results: TResult[] = [];
|
|
29
|
+
let internalErrorCount = 0;
|
|
24
30
|
|
|
25
31
|
for (const [index, outcome] of settled.entries()) {
|
|
26
32
|
const query = queries[index];
|
|
@@ -45,9 +51,35 @@ export default async function lookupMany<TResult>(
|
|
|
45
51
|
}),
|
|
46
52
|
});
|
|
47
53
|
} else {
|
|
48
|
-
|
|
54
|
+
internalErrorCount += 1;
|
|
55
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
56
|
+
errors.push({
|
|
57
|
+
query,
|
|
58
|
+
code: "INTERNAL_ERROR",
|
|
59
|
+
message: `Internal error: ${reason}`,
|
|
60
|
+
});
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
return { results, errors };
|
|
64
|
+
return { results, errors, meta: { internalErrorCount } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build MCP envelope metadata for a lookup result.
|
|
69
|
+
*
|
|
70
|
+
* Always reports `count`; includes `internalErrorCount` only when at least
|
|
71
|
+
* one query failed unexpectedly, so healthy responses stay unchanged.
|
|
72
|
+
*
|
|
73
|
+
* @param result - A lookup result, typically produced via {@link lookupMany}.
|
|
74
|
+
* @returns Meta object for the `{ ok, data, meta }` tool envelope.
|
|
75
|
+
*/
|
|
76
|
+
export function lookupToolMeta<TResult>(result: LookupResult<TResult>): {
|
|
77
|
+
count: number;
|
|
78
|
+
internalErrorCount?: number;
|
|
79
|
+
} {
|
|
80
|
+
const internalErrorCount = result.meta?.internalErrorCount ?? 0;
|
|
81
|
+
return {
|
|
82
|
+
count: result.results.length,
|
|
83
|
+
...(internalErrorCount > 0 && { internalErrorCount }),
|
|
84
|
+
};
|
|
53
85
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import type { RawPackageEntry } from "../refs/operations/parseRef.js";
|
|
6
|
+
import { mergeAndParseRefs } from "./mergeAndParseRefs.js";
|
|
7
|
+
import { DEFAULT_PACKAGES } from "./packages.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_NAMES = DEFAULT_PACKAGES.map((entry) =>
|
|
10
|
+
typeof entry === "string" ? entry : entry.name,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
function writeGlobalRefs(
|
|
14
|
+
xdgDir: string,
|
|
15
|
+
packages: ReadonlyArray<RawPackageEntry>,
|
|
16
|
+
): void {
|
|
17
|
+
const dir = join(xdgDir, "pragma");
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
writeFileSync(join(dir, "refs.json"), JSON.stringify({ packages }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("mergeAndParseRefs", () => {
|
|
23
|
+
let tmpDir: string;
|
|
24
|
+
const origEnv = { ...process.env };
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pragma-merge-refs-"));
|
|
28
|
+
// Point XDG_CONFIG_HOME to our temp dir so readGlobalRefs() resolves there
|
|
29
|
+
process.env.XDG_CONFIG_HOME = tmpDir;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
process.env = { ...origEnv };
|
|
34
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns parsed defaults when config and global refs are absent", () => {
|
|
38
|
+
const refs = mergeAndParseRefs(undefined);
|
|
39
|
+
expect(refs.map((ref) => ref.pkg)).toEqual(DEFAULT_NAMES);
|
|
40
|
+
expect(refs.every((ref) => ref.kind === "git")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("treats an explicitly empty config list as not configured", () => {
|
|
44
|
+
expect(mergeAndParseRefs([])).toEqual(mergeAndParseRefs(undefined));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("falls back to global refs when the config list is empty", () => {
|
|
48
|
+
writeGlobalRefs(tmpDir, [
|
|
49
|
+
{ name: "@canonical/design-system", source: "file:///tmp/ds" },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const refs = mergeAndParseRefs([]);
|
|
53
|
+
|
|
54
|
+
expect(refs.map((ref) => ref.pkg)).toEqual(DEFAULT_NAMES);
|
|
55
|
+
expect(refs.at(0)).toEqual({
|
|
56
|
+
kind: "file",
|
|
57
|
+
pkg: "@canonical/design-system",
|
|
58
|
+
path: "/tmp/ds",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("merges global refs over defaults by package name", () => {
|
|
63
|
+
writeGlobalRefs(tmpDir, [
|
|
64
|
+
{ name: "@canonical/design-system", source: "file:///tmp/ds" },
|
|
65
|
+
"@acme/extra",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const refs = mergeAndParseRefs(undefined);
|
|
69
|
+
|
|
70
|
+
expect(refs.map((ref) => ref.pkg)).toEqual([
|
|
71
|
+
...DEFAULT_NAMES,
|
|
72
|
+
"@acme/extra",
|
|
73
|
+
]);
|
|
74
|
+
expect(refs.at(0)?.kind).toBe("file");
|
|
75
|
+
expect(refs.at(-1)).toEqual({ kind: "npm", pkg: "@acme/extra" });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("replaces defaults and global refs with a non-empty config list", () => {
|
|
79
|
+
writeGlobalRefs(tmpDir, ["@acme/global-only"]);
|
|
80
|
+
|
|
81
|
+
const refs = mergeAndParseRefs([
|
|
82
|
+
{ name: "@acme/only", source: "file:///tmp/only" },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(refs).toEqual([
|
|
86
|
+
{ kind: "file", pkg: "@acme/only", path: "/tmp/only" },
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PackageRef,
|
|
3
|
+
parsePackageEntry,
|
|
4
|
+
type RawPackageEntry,
|
|
5
|
+
} from "../refs/operations/parseRef.js";
|
|
6
|
+
import readGlobalRefs from "../refs/operations/readGlobalRefs.js";
|
|
7
|
+
import { DEFAULT_PACKAGES } from "./packages.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Merge config packages, global refs, and the built-in defaults into a
|
|
11
|
+
* final parsed `PackageRef` array.
|
|
12
|
+
*
|
|
13
|
+
* Priority: config `packages` > global `refs.json` > hardcoded defaults.
|
|
14
|
+
* A non-empty config list is a full replacement — only the listed packages
|
|
15
|
+
* resolve. An empty or absent list means "not configured": global refs are
|
|
16
|
+
* merged over the defaults by package name, so a global entry for
|
|
17
|
+
* "@canonical/foo" overrides the default entry for the same package.
|
|
18
|
+
*
|
|
19
|
+
* Kept free of store imports so config-only consumers (e.g.
|
|
20
|
+
* `resolveConfiguredGraphs`) can merge refs without loading the ke/WASM
|
|
21
|
+
* runtime.
|
|
22
|
+
*
|
|
23
|
+
* @note Impure — reads global refs from `~/.config/pragma/refs.json`.
|
|
24
|
+
*
|
|
25
|
+
* @param configPackages - The `packages` field from `pragma.config.json`.
|
|
26
|
+
* @returns Parsed refs ready for the loader chain.
|
|
27
|
+
* @throws PragmaError with code CONFIG_ERROR when an entry is malformed.
|
|
28
|
+
*/
|
|
29
|
+
export function mergeAndParseRefs(
|
|
30
|
+
configPackages?: ReadonlyArray<RawPackageEntry>,
|
|
31
|
+
): PackageRef[] {
|
|
32
|
+
// A non-empty config list replaces the package set entirely.
|
|
33
|
+
if (configPackages && configPackages.length > 0) {
|
|
34
|
+
return configPackages.map((entry) => parsePackageEntry(entry));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Empty or absent → merge global refs over the defaults, keyed by name.
|
|
38
|
+
const merged = new Map<string, RawPackageEntry>();
|
|
39
|
+
for (const entry of [...DEFAULT_PACKAGES, ...readGlobalRefs()]) {
|
|
40
|
+
const name = typeof entry === "string" ? entry : entry.name;
|
|
41
|
+
merged.set(name, entry);
|
|
42
|
+
}
|
|
43
|
+
return [...merged.values()].map((entry) => parsePackageEntry(entry));
|
|
44
|
+
}
|
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default semantic graph package registry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Sourced from the declarative built-in defaults document
|
|
5
|
+
* (`src/config/defaults.json`) — pragma core carries no hardcoded
|
|
6
|
+
* knowledge of specific design-system packages. These form the base
|
|
7
|
+
* package set whenever a config layer does not declare a non-empty
|
|
8
|
+
* `packages` list; global `refs.json` entries then merge over them by
|
|
9
|
+
* package name, and a non-empty config list replaces them entirely
|
|
10
|
+
* (see `mergeAndParseRefs`).
|
|
7
11
|
*
|
|
8
12
|
* Resolution is handled by the loader chain in `loaders/`.
|
|
9
13
|
*/
|
|
10
14
|
|
|
15
|
+
import { DEFAULT_CONFIG } from "../../config/defaultConfig.js";
|
|
11
16
|
import type { RawPackageEntry } from "../refs/operations/parseRef.js";
|
|
12
17
|
|
|
13
18
|
/** Default semantic graph packages loaded when no config overrides are present. */
|
|
14
|
-
export const DEFAULT_PACKAGES: readonly RawPackageEntry[] =
|
|
15
|
-
|
|
16
|
-
name: "@canonical/design-system",
|
|
17
|
-
source: "git+https://github.com/canonical/design-system.git#main",
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: "@canonical/anatomy-dsl",
|
|
21
|
-
source: "git+https://github.com/canonical/anatomy-dsl.git#main",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "@canonical/code-standards",
|
|
25
|
-
source: "git+https://github.com/canonical/web-code-standards.git#main",
|
|
26
|
-
},
|
|
27
|
-
];
|
|
19
|
+
export const DEFAULT_PACKAGES: readonly RawPackageEntry[] =
|
|
20
|
+
DEFAULT_CONFIG.packages;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import resolveConfiguredGraphs from "./resolveConfiguredGraphs.js";
|
|
6
|
+
|
|
7
|
+
describe("resolveConfiguredGraphs", () => {
|
|
8
|
+
let dir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
dir = mkdtempSync(join(tmpdir(), "pragma-rcg-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("resolves the default semantic packages' TTL graphs", async () => {
|
|
19
|
+
// No pragma.config.json in the temp dir → mergeAndParseRefs falls back to
|
|
20
|
+
// the default packages, which resolve from node_modules in the workspace.
|
|
21
|
+
const graphs = await resolveConfiguredGraphs(dir);
|
|
22
|
+
|
|
23
|
+
expect(graphs.length).toBeGreaterThan(0);
|
|
24
|
+
expect(graphs[0]).toMatchObject({ format: "turtle" });
|
|
25
|
+
expect(typeof graphs[0]?.content).toBe("string");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve every TTL graph from the semantically-configured packages.
|
|
3
|
+
*
|
|
4
|
+
* Reads `pragma.config.json`, merges its `packages` with the global refs
|
|
5
|
+
* and the built-in defaults (a non-empty `packages` list replaces the set
|
|
6
|
+
* entirely; an empty or absent list falls back to global refs merged over
|
|
7
|
+
* the defaults), resolves each package through the loader chain
|
|
8
|
+
* (local > git > bundled), and returns the union of their definitions/ and
|
|
9
|
+
* data/ graph content — the same discovery the runtime store performs at
|
|
10
|
+
* boot, minus the store.
|
|
11
|
+
*
|
|
12
|
+
* @note Impure — reads config and package files from disk.
|
|
13
|
+
*
|
|
14
|
+
* @param cwd - Working directory used to locate `pragma.config.json`.
|
|
15
|
+
* @returns The TTL graph content of every resolved package.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readConfig } from "#config";
|
|
19
|
+
import {
|
|
20
|
+
createBundledLoader,
|
|
21
|
+
createGitLoader,
|
|
22
|
+
createLocalLoader,
|
|
23
|
+
} from "./loaders/index.js";
|
|
24
|
+
import { mergeAndParseRefs } from "./mergeAndParseRefs.js";
|
|
25
|
+
import {
|
|
26
|
+
type GraphContent,
|
|
27
|
+
resolveSemanticPackages,
|
|
28
|
+
} from "./semanticPackage.js";
|
|
29
|
+
|
|
30
|
+
export default async function resolveConfiguredGraphs(
|
|
31
|
+
cwd: string,
|
|
32
|
+
): Promise<GraphContent[]> {
|
|
33
|
+
const config = readConfig(cwd);
|
|
34
|
+
const refs = mergeAndParseRefs(config.packages);
|
|
35
|
+
const packages = await resolveSemanticPackages(refs, [
|
|
36
|
+
createLocalLoader(),
|
|
37
|
+
createGitLoader(),
|
|
38
|
+
createBundledLoader(),
|
|
39
|
+
]);
|
|
40
|
+
return packages.flatMap((pkg) => pkg.graphs);
|
|
41
|
+
}
|
|
@@ -10,14 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { SourceSpec } from "@canonical/ke";
|
|
12
12
|
import { readConfig } from "#config";
|
|
13
|
-
import {
|
|
14
|
-
type PackageRef,
|
|
15
|
-
parsePackageEntry,
|
|
16
|
-
type RawPackageEntry,
|
|
17
|
-
} from "../refs/operations/parseRef.js";
|
|
18
|
-
import readGlobalRefs from "../refs/operations/readGlobalRefs.js";
|
|
19
13
|
import { bootStore } from "./bootStore.js";
|
|
20
|
-
import {
|
|
14
|
+
import { mergeAndParseRefs } from "./mergeAndParseRefs.js";
|
|
21
15
|
import type { PragmaRuntime } from "./types/index.js";
|
|
22
16
|
|
|
23
17
|
export type { PragmaRuntime } from "./types/index.js";
|
|
@@ -43,7 +37,8 @@ export async function bootPragma(options?: {
|
|
|
43
37
|
const cwd = options?.cwd ?? process.cwd();
|
|
44
38
|
const config = readConfig(cwd);
|
|
45
39
|
|
|
46
|
-
// Merge package refs:
|
|
40
|
+
// Merge package refs: a non-empty config list replaces global refs and
|
|
41
|
+
// defaults; otherwise global refs merge over the defaults.
|
|
47
42
|
const refs = options?.sources
|
|
48
43
|
? undefined
|
|
49
44
|
: mergeAndParseRefs(config.packages);
|
|
@@ -53,6 +48,7 @@ export async function bootPragma(options?: {
|
|
|
53
48
|
sources: options?.sources,
|
|
54
49
|
refs,
|
|
55
50
|
trace: config.trace,
|
|
51
|
+
prefixes: config.prefixes,
|
|
56
52
|
});
|
|
57
53
|
|
|
58
54
|
return {
|
|
@@ -63,58 +59,3 @@ export async function bootPragma(options?: {
|
|
|
63
59
|
dispose: () => store.dispose(),
|
|
64
60
|
};
|
|
65
61
|
}
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// Ref merging
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Merge global refs, project config packages, and defaults into a final
|
|
73
|
-
* parsed PackageRef array.
|
|
74
|
-
*
|
|
75
|
-
* Priority: project packages > global refs > hardcoded defaults.
|
|
76
|
-
* Merging is by package name — a project entry for "@canonical/foo"
|
|
77
|
-
* overrides the global entry for the same package.
|
|
78
|
-
*/
|
|
79
|
-
function mergeAndParseRefs(
|
|
80
|
-
projectPackages?: ReadonlyArray<RawPackageEntry>,
|
|
81
|
-
): PackageRef[] {
|
|
82
|
-
const globalEntries = readGlobalRefs();
|
|
83
|
-
|
|
84
|
-
// If neither project nor global defines packages, return undefined
|
|
85
|
-
// so resolvePackages() uses hardcoded defaults.
|
|
86
|
-
if (
|
|
87
|
-
(!projectPackages || projectPackages.length === 0) &&
|
|
88
|
-
globalEntries.length === 0
|
|
89
|
-
) {
|
|
90
|
-
return DEFAULT_PACKAGES.map((pkg) => parsePackageEntry(pkg));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Build a name → entry map, global first, project overrides.
|
|
94
|
-
const merged = new Map<string, RawPackageEntry>();
|
|
95
|
-
|
|
96
|
-
// Start with defaults (lowest priority)
|
|
97
|
-
for (const entry of DEFAULT_PACKAGES) {
|
|
98
|
-
const name = typeof entry === "string" ? entry : entry.name;
|
|
99
|
-
merged.set(name, entry);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Global overrides defaults
|
|
103
|
-
for (const entry of globalEntries) {
|
|
104
|
-
const name = typeof entry === "string" ? entry : entry.name;
|
|
105
|
-
merged.set(name, entry);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Project overrides global
|
|
109
|
-
if (projectPackages) {
|
|
110
|
-
// When project explicitly declares packages, it's a full replacement
|
|
111
|
-
// of the package list — clear defaults and global, use project only.
|
|
112
|
-
merged.clear();
|
|
113
|
-
for (const entry of projectPackages) {
|
|
114
|
-
const name = typeof entry === "string" ? entry : entry.name;
|
|
115
|
-
merged.set(name, entry);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return [...merged.values()].map(parsePackageEntry);
|
|
120
|
-
}
|
|
@@ -24,7 +24,18 @@ export interface GraphContent {
|
|
|
24
24
|
readonly format: "turtle";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** A discovered
|
|
27
|
+
/** A discovered story-pack JSON file with its content. */
|
|
28
|
+
export interface StoryFileEntry {
|
|
29
|
+
/**
|
|
30
|
+
* Original file path, or a virtual identifier for embedded sources
|
|
31
|
+
* (e.g. `(bundled)/…`) — for provenance/debugging, not guaranteed to
|
|
32
|
+
* exist on disk.
|
|
33
|
+
*/
|
|
34
|
+
readonly path: string;
|
|
35
|
+
/** The file's parsed JSON — validated by the story-pack compiler. */
|
|
36
|
+
readonly definition: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export interface SkillEntry {
|
|
29
40
|
/** Absolute path to the skill directory (must exist on disk for symlinking). */
|
|
30
41
|
readonly dir: string;
|
|
@@ -44,6 +55,11 @@ export interface SemanticPackage {
|
|
|
44
55
|
readonly graphs: GraphContent[];
|
|
45
56
|
/** Skills available in this package. */
|
|
46
57
|
readonly skills: SkillEntry[];
|
|
58
|
+
/**
|
|
59
|
+
* Raw story-pack files shipped by this package (`stories/*.json`).
|
|
60
|
+
* Parsed JSON, validated at compile time so one bad file cannot break boot.
|
|
61
|
+
*/
|
|
62
|
+
readonly stories: StoryFileEntry[];
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
/**
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PragmaError } from "#error";
|
|
3
|
+
import type { PragmaContext } from "../context.js";
|
|
4
|
+
import type { LookupResult } from "../contracts.js";
|
|
5
|
+
import compileLookupCommand from "./compileLookupCommand.js";
|
|
6
|
+
import type { LookupStory } from "./types.js";
|
|
7
|
+
|
|
8
|
+
interface FakeEntity {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly detail: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeStory(
|
|
14
|
+
overrides: Partial<LookupStory<FakeEntity, string>> = {},
|
|
15
|
+
): LookupStory<FakeEntity, string> {
|
|
16
|
+
return {
|
|
17
|
+
noun: "thing",
|
|
18
|
+
description: "Look up things",
|
|
19
|
+
toolDescription: "Look up things over MCP.",
|
|
20
|
+
namesDescription: "Thing names",
|
|
21
|
+
detailedParam: { description: "Show details" },
|
|
22
|
+
examples: ["pragma thing lookup a"],
|
|
23
|
+
resolve: async (_rt, names) => {
|
|
24
|
+
const results = names
|
|
25
|
+
.filter((name) => name !== "missing")
|
|
26
|
+
.map((name) => ({ name, detail: `detail-${name}` }));
|
|
27
|
+
const errors: LookupResult<FakeEntity>["errors"] = names
|
|
28
|
+
.filter((name) => name === "missing")
|
|
29
|
+
.map((name) => ({
|
|
30
|
+
query: name,
|
|
31
|
+
code: "ENTITY_NOT_FOUND",
|
|
32
|
+
message: `thing "${name}" not found.`,
|
|
33
|
+
}));
|
|
34
|
+
return { results, errors };
|
|
35
|
+
},
|
|
36
|
+
toFmtInput: (entity, view) =>
|
|
37
|
+
view.detailed ? `${entity.name}:${entity.detail}` : entity.name,
|
|
38
|
+
formatters: {
|
|
39
|
+
plain: (text) => `plain:${text}`,
|
|
40
|
+
llm: (text) => `llm:${text}`,
|
|
41
|
+
json: (text) => JSON.stringify(text),
|
|
42
|
+
},
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeCtx(overrides: Partial<PragmaContext> = {}): PragmaContext {
|
|
48
|
+
return {
|
|
49
|
+
cwd: "/tmp",
|
|
50
|
+
globalFlags: { llm: false, format: "text" as const, verbose: false },
|
|
51
|
+
...overrides,
|
|
52
|
+
} as PragmaContext;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function executeText(
|
|
56
|
+
story: LookupStory<FakeEntity, string>,
|
|
57
|
+
params: Record<string, unknown>,
|
|
58
|
+
ctx: PragmaContext,
|
|
59
|
+
): Promise<string> {
|
|
60
|
+
const cmd = compileLookupCommand(ctx, story);
|
|
61
|
+
const result = await cmd.execute(params, ctx);
|
|
62
|
+
if (result.tag !== "output") throw new Error("expected output result");
|
|
63
|
+
return result.render.plain(result.value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("compileLookupCommand", () => {
|
|
67
|
+
it("projects names, detailed, and extra parameters", () => {
|
|
68
|
+
const story = makeStory({
|
|
69
|
+
params: [
|
|
70
|
+
{ name: "extra", type: "boolean", description: "extra flag" },
|
|
71
|
+
{ name: "mcpOnly", type: "boolean", description: "x", surfaces: "mcp" },
|
|
72
|
+
],
|
|
73
|
+
parameterGroups: { Extras: ["extra"] },
|
|
74
|
+
});
|
|
75
|
+
const cmd = compileLookupCommand(makeCtx(), story);
|
|
76
|
+
expect(cmd.path).toEqual(["thing", "lookup"]);
|
|
77
|
+
expect(cmd.parameters.map((p) => p.name)).toEqual([
|
|
78
|
+
"names",
|
|
79
|
+
"detailed",
|
|
80
|
+
"extra",
|
|
81
|
+
]);
|
|
82
|
+
const names = cmd.parameters.at(0);
|
|
83
|
+
expect(names?.type).toBe("multiselect");
|
|
84
|
+
expect(names?.positional).toBe(true);
|
|
85
|
+
expect(names?.required).toBe(true);
|
|
86
|
+
expect(cmd.parameterGroups).toEqual({ Extras: ["extra"] });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("omits the detailed parameter when the story has none", () => {
|
|
90
|
+
const cmd = compileLookupCommand(
|
|
91
|
+
makeCtx(),
|
|
92
|
+
makeStory({ detailedParam: undefined }),
|
|
93
|
+
);
|
|
94
|
+
expect(cmd.parameters.map((p) => p.name)).toEqual(["names"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("renders each result and appends an errors section", async () => {
|
|
98
|
+
const text = await executeText(
|
|
99
|
+
makeStory(),
|
|
100
|
+
{ names: ["a", "missing"] },
|
|
101
|
+
makeCtx(),
|
|
102
|
+
);
|
|
103
|
+
expect(text).toContain("plain:a");
|
|
104
|
+
expect(text).toContain("Errors:");
|
|
105
|
+
expect(text).toContain('thing "missing" not found.');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("is summary by default and detailed with the flag", async () => {
|
|
109
|
+
const summary = await executeText(makeStory(), { names: ["a"] }, makeCtx());
|
|
110
|
+
expect(summary).toBe("plain:a");
|
|
111
|
+
|
|
112
|
+
const detailed = await executeText(
|
|
113
|
+
makeStory(),
|
|
114
|
+
{ names: ["a"], detailed: true },
|
|
115
|
+
makeCtx(),
|
|
116
|
+
);
|
|
117
|
+
expect(detailed).toBe("plain:a:detail-a");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("accepts the legacy single name parameter", async () => {
|
|
121
|
+
const text = await executeText(makeStory(), { name: "a" }, makeCtx());
|
|
122
|
+
expect(text).toBe("plain:a");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("throws the story's empty-names error when no names are given", async () => {
|
|
126
|
+
const story = makeStory({
|
|
127
|
+
emptyNamesError: () => PragmaError.invalidInput("names", "(empty)"),
|
|
128
|
+
});
|
|
129
|
+
const cmd = compileLookupCommand(makeCtx(), story);
|
|
130
|
+
await expect(cmd.execute({ names: [] }, makeCtx())).rejects.toThrow(
|
|
131
|
+
PragmaError,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("exposes an ink renderer only when the story defines one", async () => {
|
|
136
|
+
const story = makeStory({
|
|
137
|
+
renderInk: (result, view) =>
|
|
138
|
+
`ink:${result.results.length}:${view.detailed}`,
|
|
139
|
+
});
|
|
140
|
+
const cmd = compileLookupCommand(makeCtx(), story);
|
|
141
|
+
const result = await cmd.execute({ names: ["a"] }, makeCtx());
|
|
142
|
+
if (result.tag !== "output") throw new Error("expected output result");
|
|
143
|
+
expect(result.render.ink?.(result.value)).toBe("ink:1:false");
|
|
144
|
+
});
|
|
145
|
+
});
|