@canonical/pragma-cli 0.35.0 → 0.37.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 (295) hide show
  1. package/README.md +2 -2
  2. package/bundled-skills/add-standard/SKILL.md +338 -0
  3. package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
  4. package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
  5. package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
  6. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
  7. package/bundled-skills/anatomy-author/SKILL.md +1226 -0
  8. package/bundled-skills/design-auditor/SKILL.md +455 -0
  9. package/bundled-skills/implement-component/SKILL.md +333 -0
  10. package/bundled-skills/specify-component/SKILL.md +331 -0
  11. package/bundled-skills/specify-pattern/SKILL.md +292 -0
  12. package/dist/package.json +24 -20
  13. package/dist/pragma.conf.js +266 -34
  14. package/dist/pragma.conf.js.map +1 -1
  15. package/dist/src/bin.js +1 -1
  16. package/dist/src/bin.js.map +1 -1
  17. package/dist/src/capabilities/capabilities/hints.js +1 -1
  18. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  19. package/dist/src/capabilities/colophon/colophon.render.js +1 -1
  20. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  21. package/dist/src/capabilities/create/create.verb.js +2 -1
  22. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  23. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  24. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  25. package/dist/src/capabilities/create/mount.js +4 -3
  26. package/dist/src/capabilities/create/mount.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  28. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  29. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  30. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  32. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  33. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  34. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  35. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  36. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  37. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  38. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  39. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  40. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  41. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  42. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  43. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  44. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  45. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  46. package/dist/src/capabilities/graph/query.render.js +2 -2
  47. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  48. package/dist/src/capabilities/info/collectInfo.js +8 -2
  49. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  50. package/dist/src/capabilities/info/info.render.js +6 -2
  51. package/dist/src/capabilities/info/info.render.js.map +1 -1
  52. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  53. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  54. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  55. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  56. package/dist/src/capabilities/setup/mode.js +2 -2
  57. package/dist/src/capabilities/setup/operations/index.js +4 -4
  58. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  60. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
  64. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  65. package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
  66. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  67. package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
  68. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  69. package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
  70. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  71. package/dist/src/capabilities/setup/plan.js.map +1 -1
  72. package/dist/src/capabilities/setup/plan.render.js +144 -66
  73. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  74. package/dist/src/capabilities/setup/setup.render.js +1 -1
  75. package/dist/src/capabilities/setup/setup.verb.js +165 -63
  76. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  77. package/dist/src/capabilities/setup/targets.js +122 -51
  78. package/dist/src/capabilities/setup/targets.js.map +1 -1
  79. package/dist/src/capabilities/shared/index.js +7 -6
  80. package/dist/src/capabilities/shared/index.js.map +1 -1
  81. package/dist/src/capabilities/shared/packageManager.js +300 -54
  82. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  83. package/dist/src/capabilities/skill/discover.js +97 -11
  84. package/dist/src/capabilities/skill/discover.js.map +1 -1
  85. package/dist/src/capabilities/skill/render.js +2 -2
  86. package/dist/src/capabilities/skill/render.js.map +1 -1
  87. package/dist/src/capabilities/sources/installSkills.js +145 -3
  88. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  89. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  90. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  91. package/dist/src/capabilities/sources/update.verb.js +3 -3
  92. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  93. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  94. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  95. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  96. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  97. package/dist/src/constants.js +13 -2
  98. package/dist/src/constants.js.map +1 -1
  99. package/dist/src/kernel/index.js +11 -2
  100. package/dist/src/kernel/index.js.map +1 -1
  101. package/dist/src/kernel/interactivity.js +68 -0
  102. package/dist/src/kernel/interactivity.js.map +1 -0
  103. package/dist/src/kernel/packs/renderPack.js +3 -3
  104. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  105. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  106. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  107. package/dist/src/kernel/packs/runBodies.js +57 -4
  108. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  109. package/dist/src/kernel/packs/schema.js +54 -2
  110. package/dist/src/kernel/packs/schema.js.map +1 -1
  111. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  112. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
  114. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/types.js.map +1 -1
  116. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  117. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  118. package/dist/src/kernel/project/cli/dispatch.js +46 -24
  119. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  120. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  121. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  122. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  123. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  124. package/dist/src/kernel/project/cli/rootHelp.js +42 -9
  125. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  126. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  127. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  128. package/dist/src/kernel/render/renderers.js +1 -1
  129. package/dist/src/kernel/render/style.js +29 -9
  130. package/dist/src/kernel/render/style.js.map +1 -1
  131. package/dist/src/kernel/render/vocabulary.js +174 -0
  132. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  133. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  134. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  135. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  136. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  137. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  138. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  139. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  140. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  141. package/dist/src/kernel/runtime/graphpack/read.js +14 -9
  142. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  143. package/dist/src/kernel/runtime/loadSession.js +3 -1
  144. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  145. package/dist/src/kernel/runtime/probe.js +1 -1
  146. package/dist/src/kernel/runtime/probe.js.map +1 -1
  147. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  148. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  149. package/dist/src/kernel/runtime/refs/resolve.js +9 -3
  150. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  151. package/dist/src/kernel/spec/emitReference.js +1 -1
  152. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  153. package/package.json +24 -20
  154. package/pragma.conf.ts +272 -34
  155. package/src/bin.ts +8 -1
  156. package/src/capabilities/block.parity.test.ts +25 -11
  157. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  158. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  159. package/src/capabilities/capabilities/hints.ts +1 -1
  160. package/src/capabilities/colophon/colophon.render.ts +1 -2
  161. package/src/capabilities/colophon/colophon.test.ts +2 -2
  162. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  163. package/src/capabilities/create/byteEquality.test.ts +28 -1
  164. package/src/capabilities/create/create.test.ts +1 -0
  165. package/src/capabilities/create/create.verb.ts +2 -1
  166. package/src/capabilities/create/createGrammar.test.ts +5 -2
  167. package/src/capabilities/create/createSurface.generated.ts +23 -1
  168. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  169. package/src/capabilities/create/mount.ts +3 -3
  170. package/src/capabilities/distribution.parity.test.ts +4 -0
  171. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  172. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  173. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  174. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  175. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  176. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  177. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  178. package/src/capabilities/doctor/checks/index.ts +6 -6
  179. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  180. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  181. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  182. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  183. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  184. package/src/capabilities/doctor/doctor.render.ts +56 -80
  185. package/src/capabilities/doctor/doctor.test.ts +247 -35
  186. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  187. package/src/capabilities/doctor/runChecks.ts +14 -11
  188. package/src/capabilities/doctor/types.ts +15 -24
  189. package/src/capabilities/emptyRecovery.test.ts +10 -2
  190. package/src/capabilities/graph/query.render.ts +2 -2
  191. package/src/capabilities/info/collectInfo.ts +8 -1
  192. package/src/capabilities/info/info.render.ts +6 -2
  193. package/src/capabilities/info/info.test.ts +37 -1
  194. package/src/capabilities/info/types.ts +10 -2
  195. package/src/capabilities/lazy.test.ts +26 -2
  196. package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
  197. package/src/capabilities/prompt/prompt.render.ts +2 -2
  198. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  199. package/src/capabilities/resources/resources.test.ts +38 -2
  200. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  201. package/src/capabilities/setup/buildPlan.ts +45 -41
  202. package/src/capabilities/setup/index.ts +1 -1
  203. package/src/capabilities/setup/mode.ts +2 -2
  204. package/src/capabilities/setup/operations/index.ts +6 -1
  205. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  206. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  207. package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
  208. package/src/capabilities/setup/operations/setupLsp.ts +203 -38
  209. package/src/capabilities/setup/operations/setupMcp.ts +139 -62
  210. package/src/capabilities/setup/operations/setupSkills.ts +399 -60
  211. package/src/capabilities/setup/plan.render.ts +171 -76
  212. package/src/capabilities/setup/plan.ts +17 -12
  213. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  214. package/src/capabilities/setup/setup.render.test.ts +50 -36
  215. package/src/capabilities/setup/setup.render.ts +1 -1
  216. package/src/capabilities/setup/setup.test.ts +1126 -50
  217. package/src/capabilities/setup/setup.verb.ts +174 -68
  218. package/src/capabilities/setup/targets.ts +138 -60
  219. package/src/capabilities/setup/types.ts +10 -18
  220. package/src/capabilities/shared/index.ts +18 -6
  221. package/src/capabilities/shared/packageManager.test.ts +421 -6
  222. package/src/capabilities/shared/packageManager.ts +394 -59
  223. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  224. package/src/capabilities/skill/discover.ts +100 -11
  225. package/src/capabilities/skill/render.ts +2 -2
  226. package/src/capabilities/skill/skill.test.ts +17 -4
  227. package/src/capabilities/sources/installSkills.ts +181 -3
  228. package/src/capabilities/sources/runUpdate.ts +78 -2
  229. package/src/capabilities/sources/sources.test.ts +228 -0
  230. package/src/capabilities/sources/update.verb.ts +3 -3
  231. package/src/capabilities/surface.test.ts +14 -6
  232. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  233. package/src/capabilities/upgrade/types.ts +10 -2
  234. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  235. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  236. package/src/constants.ts +14 -1
  237. package/src/identity.test.ts +18 -5
  238. package/src/kernel/completion/entitySource.test.ts +15 -2
  239. package/src/kernel/completion/safety.test.ts +16 -9
  240. package/src/kernel/config/defaults.test.ts +19 -3
  241. package/src/kernel/config/readConfig.test.ts +1 -1
  242. package/src/kernel/index.ts +11 -2
  243. package/src/kernel/interactivity.test.ts +76 -0
  244. package/src/kernel/interactivity.ts +69 -0
  245. package/src/kernel/packs/compile.test.ts +92 -0
  246. package/src/kernel/packs/renderPack.ts +3 -3
  247. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  248. package/src/kernel/packs/resolveEntity.ts +88 -24
  249. package/src/kernel/packs/runBodies.ts +84 -19
  250. package/src/kernel/packs/schema.ts +60 -4
  251. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  252. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  253. package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
  254. package/src/kernel/packs/toolDescription.test.ts +35 -1
  255. package/src/kernel/packs/types.ts +125 -2
  256. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  257. package/src/kernel/project/cli/buildProgram.ts +8 -1
  258. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  259. package/src/kernel/project/cli/dispatch.ts +48 -25
  260. package/src/kernel/project/cli/globalFlags.ts +4 -1
  261. package/src/kernel/project/cli/helpFormat.ts +1 -2
  262. package/src/kernel/project/cli/rootHelp.ts +44 -8
  263. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.ts +1 -1
  266. package/src/kernel/render/style.ts +32 -9
  267. package/src/kernel/render/vocabulary.ts +276 -0
  268. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  269. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  270. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  271. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  272. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  273. package/src/kernel/runtime/graphpack/read.ts +14 -12
  274. package/src/kernel/runtime/loadSession.ts +3 -1
  275. package/src/kernel/runtime/probe.ts +1 -1
  276. package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
  277. package/src/kernel/runtime/refs/parseRef.ts +25 -1
  278. package/src/kernel/runtime/refs/resolve.ts +9 -3
  279. package/src/kernel/runtime/types.ts +17 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/types.ts +35 -6
  282. package/src/testing/PARITY_GAPS.ts +9 -3
  283. package/src/testing/behavioral/docExamples.test.ts +4 -2
  284. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  285. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  286. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  287. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  288. package/src/testing/eval/cases/readNouns.ts +245 -9
  289. package/src/testing/fixtures/blockGraph.ts +28 -13
  290. package/src/testing/fixtures/graph/canonical.ts +95 -12
  291. package/src/testing/helpers/runCli.test.ts +44 -0
  292. package/src/testing/helpers/runCli.ts +27 -3
  293. package/dist/src/capabilities/shared/bands.js +0 -16
  294. package/dist/src/capabilities/shared/bands.js.map +0 -1
  295. package/src/capabilities/shared/bands.ts +0 -16
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * THE target table — the one list `setup` and `doctor` both read.
3
3
  *
4
- * Five rows, each with a band column. This table drives the `setup <target>`
4
+ * Five rows, each with a scope column. This table drives the `setup <target>`
5
5
  * sub-verbs, the wizard's choices, the `--dry-run` plan, the progress lines, the
6
6
  * recap, the doctor rows, and each doctor row's `fix:` line. Adding a sixth
7
7
  * target is one row here — not a new sub-verb, a new check, a new recap branch
8
8
  * and a new fix string that four separate files have to agree about.
9
9
  *
10
10
  * A row owns: its identity (`id` is BOTH the sub-verb argument and the doctor
11
- * row name, so `mcp` and `setup mcp` visibly share a token), which bands it can
11
+ * row name, so `mcp` and `setup mcp` visibly share a token), which scopes it can
12
12
  * be installed into, how to DETECT its current state, how to PLAN a run from
13
13
  * that detection, and how to COMPOSE the forward and removal effects. Detection
14
14
  * runs once per invocation and feeds every reader, so setup and doctor cannot
@@ -24,6 +24,7 @@
24
24
  * doctor's), so nothing here lands on the `--help`/`__complete` fast path.
25
25
  */
26
26
 
27
+ import { dirname } from "node:path";
27
28
  import type { Task } from "@canonical/task";
28
29
  import { BIN_NAME } from "../../constants.js";
29
30
  import type { PragmaRuntime } from "../../kernel/runtime/index.js";
@@ -47,7 +48,7 @@ import {
47
48
  type LspDetection,
48
49
  lspEditorNames,
49
50
  lspSkipReason,
50
- lspUninstallRemedy,
51
+ ownedLspEditors,
51
52
  } from "./operations/setupLsp.js";
52
53
  import {
53
54
  composeMcp,
@@ -65,6 +66,8 @@ import {
65
66
  ownedSkillLinks,
66
67
  type SkillsDetection,
67
68
  skillsSkipReason,
69
+ skillsSkipRemedy,
70
+ staleSkillLinks,
68
71
  } from "./operations/setupSkills.js";
69
72
  import {
70
73
  type PlanAction,
@@ -73,7 +76,7 @@ import {
73
76
  shortenPath,
74
77
  type TargetId,
75
78
  } from "./plan.js";
76
- import type { ScopeBand } from "./types.js";
79
+ import type { Scope } from "./types.js";
77
80
 
78
81
  /** What a target contributes to one plan row, before selection is applied. */
79
82
  export interface TargetDraft {
@@ -98,20 +101,24 @@ export interface TargetDefinition<D> {
98
101
  readonly id: TargetId;
99
102
  /** Human title, used where a sentence needs one; the id is the row name. */
100
103
  readonly title: string;
101
- readonly bands: readonly ScopeBand[];
102
- /** Real reads, up front, once per (target, band) per invocation. */
103
- detect(rt: PragmaRuntime, band: ScopeBand): Promise<D>;
104
+ readonly scopes: readonly Scope[];
105
+ /** Real reads, up front, once per (target, scope) per invocation. */
106
+ detect(rt: PragmaRuntime, scope: Scope): Promise<D>;
104
107
  /** What a forward run would do. */
105
- plan(detection: D, band: ScopeBand, roots: Roots): TargetDraft;
108
+ plan(detection: D, scope: Scope, roots: Roots): TargetDraft;
106
109
  /** What a removal would do — composed from what detection says we own. */
107
- removalPlan(detection: D, band: ScopeBand, roots: Roots): TargetDraft;
110
+ removalPlan(detection: D, scope: Scope, roots: Roots): TargetDraft;
108
111
  /**
109
112
  * The forward effects. `chosen` is the row's per-child selection (MCP file
110
113
  * paths); a row without children ignores it.
111
114
  */
112
115
  compose(detection: D, chosen?: readonly string[]): Task<void>;
113
- /** The removal effects: forward re-assertion carrying the reversal as `undo`. */
114
- composeRemoval(detection: D): Task<void>;
116
+ /**
117
+ * The removal effects: forward re-assertion carrying the reversal as
118
+ * `undo`. `undoKey` (the row's identity) is stamped on every reversal so
119
+ * the undo interpreter's outcomes can be read back per row.
120
+ */
121
+ composeRemoval(detection: D, undoKey?: string): Task<void>;
115
122
  }
116
123
 
117
124
  /** A table row with its detection type erased — how every consumer holds one. */
@@ -128,35 +135,35 @@ const defineTarget = <D>(target: TargetDefinition<D>): AnyTarget =>
128
135
  const configTarget = defineTarget<ConfigDetection>({
129
136
  id: "config",
130
137
  title: "Global configuration",
131
- bands: ["global"],
138
+ scopes: ["global"],
132
139
  detect: () => detectConfigFile(),
133
- plan: (d, _band, roots) => {
140
+ plan: (d, _scope, roots) => {
134
141
  const path = shortenPath(d.path, roots);
135
142
  if (!d.exists) return { action: "install", detail: path };
136
143
  return { action: "none", detail: `${path} — present` };
137
144
  },
138
- removalPlan: (d, _band, roots) => {
145
+ removalPlan: (d, _scope, roots) => {
139
146
  const path = shortenPath(d.path, roots);
140
147
  if (!d.exists) return { action: "none", detail: `${path} — absent` };
141
148
  if (!d.isSeed) {
142
149
  return {
143
150
  action: "skip",
144
151
  detail: path,
145
- reason: "kept — it holds your settings",
152
+ reason: `it holds your own settings, so ${path} stays`,
146
153
  };
147
154
  }
148
155
  return { action: "remove", detail: path };
149
156
  },
150
157
  compose: (d) => composeConfigFile(d),
151
- composeRemoval: (d) => composeConfigRemoval(d),
158
+ composeRemoval: (d, undoKey) => composeConfigRemoval(d, undoKey),
152
159
  });
153
160
 
154
161
  const completionsTarget = defineTarget<CompletionsDetection>({
155
162
  id: "completions",
156
163
  title: "Shell completions",
157
- bands: ["global"],
164
+ scopes: ["global"],
158
165
  detect: (rt) => detectCompletions(rt.cwd),
159
- plan: (d, _band, roots) => {
166
+ plan: (d, _scope, roots) => {
160
167
  // Never guess a shell. Installing for the wrong one is invisible until the
161
168
  // user presses TAB and nothing happens, so an unresolved shell is a named
162
169
  // skip whose remedy is the one action that DOES settle it.
@@ -187,26 +194,31 @@ const completionsTarget = defineTarget<CompletionsDetection>({
187
194
  };
188
195
  }
189
196
  const where = `${d.shell} → ${shortenPath(d.path, roots)}`;
190
- if (d.state === "installed") return { action: "none", detail: where };
197
+ // A no-op row says what it FOUND, not just where it looked: `no change`
198
+ // beside a bare path leaves the reader to guess whether the script is
199
+ // there and current or missing and unreachable.
200
+ if (d.state === "installed") {
201
+ return { action: "none", detail: `${where} — already up to date` };
202
+ }
191
203
  return {
192
204
  action: d.state === "stale" ? "update" : "install",
193
205
  detail: where,
194
206
  };
195
207
  },
196
- removalPlan: (d, _band, roots) => {
208
+ removalPlan: (d, _scope, roots) => {
197
209
  if (d.path === null || d.state === "absent") {
198
210
  return { action: "none", detail: "no script installed" };
199
211
  }
200
212
  return { action: "remove", detail: shortenPath(d.path, roots) };
201
213
  },
202
214
  compose: (d) => composeCompletions(d),
203
- composeRemoval: (d) => composeCompletionsRemoval(d),
215
+ composeRemoval: (d, undoKey) => composeCompletionsRemoval(d, undoKey),
204
216
  });
205
217
 
206
218
  const lspTarget = defineTarget<LspDetection>({
207
219
  id: "lsp",
208
220
  title: "Terrazzo LSP extension",
209
- bands: ["global"],
221
+ scopes: ["global"],
210
222
  detect: (rt) => detectLsp(rt.cwd),
211
223
  plan: (d) => {
212
224
  if (d.state === "unknown") {
@@ -225,32 +237,71 @@ const lspTarget = defineTarget<LspDetection>({
225
237
  label: `${e.editor.cli} — ${e.editor.name}`,
226
238
  action: e.installed ? ("unchanged" as const) : ("add" as const),
227
239
  }));
240
+ // The detail names WHAT is being placed and WHERE — never the row's own
241
+ // state, which the action column already carries. `installed (VSCodium)`
242
+ // beside a child row reading `codium — VSCodium (unchanged)` said the
243
+ // editor's name twice and "nothing happens here" twice.
228
244
  if (d.state === "installed") {
229
245
  return {
230
246
  action: "none",
231
- detail: `installed (${lspEditorNames(d).join(", ")})`,
247
+ detail: `Terrazzo extension in ${lspEditorNames(d).join(", ")}`,
232
248
  children,
233
249
  };
234
250
  }
235
251
  const pending = d.editors.filter((e) => !e.installed);
236
252
  return {
237
253
  action: "install",
238
- detail: `${pending.length} ${pending.length === 1 ? "editor" : "editors"}`,
254
+ detail: `Terrazzo extension → ${pending
255
+ .map((e) => e.editor.name)
256
+ .join(", ")}`,
239
257
  children,
240
258
  };
241
259
  },
242
- removalPlan: (d) => ({
243
- action: "skip",
244
- detail: "not removed",
245
- reason: "an extension install cannot be reversed from here",
246
- remedy:
247
- lspUninstallRemedy(d) ??
248
- "no editor CLI is on PATH — uninstall it from your editor",
249
- }),
260
+ removalPlan: (d) => {
261
+ // No editor CLI on PATH is the SAME named skip the forward plan reports:
262
+ // there is nothing on this machine to run an uninstall with.
263
+ if (d.state === "unknown") {
264
+ return {
265
+ action: "skip",
266
+ detail: lspSkipReason(d),
267
+ reason: lspSkipReason(d),
268
+ remedy: LSP_SKIP_REMEDY,
269
+ };
270
+ }
271
+ // Removal is composed from what detection says we OWN — a copy that is
272
+ // present, whatever its version. The forward plan's version gate would let
273
+ // a dead pre-0.8.3 copy this command installed survive its own `--undo`.
274
+ const owned = ownedLspEditors(d);
275
+ if (owned.length === 0) {
276
+ return { action: "none", detail: "no editor carries the extension" };
277
+ }
278
+ return {
279
+ action: "remove",
280
+ detail: `Terrazzo extension from ${owned
281
+ .map((e) => e.editor.name)
282
+ .join(", ")}`,
283
+ children: owned.map((e) => ({
284
+ key: e.editor.cli,
285
+ label: `${e.editor.cli} — ${e.editor.name}`,
286
+ action: "update" as const,
287
+ })),
288
+ };
289
+ },
250
290
  compose: (d, chosen) => composeLsp(d, chosen),
251
- composeRemoval: (d) => composeLspRemoval(d),
291
+ composeRemoval: (d, undoKey) => composeLspRemoval(d, undoKey),
252
292
  });
253
293
 
294
+ /**
295
+ * Why the `mcp` row has nothing to write in a scope — authored once, because
296
+ * `setup`'s row and `doctor`'s row are the same finding and must not word it
297
+ * differently. Neither sentence says "scope": the two scopes are `global` and
298
+ * `local project`, after the `--global` / `--local` flags that select them.
299
+ */
300
+ export const MCP_NO_LOCATION: Record<Scope, string> = {
301
+ global: "no AI harness on this machine keeps a global config",
302
+ project: "no AI harness in this project keeps a per-project config",
303
+ };
304
+
254
305
  /** One MCP file as a plan child: its path, the harnesses sharing it, its state. */
255
306
  const mcpChild = (
256
307
  d: McpDetection,
@@ -262,7 +313,7 @@ const mcpChild = (
262
313
  key: group.path,
263
314
  label: shortenPath(group.path, roots),
264
315
  action:
265
- state === "configured"
316
+ state === "registered"
266
317
  ? "unchanged"
267
318
  : state === "drifted"
268
319
  ? "update"
@@ -273,35 +324,34 @@ const mcpChild = (
273
324
  const mcpTarget = defineTarget<McpDetection>({
274
325
  id: "mcp",
275
326
  title: "MCP server registration",
276
- bands: ["global", "project"],
277
- detect: (rt, band) => detectMcp(rt, band),
278
- plan: (d, band, roots) => {
327
+ scopes: ["global", "project"],
328
+ detect: (rt, scope) => detectMcp(rt, scope),
329
+ plan: (d, scope, roots) => {
279
330
  if (d.groups.length === 0) {
280
331
  return {
281
332
  action: "skip",
282
- detail: "no harness config location in this band",
283
- reason:
284
- band === "project"
285
- ? "no AI harness in this project writes a per-repository config"
286
- : "no detected AI harness declares a user-level config location",
333
+ detail: MCP_NO_LOCATION[scope],
334
+ reason: MCP_NO_LOCATION[scope],
287
335
  };
288
336
  }
289
337
  const children = d.groups.map((group) => mcpChild(d, group, roots));
290
338
  const pending = children.filter((c) => c.action !== "unchanged");
291
339
  return {
292
340
  action: pending.length === 0 ? "none" : "update",
293
- detail: `${d.groups.length} ${d.groups.length === 1 ? "file" : "files"}`,
341
+ detail: `${d.groups.length} ${
342
+ d.groups.length === 1 ? "config file" : "config files"
343
+ }`,
294
344
  children,
295
345
  };
296
346
  },
297
- removalPlan: (d, _band, roots) => {
347
+ removalPlan: (d, _scope, roots) => {
298
348
  const owned = ownedMcpGroups(d);
299
349
  if (owned.length === 0) {
300
350
  return { action: "none", detail: "no entry to remove" };
301
351
  }
302
352
  return {
303
353
  action: "remove",
304
- detail: `${owned.length} ${owned.length === 1 ? "file" : "files"}`,
354
+ detail: `${owned.length} ${owned.length === 1 ? "config file" : "config files"}`,
305
355
  children: owned.map((group) => ({
306
356
  key: group.path,
307
357
  label: shortenPath(group.path, roots),
@@ -311,30 +361,58 @@ const mcpTarget = defineTarget<McpDetection>({
311
361
  },
312
362
  compose: (d, chosen) =>
313
363
  composeMcp(d, chosen ? selectedGroups(d, chosen) : d.groups),
314
- composeRemoval: (d) => composeMcpRemoval(d),
364
+ composeRemoval: (d, undoKey) => composeMcpRemoval(d, undoKey),
315
365
  });
316
366
 
317
367
  const skillsTarget = defineTarget<SkillsDetection>({
318
368
  id: "skills",
319
369
  title: "Skill symlinks",
320
- bands: ["global", "project"],
321
- detect: (rt, band) => detectSkills(rt, band),
322
- plan: (d, band, roots) => {
323
- if (!d.available) {
324
- const reason = skillsSkipReason(shortenPath(d.sourceRoot, roots), band);
325
- return { action: "skip", detail: reason, reason };
370
+ scopes: ["global", "project"],
371
+ detect: (rt, scope) => detectSkills(rt, scope),
372
+ plan: (d, scope, roots) => {
373
+ // A forward run RECONCILES: stale links this scope owns are its work too, so
374
+ // an orphan-only tree is actionable rather than a row that plans `none` and
375
+ // is then deselected into doing nothing. `staleSkillLinks` carries the
376
+ // absent-root safety gate, so a machine whose source root does not exist
377
+ // contributes no sweep and the row falls back to the honest skip.
378
+ const stale = staleSkillLinks(d);
379
+ if (!d.available && stale.length === 0) {
380
+ const short = shortenPath(d.sourceRoot, roots);
381
+ const reason = skillsSkipReason(short, scope, d.rootExists);
382
+ // A skip with no remedy is a dead end — this one names the command that
383
+ // fills the scope's source root, exactly as the completions and lsp skips
384
+ // beside it name theirs.
385
+ return {
386
+ action: "skip",
387
+ detail: reason,
388
+ reason,
389
+ remedy: skillsSkipRemedy(short, scope),
390
+ };
326
391
  }
327
392
  const dirs = d.targets.map((t) => shortenPath(t.dir, roots)).join(", ");
328
- const where = `${d.skillCount} ${d.skillCount === 1 ? "skill" : "skills"} → ${d.targets.length} ${d.targets.length === 1 ? "dir" : "dirs"} (${dirs})`;
393
+ const where = `${d.skillCount} ${d.skillCount === 1 ? "skill" : "skills"} → ${d.targets.length} ${d.targets.length === 1 ? "folder" : "folders"} (${dirs})`;
394
+ const detail =
395
+ stale.length === 0
396
+ ? where
397
+ : `${where}, ${stale.length} stale ${stale.length === 1 ? "link" : "links"} to remove`;
329
398
  const pending = d.actions.filter((a) => a.action !== "skipped");
330
- return { action: pending.length === 0 ? "none" : "link", detail: where };
399
+ if (pending.length > 0) return { action: "link", detail };
400
+ // Nothing to link, but something to retire: `update` is the table's word
401
+ // for "this row has work that is not a fresh install".
402
+ if (stale.length > 0) return { action: "update", detail };
403
+ return { action: "none", detail };
331
404
  },
332
- removalPlan: (d, _band, roots) => {
405
+ removalPlan: (d, _scope, roots) => {
333
406
  const owned = ownedSkillLinks(d);
334
407
  if (owned.length === 0) {
335
408
  return { action: "none", detail: "no link to remove" };
336
409
  }
337
- const dirs = [...new Set(owned.map((a) => shortenPath(a.linkPath, roots)))];
410
+ // Dedupe to the FOLDERS the links live in — `linkPath` itself is one path
411
+ // per link, so a Set over it deduped nothing and the removal preview
412
+ // printed an 18-path wall where the forward plan says `… → 2 folders`.
413
+ const dirs = [
414
+ ...new Set(owned.map((a) => shortenPath(dirname(a.linkPath), roots))),
415
+ ];
338
416
  return {
339
417
  action: "remove",
340
418
  detail: `${owned.length} ${owned.length === 1 ? "link" : "links"}`,
@@ -346,7 +424,7 @@ const skillsTarget = defineTarget<SkillsDetection>({
346
424
  };
347
425
  },
348
426
  compose: (d) => composeSkills(d),
349
- composeRemoval: (d) => composeSkillsRemoval(d),
427
+ composeRemoval: (d, undoKey) => composeSkillsRemoval(d, undoKey),
350
428
  });
351
429
 
352
430
  /** THE table, in display order. */
@@ -362,6 +440,6 @@ export const TARGETS: readonly AnyTarget[] = [
362
440
  export const findTarget = (id: string): AnyTarget | undefined =>
363
441
  TARGETS.find((target) => target.id === id);
364
442
 
365
- /** Whether a target can be installed into a band. */
366
- export const supportsBand = (target: AnyTarget, band: ScopeBand): boolean =>
367
- target.bands.includes(band);
443
+ /** Whether a target can be installed into a scope. */
444
+ export const supportsScope = (target: AnyTarget, scope: Scope): boolean =>
445
+ target.scopes.includes(scope);
@@ -15,31 +15,23 @@ export type SetupMode =
15
15
  | "mcp"
16
16
  | "skills";
17
17
 
18
- /**
19
- * One of the two config bands: the per-user/home `global` band or the per-repo
20
- * `project` band. Mirrors `@canonical/harnesses`' `ScopeBand` structurally, but
21
- * is redeclared here so this statically-reachable type module never pulls the
22
- * harnesses runtime into the fast-path module graph (the lazy-dispatch invariant).
23
- */
24
- export type ScopeBand = "project" | "global";
25
-
26
- /**
27
- * The resolved `--scope` selection: which config band(s) a run touches.
28
- * `global` is the DEFAULT — the user/home band a machine-level installer
29
- * configures; `project` is the opt-in per-repository band; `both` is the
30
- * explicit "run each band in one invocation". Structurally mirrors the
31
- * harnesses `ScopeSelection`.
32
- */
33
- export type ScopeSelection = "project" | "global" | "both";
18
+ // The scope types are part of the rendering vocabulary — the one module that
19
+ // owns both the states and the words for them. Re-exported here so setup's
20
+ // type surface stays complete; the definitions (and the structural pin against
21
+ // `@canonical/harnesses`) live with the vocabulary.
22
+ export type {
23
+ Scope,
24
+ ScopeSelection,
25
+ } from "../../kernel/render/vocabulary.js";
34
26
 
35
27
  /**
36
28
  * The prior state of an MCP target file, read up front by `detectMcp`:
37
- * `absent` (no pragma entry yet), `configured` (a matching pragma entry already
29
+ * `absent` (no pragma entry yet), `registered` (a matching pragma entry already
38
30
  * present in every write — a re-run skips it), or `drifted` (a pragma entry
39
31
  * exists but differs, so a write updates it). Mirrors the skills step's
40
32
  * created/skipped/replaced idempotency at the file grain.
41
33
  */
42
- export type McpTargetState = "absent" | "configured" | "drifted";
34
+ export type McpTargetState = "absent" | "registered" | "drifted";
43
35
 
44
36
  /**
45
37
  * The prior on-disk state of the shell-completion script, read up front by
@@ -8,9 +8,11 @@
8
8
  * that capability, and the moment something here has a single consumer again
9
9
  * it should move back. What survives is the environment the CLI reports on but
10
10
  * does not own — how it was installed, what the registry says about it,
11
- * whether a subprocess it ran actually succeeded — plus the one piece of
12
- * vocabulary (`BAND_LABELS`) that keeps `setup` and `doctor` calling the two
13
- * config bands by the same names users meet in the flags.
11
+ * whether a subprocess it ran actually succeeded. (The scope labels that keep
12
+ * `setup` and `doctor` calling the two config scopes by the same names used
13
+ * to live here too; they are part of the rendering vocabulary now —
14
+ * `kernel/render/vocabulary.ts` — beside the statuses and glyphs they are
15
+ * printed with.)
14
16
  *
15
17
  * The exec guard is exported as the pair a caller needs together: `checkExecOk`
16
18
  * to classify a result inside a Task and `assertExecOk` to throw on one
@@ -23,10 +25,20 @@
23
25
  export {
24
26
  assertExecOk,
25
27
  checkExecOk,
28
+ failPragma,
26
29
  guardMissingBinary,
27
30
  } from "./assertExecOk.js";
28
- export { BAND_LABELS } from "./bands.js";
29
- export type { InstallSource } from "./packageManager.js";
30
- export { detectInstallSource, pmUpdateCommand } from "./packageManager.js";
31
+ export type {
32
+ EphemeralRunner,
33
+ GlobalInstall,
34
+ InstallProbe,
35
+ InstallSource,
36
+ PackageManager,
37
+ } from "./packageManager.js";
38
+ export {
39
+ detectInstallSource,
40
+ pmUpdateCommand,
41
+ updateGuidance,
42
+ } from "./packageManager.js";
31
43
  export type { RegistryCheckResult } from "./registry.js";
32
44
  export { checkRegistryVersion, PRAGMA_PACKAGE } from "./registry.js";