@canonical/pragma-cli 0.32.0 → 0.34.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 +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +19 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +15 -6
  6. package/src/capabilities/block.parity.test.ts +250 -0
  7. package/src/capabilities/capabilities/capabilities.render.ts +3 -2
  8. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  9. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  10. package/src/capabilities/capabilities/catalog.ts +20 -8
  11. package/src/capabilities/capabilities/hints.ts +21 -36
  12. package/src/capabilities/capabilities/types.ts +2 -0
  13. package/src/capabilities/colophon/collectColophon.ts +34 -19
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +11 -4
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/types.ts +16 -7
  20. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  21. package/src/capabilities/config/collectConfigShow.ts +41 -4
  22. package/src/capabilities/config/fields.ts +10 -108
  23. package/src/capabilities/config/set.test.ts +6 -6
  24. package/src/capabilities/config/set.verb.ts +12 -8
  25. package/src/capabilities/config/show.render.test.ts +137 -0
  26. package/src/capabilities/config/show.render.ts +57 -16
  27. package/src/capabilities/config/show.verb.ts +9 -8
  28. package/src/capabilities/config/types.ts +8 -0
  29. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/byteEquality.test.ts +82 -101
  31. package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
  32. package/src/capabilities/create/constants.ts +75 -0
  33. package/src/capabilities/create/create.mcp.test.ts +2 -2
  34. package/src/capabilities/create/create.test.ts +192 -16
  35. package/src/capabilities/create/create.verb.ts +208 -79
  36. package/src/capabilities/create/index.ts +2 -10
  37. package/src/capabilities/create/pickGenerator.ts +34 -28
  38. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  39. package/src/capabilities/create/types.ts +10 -6
  40. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  41. package/src/capabilities/distribution.test.ts +120 -0
  42. package/src/capabilities/distribution.ts +59 -0
  43. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  44. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  45. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  46. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
  47. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  48. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  49. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  50. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  51. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  52. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  53. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  54. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  55. package/src/capabilities/doctor/doctor.render.ts +117 -34
  56. package/src/capabilities/doctor/doctor.test.ts +68 -9
  57. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  58. package/src/capabilities/doctor/runChecks.ts +33 -9
  59. package/src/capabilities/doctor/types.ts +16 -1
  60. package/src/capabilities/emptyRecovery.test.ts +32 -15
  61. package/src/capabilities/graph/inspect.verb.ts +3 -2
  62. package/src/capabilities/graph/query.verb.ts +3 -2
  63. package/src/capabilities/graph/runQuery.ts +8 -5
  64. package/src/capabilities/index.ts +22 -18
  65. package/src/capabilities/info/collectInfo.ts +10 -6
  66. package/src/capabilities/info/info.render.ts +3 -2
  67. package/src/capabilities/info/info.verb.ts +3 -2
  68. package/src/capabilities/lazy.test.ts +93 -2
  69. package/src/capabilities/ontology/index.ts +10 -3
  70. package/src/capabilities/ontology/ontology.test.ts +157 -5
  71. package/src/capabilities/ontology/verbs.ts +203 -104
  72. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  73. package/src/capabilities/prompt/prompt.render.ts +23 -6
  74. package/src/capabilities/prompt/prompt.test.ts +68 -1
  75. package/src/capabilities/prompt/verbs.ts +24 -11
  76. package/src/capabilities/reference.test.ts +32 -0
  77. package/src/capabilities/resources/provider.ts +58 -25
  78. package/src/capabilities/resources/resources.test.ts +176 -21
  79. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  80. package/src/capabilities/setup/index.ts +7 -1
  81. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  82. package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
  83. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  84. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  85. package/src/capabilities/setup/operations/setupSkills.ts +10 -4
  86. package/src/capabilities/setup/setup.render.test.ts +109 -0
  87. package/src/capabilities/setup/setup.render.ts +61 -14
  88. package/src/capabilities/setup/setup.test.ts +352 -1
  89. package/src/capabilities/setup/setup.verb.ts +68 -9
  90. package/src/capabilities/setup/shell.ts +29 -3
  91. package/src/capabilities/setup/types.ts +59 -2
  92. package/src/capabilities/shared/bands.ts +16 -0
  93. package/src/capabilities/skill/discover.ts +11 -5
  94. package/src/capabilities/skill/render.ts +4 -5
  95. package/src/capabilities/skill/verbs.ts +7 -7
  96. package/src/capabilities/sources/collectStatus.ts +69 -49
  97. package/src/capabilities/sources/index.ts +1 -1
  98. package/src/capabilities/sources/installSkills.ts +2 -2
  99. package/src/capabilities/sources/runUpdate.ts +146 -98
  100. package/src/capabilities/sources/sources.test.ts +264 -84
  101. package/src/capabilities/sources/status.render.test.ts +94 -0
  102. package/src/capabilities/sources/status.render.ts +55 -33
  103. package/src/capabilities/sources/status.verb.ts +15 -8
  104. package/src/capabilities/sources/types.ts +18 -25
  105. package/src/capabilities/sources/update.render.ts +9 -2
  106. package/src/capabilities/sources/update.verb.ts +12 -23
  107. package/src/capabilities/surface.test.ts +65 -17
  108. package/src/capabilities/tier.parity.test.ts +144 -0
  109. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  110. package/src/constants.test.ts +9 -7
  111. package/src/constants.ts +49 -23
  112. package/src/identity.test.ts +333 -0
  113. package/src/index.ts +2 -2
  114. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
  115. package/src/kernel/completion/complete.test.ts +3 -5
  116. package/src/kernel/completion/emitScripts.test.ts +1 -1
  117. package/src/kernel/completion/emitScripts.ts +4 -2
  118. package/src/kernel/completion/entitySource.test.ts +185 -41
  119. package/src/kernel/completion/entitySource.ts +131 -113
  120. package/src/kernel/completion/model.test.ts +3 -3
  121. package/src/kernel/completion/model.ts +11 -1
  122. package/src/kernel/completion/parse.test.ts +2 -4
  123. package/src/kernel/completion/safety.test.ts +82 -36
  124. package/src/kernel/completion/shellDrive.test.ts +592 -0
  125. package/src/kernel/completion/templates/fish.ts +28 -6
  126. package/src/kernel/completion/types.ts +3 -3
  127. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  128. package/src/kernel/config/defaults.test.ts +85 -0
  129. package/src/kernel/config/defaults.ts +12 -23
  130. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  131. package/src/kernel/config/findProjectConfig.ts +4 -3
  132. package/src/kernel/config/firstRun.ts +12 -5
  133. package/src/kernel/config/globalConfig.ts +2 -2
  134. package/src/kernel/config/index.ts +4 -5
  135. package/src/kernel/config/paths.ts +17 -17
  136. package/src/kernel/config/readConfig.test.ts +242 -6
  137. package/src/kernel/config/readConfig.ts +8 -8
  138. package/src/kernel/config/schema.test.ts +50 -0
  139. package/src/kernel/config/schema.ts +97 -10
  140. package/src/kernel/config/types.ts +73 -19
  141. package/src/kernel/copy.test.ts +453 -0
  142. package/src/kernel/error/PragmaError.test.ts +5 -5
  143. package/src/kernel/error/constants.ts +3 -2
  144. package/src/kernel/error/index.ts +1 -1
  145. package/src/kernel/error/recovery.test.ts +22 -45
  146. package/src/kernel/error/recovery.ts +22 -27
  147. package/src/kernel/error/renderError.ts +1 -1
  148. package/src/kernel/error/types.ts +5 -4
  149. package/src/kernel/packs/collect.test.ts +195 -11
  150. package/src/kernel/packs/collect.ts +213 -58
  151. package/src/kernel/packs/compile.test.ts +163 -7
  152. package/src/kernel/packs/compile.ts +15 -12
  153. package/src/kernel/packs/disclosure.test.ts +5 -2
  154. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  155. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  156. package/src/kernel/packs/iri.ts +5 -7
  157. package/src/kernel/packs/renderPack.ts +5 -3
  158. package/src/kernel/packs/resolveEntity.ts +21 -17
  159. package/src/kernel/packs/runBodies.ts +15 -14
  160. package/src/kernel/packs/schema.ts +96 -10
  161. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  162. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  163. package/src/kernel/packs/toolDescription.test.ts +10 -2
  164. package/src/kernel/packs/types.ts +39 -4
  165. package/src/kernel/packs/uniqueness.ts +4 -4
  166. package/src/kernel/project/cli/buildProgram.test.ts +60 -11
  167. package/src/kernel/project/cli/buildProgram.ts +11 -0
  168. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  169. package/src/kernel/project/cli/dispatch.ts +34 -12
  170. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  171. package/src/kernel/project/cli/globalFlags.ts +14 -14
  172. package/src/kernel/project/cli/helpFormat.ts +1 -1
  173. package/src/kernel/project/cli/rootHelp.ts +109 -107
  174. package/src/kernel/project/mcp/buildServer.ts +11 -6
  175. package/src/kernel/project/mcp/instructions.ts +12 -7
  176. package/src/kernel/project/mcp/mcpError.ts +52 -0
  177. package/src/kernel/project/mcp/parity.test.ts +39 -1
  178. package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
  179. package/src/kernel/project/mcp/prompts/provider.ts +51 -7
  180. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  181. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  182. package/src/kernel/project/mcp/serve.ts +11 -3
  183. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  184. package/src/kernel/render/disclosure.ts +4 -2
  185. package/src/kernel/render/formatters.ts +5 -4
  186. package/src/kernel/render/prefixes.test.ts +36 -0
  187. package/src/kernel/render/prefixes.ts +40 -13
  188. package/src/kernel/render/renderers.test.ts +35 -1
  189. package/src/kernel/render/renderers.ts +25 -7
  190. package/src/kernel/render/style.test.ts +31 -0
  191. package/src/kernel/render/style.ts +74 -0
  192. package/src/kernel/runtime/graphpack/build.ts +57 -3
  193. package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
  194. package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
  195. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
  196. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
  197. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  198. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  199. package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
  200. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  201. package/src/kernel/runtime/graphpack/index.ts +1 -1
  202. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  203. package/src/kernel/runtime/graphpack/read.ts +32 -22
  204. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  205. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  206. package/src/kernel/runtime/graphpack/types.ts +72 -45
  207. package/src/kernel/runtime/loadSession.ts +4 -12
  208. package/src/kernel/runtime/paths.ts +64 -15
  209. package/src/kernel/runtime/probe.test.ts +8 -4
  210. package/src/kernel/runtime/readEntity.ts +6 -5
  211. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  212. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  213. package/src/kernel/runtime/refs/resolve.ts +141 -65
  214. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  215. package/src/kernel/runtime/resolveSources.ts +36 -25
  216. package/src/kernel/runtime/storeReadiness.ts +62 -0
  217. package/src/kernel/runtime/storeless.test.ts +1 -1
  218. package/src/kernel/runtime/types.ts +1 -1
  219. package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
  220. package/src/kernel/spec/emitReference.test.ts +196 -0
  221. package/src/kernel/spec/emitReference.ts +624 -0
  222. package/src/kernel/spec/emitSurface.ts +13 -7
  223. package/src/kernel/spec/index.ts +2 -0
  224. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  225. package/src/kernel/spec/surfaceConformance.ts +8 -0
  226. package/src/kernel/spec/types.ts +29 -9
  227. package/src/kernel/spec/validate.ts +2 -1
  228. package/src/kernel/vocabulary.test.ts +164 -0
  229. package/src/kernel/vocabulary.ts +146 -0
  230. package/src/testing/PARITY_GAPS.ts +9 -3
  231. package/src/testing/behavioral/README.md +3 -3
  232. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  233. package/src/testing/behavioral/completion.test.ts +21 -8
  234. package/src/testing/behavioral/disclosure.test.ts +12 -0
  235. package/src/testing/behavioral/docExamples.test.ts +283 -0
  236. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  237. package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
  238. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  239. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  240. package/src/testing/behavioral/journeys.cli.test.ts +116 -73
  241. package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
  242. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  243. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  244. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  245. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
  246. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  247. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  248. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  249. package/src/testing/behavioral/staleCommands.test.ts +86 -0
  250. package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
  251. package/src/testing/eval/cases/assembly.ts +16 -9
  252. package/src/testing/eval/cases/effectful.ts +31 -16
  253. package/src/testing/eval/cases/readNouns.ts +23 -18
  254. package/src/testing/eval/cases/stable.ts +5 -1
  255. package/src/testing/eval/eval.test.ts +7 -1
  256. package/src/testing/fixtures/fixtureCapability.ts +201 -1
  257. package/src/testing/fixtures/graph/canonical.ts +6 -3
  258. package/src/testing/fixtures/graph/defaultPack.ts +179 -0
  259. package/src/testing/fixtures/graph/owlExport.ts +97 -0
  260. package/src/testing/helpers/extractSnippets.ts +29 -0
  261. package/src/testing/helpers/fixtureGraph.ts +33 -16
  262. package/src/testing/helpers/golden.ts +1 -1
  263. package/src/testing/helpers/packRuntime.ts +2 -3
  264. package/src/testing/helpers/projectCli.ts +2 -1
  265. package/src/testing/helpers/projectMcp.ts +6 -0
  266. package/src/testing/perf/budgets.test.ts +4 -0
  267. package/src/testing/perf/budgets.ts +22 -7
  268. package/src/testing/perf/globalSetup.ts +121 -6
  269. package/src/types.ts +2 -2
  270. package/src/capabilities/block/blockList.verb.ts +0 -150
  271. package/src/capabilities/block/index.ts +0 -25
  272. package/src/capabilities/block/pack.ts +0 -155
  273. package/src/capabilities/block/parity.test.ts +0 -180
  274. package/src/capabilities/block/tierChain.ts +0 -78
  275. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  276. package/src/capabilities/config/field.test.ts +0 -188
  277. package/src/capabilities/modifier/index.ts +0 -15
  278. package/src/capabilities/modifier/pack.ts +0 -64
  279. package/src/capabilities/standard/index.ts +0 -18
  280. package/src/capabilities/standard/pack.ts +0 -128
  281. package/src/capabilities/tier/index.ts +0 -19
  282. package/src/capabilities/tier/lookup.render.ts +0 -34
  283. package/src/capabilities/tier/lookup.test.ts +0 -68
  284. package/src/capabilities/tier/lookup.verb.ts +0 -44
  285. package/src/capabilities/tier/pack.ts +0 -28
  286. package/src/capabilities/tier/runLookup.ts +0 -59
  287. package/src/capabilities/token/addConfig.render.ts +0 -27
  288. package/src/capabilities/token/addConfig.test.ts +0 -62
  289. package/src/capabilities/token/addConfig.verb.ts +0 -40
  290. package/src/capabilities/token/index.ts +0 -19
  291. package/src/capabilities/token/pack.ts +0 -58
  292. package/src/capabilities/token/runAddConfig.ts +0 -95
  293. package/src/kernel/completion/bashDrive.test.ts +0 -73
  294. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  295. package/src/kernel/runtime/lock.ts +0 -90
@@ -11,6 +11,12 @@
11
11
  * it more than once (the confirm-gate preview + the build), so all real reads
12
12
  * belong in detection, never in `generate`.
13
13
  *
14
+ * The `--scope` selection is threaded through detection AND step-offering so
15
+ * `listAvailableSteps` only offers steps whose band the scope runs, and the MCP
16
+ * step targets exactly the deduped files for that scope. Per-file narrowing is
17
+ * opt-in (Item 6): the "all" default never springs a per-file question — it
18
+ * configures every deduped file.
19
+ *
14
20
  * This module carries NO static import of `@canonical/summon-core` VALUES or of
15
21
  * React/Ink — it only `import type`s the generator shape and builds plain object
16
22
  * literals over `@canonical/task`. It is itself reached only through the verb's
@@ -22,20 +28,29 @@ import type {
22
28
  PromptDefinition,
23
29
  } from "@canonical/summon-core";
24
30
  import { sequence_, type Task, when } from "@canonical/task";
31
+ import { BIN_NAME } from "../../../constants.js";
25
32
  import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
26
33
  import { guardMissingBinary } from "../../shared/assertExecOk.js";
27
- import type { SetupMode, SetupResult } from "../types.js";
34
+ import type {
35
+ LspState,
36
+ ScopeSelection,
37
+ SetupMode,
38
+ SetupResult,
39
+ } from "../types.js";
28
40
  import {
29
41
  type CompletionsDetection,
30
42
  composeCompletions,
31
43
  detectCompletions,
32
44
  } from "./setupCompletions.js";
33
- import { composeLsp } from "./setupLsp.js";
45
+ import { composeLsp, detectLsp, type LspDetection } from "./setupLsp.js";
34
46
  import {
35
47
  composeMcp,
36
48
  detectMcp,
37
49
  type McpDetection,
38
50
  mcpConfigured,
51
+ mcpGroupState,
52
+ mcpTargets,
53
+ selectedGroups,
39
54
  } from "./setupMcp.js";
40
55
  import {
41
56
  composeSkills,
@@ -61,22 +76,29 @@ export interface SetupPlan {
61
76
  /** The union of every step's detection, gathered up front for the run-all. */
62
77
  interface SetupDetection {
63
78
  readonly completions: CompletionsDetection;
79
+ readonly lsp: LspDetection;
64
80
  readonly mcp: McpDetection;
65
81
  readonly skills: SkillsDetection;
66
82
  }
67
83
 
68
84
  /** Read `answers.steps` (the run-all multiselect) as a string array. */
69
- const stepsOf = (answers: Record<string, unknown>): string[] =>
85
+ const readSteps = (answers: Record<string, unknown>): string[] =>
70
86
  Array.isArray(answers.steps) ? (answers.steps as string[]) : [];
71
87
 
72
- /** Read `answers.mcpHarnesses`, falling back to ALL detected ids. */
73
- const mcpSelection = (
88
+ /** Read `answers.mcpTargets` (selected file paths), falling back to ALL files. */
89
+ const resolveMcpPaths = (
74
90
  d: McpDetection,
75
91
  answers: Record<string, unknown>,
76
92
  ): string[] =>
77
- Array.isArray(answers.mcpHarnesses)
78
- ? (answers.mcpHarnesses as string[])
79
- : d.harnesses.map((h) => h.harness.id);
93
+ Array.isArray(answers.mcpTargets)
94
+ ? (answers.mcpTargets as string[])
95
+ : d.groups.map((g) => g.path);
96
+
97
+ /** The groups the user chose (or all, under the "all" default). */
98
+ const selectChosenGroups = (
99
+ d: McpDetection,
100
+ answers: Record<string, unknown>,
101
+ ) => selectedGroups(d, resolveMcpPaths(d, answers));
80
102
 
81
103
  /**
82
104
  * The LSP-install step, guarded at its use site. An absent `bunx` (no Bun on
@@ -87,41 +109,71 @@ const mcpSelection = (
87
109
  * exec (no spawn) — and re-runnable (the guard is a `recover`, and `composeLsp`
88
110
  * is combinator-built), so it survives `execute`'s double interpretation.
89
111
  */
90
- const composeGuardedLsp = (rt: PragmaRuntime): Task<void> =>
112
+ const composeGuardedLsp = (rt: PragmaRuntime, state: LspState): Task<void> =>
91
113
  guardMissingBinary(
92
114
  "bunx",
93
115
  {
94
116
  message:
95
117
  "Install Bun (https://bun.sh) to provide `bunx`, then run this again.",
96
118
  },
97
- composeLsp(rt.cwd),
119
+ composeLsp(rt.cwd, state),
98
120
  );
99
121
 
100
122
  /** Build a generator's `meta` (no stamping — the version is just header text). */
101
- const metaFor = (
123
+ const buildMeta = (
102
124
  rt: PragmaRuntime,
103
125
  title: string,
104
126
  ): GeneratorDefinition["meta"] => ({
105
127
  name: title,
106
128
  displayName: title,
107
- description: "Configure pragma for this project",
129
+ description: `Configure ${BIN_NAME} for this project`,
108
130
  version: rt.version,
109
131
  });
110
132
 
111
- /** The MCP-harness multiselect follow-up (offered only when harnesses exist). */
112
- const mcpHarnessesPrompt = (
133
+ /** The opt-in "customize which files" gate (Item 6) — defaults to false. */
134
+ const buildCustomizePrompt = (
135
+ when?: PromptDefinition["when"],
136
+ ): PromptDefinition => ({
137
+ name: "customize",
138
+ type: "confirm",
139
+ message: `Customize which files ${BIN_NAME} configures?`,
140
+ default: false,
141
+ when,
142
+ });
143
+
144
+ /** The suffix a group's prior state adds to its multiselect label. */
145
+ const mcpStateSuffix = (d: McpDetection, path: string): string => {
146
+ switch (mcpGroupState(d, path)) {
147
+ case "configured":
148
+ return " — already configured";
149
+ case "drifted":
150
+ return " — needs update";
151
+ default:
152
+ return "";
153
+ }
154
+ };
155
+
156
+ /**
157
+ * The per-file MCP multiselect — one row per deduped {@link TargetGroup} file.
158
+ * A row's label carries its prior state, and an already-`configured` file is
159
+ * DEFAULT-DESELECTED (so a re-run does not re-offer to rewrite what is already
160
+ * current); `absent`/`drifted` files stay selected by default.
161
+ */
162
+ const buildMcpTargetsPrompt = (
113
163
  d: McpDetection,
114
164
  when?: PromptDefinition["when"],
115
165
  ): PromptDefinition => ({
116
- name: "mcpHarnesses",
166
+ name: "mcpTargets",
117
167
  type: "multiselect",
118
- message: "Configure MCP for which harnesses?",
168
+ message: "Configure MCP for which files?",
119
169
  when,
120
- choices: d.harnesses.map((h) => ({
121
- label: h.harness.name,
122
- value: h.harness.id,
170
+ choices: d.groups.map((g) => ({
171
+ label: `${g.path} — ${g.harnessNames.join(", ")} [${g.scope}]${mcpStateSuffix(d, g.path)}`,
172
+ value: g.path,
123
173
  })),
124
- default: d.harnesses.map((h) => h.harness.id),
174
+ default: d.groups
175
+ .filter((g) => mcpGroupState(d, g.path) !== "configured")
176
+ .map((g) => g.path),
125
177
  });
126
178
 
127
179
  // =============================================================================
@@ -129,39 +181,55 @@ const mcpHarnessesPrompt = (
129
181
  // =============================================================================
130
182
 
131
183
  /** Gather EVERY step's detection up front (real reads), for the run-all. */
132
- async function gatherDetection(rt: PragmaRuntime): Promise<SetupDetection> {
133
- const [completions, mcp, skills] = await Promise.all([
184
+ async function gatherDetection(
185
+ rt: PragmaRuntime,
186
+ scope: ScopeSelection,
187
+ ): Promise<SetupDetection> {
188
+ const [completions, lsp, mcp, skills] = await Promise.all([
134
189
  detectCompletions(rt.cwd),
135
- detectMcp(rt),
190
+ detectLsp(rt.cwd),
191
+ detectMcp(rt, scope),
136
192
  detectSkills(rt),
137
193
  ]);
138
- return { completions, mcp, skills };
194
+ return { completions, lsp, mcp, skills };
139
195
  }
140
196
 
141
197
  /**
142
- * The steps worth OFFERING, given detection — undetectable steps are omitted so
143
- * the run-all degrades gracefully instead of throwing a mid-wizard
144
- * EMPTY_RESULTS (skills with no skills, mcp with no harnesses, completions with
145
- * no shell). LSP is always offered (its install is a self-contained exec).
198
+ * The steps worth OFFERING, given detection AND the scope selection. An
199
+ * undetectable step is omitted so the run-all degrades gracefully instead of
200
+ * throwing a mid-wizard EMPTY_RESULTS; a step whose band the scope does not run
201
+ * is omitted too (completions/lsp are global-band, skills are project-band, MCP
202
+ * spans both via its resolved groups). Threading `scope` here is what makes
203
+ * `setup --local` skip completions+lsp and `setup --global` skip skills — the
204
+ * MCP groups are already scoped by {@link detectMcp}.
205
+ *
206
+ * @param detected - Every step's up-front detection.
207
+ * @param scope - The resolved `--scope` selection (project/global/both).
208
+ * @returns The offerable steps, in composition/display order.
146
209
  */
147
- function availableSteps(
210
+ function listAvailableSteps(
148
211
  detected: SetupDetection,
212
+ scope: ScopeSelection,
149
213
  ): { label: string; value: StepId }[] {
214
+ const hasProject = scope !== "global";
215
+ const hasGlobal = scope !== "project";
150
216
  const choices: { label: string; value: StepId }[] = [];
151
- if (detected.completions.shell) {
217
+ if (detected.completions.shell && hasGlobal) {
152
218
  choices.push({
153
219
  label: `Shell completions (${detected.completions.shell})`,
154
220
  value: "completions",
155
221
  });
156
222
  }
157
- choices.push({ label: "Terrazzo LSP extension", value: "lsp" });
158
- if (detected.mcp.harnesses.length > 0) {
223
+ if (hasGlobal) {
224
+ choices.push({ label: "Terrazzo LSP extension", value: "lsp" });
225
+ }
226
+ if (detected.mcp.groups.length > 0) {
159
227
  choices.push({
160
- label: `MCP config (${detected.mcp.harnesses.length} harness(es))`,
228
+ label: `MCP config (${detected.mcp.groups.length} file(s))`,
161
229
  value: "mcp",
162
230
  });
163
231
  }
164
- if (detected.skills.available) {
232
+ if (detected.skills.available && hasProject) {
165
233
  choices.push({
166
234
  label: `Link skills (${detected.skills.skillCount})`,
167
235
  value: "skills",
@@ -174,9 +242,18 @@ function availableSteps(
174
242
  // Plan builders
175
243
  // =============================================================================
176
244
 
177
- /** The run-all self-verb: a step multiselect + per-step composition. */
178
- function runAllPlan(rt: PragmaRuntime, detected: SetupDetection): SetupPlan {
179
- const steps = availableSteps(detected);
245
+ /**
246
+ * The run-all self-verb: a step multiselect + per-step composition. The resolved
247
+ * `scope` narrows BOTH the offered steps (via {@link listAvailableSteps}) and the
248
+ * MCP target groups (already scoped by {@link detectMcp}), so `--local` omits the
249
+ * global-band completions+lsp and `--global` omits the project-band skills.
250
+ */
251
+ function buildRunAllPlan(
252
+ rt: PragmaRuntime,
253
+ detected: SetupDetection,
254
+ scope: ScopeSelection,
255
+ ): SetupPlan {
256
+ const steps = listAvailableSteps(detected, scope);
180
257
  const prompts: PromptDefinition[] = [
181
258
  {
182
259
  name: "steps",
@@ -186,11 +263,16 @@ function runAllPlan(rt: PragmaRuntime, detected: SetupDetection): SetupPlan {
186
263
  default: steps.map((c) => c.value), // --yes / non-TTY ⇒ every step
187
264
  },
188
265
  ];
189
- if (detected.mcp.harnesses.length > 0) {
266
+ // Item 6: per-file narrowing is opt-in. The "customize?" gate (default false)
267
+ // only surfaces when MCP is chosen and there is more than one file; the
268
+ // per-file multiselect only surfaces after an explicit yes — so the "all"
269
+ // default configures every deduped file without an extra question.
270
+ if (detected.mcp.groups.length > 0) {
190
271
  prompts.push(
191
- mcpHarnessesPrompt(
272
+ buildCustomizePrompt((a) => readSteps(a).includes("mcp")),
273
+ buildMcpTargetsPrompt(
192
274
  detected.mcp,
193
- (a) => stepsOf(a).includes("mcp"), // only when mcp is chosen
275
+ (a) => a.customize === true && readSteps(a).includes("mcp"),
194
276
  ),
195
277
  );
196
278
  }
@@ -201,19 +283,19 @@ function runAllPlan(rt: PragmaRuntime, detected: SetupDetection): SetupPlan {
201
283
  // reads already happened up front). Composing an unchosen step is harmless:
202
284
  // `when(false, …)` discards the (side-effect-free) task it was handed.
203
285
  const generator: GeneratorDefinition = {
204
- meta: metaFor(rt, "pragma setup"),
286
+ meta: buildMeta(rt, `${BIN_NAME} setup`),
205
287
  prompts,
206
288
  generate: (answers) => {
207
- const chosen = stepsOf(answers);
289
+ const chosen = readSteps(answers);
208
290
  return sequence_([
209
291
  when(
210
292
  chosen.includes("completions"),
211
293
  composeCompletions(detected.completions),
212
294
  ),
213
- when(chosen.includes("lsp"), composeGuardedLsp(rt)),
295
+ when(chosen.includes("lsp"), composeGuardedLsp(rt, detected.lsp.state)),
214
296
  when(
215
297
  chosen.includes("mcp"),
216
- composeMcp(detected.mcp, mcpSelection(detected.mcp, answers)),
298
+ composeMcp(detected.mcp, selectChosenGroups(detected.mcp, answers)),
217
299
  ),
218
300
  when(chosen.includes("skills"), composeSkills(detected.skills)),
219
301
  ]);
@@ -222,18 +304,18 @@ function runAllPlan(rt: PragmaRuntime, detected: SetupDetection): SetupPlan {
222
304
 
223
305
  return {
224
306
  generator,
225
- toResult: (answers) => ({ kind: "all", steps: stepsOf(answers) }),
307
+ toResult: (answers) => ({ kind: "all", steps: readSteps(answers) }),
226
308
  };
227
309
  }
228
310
 
229
311
  /** A single-step generator (no run-all multiselect) for one sub-verb. */
230
- function singleStep(
312
+ function buildSingleStep(
231
313
  rt: PragmaRuntime,
232
314
  title: string,
233
315
  prompts: PromptDefinition[],
234
316
  generate: (answers: Record<string, unknown>) => Task<void>,
235
317
  ): GeneratorDefinition {
236
- return { meta: metaFor(rt, title), prompts, generate };
318
+ return { meta: buildMeta(rt, title), prompts, generate };
237
319
  }
238
320
 
239
321
  /**
@@ -244,6 +326,7 @@ function singleStep(
244
326
  *
245
327
  * @param rt - The per-invocation runtime.
246
328
  * @param mode - The entry point (`all` or one sub-verb).
329
+ * @param scope - The resolved `--scope` selection (project/global/both).
247
330
  * @returns The generator + result mapper.
248
331
  * @throws PragmaError EMPTY_RESULTS for a direct `setup skills` with no skills.
249
332
  * @note Impure — performs each step's real detection.
@@ -251,45 +334,65 @@ function singleStep(
251
334
  export async function buildSetupPlan(
252
335
  rt: PragmaRuntime,
253
336
  mode: SetupMode,
337
+ scope: ScopeSelection,
254
338
  ): Promise<SetupPlan> {
255
339
  switch (mode) {
256
340
  case "all":
257
- return runAllPlan(rt, await gatherDetection(rt));
341
+ return buildRunAllPlan(rt, await gatherDetection(rt, scope), scope);
258
342
 
259
343
  case "completions": {
260
344
  const d = await detectCompletions(rt.cwd);
261
345
  return {
262
- generator: singleStep(rt, "pragma setup completions", [], () =>
263
- composeCompletions(d),
346
+ generator: buildSingleStep(
347
+ rt,
348
+ `${BIN_NAME} setup completions`,
349
+ [],
350
+ () => composeCompletions(d),
264
351
  ),
265
352
  toResult: () => ({
266
353
  kind: "completions",
267
354
  shell: d.shell,
268
355
  path: d.path,
269
356
  installed: d.shell !== null,
357
+ state: d.state,
270
358
  }),
271
359
  };
272
360
  }
273
361
 
274
- case "lsp":
362
+ case "lsp": {
363
+ const d = await detectLsp(rt.cwd);
275
364
  return {
276
- generator: singleStep(rt, "pragma setup lsp", [], () =>
277
- composeGuardedLsp(rt),
365
+ generator: buildSingleStep(rt, `${BIN_NAME} setup lsp`, [], () =>
366
+ composeGuardedLsp(rt, d.state),
278
367
  ),
279
- toResult: () => ({ kind: "lsp" }),
368
+ toResult: () => ({ kind: "lsp", state: d.state }),
280
369
  };
370
+ }
281
371
 
282
372
  case "mcp": {
283
- const d = await detectMcp(rt);
284
- const prompts = d.harnesses.length > 0 ? [mcpHarnessesPrompt(d)] : [];
373
+ const d = await detectMcp(rt, scope);
374
+ const prompts: PromptDefinition[] =
375
+ d.groups.length > 0
376
+ ? [
377
+ buildCustomizePrompt(),
378
+ buildMcpTargetsPrompt(d, (a) => a.customize === true),
379
+ ]
380
+ : [];
285
381
  return {
286
- generator: singleStep(rt, "pragma setup mcp", prompts, (answers) =>
287
- composeMcp(d, mcpSelection(d, answers)),
382
+ generator: buildSingleStep(
383
+ rt,
384
+ `${BIN_NAME} setup mcp`,
385
+ prompts,
386
+ (answers) => composeMcp(d, selectChosenGroups(d, answers)),
288
387
  ),
289
- toResult: (answers) => ({
290
- kind: "mcp",
291
- configured: mcpConfigured(d, mcpSelection(d, answers)),
292
- }),
388
+ toResult: (answers) => {
389
+ const sel = selectChosenGroups(d, answers);
390
+ return {
391
+ kind: "mcp",
392
+ configured: mcpConfigured(sel),
393
+ targets: mcpTargets(d, sel),
394
+ };
395
+ },
293
396
  };
294
397
  }
295
398
 
@@ -297,7 +400,7 @@ export async function buildSetupPlan(
297
400
  const d = await detectSkills(rt);
298
401
  if (!d.available) throw skillsEmptyError();
299
402
  return {
300
- generator: singleStep(rt, "pragma setup skills", [], () =>
403
+ generator: buildSingleStep(rt, `${BIN_NAME} setup skills`, [], () =>
301
404
  composeSkills(d),
302
405
  ),
303
406
  toResult: () => ({ kind: "skills", result: toSkillsResult(d) }),
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * `setup lsp` — ensure the Terrazzo LSP VS Code extension is installed.
3
3
  *
4
- * There is nothing to detect (the installer is a single idempotent `exec`), so
5
- * `detectLsp` is a trivial marker and `composeLsp` holds the sole mutation —
6
- * mocked under `--dry-run`, run for real otherwise. A successful run reports it
7
- * installed (up to date), not a fresh install.
4
+ * `detectLsp` probes for the extension FOR REAL up front — it runs
5
+ * `code --list-extensions` (via the `exec` seam, over `runTask`) and matches the
6
+ * `terrazzo` extension id — so a re-run of an already-installed extension is a
7
+ * true no-op and the recap says so, rather than always shelling out. When the
8
+ * `code` CLI is absent from PATH the state is `unknown` (we cannot enumerate,
9
+ * so the installer still runs). `composeLsp` holds the sole mutation, mocked
10
+ * under `--dry-run` and skipped when detection already found the extension.
8
11
  */
9
12
 
10
13
  import {
@@ -17,18 +20,51 @@ import {
17
20
  type Task,
18
21
  } from "@canonical/task";
19
22
  import { assertExecOk } from "../../shared/assertExecOk.js";
23
+ import type { LspState } from "../types.js";
20
24
 
21
- /** LSP has no real detection — the install is one idempotent subprocess. */
25
+ /** The VS Code CLI queried for the installed-extensions list. */
26
+ const VSCODE_CLI = "code";
27
+
28
+ /** The substring an installed Terrazzo extension id contains (publisher-agnostic). */
29
+ const TERRAZZO_EXTENSION_MATCH = "terrazzo";
30
+
31
+ /**
32
+ * The detected LSP state: whether the Terrazzo extension is already `installed`,
33
+ * `absent`, or `unknown` (the `code` CLI is not on PATH, so we cannot enumerate).
34
+ */
22
35
  export interface LspDetection {
23
36
  readonly available: true;
37
+ readonly state: LspState;
24
38
  }
25
39
 
26
40
  /**
27
- * "Detect" the LSP step. Always available — kept for symmetry with the other
28
- * steps' detect phase (so the run-all can gather every step uniformly).
41
+ * Probe VS Code for the Terrazzo extension. Runs `code --list-extensions` and
42
+ * matches the `terrazzo` id; an absent `code` CLI (spawn ENOENT) or a nonzero
43
+ * exit yields `unknown` (we fall through to running the installer).
44
+ *
45
+ * @param cwd - The directory the probe runs in.
46
+ * @returns The detected {@link LspState}.
47
+ * @note Impure — spawns `code --list-extensions`.
29
48
  */
30
- export function detectLsp(): LspDetection {
31
- return { available: true };
49
+ export async function detectLsp(cwd: string): Promise<LspDetection> {
50
+ const { runTask } = await import("@canonical/task/node");
51
+ let state: LspState = "unknown";
52
+ try {
53
+ const result = (await runTask(
54
+ exec(VSCODE_CLI, ["--list-extensions"], cwd),
55
+ )) as ExecResult;
56
+ if (result.exitCode === 0) {
57
+ const installed = result.stdout
58
+ .toLowerCase()
59
+ .includes(TERRAZZO_EXTENSION_MATCH);
60
+ state = installed ? "installed" : "absent";
61
+ }
62
+ } catch {
63
+ // `code` absent from PATH (ENOENT) or any spawn failure → we cannot tell;
64
+ // leave `unknown` so the installer still runs.
65
+ state = "unknown";
66
+ }
67
+ return { available: true, state };
32
68
  }
33
69
 
34
70
  /**
@@ -37,12 +73,20 @@ export function detectLsp(): LspDetection {
37
73
  * Built from re-runnable combinators (NOT a single-use `gen`) because `execute`
38
74
  * interprets the task twice (preview + perform). Under the dry-run preview the
39
75
  * `exec` is MOCKED to `exitCode 0`, so `assertExecOk` passes there; a real run's
40
- * nonzero exit still fails loudly.
76
+ * nonzero exit still fails loudly. When detection already found the extension
77
+ * `installed`, the install is SKIPPED — a re-run is a true no-op.
41
78
  *
42
79
  * @param cwd - The directory to run the installer in.
43
- * @returns A Task that execs the extension installer, failing loudly on nonzero.
80
+ * @param state - The prior state from {@link detectLsp} (defaults to `unknown`).
81
+ * @returns A Task that execs the installer (or reports already-installed).
44
82
  */
45
- export function composeLsp(cwd: string): Task<void> {
83
+ export function composeLsp(
84
+ cwd: string,
85
+ state: LspState = "unknown",
86
+ ): Task<void> {
87
+ if (state === "installed") {
88
+ return info("Terrazzo LSP VS Code extension already installed — skipped.");
89
+ }
46
90
  return sequence_([
47
91
  info("Ensuring the Terrazzo LSP VS Code extension is installed..."),
48
92
  // The interpreter RESOLVES on a nonzero exit — a failed installer must fail