@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,408 @@
|
|
|
1
|
+
import { PragmaError } from "#error";
|
|
2
|
+
import type {
|
|
3
|
+
StoryPackColumn,
|
|
4
|
+
StoryPackDefinition,
|
|
5
|
+
StoryPackField,
|
|
6
|
+
StoryPackFilter,
|
|
7
|
+
StoryPackLookup,
|
|
8
|
+
StoryPackSection,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
const NOUN_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
12
|
+
const FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
13
|
+
/** A prefixed name (`ex:Recipe`) — prefix + local part, no path slashes. */
|
|
14
|
+
const PREFIXED_NAME_PATTERN = /^[A-Za-z][\w-]*:[^/<>"\s]+$/;
|
|
15
|
+
|
|
16
|
+
/** An absolute IRI that can be safely embedded as `<iri>` in SPARQL. */
|
|
17
|
+
const EMBEDDABLE_IRI_PATTERN = /^[A-Za-z][\w+.-]*:\/\/[^<>"\s]+$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A filter parameter name: a single lowercase word.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately stricter than a noun (no hyphens): the name is used
|
|
23
|
+
* verbatim as the CLI flag, the Commander result key, and the MCP
|
|
24
|
+
* parameter key. A hyphenated name would register as `--meal-type` but
|
|
25
|
+
* be read back by Commander under the camelCased `mealType`, so the
|
|
26
|
+
* filter would silently never apply on the CLI while working on MCP.
|
|
27
|
+
* Restricting to one lowercase word keeps all three identical.
|
|
28
|
+
*/
|
|
29
|
+
const FILTER_PARAM_PATTERN = /^[a-z][a-z0-9]*$/;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parameter names taken by global flags or parameters the kernel appends
|
|
33
|
+
* to every read story — `condensed` is added to every MCP list tool, and
|
|
34
|
+
* `detailed` to every lookup — so a filter may not reuse them.
|
|
35
|
+
*/
|
|
36
|
+
const RESERVED_FILTER_PARAMS = new Set([
|
|
37
|
+
"llm",
|
|
38
|
+
"format",
|
|
39
|
+
"verbose",
|
|
40
|
+
"detailed",
|
|
41
|
+
"condensed",
|
|
42
|
+
"names",
|
|
43
|
+
"help",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate one raw story-pack definition.
|
|
48
|
+
*
|
|
49
|
+
* Fails fast with `CONFIG_ERROR` naming the source and the offending
|
|
50
|
+
* field, mirroring how the `packages` config field is validated.
|
|
51
|
+
*
|
|
52
|
+
* @param raw - Parsed JSON value to validate.
|
|
53
|
+
* @param source - Where the definition came from, for error messages.
|
|
54
|
+
* @returns The validated definition.
|
|
55
|
+
* @throws PragmaError with code `CONFIG_ERROR` when the shape is invalid.
|
|
56
|
+
*/
|
|
57
|
+
export default function validateStoryPackDefinition(
|
|
58
|
+
raw: unknown,
|
|
59
|
+
source: string,
|
|
60
|
+
): StoryPackDefinition {
|
|
61
|
+
const obj = requireObject(raw, "story", source);
|
|
62
|
+
|
|
63
|
+
const noun = requireString(obj.noun, "noun", source);
|
|
64
|
+
if (!NOUN_PATTERN.test(noun)) {
|
|
65
|
+
throw buildStoryConfigError(
|
|
66
|
+
source,
|
|
67
|
+
`"noun" must be kebab-case, got "${noun}".`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const description =
|
|
72
|
+
obj.description === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: requireString(obj.description, "description", source);
|
|
75
|
+
const toolDescription =
|
|
76
|
+
obj.toolDescription === undefined
|
|
77
|
+
? undefined
|
|
78
|
+
: requireString(obj.toolDescription, "toolDescription", source);
|
|
79
|
+
|
|
80
|
+
const list = validateList(obj.list, source);
|
|
81
|
+
const lookup =
|
|
82
|
+
obj.lookup === undefined ? undefined : validateLookup(obj.lookup, source);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
noun,
|
|
86
|
+
...(description !== undefined ? { description } : {}),
|
|
87
|
+
...(toolDescription !== undefined ? { toolDescription } : {}),
|
|
88
|
+
list,
|
|
89
|
+
...(lookup ? { lookup } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateList(
|
|
94
|
+
raw: unknown,
|
|
95
|
+
source: string,
|
|
96
|
+
): StoryPackDefinition["list"] {
|
|
97
|
+
const obj = requireObject(raw, "list", source);
|
|
98
|
+
const query = requireString(obj.query, "list.query", source);
|
|
99
|
+
// Strip leading PREFIX declarations so the real query verb is checked —
|
|
100
|
+
// "PREFIX ex: <…> CONSTRUCT {…}" must fail here, not at first use.
|
|
101
|
+
const afterPrefixes = query.replace(
|
|
102
|
+
/^(\s*PREFIX\s+[A-Za-z][\w-]*:\s*<[^>]*>)+/i,
|
|
103
|
+
"",
|
|
104
|
+
);
|
|
105
|
+
if (!/^\s*SELECT\s/i.test(afterPrefixes)) {
|
|
106
|
+
throw buildStoryConfigError(
|
|
107
|
+
source,
|
|
108
|
+
'"list.query" must be a SPARQL SELECT query.',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!Array.isArray(obj.columns) || obj.columns.length === 0) {
|
|
113
|
+
throw buildStoryConfigError(
|
|
114
|
+
source,
|
|
115
|
+
'"list.columns" must be a non-empty array.',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const columns = obj.columns.map((column, index) =>
|
|
119
|
+
validateColumn(column, `list.columns[${index}]`, source),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const filters = validateFilterArray(obj.filters, source);
|
|
123
|
+
for (const filter of filters ?? []) {
|
|
124
|
+
if (!referencesVariable(query, filter.variable)) {
|
|
125
|
+
throw buildStoryConfigError(
|
|
126
|
+
source,
|
|
127
|
+
`filter variable "?${filter.variable}" does not appear in "list.query".`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { query, columns, ...(filters ? { filters } : {}) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check that the query mentions the variable (`?name` or `$name`).
|
|
137
|
+
*
|
|
138
|
+
* A textual check, not a SPARQL parse — it exists to fail typos at boot
|
|
139
|
+
* instead of silently filtering every row out at first use.
|
|
140
|
+
*/
|
|
141
|
+
function referencesVariable(query: string, variable: string): boolean {
|
|
142
|
+
const pattern = new RegExp(`[?$]${variable}\\b`);
|
|
143
|
+
return pattern.test(query);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateFilterArray(
|
|
147
|
+
raw: unknown,
|
|
148
|
+
source: string,
|
|
149
|
+
): readonly StoryPackFilter[] | undefined {
|
|
150
|
+
if (raw === undefined) return undefined;
|
|
151
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
152
|
+
throw buildStoryConfigError(
|
|
153
|
+
source,
|
|
154
|
+
'"list.filters" must be a non-empty array.',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const filters = raw.map((entry, index) =>
|
|
158
|
+
validateFilter(entry, `list.filters[${index}]`, source),
|
|
159
|
+
);
|
|
160
|
+
const seen = new Set<string>();
|
|
161
|
+
for (const filter of filters) {
|
|
162
|
+
if (seen.has(filter.param)) {
|
|
163
|
+
throw buildStoryConfigError(
|
|
164
|
+
source,
|
|
165
|
+
`duplicate filter param "${filter.param}".`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
seen.add(filter.param);
|
|
169
|
+
}
|
|
170
|
+
return filters;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateFilter(
|
|
174
|
+
raw: unknown,
|
|
175
|
+
where: string,
|
|
176
|
+
source: string,
|
|
177
|
+
): StoryPackFilter {
|
|
178
|
+
const obj = requireObject(raw, where, source);
|
|
179
|
+
const param = requireString(obj.param, `${where}.param`, source);
|
|
180
|
+
if (!FILTER_PARAM_PATTERN.test(param)) {
|
|
181
|
+
throw buildStoryConfigError(
|
|
182
|
+
source,
|
|
183
|
+
`"${where}.param" must be a single lowercase word (letters and digits, no hyphens), got "${param}".`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (RESERVED_FILTER_PARAMS.has(param)) {
|
|
187
|
+
throw buildStoryConfigError(
|
|
188
|
+
source,
|
|
189
|
+
`"${where}.param" "${param}" is a reserved name.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const variable = requireString(obj.variable, `${where}.variable`, source);
|
|
193
|
+
if (!FIELD_PATTERN.test(variable)) {
|
|
194
|
+
throw buildStoryConfigError(
|
|
195
|
+
source,
|
|
196
|
+
`"${where}.variable" must name a SELECT variable.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (!Array.isArray(obj.values) || obj.values.length === 0) {
|
|
200
|
+
throw buildStoryConfigError(
|
|
201
|
+
source,
|
|
202
|
+
`"${where}.values" must be a non-empty array.`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const values = obj.values.map((value, index) =>
|
|
206
|
+
requireString(value, `${where}.values[${index}]`, source),
|
|
207
|
+
);
|
|
208
|
+
const lowered = new Set<string>();
|
|
209
|
+
for (const value of values) {
|
|
210
|
+
const key = value.toLowerCase();
|
|
211
|
+
if (lowered.has(key)) {
|
|
212
|
+
throw buildStoryConfigError(
|
|
213
|
+
source,
|
|
214
|
+
`"${where}.values" contains duplicate value "${value}" ` +
|
|
215
|
+
"(values must be unique, case-insensitively).",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
lowered.add(key);
|
|
219
|
+
}
|
|
220
|
+
const description =
|
|
221
|
+
obj.description === undefined
|
|
222
|
+
? undefined
|
|
223
|
+
: requireString(obj.description, `${where}.description`, source);
|
|
224
|
+
return {
|
|
225
|
+
param,
|
|
226
|
+
variable,
|
|
227
|
+
values,
|
|
228
|
+
...(description !== undefined ? { description } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function validateColumn(
|
|
233
|
+
raw: unknown,
|
|
234
|
+
where: string,
|
|
235
|
+
source: string,
|
|
236
|
+
): StoryPackColumn {
|
|
237
|
+
const obj = requireObject(raw, where, source);
|
|
238
|
+
const field = requireString(obj.field, `${where}.field`, source);
|
|
239
|
+
if (!FIELD_PATTERN.test(field)) {
|
|
240
|
+
throw buildStoryConfigError(
|
|
241
|
+
source,
|
|
242
|
+
`"${where}.field" must name a SELECT variable.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const label =
|
|
246
|
+
obj.label === undefined
|
|
247
|
+
? undefined
|
|
248
|
+
: requireString(obj.label, `${where}.label`, source);
|
|
249
|
+
return { field, ...(label !== undefined ? { label } : {}) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function validateLookup(raw: unknown, source: string): StoryPackLookup {
|
|
253
|
+
const obj = requireObject(raw, "lookup", source);
|
|
254
|
+
const by = requirePredicateTerm(obj.by, "lookup.by", source);
|
|
255
|
+
const type =
|
|
256
|
+
obj.type === undefined
|
|
257
|
+
? undefined
|
|
258
|
+
: requireTerm(obj.type, "lookup.type", source);
|
|
259
|
+
|
|
260
|
+
const fields = validateFieldArray(obj.fields, "lookup.fields", source);
|
|
261
|
+
const sections = validateSectionArray(
|
|
262
|
+
obj.sections,
|
|
263
|
+
"lookup.sections",
|
|
264
|
+
source,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
by,
|
|
269
|
+
...(type !== undefined ? { type } : {}),
|
|
270
|
+
...(fields ? { fields } : {}),
|
|
271
|
+
...(sections ? { sections } : {}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateFieldArray(
|
|
276
|
+
raw: unknown,
|
|
277
|
+
where: string,
|
|
278
|
+
source: string,
|
|
279
|
+
): readonly StoryPackField[] | undefined {
|
|
280
|
+
if (raw === undefined) return undefined;
|
|
281
|
+
if (!Array.isArray(raw)) {
|
|
282
|
+
throw buildStoryConfigError(source, `"${where}" must be an array.`);
|
|
283
|
+
}
|
|
284
|
+
return raw.map((entry, index) =>
|
|
285
|
+
validateField(entry, `${where}[${index}]`, source),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function validateSectionArray(
|
|
290
|
+
raw: unknown,
|
|
291
|
+
where: string,
|
|
292
|
+
source: string,
|
|
293
|
+
): readonly StoryPackSection[] | undefined {
|
|
294
|
+
if (raw === undefined) return undefined;
|
|
295
|
+
if (!Array.isArray(raw)) {
|
|
296
|
+
throw buildStoryConfigError(source, `"${where}" must be an array.`);
|
|
297
|
+
}
|
|
298
|
+
return raw.map((entry, index) => {
|
|
299
|
+
const base = validateField(entry, `${where}[${index}]`, source);
|
|
300
|
+
const obj = entry as Record<string, unknown>;
|
|
301
|
+
if (obj.kind !== undefined && obj.kind !== "field" && obj.kind !== "code") {
|
|
302
|
+
throw buildStoryConfigError(
|
|
303
|
+
source,
|
|
304
|
+
`"${where}[${index}].kind" must be "field" or "code".`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
...base,
|
|
309
|
+
...(obj.kind !== undefined ? { kind: obj.kind as "field" | "code" } : {}),
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function validateField(
|
|
315
|
+
raw: unknown,
|
|
316
|
+
where: string,
|
|
317
|
+
source: string,
|
|
318
|
+
): StoryPackField {
|
|
319
|
+
const obj = requireObject(raw, where, source);
|
|
320
|
+
const name = requireString(obj.name, `${where}.name`, source);
|
|
321
|
+
if (!FIELD_PATTERN.test(name)) {
|
|
322
|
+
throw buildStoryConfigError(
|
|
323
|
+
source,
|
|
324
|
+
`"${where}.name" must be a simple identifier.`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const property = requirePredicateTerm(
|
|
328
|
+
obj.property,
|
|
329
|
+
`${where}.property`,
|
|
330
|
+
source,
|
|
331
|
+
);
|
|
332
|
+
const label =
|
|
333
|
+
obj.label === undefined
|
|
334
|
+
? undefined
|
|
335
|
+
: requireString(obj.label, `${where}.label`, source);
|
|
336
|
+
return { name, property, ...(label !== undefined ? { label } : {}) };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function requireObject(
|
|
340
|
+
raw: unknown,
|
|
341
|
+
where: string,
|
|
342
|
+
source: string,
|
|
343
|
+
): Record<string, unknown> {
|
|
344
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
345
|
+
throw buildStoryConfigError(source, `"${where}" must be an object.`);
|
|
346
|
+
}
|
|
347
|
+
return raw as Record<string, unknown>;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function requireString(raw: unknown, where: string, source: string): string {
|
|
351
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
352
|
+
throw buildStoryConfigError(
|
|
353
|
+
source,
|
|
354
|
+
`"${where}" must be a non-empty string.`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return raw;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Require a prefixed name (`ex:Recipe`) or an embeddable absolute IRI. */
|
|
361
|
+
function requireTerm(raw: unknown, where: string, source: string): string {
|
|
362
|
+
const value = requireString(raw, where, source);
|
|
363
|
+
if (value.includes("://")) {
|
|
364
|
+
if (EMBEDDABLE_IRI_PATTERN.test(value)) return value;
|
|
365
|
+
throw buildStoryConfigError(
|
|
366
|
+
source,
|
|
367
|
+
`"${where}" is not a valid IRI (no whitespace or <>" characters).`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (PREFIXED_NAME_PATTERN.test(value)) return value;
|
|
371
|
+
throw buildStoryConfigError(
|
|
372
|
+
source,
|
|
373
|
+
`"${where}" must be a prefixed name (ex:Recipe) or an absolute IRI.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Require a predicate term: a single term or a slash-separated SPARQL
|
|
379
|
+
* property path of prefixed names (`cs:hasCategory/cs:slug`).
|
|
380
|
+
*
|
|
381
|
+
* Paths are only meaningful in predicate position (`lookup.by` and
|
|
382
|
+
* `fields[].property`/`sections[].property`); class terms go through
|
|
383
|
+
* `requireTerm` and reject paths.
|
|
384
|
+
*/
|
|
385
|
+
function requirePredicateTerm(
|
|
386
|
+
raw: unknown,
|
|
387
|
+
where: string,
|
|
388
|
+
source: string,
|
|
389
|
+
): string {
|
|
390
|
+
const value = requireString(raw, where, source);
|
|
391
|
+
if (value.includes("://") || !value.includes("/")) {
|
|
392
|
+
return requireTerm(value, where, source);
|
|
393
|
+
}
|
|
394
|
+
const segments = value.split("/");
|
|
395
|
+
for (const segment of segments) {
|
|
396
|
+
if (!PREFIXED_NAME_PATTERN.test(segment)) {
|
|
397
|
+
throw buildStoryConfigError(
|
|
398
|
+
source,
|
|
399
|
+
`"${where}" property path segment "${segment}" must be a prefixed name.`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildStoryConfigError(source: string, message: string): PragmaError {
|
|
407
|
+
return PragmaError.configError(`Invalid story in ${source}: ${message}`);
|
|
408
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import projectCliParameters from "./projectCliParameters.js";
|
|
3
|
+
import type { StoryParam } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("projectCliParameters", () => {
|
|
6
|
+
it("drops MCP-only parameters", () => {
|
|
7
|
+
const params: StoryParam[] = [
|
|
8
|
+
{ name: "a", type: "boolean", description: "a" },
|
|
9
|
+
{ name: "b", type: "boolean", description: "b", surfaces: "mcp" },
|
|
10
|
+
{ name: "c", type: "boolean", description: "c", surfaces: "cli" },
|
|
11
|
+
];
|
|
12
|
+
expect(projectCliParameters(params).map((p) => p.name)).toEqual(["a", "c"]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("maps string[] to multiselect and enum strings to select with choices", () => {
|
|
16
|
+
const params: StoryParam[] = [
|
|
17
|
+
{ name: "names", type: "string[]", description: "names" },
|
|
18
|
+
{
|
|
19
|
+
name: "mode",
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "mode",
|
|
22
|
+
enum: ["fast", "slow"],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
const projected = projectCliParameters(params);
|
|
26
|
+
expect(projected.at(0)?.type).toBe("multiselect");
|
|
27
|
+
expect(projected.at(1)?.type).toBe("select");
|
|
28
|
+
expect(projected.at(1)?.choices).toEqual([
|
|
29
|
+
{ label: "fast", value: "fast" },
|
|
30
|
+
{ label: "slow", value: "slow" },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("carries defaults, positionals, required, and completion through", async () => {
|
|
35
|
+
const complete = async () => ["x"];
|
|
36
|
+
const params: StoryParam[] = [
|
|
37
|
+
{
|
|
38
|
+
name: "prefix",
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "prefix",
|
|
41
|
+
positional: true,
|
|
42
|
+
required: true,
|
|
43
|
+
default: "ds",
|
|
44
|
+
complete,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
const projected = projectCliParameters(params);
|
|
48
|
+
const first = projected.at(0);
|
|
49
|
+
expect(first?.positional).toBe(true);
|
|
50
|
+
expect(first?.required).toBe(true);
|
|
51
|
+
expect(first?.default).toBe("ds");
|
|
52
|
+
await expect(
|
|
53
|
+
first?.complete?.("d", {
|
|
54
|
+
cwd: "/",
|
|
55
|
+
globalFlags: { llm: false, format: "text", verbose: false },
|
|
56
|
+
}),
|
|
57
|
+
).resolves.toEqual(["x"]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ParameterDefinition } from "@canonical/cli-core";
|
|
2
|
+
import type { StoryParam } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Project story parameters onto cli-core `ParameterDefinition`s.
|
|
6
|
+
*
|
|
7
|
+
* MCP-only parameters are dropped; `string[]` becomes a multiselect;
|
|
8
|
+
* an `enum` on a string parameter becomes a select with choices.
|
|
9
|
+
*/
|
|
10
|
+
export default function projectCliParameters(
|
|
11
|
+
params: readonly StoryParam[],
|
|
12
|
+
): ParameterDefinition[] {
|
|
13
|
+
return params
|
|
14
|
+
.filter((param) => (param.surfaces ?? "both") !== "mcp")
|
|
15
|
+
.map((param) => toParameterDefinition(param));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toParameterDefinition(param: StoryParam): ParameterDefinition {
|
|
19
|
+
const definition: {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
type: ParameterDefinition["type"];
|
|
23
|
+
choices?: ParameterDefinition["choices"];
|
|
24
|
+
default?: unknown;
|
|
25
|
+
positional?: boolean;
|
|
26
|
+
required?: boolean;
|
|
27
|
+
complete?: ParameterDefinition["complete"];
|
|
28
|
+
} = {
|
|
29
|
+
name: param.name,
|
|
30
|
+
description: param.description,
|
|
31
|
+
type: resolveCliType(param),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (param.enum) {
|
|
35
|
+
definition.choices = param.enum.map((value) => ({ label: value, value }));
|
|
36
|
+
}
|
|
37
|
+
if (param.default !== undefined) {
|
|
38
|
+
definition.default = param.default;
|
|
39
|
+
}
|
|
40
|
+
if (param.positional === true) {
|
|
41
|
+
definition.positional = true;
|
|
42
|
+
}
|
|
43
|
+
if (param.required === true) {
|
|
44
|
+
definition.required = true;
|
|
45
|
+
}
|
|
46
|
+
if (param.complete) {
|
|
47
|
+
definition.complete = param.complete;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return definition;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveCliType(param: StoryParam): ParameterDefinition["type"] {
|
|
54
|
+
if (param.type === "string[]") return "multiselect";
|
|
55
|
+
if (param.type === "string" && param.enum) return "select";
|
|
56
|
+
return param.type;
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import projectToolParams from "./projectToolParams.js";
|
|
3
|
+
import type { StoryParam } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("projectToolParams", () => {
|
|
6
|
+
it("drops CLI-only parameters", () => {
|
|
7
|
+
const params: StoryParam[] = [
|
|
8
|
+
{ name: "a", type: "boolean", description: "a" },
|
|
9
|
+
{ name: "b", type: "boolean", description: "b", surfaces: "cli" },
|
|
10
|
+
{ name: "c", type: "boolean", description: "c", surfaces: "mcp" },
|
|
11
|
+
];
|
|
12
|
+
expect(Object.keys(projectToolParams(params))).toEqual(["a", "c"]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("prefers the tool description and sets optional from required", () => {
|
|
16
|
+
const params: StoryParam[] = [
|
|
17
|
+
{
|
|
18
|
+
name: "names",
|
|
19
|
+
type: "string[]",
|
|
20
|
+
description: "cli text",
|
|
21
|
+
toolDescription: "mcp text",
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
24
|
+
{ name: "flag", type: "boolean", description: "shared text" },
|
|
25
|
+
];
|
|
26
|
+
const projected = projectToolParams(params);
|
|
27
|
+
expect(projected.names).toEqual({
|
|
28
|
+
type: "string[]",
|
|
29
|
+
description: "mcp text",
|
|
30
|
+
optional: false,
|
|
31
|
+
});
|
|
32
|
+
expect(projected.flag).toEqual({
|
|
33
|
+
type: "boolean",
|
|
34
|
+
description: "shared text",
|
|
35
|
+
optional: true,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("carries enums through", () => {
|
|
40
|
+
const params: StoryParam[] = [
|
|
41
|
+
{
|
|
42
|
+
name: "mode",
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "mode",
|
|
45
|
+
enum: ["fast", "slow"],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
expect(projectToolParams(params).mode?.enum).toEqual(["fast", "slow"]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ToolParamDef } from "../ToolSpec.js";
|
|
2
|
+
import type { StoryParam } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Project story parameters onto MCP `ToolParamDef`s.
|
|
6
|
+
*
|
|
7
|
+
* CLI-only parameters are dropped, along with CLI-only attributes
|
|
8
|
+
* (defaults, positionals, completion). Required parameters set
|
|
9
|
+
* `optional: false` explicitly — the MCP adapter treats anything
|
|
10
|
+
* else as optional.
|
|
11
|
+
*/
|
|
12
|
+
export default function projectToolParams(
|
|
13
|
+
params: readonly StoryParam[],
|
|
14
|
+
): Record<string, ToolParamDef> {
|
|
15
|
+
const projected: Record<string, ToolParamDef> = {};
|
|
16
|
+
|
|
17
|
+
for (const param of params) {
|
|
18
|
+
if ((param.surfaces ?? "both") === "cli") continue;
|
|
19
|
+
projected[param.name] = toToolParamDef(param);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return projected;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toToolParamDef(param: StoryParam): ToolParamDef {
|
|
26
|
+
return {
|
|
27
|
+
type: param.type,
|
|
28
|
+
description: param.toolDescription ?? param.description,
|
|
29
|
+
optional: param.required !== true,
|
|
30
|
+
...(param.enum && { enum: param.enum }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PragmaError } from "#error";
|
|
3
|
+
import type { PragmaContext } from "../context.js";
|
|
4
|
+
import requirePragmaContext from "./requirePragmaContext.js";
|
|
5
|
+
|
|
6
|
+
describe("requirePragmaContext", () => {
|
|
7
|
+
it("returns a context that carries the pragma runtime", () => {
|
|
8
|
+
const ctx = {
|
|
9
|
+
cwd: "/",
|
|
10
|
+
globalFlags: { llm: false, format: "text", verbose: false },
|
|
11
|
+
store: {},
|
|
12
|
+
} as unknown as PragmaContext;
|
|
13
|
+
expect(requirePragmaContext(ctx)).toBe(ctx);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("throws for a context without the runtime", () => {
|
|
17
|
+
const bare = {
|
|
18
|
+
cwd: "/",
|
|
19
|
+
globalFlags: { llm: false, format: "text" as const, verbose: false },
|
|
20
|
+
};
|
|
21
|
+
expect(() => requirePragmaContext(bare)).toThrow(PragmaError);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CommandContext } from "@canonical/cli-core";
|
|
2
|
+
import { PragmaError } from "#error";
|
|
3
|
+
import type { PragmaContext } from "../context.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Narrow a cli-core `CommandContext` to the pragma context.
|
|
7
|
+
*
|
|
8
|
+
* Story completion callbacks receive the cli-core context type, but every
|
|
9
|
+
* context the pragma pipeline constructs is a `PragmaContext` (it carries
|
|
10
|
+
* the ke store and config). This asserts that invariant at runtime instead
|
|
11
|
+
* of casting silently, failing fast if a foreign context ever appears.
|
|
12
|
+
*/
|
|
13
|
+
export default function requirePragmaContext(
|
|
14
|
+
ctx: CommandContext,
|
|
15
|
+
): PragmaContext {
|
|
16
|
+
if (!("store" in ctx)) {
|
|
17
|
+
throw PragmaError.internalError(
|
|
18
|
+
"completion context is missing the pragma runtime",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
// Proven one line up: a context carrying `store` is a PragmaContext.
|
|
22
|
+
return ctx as PragmaContext;
|
|
23
|
+
}
|