@canonical/pragma-cli 0.18.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 (439) hide show
  1. package/README.md +377 -0
  2. package/package.json +78 -0
  3. package/src/bin.test.ts +52 -0
  4. package/src/bin.ts +19 -0
  5. package/src/compile-validation.runner.ts +137 -0
  6. package/src/compile-validation.ts +192 -0
  7. package/src/completions/computeSocketPath.test.ts +26 -0
  8. package/src/completions/computeSocketPath.ts +16 -0
  9. package/src/completions/constants.ts +18 -0
  10. package/src/completions/handleQuery.test.ts +93 -0
  11. package/src/completions/handleQuery.ts +30 -0
  12. package/src/completions/index.ts +14 -0
  13. package/src/completions/queryCompletions.test.ts +100 -0
  14. package/src/completions/queryCompletions.ts +61 -0
  15. package/src/completions/querySocket.test.ts +70 -0
  16. package/src/completions/querySocket.ts +44 -0
  17. package/src/completions/startCompletionsServer.test.ts +74 -0
  18. package/src/completions/startCompletionsServer.ts +75 -0
  19. package/src/completions/startCompletionsServer.unit.test.ts +131 -0
  20. package/src/completions/waitForSocket.test.ts +27 -0
  21. package/src/completions/waitForSocket.ts +27 -0
  22. package/src/config/configExists.ts +14 -0
  23. package/src/config/index.ts +6 -0
  24. package/src/config/readConfig.test.ts +152 -0
  25. package/src/config/readConfig.ts +64 -0
  26. package/src/config/resolveConfigPath.test.ts +20 -0
  27. package/src/config/resolveConfigPath.ts +11 -0
  28. package/src/config/types.ts +26 -0
  29. package/src/config/writeConfig.ts +49 -0
  30. package/src/constants.ts +78 -0
  31. package/src/domains/block/blockConfig.ts +53 -0
  32. package/src/domains/block/commands/index.ts +4 -0
  33. package/src/domains/block/commands/list.test.ts +111 -0
  34. package/src/domains/block/commands/list.ts +69 -0
  35. package/src/domains/block/commands/lookup.test.ts +187 -0
  36. package/src/domains/block/commands/lookup.ts +199 -0
  37. package/src/domains/block/formatters/index.ts +4 -0
  38. package/src/domains/block/formatters/list.test.ts +107 -0
  39. package/src/domains/block/formatters/list.ts +64 -0
  40. package/src/domains/block/formatters/lookup.test.ts +317 -0
  41. package/src/domains/block/formatters/lookup.ts +354 -0
  42. package/src/domains/block/formatters/types.ts +16 -0
  43. package/src/domains/block/helpers/index.ts +4 -0
  44. package/src/domains/block/helpers/resolveAspects.test.ts +64 -0
  45. package/src/domains/block/helpers/resolveAspects.ts +35 -0
  46. package/src/domains/block/index.ts +25 -0
  47. package/src/domains/block/mcp/index.ts +2 -0
  48. package/src/domains/block/mcp/specs.ts +199 -0
  49. package/src/domains/block/operations/index.ts +4 -0
  50. package/src/domains/block/operations/list.test.ts +80 -0
  51. package/src/domains/block/operations/list.ts +72 -0
  52. package/src/domains/block/operations/lookup.test.ts +108 -0
  53. package/src/domains/block/operations/lookup.ts +359 -0
  54. package/src/domains/block/orchestration/blockEmptyError.ts +19 -0
  55. package/src/domains/block/orchestration/buildBlockFilters.ts +12 -0
  56. package/src/domains/block/orchestration/enrichBlocks.ts +37 -0
  57. package/src/domains/block/orchestration/index.ts +5 -0
  58. package/src/domains/block/orchestration/resolveBlockList.ts +39 -0
  59. package/src/domains/block/orchestration/resolveBlockLookup.ts +21 -0
  60. package/src/domains/block/types.ts +30 -0
  61. package/src/domains/config/commands/channel.test.ts +101 -0
  62. package/src/domains/config/commands/channel.ts +80 -0
  63. package/src/domains/config/commands/index.ts +4 -0
  64. package/src/domains/config/commands/show.test.ts +61 -0
  65. package/src/domains/config/commands/show.ts +51 -0
  66. package/src/domains/config/commands/tier.test.ts +84 -0
  67. package/src/domains/config/commands/tier.ts +78 -0
  68. package/src/domains/config/formatters/channel.test.ts +56 -0
  69. package/src/domains/config/formatters/channel.ts +34 -0
  70. package/src/domains/config/formatters/index.ts +4 -0
  71. package/src/domains/config/formatters/show.test.ts +126 -0
  72. package/src/domains/config/formatters/show.ts +66 -0
  73. package/src/domains/config/formatters/tier.test.ts +70 -0
  74. package/src/domains/config/formatters/tier.ts +40 -0
  75. package/src/domains/config/index.ts +23 -0
  76. package/src/domains/config/mcp/index.ts +2 -0
  77. package/src/domains/config/mcp/specs.ts +108 -0
  78. package/src/domains/config/operations/index.ts +5 -0
  79. package/src/domains/config/operations/show.test.ts +60 -0
  80. package/src/domains/config/operations/show.ts +42 -0
  81. package/src/domains/config/operations/types.ts +13 -0
  82. package/src/domains/config/operations/validateChannel.test.ts +36 -0
  83. package/src/domains/config/operations/validateChannel.ts +24 -0
  84. package/src/domains/config/operations/validateTier.test.ts +56 -0
  85. package/src/domains/config/operations/validateTier.ts +35 -0
  86. package/src/domains/create/commands/component.test.ts +166 -0
  87. package/src/domains/create/commands/component.ts +129 -0
  88. package/src/domains/create/commands/index.ts +3 -0
  89. package/src/domains/create/commands/package.test.ts +35 -0
  90. package/src/domains/create/commands/package.ts +21 -0
  91. package/src/domains/create/index.ts +15 -0
  92. package/src/domains/create/mcp/index.ts +2 -0
  93. package/src/domains/create/mcp/specs.ts +174 -0
  94. package/src/domains/doctor/commands/doctor.test.ts +75 -0
  95. package/src/domains/doctor/commands/doctor.ts +43 -0
  96. package/src/domains/doctor/commands/index.ts +2 -0
  97. package/src/domains/doctor/formatters/doctor.test.ts +117 -0
  98. package/src/domains/doctor/formatters/doctor.ts +84 -0
  99. package/src/domains/doctor/formatters/index.ts +2 -0
  100. package/src/domains/doctor/index.ts +9 -0
  101. package/src/domains/doctor/mcp/index.ts +2 -0
  102. package/src/domains/doctor/mcp/specs.ts +88 -0
  103. package/src/domains/doctor/operations/checks/checkConfigFile.ts +28 -0
  104. package/src/domains/doctor/operations/checks/checkKeStore.ts +38 -0
  105. package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +65 -0
  106. package/src/domains/doctor/operations/checks/checkNodeVersion.ts +25 -0
  107. package/src/domains/doctor/operations/checks/checkPragmaVersion.ts +20 -0
  108. package/src/domains/doctor/operations/checks/checkShellCompletions.ts +42 -0
  109. package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +64 -0
  110. package/src/domains/doctor/operations/checks/index.ts +8 -0
  111. package/src/domains/doctor/operations/checks.test.ts +151 -0
  112. package/src/domains/doctor/operations/index.ts +3 -0
  113. package/src/domains/doctor/operations/runChecks.test.ts +89 -0
  114. package/src/domains/doctor/operations/runChecks.ts +38 -0
  115. package/src/domains/doctor/operations/types.ts +20 -0
  116. package/src/domains/graph/commands/index.ts +4 -0
  117. package/src/domains/graph/commands/inspect.test.ts +94 -0
  118. package/src/domains/graph/commands/inspect.ts +63 -0
  119. package/src/domains/graph/commands/query.test.ts +92 -0
  120. package/src/domains/graph/commands/query.ts +60 -0
  121. package/src/domains/graph/formatters/index.ts +4 -0
  122. package/src/domains/graph/formatters/inspect.ts +45 -0
  123. package/src/domains/graph/formatters/query.ts +37 -0
  124. package/src/domains/graph/helpers/index.ts +3 -0
  125. package/src/domains/graph/helpers/resolveUri.ts +70 -0
  126. package/src/domains/graph/index.ts +22 -0
  127. package/src/domains/graph/mcp/index.ts +2 -0
  128. package/src/domains/graph/mcp/specs.ts +82 -0
  129. package/src/domains/graph/operations/executeQuery.test.ts +68 -0
  130. package/src/domains/graph/operations/executeQuery.ts +32 -0
  131. package/src/domains/graph/operations/index.ts +4 -0
  132. package/src/domains/graph/operations/inspectUri.test.ts +101 -0
  133. package/src/domains/graph/operations/inspectUri.ts +61 -0
  134. package/src/domains/info/commands/index.ts +4 -0
  135. package/src/domains/info/commands/info.test.ts +88 -0
  136. package/src/domains/info/commands/info.ts +47 -0
  137. package/src/domains/info/commands/upgrade.test.ts +183 -0
  138. package/src/domains/info/commands/upgrade.ts +150 -0
  139. package/src/domains/info/constants.test.ts +22 -0
  140. package/src/domains/info/constants.ts +15 -0
  141. package/src/domains/info/formatters/index.ts +8 -0
  142. package/src/domains/info/formatters/info.test.ts +173 -0
  143. package/src/domains/info/formatters/info.ts +116 -0
  144. package/src/domains/info/formatters/upgrade.test.ts +133 -0
  145. package/src/domains/info/formatters/upgrade.ts +68 -0
  146. package/src/domains/info/index.ts +7 -0
  147. package/src/domains/info/operations/checkRegistryVersion.test.ts +124 -0
  148. package/src/domains/info/operations/checkRegistryVersion.ts +45 -0
  149. package/src/domains/info/operations/collectInfo.test.ts +111 -0
  150. package/src/domains/info/operations/collectInfo.ts +68 -0
  151. package/src/domains/info/operations/collectStoreSummary.test.ts +61 -0
  152. package/src/domains/info/operations/collectStoreSummary.ts +42 -0
  153. package/src/domains/info/operations/index.ts +5 -0
  154. package/src/domains/info/types.test-d.ts +35 -0
  155. package/src/domains/info/types.ts +46 -0
  156. package/src/domains/llm/commands/index.ts +3 -0
  157. package/src/domains/llm/commands/llm.ts +37 -0
  158. package/src/domains/llm/data/commandReference.ts +25 -0
  159. package/src/domains/llm/data/decisionTrees.ts +46 -0
  160. package/src/domains/llm/data/index.ts +4 -0
  161. package/src/domains/llm/formatters/index.ts +3 -0
  162. package/src/domains/llm/formatters/orientation.ts +53 -0
  163. package/src/domains/llm/index.ts +8 -0
  164. package/src/domains/llm/llmCommand.test.ts +93 -0
  165. package/src/domains/llm/mcp/index.ts +2 -0
  166. package/src/domains/llm/mcp/specs.ts +87 -0
  167. package/src/domains/llm/operations/collectContext.ts +47 -0
  168. package/src/domains/llm/operations/index.ts +3 -0
  169. package/src/domains/llm/types.ts +32 -0
  170. package/src/domains/modifier/commands/index.ts +4 -0
  171. package/src/domains/modifier/commands/list.test.ts +57 -0
  172. package/src/domains/modifier/commands/list.ts +33 -0
  173. package/src/domains/modifier/commands/lookup.test.ts +91 -0
  174. package/src/domains/modifier/commands/lookup.ts +105 -0
  175. package/src/domains/modifier/formatters/index.ts +4 -0
  176. package/src/domains/modifier/formatters/list.test.ts +38 -0
  177. package/src/domains/modifier/formatters/list.ts +35 -0
  178. package/src/domains/modifier/formatters/lookup.test.ts +33 -0
  179. package/src/domains/modifier/formatters/lookup.ts +38 -0
  180. package/src/domains/modifier/index.ts +22 -0
  181. package/src/domains/modifier/mcp/index.ts +2 -0
  182. package/src/domains/modifier/mcp/specs.ts +97 -0
  183. package/src/domains/modifier/operations/index.ts +4 -0
  184. package/src/domains/modifier/operations/list.test.ts +39 -0
  185. package/src/domains/modifier/operations/list.ts +41 -0
  186. package/src/domains/modifier/operations/lookup.test.ts +37 -0
  187. package/src/domains/modifier/operations/lookup.ts +57 -0
  188. package/src/domains/modifier/orchestration/index.ts +3 -0
  189. package/src/domains/modifier/orchestration/modifierEmptyError.ts +10 -0
  190. package/src/domains/modifier/orchestration/resolveModifierList.ts +16 -0
  191. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +20 -0
  192. package/src/domains/modifier/types.ts +5 -0
  193. package/src/domains/ontology/commands/index.ts +4 -0
  194. package/src/domains/ontology/commands/list.test.ts +82 -0
  195. package/src/domains/ontology/commands/list.ts +46 -0
  196. package/src/domains/ontology/commands/show.test.ts +103 -0
  197. package/src/domains/ontology/commands/show.ts +66 -0
  198. package/src/domains/ontology/formatters/index.ts +4 -0
  199. package/src/domains/ontology/formatters/list.ts +37 -0
  200. package/src/domains/ontology/formatters/show.ts +74 -0
  201. package/src/domains/ontology/helpers/findNamespace.ts +23 -0
  202. package/src/domains/ontology/helpers/index.ts +7 -0
  203. package/src/domains/ontology/helpers/queryClasses.ts +50 -0
  204. package/src/domains/ontology/helpers/queryProperties.ts +57 -0
  205. package/src/domains/ontology/helpers/resolvePrefix.ts +45 -0
  206. package/src/domains/ontology/index.ts +26 -0
  207. package/src/domains/ontology/mcp/index.ts +2 -0
  208. package/src/domains/ontology/mcp/specs.ts +93 -0
  209. package/src/domains/ontology/operations/index.ts +5 -0
  210. package/src/domains/ontology/operations/listOntologies.test.ts +45 -0
  211. package/src/domains/ontology/operations/listOntologies.ts +101 -0
  212. package/src/domains/ontology/operations/showOntology.test.ts +65 -0
  213. package/src/domains/ontology/operations/showOntology.ts +41 -0
  214. package/src/domains/ontology/operations/showOntologyRaw.test.ts +39 -0
  215. package/src/domains/ontology/operations/showOntologyRaw.ts +50 -0
  216. package/src/domains/setup/commands/all.ts +58 -0
  217. package/src/domains/setup/commands/completions.ts +85 -0
  218. package/src/domains/setup/commands/entrypoints.test.ts +170 -0
  219. package/src/domains/setup/commands/index.ts +6 -0
  220. package/src/domains/setup/commands/lsp.ts +49 -0
  221. package/src/domains/setup/commands/mcp.ts +119 -0
  222. package/src/domains/setup/commands/skills.test.ts +64 -0
  223. package/src/domains/setup/commands/skills.ts +106 -0
  224. package/src/domains/setup/formatters/index.ts +2 -0
  225. package/src/domains/setup/formatters/skills.test.ts +103 -0
  226. package/src/domains/setup/formatters/skills.ts +112 -0
  227. package/src/domains/setup/formatters/types.ts +7 -0
  228. package/src/domains/setup/helpers/completionScripts.test.ts +58 -0
  229. package/src/domains/setup/helpers/completionScripts.ts +92 -0
  230. package/src/domains/setup/helpers/constants.ts +2 -0
  231. package/src/domains/setup/helpers/detectShell.test.ts +48 -0
  232. package/src/domains/setup/helpers/detectShell.ts +20 -0
  233. package/src/domains/setup/helpers/index.ts +12 -0
  234. package/src/domains/setup/helpers/runSetupTask.test.ts +279 -0
  235. package/src/domains/setup/helpers/runSetupTask.ts +229 -0
  236. package/src/domains/setup/index.ts +33 -0
  237. package/src/domains/setup/operations/index.ts +6 -0
  238. package/src/domains/setup/operations/setupAll.test.ts +48 -0
  239. package/src/domains/setup/operations/setupAll.ts +41 -0
  240. package/src/domains/setup/operations/setupCompletions.test.ts +94 -0
  241. package/src/domains/setup/operations/setupCompletions.ts +54 -0
  242. package/src/domains/setup/operations/setupLsp.test.ts +29 -0
  243. package/src/domains/setup/operations/setupLsp.ts +17 -0
  244. package/src/domains/setup/operations/setupMcp.test.ts +115 -0
  245. package/src/domains/setup/operations/setupMcp.ts +108 -0
  246. package/src/domains/setup/operations/setupSkills.test.ts +191 -0
  247. package/src/domains/setup/operations/setupSkills.ts +160 -0
  248. package/src/domains/setup/types.ts +18 -0
  249. package/src/domains/shared/ToolSpec.ts +49 -0
  250. package/src/domains/shared/bootStore.ts +77 -0
  251. package/src/domains/shared/buildQuery.ts +18 -0
  252. package/src/domains/shared/compactUri.test.ts +21 -0
  253. package/src/domains/shared/compactUri.ts +16 -0
  254. package/src/domains/shared/context.ts +15 -0
  255. package/src/domains/shared/contracts.ts +113 -0
  256. package/src/domains/shared/extractLocalName.test.ts +21 -0
  257. package/src/domains/shared/extractLocalName.ts +14 -0
  258. package/src/domains/shared/filters/buildChannelFilter.test.ts +52 -0
  259. package/src/domains/shared/filters/buildChannelFilter.ts +41 -0
  260. package/src/domains/shared/filters/buildFilterConfig.ts +16 -0
  261. package/src/domains/shared/filters/buildFilters.test.ts +33 -0
  262. package/src/domains/shared/filters/buildFilters.ts +30 -0
  263. package/src/domains/shared/filters/buildTierFilter.test.ts +78 -0
  264. package/src/domains/shared/filters/buildTierFilter.ts +69 -0
  265. package/src/domains/shared/filters/describeFilters.ts +14 -0
  266. package/src/domains/shared/filters/index.ts +10 -0
  267. package/src/domains/shared/formatters.ts +35 -0
  268. package/src/domains/shared/lookupMany.ts +45 -0
  269. package/src/domains/shared/namespaces.ts +18 -0
  270. package/src/domains/shared/packages.ts +76 -0
  271. package/src/domains/shared/prefixes.ts +37 -0
  272. package/src/domains/shared/runtime.ts +62 -0
  273. package/src/domains/shared/types.test-d.ts +127 -0
  274. package/src/domains/shared/types.ts +348 -0
  275. package/src/domains/skill/commands/index.ts +2 -0
  276. package/src/domains/skill/commands/list.test.ts +111 -0
  277. package/src/domains/skill/commands/list.ts +69 -0
  278. package/src/domains/skill/formatters/index.ts +2 -0
  279. package/src/domains/skill/formatters/list.test.ts +140 -0
  280. package/src/domains/skill/formatters/list.ts +119 -0
  281. package/src/domains/skill/formatters/types.ts +8 -0
  282. package/src/domains/skill/helpers/index.ts +6 -0
  283. package/src/domains/skill/helpers/parseFrontmatter.test.ts +122 -0
  284. package/src/domains/skill/helpers/parseFrontmatter.ts +112 -0
  285. package/src/domains/skill/helpers/resolveSkillSources.ts +34 -0
  286. package/src/domains/skill/index.ts +19 -0
  287. package/src/domains/skill/mcp/index.ts +2 -0
  288. package/src/domains/skill/mcp/specs.ts +38 -0
  289. package/src/domains/skill/operations/discover.test.ts +145 -0
  290. package/src/domains/skill/operations/discover.ts +68 -0
  291. package/src/domains/skill/operations/index.ts +4 -0
  292. package/src/domains/skill/operations/list.test.ts +68 -0
  293. package/src/domains/skill/operations/list.ts +47 -0
  294. package/src/domains/skill/types.ts +25 -0
  295. package/src/domains/standard/commands/categories.test.ts +70 -0
  296. package/src/domains/standard/commands/categories.ts +48 -0
  297. package/src/domains/standard/commands/index.ts +5 -0
  298. package/src/domains/standard/commands/list.test.ts +105 -0
  299. package/src/domains/standard/commands/list.ts +81 -0
  300. package/src/domains/standard/commands/lookup.test.ts +135 -0
  301. package/src/domains/standard/commands/lookup.ts +127 -0
  302. package/src/domains/standard/formatters/categories.test.ts +44 -0
  303. package/src/domains/standard/formatters/categories.ts +38 -0
  304. package/src/domains/standard/formatters/index.ts +6 -0
  305. package/src/domains/standard/formatters/list.test.ts +166 -0
  306. package/src/domains/standard/formatters/list.ts +156 -0
  307. package/src/domains/standard/formatters/lookup.test.ts +95 -0
  308. package/src/domains/standard/formatters/lookup.ts +86 -0
  309. package/src/domains/standard/formatters/types.ts +31 -0
  310. package/src/domains/standard/index.ts +32 -0
  311. package/src/domains/standard/mcp/index.ts +2 -0
  312. package/src/domains/standard/mcp/specs.ts +197 -0
  313. package/src/domains/standard/operations/categories.test.ts +61 -0
  314. package/src/domains/standard/operations/categories.ts +38 -0
  315. package/src/domains/standard/operations/index.ts +5 -0
  316. package/src/domains/standard/operations/list.test.ts +75 -0
  317. package/src/domains/standard/operations/list.ts +64 -0
  318. package/src/domains/standard/operations/lookup.test.ts +58 -0
  319. package/src/domains/standard/operations/lookup.ts +142 -0
  320. package/src/domains/standard/orchestration/buildStandardFilters.ts +11 -0
  321. package/src/domains/standard/orchestration/index.ts +4 -0
  322. package/src/domains/standard/orchestration/resolveStandardList.ts +42 -0
  323. package/src/domains/standard/orchestration/resolveStandardLookup.ts +20 -0
  324. package/src/domains/standard/orchestration/standardEmptyError.ts +19 -0
  325. package/src/domains/standard/types.ts +15 -0
  326. package/src/domains/tier/commands/index.ts +3 -0
  327. package/src/domains/tier/commands/list.test.ts +67 -0
  328. package/src/domains/tier/commands/list.ts +43 -0
  329. package/src/domains/tier/formatters/index.ts +3 -0
  330. package/src/domains/tier/formatters/list.test.ts +33 -0
  331. package/src/domains/tier/formatters/list.ts +40 -0
  332. package/src/domains/tier/index.ts +17 -0
  333. package/src/domains/tier/mcp/index.ts +2 -0
  334. package/src/domains/tier/mcp/specs.ts +38 -0
  335. package/src/domains/tier/operations/index.ts +3 -0
  336. package/src/domains/tier/operations/list.test.ts +32 -0
  337. package/src/domains/tier/operations/list.ts +33 -0
  338. package/src/domains/token/commands/add-config.test.ts +68 -0
  339. package/src/domains/token/commands/add-config.ts +62 -0
  340. package/src/domains/token/commands/index.ts +5 -0
  341. package/src/domains/token/commands/list.test.ts +82 -0
  342. package/src/domains/token/commands/list.ts +45 -0
  343. package/src/domains/token/commands/lookup.test.ts +125 -0
  344. package/src/domains/token/commands/lookup.ts +121 -0
  345. package/src/domains/token/formatters/index.ts +4 -0
  346. package/src/domains/token/formatters/list.test.ts +37 -0
  347. package/src/domains/token/formatters/list.ts +37 -0
  348. package/src/domains/token/formatters/lookup.test.ts +61 -0
  349. package/src/domains/token/formatters/lookup.ts +72 -0
  350. package/src/domains/token/index.ts +32 -0
  351. package/src/domains/token/mcp/index.ts +2 -0
  352. package/src/domains/token/mcp/specs.ts +145 -0
  353. package/src/domains/token/operations/add-config.test.ts +68 -0
  354. package/src/domains/token/operations/add-config.ts +61 -0
  355. package/src/domains/token/operations/index.ts +7 -0
  356. package/src/domains/token/operations/list.test.ts +50 -0
  357. package/src/domains/token/operations/list.ts +52 -0
  358. package/src/domains/token/operations/lookup.test.ts +42 -0
  359. package/src/domains/token/operations/lookup.ts +66 -0
  360. package/src/domains/token/orchestration/index.ts +3 -0
  361. package/src/domains/token/orchestration/resolveTokenList.ts +17 -0
  362. package/src/domains/token/orchestration/resolveTokenLookup.ts +20 -0
  363. package/src/domains/token/orchestration/tokenEmptyError.ts +17 -0
  364. package/src/domains/token/types.ts +6 -0
  365. package/src/error/PragmaError.test.ts +121 -0
  366. package/src/error/PragmaError.ts +164 -0
  367. package/src/error/constants.ts +12 -0
  368. package/src/error/index.ts +4 -0
  369. package/src/error/types.ts +41 -0
  370. package/src/index.ts +141 -0
  371. package/src/mcp/createMcpServer.test.ts +41 -0
  372. package/src/mcp/createMcpServer.ts +53 -0
  373. package/src/mcp/error/buildRecovery.test.ts +37 -0
  374. package/src/mcp/error/buildRecovery.ts +17 -0
  375. package/src/mcp/error/index.ts +12 -0
  376. package/src/mcp/error/serializeError.test.ts +121 -0
  377. package/src/mcp/error/serializeError.ts +31 -0
  378. package/src/mcp/error/serializeErrorPayload.test.ts +38 -0
  379. package/src/mcp/error/serializeErrorPayload.ts +27 -0
  380. package/src/mcp/error/types.ts +51 -0
  381. package/src/mcp/index.ts +15 -0
  382. package/src/mcp/registerResources.test.ts +236 -0
  383. package/src/mcp/registerResources.ts +303 -0
  384. package/src/mcp/runMcpServer.test.ts +66 -0
  385. package/src/mcp/runMcpServer.ts +24 -0
  386. package/src/mcp/tools/index.ts +55 -0
  387. package/src/mcp/tools/registerFromSpec.ts +97 -0
  388. package/src/mcp/tools/registerTools.test.ts +1124 -0
  389. package/src/mcp/types.ts +62 -0
  390. package/src/mcp/utils/estimateTokens.test.ts +14 -0
  391. package/src/mcp/utils/estimateTokens.ts +9 -0
  392. package/src/mcp/utils/index.ts +11 -0
  393. package/src/mcp/utils/wrapTool.test.ts +108 -0
  394. package/src/mcp/utils/wrapTool.ts +81 -0
  395. package/src/package-manager/constants.ts +29 -0
  396. package/src/package-manager/detectInstallSource.test.ts +23 -0
  397. package/src/package-manager/detectInstallSource.ts +111 -0
  398. package/src/package-manager/detectLocalInstall.test.ts +16 -0
  399. package/src/package-manager/detectLocalInstall.ts +43 -0
  400. package/src/package-manager/detectPackageManager.test.ts +81 -0
  401. package/src/package-manager/detectPackageManager.ts +30 -0
  402. package/src/package-manager/index.ts +6 -0
  403. package/src/package-manager/types.ts +12 -0
  404. package/src/pipeline/collectCommands.test.ts +37 -0
  405. package/src/pipeline/collectCommands.ts +47 -0
  406. package/src/pipeline/constants.ts +14 -0
  407. package/src/pipeline/createProgram.test.ts +112 -0
  408. package/src/pipeline/createProgram.ts +70 -0
  409. package/src/pipeline/formatTerminal.test.ts +43 -0
  410. package/src/pipeline/formatTerminal.ts +53 -0
  411. package/src/pipeline/index.ts +19 -0
  412. package/src/pipeline/mapExitCode.test.ts +41 -0
  413. package/src/pipeline/mapExitCode.ts +12 -0
  414. package/src/pipeline/parseGlobalFlags.ts +21 -0
  415. package/src/pipeline/renderError.test.ts +151 -0
  416. package/src/pipeline/renderError.ts +114 -0
  417. package/src/pipeline/resolveCommandKind.test.ts +68 -0
  418. package/src/pipeline/resolveCommandKind.ts +71 -0
  419. package/src/pipeline/runCli.orchestration.test.ts +418 -0
  420. package/src/pipeline/runCli.test.ts +70 -0
  421. package/src/pipeline/runCli.ts +215 -0
  422. package/src/pipeline/types.ts +17 -0
  423. package/src/testing/cli.ts +43 -0
  424. package/src/testing/createTestMcpClient.ts +64 -0
  425. package/src/testing/dsFixtures.ts +506 -0
  426. package/src/testing/fixtures/canonical-config.json +4 -0
  427. package/src/testing/fixtures/canonical.ttl +437 -0
  428. package/src/testing/fixtures/filtered-config.json +4 -0
  429. package/src/testing/fixtures.ts +34 -0
  430. package/src/testing/helpers/assertParity.ts +28 -0
  431. package/src/testing/helpers/createTestMcpClient.ts +40 -0
  432. package/src/testing/helpers/createTestRuntime.ts +67 -0
  433. package/src/testing/index.ts +8 -0
  434. package/src/testing/integration/agent-session.test.ts +148 -0
  435. package/src/testing/integration/boot.test.ts +67 -0
  436. package/src/testing/integration/mcp-surface.test.ts +574 -0
  437. package/src/testing/integration/parity.test.ts +192 -0
  438. package/src/testing/store.ts +39 -0
  439. package/src/testing/types.ts +20 -0
@@ -0,0 +1,74 @@
1
+ import { spawn } from "node:child_process";
2
+ import { unlinkSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import computeSocketPath from "./computeSocketPath.js";
6
+ import waitForSocket from "./waitForSocket.js";
7
+
8
+ const hasBun = typeof globalThis.Bun !== "undefined";
9
+
10
+ const BIN_PATH = resolve(import.meta.dirname, "../bin.ts");
11
+ const cwd = process.cwd();
12
+ const socketPath = computeSocketPath(cwd);
13
+
14
+ function cleanSocket(): void {
15
+ try {
16
+ unlinkSync(socketPath);
17
+ } catch {
18
+ // May not exist.
19
+ }
20
+ }
21
+
22
+ afterEach(() => {
23
+ cleanSocket();
24
+ });
25
+
26
+ describe.skipIf(!hasBun)("startCompletionsServer", () => {
27
+ it("starts, accepts a query, and returns noun completions", async () => {
28
+ cleanSocket();
29
+
30
+ const { default: querySocket } = await import("./querySocket.js");
31
+
32
+ const server = spawn("bun", ["run", BIN_PATH, "_completions-server"], {
33
+ stdio: "ignore",
34
+ cwd,
35
+ detached: true,
36
+ });
37
+
38
+ try {
39
+ const ready = await waitForSocket(socketPath, 10_000);
40
+ expect(ready).toBe(true);
41
+
42
+ const result = await querySocket(socketPath, "");
43
+ expect(result).toContain("component");
44
+ expect(result).toContain("standard");
45
+ } finally {
46
+ server.kill();
47
+ cleanSocket();
48
+ }
49
+ }, 15_000);
50
+
51
+ it("returns verb completions for a noun with trailing space", async () => {
52
+ cleanSocket();
53
+
54
+ const { default: querySocket } = await import("./querySocket.js");
55
+
56
+ const server = spawn("bun", ["run", BIN_PATH, "_completions-server"], {
57
+ stdio: "ignore",
58
+ cwd,
59
+ detached: true,
60
+ });
61
+
62
+ try {
63
+ const ready = await waitForSocket(socketPath, 10_000);
64
+ expect(ready).toBe(true);
65
+
66
+ const result = await querySocket(socketPath, "component ");
67
+ expect(result).toContain("list");
68
+ expect(result).toContain("get");
69
+ } finally {
70
+ server.kill();
71
+ cleanSocket();
72
+ }
73
+ }, 15_000);
74
+ });
@@ -0,0 +1,75 @@
1
+ import { existsSync, unlinkSync } from "node:fs";
2
+ import type { GlobalFlags } from "@canonical/cli-core";
3
+ import { buildCompleters } from "@canonical/cli-core";
4
+ import type { PragmaContext } from "../domains/shared/context.js";
5
+ import type { PragmaRuntime } from "../domains/shared/runtime.js";
6
+ import { bootPragma } from "../domains/shared/runtime.js";
7
+ import collectCommands from "../pipeline/collectCommands.js";
8
+ import computeSocketPath from "./computeSocketPath.js";
9
+ import { IDLE_TIMEOUT_MS } from "./constants.js";
10
+ import handleQuery from "./handleQuery.js";
11
+
12
+ const COMPLETIONS_FLAGS: GlobalFlags = {
13
+ llm: false,
14
+ format: "text",
15
+ verbose: false,
16
+ };
17
+
18
+ /**
19
+ * Boot a ke-backed completions server on a Unix domain socket.
20
+ *
21
+ * Uses `bootPragma()` for unified store + config initialization, builds
22
+ * the completion tree from all registered commands, and listens on a
23
+ * project-scoped Unix domain socket. Auto-exits after 10 seconds of idle.
24
+ *
25
+ * @note Impure
26
+ */
27
+ export default async function startCompletionsServer(): Promise<void> {
28
+ const runtime: PragmaRuntime = await bootPragma();
29
+
30
+ const ctx: PragmaContext = { ...runtime, globalFlags: COMPLETIONS_FLAGS };
31
+ const commands = collectCommands(ctx);
32
+ const tree = buildCompleters(commands);
33
+
34
+ const socketPath = computeSocketPath(runtime.cwd);
35
+
36
+ if (existsSync(socketPath)) {
37
+ unlinkSync(socketPath);
38
+ }
39
+
40
+ let idleTimer: Timer;
41
+
42
+ function resetIdle(): void {
43
+ clearTimeout(idleTimer);
44
+ idleTimer = setTimeout(() => {
45
+ runtime.dispose();
46
+ server.stop();
47
+ try {
48
+ unlinkSync(socketPath);
49
+ } catch {
50
+ // Socket may already be removed.
51
+ }
52
+ process.exit(0);
53
+ }, IDLE_TIMEOUT_MS);
54
+ }
55
+
56
+ const server = Bun.listen({
57
+ unix: socketPath,
58
+ socket: {
59
+ async data(socket, rawData) {
60
+ resetIdle();
61
+ const partial = Buffer.from(rawData)
62
+ .toString("utf-8")
63
+ .replace(/\n$/, "");
64
+ const result = await handleQuery(partial, tree, ctx);
65
+ socket.write(result);
66
+ socket.end();
67
+ },
68
+ open() {},
69
+ close() {},
70
+ error() {},
71
+ },
72
+ });
73
+
74
+ resetIdle();
75
+ }
@@ -0,0 +1,131 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { IDLE_TIMEOUT_MS } from "./constants.js";
3
+
4
+ const existsSyncMock = vi.fn();
5
+ const unlinkSyncMock = vi.fn();
6
+ const buildCompletersMock = vi.fn(() => ({ root: true }));
7
+ const bootPragmaMock = vi.fn();
8
+ const collectCommandsMock = vi.fn(() => []);
9
+ const computeSocketPathMock = vi.fn(() => "/tmp/pragma-completions.sock");
10
+ const handleQueryMock = vi.fn();
11
+ const listenMock = vi.fn();
12
+ const stopMock = vi.fn();
13
+
14
+ vi.mock("node:fs", () => ({
15
+ existsSync: existsSyncMock,
16
+ unlinkSync: unlinkSyncMock,
17
+ }));
18
+
19
+ vi.mock("@canonical/cli-core", async (importOriginal) => {
20
+ const actual = await importOriginal<typeof import("@canonical/cli-core")>();
21
+ return {
22
+ ...actual,
23
+ buildCompleters: buildCompletersMock,
24
+ };
25
+ });
26
+
27
+ vi.mock("../domains/shared/runtime.js", () => ({
28
+ bootPragma: bootPragmaMock,
29
+ }));
30
+
31
+ vi.mock("../pipeline/collectCommands.js", () => ({
32
+ default: collectCommandsMock,
33
+ }));
34
+
35
+ vi.mock("./computeSocketPath.js", () => ({
36
+ default: computeSocketPathMock,
37
+ }));
38
+
39
+ vi.mock("./handleQuery.js", () => ({
40
+ default: handleQueryMock,
41
+ }));
42
+
43
+ describe("startCompletionsServer", () => {
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ vi.useFakeTimers();
47
+ stopMock.mockReset();
48
+ listenMock.mockReset();
49
+ listenMock.mockReturnValue({ stop: stopMock });
50
+ vi.stubGlobal("Bun", { listen: listenMock });
51
+ });
52
+
53
+ afterEach(() => {
54
+ vi.useRealTimers();
55
+ vi.unstubAllGlobals();
56
+ });
57
+
58
+ it("boots, removes stale sockets, and handles incoming queries", async () => {
59
+ existsSyncMock.mockReturnValue(true);
60
+ const dispose = vi.fn();
61
+ bootPragmaMock.mockResolvedValue({
62
+ cwd: "/workspace",
63
+ store: {},
64
+ config: { tier: undefined, channel: "normal" },
65
+ dispose,
66
+ });
67
+ handleQueryMock.mockResolvedValue("block\nstandard");
68
+
69
+ const { default: startCompletionsServer } = await import(
70
+ "./startCompletionsServer.js"
71
+ );
72
+ await startCompletionsServer();
73
+
74
+ expect(unlinkSyncMock).toHaveBeenCalledWith("/tmp/pragma-completions.sock");
75
+ expect(buildCompletersMock).toHaveBeenCalledTimes(1);
76
+ expect(listenMock).toHaveBeenCalledTimes(1);
77
+
78
+ const listenConfig = listenMock.mock.calls[0]?.[0] as {
79
+ socket: {
80
+ data: (
81
+ socket: { write: (value: string) => void; end: () => void },
82
+ rawData: Uint8Array,
83
+ ) => Promise<void>;
84
+ };
85
+ };
86
+ const socket = { write: vi.fn(), end: vi.fn() };
87
+
88
+ await listenConfig.socket.data(socket, Buffer.from("blo\n"));
89
+
90
+ expect(handleQueryMock).toHaveBeenCalledWith(
91
+ "blo",
92
+ { root: true },
93
+ expect.objectContaining({
94
+ cwd: "/workspace",
95
+ globalFlags: { llm: false, format: "text", verbose: false },
96
+ }),
97
+ );
98
+ expect(socket.write).toHaveBeenCalledWith("block\nstandard");
99
+ expect(socket.end).toHaveBeenCalledTimes(1);
100
+ expect(dispose).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it("disposes and exits after the idle timeout", async () => {
104
+ existsSyncMock.mockReturnValue(false);
105
+ const dispose = vi.fn();
106
+ const exitSpy = vi
107
+ .spyOn(process, "exit")
108
+ .mockImplementation((() => undefined) as never);
109
+
110
+ bootPragmaMock.mockResolvedValue({
111
+ cwd: "/workspace",
112
+ store: {},
113
+ config: { tier: undefined, channel: "normal" },
114
+ dispose,
115
+ });
116
+
117
+ const { default: startCompletionsServer } = await import(
118
+ "./startCompletionsServer.js"
119
+ );
120
+ await startCompletionsServer();
121
+
122
+ vi.advanceTimersByTime(IDLE_TIMEOUT_MS);
123
+
124
+ expect(dispose).toHaveBeenCalledTimes(1);
125
+ expect(stopMock).toHaveBeenCalledTimes(1);
126
+ expect(unlinkSyncMock).toHaveBeenCalledWith("/tmp/pragma-completions.sock");
127
+ expect(exitSpy).toHaveBeenCalledWith(0);
128
+
129
+ exitSpy.mockRestore();
130
+ });
131
+ });
@@ -0,0 +1,27 @@
1
+ import { mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import waitForSocket from "./waitForSocket.js";
6
+
7
+ describe("waitForSocket", () => {
8
+ it("returns true immediately when file exists", async () => {
9
+ const dir = mkdtempSync(join(tmpdir(), "pragma-ws-"));
10
+ const path = join(dir, "test.sock");
11
+ writeFileSync(path, "");
12
+
13
+ try {
14
+ const result = await waitForSocket(path, 1_000);
15
+ expect(result).toBe(true);
16
+ } finally {
17
+ unlinkSync(path);
18
+ rmSync(dir, { recursive: true, force: true });
19
+ }
20
+ });
21
+
22
+ it("returns false after timeout when file does not exist", async () => {
23
+ const path = "/tmp/pragma-test-nonexistent-socket.sock";
24
+ const result = await waitForSocket(path, 200);
25
+ expect(result).toBe(false);
26
+ });
27
+ });
@@ -0,0 +1,27 @@
1
+ import { existsSync } from "node:fs";
2
+ import { setTimeout } from "node:timers/promises";
3
+ import { POLL_INTERVAL_MS } from "./constants.js";
4
+
5
+ /**
6
+ * Poll for a Unix socket file to appear on disk.
7
+ *
8
+ * Checks at `POLL_INTERVAL_MS` intervals until the file exists or
9
+ * the timeout elapses.
10
+ *
11
+ * @param path - Absolute path to the expected socket file.
12
+ * @param timeoutMs - Maximum time to wait in milliseconds.
13
+ * @returns `true` if the socket appeared, `false` on timeout.
14
+ *
15
+ * @note Impure
16
+ */
17
+ export default async function waitForSocket(
18
+ path: string,
19
+ timeoutMs: number,
20
+ ): Promise<boolean> {
21
+ const start = Date.now();
22
+ while (Date.now() - start < timeoutMs) {
23
+ if (existsSync(path)) return true;
24
+ await setTimeout(POLL_INTERVAL_MS);
25
+ }
26
+ return false;
27
+ }
@@ -0,0 +1,14 @@
1
+ import { existsSync } from "node:fs";
2
+ import resolveConfigPath from "./resolveConfigPath.js";
3
+
4
+ /**
5
+ * Check whether a pragma.config.json exists at the given cwd.
6
+ *
7
+ * @param cwd - Directory to check for configuration.
8
+ * @returns `true` if the config file exists.
9
+ *
10
+ * @note Impure — reads filesystem.
11
+ */
12
+ export default function configExists(cwd: string): boolean {
13
+ return existsSync(resolveConfigPath(cwd));
14
+ }
@@ -0,0 +1,6 @@
1
+ /** @module Configuration file reading, writing, and path resolution. */
2
+ export { default as configExists } from "./configExists.js";
3
+ export { default as readConfig } from "./readConfig.js";
4
+ export { default as resolveConfigPath } from "./resolveConfigPath.js";
5
+ export type { ConfigUpdate, PragmaConfig } from "./types.js";
6
+ export { default as writeConfig } from "./writeConfig.js";
@@ -0,0 +1,152 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { PragmaError } from "../error/PragmaError.js";
6
+ import readConfig from "./readConfig.js";
7
+ import writeConfig from "./writeConfig.js";
8
+
9
+ describe("readConfig", () => {
10
+ let dir: string;
11
+
12
+ beforeEach(() => {
13
+ dir = mkdtempSync(join(tmpdir(), "pragma-config-"));
14
+ });
15
+
16
+ afterEach(() => {
17
+ rmSync(dir, { recursive: true, force: true });
18
+ });
19
+
20
+ it("returns defaults when no config file exists", () => {
21
+ const config = readConfig(dir);
22
+ expect(config).toEqual({ tier: undefined, channel: "normal" });
23
+ });
24
+
25
+ it("parses tier and channel", () => {
26
+ writeFileSync(
27
+ join(dir, "pragma.config.json"),
28
+ '{"tier":"apps/lxd","channel":"experimental"}',
29
+ );
30
+ const config = readConfig(dir);
31
+ expect(config.tier).toBe("apps/lxd");
32
+ expect(config.channel).toBe("experimental");
33
+ });
34
+
35
+ it("parses tier only, defaults channel to normal", () => {
36
+ writeFileSync(join(dir, "pragma.config.json"), '{"tier":"global"}');
37
+ const config = readConfig(dir);
38
+ expect(config.tier).toBe("global");
39
+ expect(config.channel).toBe("normal");
40
+ });
41
+
42
+ it("parses channel only, tier is undefined", () => {
43
+ writeFileSync(join(dir, "pragma.config.json"), '{"channel":"prerelease"}');
44
+ const config = readConfig(dir);
45
+ expect(config.tier).toBeUndefined();
46
+ expect(config.channel).toBe("prerelease");
47
+ });
48
+
49
+ it("handles empty file", () => {
50
+ writeFileSync(join(dir, "pragma.config.json"), "");
51
+ const config = readConfig(dir);
52
+ expect(config).toEqual({ tier: undefined, channel: "normal" });
53
+ });
54
+
55
+ it("throws PragmaError on invalid channel", () => {
56
+ writeFileSync(join(dir, "pragma.config.json"), '{"channel":"aggressive"}');
57
+ expect(() => readConfig(dir)).toThrow(PragmaError);
58
+ try {
59
+ readConfig(dir);
60
+ } catch (e) {
61
+ expect(e).toBeInstanceOf(PragmaError);
62
+ expect((e as PragmaError).code).toBe("CONFIG_ERROR");
63
+ expect((e as PragmaError).validOptions).toEqual([
64
+ "normal",
65
+ "experimental",
66
+ "prerelease",
67
+ ]);
68
+ }
69
+ });
70
+
71
+ it("ignores non-string tier", () => {
72
+ writeFileSync(join(dir, "pragma.config.json"), '{"tier":42}');
73
+ const config = readConfig(dir);
74
+ expect(config.tier).toBeUndefined();
75
+ });
76
+ });
77
+
78
+ describe("writeConfig", () => {
79
+ let dir: string;
80
+
81
+ beforeEach(() => {
82
+ dir = mkdtempSync(join(tmpdir(), "pragma-config-write-"));
83
+ });
84
+
85
+ afterEach(() => {
86
+ rmSync(dir, { recursive: true, force: true });
87
+ });
88
+
89
+ it("creates a new config file with tier", () => {
90
+ writeConfig(dir, { tier: "apps/lxd" });
91
+ const config = readConfig(dir);
92
+ expect(config.tier).toBe("apps/lxd");
93
+ expect(config.channel).toBe("normal");
94
+ });
95
+
96
+ it("creates a new config file with channel", () => {
97
+ writeConfig(dir, { channel: "experimental" });
98
+ const config = readConfig(dir);
99
+ expect(config.channel).toBe("experimental");
100
+ expect(config.tier).toBeUndefined();
101
+ });
102
+
103
+ it("merges tier into existing config", () => {
104
+ writeFileSync(join(dir, "pragma.config.json"), '{"channel":"prerelease"}');
105
+ writeConfig(dir, { tier: "global" });
106
+ const config = readConfig(dir);
107
+ expect(config.tier).toBe("global");
108
+ expect(config.channel).toBe("prerelease");
109
+ });
110
+
111
+ it("removes tier when set to undefined", () => {
112
+ writeFileSync(
113
+ join(dir, "pragma.config.json"),
114
+ '{"tier":"apps","channel":"normal"}',
115
+ );
116
+ writeConfig(dir, { tier: undefined });
117
+ const config = readConfig(dir);
118
+ expect(config.tier).toBeUndefined();
119
+ expect(config.channel).toBe("normal");
120
+ });
121
+
122
+ it("removes channel when set to undefined", () => {
123
+ writeFileSync(
124
+ join(dir, "pragma.config.json"),
125
+ '{"tier":"apps","channel":"experimental"}',
126
+ );
127
+ writeConfig(dir, { channel: undefined });
128
+ const config = readConfig(dir);
129
+ expect(config.tier).toBe("apps");
130
+ // Channel defaults to "normal" when absent from file
131
+ expect(config.channel).toBe("normal");
132
+ });
133
+
134
+ it("round-trips tier and channel through write then read", () => {
135
+ writeConfig(dir, { tier: "apps/lxd", channel: "prerelease" });
136
+ const config = readConfig(dir);
137
+ expect(config.tier).toBe("apps/lxd");
138
+ expect(config.channel).toBe("prerelease");
139
+ });
140
+
141
+ it("writes valid empty JSON when all fields removed", () => {
142
+ writeFileSync(join(dir, "pragma.config.json"), '{"tier":"apps"}');
143
+ writeConfig(dir, { tier: undefined });
144
+ const raw = readFileSync(join(dir, "pragma.config.json"), "utf-8");
145
+ expect(raw).toBe("{}\n");
146
+ });
147
+
148
+ it("throws on unparseable existing config instead of silently overwriting", () => {
149
+ writeFileSync(join(dir, "pragma.config.json"), "{invalid json");
150
+ expect(() => writeConfig(dir, { tier: "apps" })).toThrow();
151
+ });
152
+ });
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { type Channel, VALID_CHANNELS } from "../constants.js";
3
+ import { PragmaError } from "../error/PragmaError.js";
4
+ import resolveConfigPath from "./resolveConfigPath.js";
5
+ import type { PragmaConfig } from "./types.js";
6
+
7
+ /**
8
+ * Type guard: check whether a value is a recognized channel string.
9
+ *
10
+ * @param value - Candidate value.
11
+ * @returns `true` if value is a valid Channel.
12
+ */
13
+ function isValidChannel(value: unknown): value is Channel {
14
+ return typeof value === "string" && VALID_CHANNELS.includes(value as Channel);
15
+ }
16
+
17
+ /**
18
+ * Read pragma config from `pragma.config.json` in the given directory.
19
+ *
20
+ * Returns defaults (no tier, `"normal"` channel) when the file is missing or empty.
21
+ *
22
+ * @param cwd - Directory containing pragma.config.json (defaults to `process.cwd()`).
23
+ * @returns Parsed configuration.
24
+ * @throws PragmaError with code `CONFIG_ERROR` if JSON is invalid or channel is unrecognized.
25
+ *
26
+ * @note Impure — reads config from filesystem.
27
+ */
28
+ export default function readConfig(cwd: string = process.cwd()): PragmaConfig {
29
+ const configPath = resolveConfigPath(cwd);
30
+
31
+ let raw: string;
32
+ try {
33
+ raw = readFileSync(configPath, "utf-8");
34
+ } catch {
35
+ return { tier: undefined, channel: "normal" };
36
+ }
37
+
38
+ const trimmed = raw.trim();
39
+ if (trimmed === "") {
40
+ return { tier: undefined, channel: "normal" };
41
+ }
42
+
43
+ let parsed: Record<string, unknown>;
44
+ try {
45
+ parsed = JSON.parse(trimmed) as Record<string, unknown>;
46
+ } catch {
47
+ throw PragmaError.configError(`Invalid JSON in ${configPath}.`);
48
+ }
49
+
50
+ const tier = typeof parsed.tier === "string" ? parsed.tier : undefined;
51
+
52
+ let channel: Channel = "normal";
53
+ if (parsed.channel !== undefined) {
54
+ if (!isValidChannel(parsed.channel)) {
55
+ throw PragmaError.configError(
56
+ `Invalid channel "${String(parsed.channel)}".`,
57
+ { validOptions: [...VALID_CHANNELS] },
58
+ );
59
+ }
60
+ channel = parsed.channel;
61
+ }
62
+
63
+ return { tier, channel };
64
+ }
@@ -0,0 +1,20 @@
1
+ import { resolve } from "node:path";
2
+ import { describe, expect, it } from "vitest";
3
+ import resolveConfigPath from "./resolveConfigPath.js";
4
+
5
+ describe("resolveConfigPath", () => {
6
+ it("appends pragma.config.json to the given directory", () => {
7
+ expect(resolveConfigPath("/home/user/project")).toBe(
8
+ resolve("/home/user/project", "pragma.config.json"),
9
+ );
10
+ });
11
+
12
+ it("resolves relative paths against cwd", () => {
13
+ const result = resolveConfigPath(".");
14
+ expect(result).toBe(resolve(process.cwd(), "pragma.config.json"));
15
+ });
16
+
17
+ it("handles nested directories", () => {
18
+ expect(resolveConfigPath("/a/b/c")).toBe("/a/b/c/pragma.config.json");
19
+ });
20
+ });
@@ -0,0 +1,11 @@
1
+ import { resolve } from "node:path";
2
+
3
+ /**
4
+ * Resolve the absolute path to pragma.config.json for a given cwd.
5
+ *
6
+ * @param cwd - Directory to resolve from.
7
+ * @returns Absolute file path to the configuration file.
8
+ */
9
+ export default function resolveConfigPath(cwd: string): string {
10
+ return resolve(cwd, "pragma.config.json");
11
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Configuration data shapes for pragma.config.json.
3
+ *
4
+ * Separates the persisted config shape from the read/write logic
5
+ * so consumers can import types without pulling in filesystem code.
6
+ */
7
+
8
+ import type { Channel } from "../constants.js";
9
+
10
+ /** Parsed contents of pragma.config.json. */
11
+ interface PragmaConfig {
12
+ /** Active tier path, or `undefined` when no tier is configured. */
13
+ tier: string | undefined;
14
+ /** Release channel controlling component visibility. */
15
+ channel: Channel;
16
+ }
17
+
18
+ /** Partial update payload for writing config changes. */
19
+ interface ConfigUpdate {
20
+ /** New tier path, `undefined` to remove the tier field. */
21
+ tier?: string | undefined;
22
+ /** New channel, `undefined` to remove the channel field. */
23
+ channel?: Channel | undefined;
24
+ }
25
+
26
+ export type { ConfigUpdate, PragmaConfig };
@@ -0,0 +1,49 @@
1
+ import { readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import resolveConfigPath from "./resolveConfigPath.js";
3
+ import type { ConfigUpdate } from "./types.js";
4
+
5
+ /**
6
+ * Write config updates to pragma.config.json.
7
+ *
8
+ * Merges updates into existing JSON. A field set to `undefined` removes it.
9
+ * Creates the file if it doesn't exist.
10
+ *
11
+ * @param cwd - Directory containing (or to contain) pragma.config.json.
12
+ * @param update - Fields to set or remove.
13
+ *
14
+ * @note Impure — writes to the filesystem.
15
+ */
16
+ export default function writeConfig(cwd: string, update: ConfigUpdate): void {
17
+ const configPath = resolveConfigPath(cwd);
18
+ const tempPath = `${configPath}.tmp`;
19
+
20
+ let existing: Record<string, unknown> = {};
21
+ try {
22
+ const raw = readFileSync(configPath, "utf-8");
23
+ existing = JSON.parse(raw) as Record<string, unknown>;
24
+ } catch (err: unknown) {
25
+ // Only swallow "file not found" — surface parse or permission errors.
26
+ const isNotFound =
27
+ err instanceof Error &&
28
+ "code" in err &&
29
+ (err as NodeJS.ErrnoException).code === "ENOENT";
30
+ if (!isNotFound) {
31
+ throw err;
32
+ }
33
+ }
34
+
35
+ if (update.tier !== undefined) {
36
+ existing.tier = update.tier;
37
+ } else if ("tier" in update) {
38
+ delete existing.tier;
39
+ }
40
+
41
+ if (update.channel !== undefined) {
42
+ existing.channel = update.channel;
43
+ } else if ("channel" in update) {
44
+ delete existing.channel;
45
+ }
46
+
47
+ writeFileSync(tempPath, `${JSON.stringify(existing, null, 2)}\n`);
48
+ renameSync(tempPath, configPath);
49
+ }