@canonical/pragma-cli 0.30.0 → 0.31.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 (254) hide show
  1. package/README.md +64 -35
  2. package/dist/pragma +0 -0
  3. package/package.json +15 -13
  4. package/src/completions/constants.ts +7 -0
  5. package/src/completions/handleQuery.test.ts +32 -0
  6. package/src/completions/handleQuery.ts +20 -1
  7. package/src/completions/queryCompletions.test.ts +5 -0
  8. package/src/completions/queryCompletions.ts +17 -3
  9. package/src/completions/querySocket.test.ts +31 -0
  10. package/src/completions/querySocket.ts +36 -9
  11. package/src/config/parseConfigValues.ts +2 -0
  12. package/src/config/readConfigLayers.test.ts +2 -0
  13. package/src/config/readConfigLayers.ts +3 -0
  14. package/src/config/types.ts +7 -0
  15. package/src/constants.ts +3 -1
  16. package/src/domains/block/commands/sample.ts +2 -1
  17. package/src/domains/block/formatters/list.test.ts +3 -3
  18. package/src/domains/block/formatters/lookup.test.ts +5 -5
  19. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  20. package/src/domains/block/stories.ts +9 -3
  21. package/src/domains/config/commands/channel.test.ts +16 -0
  22. package/src/domains/config/commands/channel.ts +14 -1
  23. package/src/domains/config/commands/framework.test.ts +14 -0
  24. package/src/domains/config/commands/framework.ts +13 -1
  25. package/src/domains/config/commands/tier.test.ts +7 -0
  26. package/src/domains/config/commands/tier.ts +14 -1
  27. package/src/domains/config/operations/validateTier.test.ts +9 -0
  28. package/src/domains/config/operations/validateTier.ts +10 -2
  29. package/src/domains/create/generatorPack/bundled.ts +49 -0
  30. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  31. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  32. package/src/domains/create/index.ts +13 -7
  33. package/src/domains/create/mcp/specs.ts +6 -161
  34. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  35. package/src/domains/create/renderGeneratorUi.ts +65 -0
  36. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  37. package/src/domains/doctor/formatters/doctor.ts +90 -38
  38. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
  39. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
  40. package/src/domains/doctor/operations/checks.test.ts +6 -4
  41. package/src/domains/doctor/operations/types.ts +23 -1
  42. package/src/domains/graph/commands/inspect.test.ts +2 -2
  43. package/src/domains/graph/commands/query.test.ts +5 -3
  44. package/src/domains/graph/formatters/inspect.ts +7 -7
  45. package/src/domains/graph/formatters/query.test.ts +87 -0
  46. package/src/domains/graph/formatters/query.ts +42 -5
  47. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  48. package/src/domains/graphql/commands/serve.test.ts +12 -6
  49. package/src/domains/graphql/commands/serve.ts +16 -6
  50. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  51. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  52. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  53. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  54. package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
  55. package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
  56. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  57. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  58. package/src/domains/graphql/operations/index.ts +5 -0
  59. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  60. package/src/domains/info/formatters/upgrade.ts +3 -1
  61. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  62. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  63. package/src/domains/info/operations/index.ts +1 -0
  64. package/src/domains/info/types.ts +13 -0
  65. package/src/domains/llm/capabilities.test.ts +23 -3
  66. package/src/domains/llm/commands/capabilities.ts +12 -3
  67. package/src/domains/llm/commands/llm.ts +5 -1
  68. package/src/domains/llm/data/commandReference.ts +20 -11
  69. package/src/domains/llm/data/decisionTrees.ts +10 -5
  70. package/src/domains/llm/data/toolCatalog.ts +49 -18
  71. package/src/domains/llm/formatters/orientation.ts +5 -1
  72. package/src/domains/llm/llmCommand.test.ts +11 -3
  73. package/src/domains/llm/mcp/specs.ts +3 -4
  74. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  75. package/src/domains/llm/operations/collectContext.ts +7 -17
  76. package/src/domains/modifier/commands/sample.ts +2 -1
  77. package/src/domains/modifier/operations/lookup.ts +5 -3
  78. package/src/domains/modifier/stories.ts +9 -0
  79. package/src/domains/ontology/commands/list.test.ts +5 -3
  80. package/src/domains/ontology/commands/show.test.ts +85 -9
  81. package/src/domains/ontology/formatters/index.ts +1 -0
  82. package/src/domains/ontology/formatters/list.ts +37 -12
  83. package/src/domains/ontology/formatters/show.ts +358 -67
  84. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  85. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  86. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  87. package/src/domains/ontology/helpers/index.ts +14 -0
  88. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  89. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  90. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  91. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  92. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  93. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  94. package/src/domains/ontology/operations/showOntology.ts +289 -7
  95. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  96. package/src/domains/ontology/stories.ts +53 -8
  97. package/src/domains/refs/operations/updateRefs.ts +0 -1
  98. package/src/domains/setup/commands/index.ts +1 -0
  99. package/src/domains/setup/commands/setup.ts +70 -0
  100. package/src/domains/setup/index.ts +7 -3
  101. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  102. package/src/domains/setup/operations/setupLsp.ts +7 -2
  103. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  104. package/src/domains/shared/bootStore.test.ts +82 -0
  105. package/src/domains/shared/bootStore.ts +56 -20
  106. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  107. package/src/domains/shared/extractLocalName.test.ts +3 -3
  108. package/src/domains/shared/formatters.ts +23 -2
  109. package/src/domains/shared/listTiers.ts +40 -0
  110. package/src/domains/shared/loaders/bundledLoader.ts +48 -0
  111. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  112. package/src/domains/shared/loaders/localLoader.ts +2 -18
  113. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  114. package/src/domains/shared/loaders/readPackageDir.ts +46 -8
  115. package/src/domains/shared/lookupMany.test.ts +15 -0
  116. package/src/domains/shared/lookupMany.ts +22 -6
  117. package/src/domains/shared/namespaces.ts +13 -5
  118. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  119. package/src/domains/shared/parseSampleCount.ts +45 -0
  120. package/src/domains/shared/prefixes.test.ts +211 -0
  121. package/src/domains/shared/prefixes.ts +182 -15
  122. package/src/domains/shared/renderers.ts +5 -5
  123. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  124. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  125. package/src/domains/shared/semanticPackage.test.ts +89 -0
  126. package/src/domains/shared/semanticPackage.ts +17 -2
  127. package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
  128. package/src/domains/shared/stories/compileLookupTool.ts +1 -1
  129. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
  130. package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
  131. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  132. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  133. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
  134. package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
  135. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  136. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  137. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  138. package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
  139. package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
  140. package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
  141. package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
  142. package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
  143. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
  144. package/src/domains/shared/stories/pack/index.ts +14 -0
  145. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  146. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  147. package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
  148. package/src/domains/shared/stories/pack/types.ts +123 -3
  149. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
  150. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
  151. package/src/domains/shared/stories/types.ts +9 -1
  152. package/src/domains/shared/types/index.ts +5 -9
  153. package/src/domains/shared/types/ontology.ts +119 -24
  154. package/src/domains/shared/types/standard.ts +9 -52
  155. package/src/domains/token/commands/lookup.test.ts +8 -0
  156. package/src/domains/token/commands/lookup.ts +12 -0
  157. package/src/domains/token/commands/sample.ts +2 -1
  158. package/src/domains/token/featureFlag.ts +17 -0
  159. package/src/domains/token/index.ts +5 -0
  160. package/src/domains/token/mcp/specs.ts +13 -1
  161. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  162. package/src/domains/trace/commands/show.ts +30 -4
  163. package/src/index.ts +1 -13
  164. package/src/mcp/registerResources.test.ts +2 -2
  165. package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
  166. package/src/mcp/tools/index.ts +18 -10
  167. package/src/mcp/tools/registerTools.test.ts +106 -30
  168. package/src/pipeline/collectCommands.test.ts +151 -1
  169. package/src/pipeline/collectCommands.ts +34 -19
  170. package/src/pipeline/createProgram.test.ts +5 -1
  171. package/src/pipeline/createProgram.ts +2 -2
  172. package/src/pipeline/firstRun.test.ts +80 -0
  173. package/src/pipeline/firstRun.ts +99 -0
  174. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  175. package/src/pipeline/parseGlobalFlags.ts +98 -9
  176. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  177. package/src/pipeline/resolveCommandKind.ts +4 -0
  178. package/src/pipeline/rootHelp.test.ts +87 -0
  179. package/src/pipeline/rootHelp.ts +265 -0
  180. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  181. package/src/pipeline/runCli.test.ts +17 -4
  182. package/src/pipeline/runCli.ts +162 -19
  183. package/src/testing/cli.ts +30 -2
  184. package/src/testing/dsFixtures.ts +21 -4
  185. package/src/testing/index.ts +1 -4
  186. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
  187. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  188. package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
  189. package/src/testing/integration/edge-cases.test.ts +62 -50
  190. package/src/testing/integration/executor-execution.test.ts +6 -4
  191. package/src/testing/integration/executor-golden.test.ts +2 -2
  192. package/src/testing/integration/mcp-surface.test.ts +40 -15
  193. package/src/testing/integration/parity.test.ts +119 -70
  194. package/src/testing/setupXdgIsolation.ts +16 -5
  195. package/src/testing/standardParityFixtures.ts +26 -83
  196. package/src/testing/store.ts +4 -4
  197. package/src/domains/create/commands/component.test.ts +0 -166
  198. package/src/domains/create/commands/component.ts +0 -123
  199. package/src/domains/create/commands/index.ts +0 -3
  200. package/src/domains/create/commands/package.test.ts +0 -35
  201. package/src/domains/create/commands/package.ts +0 -21
  202. package/src/domains/create/generators.ts +0 -8
  203. package/src/domains/standard/commands/categories.test.ts +0 -70
  204. package/src/domains/standard/commands/categories.ts +0 -48
  205. package/src/domains/standard/commands/index.ts +0 -6
  206. package/src/domains/standard/commands/list.test.ts +0 -105
  207. package/src/domains/standard/commands/list.ts +0 -11
  208. package/src/domains/standard/commands/lookup.test.ts +0 -135
  209. package/src/domains/standard/commands/lookup.ts +0 -14
  210. package/src/domains/standard/commands/sample.ts +0 -57
  211. package/src/domains/standard/formatters/categories.test.ts +0 -44
  212. package/src/domains/standard/formatters/categories.ts +0 -38
  213. package/src/domains/standard/formatters/index.ts +0 -10
  214. package/src/domains/standard/formatters/list.test.ts +0 -169
  215. package/src/domains/standard/formatters/list.ts +0 -184
  216. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  217. package/src/domains/standard/formatters/lookup.ts +0 -101
  218. package/src/domains/standard/formatters/sample.ts +0 -15
  219. package/src/domains/standard/formatters/types.ts +0 -31
  220. package/src/domains/standard/index.ts +0 -40
  221. package/src/domains/standard/mcp/index.ts +0 -2
  222. package/src/domains/standard/mcp/specs.ts +0 -89
  223. package/src/domains/standard/operations/categories.test.ts +0 -61
  224. package/src/domains/standard/operations/categories.ts +0 -38
  225. package/src/domains/standard/operations/index.ts +0 -6
  226. package/src/domains/standard/operations/list.test.ts +0 -75
  227. package/src/domains/standard/operations/list.ts +0 -64
  228. package/src/domains/standard/operations/lookup.test.ts +0 -58
  229. package/src/domains/standard/operations/lookup.ts +0 -151
  230. package/src/domains/standard/operations/sample.ts +0 -35
  231. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  232. package/src/domains/standard/orchestration/index.ts +0 -4
  233. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  234. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
  235. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  236. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  237. package/src/domains/standard/standardConfig.ts +0 -42
  238. package/src/domains/standard/stories.test.ts +0 -24
  239. package/src/domains/standard/stories.ts +0 -151
  240. package/src/domains/standard/types.ts +0 -15
  241. package/src/domains/tier/commands/index.ts +0 -3
  242. package/src/domains/tier/commands/list.test.ts +0 -67
  243. package/src/domains/tier/commands/list.ts +0 -9
  244. package/src/domains/tier/formatters/index.ts +0 -3
  245. package/src/domains/tier/formatters/list.test.ts +0 -33
  246. package/src/domains/tier/formatters/list.ts +0 -45
  247. package/src/domains/tier/index.ts +0 -17
  248. package/src/domains/tier/mcp/index.ts +0 -2
  249. package/src/domains/tier/mcp/specs.ts +0 -12
  250. package/src/domains/tier/operations/index.ts +0 -3
  251. package/src/domains/tier/operations/list.test.ts +0 -32
  252. package/src/domains/tier/operations/list.ts +0 -33
  253. package/src/domains/tier/stories.test.ts +0 -22
  254. package/src/domains/tier/stories.ts +0 -52
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Curated root `--help` for `pragma`.
3
+ *
4
+ * cli-core's generic `formatHelp` derives a description per top-level noun from
5
+ * its commands, but noun groups that only have verbs (e.g. `block list`) have
6
+ * no command to carry one — so it emits placeholder text like "block commands".
7
+ * The root help is also the CLI's front door, so its content and grouping are
8
+ * worth hand-curating here rather than auto-generating.
9
+ *
10
+ * This renderer:
11
+ * - groups nouns into task-oriented sections with real one-line summaries,
12
+ * - derives each section's live nouns from the registered commands (so a noun
13
+ * that disappears is dropped, and a new, uncatalogued one still shows up
14
+ * under "Other" instead of vanishing),
15
+ * - expands `create` to show its scaffolding subtypes, including the component
16
+ * frameworks pulled straight from the command's parameter choices.
17
+ */
18
+
19
+ import type { CommandDefinition } from "@canonical/cli-core";
20
+ import chalk from "chalk";
21
+
22
+ interface NounSummary {
23
+ readonly noun: string;
24
+ readonly summary: string;
25
+ }
26
+
27
+ interface HelpGroup {
28
+ readonly title: string;
29
+ readonly nouns: readonly NounSummary[];
30
+ }
31
+
32
+ /**
33
+ * Task-oriented grouping with curated summaries. Order is intentional: the
34
+ * things people reach for most (exploring the system, generating code) come
35
+ * first; agent/integration tooling comes last.
36
+ */
37
+ const HELP_GROUPS: readonly HelpGroup[] = [
38
+ {
39
+ title: "Explore the design system",
40
+ nouns: [
41
+ {
42
+ noun: "block",
43
+ summary: "Inspect components & patterns and their anatomy",
44
+ },
45
+ { noun: "token", summary: "Look up design tokens and their values" },
46
+ { noun: "modifier", summary: "Inspect modifiers and modifier families" },
47
+ { noun: "standard", summary: "Browse code standards and categories" },
48
+ {
49
+ noun: "ontology",
50
+ summary: "Explore the loaded ontologies (classes, properties)",
51
+ },
52
+ { noun: "tier", summary: "List the design-system tiers" },
53
+ ],
54
+ },
55
+ {
56
+ title: "Generate code",
57
+ nouns: [
58
+ { noun: "create", summary: "Scaffold components, packages, and apps" },
59
+ ],
60
+ },
61
+ {
62
+ title: "Query & serve the graph",
63
+ nouns: [
64
+ {
65
+ noun: "graph",
66
+ summary: "Run SPARQL queries or inspect a URI directly",
67
+ },
68
+ {
69
+ noun: "graphql",
70
+ summary: "Compile and serve a GraphQL API over the ontologies",
71
+ },
72
+ ],
73
+ },
74
+ {
75
+ title: "Set up & maintain",
76
+ nouns: [
77
+ { noun: "doctor", summary: "Check environment health" },
78
+ {
79
+ noun: "setup",
80
+ summary: "Configure MCP, skills, completions, and the LSP",
81
+ },
82
+ { noun: "config", summary: "Read and write pragma.config.json" },
83
+ { noun: "tokens", summary: "Generate a Terrazzo tokens config" },
84
+ {
85
+ noun: "info",
86
+ summary: "Show version, config, update status, and store summary",
87
+ },
88
+ {
89
+ noun: "upgrade",
90
+ summary: "Upgrade the pragma CLI to the latest version",
91
+ },
92
+ ],
93
+ },
94
+ {
95
+ title: "For AI agents",
96
+ nouns: [
97
+ {
98
+ noun: "capabilities",
99
+ summary: "Discover conventions, tools, and the discovery sequence",
100
+ },
101
+ {
102
+ noun: "llm",
103
+ summary: "LLM orientation: context, decision trees, command reference",
104
+ },
105
+ { noun: "mcp", summary: "Start the MCP server over stdio" },
106
+ {
107
+ noun: "skill",
108
+ summary: "Browse agent skills from design-system packages",
109
+ },
110
+ ],
111
+ },
112
+ ];
113
+
114
+ /** All distinct top-level nouns present in the registered commands. */
115
+ function nounsFrom(commands: readonly CommandDefinition[]): Set<string> {
116
+ const nouns = new Set<string>();
117
+ for (const cmd of commands) {
118
+ const noun = cmd.path[0];
119
+ if (noun) nouns.add(noun);
120
+ }
121
+ return nouns;
122
+ }
123
+
124
+ /** The verbs (second path segment) registered under a noun, in order. */
125
+ function verbsOf(
126
+ noun: string,
127
+ commands: readonly CommandDefinition[],
128
+ ): CommandDefinition[] {
129
+ return commands.filter((c) => c.path[0] === noun && c.path.length > 1);
130
+ }
131
+
132
+ /**
133
+ * The framework values offered by `create component`, e.g. `react · svelte ·
134
+ * lit`, read from the command's `framework` parameter choices so the help
135
+ * can never drift from what the generator actually accepts.
136
+ */
137
+ function componentFrameworks(
138
+ commands: readonly CommandDefinition[],
139
+ ): string | undefined {
140
+ const component = commands.find(
141
+ (c) => c.path[0] === "create" && c.path[1] === "component",
142
+ );
143
+ const choices = component?.parameters.find(
144
+ (p) => p.name === "framework",
145
+ )?.choices;
146
+ if (!choices || choices.length === 0) return undefined;
147
+ return choices.map((c) => c.label).join(" · ");
148
+ }
149
+
150
+ /** Render the `create` subtypes as indented sub-lines. */
151
+ function createSubtypes(
152
+ commands: readonly CommandDefinition[],
153
+ subWidth: number,
154
+ ): string[] {
155
+ const lines: string[] = [];
156
+ const frameworks = componentFrameworks(commands);
157
+ for (const verb of verbsOf("create", commands)) {
158
+ const name = verb.path[1] as string;
159
+ const note =
160
+ name === "component" && frameworks
161
+ ? frameworks
162
+ : name === "package"
163
+ ? "a new package for the monorepo"
164
+ : verb.description;
165
+ lines.push(
166
+ ` ${chalk.dim("·")} ${chalk.cyan(name.padEnd(subWidth))} ${chalk.dim(note)}`,
167
+ );
168
+ }
169
+ return lines;
170
+ }
171
+
172
+ /**
173
+ * Build the curated root help string.
174
+ *
175
+ * @param programName - The CLI binary name (`pragma`).
176
+ * @param description - The program description shown in the header.
177
+ * @param commands - All registered command definitions.
178
+ * @returns The formatted, colorized help text.
179
+ */
180
+ export default function formatRootHelp(
181
+ programName: string,
182
+ description: string,
183
+ commands: readonly CommandDefinition[],
184
+ ): string {
185
+ const present = nounsFrom(commands);
186
+ // `mcp` is attached to Commander directly (not via the command list), but is
187
+ // always available — surface it so the front door is complete.
188
+ present.add("mcp");
189
+ const catalogued = new Set(
190
+ HELP_GROUPS.flatMap((g) => g.nouns.map((n) => n.noun)),
191
+ );
192
+
193
+ // Any live noun we didn't curate is surfaced so nothing silently disappears.
194
+ const uncatalogued = [...present].filter((n) => !catalogued.has(n)).sort();
195
+ const groups: HelpGroup[] = [
196
+ ...HELP_GROUPS.map((g) => ({
197
+ ...g,
198
+ nouns: g.nouns.filter((n) => present.has(n.noun)),
199
+ })),
200
+ ...(uncatalogued.length > 0
201
+ ? [
202
+ {
203
+ title: "Other",
204
+ nouns: uncatalogued.map((noun) => ({
205
+ noun,
206
+ summary:
207
+ verbsOf(noun, commands)[0]?.description ?? `${noun} commands`,
208
+ })),
209
+ },
210
+ ]
211
+ : []),
212
+ ].filter((g) => g.nouns.length > 0);
213
+
214
+ // One shared column width so summaries line up across every group.
215
+ const nounWidth = Math.max(
216
+ ...groups.flatMap((g) => g.nouns.map((n) => n.noun.length)),
217
+ 0,
218
+ );
219
+ const subWidth = Math.max(
220
+ ...verbsOf("create", commands).map((v) => (v.path[1] as string).length),
221
+ 0,
222
+ );
223
+
224
+ const lines: string[] = [
225
+ `${chalk.bold(programName)} — ${description}`,
226
+ "",
227
+ `${chalk.dim("Usage:")} ${programName} ${chalk.cyan("<command>")} ${chalk.dim("[subcommand] [flags]")}`,
228
+ "",
229
+ ];
230
+
231
+ for (const group of groups) {
232
+ lines.push(chalk.bold(group.title));
233
+ for (const { noun, summary } of group.nouns) {
234
+ lines.push(
235
+ ` ${chalk.cyan(noun.padEnd(nounWidth))} ${chalk.dim(summary)}`,
236
+ );
237
+ if (noun === "create") {
238
+ lines.push(...createSubtypes(commands, subWidth));
239
+ }
240
+ }
241
+ lines.push("");
242
+ }
243
+
244
+ lines.push(chalk.bold("Global flags"));
245
+ const flags: [string, string][] = [
246
+ ["--llm", "Condensed Markdown output for agents"],
247
+ ["--format json", "Machine-readable JSON output"],
248
+ ["--verbose", "Diagnostic output on stderr"],
249
+ ["--help", "Show help (works on any command)"],
250
+ ["--version", "Show the CLI version"],
251
+ ];
252
+ const flagWidth = Math.max(...flags.map(([f]) => f.length));
253
+ for (const [flag, desc] of flags) {
254
+ lines.push(` ${chalk.cyan(flag.padEnd(flagWidth))} ${chalk.dim(desc)}`);
255
+ }
256
+ lines.push("");
257
+
258
+ lines.push(
259
+ chalk.dim(
260
+ `Run \`${programName} <command> --help\` for details, or \`${programName} capabilities\` to get oriented.`,
261
+ ),
262
+ );
263
+
264
+ return lines.join("\n");
265
+ }
@@ -33,6 +33,7 @@ vi.mock("./mapExitCode.js", () => ({
33
33
  vi.mock("./parseGlobalFlags.js", () => ({
34
34
  default: parseGlobalFlagsMock,
35
35
  stripGlobalFlags: (argv: readonly string[]) => [...argv],
36
+ readRawFormat: () => undefined,
36
37
  }));
37
38
 
38
39
  vi.mock("./resolveCommandKind.js", () => ({
@@ -109,10 +110,14 @@ describe("runCli orchestration", () => {
109
110
  collectCommandsMock.mockReturnValue([] as CommandDefinition[]);
110
111
  createProgramMock.mockReturnValue({ parseAsync });
111
112
 
113
+ // Completion queries are classified before the root-help shortcut, so
114
+ // resolveCommandKind runs first; a non-completion result still falls
115
+ // through to root help without booting the store.
116
+ resolveCommandKindMock.mockReturnValue({ kind: "store-required" });
117
+
112
118
  const { default: runCli } = await import("./runCli.js");
113
119
  await runCli(["node", "pragma", "--verbose"]);
114
120
 
115
- expect(resolveCommandKindMock).not.toHaveBeenCalled();
116
121
  expect(bootPragmaMock).not.toHaveBeenCalled();
117
122
  expect(parseAsync).toHaveBeenCalledWith(["node", "pragma", "--help"]);
118
123
  });
@@ -257,19 +262,30 @@ describe("runCli orchestration", () => {
257
262
  expect(dispose).toHaveBeenCalledTimes(1);
258
263
  });
259
264
 
260
- it("treats version commander errors as success", async () => {
261
- const parseAsync = vi.fn(async () => {
262
- throw new CommanderError(0, "commander.version", "version");
263
- });
265
+ it("prints the version directly and never reaches the program", async () => {
266
+ // --version is a global flag handled before command dispatch, so it neither
267
+ // boots the store nor builds a Commander program.
268
+ const stdout: string[] = [];
269
+ const stdoutSpy = vi
270
+ .spyOn(process.stdout, "write")
271
+ .mockImplementation((chunk) => {
272
+ stdout.push(String(chunk));
273
+ return true;
274
+ });
264
275
 
265
276
  resolveCommandKindMock.mockReturnValue({ kind: "store-skip" });
266
- setupCommandsMock.mockReturnValue([{ path: ["setup", "skills"] }]);
267
- createProgramMock.mockReturnValue({ parseAsync });
268
-
269
- const { default: runCli } = await import("./runCli.js");
270
- await runCli(["node", "pragma", "--version"]);
271
-
272
- expect(process.exitCode).toBe(0);
277
+ createProgramMock.mockClear();
278
+
279
+ try {
280
+ const { default: runCli } = await import("./runCli.js");
281
+ await runCli(["node", "pragma", "--version"]);
282
+ } finally {
283
+ stdoutSpy.mockRestore();
284
+ }
285
+
286
+ expect(stdout.join("")).toContain(".");
287
+ expect(createProgramMock).not.toHaveBeenCalled();
288
+ expect(process.exitCode).toBeUndefined();
273
289
  });
274
290
 
275
291
  it("renders pragma errors from the program using mapped exit codes", async () => {
@@ -4,9 +4,17 @@ import createProgram from "./createProgram.js";
4
4
  import parseGlobalFlags from "./parseGlobalFlags.js";
5
5
 
6
6
  describe("parseGlobalFlags", () => {
7
- it("parses defaults", () => {
8
- const flags = parseGlobalFlags(["node", "pragma"]);
9
- expect(flags).toEqual({ llm: false, format: "text", verbose: false });
7
+ it("parses defaults on an interactive terminal", () => {
8
+ const flags = parseGlobalFlags(["node", "pragma"], {
9
+ isTty: true,
10
+ noAutoLlm: false,
11
+ });
12
+ expect(flags).toEqual({
13
+ llm: false,
14
+ autoLlm: false,
15
+ format: "text",
16
+ verbose: false,
17
+ });
10
18
  });
11
19
 
12
20
  it("parses --llm", () => {
@@ -33,7 +41,12 @@ describe("parseGlobalFlags", () => {
33
41
  "json",
34
42
  "--verbose",
35
43
  ]);
36
- expect(flags).toEqual({ llm: true, format: "json", verbose: true });
44
+ expect(flags).toEqual({
45
+ llm: true,
46
+ autoLlm: false,
47
+ format: "json",
48
+ verbose: true,
49
+ });
37
50
  });
38
51
  });
39
52
 
@@ -1,5 +1,7 @@
1
1
  import type { GlobalFlags } from "@canonical/cli-core";
2
2
  import { CommanderError } from "commander";
3
+ import { PROGRAM_NAME, VERSION } from "../constants.js";
4
+ import { commands as createCommands } from "../domains/create/index.js";
3
5
  import { commands as graphqlCommands } from "../domains/graphql/index.js";
4
6
  import { buildCapabilitiesCommand } from "../domains/llm/index.js";
5
7
  import { commands as refsCommands } from "../domains/refs/index.js";
@@ -12,8 +14,12 @@ import { commands as traceCommands } from "../domains/trace/index.js";
12
14
  import { PragmaError } from "../error/index.js";
13
15
  import collectCommands from "./collectCommands.js";
14
16
  import createProgram from "./createProgram.js";
17
+ import ensureFirstRun from "./firstRun.js";
15
18
  import mapExitCode from "./mapExitCode.js";
16
- import parseGlobalFlags, { stripGlobalFlags } from "./parseGlobalFlags.js";
19
+ import parseGlobalFlags, {
20
+ readRawFormat,
21
+ stripGlobalFlags,
22
+ } from "./parseGlobalFlags.js";
17
23
  import {
18
24
  renderErrorJson,
19
25
  renderErrorLlm,
@@ -67,8 +73,23 @@ async function handleCompletionsServer(): Promise<void> {
67
73
  await startCompletionsServer();
68
74
  }
69
75
 
70
- async function handleDoctor(globalFlags: GlobalFlags): Promise<void> {
76
+ async function handleDoctor(
77
+ argv: readonly string[],
78
+ globalFlags: GlobalFlags,
79
+ ): Promise<void> {
71
80
  const { doctorCommand } = await import("../domains/doctor/commands/index.js");
81
+
82
+ // `doctor` runs before Commander, so honour --help/-h here rather than
83
+ // executing the health check when the user only asked for usage.
84
+ const wantsHelp = argv
85
+ .slice(2)
86
+ .some((arg) => arg === "--help" || arg === "-h");
87
+ if (wantsHelp) {
88
+ const { formatVerbHelp } = await import("@canonical/cli-core");
89
+ process.stdout.write(`${formatVerbHelp(PROGRAM_NAME, doctorCommand)}\n`);
90
+ return;
91
+ }
92
+
72
93
  const ctx = { cwd: process.cwd(), globalFlags };
73
94
  const result = await doctorCommand.execute({}, ctx);
74
95
  if (result.tag === "output") {
@@ -91,7 +112,7 @@ async function handleRootHelp(globalFlags: GlobalFlags): Promise<void> {
91
112
  try {
92
113
  await program.parseAsync(["node", "pragma", "--help"]);
93
114
  } catch (err) {
94
- handleProgramError(err, globalFlags);
115
+ await handleProgramError(err, globalFlags, ["node", "pragma", "--help"]);
95
116
  }
96
117
  }
97
118
 
@@ -151,6 +172,8 @@ async function bootAndRun(
151
172
  return;
152
173
  }
153
174
 
175
+ await emitSchemaArtifact(runtime, globalFlags);
176
+
154
177
  try {
155
178
  const ctx: PragmaContext = {
156
179
  ...runtime,
@@ -161,12 +184,48 @@ async function bootAndRun(
161
184
  const program = createProgram(commands, ctx);
162
185
  await program.parseAsync(stripGlobalFlags(argv));
163
186
  } catch (err) {
164
- handleProgramError(err, globalFlags);
187
+ await handleProgramError(err, globalFlags, argv);
165
188
  } finally {
166
189
  runtime.dispose();
167
190
  }
168
191
  }
169
192
 
193
+ /**
194
+ * Keep the `schema.graphql` artifact current on every booted run.
195
+ *
196
+ * The freshness probe is a source hash against the artifact's header, so
197
+ * the common case costs one hash + one small read; compilation runs only
198
+ * when the resolved TTL sources actually changed (or on first use).
199
+ * Never fatal — a failed compile warns and the command proceeds.
200
+ */
201
+ async function emitSchemaArtifact(
202
+ runtime: PragmaRuntime,
203
+ globalFlags: GlobalFlags,
204
+ ): Promise<void> {
205
+ try {
206
+ const { ensureSchemaArtifact } = await import(
207
+ "../domains/graphql/operations/index.js"
208
+ );
209
+ const result = await ensureSchemaArtifact(runtime);
210
+ if (result.status === "written") {
211
+ process.stderr.write(
212
+ `Updated GraphQL schema artifact (sources changed): ${result.path}\n`,
213
+ );
214
+ } else if (result.status === "failed" && globalFlags.verbose) {
215
+ process.stderr.write(
216
+ `Warning: GraphQL schema compilation failed; ${result.path} not updated. Run \`pragma graphql check\` for diagnostics.\n`,
217
+ );
218
+ }
219
+ } catch (err) {
220
+ if (globalFlags.verbose) {
221
+ const reason = err instanceof Error ? err.message : String(err);
222
+ process.stderr.write(
223
+ `Warning: could not update GraphQL schema artifact: ${reason}\n`,
224
+ );
225
+ }
226
+ }
227
+ }
228
+
170
229
  async function runStoreSkip(
171
230
  argv: readonly string[],
172
231
  globalFlags: GlobalFlags,
@@ -185,6 +244,7 @@ async function runStoreSkip(
185
244
  ...refsCommands(),
186
245
  ...traceCommands(),
187
246
  ...graphqlCommands(),
247
+ ...createCommands(),
188
248
  buildCapabilitiesCommand(),
189
249
  ];
190
250
  const program = createProgram(commands, stubCtx);
@@ -192,11 +252,53 @@ async function runStoreSkip(
192
252
  try {
193
253
  await program.parseAsync(stripGlobalFlags(argv));
194
254
  } catch (err) {
195
- handleProgramError(err, globalFlags);
255
+ await handleProgramError(err, globalFlags, argv);
196
256
  }
197
257
  }
198
258
 
199
- function handleProgramError(err: unknown, globalFlags: GlobalFlags): void {
259
+ /**
260
+ * First non-flag token in argv — the command noun, if any.
261
+ *
262
+ * Global flags are stripped first so a flag *value* (e.g. the `json` in
263
+ * `--format json`) is never mistaken for the noun; otherwise
264
+ * `pragma --format json standard frobnicate` would scan `json` and fail to
265
+ * suggest `standard`'s verbs.
266
+ */
267
+ function findNoun(argv: readonly string[]): string | undefined {
268
+ return stripGlobalFlags(argv)
269
+ .slice(2)
270
+ .find((arg) => !arg.startsWith("-"));
271
+ }
272
+
273
+ /**
274
+ * When an unknown command/verb is entered under a known noun, print that noun's
275
+ * valid verbs so the user can recover — "defer to the category that exists".
276
+ *
277
+ * @note Impure — writes the noun's verb list to stderr.
278
+ */
279
+ async function suggestNounVerbs(argv: readonly string[]): Promise<void> {
280
+ const noun = findNoun(argv);
281
+ if (!noun) return;
282
+ const stubCtx: PragmaContext = {
283
+ store: {} as PragmaRuntime["store"],
284
+ ...(await resolveHelpConfig()),
285
+ cwd: process.cwd(),
286
+ dispose: () => {},
287
+ globalFlags: { llm: false, autoLlm: false, format: "text", verbose: false },
288
+ promptSession: createInteractivePromptSession,
289
+ };
290
+ const commands = collectCommands(stubCtx);
291
+ const hasNoun = commands.some((cmd) => cmd.path.at(0) === noun);
292
+ if (!hasNoun) return;
293
+ const { formatNounHelp } = await import("@canonical/cli-core");
294
+ process.stderr.write(`\n${formatNounHelp(PROGRAM_NAME, noun, commands)}\n`);
295
+ }
296
+
297
+ async function handleProgramError(
298
+ err: unknown,
299
+ globalFlags: GlobalFlags,
300
+ argv: readonly string[],
301
+ ): Promise<void> {
200
302
  if (err instanceof CommanderError) {
201
303
  if (
202
304
  err.code === "commander.helpDisplayed" ||
@@ -205,6 +307,9 @@ function handleProgramError(err: unknown, globalFlags: GlobalFlags): void {
205
307
  process.exitCode = 0;
206
308
  return;
207
309
  }
310
+ if (err.code === "commander.unknownCommand") {
311
+ await suggestNounVerbs(argv);
312
+ }
208
313
  process.exitCode = err.exitCode;
209
314
  return;
210
315
  }
@@ -240,26 +345,64 @@ function handleProgramError(err: unknown, globalFlags: GlobalFlags): void {
240
345
  */
241
346
  export default async function runCli(argv: readonly string[]): Promise<void> {
242
347
  const globalFlags = parseGlobalFlags(argv);
243
- const explicitHelpOrVersion = argv
348
+
349
+ const commandKind = resolveCommandKind(argv);
350
+
351
+ // Completion queries must be handled before the root-help shortcut:
352
+ // `pragma --completions` (empty first-Tab partial) has no non-flag command
353
+ // arg, so it would otherwise fall through to root help and dump the help
354
+ // banner into the shell's completion buffer.
355
+ if (commandKind.kind === "completions-client") {
356
+ return handleCompletionsClient(commandKind.partial);
357
+ }
358
+ if (commandKind.kind === "completions-server") {
359
+ return handleCompletionsServer();
360
+ }
361
+
362
+ // First-run onboarding: when the global config does not exist yet, greet on
363
+ // stderr and create it with defaults. After the completions early-exits so
364
+ // the note can never leak into a shell completion buffer; stderr-only so
365
+ // stdout (command output, --format json, MCP stdio) stays untouched; and
366
+ // failure-tolerant — onboarding must never break an invocation.
367
+ await ensureFirstRun();
368
+
369
+ // `--version`/`-V` is a global flag: print the version and exit regardless
370
+ // of where it appears (root or after a command/verb), so `block list -V`
371
+ // behaves like the root `pragma --version` instead of erroring.
372
+ if (argv.slice(2).some((arg) => arg === "--version" || arg === "-V")) {
373
+ process.stdout.write(`${VERSION}\n`);
374
+ return;
375
+ }
376
+
377
+ const explicitHelp = argv
244
378
  .slice(2)
245
- .some(
246
- (arg) =>
247
- arg === "--help" || arg === "-h" || arg === "--version" || arg === "-V",
248
- );
379
+ .some((arg) => arg === "--help" || arg === "-h");
249
380
 
250
- if (!hasCommandArg(argv) && !explicitHelpOrVersion) {
251
- return handleRootHelp(globalFlags);
381
+ // Reject an unknown --format value early (completion queries and --version
382
+ // are handled above, so neither errors). Skip when the user asked for help —
383
+ // it should print regardless of a bad --format. Only text/json are supported.
384
+ const rawFormat = readRawFormat(argv);
385
+ if (
386
+ !explicitHelp &&
387
+ rawFormat !== undefined &&
388
+ rawFormat !== "text" &&
389
+ rawFormat !== "json"
390
+ ) {
391
+ const error = PragmaError.invalidInput("format", rawFormat, {
392
+ validOptions: ["text", "json"],
393
+ });
394
+ process.stderr.write(`${renderError(error, globalFlags)}\n`);
395
+ process.exitCode = mapExitCode(error.code);
396
+ return;
252
397
  }
253
398
 
254
- const commandKind = resolveCommandKind(argv);
399
+ if (!hasCommandArg(argv) && !explicitHelp) {
400
+ return handleRootHelp(globalFlags);
401
+ }
255
402
 
256
403
  switch (commandKind.kind) {
257
- case "completions-client":
258
- return handleCompletionsClient(commandKind.partial);
259
- case "completions-server":
260
- return handleCompletionsServer();
261
404
  case "doctor":
262
- return handleDoctor(globalFlags);
405
+ return handleDoctor(argv, globalFlags);
263
406
  case "store-skip":
264
407
  return runStoreSkip(argv, globalFlags);
265
408
  case "store-required":
@@ -6,7 +6,9 @@
6
6
  */
7
7
 
8
8
  import { spawnSync } from "node:child_process";
9
- import { resolve } from "node:path";
9
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join, resolve } from "node:path";
10
12
 
11
13
  /** Captured output from a CLI subprocess invocation. */
12
14
  interface CommandResult {
@@ -15,21 +17,47 @@ interface CommandResult {
15
17
  exitCode: number;
16
18
  }
17
19
 
20
+ /**
21
+ * XDG home whose global config already exists, shared by all default
22
+ * invocations in this file's worker.
23
+ *
24
+ * Subprocesses would otherwise trigger the first-run greeting in whichever
25
+ * test happens to spawn first (the setup-file XDG is fresh per file), making
26
+ * stderr assertions ordering-dependent. Seeding the config keeps every
27
+ * default `runCommand` past first-run; tests that WANT the first-run path
28
+ * pass their own fresh XDG via `env`.
29
+ */
30
+ let seededXdg: string | undefined;
31
+ function seededXdgHome(): string {
32
+ if (!seededXdg) {
33
+ seededXdg = mkdtempSync(join(tmpdir(), "pragma-cli-xdg-"));
34
+ mkdirSync(join(seededXdg, "pragma"), { recursive: true });
35
+ writeFileSync(join(seededXdg, "pragma", "config.json"), "{}\n");
36
+ }
37
+ return seededXdg;
38
+ }
39
+
18
40
  /**
19
41
  * Spawn the pragma CLI binary synchronously and capture its output.
20
42
  *
21
43
  * @param args - CLI arguments to pass after the binary path.
22
44
  * @param cwd - Optional working directory for the subprocess.
45
+ * @param env - Extra environment overrides (merged over the seeded default).
23
46
  * @returns Captured stdout, stderr, and exit code.
24
47
  *
25
48
  * @note Impure
26
49
  */
27
- function runCommand(args: string[], cwd?: string): CommandResult {
50
+ function runCommand(
51
+ args: string[],
52
+ cwd?: string,
53
+ env?: Record<string, string>,
54
+ ): CommandResult {
28
55
  const binPath = resolve(import.meta.dirname, "../bin.ts");
29
56
  const result = spawnSync("bun", ["run", binPath, ...args], {
30
57
  cwd,
31
58
  encoding: "utf-8",
32
59
  timeout: 20_000,
60
+ env: { ...process.env, XDG_CONFIG_HOME: seededXdgHome(), ...env },
33
61
  });
34
62
 
35
63
  return {