@canonical/pragma-cli 0.27.1-experimental.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/README.md +67 -15
  2. package/dist/pragma +0 -0
  3. package/package.json +16 -17
  4. package/src/config/defaultConfig.test.ts +33 -0
  5. package/src/config/defaultConfig.ts +59 -0
  6. package/src/config/defaults.json +17 -0
  7. package/src/config/findProjectConfigPath.test.ts +53 -0
  8. package/src/config/findProjectConfigPath.ts +36 -0
  9. package/src/config/index.ts +17 -2
  10. package/src/config/mergeConfigFileUpdate.ts +58 -0
  11. package/src/config/parseConfigValues.ts +215 -0
  12. package/src/config/readConfig.test.ts +104 -13
  13. package/src/config/readConfig.ts +12 -117
  14. package/src/config/readConfigLayers.test.ts +107 -0
  15. package/src/config/readConfigLayers.ts +115 -0
  16. package/src/config/resolveGlobalConfigPath.ts +13 -0
  17. package/src/config/resolveWriteConfigPath.test.ts +53 -0
  18. package/src/config/resolveWriteConfigPath.ts +31 -0
  19. package/src/config/types.ts +88 -2
  20. package/src/config/writeConfig.ts +25 -43
  21. package/src/constants.ts +12 -1
  22. package/src/domains/block/commands/index.ts +2 -1
  23. package/src/domains/block/commands/list.ts +5 -90
  24. package/src/domains/block/commands/lookup.ts +8 -167
  25. package/src/domains/block/commands/sample.ts +55 -0
  26. package/src/domains/block/formatters/index.ts +2 -1
  27. package/src/domains/block/formatters/sample.ts +21 -0
  28. package/src/domains/block/index.ts +3 -3
  29. package/src/domains/block/mcp/specs.ts +36 -168
  30. package/src/domains/block/operations/index.ts +2 -1
  31. package/src/domains/block/operations/lookup.ts +9 -0
  32. package/src/domains/block/operations/sample.ts +41 -0
  33. package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
  34. package/src/domains/block/orchestration/resolveBlockLookup.ts +11 -3
  35. package/src/domains/block/stories.test.ts +28 -0
  36. package/src/domains/block/stories.ts +254 -0
  37. package/src/domains/config/commands/channel.test.ts +14 -3
  38. package/src/domains/config/commands/channel.ts +11 -7
  39. package/src/domains/config/commands/configScope.ts +46 -0
  40. package/src/domains/config/commands/framework.test.ts +105 -0
  41. package/src/domains/config/commands/framework.ts +91 -0
  42. package/src/domains/config/commands/index.ts +2 -0
  43. package/src/domains/config/commands/show.ts +5 -45
  44. package/src/domains/config/commands/tier.test.ts +13 -2
  45. package/src/domains/config/commands/tier.ts +11 -7
  46. package/src/domains/config/commands/trace.test.ts +165 -0
  47. package/src/domains/config/commands/trace.ts +83 -0
  48. package/src/domains/config/formatters/channel.test.ts +20 -6
  49. package/src/domains/config/formatters/channel.ts +14 -11
  50. package/src/domains/config/formatters/framework.ts +37 -0
  51. package/src/domains/config/formatters/index.ts +2 -0
  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 +28 -0
  57. package/src/domains/config/index.ts +15 -2
  58. package/src/domains/config/mcp/specs.ts +33 -53
  59. package/src/domains/config/operations/index.ts +1 -0
  60. package/src/domains/config/operations/show.test.ts +18 -0
  61. package/src/domains/config/operations/show.ts +7 -1
  62. package/src/domains/config/operations/types.ts +7 -0
  63. package/src/domains/config/operations/validateFramework.ts +24 -0
  64. package/src/domains/config/stories.ts +42 -0
  65. package/src/domains/config/tasks/index.ts +3 -0
  66. package/src/domains/config/tasks/setChannel.ts +13 -22
  67. package/src/domains/config/tasks/setFramework.ts +21 -0
  68. package/src/domains/config/tasks/setTier.ts +10 -23
  69. package/src/domains/config/tasks/setTrace.ts +20 -0
  70. package/src/domains/config/tasks/writeConfigField.ts +41 -0
  71. package/src/domains/create/commands/component.ts +5 -5
  72. package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
  73. package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
  74. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
  75. package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
  76. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -48
  77. package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
  78. package/src/domains/doctor/operations/checks/index.ts +1 -0
  79. package/src/domains/doctor/operations/checks.test.ts +162 -8
  80. package/src/domains/doctor/operations/runChecks.test.ts +85 -4
  81. package/src/domains/doctor/operations/runChecks.ts +50 -12
  82. package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
  83. package/src/domains/graph/helpers/resolveUri.ts +28 -21
  84. package/src/domains/graphql/commands/build.test.ts +196 -0
  85. package/src/domains/graphql/commands/build.ts +117 -0
  86. package/src/domains/graphql/commands/check.test.ts +141 -0
  87. package/src/domains/graphql/commands/check.ts +81 -0
  88. package/src/domains/graphql/commands/index.ts +5 -0
  89. package/src/domains/graphql/commands/serve.test.ts +40 -0
  90. package/src/domains/graphql/commands/serve.ts +131 -0
  91. package/src/domains/graphql/constants.ts +10 -0
  92. package/src/domains/graphql/formatters/index.ts +3 -0
  93. package/src/domains/graphql/formatters/report.ts +78 -0
  94. package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
  95. package/src/domains/graphql/helpers/gatherSources.ts +58 -0
  96. package/src/domains/graphql/helpers/index.ts +6 -0
  97. package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
  98. package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
  99. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
  100. package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
  101. package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
  102. package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
  103. package/src/domains/graphql/index.ts +24 -0
  104. package/src/domains/graphql/operations/compileSchema.test.ts +95 -0
  105. package/src/domains/graphql/operations/compileSchema.ts +107 -0
  106. package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
  107. package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
  108. package/src/domains/graphql/operations/index.ts +12 -0
  109. package/src/domains/graphql/types.ts +29 -0
  110. package/src/domains/info/operations/collectInfo.test.ts +16 -5
  111. package/src/domains/info/operations/collectInfo.ts +16 -19
  112. package/src/domains/llm/capabilities.test.ts +91 -0
  113. package/src/domains/llm/commands/capabilities.ts +28 -0
  114. package/src/domains/llm/commands/index.ts +1 -0
  115. package/src/domains/llm/data/commandReference.ts +90 -18
  116. package/src/domains/llm/data/decisionTrees.ts +15 -13
  117. package/src/domains/llm/data/index.ts +2 -0
  118. package/src/domains/llm/data/toolCatalog.ts +190 -0
  119. package/src/domains/llm/formatters/capabilities.ts +50 -0
  120. package/src/domains/llm/formatters/index.ts +1 -0
  121. package/src/domains/llm/index.ts +3 -2
  122. package/src/domains/llm/mcp/index.ts +1 -1
  123. package/src/domains/llm/mcp/specs.ts +59 -52
  124. package/src/domains/llm/types.ts +40 -0
  125. package/src/domains/modifier/commands/index.ts +2 -1
  126. package/src/domains/modifier/commands/list.ts +5 -38
  127. package/src/domains/modifier/commands/lookup.ts +8 -79
  128. package/src/domains/modifier/commands/sample.ts +53 -0
  129. package/src/domains/modifier/formatters/index.ts +2 -1
  130. package/src/domains/modifier/formatters/sample.ts +15 -0
  131. package/src/domains/modifier/index.ts +7 -3
  132. package/src/domains/modifier/mcp/specs.ts +38 -70
  133. package/src/domains/modifier/operations/index.ts +2 -1
  134. package/src/domains/modifier/operations/lookup.ts +9 -0
  135. package/src/domains/modifier/operations/sample.ts +32 -0
  136. package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
  137. package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
  138. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +15 -2
  139. package/src/domains/modifier/stories.test.ts +20 -0
  140. package/src/domains/modifier/stories.ts +77 -0
  141. package/src/domains/ontology/commands/list.ts +5 -58
  142. package/src/domains/ontology/commands/show.ts +6 -58
  143. package/src/domains/ontology/formatters/show.ts +7 -1
  144. package/src/domains/ontology/mcp/specs.ts +7 -84
  145. package/src/domains/ontology/stories.test.ts +36 -0
  146. package/src/domains/ontology/stories.ts +108 -0
  147. package/src/domains/refs/operations/index.ts +1 -1
  148. package/src/domains/refs/operations/paths.ts +29 -1
  149. package/src/domains/refs/operations/updateRefs.test.ts +6 -3
  150. package/src/domains/refs/operations/updateRefs.ts +2 -36
  151. package/src/domains/setup/commands/skills.test.ts +6 -2
  152. package/src/domains/setup/commands/skills.ts +6 -2
  153. package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
  154. package/src/domains/setup/helpers/runSetupTask.ts +62 -70
  155. package/src/domains/setup/operations/setupMcp.test.ts +6 -1
  156. package/src/domains/shared/bootStore.ts +92 -50
  157. package/src/domains/shared/compactUri.test.ts +11 -0
  158. package/src/domains/shared/compactUri.ts +10 -5
  159. package/src/domains/shared/context.ts +2 -2
  160. package/src/domains/shared/contracts.ts +12 -0
  161. package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
  162. package/src/domains/shared/createInteractivePromptSession.ts +236 -0
  163. package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
  164. package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
  165. package/src/domains/shared/loaders/bundledLoader.ts +206 -0
  166. package/src/domains/shared/loaders/gitLoader.ts +31 -0
  167. package/src/domains/shared/loaders/index.ts +9 -0
  168. package/src/domains/shared/loaders/localLoader.ts +87 -0
  169. package/src/domains/shared/loaders/readPackageDir.ts +147 -0
  170. package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
  171. package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
  172. package/src/domains/shared/lookupMany.test.ts +120 -0
  173. package/src/domains/shared/lookupMany.ts +43 -3
  174. package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
  175. package/src/domains/shared/mergeAndParseRefs.ts +44 -0
  176. package/src/domains/shared/packages.ts +14 -120
  177. package/src/domains/shared/pickRandom.ts +22 -0
  178. package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
  179. package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
  180. package/src/domains/shared/runtime.ts +7 -63
  181. package/src/domains/shared/sampleFormatters.ts +47 -0
  182. package/src/domains/shared/semanticPackage.ts +112 -0
  183. package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
  184. package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
  185. package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
  186. package/src/domains/shared/stories/compileLookupTool.ts +96 -0
  187. package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
  188. package/src/domains/shared/stories/compileReadCommand.ts +48 -0
  189. package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
  190. package/src/domains/shared/stories/compileReadTool.ts +38 -0
  191. package/src/domains/shared/stories/condense.test.ts +18 -0
  192. package/src/domains/shared/stories/condense.ts +15 -0
  193. package/src/domains/shared/stories/index.ts +20 -0
  194. package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
  195. package/src/domains/shared/stories/normalizeNames.ts +22 -0
  196. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
  197. package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
  198. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
  199. package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
  200. package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
  201. package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
  202. package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
  203. package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
  204. package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
  205. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
  206. package/src/domains/shared/stories/pack/index.ts +28 -0
  207. package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
  208. package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
  209. package/src/domains/shared/stories/pack/types.ts +104 -0
  210. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
  211. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
  212. package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
  213. package/src/domains/shared/stories/projectCliParameters.ts +57 -0
  214. package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
  215. package/src/domains/shared/stories/projectToolParams.ts +32 -0
  216. package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
  217. package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
  218. package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
  219. package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
  220. package/src/domains/shared/stories/types.ts +171 -0
  221. package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
  222. package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
  223. package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
  224. package/src/domains/shared/suggestions/expandGlob.ts +52 -0
  225. package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
  226. package/src/domains/shared/suggestions/index.ts +7 -0
  227. package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
  228. package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
  229. package/src/domains/shared/suggestions/suggestNames.ts +130 -0
  230. package/src/domains/shared/types/index.ts +1 -0
  231. package/src/domains/shared/types/runtime.ts +4 -1
  232. package/src/domains/shared/types/sample.ts +17 -0
  233. package/src/domains/skill/commands/index.ts +3 -1
  234. package/src/domains/skill/commands/list.test.ts +6 -2
  235. package/src/domains/skill/commands/list.ts +5 -63
  236. package/src/domains/skill/commands/lookup.test.ts +118 -0
  237. package/src/domains/skill/commands/lookup.ts +14 -0
  238. package/src/domains/skill/formatters/index.ts +1 -0
  239. package/src/domains/skill/formatters/lookup.test.ts +46 -0
  240. package/src/domains/skill/formatters/lookup.ts +57 -0
  241. package/src/domains/skill/helpers/resolveSkillSources.ts +48 -16
  242. package/src/domains/skill/index.ts +7 -3
  243. package/src/domains/skill/mcp/specs.ts +10 -29
  244. package/src/domains/skill/operations/discover.test.ts +61 -30
  245. package/src/domains/skill/operations/discover.ts +28 -37
  246. package/src/domains/skill/operations/index.ts +2 -1
  247. package/src/domains/skill/operations/list.test.ts +15 -7
  248. package/src/domains/skill/operations/lookup.test.ts +91 -0
  249. package/src/domains/skill/operations/lookup.ts +70 -0
  250. package/src/domains/skill/stories.test.ts +43 -0
  251. package/src/domains/skill/stories.ts +103 -0
  252. package/src/domains/skill/types.ts +8 -0
  253. package/src/domains/standard/commands/index.ts +2 -1
  254. package/src/domains/standard/commands/list.ts +5 -84
  255. package/src/domains/standard/commands/lookup.ts +8 -93
  256. package/src/domains/standard/commands/sample.ts +57 -0
  257. package/src/domains/standard/formatters/index.ts +2 -1
  258. package/src/domains/standard/formatters/sample.ts +15 -0
  259. package/src/domains/standard/index.ts +8 -1
  260. package/src/domains/standard/mcp/specs.ts +44 -152
  261. package/src/domains/standard/operations/index.ts +2 -1
  262. package/src/domains/standard/operations/lookup.ts +9 -0
  263. package/src/domains/standard/operations/sample.ts +35 -0
  264. package/src/domains/standard/orchestration/resolveStandardLookup.ts +15 -2
  265. package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
  266. package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
  267. package/src/domains/standard/stories.test.ts +24 -0
  268. package/src/domains/standard/stories.ts +151 -0
  269. package/src/domains/tier/commands/list.ts +5 -55
  270. package/src/domains/tier/mcp/specs.ts +5 -31
  271. package/src/domains/tier/stories.test.ts +22 -0
  272. package/src/domains/tier/stories.ts +52 -0
  273. package/src/domains/token/commands/index.ts +2 -1
  274. package/src/domains/token/commands/sample.ts +53 -0
  275. package/src/domains/token/formatters/index.ts +2 -1
  276. package/src/domains/token/formatters/sample.ts +16 -0
  277. package/src/domains/token/index.ts +8 -1
  278. package/src/domains/token/mcp/specs.ts +52 -2
  279. package/src/domains/token/operations/index.ts +2 -1
  280. package/src/domains/token/operations/lookup.ts +9 -0
  281. package/src/domains/token/operations/sample.ts +32 -0
  282. package/src/domains/token/orchestration/resolveTokenLookup.ts +15 -2
  283. package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
  284. package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
  285. package/src/domains/trace/commands/clear.ts +71 -0
  286. package/src/domains/trace/commands/index.ts +5 -0
  287. package/src/domains/trace/commands/sessions.ts +32 -0
  288. package/src/domains/trace/commands/show.ts +145 -0
  289. package/src/domains/trace/formatters/index.ts +9 -0
  290. package/src/domains/trace/formatters/sessions.ts +80 -0
  291. package/src/domains/trace/formatters/show.ts +122 -0
  292. package/src/domains/trace/index.ts +17 -0
  293. package/src/domains/trace/operations/index.ts +8 -0
  294. package/src/domains/trace/operations/listSessions.test.ts +55 -0
  295. package/src/domains/trace/operations/listSessions.ts +52 -0
  296. package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
  297. package/src/domains/trace/operations/readTraceLog.ts +104 -0
  298. package/src/domains/trace/tracePlugin.test.ts +171 -0
  299. package/src/domains/trace/tracePlugin.ts +159 -0
  300. package/src/domains/trace/types.ts +65 -0
  301. package/src/error/PragmaError.ts +15 -2
  302. package/src/error/types.ts +15 -1
  303. package/src/mcp/error/serializeErrorPayload.ts +7 -0
  304. package/src/mcp/error/types.ts +8 -0
  305. package/src/mcp/registerResources.test.ts +405 -75
  306. package/src/mcp/registerResources.ts +5 -299
  307. package/src/mcp/resources/buildGraphIndex.ts +167 -0
  308. package/src/mcp/resources/buildResourceList.test.ts +97 -0
  309. package/src/mcp/resources/buildResourceList.ts +176 -0
  310. package/src/mcp/resources/classifyEntity.test.ts +40 -0
  311. package/src/mcp/resources/classifyEntity.ts +27 -0
  312. package/src/mcp/resources/constants.ts +105 -0
  313. package/src/mcp/resources/displayPredicates.ts +62 -0
  314. package/src/mcp/resources/pickFirstValue.test.ts +29 -0
  315. package/src/mcp/resources/pickFirstValue.ts +26 -0
  316. package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
  317. package/src/mcp/resources/pickPrimaryType.ts +26 -0
  318. package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
  319. package/src/mcp/resources/rankUriCompletions.ts +64 -0
  320. package/src/mcp/resources/readEntity.ts +169 -0
  321. package/src/mcp/resources/registerResources.ts +126 -0
  322. package/src/mcp/resources/types.ts +128 -0
  323. package/src/mcp/tools/index.ts +9 -0
  324. package/src/mcp/tools/registerTools.test.ts +84 -12
  325. package/src/mcp/types.ts +4 -0
  326. package/src/mcp/utils/wrapTool.test.ts +1 -0
  327. package/src/pipeline/collectCommands.ts +15 -2
  328. package/src/pipeline/renderError.ts +15 -0
  329. package/src/pipeline/resolveCommandKind.ts +8 -1
  330. package/src/pipeline/runCli.ts +51 -6
  331. package/src/testing/createTestMcpClient.ts +5 -1
  332. package/src/testing/graphqlFixtures.ts +72 -0
  333. package/src/testing/index.ts +17 -0
  334. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
  335. package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
  336. package/src/testing/integration/executor-execution.test.ts +101 -0
  337. package/src/testing/integration/executor-golden.test.ts +145 -0
  338. package/src/testing/integration/mcp-surface.test.ts +11 -5
  339. package/src/testing/integration/parity.test.ts +17 -12
  340. package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
  341. package/src/testing/setupXdgIsolation.ts +15 -0
  342. package/src/testing/standardParityFixtures.ts +92 -0
  343. package/src/testing/storyFixtures.ts +63 -0
  344. package/src/pipeline/runInteractiveCommand.ts +0 -198
@@ -1,7 +1,10 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
3
- import type { PackageRef } from "../../refs/operations/parseRef.js";
4
- import { resolvePackages } from "../../shared/packages.js";
1
+ import { parsePackageEntry } from "../../refs/operations/parseRef.js";
2
+ import {
3
+ createGitLoader,
4
+ createLocalLoader,
5
+ } from "../../shared/loaders/index.js";
6
+ import { DEFAULT_PACKAGES } from "../../shared/packages.js";
7
+ import type { SemanticPackage } from "../../shared/semanticPackage.js";
5
8
 
6
9
  /** A resolved skill source directory from an installed package. */
7
10
  export interface SkillSource {
@@ -12,27 +15,56 @@ export interface SkillSource {
12
15
  }
13
16
 
14
17
  /**
15
- * Resolve skill source directories from resolved packages.
18
+ * Resolve skill source directories from semantic packages.
16
19
  *
17
- * Auto-detects skills by checking for a `skills/` subdirectory
18
- * in each resolved package root.
20
+ * When `packages` is provided (from PragmaRuntime), extracts skill entries
21
+ * directly. Otherwise, resolves packages using local and git loaders
22
+ * (bundled loader is excluded since it has no filesystem skills).
19
23
  *
20
- * @param refs - Parsed package references. Omit for defaults.
24
+ * @param packages - Pre-resolved semantic packages (from boot).
21
25
  * @returns Array of resolved skill sources with absolute paths.
22
- * @note Impure
26
+ * @note Impure when packages is omitted (reads filesystem).
23
27
  */
24
28
  export default function resolveSkillSources(
25
- refs?: ReadonlyArray<PackageRef>,
29
+ packages?: readonly SemanticPackage[],
30
+ ): SkillSource[] {
31
+ if (packages) {
32
+ return extractSkillSources(packages);
33
+ }
34
+
35
+ // Fallback: resolve packages ourselves (skills need filesystem paths,
36
+ // so we skip the bundled loader here)
37
+ return resolveSkillSourcesFromRefs();
38
+ }
39
+
40
+ function extractSkillSources(
41
+ packages: readonly SemanticPackage[],
26
42
  ): SkillSource[] {
27
43
  const sources: SkillSource[] = [];
28
- const resolved = resolvePackages(refs);
44
+ for (const pkg of packages) {
45
+ for (const skill of pkg.skills) {
46
+ sources.push({ dir: skill.dir, packageName: pkg.name });
47
+ }
48
+ }
49
+ return sources;
50
+ }
29
51
 
30
- for (const { pkg, dir } of resolved) {
31
- const skillsDir = join(dir, "skills");
32
- if (existsSync(skillsDir)) {
33
- sources.push({ dir: skillsDir, packageName: pkg });
52
+ function resolveSkillSourcesFromRefs(): SkillSource[] {
53
+ const refs = DEFAULT_PACKAGES.map(parsePackageEntry);
54
+ const loaders = [createLocalLoader(), createGitLoader()];
55
+
56
+ // resolveSemanticPackages is async but skills resolution is sync.
57
+ // Use local+git loaders which are both sync.
58
+ const packages: SemanticPackage[] = [];
59
+ for (const ref of refs) {
60
+ for (const loader of loaders) {
61
+ const resolved = loader.resolve(ref);
62
+ if (resolved && !(resolved instanceof Promise)) {
63
+ packages.push(resolved);
64
+ break;
65
+ }
34
66
  }
35
67
  }
36
68
 
37
- return sources;
69
+ return extractSkillSources(packages);
38
70
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { CommandDefinition } from "@canonical/cli-core";
4
4
  import type { PragmaContext } from "../shared/context.js";
5
- import { listCommand } from "./commands/index.js";
5
+ import { listCommand, lookupCommand } from "./commands/index.js";
6
6
 
7
7
  /**
8
8
  * Return all skill command definitions.
@@ -11,9 +11,13 @@ import { listCommand } from "./commands/index.js";
11
11
  * @returns An array of CommandDefinitions for the skill subcommands.
12
12
  */
13
13
  export function commands(ctx: PragmaContext): CommandDefinition[] {
14
- return [listCommand(ctx)];
14
+ return [listCommand(ctx), lookupCommand(ctx)];
15
15
  }
16
16
 
17
17
  export { specs as mcpSpecs } from "./mcp/index.js";
18
18
  export type { SkillListResult } from "./operations/index.js";
19
- export { discoverSkills, listSkills } from "./operations/index.js";
19
+ export {
20
+ discoverSkills,
21
+ listSkills,
22
+ lookupSkill,
23
+ } from "./operations/index.js";
@@ -1,38 +1,19 @@
1
1
  /**
2
- * MCP tool specs for the skill domain.
2
+ * MCP tool specs for the skill domain — skill_list and skill_lookup,
3
+ * compiled from the skill read stories in `../stories.ts`. skill_lookup
4
+ * serves full SKILL.md content so agents can load skills over MCP.
3
5
  */
4
6
 
7
+ import {
8
+ compileLookupTool,
9
+ compileReadTool,
10
+ } from "../../shared/stories/index.js";
5
11
  import type { ToolSpec } from "../../shared/ToolSpec.js";
6
- import { listFormatters as skillListFmt } from "../formatters/index.js";
7
- import { listSkills } from "../operations/index.js";
12
+ import { skillListStory, skillLookupStory } from "../stories.js";
8
13
 
9
14
  const specs: readonly ToolSpec[] = [
10
- {
11
- name: "skill_list",
12
- description: "List available agent skills from design system packages.",
13
- params: {
14
- condensed: {
15
- type: "boolean",
16
- description: "Token-optimized output",
17
- optional: true,
18
- },
19
- },
20
- readOnly: true,
21
- async execute(rt, { condensed }) {
22
- const { skills, sources } = await listSkills(rt.cwd);
23
-
24
- if (condensed) {
25
- const text = skillListFmt.llm({ skills, sources, detailed: false });
26
- return {
27
- condensed: true,
28
- text,
29
- tokens: `~${Math.ceil(text.length / 4)}`,
30
- };
31
- }
32
-
33
- return { data: { skills, sources }, meta: { count: skills.length } };
34
- },
35
- },
15
+ compileReadTool(skillListStory),
16
+ compileLookupTool(skillLookupStory),
36
17
  ];
37
18
 
38
19
  export default specs;
@@ -7,38 +7,72 @@ import discoverSkills from "./discover.js";
7
7
 
8
8
  const TMP_ROOT = join(tmpdir(), `pragma-skill-discover-${Date.now()}`);
9
9
 
10
- /** Test skill sources that mirror the real package layout. */
10
+ /**
11
+ * Test skill sources that mirror the real loader granularity: each source
12
+ * is one skill directory (`<package>/skills/<name>`) containing SKILL.md,
13
+ * as produced by `readPackageDir`.
14
+ */
11
15
  const TEST_SOURCES: SkillSource[] = [
12
16
  {
13
- dir: join(TMP_ROOT, "node_modules/@canonical/design-system/skills"),
17
+ dir: join(
18
+ TMP_ROOT,
19
+ "node_modules/@canonical/design-system/skills/design-audit",
20
+ ),
14
21
  packageName: "@canonical/design-system",
15
22
  },
16
23
  {
17
- dir: join(TMP_ROOT, "node_modules/@canonical/anatomy-dsl/skills"),
24
+ dir: join(
25
+ TMP_ROOT,
26
+ "node_modules/@canonical/design-system/skills/ds-review",
27
+ ),
28
+ packageName: "@canonical/design-system",
29
+ },
30
+ {
31
+ dir: join(
32
+ TMP_ROOT,
33
+ "node_modules/@canonical/anatomy-dsl/skills/anatomy-author",
34
+ ),
18
35
  packageName: "@canonical/anatomy-dsl",
19
36
  },
20
37
  {
21
- dir: join(TMP_ROOT, "node_modules/@canonical/pragma-cli/skills"),
38
+ dir: join(
39
+ TMP_ROOT,
40
+ "node_modules/@canonical/pragma-cli/skills/scaffold-story",
41
+ ),
42
+ packageName: "@canonical/pragma-cli",
43
+ },
44
+ {
45
+ dir: join(
46
+ TMP_ROOT,
47
+ "node_modules/@canonical/pragma-cli/skills/broken-skill",
48
+ ),
22
49
  packageName: "@canonical/pragma-cli",
23
50
  },
51
+ {
52
+ dir: join(
53
+ TMP_ROOT,
54
+ "node_modules/@canonical/design-system/skills/no-skill-md",
55
+ ),
56
+ packageName: "@canonical/design-system",
57
+ },
24
58
  ];
25
59
 
26
- function writeSkillMd(
27
- source: SkillSource,
28
- skillName: string,
29
- content: string,
30
- ): void {
31
- const dir = join(source.dir, skillName);
32
- mkdirSync(dir, { recursive: true });
33
- writeFileSync(join(dir, "SKILL.md"), content);
60
+ function writeSkillMd(source: SkillSource, content: string): void {
61
+ mkdirSync(source.dir, { recursive: true });
62
+ writeFileSync(join(source.dir, "SKILL.md"), content);
63
+ }
64
+
65
+ function sourceAt(index: number): SkillSource {
66
+ const source = TEST_SOURCES.at(index);
67
+ if (!source) throw new Error(`no test source at index ${index}`);
68
+ return source;
34
69
  }
35
70
 
36
71
  beforeAll(() => {
37
72
  mkdirSync(TMP_ROOT, { recursive: true });
38
73
 
39
74
  writeSkillMd(
40
- TEST_SOURCES[0],
41
- "design-audit",
75
+ sourceAt(0),
42
76
  `---
43
77
  name: design-audit
44
78
  description: Audit a component against DS specs
@@ -51,8 +85,7 @@ metadata:
51
85
  );
52
86
 
53
87
  writeSkillMd(
54
- TEST_SOURCES[0],
55
- "ds-review",
88
+ sourceAt(1),
56
89
  `---
57
90
  name: ds-review
58
91
  description: Review a PR for DS compliance
@@ -61,8 +94,7 @@ description: Review a PR for DS compliance
61
94
  );
62
95
 
63
96
  writeSkillMd(
64
- TEST_SOURCES[2],
65
- "scaffold-story",
97
+ sourceAt(3),
66
98
  `---
67
99
  name: scaffold-story
68
100
  description: Generate a Storybook story from anatomy
@@ -70,20 +102,19 @@ description: Generate a Storybook story from anatomy
70
102
  `,
71
103
  );
72
104
 
73
- // Invalid frontmatter — should be skipped with warning
105
+ // Invalid frontmatter (missing description) — should be skipped with warning
74
106
  writeSkillMd(
75
- TEST_SOURCES[2],
76
- "broken-skill",
107
+ sourceAt(4),
77
108
  `---
78
109
  name: broken-skill
79
110
  ---
80
111
  `,
81
112
  );
82
113
 
83
- // File without SKILL.md — should be skipped
84
- const noSkillDir = join(TEST_SOURCES[0].dir, "no-skill-md");
85
- mkdirSync(noSkillDir, { recursive: true });
86
- writeFileSync(join(noSkillDir, "README.md"), "Not a skill");
114
+ // Source directory without a SKILL.md — should be skipped
115
+ const noSkillMd = sourceAt(5);
116
+ mkdirSync(noSkillMd.dir, { recursive: true });
117
+ writeFileSync(join(noSkillMd.dir, "README.md"), "Not a skill");
87
118
  });
88
119
 
89
120
  afterAll(() => {
@@ -100,15 +131,15 @@ describe("discoverSkills", () => {
100
131
  });
101
132
 
102
133
  it("skips missing source directories", async () => {
103
- // TEST_SOURCES[1] (anatomy-dsl) was not created
134
+ // TEST_SOURCES[2] (anatomy-dsl) was never created on disk
104
135
  const skills = await discoverSkills(TMP_ROOT, TEST_SOURCES);
105
136
  const packages = [...new Set(skills.map((s) => s.sourcePackage))];
106
137
  expect(packages).not.toContain("@canonical/anatomy-dsl");
107
138
  });
108
139
 
109
- it("skips folders without SKILL.md", async () => {
140
+ it("skips source directories without SKILL.md", async () => {
110
141
  const skills = await discoverSkills(TMP_ROOT, TEST_SOURCES);
111
- const names = skills.map((s) => s.name);
142
+ const names = skills.map((s) => s.folderName);
112
143
  expect(names).not.toContain("no-skill-md");
113
144
  });
114
145
 
@@ -123,7 +154,7 @@ describe("discoverSkills", () => {
123
154
  const audit = skills.find((s) => s.name === "design-audit");
124
155
  expect(audit?.sourcePackage).toBe("@canonical/design-system");
125
156
  expect(audit?.folderName).toBe("design-audit");
126
- expect(audit?.sourcePath).toBe(join(TEST_SOURCES[0].dir, "design-audit"));
157
+ expect(audit?.sourcePath).toBe(sourceAt(0).dir);
127
158
  });
128
159
 
129
160
  it("preserves frontmatter metadata", async () => {
@@ -135,7 +166,7 @@ describe("discoverSkills", () => {
135
166
  it("returns empty array when no sources available", async () => {
136
167
  const emptySources: SkillSource[] = [
137
168
  {
138
- dir: "/nonexistent/path/skills",
169
+ dir: "/nonexistent/path/skills/some-skill",
139
170
  packageName: "@canonical/nope",
140
171
  },
141
172
  ];
@@ -1,5 +1,5 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
2
- import { resolve } from "node:path";
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, resolve } from "node:path";
3
3
  import {
4
4
  parseFrontmatter,
5
5
  resolveSkillSources,
@@ -8,14 +8,18 @@ import {
8
8
  import type { DiscoveredSkill } from "../types.js";
9
9
 
10
10
  /**
11
- * Discover skills by scanning source package directories for subdirectories
12
- * containing a valid `SKILL.md` file. Missing source paths and invalid
13
- * frontmatter are skipped gracefully.
11
+ * Discover skills from resolved skill source directories.
12
+ *
13
+ * Each source directory is one skill folder (`<package>/skills/<name>`)
14
+ * containing a `SKILL.md` with YAML frontmatter — the granularity
15
+ * produced by the package loaders' `readPackageDir`. Missing files and
16
+ * invalid frontmatter are skipped gracefully (invalid frontmatter is
17
+ * reported on stderr).
14
18
  *
15
19
  * @param _cwd - Working directory (unused, sources are resolved globally).
16
20
  * @param overrideSources - Optional override for skill source definitions.
17
21
  * @returns Array of discovered skills with parsed frontmatter.
18
- * @note Impure
22
+ * @note Impure — reads SKILL.md files from the filesystem.
19
23
  */
20
24
  export default async function discoverSkills(
21
25
  _cwd: string,
@@ -25,43 +29,30 @@ export default async function discoverSkills(
25
29
  const sources = overrideSources ?? resolveSkillSources();
26
30
 
27
31
  for (const source of sources) {
28
- let entries: string[];
32
+ const skillMdPath = resolve(source.dir, "SKILL.md");
33
+ let content: string;
29
34
  try {
30
- entries = await readdir(source.dir);
35
+ content = await readFile(skillMdPath, "utf-8");
31
36
  } catch {
32
37
  continue;
33
38
  }
34
39
 
35
- for (const entry of entries) {
36
- const entryPath = resolve(source.dir, entry);
37
- const entryStat = await stat(entryPath).catch(() => null);
38
- if (!entryStat?.isDirectory()) continue;
39
-
40
- const skillMdPath = resolve(entryPath, "SKILL.md");
41
- let content: string;
42
- try {
43
- content = await readFile(skillMdPath, "utf-8");
44
- } catch {
45
- continue;
46
- }
47
-
48
- const frontmatter = parseFrontmatter(content);
49
- if (!frontmatter) {
50
- process.stderr.write(
51
- `Warning: invalid SKILL.md frontmatter in ${skillMdPath}\n`,
52
- );
53
- continue;
54
- }
55
-
56
- skills.push({
57
- name: frontmatter.name,
58
- description: frontmatter.description,
59
- sourcePath: entryPath,
60
- sourcePackage: source.packageName,
61
- folderName: entry,
62
- frontmatter,
63
- });
40
+ const frontmatter = parseFrontmatter(content);
41
+ if (!frontmatter) {
42
+ process.stderr.write(
43
+ `Warning: invalid SKILL.md frontmatter in ${skillMdPath}\n`,
44
+ );
45
+ continue;
64
46
  }
47
+
48
+ skills.push({
49
+ name: frontmatter.name,
50
+ description: frontmatter.description,
51
+ sourcePath: source.dir,
52
+ sourcePackage: source.packageName,
53
+ folderName: basename(source.dir),
54
+ frontmatter,
55
+ });
65
56
  }
66
57
 
67
58
  return skills;
@@ -1,4 +1,5 @@
1
- /** @module Skill operations: discovery and listing with source metadata. */
1
+ /** @module Skill operations: discovery, listing, and content lookup. */
2
2
  export { default as discoverSkills } from "./discover.js";
3
3
  export type { SkillListResult } from "./list.js";
4
4
  export { default as listSkills } from "./list.js";
5
+ export { default as lookupSkill } from "./lookup.js";
@@ -7,25 +7,33 @@ import listSkills from "./list.js";
7
7
 
8
8
  const TMP_ROOT = join(tmpdir(), `pragma-skill-list-${Date.now()}`);
9
9
 
10
+ /** Each source is one skill directory, matching the loader granularity. */
10
11
  const TEST_SOURCES: SkillSource[] = [
11
12
  {
12
- dir: join(TMP_ROOT, "node_modules/@canonical/design-system/skills"),
13
+ dir: join(
14
+ TMP_ROOT,
15
+ "node_modules/@canonical/design-system/skills/design-audit",
16
+ ),
13
17
  packageName: "@canonical/design-system",
14
18
  },
15
19
  {
16
- dir: join(TMP_ROOT, "node_modules/@canonical/anatomy-dsl/skills"),
20
+ dir: join(
21
+ TMP_ROOT,
22
+ "node_modules/@canonical/anatomy-dsl/skills/anatomy-author",
23
+ ),
17
24
  packageName: "@canonical/anatomy-dsl",
18
25
  },
19
26
  {
20
- dir: join(TMP_ROOT, "node_modules/@canonical/pragma-cli/skills"),
27
+ dir: join(
28
+ TMP_ROOT,
29
+ "node_modules/@canonical/pragma-cli/skills/scaffold-story",
30
+ ),
21
31
  packageName: "@canonical/pragma-cli",
22
32
  },
23
33
  ];
24
34
 
25
35
  beforeAll(() => {
26
- mkdirSync(TMP_ROOT, { recursive: true });
27
-
28
- const skillDir = join(TEST_SOURCES[0].dir, "design-audit");
36
+ const skillDir = TEST_SOURCES.at(0)?.dir ?? "";
29
37
  mkdirSync(skillDir, { recursive: true });
30
38
  writeFileSync(
31
39
  join(skillDir, "SKILL.md"),
@@ -45,7 +53,7 @@ describe("listSkills", () => {
45
53
  it("returns discovered skills", async () => {
46
54
  const { skills } = await listSkills(TMP_ROOT, TEST_SOURCES);
47
55
  expect(skills).toHaveLength(1);
48
- expect(skills[0].name).toBe("design-audit");
56
+ expect(skills.at(0)?.name).toBe("design-audit");
49
57
  });
50
58
 
51
59
  it("reports source availability", async () => {
@@ -0,0 +1,91 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
5
+ import { PragmaError } from "#error";
6
+ import type { SkillSource } from "../helpers/index.js";
7
+ import lookupSkill from "./lookup.js";
8
+
9
+ const TMP_ROOT = join(tmpdir(), `pragma-skill-lookup-${Date.now()}`);
10
+
11
+ /** Each source is one skill directory, matching the loader granularity. */
12
+ const TEST_SOURCES: SkillSource[] = [
13
+ {
14
+ dir: join(
15
+ TMP_ROOT,
16
+ "node_modules/@canonical/design-system/skills/design-auditor",
17
+ ),
18
+ packageName: "@canonical/design-system",
19
+ },
20
+ {
21
+ dir: join(
22
+ TMP_ROOT,
23
+ "node_modules/@canonical/code-standards/skills/add-standard",
24
+ ),
25
+ packageName: "@canonical/code-standards",
26
+ },
27
+ ];
28
+
29
+ const AUDITOR_CONTENT = `---
30
+ name: design-auditor
31
+ description: Audit design system coverage
32
+ ---
33
+
34
+ # Design Auditor
35
+
36
+ Full instructions here.
37
+ `;
38
+
39
+ beforeAll(() => {
40
+ const auditor = TEST_SOURCES.at(0)?.dir ?? "";
41
+ mkdirSync(auditor, { recursive: true });
42
+ writeFileSync(join(auditor, "SKILL.md"), AUDITOR_CONTENT);
43
+ writeFileSync(join(auditor, "AUDIT_SPEC.md"), "# Spec");
44
+
45
+ const addStandard = TEST_SOURCES.at(1)?.dir ?? "";
46
+ mkdirSync(addStandard, { recursive: true });
47
+ writeFileSync(
48
+ join(addStandard, "SKILL.md"),
49
+ `---
50
+ name: add-standard
51
+ description: Create a new code standard
52
+ ---
53
+ `,
54
+ );
55
+ });
56
+
57
+ afterAll(() => {
58
+ rmSync(TMP_ROOT, { recursive: true, force: true });
59
+ });
60
+
61
+ describe("lookupSkill", () => {
62
+ it("returns the skill with full content and companion files", async () => {
63
+ const skill = await lookupSkill(TMP_ROOT, "design-auditor", TEST_SOURCES);
64
+ expect(skill.name).toBe("design-auditor");
65
+ expect(skill.content).toBe(AUDITOR_CONTENT);
66
+ expect(skill.files).toEqual(["AUDIT_SPEC.md"]);
67
+ expect(skill.sourcePackage).toBe("@canonical/design-system");
68
+ });
69
+
70
+ it("matches case-insensitively", async () => {
71
+ const skill = await lookupSkill(TMP_ROOT, "Design-Auditor", TEST_SOURCES);
72
+ expect(skill.name).toBe("design-auditor");
73
+ });
74
+
75
+ it("reports no companion files when SKILL.md is alone", async () => {
76
+ const skill = await lookupSkill(TMP_ROOT, "add-standard", TEST_SOURCES);
77
+ expect(skill.files).toEqual([]);
78
+ });
79
+
80
+ it("throws not-found with suggestions for unknown names", async () => {
81
+ await expect(
82
+ lookupSkill(TMP_ROOT, "design-audit", TEST_SOURCES),
83
+ ).rejects.toMatchObject({
84
+ code: "ENTITY_NOT_FOUND",
85
+ suggestions: ["design-auditor"],
86
+ });
87
+ await expect(
88
+ lookupSkill(TMP_ROOT, "nope", TEST_SOURCES),
89
+ ).rejects.toBeInstanceOf(PragmaError);
90
+ });
91
+ });
@@ -0,0 +1,70 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { PragmaError } from "#error";
4
+ import { suggestNames } from "../../shared/suggestions/index.js";
5
+ import type { SkillSource } from "../helpers/index.js";
6
+ import type { SkillDetailed } from "../types.js";
7
+ import discoverSkills from "./discover.js";
8
+
9
+ /**
10
+ * Look up one skill by name or folder name and serve its content.
11
+ *
12
+ * Matches case-insensitively against the frontmatter name and the skill's
13
+ * folder name, then reads the full SKILL.md text plus the names of any
14
+ * companion files shipped alongside it (e.g. spec documents).
15
+ *
16
+ * @param cwd - Working directory.
17
+ * @param query - Skill name or folder name to look up.
18
+ * @param overrideSources - Optional override for skill source definitions.
19
+ * @returns The matched skill with content and companion file names.
20
+ * @throws PragmaError with code ENTITY_NOT_FOUND and ranked suggestions
21
+ * when no skill matches.
22
+ * @note Impure — reads SKILL.md and the skill directory listing.
23
+ */
24
+ export default async function lookupSkill(
25
+ cwd: string,
26
+ query: string,
27
+ overrideSources?: SkillSource[],
28
+ ): Promise<SkillDetailed> {
29
+ const skills = await discoverSkills(cwd, overrideSources);
30
+ const queryLower = query.toLowerCase();
31
+ const match = skills.find(
32
+ (skill) =>
33
+ skill.name.toLowerCase() === queryLower ||
34
+ skill.folderName.toLowerCase() === queryLower,
35
+ );
36
+
37
+ if (!match) {
38
+ throw PragmaError.notFound("skill", query, {
39
+ suggestions: suggestNames(
40
+ query,
41
+ skills.map((skill) => skill.name),
42
+ ),
43
+ recovery: {
44
+ message: "List available skills.",
45
+ cli: "pragma skill list",
46
+ mcp: { tool: "skill_list" },
47
+ },
48
+ });
49
+ }
50
+
51
+ const content = await readFile(
52
+ resolve(match.sourcePath, "SKILL.md"),
53
+ "utf-8",
54
+ );
55
+ const files = await listCompanionFiles(match.sourcePath);
56
+
57
+ return { ...match, content, files };
58
+ }
59
+
60
+ async function listCompanionFiles(dir: string): Promise<readonly string[]> {
61
+ try {
62
+ const entries = await readdir(dir, { withFileTypes: true });
63
+ return entries
64
+ .filter((entry) => entry.isFile() && entry.name !== "SKILL.md")
65
+ .map((entry) => entry.name)
66
+ .sort();
67
+ } catch {
68
+ return [];
69
+ }
70
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { skillListStory } from "./stories.js";
3
+ import type { SkillSource } from "./types.js";
4
+
5
+ const availableSource: SkillSource = {
6
+ path: "/pkgs/design-system/skills/design-auditor",
7
+ packageName: "@canonical/design-system",
8
+ available: true,
9
+ };
10
+
11
+ const unavailableSource: SkillSource = {
12
+ path: "/pkgs/missing/skills/gone",
13
+ packageName: "@canonical/missing",
14
+ available: false,
15
+ };
16
+
17
+ describe("skillListStory", () => {
18
+ it("passes the CLI detailed flag into the formatter input", () => {
19
+ const data = { skills: [], sources: [] };
20
+ expect(skillListStory.toOutput(data, { detailed: true }).detailed).toBe(
21
+ true,
22
+ );
23
+ expect(skillListStory.toOutput(data, {}).detailed).toBe(false);
24
+ });
25
+
26
+ it("suggests installing packages when no source is available", () => {
27
+ const error = skillListStory.emptyError?.(
28
+ { skills: [], sources: [unavailableSource] },
29
+ {},
30
+ );
31
+ expect(error?.recovery?.message).toBe("Install @canonical packages first");
32
+ });
33
+
34
+ it("reports missing SKILL.md files when sources are available", () => {
35
+ const error = skillListStory.emptyError?.(
36
+ { skills: [], sources: [availableSource] },
37
+ {},
38
+ );
39
+ expect(error?.recovery?.message).toBe(
40
+ "No SKILL.md files found in source packages",
41
+ );
42
+ });
43
+ });