@canonical/pragma-cli 0.20.0 → 0.21.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 (199) hide show
  1. package/dist/pragma +0 -0
  2. package/package.json +30 -18
  3. package/src/constants.ts +1 -1
  4. package/src/domains/block/blockConfig.ts +47 -3
  5. package/src/domains/block/commands/list.test.ts +1 -1
  6. package/src/domains/block/commands/list.ts +27 -0
  7. package/src/domains/block/commands/lookup.ts +26 -52
  8. package/src/domains/block/formatters/index.ts +1 -1
  9. package/src/domains/block/formatters/list.test.ts +1 -1
  10. package/src/domains/block/formatters/list.ts +28 -42
  11. package/src/domains/block/formatters/lookup.test.ts +3 -2
  12. package/src/domains/block/formatters/lookup.ts +91 -296
  13. package/src/domains/block/formatters/types.ts +1 -1
  14. package/src/domains/block/operations/list.test.ts +1 -1
  15. package/src/domains/block/operations/list.ts +1 -1
  16. package/src/domains/block/operations/lookup.test.ts +29 -8
  17. package/src/domains/block/operations/lookup.ts +94 -16
  18. package/src/domains/block/orchestration/blockEmptyError.ts +1 -1
  19. package/src/domains/block/orchestration/buildBlockFilters.ts +1 -1
  20. package/src/domains/block/orchestration/enrichBlocks.ts +6 -2
  21. package/src/domains/block/orchestration/resolveBlockList.ts +1 -1
  22. package/src/domains/block/orchestration/resolveBlockLookup.ts +8 -2
  23. package/src/domains/block/types.ts +1 -1
  24. package/src/domains/config/commands/channel.ts +6 -4
  25. package/src/domains/config/commands/tier.ts +6 -4
  26. package/src/domains/config/mcp/specs.ts +18 -6
  27. package/src/domains/config/operations/validateTier.ts +1 -1
  28. package/src/domains/config/tasks/index.ts +3 -0
  29. package/src/domains/config/tasks/setChannel.ts +30 -0
  30. package/src/domains/config/tasks/setTier.ts +33 -0
  31. package/src/domains/create/commands/package.test.ts +2 -2
  32. package/src/domains/doctor/mcp/index.ts +1 -1
  33. package/src/domains/doctor/mcp/specs.ts +2 -49
  34. package/src/domains/graph/commands/inspect.test.ts +1 -1
  35. package/src/domains/graph/formatters/inspect.ts +12 -7
  36. package/src/domains/graph/operations/inspectUri.ts +4 -1
  37. package/src/domains/info/commands/upgrade.ts +6 -3
  38. package/src/domains/info/index.ts +3 -1
  39. package/src/domains/info/mcp/index.ts +2 -0
  40. package/src/domains/info/mcp/specs.ts +65 -0
  41. package/src/domains/llm/data/commandReference.ts +4 -4
  42. package/src/domains/llm/data/decisionTrees.ts +14 -12
  43. package/src/domains/llm/operations/collectContext.ts +1 -1
  44. package/src/domains/modifier/commands/list.ts +9 -0
  45. package/src/domains/modifier/commands/lookup.ts +22 -42
  46. package/src/domains/modifier/formatters/index.ts +4 -1
  47. package/src/domains/modifier/formatters/list.test.ts +1 -1
  48. package/src/domains/modifier/formatters/list.ts +13 -26
  49. package/src/domains/modifier/formatters/lookup.test.ts +1 -1
  50. package/src/domains/modifier/formatters/lookup.ts +41 -29
  51. package/src/domains/modifier/index.ts +1 -0
  52. package/src/domains/modifier/modifierConfig.ts +36 -0
  53. package/src/domains/modifier/operations/list.ts +1 -1
  54. package/src/domains/modifier/operations/lookup.ts +4 -3
  55. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +1 -1
  56. package/src/domains/modifier/types.ts +1 -1
  57. package/src/domains/ontology/commands/list.test.ts +1 -1
  58. package/src/domains/ontology/commands/list.ts +18 -0
  59. package/src/domains/ontology/commands/show.test.ts +1 -1
  60. package/src/domains/ontology/formatters/list.ts +5 -3
  61. package/src/domains/ontology/formatters/show.ts +38 -13
  62. package/src/domains/ontology/helpers/queryClasses.ts +1 -1
  63. package/src/domains/ontology/helpers/queryProperties.ts +1 -1
  64. package/src/domains/ontology/operations/listOntologies.ts +1 -1
  65. package/src/domains/ontology/operations/showOntology.ts +1 -1
  66. package/src/domains/setup/helpers/completionScripts.test.ts +3 -3
  67. package/src/domains/setup/helpers/completionScripts.ts +4 -4
  68. package/src/domains/shared/ToolSpec.ts +1 -1
  69. package/src/domains/shared/context.ts +3 -2
  70. package/src/domains/shared/contracts.ts +40 -1
  71. package/src/domains/shared/filters/buildFilterConfig.ts +1 -2
  72. package/src/domains/shared/filters/buildFilters.ts +1 -1
  73. package/src/domains/shared/filters/describeFilters.ts +1 -1
  74. package/src/domains/shared/formatters.ts +47 -0
  75. package/src/domains/shared/renderers.ts +410 -0
  76. package/src/domains/shared/runtime.ts +7 -22
  77. package/src/domains/shared/types/block.ts +107 -0
  78. package/src/domains/shared/types/common.ts +33 -0
  79. package/src/domains/shared/types/graph.ts +17 -0
  80. package/src/domains/shared/types/index.ts +49 -0
  81. package/src/domains/shared/types/modifier.ts +22 -0
  82. package/src/domains/shared/types/ontology.ts +50 -0
  83. package/src/domains/shared/types/runtime.ts +21 -0
  84. package/src/domains/shared/types/standard.ts +64 -0
  85. package/src/domains/shared/types/tier.ts +15 -0
  86. package/src/domains/shared/types/token.ts +30 -0
  87. package/src/domains/standard/commands/list.ts +9 -0
  88. package/src/domains/standard/commands/lookup.ts +22 -50
  89. package/src/domains/standard/formatters/categories.test.ts +1 -1
  90. package/src/domains/standard/formatters/categories.ts +1 -1
  91. package/src/domains/standard/formatters/index.ts +4 -1
  92. package/src/domains/standard/formatters/list.test.ts +4 -1
  93. package/src/domains/standard/formatters/list.ts +112 -84
  94. package/src/domains/standard/formatters/lookup.test.ts +1 -1
  95. package/src/domains/standard/formatters/lookup.ts +85 -70
  96. package/src/domains/standard/formatters/types.ts +1 -1
  97. package/src/domains/standard/index.ts +1 -0
  98. package/src/domains/standard/operations/categories.ts +1 -1
  99. package/src/domains/standard/operations/list.ts +1 -1
  100. package/src/domains/standard/operations/lookup.ts +1 -1
  101. package/src/domains/standard/orchestration/buildStandardFilters.ts +1 -1
  102. package/src/domains/standard/orchestration/resolveStandardList.ts +4 -1
  103. package/src/domains/standard/orchestration/resolveStandardLookup.ts +1 -1
  104. package/src/domains/standard/orchestration/standardEmptyError.ts +1 -1
  105. package/src/domains/standard/standardConfig.ts +42 -0
  106. package/src/domains/standard/types.ts +1 -1
  107. package/src/domains/tier/commands/list.ts +16 -0
  108. package/src/domains/tier/formatters/list.test.ts +1 -1
  109. package/src/domains/tier/formatters/list.ts +11 -6
  110. package/src/domains/tier/operations/list.ts +1 -1
  111. package/src/domains/token/commands/list.ts +9 -0
  112. package/src/domains/token/commands/lookup.ts +22 -48
  113. package/src/domains/token/formatters/index.ts +4 -1
  114. package/src/domains/token/formatters/list.test.ts +1 -1
  115. package/src/domains/token/formatters/list.ts +15 -28
  116. package/src/domains/token/formatters/lookup.test.ts +2 -2
  117. package/src/domains/token/formatters/lookup.ts +80 -45
  118. package/src/domains/token/index.ts +1 -0
  119. package/src/domains/token/operations/list.ts +1 -1
  120. package/src/domains/token/operations/lookup.ts +1 -1
  121. package/src/domains/token/orchestration/resolveTokenLookup.ts +1 -1
  122. package/src/domains/token/tokenConfig.ts +38 -0
  123. package/src/domains/token/types.ts +1 -1
  124. package/src/index.ts +1 -1
  125. package/src/mcp/runMcpServer.test.ts +2 -2
  126. package/src/mcp/tools/index.ts +2 -0
  127. package/src/mcp/tools/registerTools.test.ts +8 -3
  128. package/src/package-manager/types.ts +1 -3
  129. package/src/pipeline/createProgram.ts +20 -1
  130. package/src/pipeline/index.ts +4 -1
  131. package/src/pipeline/parseGlobalFlags.test.ts +92 -0
  132. package/src/pipeline/parseGlobalFlags.ts +25 -0
  133. package/src/pipeline/runCli.orchestration.test.ts +1 -0
  134. package/src/pipeline/runCli.ts +11 -4
  135. package/src/pipeline/runInteractiveCommand.ts +198 -0
  136. package/src/testing/cli.ts +2 -2
  137. package/src/testing/fixtures.ts +2 -2
  138. package/src/testing/integration/boot.test.ts +2 -2
  139. package/src/testing/integration/cli-user-stories.test.ts +198 -0
  140. package/src/testing/integration/edge-cases.test.ts +263 -0
  141. package/src/testing/integration/parity.test.ts +377 -27
  142. package/src/testing/store.ts +1 -1
  143. package/src/tui/components/BulletList/BulletList.tsx +46 -0
  144. package/src/tui/components/BulletList/index.ts +2 -0
  145. package/src/tui/components/BulletList/types.ts +6 -0
  146. package/src/tui/components/Card/Card.tsx +73 -0
  147. package/src/tui/components/Card/index.ts +1 -0
  148. package/src/tui/components/Card/types.ts +12 -0
  149. package/src/tui/components/CodeBlock/CodeBlock.tsx +37 -0
  150. package/src/tui/components/CodeBlock/index.ts +2 -0
  151. package/src/tui/components/CodeBlock/types.ts +6 -0
  152. package/src/tui/components/DataRow/DataRow.tsx +80 -0
  153. package/src/tui/components/DataRow/index.ts +2 -0
  154. package/src/tui/components/DataRow/types.ts +14 -0
  155. package/src/tui/components/FieldRow/FieldRow.tsx +24 -0
  156. package/src/tui/components/FieldRow/index.ts +2 -0
  157. package/src/tui/components/FieldRow/types.ts +10 -0
  158. package/src/tui/components/HeaderRow/HeaderRow.tsx +32 -0
  159. package/src/tui/components/HeaderRow/index.ts +2 -0
  160. package/src/tui/components/HeaderRow/types.ts +8 -0
  161. package/src/tui/components/MarkdownText/MarkdownText.tsx +19 -0
  162. package/src/tui/components/MarkdownText/index.ts +2 -0
  163. package/src/tui/components/MarkdownText/types.ts +4 -0
  164. package/src/tui/components/MiniTable/MiniTable.tsx +82 -0
  165. package/src/tui/components/MiniTable/index.ts +2 -0
  166. package/src/tui/components/MiniTable/types.ts +6 -0
  167. package/src/tui/components/SectionRenderer/SectionRenderer.tsx +91 -0
  168. package/src/tui/components/SectionRenderer/index.ts +2 -0
  169. package/src/tui/components/SectionRenderer/types.ts +15 -0
  170. package/src/tui/components/TreeView/TreeView.tsx +84 -0
  171. package/src/tui/components/TreeView/index.ts +2 -0
  172. package/src/tui/components/TreeView/types.ts +6 -0
  173. package/src/tui/components/index.ts +19 -0
  174. package/src/tui/constants.ts +39 -0
  175. package/src/tui/createListView.ts +23 -0
  176. package/src/tui/createLookupView.ts +26 -0
  177. package/src/tui/helpers/computeWidths.ts +53 -0
  178. package/src/tui/helpers/fitColumns.ts +33 -0
  179. package/src/tui/helpers/formatMarkdown.ts +19 -0
  180. package/src/tui/helpers/getTerminalSize.ts +16 -0
  181. package/src/tui/helpers/index.ts +6 -0
  182. package/src/tui/helpers/syntaxColor.ts +67 -0
  183. package/src/tui/helpers/truncateText.ts +12 -0
  184. package/src/tui/hooks/index.ts +3 -0
  185. package/src/tui/hooks/types.ts +7 -0
  186. package/src/tui/hooks/useAutoExit.ts +17 -0
  187. package/src/tui/hooks/useTerminalSize.ts +19 -0
  188. package/src/tui/index.ts +15 -0
  189. package/src/tui/renderInk.tsx +18 -0
  190. package/src/tui/types.ts +12 -0
  191. package/src/tui/views/ListView/ListView.tsx +104 -0
  192. package/src/tui/views/ListView/index.ts +2 -0
  193. package/src/tui/views/ListView/types.ts +14 -0
  194. package/src/tui/views/LookupView/LookupView.tsx +244 -0
  195. package/src/tui/views/LookupView/index.ts +2 -0
  196. package/src/tui/views/LookupView/types.ts +16 -0
  197. package/src/tui/views/index.ts +4 -0
  198. package/src/domains/shared/types.test-d.ts +0 -127
  199. package/src/domains/shared/types.ts +0 -348
@@ -1,65 +1,80 @@
1
- /**
2
- * Three-mode formatter factory for `pragma token lookup` output.
3
- *
4
- * - **plain** — terminal text showing token name, category, and optional
5
- * per-theme values when detailed.
6
- * - **llm** — condensed Markdown consumed by LLM agents and reused
7
- * by the MCP adapter when `condensed: true`.
8
- * - **json** — structured JSON; omits values unless detailed.
9
- */
10
-
1
+ import type { RenderLookupOptions } from "../../shared/contracts.js";
11
2
  import type { Formatters } from "../../shared/formatters.js";
12
- import type { TokenDetailed } from "../../shared/types.js";
3
+ import { renderLookupLlm, renderLookupPlain } from "../../shared/renderers.js";
4
+ import type { TokenDetailed } from "../../shared/types/index.js";
5
+ import { tokenConfig } from "../tokenConfig.js";
13
6
 
14
7
  /** Options controlling detail level for the token-lookup formatter. */
15
- export interface TokenLookupFormatterOptions {
8
+ interface TokenLookupFormatterOptions {
16
9
  readonly detailed: boolean;
17
10
  }
18
11
 
19
12
  /**
20
- * Creates a three-mode formatter set for a single token, capturing the
21
- * detail level in the closure so callers only pass the token data.
13
+ * Create a three-mode formatter set for a single token.
14
+ *
15
+ * Captures the detail level in the closure so callers only pass the token data.
22
16
  *
23
17
  * @param options - detail-level options
24
18
  * @returns plain/llm/json formatters for {@link TokenDetailed}
25
19
  */
26
- export function createLookupFormatters(
20
+ export default function createLookupFormatters(
27
21
  options: TokenLookupFormatterOptions,
28
22
  ): Formatters<TokenDetailed> {
29
23
  const { detailed } = options;
24
+ const sections = detailed ? tokenConfig.lookupSections : [];
30
25
 
31
26
  return {
32
- plain: (token) => {
33
- const lines: string[] = [];
34
- lines.push(token.name);
35
- lines.push(`Category: ${token.category || "—"}`);
36
-
37
- if (detailed && token.values.length > 0) {
38
- lines.push("");
39
- lines.push("Values:");
40
- for (const v of token.values) {
41
- lines.push(` ${v.theme}: ${v.value}`);
42
- }
43
- }
44
-
45
- return lines.join("\n");
46
- },
47
-
48
- llm: (token) => {
49
- const lines: string[] = [];
50
- lines.push(`## ${token.name}`);
51
- lines.push(`Category: ${token.category || "—"}`);
52
-
53
- if (detailed && token.values.length > 0) {
54
- lines.push("");
55
- lines.push("### Values");
56
- for (const v of token.values) {
57
- lines.push(`- ${v.theme}: \`${v.value}\``);
58
- }
59
- }
27
+ plain: (token) =>
28
+ renderLookupPlain(token, {
29
+ title: (entry) => entry.name,
30
+ fields: [
31
+ { label: "URI", value: (entry) => entry.uri },
32
+ { label: "Category", value: (entry) => entry.category || "—" },
33
+ ],
34
+ sections,
35
+ sectionOverrides: {
36
+ values: {
37
+ plain: (entry) =>
38
+ entry.values.length > 0
39
+ ? entry.values
40
+ .map((value) => ` ${value.theme}: ${value.value}`)
41
+ .join("\n")
42
+ : null,
43
+ llm: (entry) =>
44
+ entry.values.length > 0
45
+ ? entry.values
46
+ .map((value) => `- ${value.theme}: \`${value.value}\``)
47
+ .join("\n")
48
+ : null,
49
+ },
50
+ },
51
+ }),
60
52
 
61
- return lines.join("\n");
62
- },
53
+ llm: (token) =>
54
+ renderLookupLlm(token, {
55
+ title: (entry) => entry.name,
56
+ fields: [
57
+ { label: "URI", value: (entry) => entry.uri },
58
+ { label: "Category", value: (entry) => entry.category || "—" },
59
+ ],
60
+ sections,
61
+ sectionOverrides: {
62
+ values: {
63
+ plain: (entry) =>
64
+ entry.values.length > 0
65
+ ? entry.values
66
+ .map((value) => ` ${value.theme}: ${value.value}`)
67
+ .join("\n")
68
+ : null,
69
+ llm: (entry) =>
70
+ entry.values.length > 0
71
+ ? entry.values
72
+ .map((value) => `- ${value.theme}: \`${value.value}\``)
73
+ .join("\n")
74
+ : null,
75
+ },
76
+ },
77
+ }),
63
78
 
64
79
  json: (token) => {
65
80
  if (detailed) {
@@ -70,3 +85,23 @@ export function createLookupFormatters(
70
85
  },
71
86
  };
72
87
  }
88
+
89
+ /**
90
+ * Build lookup rendering options for the Ink TUI view.
91
+ *
92
+ * @param options - detail-level options
93
+ * @returns Options compatible with the LookupView component.
94
+ */
95
+ export function createInkLookupOptions(
96
+ options: TokenLookupFormatterOptions,
97
+ ): RenderLookupOptions<TokenDetailed> {
98
+ const sections = options.detailed ? tokenConfig.lookupSections : [];
99
+ return {
100
+ title: (entry) => entry.name,
101
+ fields: [
102
+ { label: "URI", value: (entry) => entry.uri },
103
+ { label: "Category", value: (entry) => entry.category || "—" },
104
+ ],
105
+ sections,
106
+ };
107
+ }
@@ -30,3 +30,4 @@ export {
30
30
  resolveTokenLookup,
31
31
  tokenEmptyError,
32
32
  } from "./orchestration/index.js";
33
+ export { tokenConfig } from "./tokenConfig.js";
@@ -11,7 +11,7 @@ import type { Store, URI } from "@canonical/ke";
11
11
  import { escapeSparqlValue } from "@canonical/ke";
12
12
  import { buildQuery } from "../../shared/buildQuery.js";
13
13
  import { P } from "../../shared/prefixes.js";
14
- import type { TokenSummary } from "../../shared/types.js";
14
+ import type { TokenSummary } from "../../shared/types/index.js";
15
15
 
16
16
  /** Optional filters for the token list operation. */
17
17
  export interface TokenListFilters {
@@ -15,7 +15,7 @@ import { escapeSparqlValue } from "@canonical/ke";
15
15
  import { PragmaError } from "#error";
16
16
  import { buildQuery } from "../../shared/buildQuery.js";
17
17
  import { P } from "../../shared/prefixes.js";
18
- import type { TokenDetailed } from "../../shared/types.js";
18
+ import type { TokenDetailed } from "../../shared/types/index.js";
19
19
 
20
20
  export default async function lookupToken(
21
21
  store: Store,
@@ -1,7 +1,7 @@
1
1
  import type { LookupContract, LookupResult } from "../../shared/contracts.js";
2
2
  import lookupMany from "../../shared/lookupMany.js";
3
3
  import type { PragmaRuntime } from "../../shared/runtime.js";
4
- import type { TokenDetailed } from "../../shared/types.js";
4
+ import type { TokenDetailed } from "../../shared/types/index.js";
5
5
  import { lookupToken } from "../operations/index.js";
6
6
 
7
7
  export default async function resolveTokenLookup(
@@ -0,0 +1,38 @@
1
+ import type { EntityDisplayConfig } from "../shared/contracts.js";
2
+ import { P } from "../shared/prefixes.js";
3
+ import type {
4
+ FilterConfig,
5
+ TokenDetailed,
6
+ TokenDigest,
7
+ TokenSummary,
8
+ } from "../shared/types/index.js";
9
+
10
+ /** Display-contract configuration for design-token entities. */
11
+ export const tokenConfig: EntityDisplayConfig<
12
+ TokenSummary,
13
+ TokenDigest,
14
+ TokenDetailed
15
+ > = {
16
+ domain: "token",
17
+ entityName: "token",
18
+ rdfTypes: [`${P.ds}Token`],
19
+ listColumns: [
20
+ { key: "uri", label: "IRI" },
21
+ { key: "name", label: "Name" },
22
+ { key: "category", label: "Category" },
23
+ ],
24
+ digestColumns: [],
25
+ lookupSections: [
26
+ { key: "values", heading: "Values", kind: "table", showWhenEmpty: false },
27
+ ],
28
+ emptyRecovery: (_filters: FilterConfig) => ({
29
+ message: "List available tokens.",
30
+ cli: "pragma token list",
31
+ mcp: { tool: "token_list" },
32
+ }),
33
+ notFoundRecovery: (_query: string) => ({
34
+ message: "List available tokens.",
35
+ cli: "pragma token list",
36
+ mcp: { tool: "token_list" },
37
+ }),
38
+ };
@@ -1,4 +1,4 @@
1
- import type { TokenSummary } from "../shared/types.js";
1
+ import type { TokenSummary } from "../shared/types/index.js";
2
2
 
3
3
  export interface TokenListResolution {
4
4
  readonly params: { category?: string };
package/src/index.ts CHANGED
@@ -62,7 +62,7 @@ export type {
62
62
  TokenDetailed,
63
63
  TokenRef,
64
64
  TokenSummary,
65
- } from "./domains/shared/types.js";
65
+ } from "./domains/shared/types/index.js";
66
66
 
67
67
  // — Domain Operations —————————————————————————————————————————————————————————
68
68
 
@@ -53,7 +53,7 @@ describe("runMcpServer", () => {
53
53
  const result = spawnSync("bun", ["run", binPath, "mcp"], {
54
54
  input: "",
55
55
  encoding: "utf-8",
56
- timeout: 10_000,
56
+ timeout: 20_000,
57
57
  });
58
58
 
59
59
  expect(result.error).toBeUndefined();
@@ -62,5 +62,5 @@ describe("runMcpServer", () => {
62
62
  console.error("pragma mcp stderr:", result.stderr);
63
63
  }
64
64
  expect(result.status).toBe(0);
65
- });
65
+ }, 20_000);
66
66
  });
@@ -13,6 +13,7 @@ import { specs as configSpecs } from "../../domains/config/mcp/index.js";
13
13
  import { specs as createSpecs } from "../../domains/create/mcp/index.js";
14
14
  import { specs as diagnosticSpecs } from "../../domains/doctor/mcp/index.js";
15
15
  import { specs as graphSpecs } from "../../domains/graph/mcp/index.js";
16
+ import { specs as infoSpecs } from "../../domains/info/mcp/index.js";
16
17
  import { specs as orientationSpecs } from "../../domains/llm/mcp/index.js";
17
18
  import { specs as modifierSpecs } from "../../domains/modifier/mcp/index.js";
18
19
  import { specs as ontologySpecs } from "../../domains/ontology/mcp/index.js";
@@ -35,6 +36,7 @@ const allSpecs: readonly ToolSpec[] = [
35
36
  ...graphSpecs,
36
37
  ...skillSpecs,
37
38
  ...diagnosticSpecs,
39
+ ...infoSpecs,
38
40
  ...orientationSpecs,
39
41
  ...createSpecs,
40
42
  ];
@@ -476,11 +476,15 @@ describe("config_show", () => {
476
476
  arguments: {},
477
477
  });
478
478
  const data = parseData(result) as {
479
- tier: string | null;
479
+ tier: string | undefined;
480
480
  channel: string;
481
+ tierChain: string[];
482
+ includedReleases: string[];
481
483
  };
482
484
  expect(data.channel).toBe("normal");
483
- expect(data.tier).toBeNull();
485
+ expect(data.tier).toBeUndefined();
486
+ expect(data.tierChain).toEqual([]);
487
+ expect(data.includedReleases).toContain("stable");
484
488
  });
485
489
 
486
490
  it("returns condensed config text", async () => {
@@ -491,7 +495,8 @@ describe("config_show", () => {
491
495
  const envelope = parseEnvelope(result);
492
496
  expect(envelope.ok).toBe(true);
493
497
  expect(envelope.condensed).toBe(true);
494
- expect(envelope.text).toBe("Config: tier=(none) channel=normal");
498
+ expect(typeof envelope.text).toBe("string");
499
+ expect(envelope.text as string).toContain("Configuration");
495
500
  });
496
501
  });
497
502
 
@@ -7,6 +7,4 @@ interface InstallSource {
7
7
  readonly label: string;
8
8
  }
9
9
 
10
- export type { PackageManager };
11
-
12
- export type { InstallSource };
10
+ export type { InstallSource, PackageManager };
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  type CommandDefinition,
3
+ detectRenderMode,
3
4
  formatHelp,
4
5
  formatLlmHelp,
6
+ type HandleResultOptions,
5
7
  registerAll,
6
8
  } from "@canonical/cli-core";
7
9
  import { Command } from "commander";
@@ -35,6 +37,9 @@ export default function createProgram(
35
37
  writeErr: (str) => process.stderr.write(str),
36
38
  });
37
39
 
40
+ // Global flags (--llm, --format, --verbose) are pre-parsed by
41
+ // parseGlobalFlags() and stripped from argv before Commander sees it.
42
+ // Declaring them here keeps them visible in --help output.
38
43
  program.option(
39
44
  "--llm",
40
45
  "Condensed Markdown output for LLM consumption",
@@ -42,6 +47,8 @@ export default function createProgram(
42
47
  );
43
48
  program.option("--format <type>", "Output format (text or json)", "text");
44
49
  program.option("--verbose", "Diagnostic output to stderr", false);
50
+ // Commander will never see these flags in argv (they're stripped),
51
+ // so enablePositionalOptions() scoping can't reject them.
45
52
 
46
53
  program.addHelpText("beforeAll", (_ctx) => {
47
54
  // Only show root-level help when the help is for the root program itself.
@@ -64,7 +71,19 @@ export default function createProgram(
64
71
  await runMcpServer();
65
72
  });
66
73
 
67
- registerAll(program, commands, ctx);
74
+ const mode = detectRenderMode(ctx.globalFlags);
75
+ const resultOptions: HandleResultOptions = {
76
+ mode,
77
+ renderInk:
78
+ mode === "ink"
79
+ ? async (element: unknown) => {
80
+ const { renderInk } = await import("#tui");
81
+ await renderInk(element as import("react").ReactElement);
82
+ }
83
+ : undefined,
84
+ };
85
+
86
+ registerAll(program, commands, ctx, resultOptions);
68
87
 
69
88
  return program;
70
89
  }
@@ -8,7 +8,10 @@ export {
8
8
  formatSection,
9
9
  } from "./formatTerminal.js";
10
10
  export { default as mapExitCode } from "./mapExitCode.js";
11
- export { default as parseGlobalFlags } from "./parseGlobalFlags.js";
11
+ export {
12
+ default as parseGlobalFlags,
13
+ stripGlobalFlags,
14
+ } from "./parseGlobalFlags.js";
12
15
  export {
13
16
  renderErrorJson,
14
17
  renderErrorLlm,
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import parseGlobalFlags, { stripGlobalFlags } from "./parseGlobalFlags.js";
3
+
4
+ describe("parseGlobalFlags", () => {
5
+ it("extracts --llm", () => {
6
+ const flags = parseGlobalFlags([
7
+ "node",
8
+ "pragma",
9
+ "block",
10
+ "list",
11
+ "--llm",
12
+ ]);
13
+ expect(flags.llm).toBe(true);
14
+ });
15
+
16
+ it("extracts --format json", () => {
17
+ const flags = parseGlobalFlags([
18
+ "node",
19
+ "pragma",
20
+ "--format",
21
+ "json",
22
+ "info",
23
+ ]);
24
+ expect(flags.format).toBe("json");
25
+ });
26
+
27
+ it("defaults to text when --format is absent", () => {
28
+ const flags = parseGlobalFlags(["node", "pragma", "info"]);
29
+ expect(flags.format).toBe("text");
30
+ });
31
+
32
+ it("extracts --verbose", () => {
33
+ const flags = parseGlobalFlags([
34
+ "node",
35
+ "pragma",
36
+ "--verbose",
37
+ "block",
38
+ "list",
39
+ ]);
40
+ expect(flags.verbose).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe("stripGlobalFlags", () => {
45
+ it("removes --llm from any position", () => {
46
+ expect(
47
+ stripGlobalFlags(["node", "pragma", "--llm", "tier", "list"]),
48
+ ).toEqual(["node", "pragma", "tier", "list"]);
49
+ expect(
50
+ stripGlobalFlags(["node", "pragma", "tier", "list", "--llm"]),
51
+ ).toEqual(["node", "pragma", "tier", "list"]);
52
+ });
53
+
54
+ it("removes --format and its value", () => {
55
+ expect(
56
+ stripGlobalFlags(["node", "pragma", "--format", "json", "info"]),
57
+ ).toEqual(["node", "pragma", "info"]);
58
+ expect(
59
+ stripGlobalFlags(["node", "pragma", "info", "--format", "json"]),
60
+ ).toEqual(["node", "pragma", "info"]);
61
+ });
62
+
63
+ it("removes --verbose from any position", () => {
64
+ expect(
65
+ stripGlobalFlags(["node", "pragma", "block", "list", "--verbose"]),
66
+ ).toEqual(["node", "pragma", "block", "list"]);
67
+ });
68
+
69
+ it("removes all global flags at once", () => {
70
+ expect(
71
+ stripGlobalFlags([
72
+ "node",
73
+ "pragma",
74
+ "--llm",
75
+ "--verbose",
76
+ "--format",
77
+ "json",
78
+ "block",
79
+ "list",
80
+ ]),
81
+ ).toEqual(["node", "pragma", "block", "list"]);
82
+ });
83
+
84
+ it("preserves argv when no global flags present", () => {
85
+ expect(stripGlobalFlags(["node", "pragma", "block", "list"])).toEqual([
86
+ "node",
87
+ "pragma",
88
+ "block",
89
+ "list",
90
+ ]);
91
+ });
92
+ });
@@ -19,3 +19,28 @@ export default function parseGlobalFlags(argv: readonly string[]): GlobalFlags {
19
19
  verbose: argv.includes("--verbose"),
20
20
  };
21
21
  }
22
+
23
+ /**
24
+ * Return a copy of argv with global flags stripped out.
25
+ *
26
+ * Since {@link parseGlobalFlags} pre-parses these flags before Commander
27
+ * runs, we remove them so they don't collide with Commander's
28
+ * `enablePositionalOptions()` scoping. This lets users place `--llm`,
29
+ * `--format`, and `--verbose` anywhere in the command line.
30
+ *
31
+ * @param argv - Raw process.argv array.
32
+ * @returns New array with global flags removed.
33
+ */
34
+ export function stripGlobalFlags(argv: readonly string[]): string[] {
35
+ const result: string[] = [];
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const arg = argv[i];
38
+ if (arg === "--llm" || arg === "--verbose") continue;
39
+ if (arg === "--format") {
40
+ i += 1; // skip the value too
41
+ continue;
42
+ }
43
+ result.push(arg as string);
44
+ }
45
+ return result;
46
+ }
@@ -32,6 +32,7 @@ vi.mock("./mapExitCode.js", () => ({
32
32
 
33
33
  vi.mock("./parseGlobalFlags.js", () => ({
34
34
  default: parseGlobalFlagsMock,
35
+ stripGlobalFlags: (argv: readonly string[]) => [...argv],
35
36
  }));
36
37
 
37
38
  vi.mock("./resolveCommandKind.js", () => ({
@@ -8,13 +8,14 @@ import { PragmaError } from "../error/index.js";
8
8
  import collectCommands from "./collectCommands.js";
9
9
  import createProgram from "./createProgram.js";
10
10
  import mapExitCode from "./mapExitCode.js";
11
- import parseGlobalFlags from "./parseGlobalFlags.js";
11
+ import parseGlobalFlags, { stripGlobalFlags } from "./parseGlobalFlags.js";
12
12
  import {
13
13
  renderErrorJson,
14
14
  renderErrorLlm,
15
15
  renderErrorPlain,
16
16
  } from "./renderError.js";
17
17
  import resolveCommandKind from "./resolveCommandKind.js";
18
+ import runInteractiveCommand from "./runInteractiveCommand.js";
18
19
 
19
20
  function hasCommandArg(argv: readonly string[]): boolean {
20
21
  const args = argv.slice(2);
@@ -78,6 +79,7 @@ async function handleRootHelp(globalFlags: GlobalFlags): Promise<void> {
78
79
  cwd: process.cwd(),
79
80
  dispose: () => {},
80
81
  globalFlags,
82
+ interactive: runInteractiveCommand,
81
83
  };
82
84
  const commands = collectCommands(stubCtx);
83
85
  const program = createProgram(commands, stubCtx);
@@ -111,10 +113,14 @@ async function bootAndRun(
111
113
  }
112
114
 
113
115
  try {
114
- const ctx: PragmaContext = { ...runtime, globalFlags };
116
+ const ctx: PragmaContext = {
117
+ ...runtime,
118
+ globalFlags,
119
+ interactive: runInteractiveCommand,
120
+ };
115
121
  const commands = collectCommands(ctx);
116
122
  const program = createProgram(commands, ctx);
117
- await program.parseAsync(argv);
123
+ await program.parseAsync(stripGlobalFlags(argv));
118
124
  } catch (err) {
119
125
  handleProgramError(err, globalFlags);
120
126
  } finally {
@@ -132,12 +138,13 @@ async function runStoreSkip(
132
138
  cwd: process.cwd(),
133
139
  dispose: () => {},
134
140
  globalFlags,
141
+ interactive: runInteractiveCommand,
135
142
  };
136
143
  const commands = [...setupCommands()];
137
144
  const program = createProgram(commands, stubCtx);
138
145
 
139
146
  try {
140
- await program.parseAsync(argv);
147
+ await program.parseAsync(stripGlobalFlags(argv));
141
148
  } catch (err) {
142
149
  handleProgramError(err, globalFlags);
143
150
  }