@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
@@ -185,7 +185,11 @@ describe("setup mcp — recap gate", () => {
185
185
  mkdirSync(join(cwd, ".cursor"), { recursive: true });
186
186
  const configPath = join(cwd, ".cursor", "mcp.json");
187
187
 
188
- const { generator } = await buildSetupPlan(bootRuntime(FLAGS, cwd), "mcp");
188
+ const { generator } = await buildSetupPlan(
189
+ bootRuntime(FLAGS, cwd),
190
+ "mcp",
191
+ "both",
192
+ );
189
193
  // A handler that declines EVERYTHING (incl. execute's "Proceed?" gate) must
190
194
  // fail the run cleanly — the GENERATOR_CANCELLED code the boundary renders
191
195
  // as a plain "Cancelled." — and write nothing.
@@ -197,6 +201,207 @@ describe("setup mcp — recap gate", () => {
197
201
  });
198
202
  });
199
203
 
204
+ describe("setup mcp — scope & dedup", () => {
205
+ // Isolate PATH so a `claude`/`codex` on the host PATH can't inject harnesses
206
+ // via a `process` signal — detection is driven only by the dirs each test makes.
207
+ let prevPath: string | undefined;
208
+ beforeEach(() => {
209
+ prevPath = process.env.PATH;
210
+ process.env.PATH = tmp("pragma-scope-path-");
211
+ });
212
+ afterEach(() => {
213
+ process.env.PATH = prevPath;
214
+ });
215
+
216
+ it("writes both VS Code and Cline keys into one .vscode/mcp.json (7a dedup)", async () => {
217
+ const cwd = tmp("pragma-setup-proj-");
218
+ mkdirSync(join(cwd, ".vscode"), { recursive: true }); // VS Code (dir signal)
219
+ // Cline is detected by its extension (NOT the .vscode dir), so install a fake
220
+ // one under the isolated HOME — both harnesses then resolve to the SAME
221
+ // .vscode/mcp.json, which is what the two-level dedup write must handle. The
222
+ // extension is matched by its `package.json` manifest, so write one.
223
+ const clineExt = join(
224
+ process.env.HOME ?? "",
225
+ ".vscode",
226
+ "extensions",
227
+ "saoudrizwan.claude-dev-1.0.0",
228
+ );
229
+ mkdirSync(clineExt, { recursive: true });
230
+ writeFileSync(join(clineExt, "package.json"), "{}");
231
+ const outcome = await executeVerb(
232
+ verbOf("mcp"),
233
+ {},
234
+ YES,
235
+ bootRuntime(FLAGS, cwd),
236
+ );
237
+ expect(outcome.exitCode).toBe(0);
238
+ const config = JSON.parse(
239
+ readFileSync(join(cwd, ".vscode", "mcp.json"), "utf-8"),
240
+ );
241
+ // VS Code writes under `servers`, Cline under `mcpServers` — the two-level
242
+ // dedup makes two writes to one file, each preserving the other's key.
243
+ expect(config.servers?.pragma?.command).toBe("pragma");
244
+ expect(config.mcpServers?.pragma?.command).toBe("pragma");
245
+ });
246
+
247
+ it("--local writes the project config and skips a global-only harness", async () => {
248
+ const cwd = tmp("pragma-setup-proj-");
249
+ const home = process.env.HOME ?? "";
250
+ mkdirSync(join(cwd, ".cursor"), { recursive: true }); // project scope
251
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // global scope
252
+ await executeVerb(
253
+ verbOf("mcp"),
254
+ { local: true },
255
+ YES,
256
+ bootRuntime(FLAGS, cwd),
257
+ );
258
+ expect(existsSync(join(cwd, ".cursor", "mcp.json"))).toBe(true);
259
+ expect(
260
+ existsSync(join(home, ".codeium", "windsurf", "mcp_config.json")),
261
+ ).toBe(false);
262
+ });
263
+
264
+ it("--global writes a global harness's home config, skipping project harnesses", async () => {
265
+ const cwd = tmp("pragma-setup-proj-");
266
+ const home = process.env.HOME ?? "";
267
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // global scope
268
+ mkdirSync(join(cwd, ".cursor"), { recursive: true }); // project scope
269
+ await executeVerb(
270
+ verbOf("mcp"),
271
+ { global: true },
272
+ YES,
273
+ bootRuntime(FLAGS, cwd),
274
+ );
275
+ const windsurfHome = JSON.parse(
276
+ readFileSync(
277
+ join(home, ".codeium", "windsurf", "mcp_config.json"),
278
+ "utf-8",
279
+ ),
280
+ );
281
+ expect(windsurfHome.mcpServers?.pragma?.command).toBe("pragma");
282
+ expect(existsSync(join(cwd, ".cursor", "mcp.json"))).toBe(false);
283
+ });
284
+
285
+ // The `--scope <enum>` values (project/global/both) resolve independently of
286
+ // the `--global`/`--local` boolean sugars above — a distinct resolveScope arm.
287
+ it("--scope project (the enum) writes the project config, skipping a global harness", async () => {
288
+ const cwd = tmp("pragma-setup-proj-");
289
+ const home = process.env.HOME ?? "";
290
+ mkdirSync(join(cwd, ".cursor"), { recursive: true }); // project scope
291
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // global scope
292
+ await executeVerb(
293
+ verbOf("mcp"),
294
+ { scope: "project" },
295
+ YES,
296
+ bootRuntime(FLAGS, cwd),
297
+ );
298
+ expect(existsSync(join(cwd, ".cursor", "mcp.json"))).toBe(true);
299
+ expect(
300
+ existsSync(join(home, ".codeium", "windsurf", "mcp_config.json")),
301
+ ).toBe(false);
302
+ });
303
+
304
+ it("--scope global (the enum) writes the home config, skipping a project harness", async () => {
305
+ const cwd = tmp("pragma-setup-proj-");
306
+ const home = process.env.HOME ?? "";
307
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // global scope
308
+ mkdirSync(join(cwd, ".cursor"), { recursive: true }); // project scope
309
+ await executeVerb(
310
+ verbOf("mcp"),
311
+ { scope: "global" },
312
+ YES,
313
+ bootRuntime(FLAGS, cwd),
314
+ );
315
+ expect(
316
+ existsSync(join(home, ".codeium", "windsurf", "mcp_config.json")),
317
+ ).toBe(true);
318
+ expect(existsSync(join(cwd, ".cursor", "mcp.json"))).toBe(false);
319
+ });
320
+ });
321
+
322
+ describe("setup mcp — customize opt-in gate (Item 6)", () => {
323
+ it("gates the per-file multiselect behind an explicit customize=true", async () => {
324
+ const cwd = tmp("pragma-setup-proj-");
325
+ mkdirSync(join(cwd, ".cursor"), { recursive: true }); // ⇒ one MCP target group
326
+ const { generator } = await buildSetupPlan(
327
+ bootRuntime(FLAGS, cwd),
328
+ "mcp",
329
+ "both",
330
+ );
331
+ const customize = generator.prompts.find((p) => p.name === "customize");
332
+ const targets = generator.prompts.find((p) => p.name === "mcpTargets");
333
+ // The gate is an opt-in confirm that defaults to NOT customizing.
334
+ expect(customize?.type).toBe("confirm");
335
+ expect(customize?.default).toBe(false);
336
+ // The per-file multiselect only surfaces after an explicit yes — so the
337
+ // "all" default configures every deduped file without an extra question.
338
+ expect(targets?.when?.({})).toBe(false);
339
+ expect(targets?.when?.({ customize: false })).toBe(false);
340
+ expect(targets?.when?.({ customize: true })).toBe(true);
341
+ });
342
+ });
343
+
344
+ describe("setup (run-all wizard) — scope threading", () => {
345
+ // Isolate PATH so an ambient `claude`/`codex` can't inject a harness via a
346
+ // `process` signal — detection is driven only by the dirs each test makes.
347
+ let prevPath: string | undefined;
348
+ beforeEach(() => {
349
+ prevPath = process.env.PATH;
350
+ process.env.PATH = tmp("pragma-runall-path-");
351
+ });
352
+ afterEach(() => {
353
+ process.env.PATH = prevPath;
354
+ });
355
+
356
+ /** Seed a discoverable skill so the project-band skills step is offerable. */
357
+ const seedSkill = (cwd: string): void => {
358
+ const dir = join(cwd, ".pragma", "skills", "s");
359
+ mkdirSync(dir, { recursive: true });
360
+ writeFileSync(
361
+ join(dir, "SKILL.md"),
362
+ "---\nname: s\ndescription: A skill.\n---\n",
363
+ );
364
+ };
365
+
366
+ it("--local omits the global-band completions + lsp steps, keeping project steps", async () => {
367
+ const cwd = tmp("pragma-setup-proj-");
368
+ mkdirSync(join(cwd, ".cursor"), { recursive: true }); // project-band MCP target
369
+ seedSkill(cwd); // project-band skills step
370
+ const outcome = await executeVerb(
371
+ setupSelfVerb,
372
+ { local: true },
373
+ DRY,
374
+ bootRuntime(FLAGS, cwd),
375
+ );
376
+ expect(outcome.exitCode).toBe(0);
377
+ const plan = outcome.stdout ?? "";
378
+ // The global-band steps are gone under --local (the bug: they used to run).
379
+ expect(plan).not.toContain(completionScriptPath("zsh"));
380
+ expect(plan).not.toContain("terrazzo-lsp-extension");
381
+ // The project-band steps remain: MCP into .cursor, skills into .agents/skills.
382
+ expect(plan).toContain("mcp.json");
383
+ expect(plan).toContain(".agents/skills");
384
+ });
385
+
386
+ it("--global omits the project-band skills step, keeping global steps", async () => {
387
+ const cwd = tmp("pragma-setup-proj-");
388
+ seedSkill(cwd); // WOULD be offered under the default `both`
389
+ const outcome = await executeVerb(
390
+ setupSelfVerb,
391
+ { global: true },
392
+ DRY,
393
+ bootRuntime(FLAGS, cwd),
394
+ );
395
+ expect(outcome.exitCode).toBe(0);
396
+ const plan = outcome.stdout ?? "";
397
+ // The global-band steps are present under --global.
398
+ expect(plan).toContain(completionScriptPath("zsh"));
399
+ expect(plan).toContain("terrazzo-lsp-extension");
400
+ // The project-band skills step is gone (the bug: it used to run under --global).
401
+ expect(plan).not.toContain(".agents/skills");
402
+ });
403
+ });
404
+
200
405
  describe("setup skills", () => {
201
406
  it("empty skills raise EMPTY_RESULTS on the direct sub-verb", async () => {
202
407
  const cwd = tmp("pragma-setup-proj-");
@@ -261,6 +466,152 @@ describe("setup lsp — missing-binary guard (bunx absent)", () => {
261
466
  });
262
467
  });
263
468
 
469
+ describe("setup — idempotent detection of already-present config", () => {
470
+ // Isolate PATH so an ambient harness can't inject via a `process` signal.
471
+ let prevPath: string | undefined;
472
+ beforeEach(() => {
473
+ prevPath = process.env.PATH;
474
+ process.env.PATH = tmp("pragma-idem-path-");
475
+ });
476
+ afterEach(() => {
477
+ process.env.PATH = prevPath;
478
+ });
479
+
480
+ it("mcp: a second run detects the already-configured file (byte-identical rewrite)", async () => {
481
+ const cwd = tmp("pragma-setup-proj-");
482
+ mkdirSync(join(cwd, ".cursor"), { recursive: true });
483
+ const configPath = join(cwd, ".cursor", "mcp.json");
484
+
485
+ // First run writes the pragma entry.
486
+ await executeVerb(verbOf("mcp"), {}, YES, bootRuntime(FLAGS, cwd));
487
+ const firstBody = readFileSync(configPath, "utf-8");
488
+
489
+ // Detection now classifies the group `configured`.
490
+ const { toResult } = await buildSetupPlan(
491
+ bootRuntime(FLAGS, cwd),
492
+ "mcp",
493
+ "both",
494
+ );
495
+ const result = toResult({});
496
+ expect(result.kind).toBe("mcp");
497
+ if (result.kind === "mcp") {
498
+ expect(result.targets.at(0)?.state).toBe("configured");
499
+ }
500
+
501
+ // The wizard default-DESELECTS a configured target (so a plain re-run offers
502
+ // it unchecked) — proven on the generated multiselect's default.
503
+ const { generator } = await buildSetupPlan(
504
+ bootRuntime(FLAGS, cwd),
505
+ "mcp",
506
+ "both",
507
+ );
508
+ const multiselect = generator.prompts.find((p) => p.name === "mcpTargets");
509
+ expect(multiselect?.default).toEqual([]); // the only file is configured
510
+ expect(
511
+ (multiselect?.choices ?? []).some((c) =>
512
+ typeof c === "object" && "label" in c
513
+ ? c.label.includes("already configured")
514
+ : false,
515
+ ),
516
+ ).toBe(true);
517
+
518
+ // A real second run is idempotent: the file stays byte-identical (the
519
+ // read-modify-write re-merges the SAME pragma entry).
520
+ await executeVerb(verbOf("mcp"), {}, YES, bootRuntime(FLAGS, cwd));
521
+ expect(readFileSync(configPath, "utf-8")).toBe(firstBody);
522
+ });
523
+
524
+ it("mcp: a drifted pragma entry (wrong cwd) reads as `drifted` and is updated", async () => {
525
+ const cwd = tmp("pragma-setup-proj-");
526
+ mkdirSync(join(cwd, ".cursor"), { recursive: true });
527
+ const configPath = join(cwd, ".cursor", "mcp.json");
528
+ // Seed a stale pragma entry pointing at a different cwd.
529
+ writeFileSync(
530
+ configPath,
531
+ `${JSON.stringify(
532
+ {
533
+ mcpServers: {
534
+ pragma: { command: "pragma", args: ["mcp"], cwd: "/old" },
535
+ },
536
+ },
537
+ null,
538
+ 2,
539
+ )}\n`,
540
+ );
541
+
542
+ const { toResult, generator } = await buildSetupPlan(
543
+ bootRuntime(FLAGS, cwd),
544
+ "mcp",
545
+ "both",
546
+ );
547
+ const result = toResult({});
548
+ if (result.kind === "mcp") {
549
+ expect(result.targets.at(0)?.state).toBe("drifted");
550
+ }
551
+ // A drifted target stays SELECTED by default (it needs the update).
552
+ const multiselect = generator.prompts.find((p) => p.name === "mcpTargets");
553
+ expect(multiselect?.default).toEqual([configPath]);
554
+
555
+ await executeVerb(verbOf("mcp"), {}, YES, bootRuntime(FLAGS, cwd));
556
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
557
+ expect(config.mcpServers.pragma.cwd).toBe(cwd); // updated to the real cwd
558
+ });
559
+
560
+ it("completions: a second run detects the up-to-date script (state=installed)", async () => {
561
+ const cwd = tmp("pragma-setup-proj-");
562
+ const path = completionScriptPath("zsh");
563
+ // First install.
564
+ await executeVerb(completionsVerb, {}, YES, bootRuntime(FLAGS, cwd));
565
+ const firstBody = readFileSync(path, "utf-8");
566
+
567
+ // Detection now classifies the installed script `installed`.
568
+ const { toResult } = await buildSetupPlan(
569
+ bootRuntime(FLAGS, cwd),
570
+ "completions",
571
+ "both",
572
+ );
573
+ const result = toResult({});
574
+ if (result.kind === "completions") {
575
+ expect(result.state).toBe("installed");
576
+ }
577
+
578
+ // A real second run is idempotent — the byte-identical script survives.
579
+ await executeVerb(completionsVerb, {}, YES, bootRuntime(FLAGS, cwd));
580
+ expect(readFileSync(path, "utf-8")).toBe(firstBody);
581
+ });
582
+
583
+ it("completions: a stale script (different body) reads as `stale`", async () => {
584
+ const cwd = tmp("pragma-setup-proj-");
585
+ const path = completionScriptPath("zsh");
586
+ mkdirSync(join(path, ".."), { recursive: true });
587
+ writeFileSync(path, "# stale hand-edited completion\n");
588
+ const { toResult } = await buildSetupPlan(
589
+ bootRuntime(FLAGS, cwd),
590
+ "completions",
591
+ "both",
592
+ );
593
+ const result = toResult({});
594
+ if (result.kind === "completions") {
595
+ expect(result.state).toBe("stale");
596
+ }
597
+ });
598
+
599
+ it("lsp: reports `unknown` when the `code` CLI is absent from PATH", async () => {
600
+ // PATH is the isolated empty dir (beforeEach), so `code` is unresolvable:
601
+ // detection cannot enumerate and reports `unknown` (installer still runs).
602
+ const { toResult } = await buildSetupPlan(
603
+ bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
604
+ "lsp",
605
+ "both",
606
+ );
607
+ const result = toResult({});
608
+ expect(result.kind).toBe("lsp");
609
+ if (result.kind === "lsp") {
610
+ expect(result.state).toBe("unknown");
611
+ }
612
+ });
613
+ });
614
+
264
615
  describe("setup (run-all wizard)", () => {
265
616
  it("--dry-run previews every DETECTED step (completions + lsp + mcp), writing nothing", async () => {
266
617
  const cwd = tmp("pragma-setup-proj-");
@@ -21,11 +21,55 @@
21
21
 
22
22
  import type { GeneratorResult } from "@canonical/summon-core";
23
23
  import { $, gen, type Task } from "@canonical/task";
24
+ import { BIN_NAME } from "../../constants.js";
24
25
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
25
26
  import { asVerb } from "../../kernel/spec/asVerb.js";
26
- import type { CapabilityModule, VerbSpec } from "../../kernel/spec/types.js";
27
+ import type {
28
+ CapabilityModule,
29
+ ParamSpec,
30
+ VerbSpec,
31
+ } from "../../kernel/spec/types.js";
27
32
  import { setupFormatters } from "./setup.render.js";
28
- import type { SetupMode, SetupResult } from "./types.js";
33
+ import type { ScopeSelection, SetupMode, SetupResult } from "./types.js";
34
+
35
+ /**
36
+ * The `--scope` flag + its `--global`/`--local` boolean sugars, shared by the
37
+ * band-aware verbs (`setup`/`mcp`/`skills`). `completions`/`lsp` never carry
38
+ * them — they are single-band installers.
39
+ */
40
+ const SCOPE_PARAMS: readonly ParamSpec[] = [
41
+ {
42
+ kind: "enum",
43
+ name: "scope",
44
+ doc: "Which config band(s) to configure: project, global, or both.",
45
+ values: ["project", "global", "both"],
46
+ default: "both",
47
+ },
48
+ {
49
+ kind: "boolean",
50
+ name: "global",
51
+ doc: "Shorthand for --scope global (configure the user/home band).",
52
+ },
53
+ {
54
+ kind: "boolean",
55
+ name: "local",
56
+ doc: "Shorthand for --scope project (configure the per-project band).",
57
+ },
58
+ ];
59
+
60
+ /**
61
+ * Resolve the `--scope`/`--global`/`--local` params into a single selection.
62
+ * The boolean sugars win over `--scope`; `--global` wins over `--local`.
63
+ *
64
+ * @param params - The coerced param bag.
65
+ * @returns The resolved scope selection (defaults to `both`).
66
+ */
67
+ function resolveScope(params: Record<string, unknown>): ScopeSelection {
68
+ if (params.global === true) return "global";
69
+ if (params.local === true) return "project";
70
+ const scope = params.scope;
71
+ return scope === "global" || scope === "project" ? scope : "both";
72
+ }
29
73
 
30
74
  /** The exposed-to-MCP capability the run-all self-verb declares. */
31
75
  const SELF_CAPABILITY = {
@@ -76,6 +120,7 @@ const SUB_CAPABILITY = {
76
120
  async function runSetup(
77
121
  mode: SetupMode,
78
122
  rt: PragmaRuntime,
123
+ scope: ScopeSelection,
79
124
  ): Promise<Task<SetupResult>> {
80
125
  // Lazy dynamic imports (R1 lazy-React discipline): summon-core's barrel is
81
126
  // React-free, and the non-TTY branch picks autoPrompt/mcpPrompt (never mounts
@@ -85,7 +130,7 @@ async function runSetup(
85
130
  import("./operations/setupGenerator.js"),
86
131
  ]);
87
132
 
88
- const { generator, toResult } = await ops.buildSetupPlan(rt, mode);
133
+ const { generator, toResult } = await ops.buildSetupPlan(rt, mode, scope);
89
134
 
90
135
  const { isTTY, transport, yes, signal, abort } = rt.interaction ?? {
91
136
  isTTY: false,
@@ -149,7 +194,8 @@ function setupVerb(
149
194
  output: { formatters: setupFormatters },
150
195
  capability,
151
196
  ...extras,
152
- run: (_params, rt) => runSetup(mode, rt) as unknown as Task<SetupResult>,
197
+ run: (params, rt) =>
198
+ runSetup(mode, rt, resolveScope(params)) as unknown as Task<SetupResult>,
153
199
  };
154
200
  }
155
201
 
@@ -159,20 +205,32 @@ const setupAllVerb = setupVerb(
159
205
  "all",
160
206
  SELF_CAPABILITY,
161
207
  {
162
- doc: "Runs the shell-completions, LSP, MCP, and skills installers as a single wizard: pick the steps, review the recap, then apply.",
208
+ params: SCOPE_PARAMS,
209
+ doc: "Runs the shell-completions, LSP, MCP, and skills installers as a single wizard: pick the steps, review the recap, then apply. The scope option targets the project band, the user/home band, or both.",
163
210
  examples: [
164
- { cmd: "pragma setup" },
165
- { cmd: "pragma setup --dry-run", note: "preview every step's effects" },
166
- { cmd: "pragma setup mcp", note: "just the MCP server registration" },
211
+ { cmd: `${BIN_NAME} setup` },
212
+ {
213
+ cmd: `${BIN_NAME} setup --dry-run`,
214
+ note: "preview every step's effects",
215
+ },
216
+ {
217
+ cmd: `${BIN_NAME} setup --global`,
218
+ note: "configure only the user/home band",
219
+ },
220
+ {
221
+ cmd: `${BIN_NAME} setup mcp`,
222
+ note: "just the MCP server registration",
223
+ },
167
224
  ],
168
225
  },
169
226
  );
170
227
 
171
228
  const mcpVerb = setupVerb(
172
229
  ["setup", "mcp"],
173
- "Register the pragma MCP server in detected AI harnesses.",
230
+ `Register the ${BIN_NAME} MCP server in detected AI harnesses.`,
174
231
  "mcp",
175
232
  SUB_CAPABILITY,
233
+ { params: SCOPE_PARAMS },
176
234
  );
177
235
 
178
236
  const completionsVerb = setupVerb(
@@ -187,6 +245,7 @@ const skillsVerb = setupVerb(
187
245
  "Symlink discovered skills into each AI harness.",
188
246
  "skills",
189
247
  SUB_CAPABILITY,
248
+ { params: SCOPE_PARAMS },
190
249
  );
191
250
 
192
251
  const lspVerb = setupVerb(
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import { BIN_NAME } from "../../constants.js";
12
13
 
13
14
  /** The shells `setup completions` can install for. */
14
15
  export type ShellId = "zsh" | "bash" | "fish";
@@ -34,6 +35,31 @@ export function detectShell(): ShellId | null {
34
35
  /**
35
36
  * The standard install path for a shell's completion script.
36
37
  *
38
+ * The BASENAME is {@link BIN_NAME}, never a literal, because `emitScripts`
39
+ * derives the script's own `#compdef` / `complete -F` / `complete -c` from that
40
+ * same name, and for two of the three shells the basename decides whether the
41
+ * file is ever loaded. Measured against real shells with the distribution
42
+ * renamed to `widget9`:
43
+ *
44
+ * - **fish** autoloads `completions/<cmd>.fish` BY NAME. A correct `widget9`
45
+ * script in a file called `pragma.fish` is never loaded — TAB silently falls
46
+ * back to filenames. Verified: renaming that one file is the entire
47
+ * difference between candidates and no candidates.
48
+ * - **bash-completion** looks up `completions/<cmd>` by the command name the
49
+ * same way. Verified against bash-completion 2.11's `__load_completion`:
50
+ * with the file named `widget9`, `complete -p widget9` reports
51
+ * `complete -F _widget9 widget9`; with the identical file named `pragma`,
52
+ * `_widget9` is never defined and bash falls back to `_minimal`.
53
+ * - **zsh** does NOT work that way: `compinit` binds by the `#compdef` tag
54
+ * inside the file, so `~/.zfunc/_pragma` holding `#compdef widget9` does
55
+ * complete `widget9` (verified — `_comps[widget9]` resolves to `_pragma`).
56
+ * The basename still matters, for a different reason: it names the function
57
+ * zsh autoloads, and two distributions both writing `_pragma` would silently
58
+ * overwrite each other's completions.
59
+ *
60
+ * Either way `setup completions` and `doctor` report success, so nothing else
61
+ * would tell the user.
62
+ *
37
63
  * @param shell - The target shell.
38
64
  * @returns The absolute path the completion script is written to.
39
65
  * @note Impure — reads the home directory.
@@ -42,7 +68,7 @@ export function completionScriptPath(shell: ShellId): string {
42
68
  const home = homedir();
43
69
  switch (shell) {
44
70
  case "zsh":
45
- return join(home, ".zfunc", "_pragma");
71
+ return join(home, ".zfunc", `_${BIN_NAME}`);
46
72
  case "bash":
47
73
  return join(
48
74
  home,
@@ -50,10 +76,10 @@ export function completionScriptPath(shell: ShellId): string {
50
76
  "share",
51
77
  "bash-completion",
52
78
  "completions",
53
- "pragma",
79
+ BIN_NAME,
54
80
  );
55
81
  case "fish":
56
- return join(home, ".config", "fish", "completions", "pragma.fish");
82
+ return join(home, ".config", "fish", "completions", `${BIN_NAME}.fish`);
57
83
  }
58
84
  }
59
85
 
@@ -11,6 +11,58 @@ import type { ShellId } from "./shell.js";
11
11
  */
12
12
  export type SetupMode = "all" | "completions" | "lsp" | "mcp" | "skills";
13
13
 
14
+ /**
15
+ * One of the two config bands: the per-user/home `global` band or the per-repo
16
+ * `project` band. Mirrors `@canonical/harnesses`' `ScopeBand` structurally, but
17
+ * is redeclared here so this statically-reachable type module never pulls the
18
+ * harnesses runtime into the fast-path module graph (the lazy-dispatch invariant).
19
+ */
20
+ export type ScopeBand = "project" | "global";
21
+
22
+ /**
23
+ * The resolved `--scope` selection: which config band(s) a run touches. `both`
24
+ * (the default) runs each detected harness's default band; `global`/`project`
25
+ * run only that band. Structurally mirrors the harnesses `ScopeSelection`.
26
+ */
27
+ export type ScopeSelection = "project" | "global" | "both";
28
+
29
+ /**
30
+ * The prior state of an MCP target file, read up front by `detectMcp`:
31
+ * `absent` (no pragma entry yet), `configured` (a matching pragma entry already
32
+ * present in every write — a re-run skips it), or `drifted` (a pragma entry
33
+ * exists but differs, so a write updates it). Mirrors the skills step's
34
+ * created/skipped/replaced idempotency at the file grain.
35
+ */
36
+ export type McpTargetState = "absent" | "configured" | "drifted";
37
+
38
+ /**
39
+ * One configured MCP target in a result: the file that was written, which band
40
+ * it belongs to, the harness name(s) that share it, and its prior
41
+ * {@link McpTargetState} (so the recap can report new vs updated vs unchanged).
42
+ */
43
+ export interface ConfiguredTarget {
44
+ readonly name: string;
45
+ readonly band: ScopeBand;
46
+ readonly path: string;
47
+ readonly state: McpTargetState;
48
+ }
49
+
50
+ /**
51
+ * The prior on-disk state of the shell-completion script, read up front by
52
+ * `detectCompletions`: `absent` (no script), `installed` (a byte-identical
53
+ * script is already present — a re-run skips it), or `stale` (a different
54
+ * script is present, so a write updates it).
55
+ */
56
+ export type CompletionsState = "absent" | "installed" | "stale";
57
+
58
+ /**
59
+ * The detected state of the Terrazzo LSP VS Code extension, probed up front by
60
+ * `detectLsp`: `installed` (already present — a re-run skips it), `absent` (not
61
+ * present, so the installer runs), or `unknown` (the `code` CLI is not on PATH,
62
+ * so we cannot enumerate and the installer runs regardless).
63
+ */
64
+ export type LspState = "installed" | "absent" | "unknown";
65
+
14
66
  /** One symlink create/skip/replace action performed during skill setup. */
15
67
  export interface SymlinkAction {
16
68
  readonly skillName: string;
@@ -35,8 +87,13 @@ export type SetupResult =
35
87
  readonly shell: ShellId | null;
36
88
  readonly path: string | null;
37
89
  readonly installed: boolean;
90
+ readonly state: CompletionsState;
91
+ }
92
+ | { readonly kind: "lsp"; readonly state: LspState }
93
+ | {
94
+ readonly kind: "mcp";
95
+ readonly configured: readonly string[];
96
+ readonly targets: readonly ConfiguredTarget[];
38
97
  }
39
- | { readonly kind: "lsp" }
40
- | { readonly kind: "mcp"; readonly configured: readonly string[] }
41
98
  | { readonly kind: "skills"; readonly result: SetupSkillsResult }
42
99
  | { readonly kind: "all"; readonly steps: readonly string[] };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * User-facing labels for the two config bands, shared by the setup recap
3
+ * ({@link ../setup/setup.render}) and the doctor report
4
+ * ({@link ../doctor/doctor.render}) so ONE consistent vocabulary appears across
5
+ * both. The pair matches the CLI grammar users already meet elsewhere: the
6
+ * `--global`/`--local` flags, the `--scope project|global|both` enum, config
7
+ * show's `[global]`/`[project]` origin markers, and the `setup` manifest's
8
+ * `[global]`/`[project]` lines. (Earlier these two surfaces said MACHINE/PROJECT
9
+ * — a third term for the same bands; this is the single source that unifies it.)
10
+ */
11
+
12
+ /** The user-facing display label for each of the two config bands. */
13
+ export const BAND_LABELS: Record<"project" | "global", string> = {
14
+ global: "Global",
15
+ project: "Project",
16
+ };