@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,201 +1,26 @@
1
1
  /**
2
- * MCP tool specs for the block domain — block_list, block_lookup.
2
+ * MCP tool specs for the block domain — block_list, block_lookup,
3
+ * block_sample.
3
4
  *
4
- * Declarative definitions — no MCP imports. The adapter layer
5
- * converts these into registered MCP tools via `registerFromSpec()`.
5
+ * The list and lookup tools are compiled from the block read stories in
6
+ * `../stories.ts`; only the sample tool is spec'd by hand. The adapter
7
+ * layer converts these into registered MCP tools via `registerFromSpec()`.
6
8
  */
7
9
 
8
- import type { ToolSpec } from "../../shared/ToolSpec.js";
9
10
  import {
10
- listFormatters as blockListFmt,
11
- lookupFormatters as blockLookupFmt,
12
- sampleFormatters as blockSampleFmt,
13
- } from "../formatters/index.js";
11
+ compileLookupTool,
12
+ compileReadTool,
13
+ condense,
14
+ } from "../../shared/stories/index.js";
15
+ import type { ToolSpec } from "../../shared/ToolSpec.js";
16
+ import { sampleFormatters as blockSampleFmt } from "../formatters/index.js";
14
17
  import { sampleBlocks } from "../operations/index.js";
15
- import {
16
- buildBlockFilters,
17
- resolveBlockList,
18
- resolveBlockLookup,
19
- } from "../orchestration/index.js";
18
+ import { buildBlockFilters } from "../orchestration/index.js";
19
+ import { blockListStory, blockLookupStory } from "../stories.js";
20
20
 
21
21
  const specs: readonly ToolSpec[] = [
22
- {
23
- name: "block_list",
24
- description:
25
- "List design system blocks visible under current tier and channel configuration.",
26
- params: {
27
- allTiers: {
28
- type: "boolean",
29
- description: "Show blocks from all tiers, ignoring tier filter",
30
- optional: true,
31
- },
32
- digest: {
33
- type: "boolean",
34
- description: "Include implementations per block",
35
- optional: true,
36
- },
37
- detailed: {
38
- type: "boolean",
39
- description: "Include full details per block",
40
- optional: true,
41
- },
42
- condensed: {
43
- type: "boolean",
44
- description: "Token-optimized output",
45
- optional: true,
46
- },
47
- },
48
- readOnly: true,
49
- async execute(rt, { allTiers, digest, detailed, condensed }) {
50
- const contract = await resolveBlockList(rt, {
51
- allTiers: allTiers === true,
52
- digest: digest === true,
53
- detailed: detailed === true,
54
- });
55
-
56
- if (condensed) {
57
- const text = blockListFmt.llm(contract.result.items);
58
- return {
59
- condensed: true,
60
- text,
61
- tokens: `~${Math.ceil(text.length / 4)}`,
62
- };
63
- }
64
-
65
- if (contract.result.disclosure.level === "detailed") {
66
- return {
67
- data: contract.result.items,
68
- meta: {
69
- count: contract.result.items.length,
70
- disclosure: "detailed",
71
- filters: contract.result.filters,
72
- },
73
- };
74
- }
75
-
76
- if (contract.result.disclosure.level === "digest") {
77
- return {
78
- data: contract.result.items,
79
- meta: {
80
- count: contract.result.items.length,
81
- disclosure: "digest",
82
- filters: contract.result.filters,
83
- },
84
- };
85
- }
86
-
87
- return {
88
- data: contract.result.items,
89
- meta: {
90
- count: contract.result.items.length,
91
- filters: contract.result.filters,
92
- },
93
- };
94
- },
95
- },
96
-
97
- {
98
- name: "block_lookup",
99
- description:
100
- "Get detailed information about one or more design system blocks including anatomy, modifiers, tokens, and applicable standards.",
101
- params: {
102
- names: {
103
- type: "string[]",
104
- description:
105
- "Block names or IRIs to look up (e.g. ['Button', 'ds:global.component.card'])",
106
- optional: false,
107
- },
108
- detailed: {
109
- type: "boolean",
110
- description: "Return full details (default: true for MCP)",
111
- optional: true,
112
- },
113
- condensed: {
114
- type: "boolean",
115
- description: "Token-optimized output",
116
- optional: true,
117
- },
118
- },
119
- readOnly: true,
120
- async execute(rt, { names, detailed, condensed }) {
121
- const queries = names as string[];
122
- const showDetailed = (detailed as boolean | undefined) ?? true;
123
- const contract = await resolveBlockLookup(
124
- rt.store,
125
- queries,
126
- buildBlockFilters(rt),
127
- );
128
- const result = contract.result;
129
-
130
- if (condensed) {
131
- const textParts = result.results.map((block) =>
132
- blockLookupFmt.llm({
133
- block,
134
- detailed: showDetailed,
135
- aspects: {
136
- anatomy: true,
137
- modifiers: true,
138
- tokens: true,
139
- implementations: true,
140
- },
141
- }),
142
- );
143
-
144
- if (result.errors.length > 0) {
145
- textParts.push(
146
- [
147
- "### Errors",
148
- ...result.errors.map(
149
- (error) => `- ${error.query}: ${error.message}`,
150
- ),
151
- ].join("\n"),
152
- );
153
- }
154
-
155
- return {
156
- condensed: true,
157
- text: textParts.join("\n\n"),
158
- tokens: `~${Math.ceil(textParts.join("\n\n").length / 4)}`,
159
- };
160
- }
161
-
162
- if (!showDetailed) {
163
- const summaries = result.results.map(
164
- ({
165
- uri,
166
- name,
167
- type,
168
- tier,
169
- modifiers,
170
- implementations,
171
- nodeCount,
172
- tokenCount,
173
- summary,
174
- }) => ({
175
- uri,
176
- name,
177
- type,
178
- tier,
179
- modifiers,
180
- implementations,
181
- nodeCount,
182
- tokenCount,
183
- summary,
184
- }),
185
- );
186
-
187
- return {
188
- data: {
189
- results: summaries,
190
- errors: result.errors,
191
- },
192
- meta: { count: summaries.length },
193
- };
194
- }
195
-
196
- return { data: result, meta: { count: result.results.length } };
197
- },
198
- },
22
+ compileReadTool(blockListStory),
23
+ compileLookupTool(blockLookupStory),
199
24
  {
200
25
  name: "block_sample",
201
26
  description:
@@ -224,12 +49,7 @@ const specs: readonly ToolSpec[] = [
224
49
  ];
225
50
 
226
51
  if (condensed) {
227
- const text = blockSampleFmt.llm({ ...result, nextSteps });
228
- return {
229
- condensed: true,
230
- text,
231
- tokens: `~${Math.ceil(text.length / 4)}`,
232
- };
52
+ return condense(blockSampleFmt.llm({ ...result, nextSteps }));
233
53
  }
234
54
 
235
55
  return {
@@ -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") {
@@ -19,12 +19,13 @@ export default async function resolveBlockLookup(
19
19
  const nested: LookupResult<BlockDetailed[]> =
20
20
  names.length > 0
21
21
  ? await lookupMany(names, (query) => lookupBlock(store, query, filters))
22
- : { results: [], errors: [] };
22
+ : { results: [], errors: [], meta: { internalErrorCount: 0 } };
23
23
 
24
24
  // lookupBlock returns BlockDetailed[] (multiple matches per name) — flatten
25
25
  const result: LookupResult<BlockDetailed> = {
26
26
  results: nested.results.flat(),
27
27
  errors: [...nested.errors, ...globErrors],
28
+ meta: nested.meta ?? { internalErrorCount: 0 },
28
29
  };
29
30
 
30
31
  return {
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { blockListStory, blockLookupStory } from "./stories.js";
3
+
4
+ describe("blockListStory", () => {
5
+ it("renders the ink list view", () => {
6
+ const view = blockListStory.renderInk?.([]);
7
+ expect(typeof view).toBe("string");
8
+ expect(view).toContain("Blocks");
9
+ });
10
+ });
11
+
12
+ describe("blockLookupStory", () => {
13
+ it("renders the ink lookup view", () => {
14
+ const view = blockLookupStory.renderInk?.(
15
+ { results: [], errors: [] },
16
+ { surface: "cli", detailed: false, params: {} },
17
+ );
18
+ expect(typeof view).toBe("string");
19
+ });
20
+
21
+ it("implies detail from aspect flags on the CLI only", () => {
22
+ const resolveDetailed = blockLookupStory.resolveDetailed;
23
+ expect(resolveDetailed?.("cli", { anatomy: true })).toBe(true);
24
+ expect(resolveDetailed?.("cli", {})).toBe(false);
25
+ expect(resolveDetailed?.("mcp", {})).toBe(true);
26
+ expect(resolveDetailed?.("mcp", { detailed: false })).toBe(false);
27
+ });
28
+ });
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Block read stories — the single declaration of `block list` and
3
+ * `block lookup` for both surfaces.
4
+ *
5
+ * The story kernel compiles these into the CLI commands and MCP tools,
6
+ * so descriptions, parameters, resolution, and rendering live here once.
7
+ */
8
+
9
+ import { PragmaError } from "#error";
10
+ import { createListView, createLookupView } from "#tui";
11
+ import type { ColumnDef } from "../shared/contracts.js";
12
+ import {
13
+ type LookupStory,
14
+ type LookupStoryView,
15
+ type ReadStory,
16
+ requirePragmaContext,
17
+ } from "../shared/stories/index.js";
18
+ import type { BlockDetailed } from "../shared/types/index.js";
19
+ import { blockConfig } from "./blockConfig.js";
20
+ import {
21
+ createLookupOptions,
22
+ listFormatters,
23
+ lookupFormatters,
24
+ } from "./formatters/index.js";
25
+ import type { BlockLookupInput } from "./formatters/types.js";
26
+ import { resolveAspects } from "./helpers/index.js";
27
+ import { listBlocks } from "./operations/index.js";
28
+ import {
29
+ buildBlockFilters,
30
+ resolveBlockList,
31
+ resolveBlockLookup,
32
+ } from "./orchestration/index.js";
33
+ import type { AspectFlags, BlockListContract } from "./types.js";
34
+
35
+ type BlockListItems = BlockListContract["result"]["items"];
36
+
37
+ const LIST_COLUMN_ORDER = ["name", "type", "tier", "modifiers", "uri"] as const;
38
+
39
+ /** The `block list` / `block_list` read story. */
40
+ export const blockListStory: ReadStory<BlockListContract, BlockListItems> = {
41
+ noun: "block",
42
+ verb: "list",
43
+ description: "List blocks in the design system",
44
+ toolDescription:
45
+ "List design system blocks visible under current tier and channel configuration.",
46
+ params: [
47
+ {
48
+ name: "allTiers",
49
+ type: "boolean",
50
+ description: "Show blocks from all tiers",
51
+ toolDescription: "Show blocks from all tiers, ignoring tier filter",
52
+ default: false,
53
+ },
54
+ {
55
+ name: "digest",
56
+ type: "boolean",
57
+ description: "Show enriched summary with implementations",
58
+ toolDescription: "Include implementations per block",
59
+ default: false,
60
+ },
61
+ {
62
+ name: "detailed",
63
+ type: "boolean",
64
+ description: "Show full details for each block",
65
+ toolDescription: "Include full details per block",
66
+ default: false,
67
+ },
68
+ ],
69
+ examples: [
70
+ "pragma block list",
71
+ "pragma block list --all-tiers",
72
+ "pragma block list --digest",
73
+ "pragma block list --detailed",
74
+ "pragma block list --llm",
75
+ "pragma block list --format json",
76
+ ],
77
+ resolve: (rt, params) =>
78
+ resolveBlockList(rt, {
79
+ allTiers: params.allTiers === true,
80
+ digest: params.digest === true,
81
+ detailed: params.detailed === true,
82
+ }),
83
+ toOutput: (contract) => contract.result.items,
84
+ formatters: listFormatters,
85
+ toEnvelope: (contract) => {
86
+ const { items, disclosure, filters } = contract.result;
87
+ if (disclosure.level === "summary") {
88
+ return { data: items, meta: { count: items.length, filters } };
89
+ }
90
+ return {
91
+ data: items,
92
+ meta: { count: items.length, disclosure: disclosure.level, filters },
93
+ };
94
+ },
95
+ renderInk: (items) =>
96
+ createListView({
97
+ heading: "Blocks",
98
+ domain: "block",
99
+ items,
100
+ columns: reorderColumns(blockConfig.listColumns, LIST_COLUMN_ORDER),
101
+ }),
102
+ };
103
+
104
+ /** The `block lookup` / `block_lookup` read story. */
105
+ export const blockLookupStory: LookupStory<BlockDetailed, BlockLookupInput> = {
106
+ noun: "block",
107
+ description: "Look up block details",
108
+ toolDescription:
109
+ "Get detailed information about one or more design system blocks including anatomy, modifiers, tokens, and applicable standards.",
110
+ namesDescription: "Block names or IRIs",
111
+ namesToolDescription:
112
+ "Block names or IRIs to look up (e.g. ['Button', 'ds:global.component.card'])",
113
+ complete: async (partial, cmdCtx) => {
114
+ const ctx = requirePragmaContext(cmdCtx);
115
+ const all = await listBlocks(ctx.store, ctx.config);
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];
125
+ },
126
+ detailedParam: {
127
+ description:
128
+ "Show full details including anatomy, modifiers, tokens, and implementations",
129
+ toolDescription: "Return full details (default: true for MCP)",
130
+ },
131
+ params: [
132
+ {
133
+ name: "anatomy",
134
+ type: "boolean",
135
+ description: "Show anatomy tree",
136
+ default: false,
137
+ surfaces: "cli",
138
+ },
139
+ {
140
+ name: "modifiers",
141
+ type: "boolean",
142
+ description: "Show modifier values",
143
+ default: false,
144
+ surfaces: "cli",
145
+ },
146
+ {
147
+ name: "tokens",
148
+ type: "boolean",
149
+ description: "Show token references",
150
+ default: false,
151
+ surfaces: "cli",
152
+ },
153
+ {
154
+ name: "implementations",
155
+ type: "boolean",
156
+ description: "Show implementation paths",
157
+ default: false,
158
+ surfaces: "cli",
159
+ },
160
+ ],
161
+ parameterGroups: {
162
+ "Aspect filters": ["anatomy", "modifiers", "tokens", "implementations"],
163
+ },
164
+ examples: [
165
+ "pragma block lookup Button",
166
+ "pragma block lookup Button Card",
167
+ "pragma block lookup Button --detailed",
168
+ "pragma block lookup Button --anatomy --modifiers",
169
+ "pragma block lookup Button --detailed --llm",
170
+ "pragma block lookup ds:global.component.button",
171
+ ],
172
+ resolve: async (rt, names) => {
173
+ const contract = await resolveBlockLookup(
174
+ rt.store,
175
+ names,
176
+ buildBlockFilters(rt),
177
+ );
178
+ return contract.result;
179
+ },
180
+ resolveDetailed: (surface, params) => {
181
+ if (surface === "mcp") {
182
+ return ((params.detailed as boolean | undefined) ?? true) === true;
183
+ }
184
+ return params.detailed === true || isAspectSelected(params);
185
+ },
186
+ toFmtInput: (block, view) => ({
187
+ block,
188
+ detailed: view.detailed,
189
+ aspects: resolveViewAspects(view),
190
+ }),
191
+ formatters: lookupFormatters,
192
+ project: (block) => {
193
+ const {
194
+ uri,
195
+ name,
196
+ type,
197
+ tier,
198
+ modifiers,
199
+ implementations,
200
+ nodeCount,
201
+ tokenCount,
202
+ summary,
203
+ } = block;
204
+ return {
205
+ uri,
206
+ name,
207
+ type,
208
+ tier,
209
+ modifiers,
210
+ implementations,
211
+ nodeCount,
212
+ tokenCount,
213
+ summary,
214
+ };
215
+ },
216
+ emptyNamesError: () =>
217
+ PragmaError.invalidInput("names", "(empty)", {
218
+ recovery: {
219
+ message: "List available blocks.",
220
+ cli: "pragma block list",
221
+ mcp: { tool: "block_list" },
222
+ },
223
+ }),
224
+ renderInk: (result, view) =>
225
+ createLookupView({
226
+ results: result.results,
227
+ errors: result.errors,
228
+ domain: "block",
229
+ options: createLookupOptions(view.detailed, resolveViewAspects(view)),
230
+ }),
231
+ };
232
+
233
+ function isAspectSelected(params: Record<string, unknown>): boolean {
234
+ return (
235
+ params.anatomy === true ||
236
+ params.modifiers === true ||
237
+ params.tokens === true ||
238
+ params.implementations === true
239
+ );
240
+ }
241
+
242
+ function resolveViewAspects(view: LookupStoryView): AspectFlags {
243
+ return resolveAspects({
244
+ anatomy: view.params.anatomy === true,
245
+ modifiers: view.params.modifiers === true,
246
+ tokens: view.params.tokens === true,
247
+ implementations: view.params.implementations === true,
248
+ });
249
+ }
250
+
251
+ function reorderColumns<T>(
252
+ columns: readonly ColumnDef<T>[],
253
+ order: readonly string[],
254
+ ): readonly ColumnDef<T>[] {
255
+ const byKey = new Map(columns.map((col) => [col.key, col]));
256
+ return order.flatMap((key) => {
257
+ const col = byKey.get(key as keyof T & string);
258
+ return col ? [col] : [];
259
+ });
260
+ }
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { Store } from "@canonical/ke";
@@ -18,13 +18,24 @@ function makeCtx(cwd: string): PragmaContext {
18
18
 
19
19
  describe("config channel command", () => {
20
20
  let dir: string;
21
+ let xdgDir: string;
22
+ let originalXdg: string | undefined;
21
23
 
22
24
  beforeEach(() => {
23
25
  dir = mkdtempSync(join(tmpdir(), "pragma-cmd-channel-"));
26
+ // Bound the config walk at the fixture root, seed an empty project
27
+ // file so writes stay local, and isolate the global XDG layer.
28
+ mkdirSync(join(dir, ".git"));
29
+ writeFileSync(join(dir, "pragma.config.json"), "{}");
30
+ originalXdg = process.env.XDG_CONFIG_HOME;
31
+ xdgDir = mkdtempSync(join(tmpdir(), "pragma-cmd-channel-xdg-"));
32
+ process.env.XDG_CONFIG_HOME = xdgDir;
24
33
  });
25
34
 
26
35
  afterEach(() => {
36
+ process.env.XDG_CONFIG_HOME = originalXdg;
27
37
  rmSync(dir, { recursive: true, force: true });
38
+ rmSync(xdgDir, { recursive: true, force: true });
28
39
  });
29
40
 
30
41
  it("sets channel to experimental", async () => {
@@ -45,10 +56,26 @@ describe("config channel command", () => {
45
56
  expect(result.tag).toBe("output");
46
57
  if (result.tag === "output") {
47
58
  const text = result.render.plain(result.value);
48
- expect(text).toBe('Set channel to "prerelease".');
59
+ expect(text).toContain('Set channel to "prerelease".');
49
60
  }
50
61
  });
51
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
+
52
79
  it("resets channel via --reset flag", async () => {
53
80
  writeFileSync(
54
81
  join(dir, "pragma.config.json"),
@@ -72,7 +99,7 @@ describe("config channel command", () => {
72
99
  expect(result.tag).toBe("output");
73
100
  if (result.tag === "output") {
74
101
  const text = result.render.plain(result.value);
75
- expect(text).toBe("Reset channel to default.");
102
+ expect(text).toContain("Reset channel to default.");
76
103
  }
77
104
  });
78
105
 
@@ -3,13 +3,15 @@ import {
3
3
  type CommandResult,
4
4
  createOutputResult,
5
5
  } from "@canonical/cli-core";
6
- import { runTask } from "@canonical/task";
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";
11
12
  import { validateChannel } from "../operations/index.js";
12
13
  import { setChannelTask } from "../tasks/index.js";
14
+ import { resolveConfigScope, SCOPE_PARAMETERS } from "./configScope.js";
13
15
 
14
16
  /**
15
17
  * Build the `pragma config channel` command definition.
@@ -40,6 +42,7 @@ export default function buildChannelCommand(
40
42
  type: "boolean",
41
43
  default: false,
42
44
  },
45
+ ...SCOPE_PARAMETERS,
43
46
  ],
44
47
  meta: {
45
48
  examples: [
@@ -50,18 +53,32 @@ export default function buildChannelCommand(
50
53
  execute: async (
51
54
  params: Record<string, unknown>,
52
55
  ): Promise<CommandResult> => {
56
+ const scope = resolveConfigScope(params);
53
57
  const reset = params.reset === true;
54
58
  const value = params.value as string | undefined;
55
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
+
56
70
  if (reset) {
57
- await runTask(setChannelTask(ctx.cwd, undefined));
71
+ const result = await runTask(setChannelTask(ctx.cwd, undefined, scope));
58
72
  const format = selectFormatter(ctx, channelFormatters.reset);
59
- return createOutputResult("Reset channel to default.", {
60
- plain: format,
61
- });
73
+ return createOutputResult(
74
+ { field: "channel", path: result.path },
75
+ { plain: format },
76
+ );
62
77
  }
63
78
 
64
- 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) {
65
82
  const config = readConfig(ctx.cwd);
66
83
  const format = selectFormatter(ctx, channelFormatters.query);
67
84
  return createOutputResult(config.channel, {
@@ -71,10 +88,10 @@ export default function buildChannelCommand(
71
88
 
72
89
  const channel = validateChannel(value);
73
90
 
74
- await runTask(setChannelTask(ctx.cwd, channel));
91
+ const result = await runTask(setChannelTask(ctx.cwd, channel, scope));
75
92
  const format = selectFormatter(ctx, channelFormatters.set);
76
93
  return createOutputResult(
77
- { field: "channel", value: channel },
94
+ { field: "channel", value: channel, path: result.path },
78
95
  { plain: format },
79
96
  );
80
97
  },