@canonical/pragma-cli 0.33.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 (250) hide show
  1. package/README.md +7 -7
  2. package/dist/pragma +0 -0
  3. package/package.json +18 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +13 -4
  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.verb.ts +31 -13
  9. package/src/capabilities/capabilities/catalog.ts +18 -10
  10. package/src/capabilities/capabilities/hints.ts +13 -19
  11. package/src/capabilities/colophon/collectColophon.ts +34 -19
  12. package/src/capabilities/colophon/colophon.verb.ts +8 -4
  13. package/src/capabilities/colophon/types.ts +16 -7
  14. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  15. package/src/capabilities/config/collectConfigShow.ts +41 -4
  16. package/src/capabilities/config/fields.ts +0 -27
  17. package/src/capabilities/config/set.verb.ts +7 -3
  18. package/src/capabilities/config/show.render.test.ts +77 -5
  19. package/src/capabilities/config/show.render.ts +7 -12
  20. package/src/capabilities/config/show.verb.ts +4 -3
  21. package/src/capabilities/config/types.ts +8 -0
  22. package/src/capabilities/create/byteEquality.test.ts +78 -100
  23. package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
  24. package/src/capabilities/create/constants.ts +75 -0
  25. package/src/capabilities/create/create.mcp.test.ts +2 -2
  26. package/src/capabilities/create/create.test.ts +162 -15
  27. package/src/capabilities/create/create.verb.ts +140 -69
  28. package/src/capabilities/create/index.ts +2 -10
  29. package/src/capabilities/create/pickGenerator.ts +34 -28
  30. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  31. package/src/capabilities/create/types.ts +10 -6
  32. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  33. package/src/capabilities/distribution.test.ts +120 -0
  34. package/src/capabilities/distribution.ts +59 -0
  35. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  36. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  37. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
  38. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  39. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  40. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  41. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  42. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  43. package/src/capabilities/doctor/doctor.render.test.ts +6 -6
  44. package/src/capabilities/doctor/doctor.render.ts +2 -1
  45. package/src/capabilities/doctor/doctor.test.ts +37 -8
  46. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  47. package/src/capabilities/doctor/runChecks.ts +5 -5
  48. package/src/capabilities/doctor/types.ts +1 -1
  49. package/src/capabilities/emptyRecovery.test.ts +32 -15
  50. package/src/capabilities/graph/inspect.verb.ts +3 -2
  51. package/src/capabilities/graph/query.verb.ts +3 -2
  52. package/src/capabilities/graph/runQuery.ts +8 -5
  53. package/src/capabilities/index.ts +22 -18
  54. package/src/capabilities/info/collectInfo.ts +10 -6
  55. package/src/capabilities/info/info.render.ts +3 -2
  56. package/src/capabilities/info/info.verb.ts +3 -2
  57. package/src/capabilities/lazy.test.ts +93 -2
  58. package/src/capabilities/ontology/verbs.ts +21 -21
  59. package/src/capabilities/prompt/prompt.test.ts +12 -6
  60. package/src/capabilities/prompt/verbs.ts +24 -11
  61. package/src/capabilities/resources/provider.ts +30 -11
  62. package/src/capabilities/resources/resources.test.ts +85 -29
  63. package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
  64. package/src/capabilities/setup/operations/setupMcp.ts +2 -2
  65. package/src/capabilities/setup/operations/setupSkills.ts +3 -3
  66. package/src/capabilities/setup/setup.verb.ts +12 -5
  67. package/src/capabilities/setup/shell.ts +29 -3
  68. package/src/capabilities/skill/discover.ts +11 -5
  69. package/src/capabilities/skill/render.ts +4 -5
  70. package/src/capabilities/skill/verbs.ts +7 -7
  71. package/src/capabilities/sources/collectStatus.ts +55 -50
  72. package/src/capabilities/sources/index.ts +1 -1
  73. package/src/capabilities/sources/installSkills.ts +2 -2
  74. package/src/capabilities/sources/runUpdate.ts +113 -122
  75. package/src/capabilities/sources/sources.test.ts +111 -117
  76. package/src/capabilities/sources/status.render.test.ts +53 -24
  77. package/src/capabilities/sources/status.render.ts +24 -29
  78. package/src/capabilities/sources/status.verb.ts +15 -8
  79. package/src/capabilities/sources/types.ts +18 -25
  80. package/src/capabilities/sources/update.render.ts +9 -2
  81. package/src/capabilities/sources/update.verb.ts +12 -23
  82. package/src/capabilities/surface.test.ts +33 -10
  83. package/src/capabilities/tier.parity.test.ts +144 -0
  84. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  85. package/src/constants.test.ts +8 -6
  86. package/src/constants.ts +47 -21
  87. package/src/identity.test.ts +333 -0
  88. package/src/index.ts +2 -2
  89. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
  90. package/src/kernel/completion/emitScripts.test.ts +1 -1
  91. package/src/kernel/completion/emitScripts.ts +4 -2
  92. package/src/kernel/completion/entitySource.test.ts +185 -41
  93. package/src/kernel/completion/entitySource.ts +117 -107
  94. package/src/kernel/completion/model.test.ts +1 -1
  95. package/src/kernel/completion/model.ts +11 -0
  96. package/src/kernel/completion/safety.test.ts +82 -36
  97. package/src/kernel/completion/shellDrive.test.ts +592 -0
  98. package/src/kernel/completion/templates/fish.ts +28 -6
  99. package/src/kernel/completion/types.ts +3 -3
  100. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  101. package/src/kernel/config/defaults.test.ts +85 -0
  102. package/src/kernel/config/defaults.ts +12 -23
  103. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  104. package/src/kernel/config/findProjectConfig.ts +4 -3
  105. package/src/kernel/config/firstRun.ts +12 -5
  106. package/src/kernel/config/globalConfig.ts +2 -2
  107. package/src/kernel/config/index.ts +4 -5
  108. package/src/kernel/config/paths.ts +17 -17
  109. package/src/kernel/config/readConfig.test.ts +242 -6
  110. package/src/kernel/config/readConfig.ts +8 -8
  111. package/src/kernel/config/schema.test.ts +50 -0
  112. package/src/kernel/config/schema.ts +97 -10
  113. package/src/kernel/config/types.ts +73 -19
  114. package/src/kernel/copy.test.ts +453 -0
  115. package/src/kernel/error/PragmaError.test.ts +1 -1
  116. package/src/kernel/error/constants.ts +3 -2
  117. package/src/kernel/error/index.ts +1 -1
  118. package/src/kernel/error/recovery.test.ts +22 -45
  119. package/src/kernel/error/recovery.ts +22 -27
  120. package/src/kernel/error/types.ts +5 -4
  121. package/src/kernel/packs/collect.test.ts +195 -11
  122. package/src/kernel/packs/collect.ts +213 -58
  123. package/src/kernel/packs/compile.test.ts +163 -7
  124. package/src/kernel/packs/compile.ts +15 -12
  125. package/src/kernel/packs/disclosure.test.ts +5 -2
  126. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  127. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  128. package/src/kernel/packs/iri.ts +5 -7
  129. package/src/kernel/packs/renderPack.ts +5 -3
  130. package/src/kernel/packs/resolveEntity.ts +21 -17
  131. package/src/kernel/packs/runBodies.ts +15 -14
  132. package/src/kernel/packs/schema.ts +96 -10
  133. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  134. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  135. package/src/kernel/packs/toolDescription.test.ts +10 -2
  136. package/src/kernel/packs/types.ts +39 -4
  137. package/src/kernel/packs/uniqueness.ts +4 -4
  138. package/src/kernel/project/cli/buildProgram.test.ts +9 -4
  139. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  140. package/src/kernel/project/cli/dispatch.ts +34 -12
  141. package/src/kernel/project/cli/helpFormat.ts +1 -1
  142. package/src/kernel/project/cli/rootHelp.ts +105 -105
  143. package/src/kernel/project/mcp/buildServer.ts +11 -6
  144. package/src/kernel/project/mcp/instructions.ts +11 -6
  145. package/src/kernel/project/mcp/parity.test.ts +39 -1
  146. package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
  147. package/src/kernel/project/mcp/prompts/provider.ts +22 -8
  148. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  149. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  150. package/src/kernel/project/mcp/serve.ts +11 -3
  151. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  152. package/src/kernel/render/disclosure.ts +4 -2
  153. package/src/kernel/render/prefixes.test.ts +36 -0
  154. package/src/kernel/render/prefixes.ts +40 -13
  155. package/src/kernel/render/renderers.test.ts +1 -1
  156. package/src/kernel/runtime/graphpack/build.ts +29 -3
  157. package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
  158. package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
  159. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
  160. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
  161. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  162. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  163. package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
  164. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  165. package/src/kernel/runtime/graphpack/index.ts +1 -1
  166. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  167. package/src/kernel/runtime/graphpack/read.ts +4 -9
  168. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  169. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  170. package/src/kernel/runtime/graphpack/types.ts +61 -48
  171. package/src/kernel/runtime/paths.ts +64 -15
  172. package/src/kernel/runtime/probe.test.ts +8 -4
  173. package/src/kernel/runtime/readEntity.ts +6 -5
  174. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  175. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  176. package/src/kernel/runtime/refs/resolve.ts +78 -62
  177. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  178. package/src/kernel/runtime/resolveSources.ts +36 -25
  179. package/src/kernel/runtime/storeReadiness.ts +9 -8
  180. package/src/kernel/runtime/storeless.test.ts +1 -1
  181. package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
  182. package/src/kernel/spec/emitReference.test.ts +3 -3
  183. package/src/kernel/spec/emitReference.ts +144 -11
  184. package/src/kernel/spec/emitSurface.ts +9 -5
  185. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  186. package/src/kernel/spec/surfaceConformance.ts +8 -0
  187. package/src/kernel/spec/types.ts +29 -9
  188. package/src/kernel/spec/validate.ts +2 -1
  189. package/src/kernel/vocabulary.test.ts +164 -0
  190. package/src/kernel/vocabulary.ts +146 -0
  191. package/src/testing/PARITY_GAPS.ts +8 -2
  192. package/src/testing/behavioral/README.md +3 -3
  193. package/src/testing/behavioral/completion.test.ts +19 -6
  194. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  195. package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
  196. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  197. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  198. package/src/testing/behavioral/journeys.cli.test.ts +116 -75
  199. package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
  200. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  201. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
  202. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  203. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  204. package/src/testing/behavioral/mcpSurface.test.ts +9 -4
  205. package/src/testing/behavioral/staleCommands.test.ts +10 -0
  206. package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
  207. package/src/testing/eval/cases/assembly.ts +13 -6
  208. package/src/testing/eval/cases/effectful.ts +25 -13
  209. package/src/testing/eval/cases/readNouns.ts +22 -20
  210. package/src/testing/eval/cases/stable.ts +5 -1
  211. package/src/testing/eval/eval.test.ts +7 -1
  212. package/src/testing/fixtures/fixtureCapability.ts +45 -1
  213. package/src/testing/fixtures/graph/canonical.ts +6 -3
  214. package/src/testing/fixtures/graph/defaultPack.ts +6 -5
  215. package/src/testing/fixtures/graph/owlExport.ts +7 -5
  216. package/src/testing/helpers/fixtureGraph.ts +33 -16
  217. package/src/testing/helpers/packRuntime.ts +2 -3
  218. package/src/testing/helpers/projectCli.ts +2 -1
  219. package/src/testing/helpers/projectMcp.ts +6 -0
  220. package/src/testing/perf/budgets.test.ts +4 -0
  221. package/src/testing/perf/budgets.ts +22 -7
  222. package/src/testing/perf/globalSetup.ts +121 -6
  223. package/src/types.ts +2 -2
  224. package/src/capabilities/block/blockList.test.ts +0 -79
  225. package/src/capabilities/block/blockList.verb.ts +0 -163
  226. package/src/capabilities/block/index.ts +0 -25
  227. package/src/capabilities/block/pack.ts +0 -166
  228. package/src/capabilities/block/parity.test.ts +0 -187
  229. package/src/capabilities/block/tierChain.ts +0 -78
  230. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  231. package/src/capabilities/modifier/index.ts +0 -15
  232. package/src/capabilities/modifier/pack.ts +0 -64
  233. package/src/capabilities/standard/index.ts +0 -18
  234. package/src/capabilities/standard/pack.ts +0 -128
  235. package/src/capabilities/tier/index.ts +0 -19
  236. package/src/capabilities/tier/lookup.render.test.ts +0 -72
  237. package/src/capabilities/tier/lookup.render.ts +0 -59
  238. package/src/capabilities/tier/lookup.test.ts +0 -83
  239. package/src/capabilities/tier/lookup.verb.ts +0 -44
  240. package/src/capabilities/tier/pack.ts +0 -28
  241. package/src/capabilities/tier/runLookup.ts +0 -87
  242. package/src/capabilities/token/addConfig.render.ts +0 -27
  243. package/src/capabilities/token/addConfig.test.ts +0 -62
  244. package/src/capabilities/token/addConfig.verb.ts +0 -40
  245. package/src/capabilities/token/index.ts +0 -19
  246. package/src/capabilities/token/pack.ts +0 -58
  247. package/src/capabilities/token/runAddConfig.ts +0 -95
  248. package/src/kernel/completion/bashDrive.test.ts +0 -73
  249. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  250. package/src/kernel/runtime/lock.ts +0 -90
@@ -25,14 +25,14 @@ import {
25
25
  import { bootRuntime } from "../../kernel/runtime/boot.js";
26
26
  import type { GlobalFlags } from "../../kernel/runtime/types.js";
27
27
  import type { ParamSpec } from "../../kernel/spec/types.js";
28
+ import { CREATE_GENERATORS } from "./constants.js";
28
29
  import {
29
- createApplicationVerb,
30
- createComponentVerb,
31
- createPackageVerb,
30
+ createVerbs,
32
31
  INCLUDE_FLAG_ALIASES,
33
32
  isModuleNotFound,
34
33
  } from "./create.verb.js";
35
34
  import { generatorToParams } from "./generatorToVerbSpec.js";
35
+ import { createModule } from "./index.js";
36
36
 
37
37
  const FLAGS: GlobalFlags = {
38
38
  llm: false,
@@ -53,7 +53,7 @@ async function runIn(
53
53
  process.chdir(dir);
54
54
  try {
55
55
  return await executeVerb(
56
- createComponentVerb,
56
+ createVerbs.component,
57
57
  params,
58
58
  mutation,
59
59
  bootRuntime(FLAGS, dir),
@@ -86,7 +86,11 @@ describe("create — real generation + stamp", () => {
86
86
  expect(readFileSync(main, "utf-8").split("\n")[0]).toBe(
87
87
  "// Generated by @canonical/summon-component:react v0.1.0",
88
88
  );
89
- });
89
+ // Explicit timeout, like the package's other real-run gates: this drives a
90
+ // full generator run (~2.4s idle), and under a contended CI matrix the 5s
91
+ // vitest default fails it on load, not on a defect — both build-matrix
92
+ // legs timed out at 5000ms on a run whose import phase alone took 88s.
93
+ }, 60_000);
90
94
 
91
95
  it("--dry-run writes nothing and previews effects (no Prompt lines)", async () => {
92
96
  const dir = freshCwd();
@@ -101,6 +105,45 @@ describe("create — real generation + stamp", () => {
101
105
  expect(walk(dir)).toEqual([]); // nothing written
102
106
  });
103
107
 
108
+ it("--dry-run reports the bytes the real run writes (post-stamp)", async () => {
109
+ // The mock previewer never called `onEffectStart`, so summon's stamping
110
+ // transform did not run and every planned byte count under-reported the
111
+ // written file by the stamp's length. The honest previewer runs it.
112
+ const params = {
113
+ framework: "react",
114
+ componentPath: "src/components/Widget",
115
+ withStyles: true,
116
+ withStories: false,
117
+ withSsrTests: false,
118
+ };
119
+ const previewDir = freshCwd();
120
+ const preview = await runIn(previewDir, params, {
121
+ dryRun: true,
122
+ undo: false,
123
+ yes: false,
124
+ });
125
+ expect(preview.exitCode).toBe(0);
126
+ expect(walk(previewDir)).toEqual([]);
127
+
128
+ const planned = new Map<string, number>();
129
+ for (const line of (preview.stdout ?? "").split("\n")) {
130
+ const match = /^ {2}- Write file: (.+) \((\d+) bytes\)$/.exec(line);
131
+ if (match) planned.set(match[1] as string, Number(match[2]));
132
+ }
133
+ expect(planned.size).toBeGreaterThan(0);
134
+
135
+ const realDir = freshCwd();
136
+ expect((await runIn(realDir, params)).exitCode).toBe(0);
137
+ for (const [relative, bytes] of planned) {
138
+ expect({
139
+ file: relative,
140
+ bytes: readFileSync(join(realDir, relative), "utf-8").length,
141
+ }).toEqual({ file: relative, bytes });
142
+ }
143
+ // Same set of files, not just the same counts for the ones checked.
144
+ expect(walk(realDir).sort()).toEqual([...planned.keys()].sort());
145
+ });
146
+
104
147
  it("--undo reverses a run", async () => {
105
148
  const dir = freshCwd();
106
149
  await runIn(dir, {
@@ -123,7 +166,9 @@ describe("create — real generation + stamp", () => {
123
166
  );
124
167
  expect(outcome.exitCode).toBe(0);
125
168
  expect(outcome.stdout).toMatch(/Undid \d+ step/);
126
- });
169
+ // TWO real runs (generate, then undo) — the same load-sensitivity as the
170
+ // stamp test above, doubled; same explicit ceiling.
171
+ }, 60_000);
127
172
 
128
173
  it("rejects an unknown framework with INVALID_INPUT", async () => {
129
174
  const dir = freshCwd();
@@ -131,7 +176,7 @@ describe("create — real generation + stamp", () => {
131
176
  // framework param to exercise pickGenerator's own guard too.
132
177
  const runtime = bootRuntime(FLAGS, dir);
133
178
  await expect(
134
- createComponentVerb.run({ framework: "vue" }, {
179
+ createVerbs.component.run({ framework: "vue" }, {
135
180
  ...runtime,
136
181
  interaction: { isTTY: false, transport: "cli", yes: true },
137
182
  } as never),
@@ -217,9 +262,19 @@ describe("lazy-React discipline (PROTECTED)", () => {
217
262
  "@canonical/summon-component",
218
263
  "@canonical/summon-package",
219
264
  "@canonical/summon-application",
265
+ // The generators pull summon-core, and summon-core is what costs the
266
+ // fast paths (+43 MB RSS once it loads). Dropping the `type` from one of
267
+ // the four `import type … from "@canonical/summon-core"` lines in this
268
+ // directory is a one-word regression nothing else catches.
269
+ "@canonical/summon-core",
220
270
  ];
221
271
  for (const file of graph) {
222
- const src = readFileSync(file, "utf-8");
272
+ // Type-only imports are erased before runtime, so they load nothing —
273
+ // strip them before asking what the file actually pulls in.
274
+ const src = readFileSync(file, "utf-8").replace(
275
+ /\bimport\s+type\b[^;]*;/g,
276
+ "",
277
+ );
223
278
  for (const pkg of forbidden) {
224
279
  const esc = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
225
280
  expect(
@@ -285,7 +340,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
285
340
  (generators as Record<string, { prompts: unknown[] }>)["component/react"]
286
341
  .prompts as never,
287
342
  );
288
- const verb = createComponentVerb.params;
343
+ const verb = createVerbs.component.params;
289
344
 
290
345
  // The verb SYNTHESIZES a `framework` enum (the 3 framework generators
291
346
  // collapse to one verb): react|svelte|lit, default react — no generator
@@ -328,7 +383,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
328
383
  .prompts as never,
329
384
  );
330
385
  // The package verb's grammar IS generatorToParams(PACKAGE_MIRROR).
331
- expect(behavioural(createPackageVerb.params)).toEqual(
386
+ expect(behavioural(createVerbs.package.params)).toEqual(
332
387
  behavioural(applyRunInstallOverride(real)),
333
388
  );
334
389
 
@@ -336,7 +391,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
336
391
  const realRunInstall = real.find((p) => p.name === "runInstall") as
337
392
  | Record<string, unknown>
338
393
  | undefined;
339
- const mirrorRunInstall = createPackageVerb.params.find(
394
+ const mirrorRunInstall = createVerbs.package.params.find(
340
395
  (p) => p.name === "runInstall",
341
396
  ) as Record<string, unknown> | undefined;
342
397
  expect(realRunInstall?.default).toBe(true);
@@ -375,14 +430,14 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
375
430
  // generator's `ssr`/`router`/`forms`.
376
431
  expect(
377
432
  behavioural(
378
- toGeneratorNames("application", createApplicationVerb.params),
433
+ toGeneratorNames("application", createVerbs.application.params),
379
434
  ),
380
435
  ).toEqual(behavioural(applyRunInstallOverride(real)));
381
436
 
382
437
  // Guard the B8 rename itself: ALL application include-flags ARE on the
383
438
  // `--with-X` convention (params `withSsr`/`withRouter`/`withForms`/
384
439
  // `withRelay`), not the bare `ssr`/`router`/`forms`/`relay` booleans.
385
- const names = createApplicationVerb.params.map((p) => p.name);
440
+ const names = createVerbs.application.params.map((p) => p.name);
386
441
  expect(names).toContain("withSsr");
387
442
  expect(names).toContain("withRouter");
388
443
  expect(names).toContain("withForms");
@@ -393,7 +448,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
393
448
  const realRunInstall = real.find((p) => p.name === "runInstall") as
394
449
  | Record<string, unknown>
395
450
  | undefined;
396
- const mirrorRunInstall = createApplicationVerb.params.find(
451
+ const mirrorRunInstall = createVerbs.application.params.find(
397
452
  (p) => p.name === "runInstall",
398
453
  ) as Record<string, unknown> | undefined;
399
454
  expect(realRunInstall?.default).toBe(true);
@@ -401,7 +456,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
401
456
 
402
457
  // appPath is the positional entry point (its default carries across).
403
458
  expect(
404
- createApplicationVerb.params.find((p) => p.name === "appPath"),
459
+ createVerbs.application.params.find((p) => p.name === "appPath"),
405
460
  ).toMatchObject({ kind: "string", positional: true, default: "my-app" });
406
461
  });
407
462
  });
@@ -430,3 +485,95 @@ describe("compiled-binary create gate (M4)", () => {
430
485
  expect(isModuleNotFound("nope")).toBe(false);
431
486
  });
432
487
  });
488
+
489
+ describe("declared generator bindings (PROTECTED)", () => {
490
+ // CREATE_GENERATORS is the one place the create surface's generator facts are
491
+ // written down. What is checkable against something outside itself — the live
492
+ // generator maps and the embedded manifest — is checked here.
493
+
494
+ it("every binding resolves to the generator it names", async () => {
495
+ const { pickGenerator } = await import("./pickGenerator.js");
496
+ // `meta.name` is the generator's OWN identity, so a swapped `key` or a
497
+ // framework the map does not carry turns this red — which `toBeDefined()`
498
+ // on a non-nullable return could not.
499
+ for (const framework of CREATE_GENERATORS.component.frameworks) {
500
+ expect(pickGenerator("component", { framework }).meta.name).toBe(
501
+ `component/${framework}`,
502
+ );
503
+ }
504
+ expect(pickGenerator("package", {}).meta.name).toBe("package");
505
+ expect(pickGenerator("application", {}).meta.name).toBe(
506
+ "application/react",
507
+ );
508
+ });
509
+
510
+ it("exactly the two source-run-only verbs publish the availability caveat", () => {
511
+ // A LITERAL list, for the same reason `compiledCreate.subprocess.test.ts`
512
+ // uses one: the caveat is DERIVED from `readsEmbeddedTemplates`, so an
513
+ // assertion derived from the same bit would agree with itself no matter
514
+ // which way the bit was flipped. These two are the nouns that test proves
515
+ // refuse on a real compiled binary, so this is what the reference is held
516
+ // to — and flipping the bit turns this red, along with the reference
517
+ // drift-guard, in the same run.
518
+ const caveated = createModule.verbs
519
+ .filter((verb) => verb.summary.includes("Source-run only."))
520
+ .map((verb) => verb.path.at(1));
521
+ expect(caveated).toEqual(["package", "application"]);
522
+ for (const verb of createModule.verbs) {
523
+ const kind = verb.path.at(1);
524
+ const gated = caveated.includes(kind);
525
+ // The `doc` is what `tools.md` and the MCP tool description render, so
526
+ // the explanation has to travel with the marker, and it quotes the
527
+ // command it is about — a copy-paste between bindings is then visible.
528
+ expect(verb.doc?.includes("UNSUPPORTED") ?? false, kind).toBe(gated);
529
+ if (gated) {
530
+ expect(verb.doc).toContain(`create ${kind}`);
531
+ // And it still says what the verb MAKES. `doc ?? summary` means a doc
532
+ // REPLACES the summary on the MCP surface, so a caveat alone left an
533
+ // agent no statement anywhere of what `create_package` scaffolds.
534
+ expect(verb.doc).toContain(
535
+ verb.summary.replace(" Source-run only.", ""),
536
+ );
537
+ }
538
+ }
539
+ });
540
+
541
+ it("the --framework enum offers the declared frameworks and defaults to one of them", () => {
542
+ const framework = createVerbs.component.params.find(
543
+ (p) => p.name === "framework",
544
+ );
545
+ // A `values: []` enum with a `default` would leave every `--framework`
546
+ // rejected while `--help` still advertises a default — the hazard this names.
547
+ // (That every value resolves is the test above.)
548
+ expect(framework?.kind).toBe("enum");
549
+ const values = (framework as { values: readonly string[] }).values;
550
+ expect(values.length).toBeGreaterThan(0);
551
+ expect(values).toContain(framework?.default);
552
+ });
553
+
554
+ it("embeds only the templates the component loader can key", async () => {
555
+ const { TEMPLATES } = await import("./templates.embedded.generated.js");
556
+ const keys = Object.keys(TEMPLATES);
557
+ // summon-component's `qualifiedKey()` prefixes EVERY lookup with
558
+ // `component/`, so any other entry ships dead weight the binary can never
559
+ // read — and a binding embedded without a manifest-reading generator would
560
+ // put one here. Non-empty first: `.every()` over an empty list is vacuous.
561
+ expect(keys.length).toBeGreaterThan(0);
562
+ expect(keys.filter((k) => !k.startsWith("component/"))).toEqual([]);
563
+ });
564
+
565
+ it("create surfaces exactly the three declared nouns", () => {
566
+ // A LITERAL surface pin, deliberately not derived from CREATE_GENERATORS:
567
+ // surfacing a noun also needs a hand-written prompt mirror, path param and
568
+ // examples in create.verb.ts, so the surface is a deliberate SUBSET of what
569
+ // the declared packages ship (`@canonical/summon-application` also ships
570
+ // `domain`, `route` and `wrapper`). Pinning OUR three nouns rather than any
571
+ // third-party package's generator list keeps an upstream release from
572
+ // turning this red.
573
+ expect(createModule.verbs.map((v) => v.path[1])).toEqual([
574
+ "component",
575
+ "package",
576
+ "application",
577
+ ]);
578
+ });
579
+ });
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * The `create` verbs — `component` / `package` / `application` (path capped at
3
- * two segments; the three component frameworks collapse to one verb + a
3
+ * two segments; the declared component frameworks collapse to one verb + a
4
4
  * `--framework` enum). Each is a mutating, interactive, storeless verb.
5
5
  *
6
6
  * LAZY DISPATCH (R9 + lazy-React): the params are STATIC — built by the
7
7
  * generator→grammar adapter over static prompt MIRRORS, never the live
8
- * generators (importing a generator runs a top-level `await loadTemplate`). The
8
+ * generators (importing a generator pulls summon-core, and with it React). The
9
9
  * `run` body lazily `import()`s `pickGenerator` + summon-core, so `buildProgram`
10
10
  * / `--help` / `__complete` / reads never load summon-core or the generators —
11
11
  * and `create --yes` never loads React (the Ink UI is dynamic-only, and only the
@@ -15,9 +15,11 @@
15
15
 
16
16
  import type { GeneratorResult, PromptDefinition } from "@canonical/summon-core";
17
17
  import type { Task } from "@canonical/task";
18
+ import { BIN_NAME } from "../../constants.js";
18
19
  import { PragmaError } from "../../kernel/error/PragmaError.js";
19
20
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
20
21
  import type { ParamSpec, VerbSpec } from "../../kernel/spec/types.js";
22
+ import { CREATE_GENERATORS } from "./constants.js";
21
23
  import { createFormatters } from "./create.render.js";
22
24
  import { generatorToParams } from "./generatorToVerbSpec.js";
23
25
  import { assertInsideWorkspace } from "./pathJail.js";
@@ -27,13 +29,14 @@ import type { CreateKind } from "./types.js";
27
29
  // Static params (mirrors of the generators' prompts — see the module doc)
28
30
  // =============================================================================
29
31
 
30
- /** `--framework` — the three component generators collapsed to one enum. */
32
+ /** `--framework` — the declared component generators collapsed to one enum. */
31
33
  const FRAMEWORK_PARAM: ParamSpec = {
32
34
  kind: "enum",
33
35
  name: "framework",
34
36
  doc: "Component framework.",
35
- values: ["react", "svelte", "lit"],
36
- default: "react",
37
+ values: CREATE_GENERATORS.component.frameworks,
38
+ // The FIRST declared framework, so the default is always inside the enum.
39
+ default: CREATE_GENERATORS.component.frameworks[0],
37
40
  };
38
41
 
39
42
  /**
@@ -234,20 +237,16 @@ const PATH_PARAM: Record<CreateKind, string | undefined> = {
234
237
  // The lazy run — the one-line summon↔pragma seam per invocation
235
238
  // =============================================================================
236
239
 
237
- /**
238
- * Build the `create` Task for one invocation: pick the generator, jail its
239
- * output path, pre-validate flag/arg answers, pick the prompt strategy against
240
- * the interaction context, wire `runtime.exec`, and return `execute`.
241
- *
242
- * @returns A `Promise<Task<GeneratorResult>>` (the union's third arm) the
243
- * dispatcher/MCP handler awaits into a Task before interpreting.
244
- */
245
240
  /**
246
241
  * The standalone `bun build --compile` binary resolves every bundled module
247
242
  * under the virtual `/$bunfs` filesystem — a marker absent from any source or
248
- * `bun`-run module URL. `create component` is fully embedded and runs from the
249
- * binary; `create package` / `create application` are NOT (application also
250
- * copies non-`.ejs` assets), so they are gated to a source run below.
243
+ * `bun`-run module URL. Only a generator that routes its template reads through
244
+ * the embedded manifest can run there
245
+ * (`CREATE_GENERATORS[kind].readsEmbeddedTemplates`);
246
+ * the others call `template({ source })`, which falls through to
247
+ * `readFile(source)` and dies with `ENOENT … /$bunfs/…` after `mkdir` has
248
+ * already run, leaving partially-created files behind.
249
+ * `compiledCreate.subprocess.test.ts` pins the refusal AND the empty cwd.
251
250
  */
252
251
  const IS_COMPILED_BINARY = import.meta.url.includes("/$bunfs/");
253
252
 
@@ -281,23 +280,22 @@ export function isModuleNotFound(cause: unknown): boolean {
281
280
  * `--compile` bundler includes them — they stay behind this lazy boundary, so
282
281
  * the fast paths and `create --yes` still load neither summon-core nor React).
283
282
  *
284
- * The manifest is injected BEFORE `pickGenerator` — importing the generator
285
- * packages runs a top-level `await loadTemplate`, and the component loader
286
- * consults the manifest when its disk read fails (the compiled binary). It is
287
- * imported from summon-component's `loadTemplate` submodule so this does not
288
- * evaluate the generator index first. In a source run the disk read wins and the
289
- * manifest is inert.
283
+ * The manifest is injected BEFORE `pickGenerator` — the component loader
284
+ * consults it when its disk read fails (the compiled binary), and the generators
285
+ * load their templates on first `generate()`. It is imported from
286
+ * summon-component's `loadTemplate` submodule so this does not evaluate the
287
+ * generator index first. In a source run the disk read wins and the manifest is
288
+ * inert.
290
289
  *
291
- * `create package` / `create application` are not yet embedded for the compiled
292
- * binary (application also copies non-`.ejs` assets), so they are gated to a
293
- * source run here. A stale resolution failure is a defensive backstop
294
- * {@link isModuleNotFound} turns into the same clean gate.
290
+ * A binding whose generator does not read through that manifest is gated to a
291
+ * source run here ({@link IS_COMPILED_BINARY}). A stale resolution failure is a
292
+ * defensive backstop {@link isModuleNotFound} turns into the same clean gate.
295
293
  */
296
294
  async function loadCreateRuntime(kind: CreateKind) {
297
- if (IS_COMPILED_BINARY && kind !== "component") {
295
+ if (IS_COMPILED_BINARY && !CREATE_GENERATORS[kind].readsEmbeddedTemplates) {
298
296
  throw new PragmaError({
299
297
  code: "UNSUPPORTED",
300
- message: `\`create ${kind}\` is not available in the compiled pragma binary yet — only \`create component\` is embedded. Run it from a source checkout, or use the \`summon\` CLI.`,
298
+ message: `\`create ${kind}\` is not available in the compiled ${BIN_NAME} binary — ${SOURCE_ONLY_REASON}. Run it from a source checkout, or use the \`summon\` CLI.`,
301
299
  recovery: {
302
300
  message: `Run \`create ${kind}\` from a source checkout, or use \`summon\`.`,
303
301
  },
@@ -331,15 +329,26 @@ async function loadCreateRuntime(kind: CreateKind) {
331
329
  }
332
330
  }
333
331
 
332
+ /**
333
+ * Build the `create` Task for one invocation: pick the generator, jail its
334
+ * output path, pre-validate flag/arg answers, pick the prompt strategy against
335
+ * the interaction context, wire `runtime.exec`, and return `execute`.
336
+ *
337
+ * @param kind - The create noun.
338
+ * @param params - The coerced params for this invocation.
339
+ * @param rt - The pragma runtime.
340
+ * @returns A `Promise<Task<GeneratorResult>>` (the union's third arm) the
341
+ * dispatcher/MCP handler awaits into a Task before interpreting.
342
+ */
334
343
  async function runCreate(
335
344
  kind: CreateKind,
336
345
  params: Record<string, unknown>,
337
346
  rt: PragmaRuntime,
338
347
  ): Promise<Task<GeneratorResult>> {
339
- // Lazy: importing these runs the generators' top-level template loads and
340
- // pulls summon-core — kept off every non-create path. Now STATIC dynamic
341
- // imports so `--compile` bundles them; the generators' `.ejs` are embedded and
342
- // injected here. `package`/`application` stay a source-run feature in the binary.
348
+ // Lazy: importing these pulls summon-core (and with it React) — kept off every
349
+ // non-create path. Now STATIC dynamic imports so `--compile` bundles them; the
350
+ // embedded `.ejs` manifest is injected here. A binding that does not read
351
+ // through it stays a source-run feature in the binary.
343
352
  const { pickGenerator, summon } = await loadCreateRuntime(kind);
344
353
 
345
354
  // Normalize the CLI/MCP `--with-X` include-flags to the generator prompt names
@@ -374,7 +383,13 @@ async function runCreate(
374
383
  // `onCancel` (H2): an in-Ink Ctrl-C during execution aborts the run so the
375
384
  // interpreter stops writing — raw mode swallows the SIGINT, so the wizard
376
385
  // drives the abort. Shared with setup by construction.
377
- const session = summon.inkPrompt(generator, { signal, onCancel: abort });
386
+ // `cwd` also reaches the wizard: its confirm-gate preview reads the same
387
+ // jail-checked write root the interpreter resolves effect paths against.
388
+ const session = summon.inkPrompt(generator, {
389
+ signal,
390
+ onCancel: abort,
391
+ cwd: rt.cwd,
392
+ });
378
393
  // Thread the per-call write root: the interpreter resolves the generator's
379
394
  // relative output paths against `rt.cwd` — the SAME dir the SEC-2 jail
380
395
  // validated above — so the write can never escape the checked directory.
@@ -433,10 +448,51 @@ const CREATE_CAPABILITY = {
433
448
  },
434
449
  };
435
450
 
451
+ /**
452
+ * Why a binding is source-run only — the ONE authoring of it, shared by the
453
+ * documentation below and by {@link loadCreateRuntime}'s `UNSUPPORTED` refusal,
454
+ * so what the reference promises and what the binary does cannot disagree.
455
+ */
456
+ const SOURCE_ONLY_REASON =
457
+ "its generator reads templates from disk, which the binary does not carry";
458
+
459
+ /**
460
+ * The published caveat for a binding the compiled binary cannot run. DERIVED
461
+ * from `CREATE_GENERATORS[kind].readsEmbeddedTemplates` — flipping that bit
462
+ * moves the caveat, `docs/reference/*.md`, and `create.test.ts`'s binding
463
+ * assertion together.
464
+ *
465
+ * It LEADS WITH THE PURPOSE, because `tools.md` and the MCP tool description
466
+ * both render `doc ?? summary` — so a caveat alone REPLACED the only sentence
467
+ * saying what the tool makes, and an agent enumerating tools could not learn
468
+ * that `create_package` scaffolds a package. The tool works from a source
469
+ * checkout; describing nothing but its refusal made a live tool
470
+ * undiscoverable. The repeated sentence in `commands.md`'s section is the
471
+ * cheaper of the two costs.
472
+ *
473
+ * It names no CLI flag (`mcp/toolDescriptions.test.ts` forbids one, and an
474
+ * agent has no flags): the plan-only refusal is stated in the terms BOTH
475
+ * surfaces share.
476
+ *
477
+ * @param kind - The create binding.
478
+ * @param summary - The verb's own one-liner, which the `doc` replaces on MCP.
479
+ * @returns The `doc` for a source-run-only binding, or `undefined`.
480
+ */
481
+ function buildAvailabilityDoc(
482
+ kind: CreateKind,
483
+ summary: string,
484
+ ): string | undefined {
485
+ if (CREATE_GENERATORS[kind].readsEmbeddedTemplates) return undefined;
486
+ return `${summary} From the compiled ${BIN_NAME} binary, \`create ${kind}\` refuses with \`UNSUPPORTED\` and writes nothing. Asking it only to PLAN refuses too — the gate runs while the plan is built — so a successful plan is never evidence it would run. The cause is that ${SOURCE_ONLY_REASON}. Run it from a source checkout, or use the \`summon\` CLI.`;
487
+ }
488
+
436
489
  /**
437
490
  * Build a create verb. `run` presents `Promise<Task<R>>` through the `Task<R>`
438
491
  * arm by an honest cast at this one site (mirroring `sources update`): a literal
439
492
  * `Promise<Task<R>>` arm in the union would poison async read-verb inference.
493
+ *
494
+ * A binding the compiled binary cannot run says so in its `summary` (which is
495
+ * what `--help` and the noun listing show) and explains itself in its `doc`.
440
496
  */
441
497
  function createVerb(
442
498
  kind: CreateKind,
@@ -444,9 +500,11 @@ function createVerb(
444
500
  params: ParamSpec[],
445
501
  examples: VerbSpec["examples"],
446
502
  ): VerbSpec<Record<string, unknown>, GeneratorResult> {
503
+ const doc = buildAvailabilityDoc(kind, summary);
447
504
  return {
448
505
  path: ["create", kind],
449
- summary,
506
+ summary: doc ? `${summary} Source-run only.` : summary,
507
+ ...(doc ? { doc } : {}),
450
508
  params,
451
509
  output: { formatters: createFormatters },
452
510
  examples,
@@ -456,38 +514,51 @@ function createVerb(
456
514
  };
457
515
  }
458
516
 
459
- export const createComponentVerb = createVerb(
460
- "component",
461
- "Scaffold a React, Svelte, or Lit component.",
462
- componentParams,
463
- [
464
- {
465
- cmd: "pragma create component src/components/Button --framework react",
466
- note: "React component with tests, stories, and styles",
467
- },
468
- {
469
- cmd: "pragma create component src/lib/Card --framework svelte --dry-run",
470
- note: "preview the files without writing",
471
- },
472
- ],
473
- );
474
-
475
- export const createPackageVerb = createVerb(
476
- "package",
477
- "Scaffold a new npm package for the monorepo.",
478
- packageParams,
479
- [
480
- { cmd: "pragma create package --name @canonical/my-lib --type library" },
481
- { cmd: "pragma create package --name @canonical/my-tool --run-install" },
482
- ],
483
- );
484
-
485
- export const createApplicationVerb = createVerb(
486
- "application",
487
- "Scaffold a full React application with SSR and routing.",
488
- applicationParams,
489
- [
490
- { cmd: "pragma create application my-app" },
491
- { cmd: "pragma create application my-app --with-relay" },
492
- ],
493
- );
517
+ /**
518
+ * The `create` verbs, one per declared generator binding — so adding a binding
519
+ * to {@link CREATE_GENERATORS} without surfacing it is a type error rather than
520
+ * a silent no-op. Key order is authoring order, which `index.ts` preserves into
521
+ * the command tree, `--help` and the emitted surface.
522
+ */
523
+ export const createVerbs: Record<
524
+ CreateKind,
525
+ VerbSpec<Record<string, unknown>, GeneratorResult>
526
+ > = {
527
+ component: createVerb(
528
+ "component",
529
+ "Scaffold a React, Svelte, or Lit component.",
530
+ componentParams,
531
+ [
532
+ {
533
+ cmd: `${BIN_NAME} create component src/components/Button --framework react`,
534
+ note: "React component with tests, stories, and styles",
535
+ },
536
+ {
537
+ cmd: `${BIN_NAME} create component src/lib/Card --framework svelte --dry-run`,
538
+ note: "preview the files without writing",
539
+ },
540
+ ],
541
+ ),
542
+ package: createVerb(
543
+ "package",
544
+ "Scaffold a new npm package for the monorepo.",
545
+ packageParams,
546
+ [
547
+ {
548
+ cmd: `${BIN_NAME} create package --name @canonical/my-lib --type library`,
549
+ },
550
+ {
551
+ cmd: `${BIN_NAME} create package --name @canonical/my-tool --run-install`,
552
+ },
553
+ ],
554
+ ),
555
+ application: createVerb(
556
+ "application",
557
+ "Scaffold a full React application with SSR and routing.",
558
+ applicationParams,
559
+ [
560
+ { cmd: `${BIN_NAME} create application my-app` },
561
+ { cmd: `${BIN_NAME} create application my-app --with-relay` },
562
+ ],
563
+ ),
564
+ };
@@ -8,18 +8,10 @@
8
8
 
9
9
  import { asVerb } from "../../kernel/spec/asVerb.js";
10
10
  import type { CapabilityModule } from "../../kernel/spec/types.js";
11
- import {
12
- createApplicationVerb,
13
- createComponentVerb,
14
- createPackageVerb,
15
- } from "./create.verb.js";
11
+ import { createVerbs } from "./create.verb.js";
16
12
 
17
13
  /** The `create` capability module. */
18
14
  export const createModule: CapabilityModule = {
19
15
  name: "create",
20
- verbs: [
21
- asVerb(createComponentVerb),
22
- asVerb(createPackageVerb),
23
- asVerb(createApplicationVerb),
24
- ],
16
+ verbs: Object.values(createVerbs).map(asVerb),
25
17
  };