@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
@@ -61,6 +61,50 @@ describe("buildProgram — flags", () => {
61
61
  });
62
62
  });
63
63
 
64
+ describe("buildProgram — default-true boolean negation (B9)", () => {
65
+ const negatableVerb: VerbSpec = {
66
+ path: ["thing", "do"],
67
+ summary: "thing do summary",
68
+ params: [
69
+ { kind: "boolean", name: "ssr", doc: "Include SSR.", default: true },
70
+ {
71
+ kind: "boolean",
72
+ name: "runInstall",
73
+ doc: "Install now.",
74
+ default: false,
75
+ },
76
+ { kind: "boolean", name: "withHistory", doc: "Seed history." },
77
+ ],
78
+ output: {
79
+ formatters: {
80
+ plain: (d) => String(d),
81
+ llm: (d) => String(d),
82
+ json: (d) => JSON.stringify(d),
83
+ },
84
+ },
85
+ capability: { needsStore: false, mutates: false, mcp: { expose: true } },
86
+ run: async () => null,
87
+ };
88
+ const program = projectCli([{ name: "thing", verbs: [negatableVerb] }]);
89
+ const doCmd = program.commands
90
+ .find((c) => c.name() === "thing")
91
+ ?.commands.find((c) => c.name() === "do");
92
+
93
+ it("pairs a `--no-` negation onto a default-true boolean so it is disableable", () => {
94
+ const longs = doCmd?.options.map((o) => o.long) ?? [];
95
+ expect(longs).toContain("--ssr");
96
+ expect(longs).toContain("--no-ssr");
97
+ });
98
+
99
+ it("leaves default-false / defaultless booleans un-negated (no Commander default-flip)", () => {
100
+ const longs = doCmd?.options.map((o) => o.long) ?? [];
101
+ expect(longs).toContain("--run-install");
102
+ expect(longs).not.toContain("--no-run-install");
103
+ expect(longs).toContain("--with-history");
104
+ expect(longs).not.toContain("--no-with-history");
105
+ });
106
+ });
107
+
64
108
  describe("buildProgram — early exits", () => {
65
109
  it("throws commander.version for --version, writing the version", async () => {
66
110
  const program = projectCli([fixtureModule]);
@@ -334,9 +378,15 @@ describe("formatRootHelp — grouping", () => {
334
378
  makeVerb(["block", "list"]),
335
379
  ]);
336
380
 
337
- it("shows the task-oriented section titles for live nouns", () => {
338
- expect(help).toContain("Explore the design system");
381
+ it("leads with the uncurated nouns, untitled, under the usage line", () => {
382
+ // `block` is not in the kernel's curated table, so it leads the page with
383
+ // no heading of its own — the header already carries the blurb, and the
384
+ // kernel has no name for the domain. The kernel's own nouns keep their
385
+ // task-oriented sections.
386
+ expect(help).toMatch(/^Usage: pragma .*\n\n {2}block\b/m);
339
387
  expect(help).toContain("Set up & maintain");
388
+ // The blurb appears exactly once: in the header.
389
+ expect(help.match(/pragma test/g)).toHaveLength(1);
340
390
  });
341
391
 
342
392
  it("lists the live nouns and always-available mcp", () => {
@@ -353,7 +403,8 @@ describe("formatRootHelp — grouping", () => {
353
403
 
354
404
  it("shows the global flags block", () => {
355
405
  expect(help).toContain("Global flags");
356
- expect(help).toContain("--llm");
406
+ expect(help).not.toContain("--llm");
407
+ expect(help).toContain("--format <plain|llm|json>");
357
408
  expect(help).toContain("--detail <level>");
358
409
  });
359
410
 
@@ -363,8 +414,7 @@ describe("formatRootHelp — grouping", () => {
363
414
 
364
415
  Usage: pragma <command> [subcommand] [flags]
365
416
 
366
- Explore the design system
367
- block Inspect components & patterns and their anatomy
417
+ block block list summary
368
418
 
369
419
  Set up & maintain
370
420
  config Read and write pragma configuration
@@ -374,12 +424,11 @@ describe("formatRootHelp — grouping", () => {
374
424
  mcp Start the MCP server over stdio
375
425
 
376
426
  Global flags
377
- --llm Condensed Markdown output for agents
378
- --format <json|plain> Select output format
379
- --detail <level> Progressive-disclosure level (summary, standard, detailed)
380
- --verbose Diagnostic output on stderr
381
- --help Show help (works on any command)
382
- --version Show the CLI version
427
+ --format <plain|llm|json> Select output format (llm = condensed Markdown for agents)
428
+ --detail <level> Progressive-disclosure level (summary, standard, detailed)
429
+ --verbose Diagnostic output on stderr
430
+ --help Show help (works on any command)
431
+ --version Show the CLI version
383
432
 
384
433
  Run \`pragma <command> --help\` for details, or \`pragma capabilities\` to get oriented."
385
434
  `);
@@ -70,6 +70,17 @@ function registerParams(command: Command, verb: VerbSpec): void {
70
70
  } else {
71
71
  command.option(spec, param.doc);
72
72
  }
73
+ // B9: a default-`true` boolean is otherwise undisableable — the grammar has
74
+ // no negation form, so `create`'s sibling booleans (--ssr/--with-styles/…)
75
+ // could never be turned off. Register a paired `--no-<flag>` so they can:
76
+ // Commander keeps the `true` default and lets `--no-<flag>` set it false
77
+ // (verified: an unset default-false boolean is left alone). This is a CLI
78
+ // PARSE convenience, never a ParamSpec — `emitSurface`/MCP/the covenant are
79
+ // untouched — so the frozen flag list stands and negation stays additive.
80
+ if (param.kind === "boolean" && param.default === true) {
81
+ const flag = kebabCase(param.name);
82
+ command.option(`--no-${flag}`, `Disable --${flag} (on by default).`);
83
+ }
73
84
  }
74
85
 
75
86
  if (verb.capability.mutates) {
@@ -1,11 +1,18 @@
1
+ import { existsSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { fail, succeed } from "@canonical/task";
2
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { fixturePreviewModule } from "../../../testing/fixtures/fixtureCapability.js";
3
7
  import { PragmaError } from "../../error/PragmaError.js";
4
8
  import { bootRuntime } from "../../runtime/boot.js";
5
9
  import type { GlobalFlags } from "../../runtime/types.js";
6
10
  import type { ParamSpec, VerbSpec } from "../../spec/types.js";
7
11
  import { dispatch, executeVerb, extractParams } from "./dispatch.js";
8
12
 
13
+ /** The read-then-write mutation the honest-preview guards drive. */
14
+ const graft = fixturePreviewModule.verbs[0] as VerbSpec;
15
+
9
16
  const PLAIN: GlobalFlags = {
10
17
  llm: false,
11
18
  autoLlm: false,
@@ -233,6 +240,66 @@ describe("dispatch — errors", () => {
233
240
  });
234
241
  });
235
242
 
243
+ describe("--dry-run is honest (PRA-104)", () => {
244
+ const savedExit = process.exitCode;
245
+ afterEach(() => {
246
+ process.exitCode = savedExit;
247
+ });
248
+
249
+ /** Capture stderr for one dispatch. */
250
+ const dispatchCapturing = async (
251
+ positionals: readonly string[],
252
+ opts: Record<string, unknown>,
253
+ ): Promise<string> => {
254
+ const errs: string[] = [];
255
+ const spy = vi
256
+ .spyOn(process.stderr, "write")
257
+ .mockImplementation((chunk: string | Uint8Array) => {
258
+ errs.push(String(chunk));
259
+ return true;
260
+ });
261
+ process.exitCode = 0;
262
+ try {
263
+ await dispatch(graft, positionals, opts, PLAIN);
264
+ } finally {
265
+ spy.mockRestore();
266
+ }
267
+ return errs.join("");
268
+ };
269
+
270
+ it("exits NONZERO when the mutation's first read would fail for real", async () => {
271
+ // The structural false positive: this same call used to print a full plan
272
+ // and exit 0, while the run without --dry-run died on the missing file.
273
+ const missing = join(mkdtempSync(join(tmpdir(), "pragma-graft-")), "gone");
274
+ const out = await dispatchCapturing([missing], { dryRun: true });
275
+ expect(process.exitCode).toBe(1);
276
+ expect(out).toContain(missing);
277
+ expect(existsSync(`${missing}.grafted`)).toBe(false);
278
+ });
279
+
280
+ it("still plans, exits 0, and writes nothing when the read would succeed", async () => {
281
+ const dir = mkdtempSync(join(tmpdir(), "pragma-graft-"));
282
+ const source = join(dir, "present.txt");
283
+ writeFileSync(source, "content\n");
284
+ const outs: string[] = [];
285
+ const spy = vi
286
+ .spyOn(process.stdout, "write")
287
+ .mockImplementation((chunk: string | Uint8Array) => {
288
+ outs.push(String(chunk));
289
+ return true;
290
+ });
291
+ process.exitCode = 0;
292
+ try {
293
+ await dispatch(graft, [source], { dryRun: true }, PLAIN);
294
+ } finally {
295
+ spy.mockRestore();
296
+ }
297
+ expect(process.exitCode).toBe(0);
298
+ expect(outs.join("")).toContain(`Write file: ${source}.grafted`);
299
+ expect(readdirSync(dir)).toEqual(["present.txt"]);
300
+ });
301
+ });
302
+
236
303
  describe("executeVerb — interactivity gate (H3)", () => {
237
304
  const setTTY = (
238
305
  stream: NodeJS.ReadStream | NodeJS.WriteStream,
@@ -11,8 +11,8 @@
11
11
  * exit code.
12
12
  */
13
13
 
14
- import { describeEffect, dryRun, type Task } from "@canonical/task";
15
- import { runTask, runUndo } from "@canonical/task/node";
14
+ import { describeEffect, type Task } from "@canonical/task";
15
+ import { runPreview, runTask, runUndo } from "@canonical/task/node";
16
16
  import {
17
17
  asPragmaError,
18
18
  CANCELLED_MESSAGE,
@@ -221,8 +221,11 @@ export async function executeVerb(
221
221
  // Also hand it the interaction context so an interactive verb can pick its
222
222
  // prompt strategy. The verb's `run` sets `mutationRuntime.exec` (the runner
223
223
  // options) as its last act; the projector reads it back on the real-run
224
- // branch only — the dry-run/undo branches stay handler-free (they mock
225
- // prompts), so `--dry-run`/`--undo` are unchanged by this seam.
224
+ // branch AND on the dry-run branch, which takes `cwd` (so the preview reads
225
+ // the tree the run would write into) and `onEffectStart` (so the stamping
226
+ // transform runs, and planned byte counts match written ones). It never
227
+ // takes the prompt handler: a preview auto-answers prompts and so can never
228
+ // block on input. `--undo` stays handler-free and untouched by this seam.
226
229
  const controller = new AbortController();
227
230
  const interaction: InteractionRuntime = {
228
231
  // Gate on STDERR (H3): the Ink wizard renders to stderr and reads stdin,
@@ -252,14 +255,33 @@ export async function executeVerb(
252
255
  | Promise<Task<unknown>>,
253
256
  );
254
257
  if (mutation.dryRun) {
255
- // A plan is the effects a mutation WOULD apply — a `Prompt` is not one, so
256
- // the interactive confirm gate / answer prompts never clutter the preview.
257
- return renderPlan(
258
- flags,
259
- dryRun(task)
260
- .effects.filter((effect) => effect._tag !== "Prompt")
261
- .map(describeEffect),
262
- );
258
+ // The HONEST preview (PR7): reads hit the real filesystem, writes are
259
+ // recorded and never executed. A mutation whose real run would die on its
260
+ // first template read now fails HERE too, so `--dry-run` exits nonzero
261
+ // exactly when the run would — the plan is a prediction, not a wish.
262
+ // `exec.cwd` is the same write root the real run resolves paths against,
263
+ // and `exec.onEffectStart` carries summon's stamping transform, so the
264
+ // planned byte counts are the bytes the run would actually write.
265
+ const previewExec = mutationRuntime.exec ?? {};
266
+ try {
267
+ const { effects } = await runPreview(task, {
268
+ cwd: previewExec.cwd,
269
+ onEffectStart: previewExec.onEffectStart,
270
+ onLog: logToStderr,
271
+ });
272
+ // A plan is the effects a mutation WOULD apply — a `Prompt` is not one,
273
+ // so the interactive confirm gate / answer prompts never clutter it.
274
+ return renderPlan(
275
+ flags,
276
+ effects
277
+ .filter((effect) => effect._tag !== "Prompt")
278
+ .map(describeEffect),
279
+ );
280
+ } finally {
281
+ // A verb that mounted an interactive session before returning its Task
282
+ // must be torn down on this branch too, exactly as on the real run.
283
+ await previewExec.dispose?.();
284
+ }
263
285
  }
264
286
  if (mutation.undo) {
265
287
  const { undoCount } = await runUndo(task, { onLog: logToStderr });
@@ -9,10 +9,18 @@ const TTY = { isTty: true, noAutoLlm: false };
9
9
  const PIPE = { isTty: false, noAutoLlm: false };
10
10
 
11
11
  describe("parseGlobalFlags", () => {
12
- it("honours an explicit --llm", () => {
13
- expect(parseGlobalFlags(["--llm"], TTY)).toMatchObject({
12
+ it("honours an explicit --format llm (condensed even on a TTY)", () => {
13
+ expect(parseGlobalFlags(["--format", "llm"], TTY)).toMatchObject({
14
14
  llm: true,
15
15
  autoLlm: false,
16
+ format: "llm",
17
+ });
18
+ });
19
+
20
+ it("forces human output with --format plain down a pipe", () => {
21
+ expect(parseGlobalFlags(["--format", "plain"], PIPE)).toMatchObject({
22
+ llm: false,
23
+ autoLlm: false,
16
24
  format: "plain",
17
25
  });
18
26
  });
@@ -69,7 +77,7 @@ describe("stripGlobalFlags", () => {
69
77
  "list",
70
78
  ]);
71
79
  expect(
72
- stripGlobalFlags(["block", "--llm", "list", "--detail", "detailed"]),
80
+ stripGlobalFlags(["block", "--verbose", "list", "--detail", "detailed"]),
73
81
  ).toEqual(["block", "list"]);
74
82
  expect(stripGlobalFlags(["--format=json", "config", "show"])).toEqual([
75
83
  "config",
@@ -90,7 +98,7 @@ describe("readRawFormat", () => {
90
98
 
91
99
  it("reports a valueless --format as empty string", () => {
92
100
  expect(readRawFormat(["--format"])).toBe("");
93
- expect(readRawFormat(["--format", "--llm"])).toBe("");
101
+ expect(readRawFormat(["--format", "--verbose"])).toBe("");
94
102
  });
95
103
 
96
104
  it("returns undefined when --format is absent", () => {
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Global-flag pre-parsing, ahead of Commander.
3
3
  *
4
- * `--llm`, `--format`, `--verbose`, and `--detail` may appear anywhere on the
5
- * line, so they are scanned and stripped before Commander sees argv — otherwise
4
+ * `--format`, `--verbose`, and `--detail` may appear anywhere on the line, so
5
+ * they are scanned and stripped before Commander sees argv — otherwise
6
6
  * `enablePositionalOptions()` scoping would reject a flag placed after a verb.
7
7
  * Ported from the v1 `parseGlobalFlags`, with two v2 changes: a new `--detail`
8
8
  * flag and the `--format text` value renamed to `plain` (the kernel's
9
- * {@link OutputFormat}). Both the space (`--format json`) and equals
10
- * (`--format=json`) forms are recognized.
9
+ * {@link OutputFormat}). The dedicated `--llm` flag was folded into
10
+ * `--format llm`, leaving auto-detection as the sole implicit trigger. Both the
11
+ * space (`--format json`) and equals (`--format=json`) forms are recognized.
11
12
  */
12
13
 
13
14
  import { DETAIL_LEVELS, type DetailLevel } from "../../../constants.js";
@@ -86,10 +87,11 @@ function readDetail(argv: readonly string[]): DetailLevel | undefined {
86
87
  /**
87
88
  * Extract the global flags from raw argv (the user's arguments, argv[2:]).
88
89
  *
89
- * Auto-LLM: with no `--llm`/`--format` and a non-interactive stdout (piped or
90
+ * Auto-LLM: with no explicit `--format` and a non-interactive stdout (piped or
90
91
  * redirected — the shape an agent captures), `llm` defaults to true so agents
91
- * get condensed Markdown without a flag. `--llm`, `--format`, and
92
- * `PRAGMA_NO_AUTO_LLM` all override it.
92
+ * get condensed Markdown without a flag. Any explicit `--format` overrides it —
93
+ * `--format plain` forces human output down a pipe, `--format llm` forces the
94
+ * condensed form even on a TTY — as does `PRAGMA_NO_AUTO_LLM`.
93
95
  *
94
96
  * @param argv - The user's arguments (no `node`/script prefix).
95
97
  * @param env - Output environment probe (defaults to the real process).
@@ -100,15 +102,14 @@ export function parseGlobalFlags(
100
102
  argv: readonly string[],
101
103
  env: OutputEnvironment = readOutputEnvironment(),
102
104
  ): GlobalFlags {
103
- const explicitLlm = argv.includes("--llm");
104
105
  const rawFormat = readFlagValue(argv, "--format");
105
- const format = rawFormat === "json" ? "json" : "plain";
106
+ const format =
107
+ rawFormat === "json" ? "json" : rawFormat === "llm" ? "llm" : "plain";
106
108
  const formatRequested = rawFormat !== undefined;
107
- const autoLlm =
108
- !explicitLlm && !formatRequested && !env.isTty && !env.noAutoLlm;
109
+ const autoLlm = !formatRequested && !env.isTty && !env.noAutoLlm;
109
110
  const detail = readDetail(argv);
110
111
  return {
111
- llm: explicitLlm || autoLlm,
112
+ llm: format === "llm" || autoLlm,
112
113
  autoLlm,
113
114
  format,
114
115
  verbose: argv.includes("--verbose"),
@@ -130,10 +131,9 @@ export function stripGlobalFlags(argv: readonly string[]): string[] {
130
131
  const result: string[] = [];
131
132
  for (let i = 0; i < argv.length; i++) {
132
133
  const arg = argv[i];
133
- if (arg === "--llm" || arg === "--verbose") continue;
134
+ if (arg === "--verbose") continue;
134
135
  if (
135
136
  arg?.startsWith("--format=") ||
136
- arg?.startsWith("--llm=") ||
137
137
  arg?.startsWith("--verbose=") ||
138
138
  arg?.startsWith("--detail=")
139
139
  ) {
@@ -17,7 +17,7 @@ const INDENT = " ";
17
17
  /** Two-space gap between a term column and its description. */
18
18
  const GAP = " ";
19
19
 
20
- /** A bold section header (`Flags`, `Verbs`, `Explore the design system`). */
20
+ /** A bold section header (`Flags`, `Verbs`, `Set up & maintain`). */
21
21
  export function helpHeading(text: string): string {
22
22
  return chalk.bold(text);
23
23
  }