@canonical/pragma-cli 0.30.0 → 0.31.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.
Files changed (254) hide show
  1. package/README.md +64 -35
  2. package/dist/pragma +0 -0
  3. package/package.json +15 -13
  4. package/src/completions/constants.ts +7 -0
  5. package/src/completions/handleQuery.test.ts +32 -0
  6. package/src/completions/handleQuery.ts +20 -1
  7. package/src/completions/queryCompletions.test.ts +5 -0
  8. package/src/completions/queryCompletions.ts +17 -3
  9. package/src/completions/querySocket.test.ts +31 -0
  10. package/src/completions/querySocket.ts +36 -9
  11. package/src/config/parseConfigValues.ts +2 -0
  12. package/src/config/readConfigLayers.test.ts +2 -0
  13. package/src/config/readConfigLayers.ts +3 -0
  14. package/src/config/types.ts +7 -0
  15. package/src/constants.ts +3 -1
  16. package/src/domains/block/commands/sample.ts +2 -1
  17. package/src/domains/block/formatters/list.test.ts +3 -3
  18. package/src/domains/block/formatters/lookup.test.ts +5 -5
  19. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  20. package/src/domains/block/stories.ts +9 -3
  21. package/src/domains/config/commands/channel.test.ts +16 -0
  22. package/src/domains/config/commands/channel.ts +14 -1
  23. package/src/domains/config/commands/framework.test.ts +14 -0
  24. package/src/domains/config/commands/framework.ts +13 -1
  25. package/src/domains/config/commands/tier.test.ts +7 -0
  26. package/src/domains/config/commands/tier.ts +14 -1
  27. package/src/domains/config/operations/validateTier.test.ts +9 -0
  28. package/src/domains/config/operations/validateTier.ts +10 -2
  29. package/src/domains/create/generatorPack/bundled.ts +49 -0
  30. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  31. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  32. package/src/domains/create/index.ts +13 -7
  33. package/src/domains/create/mcp/specs.ts +6 -161
  34. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  35. package/src/domains/create/renderGeneratorUi.ts +65 -0
  36. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  37. package/src/domains/doctor/formatters/doctor.ts +90 -38
  38. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
  39. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
  40. package/src/domains/doctor/operations/checks.test.ts +6 -4
  41. package/src/domains/doctor/operations/types.ts +23 -1
  42. package/src/domains/graph/commands/inspect.test.ts +2 -2
  43. package/src/domains/graph/commands/query.test.ts +5 -3
  44. package/src/domains/graph/formatters/inspect.ts +7 -7
  45. package/src/domains/graph/formatters/query.test.ts +87 -0
  46. package/src/domains/graph/formatters/query.ts +42 -5
  47. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  48. package/src/domains/graphql/commands/serve.test.ts +12 -6
  49. package/src/domains/graphql/commands/serve.ts +16 -6
  50. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  51. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  52. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  53. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  54. package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
  55. package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
  56. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  57. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  58. package/src/domains/graphql/operations/index.ts +5 -0
  59. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  60. package/src/domains/info/formatters/upgrade.ts +3 -1
  61. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  62. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  63. package/src/domains/info/operations/index.ts +1 -0
  64. package/src/domains/info/types.ts +13 -0
  65. package/src/domains/llm/capabilities.test.ts +23 -3
  66. package/src/domains/llm/commands/capabilities.ts +12 -3
  67. package/src/domains/llm/commands/llm.ts +5 -1
  68. package/src/domains/llm/data/commandReference.ts +20 -11
  69. package/src/domains/llm/data/decisionTrees.ts +10 -5
  70. package/src/domains/llm/data/toolCatalog.ts +49 -18
  71. package/src/domains/llm/formatters/orientation.ts +5 -1
  72. package/src/domains/llm/llmCommand.test.ts +11 -3
  73. package/src/domains/llm/mcp/specs.ts +3 -4
  74. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  75. package/src/domains/llm/operations/collectContext.ts +7 -17
  76. package/src/domains/modifier/commands/sample.ts +2 -1
  77. package/src/domains/modifier/operations/lookup.ts +5 -3
  78. package/src/domains/modifier/stories.ts +9 -0
  79. package/src/domains/ontology/commands/list.test.ts +5 -3
  80. package/src/domains/ontology/commands/show.test.ts +85 -9
  81. package/src/domains/ontology/formatters/index.ts +1 -0
  82. package/src/domains/ontology/formatters/list.ts +37 -12
  83. package/src/domains/ontology/formatters/show.ts +358 -67
  84. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  85. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  86. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  87. package/src/domains/ontology/helpers/index.ts +14 -0
  88. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  89. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  90. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  91. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  92. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  93. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  94. package/src/domains/ontology/operations/showOntology.ts +289 -7
  95. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  96. package/src/domains/ontology/stories.ts +53 -8
  97. package/src/domains/refs/operations/updateRefs.ts +0 -1
  98. package/src/domains/setup/commands/index.ts +1 -0
  99. package/src/domains/setup/commands/setup.ts +70 -0
  100. package/src/domains/setup/index.ts +7 -3
  101. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  102. package/src/domains/setup/operations/setupLsp.ts +7 -2
  103. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  104. package/src/domains/shared/bootStore.test.ts +82 -0
  105. package/src/domains/shared/bootStore.ts +56 -20
  106. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  107. package/src/domains/shared/extractLocalName.test.ts +3 -3
  108. package/src/domains/shared/formatters.ts +23 -2
  109. package/src/domains/shared/listTiers.ts +40 -0
  110. package/src/domains/shared/loaders/bundledLoader.ts +48 -0
  111. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  112. package/src/domains/shared/loaders/localLoader.ts +2 -18
  113. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  114. package/src/domains/shared/loaders/readPackageDir.ts +46 -8
  115. package/src/domains/shared/lookupMany.test.ts +15 -0
  116. package/src/domains/shared/lookupMany.ts +22 -6
  117. package/src/domains/shared/namespaces.ts +13 -5
  118. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  119. package/src/domains/shared/parseSampleCount.ts +45 -0
  120. package/src/domains/shared/prefixes.test.ts +211 -0
  121. package/src/domains/shared/prefixes.ts +182 -15
  122. package/src/domains/shared/renderers.ts +5 -5
  123. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  124. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  125. package/src/domains/shared/semanticPackage.test.ts +89 -0
  126. package/src/domains/shared/semanticPackage.ts +17 -2
  127. package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
  128. package/src/domains/shared/stories/compileLookupTool.ts +1 -1
  129. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
  130. package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
  131. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  132. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  133. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
  134. package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
  135. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  136. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  137. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  138. package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
  139. package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
  140. package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
  141. package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
  142. package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
  143. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
  144. package/src/domains/shared/stories/pack/index.ts +14 -0
  145. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  146. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  147. package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
  148. package/src/domains/shared/stories/pack/types.ts +123 -3
  149. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
  150. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
  151. package/src/domains/shared/stories/types.ts +9 -1
  152. package/src/domains/shared/types/index.ts +5 -9
  153. package/src/domains/shared/types/ontology.ts +119 -24
  154. package/src/domains/shared/types/standard.ts +9 -52
  155. package/src/domains/token/commands/lookup.test.ts +8 -0
  156. package/src/domains/token/commands/lookup.ts +12 -0
  157. package/src/domains/token/commands/sample.ts +2 -1
  158. package/src/domains/token/featureFlag.ts +17 -0
  159. package/src/domains/token/index.ts +5 -0
  160. package/src/domains/token/mcp/specs.ts +13 -1
  161. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  162. package/src/domains/trace/commands/show.ts +30 -4
  163. package/src/index.ts +1 -13
  164. package/src/mcp/registerResources.test.ts +2 -2
  165. package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
  166. package/src/mcp/tools/index.ts +18 -10
  167. package/src/mcp/tools/registerTools.test.ts +106 -30
  168. package/src/pipeline/collectCommands.test.ts +151 -1
  169. package/src/pipeline/collectCommands.ts +34 -19
  170. package/src/pipeline/createProgram.test.ts +5 -1
  171. package/src/pipeline/createProgram.ts +2 -2
  172. package/src/pipeline/firstRun.test.ts +80 -0
  173. package/src/pipeline/firstRun.ts +99 -0
  174. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  175. package/src/pipeline/parseGlobalFlags.ts +98 -9
  176. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  177. package/src/pipeline/resolveCommandKind.ts +4 -0
  178. package/src/pipeline/rootHelp.test.ts +87 -0
  179. package/src/pipeline/rootHelp.ts +265 -0
  180. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  181. package/src/pipeline/runCli.test.ts +17 -4
  182. package/src/pipeline/runCli.ts +162 -19
  183. package/src/testing/cli.ts +30 -2
  184. package/src/testing/dsFixtures.ts +21 -4
  185. package/src/testing/index.ts +1 -4
  186. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
  187. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  188. package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
  189. package/src/testing/integration/edge-cases.test.ts +62 -50
  190. package/src/testing/integration/executor-execution.test.ts +6 -4
  191. package/src/testing/integration/executor-golden.test.ts +2 -2
  192. package/src/testing/integration/mcp-surface.test.ts +40 -15
  193. package/src/testing/integration/parity.test.ts +119 -70
  194. package/src/testing/setupXdgIsolation.ts +16 -5
  195. package/src/testing/standardParityFixtures.ts +26 -83
  196. package/src/testing/store.ts +4 -4
  197. package/src/domains/create/commands/component.test.ts +0 -166
  198. package/src/domains/create/commands/component.ts +0 -123
  199. package/src/domains/create/commands/index.ts +0 -3
  200. package/src/domains/create/commands/package.test.ts +0 -35
  201. package/src/domains/create/commands/package.ts +0 -21
  202. package/src/domains/create/generators.ts +0 -8
  203. package/src/domains/standard/commands/categories.test.ts +0 -70
  204. package/src/domains/standard/commands/categories.ts +0 -48
  205. package/src/domains/standard/commands/index.ts +0 -6
  206. package/src/domains/standard/commands/list.test.ts +0 -105
  207. package/src/domains/standard/commands/list.ts +0 -11
  208. package/src/domains/standard/commands/lookup.test.ts +0 -135
  209. package/src/domains/standard/commands/lookup.ts +0 -14
  210. package/src/domains/standard/commands/sample.ts +0 -57
  211. package/src/domains/standard/formatters/categories.test.ts +0 -44
  212. package/src/domains/standard/formatters/categories.ts +0 -38
  213. package/src/domains/standard/formatters/index.ts +0 -10
  214. package/src/domains/standard/formatters/list.test.ts +0 -169
  215. package/src/domains/standard/formatters/list.ts +0 -184
  216. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  217. package/src/domains/standard/formatters/lookup.ts +0 -101
  218. package/src/domains/standard/formatters/sample.ts +0 -15
  219. package/src/domains/standard/formatters/types.ts +0 -31
  220. package/src/domains/standard/index.ts +0 -40
  221. package/src/domains/standard/mcp/index.ts +0 -2
  222. package/src/domains/standard/mcp/specs.ts +0 -89
  223. package/src/domains/standard/operations/categories.test.ts +0 -61
  224. package/src/domains/standard/operations/categories.ts +0 -38
  225. package/src/domains/standard/operations/index.ts +0 -6
  226. package/src/domains/standard/operations/list.test.ts +0 -75
  227. package/src/domains/standard/operations/list.ts +0 -64
  228. package/src/domains/standard/operations/lookup.test.ts +0 -58
  229. package/src/domains/standard/operations/lookup.ts +0 -151
  230. package/src/domains/standard/operations/sample.ts +0 -35
  231. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  232. package/src/domains/standard/orchestration/index.ts +0 -4
  233. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  234. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
  235. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  236. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  237. package/src/domains/standard/standardConfig.ts +0 -42
  238. package/src/domains/standard/stories.test.ts +0 -24
  239. package/src/domains/standard/stories.ts +0 -151
  240. package/src/domains/standard/types.ts +0 -15
  241. package/src/domains/tier/commands/index.ts +0 -3
  242. package/src/domains/tier/commands/list.test.ts +0 -67
  243. package/src/domains/tier/commands/list.ts +0 -9
  244. package/src/domains/tier/formatters/index.ts +0 -3
  245. package/src/domains/tier/formatters/list.test.ts +0 -33
  246. package/src/domains/tier/formatters/list.ts +0 -45
  247. package/src/domains/tier/index.ts +0 -17
  248. package/src/domains/tier/mcp/index.ts +0 -2
  249. package/src/domains/tier/mcp/specs.ts +0 -12
  250. package/src/domains/tier/operations/index.ts +0 -3
  251. package/src/domains/tier/operations/list.test.ts +0 -32
  252. package/src/domains/tier/operations/list.ts +0 -33
  253. package/src/domains/tier/stories.test.ts +0 -22
  254. package/src/domains/tier/stories.ts +0 -52
@@ -1,11 +1,11 @@
1
1
  import type { URI } from "@canonical/ke";
2
2
  import { describe, expect, it } from "vitest";
3
- import { PREFIX_MAP } from "../../shared/prefixes.js";
3
+ import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
4
4
  import type { BlockSummary } from "../../shared/types/index.js";
5
5
  import formatters from "./list.js";
6
6
 
7
7
  const BUTTON: BlockSummary = {
8
- uri: `${PREFIX_MAP.ds}button` as URI,
8
+ uri: `${DEFAULT_PREFIX_MAP.ds}button` as URI,
9
9
  name: "Button",
10
10
  tier: "global",
11
11
  modifiers: ["importance", "density"],
@@ -18,7 +18,7 @@ const BUTTON: BlockSummary = {
18
18
  };
19
19
 
20
20
  const CARD: BlockSummary = {
21
- uri: `${PREFIX_MAP.ds}card` as URI,
21
+ uri: `${DEFAULT_PREFIX_MAP.ds}card` as URI,
22
22
  name: "Card",
23
23
  tier: "global",
24
24
  modifiers: [],
@@ -1,12 +1,12 @@
1
1
  import type { URI } from "@canonical/ke";
2
2
  import { describe, expect, it } from "vitest";
3
- import { PREFIX_MAP } from "../../shared/prefixes.js";
3
+ import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
4
4
  import type { BlockDetailed } from "../../shared/types/index.js";
5
5
  import type { AspectFlags } from "../types.js";
6
6
  import formatters from "./lookup.js";
7
7
 
8
8
  const BUTTON_DETAILED: BlockDetailed = {
9
- uri: `${PREFIX_MAP.ds}global.component.button` as URI,
9
+ uri: `${DEFAULT_PREFIX_MAP.ds}global.component.button` as URI,
10
10
  name: "Button",
11
11
  type: "component",
12
12
  tier: "global",
@@ -40,7 +40,7 @@ const BUTTON_DETAILED: BlockDetailed = {
40
40
  ],
41
41
  modifierFamilies: [
42
42
  {
43
- uri: `${PREFIX_MAP.ds}modifier_family.importance` as URI,
43
+ uri: `${DEFAULT_PREFIX_MAP.ds}modifier_family.importance` as URI,
44
44
  name: "importance",
45
45
  values: ["default", "primary", "secondary"],
46
46
  },
@@ -56,7 +56,7 @@ const BUTTON_DETAILED: BlockDetailed = {
56
56
  ],
57
57
  subcomponents: [
58
58
  {
59
- uri: `${PREFIX_MAP.ds}global.subcomponent.button.icon` as URI,
59
+ uri: `${DEFAULT_PREFIX_MAP.ds}global.subcomponent.button.icon` as URI,
60
60
  name: "Button Icon",
61
61
  },
62
62
  ],
@@ -65,7 +65,7 @@ const BUTTON_DETAILED: BlockDetailed = {
65
65
  ],
66
66
  tokens: [
67
67
  {
68
- uri: `${PREFIX_MAP.ds}token.color.primary` as URI,
68
+ uri: `${DEFAULT_PREFIX_MAP.ds}token.color.primary` as URI,
69
69
  name: "color.primary",
70
70
  },
71
71
  ],
@@ -14,12 +14,15 @@ export default async function enrichBlocks(
14
14
  filters: FilterConfig,
15
15
  disclosure: Disclosure,
16
16
  ): Promise<readonly (BlockListDigest | BlockDetailed)[]> {
17
+ // Look up by each summary's stable unique URI, not its display name: names
18
+ // recur across tiers/packages, so a by-name lookup returns every same-named
19
+ // block and taking the first match collapses distinct blocks onto one.
17
20
  const nested = await Promise.all(
18
- summaries.map((summary) => lookupBlock(store, summary.name, filters)),
21
+ summaries.map((summary) => lookupBlock(store, summary.uri, filters)),
19
22
  );
20
- // lookupBlock returns BlockDetailed[] — take the first match per summary
23
+ // lookupBlock returns BlockDetailed[] — the URI resolves to exactly one match
21
24
  const details: BlockDetailed[] = nested.flatMap((matches) =>
22
- matches[0] ? [matches[0]] : [],
25
+ matches.at(0) ? [matches[0] as BlockDetailed] : [],
23
26
  );
24
27
 
25
28
  if (disclosure.level === "detailed") {
@@ -113,9 +113,15 @@ export const blockLookupStory: LookupStory<BlockDetailed, BlockLookupInput> = {
113
113
  complete: async (partial, cmdCtx) => {
114
114
  const ctx = requirePragmaContext(cmdCtx);
115
115
  const all = await listBlocks(ctx.store, ctx.config);
116
- return all
117
- .map((block) => block.name)
118
- .filter((name) => name.toLowerCase().startsWith(partial.toLowerCase()));
116
+ const lower = partial.toLowerCase();
117
+ // Block names recur across tiers/packages; dedupe so completion offers each
118
+ // name once.
119
+ const unique = new Set(
120
+ all
121
+ .map((block) => block.name)
122
+ .filter((name) => name.toLowerCase().startsWith(lower)),
123
+ );
124
+ return [...unique];
119
125
  },
120
126
  detailedParam: {
121
127
  description:
@@ -60,6 +60,22 @@ describe("config channel command", () => {
60
60
  }
61
61
  });
62
62
 
63
+ it("rejects a value combined with --reset", async () => {
64
+ const ctx = makeCtx(dir);
65
+ const cmd = buildChannelCommand(ctx);
66
+ await expect(
67
+ cmd.execute({ value: "experimental", reset: true }, ctx),
68
+ ).rejects.toMatchObject({ code: "INVALID_INPUT" });
69
+ });
70
+
71
+ it("rejects an explicit empty-string value", async () => {
72
+ const ctx = makeCtx(dir);
73
+ const cmd = buildChannelCommand(ctx);
74
+ await expect(cmd.execute({ value: "" }, ctx)).rejects.toMatchObject({
75
+ code: "INVALID_INPUT",
76
+ });
77
+ });
78
+
63
79
  it("resets channel via --reset flag", async () => {
64
80
  writeFileSync(
65
81
  join(dir, "pragma.config.json"),
@@ -5,6 +5,7 @@ import {
5
5
  } from "@canonical/cli-core";
6
6
  import { runTask } from "@canonical/task/node";
7
7
  import { readConfig } from "#config";
8
+ import { PragmaError } from "#error";
8
9
  import type { PragmaContext } from "../../shared/context.js";
9
10
  import { selectFormatter } from "../../shared/formatters.js";
10
11
  import { channelFormatters } from "../formatters/index.js";
@@ -56,6 +57,16 @@ export default function buildChannelCommand(
56
57
  const reset = params.reset === true;
57
58
  const value = params.value as string | undefined;
58
59
 
60
+ // Contradictory input: a value AND --reset. Reject rather than silently
61
+ // letting one win.
62
+ if (reset && value !== undefined) {
63
+ throw PragmaError.invalidInput("channel", value, {
64
+ recovery: {
65
+ message: "Pass either a channel value or --reset, not both.",
66
+ },
67
+ });
68
+ }
69
+
59
70
  if (reset) {
60
71
  const result = await runTask(setChannelTask(ctx.cwd, undefined, scope));
61
72
  const format = selectFormatter(ctx, channelFormatters.reset);
@@ -65,7 +76,9 @@ export default function buildChannelCommand(
65
76
  );
66
77
  }
67
78
 
68
- if (!value) {
79
+ // No value at all → query the current channel. An explicit empty string
80
+ // is invalid input, not a query.
81
+ if (value === undefined) {
69
82
  const config = readConfig(ctx.cwd);
70
83
  const format = selectFormatter(ctx, channelFormatters.query);
71
84
  return createOutputResult(config.channel, {
@@ -62,6 +62,20 @@ describe("config framework command", () => {
62
62
  expect(readConfig(dir).framework).toBe("lit");
63
63
  });
64
64
 
65
+ it("rejects a value combined with --reset", async () => {
66
+ const ctx = makeCtx(dir);
67
+ await expect(
68
+ buildFrameworkCommand(ctx).execute({ value: "react", reset: true }, ctx),
69
+ ).rejects.toMatchObject({ code: "INVALID_INPUT" });
70
+ });
71
+
72
+ it("rejects an explicit empty-string value", async () => {
73
+ const ctx = makeCtx(dir);
74
+ await expect(
75
+ buildFrameworkCommand(ctx).execute({ value: "" }, ctx),
76
+ ).rejects.toMatchObject({ code: "INVALID_INPUT" });
77
+ });
78
+
65
79
  it("rejects an invalid framework", async () => {
66
80
  const ctx = makeCtx(dir);
67
81
  await expect(
@@ -5,6 +5,7 @@ import {
5
5
  } from "@canonical/cli-core";
6
6
  import { runTask } from "@canonical/task/node";
7
7
  import { readConfig } from "#config";
8
+ import { PragmaError } from "#error";
8
9
  import type { PragmaContext } from "../../shared/context.js";
9
10
  import { selectFormatter } from "../../shared/formatters.js";
10
11
  import { frameworkFormatters } from "../formatters/index.js";
@@ -59,6 +60,16 @@ export default function buildFrameworkCommand(
59
60
  const reset = params.reset === true;
60
61
  const value = params.value as string | undefined;
61
62
 
63
+ // Contradictory input: a value AND --reset. Reject rather than silently
64
+ // letting one win.
65
+ if (reset && value !== undefined) {
66
+ throw PragmaError.invalidInput("framework", value, {
67
+ recovery: {
68
+ message: "Pass either a framework value or --reset, not both.",
69
+ },
70
+ });
71
+ }
72
+
62
73
  if (reset) {
63
74
  const result = await runTask(
64
75
  setFrameworkTask(ctx.cwd, undefined, scope),
@@ -70,7 +81,8 @@ export default function buildFrameworkCommand(
70
81
  );
71
82
  }
72
83
 
73
- if (!value) {
84
+ // No value at all → query. An explicit empty string is invalid input.
85
+ if (value === undefined) {
74
86
  const config = readConfig(ctx.cwd);
75
87
  const format = selectFormatter(ctx, frameworkFormatters.query);
76
88
  return createOutputResult(config.framework ?? "none", {
@@ -92,4 +92,11 @@ describe("config tier command", () => {
92
92
  expect(text).toBe("No tier set (all tiers visible).");
93
93
  }
94
94
  });
95
+
96
+ it("rejects a tier path combined with --reset", async () => {
97
+ const ctx = makeCtx(dir);
98
+ await expect(
99
+ buildTierCommand(ctx).execute({ path: "apps/lxd", reset: true }, ctx),
100
+ ).rejects.toMatchObject({ code: "INVALID_INPUT" });
101
+ });
95
102
  });
@@ -5,6 +5,7 @@ import {
5
5
  } from "@canonical/cli-core";
6
6
  import { runTask } from "@canonical/task/node";
7
7
  import { readConfig } from "#config";
8
+ import { PragmaError } from "#error";
8
9
  import type { PragmaContext } from "../../shared/context.js";
9
10
  import { selectFormatter } from "../../shared/formatters.js";
10
11
  import { tierFormatters } from "../formatters/index.js";
@@ -53,6 +54,16 @@ export default function buildTierCommand(
53
54
  const reset = params.reset === true;
54
55
  const tierPath = params.path as string | undefined;
55
56
 
57
+ // Contradictory input: a tier path AND --reset. Reject rather than
58
+ // silently letting one win.
59
+ if (reset && tierPath !== undefined) {
60
+ throw PragmaError.invalidInput("tier", tierPath, {
61
+ recovery: {
62
+ message: "Pass either a tier path or --reset, not both.",
63
+ },
64
+ });
65
+ }
66
+
56
67
  if (reset) {
57
68
  const result = await runTask(setTierTask(ctx.cwd, undefined, scope));
58
69
  const format = selectFormatter(ctx, tierFormatters.reset);
@@ -62,7 +73,9 @@ export default function buildTierCommand(
62
73
  );
63
74
  }
64
75
 
65
- if (!tierPath) {
76
+ // No path at all → query. An explicit empty string is invalid input and
77
+ // is rejected by validateTier below.
78
+ if (tierPath === undefined) {
66
79
  const config = readConfig(ctx.cwd);
67
80
  const format = selectFormatter(ctx, tierFormatters.query);
68
81
  return createOutputResult(config.tier, {
@@ -30,6 +30,15 @@ describe("validateTier", () => {
30
30
  expect(entry.path).toBe("global");
31
31
  });
32
32
 
33
+ it("matches case- and whitespace-insensitively", async () => {
34
+ // The documented lowercase slash-path must match regardless of the label's
35
+ // casing in the ontology (e.g. real graph uses `Apps/LXD`).
36
+ const upper = await validateTier(store, "APPS/LXD");
37
+ expect(upper.path).toBe("apps/lxd");
38
+ const padded = await validateTier(store, " Apps/Lxd ");
39
+ expect(padded.path).toBe("apps/lxd");
40
+ });
41
+
33
42
  it("throws INVALID_INPUT for a nonexistent tier", async () => {
34
43
  await expect(validateTier(store, "apps/nonexistent")).rejects.toThrow(
35
44
  'Invalid tier "apps/nonexistent"',
@@ -1,7 +1,12 @@
1
1
  import type { Store } from "@canonical/ke";
2
2
  import { PragmaError } from "#error";
3
+ import { listTiers } from "../../shared/listTiers.js";
3
4
  import type { TierEntry } from "../../shared/types/index.js";
4
- import { listTiers } from "../../tier/operations/index.js";
5
+
6
+ /** Normalise a tier path for comparison: trim and lowercase. */
7
+ function normalizeTierPath(path: string): string {
8
+ return path.trim().toLowerCase();
9
+ }
5
10
 
6
11
  /**
7
12
  * Validate a tier path against the ke store ontology.
@@ -17,7 +22,10 @@ export default async function validateTier(
17
22
  tierPath: string,
18
23
  ): Promise<TierEntry> {
19
24
  const tiers = await listTiers(store);
20
- const match = tiers.find((t) => t.path === tierPath);
25
+ // Compare case/format-insensitively so the documented lowercase slash-path
26
+ // (`apps/lxd`) matches the ontology's title-case label (`Apps/LXD`).
27
+ const target = normalizeTierPath(tierPath);
28
+ const match = tiers.find((t) => normalizeTierPath(t.path) === target);
21
29
 
22
30
  if (!match) {
23
31
  const validPaths = tiers.map((t) => t.path);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Bundled generator packs.
3
+ *
4
+ * The generator sets pragma ships by default, statically imported so the
5
+ * single-file build embeds them. Each package's full generator set is exposed —
6
+ * every generator, always — grouped into `create <noun>` commands and
7
+ * `create_<noun>` MCP tools by {@link compileGeneratorPack}.
8
+ *
9
+ * This is the write-side counterpart of the bundled story packs. In a later
10
+ * slice these move into the packages themselves (declared, dynamically
11
+ * resolved) so the core ships no generator wiring at all; today they are
12
+ * bundled transitional entries, like the story packs.
13
+ */
14
+
15
+ import { generators as applicationGenerators } from "@canonical/summon-application";
16
+ import { generators as componentGenerators } from "@canonical/summon-component";
17
+ import { generators as packageGenerators } from "@canonical/summon-package";
18
+ import compileGeneratorPack, {
19
+ type CompiledGeneratorPack,
20
+ type GeneratorSet,
21
+ } from "./compileGeneratorPack.js";
22
+
23
+ /** Every generator set pragma bundles, in surface order. */
24
+ const BUNDLED_GENERATOR_SETS: readonly GeneratorSet[] = [
25
+ componentGenerators,
26
+ packageGenerators,
27
+ applicationGenerators,
28
+ ];
29
+
30
+ let cached: CompiledGeneratorPack | undefined;
31
+
32
+ /**
33
+ * Compile every bundled generator pack into its CLI commands and MCP specs.
34
+ *
35
+ * Memoized — the generator sets are static, and both the CLI and MCP
36
+ * registration paths call this.
37
+ *
38
+ * @returns The merged commands and specs across all bundled packs.
39
+ */
40
+ export default function bundledGeneratorPacks(): CompiledGeneratorPack {
41
+ cached ??= BUNDLED_GENERATOR_SETS.map(compileGeneratorPack).reduce(
42
+ (merged, pack) => ({
43
+ commands: [...merged.commands, ...pack.commands],
44
+ specs: [...merged.specs, ...pack.specs],
45
+ }),
46
+ { commands: [], specs: [] },
47
+ );
48
+ return cached;
49
+ }
@@ -0,0 +1,159 @@
1
+ import type { AnyGenerator } from "@canonical/summon-core";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { PragmaRuntime } from "../../shared/types/index.js";
4
+ import bundledGeneratorPacks from "./bundled.js";
5
+ import compileGeneratorPack from "./compileGeneratorPack.js";
6
+
7
+ describe("compileGeneratorPack (bundled)", () => {
8
+ const { commands, specs } = bundledGeneratorPacks();
9
+ const cliPaths = commands.map((c) => c.path.join(" "));
10
+ const toolNames = specs.map((s) => s.name);
11
+
12
+ it("exposes every generator from every bundled package as a create command", () => {
13
+ // component (react/svelte/lit → one noun), package, and the four
14
+ // application-package generators.
15
+ expect(cliPaths).toEqual([
16
+ "create component",
17
+ "create package",
18
+ "create application",
19
+ "create domain",
20
+ "create route",
21
+ "create wrapper",
22
+ ]);
23
+ });
24
+
25
+ it("emits a matching MCP tool per noun", () => {
26
+ expect(toolNames).toEqual([
27
+ "create_component",
28
+ "create_package",
29
+ "create_application",
30
+ "create_domain",
31
+ "create_route",
32
+ "create_wrapper",
33
+ ]);
34
+ });
35
+
36
+ it("dispatches a multi-variant noun via a `variant` positional select", () => {
37
+ const component = commands.find(
38
+ (c) => c.path.join(" ") === "create component",
39
+ );
40
+ const variant = component?.parameters.find((p) => p.name === "variant");
41
+ expect(variant?.type).toBe("select");
42
+ expect(variant?.positional).toBe(true);
43
+ expect(variant?.choices?.map((ch) => ch.value)).toEqual([
44
+ "react",
45
+ "svelte",
46
+ "lit",
47
+ ]);
48
+ });
49
+
50
+ it("gives the multi-variant MCP tool a `variant` enum parameter", () => {
51
+ const component = specs.find((s) => s.name === "create_component");
52
+ expect(component?.params?.variant?.enum).toEqual([
53
+ "react",
54
+ "svelte",
55
+ "lit",
56
+ ]);
57
+ });
58
+
59
+ it("collapses a single-variant noun to a plain `create <noun>` (no variant arg)", () => {
60
+ const application = commands.find(
61
+ (c) => c.path.join(" ") === "create application",
62
+ );
63
+ expect(application?.parameters.some((p) => p.name === "variant")).toBe(
64
+ false,
65
+ );
66
+ // Its prompts still project — appPath comes from the generator.
67
+ expect(application?.parameters.some((p) => p.name === "appPath")).toBe(
68
+ true,
69
+ );
70
+ });
71
+
72
+ it("derives single-generator MCP params from the generator's prompts", () => {
73
+ const pkg = specs.find((s) => s.name === "create_package");
74
+ expect(pkg?.params).toBeDefined();
75
+ // The package generator prompts for a package name.
76
+ expect(Object.keys(pkg?.params ?? {}).length).toBeGreaterThan(0);
77
+ });
78
+
79
+ it("never advertises params the runner pins (runInstall is forced off)", () => {
80
+ // The runner always executes a dry-run plan with runInstall: false, so
81
+ // no create tool may advertise the parameter it would ignore.
82
+ for (const spec of specs) {
83
+ expect(spec.params).not.toHaveProperty("runInstall");
84
+ }
85
+ });
86
+ });
87
+
88
+ /** Minimal synthetic generator for edge-case tests. */
89
+ function makeGenerator(name: string, generate?: () => never): AnyGenerator {
90
+ return {
91
+ meta: {
92
+ name,
93
+ displayName: name,
94
+ description: `Test generator ${name}`,
95
+ version: "0.0.0",
96
+ },
97
+ prompts: [],
98
+ generate:
99
+ generate ??
100
+ (() => {
101
+ throw new Error("not exercised");
102
+ }),
103
+ } as unknown as AnyGenerator;
104
+ }
105
+
106
+ describe("compileGeneratorPack — edge cases", () => {
107
+ let warnings: string[];
108
+ let warn: ReturnType<typeof vi.spyOn>;
109
+
110
+ beforeEach(() => {
111
+ warnings = [];
112
+ warn = vi
113
+ .spyOn(process.stderr, "write")
114
+ .mockImplementation((chunk: string | Uint8Array) => {
115
+ warnings.push(String(chunk));
116
+ return true;
117
+ });
118
+ });
119
+
120
+ afterEach(() => {
121
+ warn.mockRestore();
122
+ });
123
+
124
+ it("warns and exposes only the first generator of a mixed bare/variant noun", () => {
125
+ const { commands, specs } = compileGeneratorPack({
126
+ thing: makeGenerator("thing"),
127
+ "thing/extra": makeGenerator("thing/extra"),
128
+ });
129
+ expect(commands.map((c) => c.path.join(" "))).toEqual(["create thing"]);
130
+ expect(specs.map((s) => s.name)).toEqual(["create_thing"]);
131
+ expect(warnings.join("")).toContain("mixes bare and variant keys");
132
+ });
133
+
134
+ it("maps a generator that throws to INVALID_INPUT with its message as recovery", async () => {
135
+ const throwing = makeGenerator("boom", () => {
136
+ throw new Error("boom requires --both flags");
137
+ });
138
+ const { specs } = compileGeneratorPack({ boom: throwing });
139
+ const spec = specs.at(0);
140
+ const rt = { cwd: "/tmp" } as PragmaRuntime;
141
+ await expect(spec?.execute(rt, {})).rejects.toMatchObject({
142
+ code: "INVALID_INPUT",
143
+ recovery: { message: "boom requires --both flags" },
144
+ });
145
+ });
146
+
147
+ it("rejects an unknown variant with INVALID_INPUT and the valid options", async () => {
148
+ const { specs } = compileGeneratorPack({
149
+ "widget/a": makeGenerator("widget/a"),
150
+ "widget/b": makeGenerator("widget/b"),
151
+ });
152
+ const spec = specs.at(0);
153
+ const rt = { cwd: "/tmp" } as PragmaRuntime;
154
+ await expect(spec?.execute(rt, { variant: "c" })).rejects.toMatchObject({
155
+ code: "INVALID_INPUT",
156
+ validOptions: ["a", "b"],
157
+ });
158
+ });
159
+ });