@canonical/pragma-cli 0.29.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 (410) hide show
  1. package/README.md +128 -47
  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/defaultConfig.test.ts +33 -0
  12. package/src/config/defaultConfig.ts +59 -0
  13. package/src/config/defaults.json +17 -0
  14. package/src/config/findProjectConfigPath.test.ts +53 -0
  15. package/src/config/findProjectConfigPath.ts +36 -0
  16. package/src/config/index.ts +17 -2
  17. package/src/config/mergeConfigFileUpdate.ts +58 -0
  18. package/src/config/parseConfigValues.ts +217 -0
  19. package/src/config/readConfig.test.ts +104 -13
  20. package/src/config/readConfig.ts +12 -153
  21. package/src/config/readConfigLayers.test.ts +109 -0
  22. package/src/config/readConfigLayers.ts +118 -0
  23. package/src/config/resolveGlobalConfigPath.ts +13 -0
  24. package/src/config/resolveWriteConfigPath.test.ts +53 -0
  25. package/src/config/resolveWriteConfigPath.ts +31 -0
  26. package/src/config/types.ts +83 -1
  27. package/src/config/writeConfig.ts +25 -55
  28. package/src/constants.ts +3 -1
  29. package/src/domains/block/commands/list.ts +5 -90
  30. package/src/domains/block/commands/lookup.ts +8 -167
  31. package/src/domains/block/commands/sample.ts +2 -1
  32. package/src/domains/block/formatters/list.test.ts +3 -3
  33. package/src/domains/block/formatters/lookup.test.ts +5 -5
  34. package/src/domains/block/mcp/specs.ts +16 -196
  35. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  36. package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
  37. package/src/domains/block/stories.test.ts +28 -0
  38. package/src/domains/block/stories.ts +260 -0
  39. package/src/domains/config/commands/channel.test.ts +30 -3
  40. package/src/domains/config/commands/channel.ts +25 -8
  41. package/src/domains/config/commands/configScope.ts +46 -0
  42. package/src/domains/config/commands/framework.test.ts +32 -1
  43. package/src/domains/config/commands/framework.ts +26 -8
  44. package/src/domains/config/commands/show.ts +5 -45
  45. package/src/domains/config/commands/tier.test.ts +20 -2
  46. package/src/domains/config/commands/tier.ts +25 -8
  47. package/src/domains/config/commands/trace.test.ts +19 -2
  48. package/src/domains/config/commands/trace.ts +6 -3
  49. package/src/domains/config/formatters/channel.test.ts +20 -6
  50. package/src/domains/config/formatters/channel.ts +14 -11
  51. package/src/domains/config/formatters/framework.ts +14 -11
  52. package/src/domains/config/formatters/show.test.ts +27 -0
  53. package/src/domains/config/formatters/show.ts +27 -4
  54. package/src/domains/config/formatters/tier.test.ts +26 -10
  55. package/src/domains/config/formatters/tier.ts +19 -11
  56. package/src/domains/config/formatters/trace.ts +7 -7
  57. package/src/domains/config/mcp/specs.ts +33 -53
  58. package/src/domains/config/operations/show.test.ts +18 -0
  59. package/src/domains/config/operations/show.ts +7 -1
  60. package/src/domains/config/operations/types.ts +7 -0
  61. package/src/domains/config/operations/validateTier.test.ts +9 -0
  62. package/src/domains/config/operations/validateTier.ts +10 -2
  63. package/src/domains/config/stories.ts +42 -0
  64. package/src/domains/config/tasks/index.ts +1 -0
  65. package/src/domains/config/tasks/setChannel.ts +13 -28
  66. package/src/domains/config/tasks/setFramework.ts +11 -30
  67. package/src/domains/config/tasks/setTier.ts +10 -29
  68. package/src/domains/config/tasks/setTrace.ts +10 -28
  69. package/src/domains/config/tasks/writeConfigField.ts +41 -0
  70. package/src/domains/create/generatorPack/bundled.ts +49 -0
  71. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  72. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  73. package/src/domains/create/index.ts +13 -7
  74. package/src/domains/create/mcp/specs.ts +6 -161
  75. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  76. package/src/domains/create/renderGeneratorUi.ts +65 -0
  77. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  78. package/src/domains/doctor/formatters/doctor.ts +90 -38
  79. package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
  80. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +133 -0
  81. package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
  82. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +32 -26
  83. package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
  84. package/src/domains/doctor/operations/checks/index.ts +1 -0
  85. package/src/domains/doctor/operations/checks.test.ts +160 -7
  86. package/src/domains/doctor/operations/runChecks.test.ts +15 -6
  87. package/src/domains/doctor/operations/runChecks.ts +2 -0
  88. package/src/domains/doctor/operations/types.ts +23 -1
  89. package/src/domains/graph/commands/inspect.test.ts +2 -2
  90. package/src/domains/graph/commands/query.test.ts +5 -3
  91. package/src/domains/graph/formatters/inspect.ts +7 -7
  92. package/src/domains/graph/formatters/query.test.ts +87 -0
  93. package/src/domains/graph/formatters/query.ts +42 -5
  94. package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
  95. package/src/domains/graph/helpers/resolveUri.ts +28 -21
  96. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  97. package/src/domains/graphql/commands/build.test.ts +5 -3
  98. package/src/domains/graphql/commands/build.ts +8 -16
  99. package/src/domains/graphql/commands/check.test.ts +5 -3
  100. package/src/domains/graphql/commands/check.ts +7 -13
  101. package/src/domains/graphql/commands/index.ts +1 -0
  102. package/src/domains/graphql/commands/serve.test.ts +46 -0
  103. package/src/domains/graphql/commands/serve.ts +141 -0
  104. package/src/domains/graphql/constants.ts +10 -0
  105. package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
  106. package/src/domains/graphql/helpers/gatherSources.ts +58 -0
  107. package/src/domains/graphql/helpers/index.ts +2 -0
  108. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  109. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  110. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  111. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  112. package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
  113. package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
  114. package/src/domains/graphql/index.ts +9 -6
  115. package/src/domains/graphql/operations/compileSchema.test.ts +23 -47
  116. package/src/domains/graphql/operations/compileSchema.ts +23 -25
  117. package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
  118. package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
  119. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  120. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  121. package/src/domains/graphql/operations/index.ts +10 -0
  122. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  123. package/src/domains/info/formatters/upgrade.ts +3 -1
  124. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  125. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  126. package/src/domains/info/operations/collectInfo.test.ts +14 -3
  127. package/src/domains/info/operations/collectInfo.ts +12 -15
  128. package/src/domains/info/operations/index.ts +1 -0
  129. package/src/domains/info/types.ts +13 -0
  130. package/src/domains/llm/capabilities.test.ts +24 -4
  131. package/src/domains/llm/commands/capabilities.ts +12 -3
  132. package/src/domains/llm/commands/llm.ts +5 -1
  133. package/src/domains/llm/data/commandReference.ts +20 -11
  134. package/src/domains/llm/data/decisionTrees.ts +10 -5
  135. package/src/domains/llm/data/toolCatalog.ts +55 -18
  136. package/src/domains/llm/formatters/orientation.ts +5 -1
  137. package/src/domains/llm/llmCommand.test.ts +11 -3
  138. package/src/domains/llm/mcp/specs.ts +3 -4
  139. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  140. package/src/domains/llm/operations/collectContext.ts +7 -17
  141. package/src/domains/modifier/commands/list.ts +5 -38
  142. package/src/domains/modifier/commands/lookup.ts +8 -79
  143. package/src/domains/modifier/commands/sample.ts +2 -1
  144. package/src/domains/modifier/mcp/specs.ts +15 -94
  145. package/src/domains/modifier/operations/lookup.ts +5 -3
  146. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
  147. package/src/domains/modifier/stories.test.ts +20 -0
  148. package/src/domains/modifier/stories.ts +86 -0
  149. package/src/domains/ontology/commands/list.test.ts +5 -3
  150. package/src/domains/ontology/commands/list.ts +5 -58
  151. package/src/domains/ontology/commands/show.test.ts +85 -9
  152. package/src/domains/ontology/commands/show.ts +6 -58
  153. package/src/domains/ontology/formatters/index.ts +1 -0
  154. package/src/domains/ontology/formatters/list.ts +37 -12
  155. package/src/domains/ontology/formatters/show.ts +361 -64
  156. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  157. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  158. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  159. package/src/domains/ontology/helpers/index.ts +14 -0
  160. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  161. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  162. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  163. package/src/domains/ontology/mcp/specs.ts +7 -84
  164. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  165. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  166. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  167. package/src/domains/ontology/operations/showOntology.ts +289 -7
  168. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  169. package/src/domains/ontology/stories.test.ts +36 -0
  170. package/src/domains/ontology/stories.ts +153 -0
  171. package/src/domains/refs/operations/updateRefs.ts +2 -38
  172. package/src/domains/setup/commands/index.ts +1 -0
  173. package/src/domains/setup/commands/setup.ts +70 -0
  174. package/src/domains/setup/commands/skills.test.ts +6 -2
  175. package/src/domains/setup/commands/skills.ts +6 -2
  176. package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
  177. package/src/domains/setup/helpers/runSetupTask.ts +62 -70
  178. package/src/domains/setup/index.ts +7 -3
  179. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  180. package/src/domains/setup/operations/setupLsp.ts +7 -2
  181. package/src/domains/setup/operations/setupMcp.test.ts +6 -1
  182. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  183. package/src/domains/shared/bootStore.test.ts +82 -0
  184. package/src/domains/shared/bootStore.ts +57 -18
  185. package/src/domains/shared/compactUri.test.ts +11 -0
  186. package/src/domains/shared/compactUri.ts +10 -5
  187. package/src/domains/shared/context.ts +2 -2
  188. package/src/domains/shared/contracts.ts +11 -0
  189. package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
  190. package/src/domains/shared/createInteractivePromptSession.ts +236 -0
  191. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  192. package/src/domains/shared/extractLocalName.test.ts +3 -3
  193. package/src/domains/shared/formatters.ts +23 -2
  194. package/src/domains/shared/listTiers.ts +40 -0
  195. package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
  196. package/src/domains/shared/loaders/bundledLoader.ts +158 -12
  197. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  198. package/src/domains/shared/loaders/index.ts +4 -1
  199. package/src/domains/shared/loaders/localLoader.ts +2 -4
  200. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  201. package/src/domains/shared/loaders/readPackageDir.ts +82 -9
  202. package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
  203. package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
  204. package/src/domains/shared/lookupMany.test.ts +135 -0
  205. package/src/domains/shared/lookupMany.ts +50 -2
  206. package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
  207. package/src/domains/shared/mergeAndParseRefs.ts +44 -0
  208. package/src/domains/shared/namespaces.ts +13 -5
  209. package/src/domains/shared/packages.ts +10 -17
  210. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  211. package/src/domains/shared/parseSampleCount.ts +45 -0
  212. package/src/domains/shared/prefixes.test.ts +211 -0
  213. package/src/domains/shared/prefixes.ts +182 -15
  214. package/src/domains/shared/renderers.ts +5 -5
  215. package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
  216. package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
  217. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  218. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  219. package/src/domains/shared/runtime.ts +4 -63
  220. package/src/domains/shared/semanticPackage.test.ts +89 -0
  221. package/src/domains/shared/semanticPackage.ts +34 -3
  222. package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
  223. package/src/domains/shared/stories/compileLookupCommand.ts +102 -0
  224. package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
  225. package/src/domains/shared/stories/compileLookupTool.ts +96 -0
  226. package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
  227. package/src/domains/shared/stories/compileReadCommand.ts +48 -0
  228. package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
  229. package/src/domains/shared/stories/compileReadTool.ts +38 -0
  230. package/src/domains/shared/stories/condense.test.ts +18 -0
  231. package/src/domains/shared/stories/condense.ts +15 -0
  232. package/src/domains/shared/stories/index.ts +20 -0
  233. package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
  234. package/src/domains/shared/stories/normalizeNames.ts +22 -0
  235. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +151 -0
  236. package/src/domains/shared/stories/pack/applyPackFilters.ts +111 -0
  237. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  238. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  239. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +81 -0
  240. package/src/domains/shared/stories/pack/buildLookupQuery.ts +154 -0
  241. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  242. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  243. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  244. package/src/domains/shared/stories/pack/collectPackStories.test.ts +406 -0
  245. package/src/domains/shared/stories/pack/collectPackStories.ts +158 -0
  246. package/src/domains/shared/stories/pack/compilePackCommands.ts +50 -0
  247. package/src/domains/shared/stories/pack/compilePackStories.test.ts +751 -0
  248. package/src/domains/shared/stories/pack/compilePackStories.ts +724 -0
  249. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +55 -0
  250. package/src/domains/shared/stories/pack/index.ts +42 -0
  251. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  252. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  253. package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
  254. package/src/domains/shared/stories/pack/standardParity.test.ts +381 -0
  255. package/src/domains/shared/stories/pack/types.ts +224 -0
  256. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +742 -0
  257. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +859 -0
  258. package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
  259. package/src/domains/shared/stories/projectCliParameters.ts +57 -0
  260. package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
  261. package/src/domains/shared/stories/projectToolParams.ts +32 -0
  262. package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
  263. package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
  264. package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
  265. package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
  266. package/src/domains/shared/stories/types.ts +179 -0
  267. package/src/domains/shared/suggestions/expandGlob.ts +1 -0
  268. package/src/domains/shared/suggestions/suggestNames.ts +43 -10
  269. package/src/domains/shared/types/index.ts +5 -9
  270. package/src/domains/shared/types/ontology.ts +119 -24
  271. package/src/domains/shared/types/standard.ts +9 -52
  272. package/src/domains/skill/commands/index.ts +3 -1
  273. package/src/domains/skill/commands/list.test.ts +6 -2
  274. package/src/domains/skill/commands/list.ts +5 -63
  275. package/src/domains/skill/commands/lookup.test.ts +118 -0
  276. package/src/domains/skill/commands/lookup.ts +14 -0
  277. package/src/domains/skill/formatters/index.ts +1 -0
  278. package/src/domains/skill/formatters/lookup.test.ts +46 -0
  279. package/src/domains/skill/formatters/lookup.ts +57 -0
  280. package/src/domains/skill/helpers/resolveSkillSources.ts +0 -1
  281. package/src/domains/skill/index.ts +7 -3
  282. package/src/domains/skill/mcp/specs.ts +10 -29
  283. package/src/domains/skill/operations/discover.test.ts +61 -30
  284. package/src/domains/skill/operations/discover.ts +28 -37
  285. package/src/domains/skill/operations/index.ts +2 -1
  286. package/src/domains/skill/operations/list.test.ts +15 -7
  287. package/src/domains/skill/operations/lookup.test.ts +91 -0
  288. package/src/domains/skill/operations/lookup.ts +70 -0
  289. package/src/domains/skill/stories.test.ts +43 -0
  290. package/src/domains/skill/stories.ts +103 -0
  291. package/src/domains/skill/types.ts +8 -0
  292. package/src/domains/token/commands/lookup.test.ts +8 -0
  293. package/src/domains/token/commands/lookup.ts +12 -0
  294. package/src/domains/token/commands/sample.ts +2 -1
  295. package/src/domains/token/featureFlag.ts +17 -0
  296. package/src/domains/token/index.ts +5 -0
  297. package/src/domains/token/mcp/specs.ts +18 -2
  298. package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
  299. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  300. package/src/domains/trace/commands/show.ts +30 -4
  301. package/src/domains/trace/tracePlugin.test.ts +1 -1
  302. package/src/index.ts +1 -13
  303. package/src/mcp/registerResources.test.ts +382 -74
  304. package/src/mcp/registerResources.ts +5 -348
  305. package/src/mcp/resources/buildGraphIndex.ts +167 -0
  306. package/src/mcp/resources/buildResourceList.test.ts +97 -0
  307. package/src/mcp/resources/buildResourceList.ts +176 -0
  308. package/src/mcp/resources/classifyEntity.test.ts +40 -0
  309. package/src/mcp/resources/classifyEntity.ts +27 -0
  310. package/src/mcp/resources/constants.ts +105 -0
  311. package/src/mcp/resources/displayPredicates.ts +62 -0
  312. package/src/mcp/resources/pickFirstValue.test.ts +29 -0
  313. package/src/mcp/resources/pickFirstValue.ts +26 -0
  314. package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
  315. package/src/mcp/resources/pickPrimaryType.ts +26 -0
  316. package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
  317. package/src/mcp/resources/rankUriCompletions.ts +64 -0
  318. package/src/mcp/resources/readEntity.ts +169 -0
  319. package/src/mcp/resources/registerResources.ts +126 -0
  320. package/src/mcp/resources/types.ts +128 -0
  321. package/src/mcp/tools/index.ts +22 -5
  322. package/src/mcp/tools/registerTools.test.ts +167 -34
  323. package/src/mcp/types.ts +4 -0
  324. package/src/pipeline/collectCommands.test.ts +151 -1
  325. package/src/pipeline/collectCommands.ts +35 -13
  326. package/src/pipeline/createProgram.test.ts +5 -1
  327. package/src/pipeline/createProgram.ts +2 -2
  328. package/src/pipeline/firstRun.test.ts +80 -0
  329. package/src/pipeline/firstRun.ts +99 -0
  330. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  331. package/src/pipeline/parseGlobalFlags.ts +98 -9
  332. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  333. package/src/pipeline/resolveCommandKind.ts +4 -0
  334. package/src/pipeline/rootHelp.test.ts +87 -0
  335. package/src/pipeline/rootHelp.ts +265 -0
  336. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  337. package/src/pipeline/runCli.test.ts +17 -4
  338. package/src/pipeline/runCli.ts +202 -25
  339. package/src/testing/cli.ts +30 -2
  340. package/src/testing/createTestMcpClient.ts +4 -1
  341. package/src/testing/dsFixtures.ts +21 -4
  342. package/src/testing/index.ts +7 -0
  343. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
  344. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  345. package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
  346. package/src/testing/integration/edge-cases.test.ts +62 -50
  347. package/src/testing/integration/executor-execution.test.ts +103 -0
  348. package/src/testing/integration/executor-golden.test.ts +145 -0
  349. package/src/testing/integration/mcp-surface.test.ts +40 -15
  350. package/src/testing/integration/parity.test.ts +136 -82
  351. package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
  352. package/src/testing/setupXdgIsolation.ts +26 -0
  353. package/src/testing/standardParityFixtures.ts +35 -0
  354. package/src/testing/store.ts +4 -4
  355. package/src/testing/storyFixtures.ts +63 -0
  356. package/src/domains/create/commands/component.test.ts +0 -166
  357. package/src/domains/create/commands/component.ts +0 -123
  358. package/src/domains/create/commands/index.ts +0 -3
  359. package/src/domains/create/commands/package.test.ts +0 -35
  360. package/src/domains/create/commands/package.ts +0 -21
  361. package/src/domains/create/generators.ts +0 -8
  362. package/src/domains/standard/commands/categories.test.ts +0 -70
  363. package/src/domains/standard/commands/categories.ts +0 -48
  364. package/src/domains/standard/commands/index.ts +0 -6
  365. package/src/domains/standard/commands/list.test.ts +0 -105
  366. package/src/domains/standard/commands/list.ts +0 -90
  367. package/src/domains/standard/commands/lookup.test.ts +0 -135
  368. package/src/domains/standard/commands/lookup.ts +0 -99
  369. package/src/domains/standard/commands/sample.ts +0 -57
  370. package/src/domains/standard/formatters/categories.test.ts +0 -44
  371. package/src/domains/standard/formatters/categories.ts +0 -38
  372. package/src/domains/standard/formatters/index.ts +0 -10
  373. package/src/domains/standard/formatters/list.test.ts +0 -169
  374. package/src/domains/standard/formatters/list.ts +0 -184
  375. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  376. package/src/domains/standard/formatters/lookup.ts +0 -101
  377. package/src/domains/standard/formatters/sample.ts +0 -15
  378. package/src/domains/standard/formatters/types.ts +0 -31
  379. package/src/domains/standard/index.ts +0 -40
  380. package/src/domains/standard/mcp/index.ts +0 -2
  381. package/src/domains/standard/mcp/specs.ts +0 -244
  382. package/src/domains/standard/operations/categories.test.ts +0 -61
  383. package/src/domains/standard/operations/categories.ts +0 -38
  384. package/src/domains/standard/operations/index.ts +0 -6
  385. package/src/domains/standard/operations/list.test.ts +0 -75
  386. package/src/domains/standard/operations/list.ts +0 -64
  387. package/src/domains/standard/operations/lookup.test.ts +0 -58
  388. package/src/domains/standard/operations/lookup.ts +0 -151
  389. package/src/domains/standard/operations/sample.ts +0 -35
  390. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  391. package/src/domains/standard/orchestration/index.ts +0 -4
  392. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  393. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -32
  394. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  395. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  396. package/src/domains/standard/standardConfig.ts +0 -42
  397. package/src/domains/standard/types.ts +0 -15
  398. package/src/domains/tier/commands/index.ts +0 -3
  399. package/src/domains/tier/commands/list.test.ts +0 -67
  400. package/src/domains/tier/commands/list.ts +0 -59
  401. package/src/domains/tier/formatters/index.ts +0 -3
  402. package/src/domains/tier/formatters/list.test.ts +0 -33
  403. package/src/domains/tier/formatters/list.ts +0 -45
  404. package/src/domains/tier/index.ts +0 -17
  405. package/src/domains/tier/mcp/index.ts +0 -2
  406. package/src/domains/tier/mcp/specs.ts +0 -38
  407. package/src/domains/tier/operations/index.ts +0 -3
  408. package/src/domains/tier/operations/list.test.ts +0 -32
  409. package/src/domains/tier/operations/list.ts +0 -33
  410. package/src/pipeline/runInteractiveCommand.ts +0 -198
@@ -1,39 +1,20 @@
1
- import { $, gen, type Task, writeFile } from "@canonical/task";
2
- import { readConfig } from "../../../config/index.js";
3
- import resolveConfigPath from "../../../config/resolveConfigPath.js";
1
+ import type { Task } from "@canonical/task";
2
+ import type { ConfigScope } from "../../../config/index.js";
3
+ import writeConfigField from "./writeConfigField.js";
4
4
 
5
5
  /**
6
6
  * Build a config-write task for setting or resetting the active tier.
7
7
  *
8
- * @param cwd - Working directory containing pragma.config.json.
8
+ * @param cwd - Working directory the target config layer is resolved from.
9
9
  * @param tier - Tier path to persist, or `undefined` to reset.
10
- * @returns Task yielding the persisted field/value payload.
11
- * @note - Impure — reads config and writes to the file system.
10
+ * @param scope - Optional explicit target layer (`"global"` | `"local"`).
11
+ * @returns Task yielding the persisted field/value payload and target path.
12
+ * @note - Impure — reads and writes the target config file.
12
13
  */
13
14
  export default function setTier(
14
15
  cwd: string,
15
16
  tier: string | undefined,
16
- ): Task<{
17
- field: "tier";
18
- value: string | undefined;
19
- }> {
20
- return gen(function* () {
21
- // Merge onto the full existing config so other fields (e.g. `packages`,
22
- // `trace`) survive. `readConfig` normalises defaults (tier: undefined,
23
- // channel: "normal"); strip those to keep the file minimal, then apply
24
- // the tier change (`undefined` resets it).
25
- const current = readConfig(cwd);
26
- const { tier: _current, channel, ...rest } = current;
27
- const next = {
28
- ...rest,
29
- ...(tier !== undefined ? { tier } : {}),
30
- ...(channel !== "normal" ? { channel } : {}),
31
- };
32
-
33
- yield* $(
34
- writeFile(resolveConfigPath(cwd), `${JSON.stringify(next, null, 2)}\n`),
35
- );
36
-
37
- return { field: "tier", value: tier };
38
- });
17
+ scope?: ConfigScope,
18
+ ): Task<{ field: "tier"; value: string | undefined; path: string }> {
19
+ return writeConfigField(cwd, "tier", tier, scope);
39
20
  }
@@ -1,38 +1,20 @@
1
- import { $, gen, type Task, writeFile } from "@canonical/task";
2
- import { readConfig } from "../../../config/index.js";
3
- import resolveConfigPath from "../../../config/resolveConfigPath.js";
1
+ import type { Task } from "@canonical/task";
2
+ import type { ConfigScope } from "../../../config/index.js";
3
+ import writeConfigField from "./writeConfigField.js";
4
4
 
5
5
  /**
6
6
  * Build a config-write task for enabling or disabling query tracing.
7
7
  *
8
- * @param cwd - Working directory containing pragma.config.json.
8
+ * @param cwd - Working directory the target config layer is resolved from.
9
9
  * @param trace - `true` to enable, `false` to disable.
10
- * @returns Task yielding the persisted field/value payload.
10
+ * @param scope - Optional explicit target layer (`"global"` | `"local"`).
11
+ * @returns Task yielding the persisted field/value payload and target path.
12
+ * @note - Impure — reads and writes the target config file.
11
13
  */
12
14
  export default function setTrace(
13
15
  cwd: string,
14
16
  trace: boolean,
15
- ): Task<{ field: "trace"; value: boolean }> {
16
- return gen(function* () {
17
- // Merge onto the full existing config so other fields (e.g. `packages`)
18
- // survive — rebuilding from a fixed field list silently drops everything
19
- // not listed, and `packages` is runtime-consumed, so dropping it is data
20
- // loss. `readConfig` normalises defaults (tier: undefined, channel:
21
- // "normal"); strip those so we keep the file minimal, matching the
22
- // existing convention of not persisting unset defaults.
23
- const current = readConfig(cwd);
24
- const { tier, channel, ...rest } = current;
25
- const next = {
26
- ...rest,
27
- ...(tier !== undefined ? { tier } : {}),
28
- ...(channel !== "normal" ? { channel } : {}),
29
- trace,
30
- };
31
-
32
- yield* $(
33
- writeFile(resolveConfigPath(cwd), `${JSON.stringify(next, null, 2)}\n`),
34
- );
35
-
36
- return { field: "trace", value: trace };
37
- });
17
+ scope?: ConfigScope,
18
+ ): Task<{ field: "trace"; value: boolean | undefined; path: string }> {
19
+ return writeConfigField(cwd, "trace", trace, scope);
38
20
  }
@@ -0,0 +1,41 @@
1
+ import { $, gen, type Task, writeFile } from "@canonical/task";
2
+ import {
3
+ type ConfigScope,
4
+ type ConfigUpdate,
5
+ mergeConfigFileUpdate,
6
+ resolveWriteConfigPath,
7
+ } from "../../../config/index.js";
8
+
9
+ /**
10
+ * Build a config-write task for one field.
11
+ *
12
+ * Resolves the target layer with the global-first write rule (explicit
13
+ * scope wins, else the nearest existing project file, else the global XDG
14
+ * file), merges the update into that file's own contents — never copying
15
+ * values from another layer — and writes through the tracked `writeFile`
16
+ * effect so the change participates in dry-run and undo. `undefined`
17
+ * removes the field from the target file.
18
+ *
19
+ * @param cwd - Working directory the project layer is resolved from.
20
+ * @param field - Config field to set or remove.
21
+ * @param value - New value, or `undefined` to remove the field.
22
+ * @param scope - Optional explicit target layer.
23
+ * @returns Task yielding the persisted field/value payload and the path written.
24
+ * @note Impure — resolves and reads the target file, writes via effect.
25
+ */
26
+ export default function writeConfigField<F extends keyof ConfigUpdate>(
27
+ cwd: string,
28
+ field: F,
29
+ value: ConfigUpdate[F],
30
+ scope?: ConfigScope,
31
+ ): Task<{ field: F; value: ConfigUpdate[F]; path: string }> {
32
+ return gen(function* () {
33
+ const path = resolveWriteConfigPath(cwd, scope);
34
+ const update = { [field]: value } as ConfigUpdate;
35
+ const content = mergeConfigFileUpdate(path, update);
36
+
37
+ yield* $(writeFile(path, content));
38
+
39
+ return { field, value, path };
40
+ });
41
+ }
@@ -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
+ });
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Generator packs — the write-side analog of story packs.
3
+ *
4
+ * A generator pack is a set of summon generators (a package's `generators`
5
+ * export). This compiler enumerates them, groups by noun, and emits `create
6
+ * <noun>` CLI commands and `create_<noun>` MCP tools — the same data-driven
7
+ * surface story packs give read commands. The core stops hardcoding which
8
+ * generators exist; a package's full set is exposed automatically.
9
+ *
10
+ * Key convention (summon's own): `"<noun>/<variant>"` (e.g. `component/react`)
11
+ * groups under `<noun>` with a `<variant>` discriminator; a bare `"<noun>"`
12
+ * (e.g. `package`) is a single generator. A noun with one variant collapses to
13
+ * `create <noun>`; a noun with several becomes `create <noun> <variant>`.
14
+ */
15
+
16
+ import {
17
+ type CommandDefinition,
18
+ executeGenerator,
19
+ generatorToCommand,
20
+ type ParameterDefinition,
21
+ } from "@canonical/cli-core";
22
+ import type { AnyGenerator, PromptDefinition } from "@canonical/summon-core";
23
+ import { PragmaError } from "#error";
24
+ import type { ToolParamDef, ToolSpec } from "../../shared/ToolSpec.js";
25
+ import renderGeneratorUi from "../renderGeneratorUi.js";
26
+
27
+ /** A package's generator set: generator key → generator. */
28
+ export type GeneratorSet = Readonly<Record<string, AnyGenerator>>;
29
+
30
+ /** Compiled surfaces for one generator pack. */
31
+ export interface CompiledGeneratorPack {
32
+ readonly commands: readonly CommandDefinition[];
33
+ readonly specs: readonly ToolSpec[];
34
+ }
35
+
36
+ /** A generator grouped under a noun, with its optional variant. */
37
+ interface GroupedGenerator {
38
+ readonly variant?: string;
39
+ readonly gen: AnyGenerator;
40
+ }
41
+
42
+ /** Split a generator key into `{ noun, variant? }` on the first slash. */
43
+ function parseGeneratorKey(key: string): { noun: string; variant?: string } {
44
+ const slash = key.indexOf("/");
45
+ if (slash === -1) return { noun: key };
46
+ return { noun: key.slice(0, slash), variant: key.slice(slash + 1) };
47
+ }
48
+
49
+ /** Group a generator set by noun, preserving declaration order. */
50
+ function groupByNoun(
51
+ generators: GeneratorSet,
52
+ ): Map<string, GroupedGenerator[]> {
53
+ const groups = new Map<string, GroupedGenerator[]>();
54
+ for (const [key, gen] of Object.entries(generators)) {
55
+ const { noun, variant } = parseGeneratorKey(key);
56
+ const group = groups.get(noun) ?? [];
57
+ group.push(variant !== undefined ? { variant, gen } : { gen });
58
+ groups.set(noun, group);
59
+ }
60
+ return groups;
61
+ }
62
+
63
+ /** Prompt type → tool param type, mirroring cli-core's `promptToParameter`. */
64
+ const PROMPT_TYPE_MAP: Record<PromptDefinition["type"], ToolParamDef["type"]> =
65
+ {
66
+ text: "string",
67
+ confirm: "boolean",
68
+ select: "string",
69
+ multiselect: "string[]",
70
+ };
71
+
72
+ /** Map a summon prompt to a neutral MCP tool parameter. */
73
+ function promptToToolParam(prompt: PromptDefinition): ToolParamDef {
74
+ const enumValues =
75
+ prompt.type === "confirm"
76
+ ? undefined
77
+ : prompt.choices?.map((choice) => choice.value);
78
+ return {
79
+ type: PROMPT_TYPE_MAP[prompt.type],
80
+ description: prompt.message,
81
+ // A prompt with a default or a `when` condition is answerable without
82
+ // input; only an unconditional, defaultless prompt is required.
83
+ optional: prompt.default !== undefined || prompt.when !== undefined,
84
+ ...(enumValues ? { enum: enumValues } : {}),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Generator answers the MCP tool runner pins to fixed values.
90
+ *
91
+ * MCP create tools always produce a dry-run plan, so `runInstall` is forced
92
+ * off regardless of input. This one map drives BOTH the schema (pinned
93
+ * answers are excluded from the advertised params — a tool must not
94
+ * advertise a parameter it ignores) and the runtime override in
95
+ * {@link runGeneratorTool}, so the two can never drift.
96
+ */
97
+ const PINNED_TOOL_ANSWERS: Readonly<Record<string, unknown>> = {
98
+ runInstall: false,
99
+ };
100
+
101
+ /**
102
+ * Build the MCP param map for a generator from its prompts, omitting
103
+ * prompts whose answers are pinned by {@link PINNED_TOOL_ANSWERS}.
104
+ */
105
+ function toolParamsFor(gen: AnyGenerator): Record<string, ToolParamDef> {
106
+ return Object.fromEntries(
107
+ gen.prompts
108
+ .filter((prompt) => !(prompt.name in PINNED_TOOL_ANSWERS))
109
+ .map((prompt) => [prompt.name, promptToToolParam(prompt)]),
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Run one generator as an MCP tool call: json-mode dry-run plan.
115
+ *
116
+ * A generator that throws (e.g. an unmet hard requirement such as
117
+ * application's ssr/router pairing) is mapped to a typed INVALID_INPUT
118
+ * carrying the generator's own message as the recovery hint — the generic
119
+ * replacement for the per-generator validation the hand-written specs did.
120
+ */
121
+ async function runGeneratorTool(
122
+ gen: AnyGenerator,
123
+ rt: { cwd: string },
124
+ params: Record<string, unknown>,
125
+ discriminator?: string,
126
+ ): Promise<{ data: unknown }> {
127
+ const batchCtx = {
128
+ cwd: rt.cwd,
129
+ globalFlags: { llm: false, format: "json" as const, verbose: false },
130
+ };
131
+ // Apply the pinned answers (never install during a dry-run plan); the
132
+ // discriminator is dispatch metadata, not a generator answer.
133
+ const genParams: Record<string, unknown> = {
134
+ ...params,
135
+ ...PINNED_TOOL_ANSWERS,
136
+ };
137
+ if (discriminator) delete genParams[discriminator];
138
+ try {
139
+ const result = await executeGenerator(gen, genParams, batchCtx);
140
+ if (result.tag === "output") {
141
+ // json mode: result.value is already the structured plan.
142
+ return { data: result.value };
143
+ }
144
+ throw PragmaError.invalidInput("parameters", "(no plan produced)", {
145
+ recovery: { message: `The ${gen.meta.name} generator rejected them.` },
146
+ });
147
+ } catch (error) {
148
+ if (error instanceof PragmaError) throw error;
149
+ throw PragmaError.invalidInput("parameters", "(rejected)", {
150
+ recovery: {
151
+ message: error instanceof Error ? error.message : String(error),
152
+ },
153
+ });
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Build the variant → generator resolver shared by the CLI and MCP dispatch.
159
+ *
160
+ * @returns A function resolving a raw `variant` param to its generator.
161
+ * @throws PragmaError with code INVALID_INPUT for a missing/unknown variant.
162
+ */
163
+ function buildVariantResolver(
164
+ group: readonly GroupedGenerator[],
165
+ variants: readonly string[],
166
+ ): (variant: unknown) => AnyGenerator {
167
+ const byVariant = new Map(group.map((g) => [g.variant, g.gen]));
168
+ return (variant) => {
169
+ const gen =
170
+ typeof variant === "string" ? byVariant.get(variant) : undefined;
171
+ if (!gen) {
172
+ throw PragmaError.invalidInput(
173
+ "variant",
174
+ typeof variant === "string" ? variant : "(missing)",
175
+ { validOptions: [...variants] },
176
+ );
177
+ }
178
+ return gen;
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Compile a package's generator set into `create <noun>` commands and
184
+ * `create_<noun>` MCP tools.
185
+ *
186
+ * @note Impure — warns on stderr when a degenerate group (duplicate noun
187
+ * with mixed bare/variant keys) forces generators to be skipped.
188
+ */
189
+ export default function compileGeneratorPack(
190
+ generators: GeneratorSet,
191
+ ): CompiledGeneratorPack {
192
+ const commands: CommandDefinition[] = [];
193
+ const specs: ToolSpec[] = [];
194
+
195
+ for (const [noun, group] of groupByNoun(generators)) {
196
+ const [first, ...rest] = group;
197
+ if (!first) continue; // groupByNoun never emits empty groups
198
+
199
+ const variants = group
200
+ .map((g) => g.variant)
201
+ .filter((v): v is string => v !== undefined);
202
+
203
+ if (rest.length > 0 && variants.length === group.length) {
204
+ commands.push(buildVariantCommand(noun, first.gen, group, variants));
205
+ specs.push(buildVariantSpec(noun, first.gen, group, variants));
206
+ continue;
207
+ }
208
+
209
+ if (rest.length > 0) {
210
+ // Mixed bare/variant keys under one noun is a malformed set — expose
211
+ // the first generator rather than none, but never drop the rest
212
+ // silently.
213
+ process.stderr.write(
214
+ `Warning: generator noun "${noun}" mixes bare and variant keys; ` +
215
+ `only "${first.gen.meta.name}" is exposed.\n`,
216
+ );
217
+ }
218
+ commands.push(buildSingleCommand(noun, first.gen));
219
+ specs.push(buildSingleSpec(noun, first.gen));
220
+ }
221
+
222
+ return { commands, specs };
223
+ }
224
+
225
+ /** `create <noun>` for a single generator, with summon's rich Ink UI. */
226
+ function buildSingleCommand(
227
+ noun: string,
228
+ gen: AnyGenerator,
229
+ ): CommandDefinition {
230
+ const base = generatorToCommand(["create", noun], gen);
231
+ return {
232
+ ...base,
233
+ execute: (params, ctx) => renderGeneratorUi(gen, params, ctx),
234
+ };
235
+ }
236
+
237
+ /**
238
+ * `create <noun> <variant>` dispatch over several generators.
239
+ *
240
+ * Help and parameters project from the reference (first-declared) generator —
241
+ * variants of one noun are assumed prompt-compatible, the same assumption the
242
+ * hand-written component command made.
243
+ */
244
+ function buildVariantCommand(
245
+ noun: string,
246
+ reference: AnyGenerator,
247
+ group: readonly GroupedGenerator[],
248
+ variants: readonly string[],
249
+ ): CommandDefinition {
250
+ const resolve = buildVariantResolver(group, variants);
251
+ const base = generatorToCommand(["create", noun], reference);
252
+ const variantParam: ParameterDefinition = {
253
+ name: "variant",
254
+ description: `Which ${noun} to scaffold (${variants.join(", ")})`,
255
+ type: "select",
256
+ choices: variants.map((v) => ({ label: v, value: v })),
257
+ positional: true,
258
+ required: true,
259
+ };
260
+ return {
261
+ ...base,
262
+ parameters: [variantParam, ...base.parameters],
263
+ // async so an unknown variant rejects instead of throwing synchronously.
264
+ execute: async (params, ctx) =>
265
+ renderGeneratorUi(resolve(params.variant), params, ctx),
266
+ };
267
+ }
268
+
269
+ /** `create_<noun>` MCP tool for a single generator. */
270
+ function buildSingleSpec(noun: string, gen: AnyGenerator): ToolSpec {
271
+ return {
272
+ name: `create_${noun}`,
273
+ description: `${gen.meta.description} Returns a JSON generation plan (dry-run).`,
274
+ params: toolParamsFor(gen),
275
+ readOnly: false,
276
+ destructive: false,
277
+ execute: (rt, params) => runGeneratorTool(gen, rt, params),
278
+ };
279
+ }
280
+
281
+ /**
282
+ * `create_<noun>` MCP tool with a `variant` discriminator.
283
+ *
284
+ * The param schema projects from the reference generator's prompts (see
285
+ * {@link buildVariantCommand} for the compatibility assumption).
286
+ */
287
+ function buildVariantSpec(
288
+ noun: string,
289
+ reference: AnyGenerator,
290
+ group: readonly GroupedGenerator[],
291
+ variants: readonly string[],
292
+ ): ToolSpec {
293
+ const resolve = buildVariantResolver(group, variants);
294
+ return {
295
+ name: `create_${noun}`,
296
+ description: `${reference.meta.description} Returns a JSON generation plan (dry-run).`,
297
+ params: {
298
+ variant: {
299
+ type: "string",
300
+ description: `Which ${noun} to scaffold`,
301
+ optional: false,
302
+ enum: [...variants],
303
+ },
304
+ ...toolParamsFor(reference),
305
+ },
306
+ readOnly: false,
307
+ destructive: false,
308
+ // async so an unknown variant rejects instead of throwing synchronously.
309
+ execute: async (rt, params) =>
310
+ runGeneratorTool(resolve(params.variant), rt, params, "variant"),
311
+ };
312
+ }