@canonical/pragma-cli 0.27.1-experimental.0 → 0.29.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 (169) hide show
  1. package/dist/pragma +0 -0
  2. package/package.json +16 -17
  3. package/src/config/readConfig.ts +38 -2
  4. package/src/config/types.ts +12 -1
  5. package/src/config/writeConfig.ts +12 -0
  6. package/src/constants.ts +12 -1
  7. package/src/domains/block/commands/index.ts +2 -1
  8. package/src/domains/block/commands/sample.ts +55 -0
  9. package/src/domains/block/formatters/index.ts +2 -1
  10. package/src/domains/block/formatters/sample.ts +21 -0
  11. package/src/domains/block/index.ts +3 -3
  12. package/src/domains/block/mcp/specs.ts +48 -0
  13. package/src/domains/block/operations/index.ts +2 -1
  14. package/src/domains/block/operations/lookup.ts +9 -0
  15. package/src/domains/block/operations/sample.ts +41 -0
  16. package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
  17. package/src/domains/block/orchestration/resolveBlockLookup.ts +10 -3
  18. package/src/domains/config/commands/framework.test.ts +88 -0
  19. package/src/domains/config/commands/framework.ts +85 -0
  20. package/src/domains/config/commands/index.ts +2 -0
  21. package/src/domains/config/commands/trace.test.ts +148 -0
  22. package/src/domains/config/commands/trace.ts +80 -0
  23. package/src/domains/config/formatters/framework.ts +34 -0
  24. package/src/domains/config/formatters/index.ts +2 -0
  25. package/src/domains/config/formatters/trace.ts +28 -0
  26. package/src/domains/config/index.ts +15 -2
  27. package/src/domains/config/operations/index.ts +1 -0
  28. package/src/domains/config/operations/validateFramework.ts +24 -0
  29. package/src/domains/config/tasks/index.ts +2 -0
  30. package/src/domains/config/tasks/setChannel.ts +7 -1
  31. package/src/domains/config/tasks/setFramework.ts +40 -0
  32. package/src/domains/config/tasks/setTier.ts +7 -1
  33. package/src/domains/config/tasks/setTrace.ts +38 -0
  34. package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
  35. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +35 -41
  36. package/src/domains/doctor/operations/checks.test.ts +4 -1
  37. package/src/domains/doctor/operations/runChecks.test.ts +72 -0
  38. package/src/domains/doctor/operations/runChecks.ts +48 -12
  39. package/src/domains/graphql/commands/build.test.ts +194 -0
  40. package/src/domains/graphql/commands/build.ts +125 -0
  41. package/src/domains/graphql/commands/check.test.ts +139 -0
  42. package/src/domains/graphql/commands/check.ts +87 -0
  43. package/src/domains/graphql/commands/index.ts +4 -0
  44. package/src/domains/graphql/formatters/index.ts +3 -0
  45. package/src/domains/graphql/formatters/report.ts +78 -0
  46. package/src/domains/graphql/helpers/index.ts +4 -0
  47. package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
  48. package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
  49. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
  50. package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
  51. package/src/domains/graphql/index.ts +21 -0
  52. package/src/domains/graphql/operations/compileSchema.test.ts +119 -0
  53. package/src/domains/graphql/operations/compileSchema.ts +109 -0
  54. package/src/domains/graphql/operations/index.ts +7 -0
  55. package/src/domains/graphql/types.ts +29 -0
  56. package/src/domains/info/operations/collectInfo.test.ts +2 -2
  57. package/src/domains/info/operations/collectInfo.ts +4 -4
  58. package/src/domains/llm/capabilities.test.ts +91 -0
  59. package/src/domains/llm/commands/capabilities.ts +28 -0
  60. package/src/domains/llm/commands/index.ts +1 -0
  61. package/src/domains/llm/data/commandReference.ts +90 -18
  62. package/src/domains/llm/data/decisionTrees.ts +15 -13
  63. package/src/domains/llm/data/index.ts +2 -0
  64. package/src/domains/llm/data/toolCatalog.ts +184 -0
  65. package/src/domains/llm/formatters/capabilities.ts +50 -0
  66. package/src/domains/llm/formatters/index.ts +1 -0
  67. package/src/domains/llm/index.ts +3 -2
  68. package/src/domains/llm/mcp/index.ts +1 -1
  69. package/src/domains/llm/mcp/specs.ts +59 -52
  70. package/src/domains/llm/types.ts +40 -0
  71. package/src/domains/modifier/commands/index.ts +2 -1
  72. package/src/domains/modifier/commands/sample.ts +53 -0
  73. package/src/domains/modifier/formatters/index.ts +2 -1
  74. package/src/domains/modifier/formatters/sample.ts +15 -0
  75. package/src/domains/modifier/index.ts +7 -3
  76. package/src/domains/modifier/mcp/specs.ts +47 -0
  77. package/src/domains/modifier/operations/index.ts +2 -1
  78. package/src/domains/modifier/operations/lookup.ts +9 -0
  79. package/src/domains/modifier/operations/sample.ts +32 -0
  80. package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
  81. package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
  82. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +14 -2
  83. package/src/domains/refs/operations/index.ts +1 -1
  84. package/src/domains/refs/operations/paths.ts +29 -1
  85. package/src/domains/refs/operations/updateRefs.test.ts +6 -3
  86. package/src/domains/refs/operations/updateRefs.ts +3 -2
  87. package/src/domains/shared/bootStore.ts +88 -49
  88. package/src/domains/shared/contracts.ts +1 -0
  89. package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
  90. package/src/domains/shared/loaders/bundledLoader.ts +108 -0
  91. package/src/domains/shared/loaders/gitLoader.ts +31 -0
  92. package/src/domains/shared/loaders/index.ts +6 -0
  93. package/src/domains/shared/loaders/localLoader.ts +73 -0
  94. package/src/domains/shared/loaders/readPackageDir.ts +112 -0
  95. package/src/domains/shared/lookupMany.ts +9 -1
  96. package/src/domains/shared/packages.ts +21 -120
  97. package/src/domains/shared/pickRandom.ts +22 -0
  98. package/src/domains/shared/runtime.ts +6 -3
  99. package/src/domains/shared/sampleFormatters.ts +47 -0
  100. package/src/domains/shared/semanticPackage.ts +96 -0
  101. package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
  102. package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
  103. package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
  104. package/src/domains/shared/suggestions/expandGlob.ts +51 -0
  105. package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
  106. package/src/domains/shared/suggestions/index.ts +7 -0
  107. package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
  108. package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
  109. package/src/domains/shared/suggestions/suggestNames.ts +97 -0
  110. package/src/domains/shared/types/index.ts +1 -0
  111. package/src/domains/shared/types/runtime.ts +4 -1
  112. package/src/domains/shared/types/sample.ts +17 -0
  113. package/src/domains/skill/helpers/resolveSkillSources.ts +49 -16
  114. package/src/domains/standard/commands/index.ts +2 -1
  115. package/src/domains/standard/commands/sample.ts +57 -0
  116. package/src/domains/standard/formatters/index.ts +2 -1
  117. package/src/domains/standard/formatters/sample.ts +15 -0
  118. package/src/domains/standard/index.ts +8 -1
  119. package/src/domains/standard/mcp/specs.ts +48 -1
  120. package/src/domains/standard/operations/index.ts +2 -1
  121. package/src/domains/standard/operations/lookup.ts +9 -0
  122. package/src/domains/standard/operations/sample.ts +35 -0
  123. package/src/domains/standard/orchestration/resolveStandardLookup.ts +14 -2
  124. package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
  125. package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
  126. package/src/domains/token/commands/index.ts +2 -1
  127. package/src/domains/token/commands/sample.ts +53 -0
  128. package/src/domains/token/formatters/index.ts +2 -1
  129. package/src/domains/token/formatters/sample.ts +16 -0
  130. package/src/domains/token/index.ts +8 -1
  131. package/src/domains/token/mcp/specs.ts +47 -1
  132. package/src/domains/token/operations/index.ts +2 -1
  133. package/src/domains/token/operations/lookup.ts +9 -0
  134. package/src/domains/token/operations/sample.ts +32 -0
  135. package/src/domains/token/orchestration/resolveTokenLookup.ts +14 -2
  136. package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
  137. package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
  138. package/src/domains/trace/commands/clear.ts +71 -0
  139. package/src/domains/trace/commands/index.ts +5 -0
  140. package/src/domains/trace/commands/sessions.ts +32 -0
  141. package/src/domains/trace/commands/show.ts +145 -0
  142. package/src/domains/trace/formatters/index.ts +9 -0
  143. package/src/domains/trace/formatters/sessions.ts +80 -0
  144. package/src/domains/trace/formatters/show.ts +122 -0
  145. package/src/domains/trace/index.ts +17 -0
  146. package/src/domains/trace/operations/index.ts +8 -0
  147. package/src/domains/trace/operations/listSessions.test.ts +55 -0
  148. package/src/domains/trace/operations/listSessions.ts +52 -0
  149. package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
  150. package/src/domains/trace/operations/readTraceLog.ts +104 -0
  151. package/src/domains/trace/tracePlugin.test.ts +171 -0
  152. package/src/domains/trace/tracePlugin.ts +159 -0
  153. package/src/domains/trace/types.ts +65 -0
  154. package/src/error/PragmaError.ts +15 -2
  155. package/src/error/types.ts +15 -1
  156. package/src/mcp/error/serializeErrorPayload.ts +7 -0
  157. package/src/mcp/error/types.ts +8 -0
  158. package/src/mcp/registerResources.test.ts +38 -16
  159. package/src/mcp/registerResources.ts +57 -8
  160. package/src/mcp/tools/registerTools.test.ts +22 -7
  161. package/src/mcp/utils/wrapTool.test.ts +1 -0
  162. package/src/pipeline/collectCommands.ts +7 -1
  163. package/src/pipeline/renderError.ts +15 -0
  164. package/src/pipeline/resolveCommandKind.ts +8 -1
  165. package/src/pipeline/runCli.ts +12 -1
  166. package/src/testing/createTestMcpClient.ts +1 -0
  167. package/src/testing/graphqlFixtures.ts +72 -0
  168. package/src/testing/index.ts +7 -0
  169. package/src/testing/integration/mcp-surface.test.ts +11 -5
@@ -0,0 +1,85 @@
1
+ import {
2
+ type CommandDefinition,
3
+ type CommandResult,
4
+ createOutputResult,
5
+ } from "@canonical/cli-core";
6
+ import { runTask } from "@canonical/task";
7
+ import { readConfig } from "#config";
8
+ import type { PragmaContext } from "../../shared/context.js";
9
+ import { selectFormatter } from "../../shared/formatters.js";
10
+ import { frameworkFormatters } from "../formatters/index.js";
11
+ import { validateFramework } from "../operations/index.js";
12
+ import { setFrameworkTask } from "../tasks/index.js";
13
+
14
+ /**
15
+ * Build the `pragma config framework` command definition.
16
+ *
17
+ * Sets the preferred UI framework (`react` | `svelte` | `lit`). The value is
18
+ * currently advisory only — no behaviour reads it yet; it is persisted for
19
+ * future framework-defaulting. Supports set, reset, and query (no argument).
20
+ *
21
+ * @param ctx - Pragma context providing cwd and formatter selection.
22
+ * @returns The command definition for `pragma config framework`.
23
+ * @note - Impure — validates framework and writes config to disk.
24
+ */
25
+ export default function buildFrameworkCommand(
26
+ ctx: PragmaContext,
27
+ ): CommandDefinition {
28
+ return {
29
+ path: ["config", "framework"],
30
+ description: "Set or reset the preferred UI framework (advisory)",
31
+ parameters: [
32
+ {
33
+ name: "value",
34
+ description: "Framework (react, svelte, lit)",
35
+ type: "string",
36
+ positional: true,
37
+ },
38
+ {
39
+ name: "reset",
40
+ description: "Reset framework to default (none)",
41
+ type: "boolean",
42
+ default: false,
43
+ },
44
+ ],
45
+ meta: {
46
+ examples: [
47
+ "pragma config framework react",
48
+ "pragma config framework --reset",
49
+ ],
50
+ extendedHelp:
51
+ "Advisory only for now: the value is persisted but no command reads it yet. Reserved for future framework defaulting.",
52
+ },
53
+ execute: async (
54
+ params: Record<string, unknown>,
55
+ ): Promise<CommandResult> => {
56
+ const reset = params.reset === true;
57
+ const value = params.value as string | undefined;
58
+
59
+ if (reset) {
60
+ await runTask(setFrameworkTask(ctx.cwd, undefined));
61
+ const format = selectFormatter(ctx, frameworkFormatters.reset);
62
+ return createOutputResult("Reset framework to default.", {
63
+ plain: format,
64
+ });
65
+ }
66
+
67
+ if (!value) {
68
+ const config = readConfig(ctx.cwd);
69
+ const format = selectFormatter(ctx, frameworkFormatters.query);
70
+ return createOutputResult(config.framework ?? "none", {
71
+ plain: format,
72
+ });
73
+ }
74
+
75
+ const framework = validateFramework(value);
76
+
77
+ await runTask(setFrameworkTask(ctx.cwd, framework));
78
+ const format = selectFormatter(ctx, frameworkFormatters.set);
79
+ return createOutputResult(
80
+ { field: "framework", value: framework },
81
+ { plain: format },
82
+ );
83
+ },
84
+ };
85
+ }
@@ -1,4 +1,6 @@
1
1
  /** @module Config command definitions for `pragma config`. */
2
2
  export { default as channelCommand } from "./channel.js";
3
+ export { default as frameworkCommand } from "./framework.js";
3
4
  export { default as showCommand } from "./show.js";
4
5
  export { default as tierCommand } from "./tier.js";
6
+ export { default as traceCommand } from "./trace.js";
@@ -0,0 +1,148 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { Store } from "@canonical/ke";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { readConfig } from "#config";
7
+ import type { PragmaContext } from "../../shared/context.js";
8
+ import buildTraceCommand from "./trace.js";
9
+
10
+ function makeCtx(cwd: string): PragmaContext {
11
+ return {
12
+ cwd,
13
+ globalFlags: { llm: false, format: "text" as const, verbose: false },
14
+ store: {} as Store,
15
+ config: { tier: undefined, channel: "normal" },
16
+ };
17
+ }
18
+
19
+ describe("config trace command", () => {
20
+ let dir: string;
21
+
22
+ beforeEach(() => {
23
+ dir = mkdtempSync(join(tmpdir(), "pragma-cmd-trace-"));
24
+ });
25
+
26
+ afterEach(() => {
27
+ rmSync(dir, { recursive: true, force: true });
28
+ });
29
+
30
+ it("enables tracing", async () => {
31
+ const ctx = makeCtx(dir);
32
+ const cmd = buildTraceCommand(ctx);
33
+ const result = await cmd.execute({ value: "on" }, ctx);
34
+
35
+ expect(result.tag).toBe("output");
36
+ expect(readConfig(dir).trace).toBe(true);
37
+ });
38
+
39
+ it("disables tracing", async () => {
40
+ writeFileSync(join(dir, "pragma.config.json"), '{"trace":true}');
41
+
42
+ const ctx = makeCtx(dir);
43
+ const cmd = buildTraceCommand(ctx);
44
+ const result = await cmd.execute({ value: "off" }, ctx);
45
+
46
+ expect(result.tag).toBe("output");
47
+ expect(readConfig(dir).trace).toBe(false);
48
+ });
49
+
50
+ it("renders enable output", async () => {
51
+ const ctx = makeCtx(dir);
52
+ const cmd = buildTraceCommand(ctx);
53
+ const result = await cmd.execute({ value: "on" }, ctx);
54
+
55
+ expect(result.tag).toBe("output");
56
+ if (result.tag === "output") {
57
+ expect(result.render.plain(result.value)).toBe("Tracing enabled.");
58
+ }
59
+ });
60
+
61
+ it("shows current status when no argument", async () => {
62
+ writeFileSync(join(dir, "pragma.config.json"), '{"trace":true}');
63
+
64
+ const ctx = makeCtx(dir);
65
+ const cmd = buildTraceCommand(ctx);
66
+ const result = await cmd.execute({}, ctx);
67
+
68
+ expect(result.tag).toBe("output");
69
+ if (result.tag === "output") {
70
+ expect(result.value).toBe("on");
71
+ }
72
+ });
73
+
74
+ it("reports off when unset", async () => {
75
+ const ctx = makeCtx(dir);
76
+ const cmd = buildTraceCommand(ctx);
77
+ const result = await cmd.execute({}, ctx);
78
+
79
+ expect(result.tag).toBe("output");
80
+ if (result.tag === "output") {
81
+ expect(result.value).toBe("off");
82
+ }
83
+ });
84
+
85
+ it("throws INVALID_INPUT for an invalid value", async () => {
86
+ const ctx = makeCtx(dir);
87
+ const cmd = buildTraceCommand(ctx);
88
+ await expect(cmd.execute({ value: "yes" }, ctx)).rejects.toThrow(/trace/i);
89
+ });
90
+
91
+ // Regression guard for the data-loss bug: enabling/disabling trace must NOT
92
+ // drop other persisted config fields. `packages` is runtime-consumed, so
93
+ // silently discarding it on a trace toggle would break package resolution.
94
+ it("preserves the packages field when toggling trace", async () => {
95
+ writeFileSync(
96
+ join(dir, "pragma.config.json"),
97
+ JSON.stringify({
98
+ tier: "Apps/WPE",
99
+ channel: "experimental",
100
+ packages: [
101
+ "@canonical/design-system",
102
+ {
103
+ name: "@canonical/anatomy-dsl",
104
+ source: "git+https://github.com/canonical/anatomy-dsl.git#main",
105
+ },
106
+ ],
107
+ }),
108
+ );
109
+
110
+ const ctx = makeCtx(dir);
111
+ const cmd = buildTraceCommand(ctx);
112
+ await cmd.execute({ value: "on" }, ctx);
113
+
114
+ const config = readConfig(dir);
115
+ expect(config.trace).toBe(true);
116
+ // Every pre-existing field survives the write.
117
+ expect(config.tier).toBe("Apps/WPE");
118
+ expect(config.channel).toBe("experimental");
119
+ expect(config.packages).toEqual([
120
+ "@canonical/design-system",
121
+ {
122
+ name: "@canonical/anatomy-dsl",
123
+ source: "git+https://github.com/canonical/anatomy-dsl.git#main",
124
+ },
125
+ ]);
126
+
127
+ // And toggling off again still preserves them.
128
+ await cmd.execute({ value: "off" }, ctx);
129
+ const after = readConfig(dir);
130
+ expect(after.trace).toBe(false);
131
+ expect(after.packages).toHaveLength(2);
132
+ });
133
+
134
+ it("does not persist the default channel when toggling trace", async () => {
135
+ // A bare `trace on` on an empty config should not write channel:"normal"
136
+ // (the default), keeping the file minimal per the existing convention.
137
+ const ctx = makeCtx(dir);
138
+ const cmd = buildTraceCommand(ctx);
139
+ await cmd.execute({ value: "on" }, ctx);
140
+
141
+ const raw = JSON.parse(
142
+ readFileSync(join(dir, "pragma.config.json"), "utf-8"),
143
+ ) as Record<string, unknown>;
144
+ expect(raw.trace).toBe(true);
145
+ expect(raw).not.toHaveProperty("channel");
146
+ expect(raw).not.toHaveProperty("tier");
147
+ });
148
+ });
@@ -0,0 +1,80 @@
1
+ import {
2
+ type CommandDefinition,
3
+ type CommandResult,
4
+ createOutputResult,
5
+ } from "@canonical/cli-core";
6
+ import { runTask } from "@canonical/task";
7
+ import { readConfig } from "#config";
8
+ import { PragmaError } from "#error";
9
+ import type { PragmaContext } from "../../shared/context.js";
10
+ import { selectFormatter } from "../../shared/formatters.js";
11
+ import { traceFormatters } from "../formatters/index.js";
12
+ import { setTraceTask } from "../tasks/index.js";
13
+
14
+ /**
15
+ * Build the `pragma config trace` command definition.
16
+ *
17
+ * Supports three modes: enable (`on`), disable (`off`), or query
18
+ * when no argument is provided.
19
+ */
20
+ export default function buildTraceCommand(
21
+ ctx: PragmaContext,
22
+ ): CommandDefinition {
23
+ return {
24
+ path: ["config", "trace"],
25
+ description: "Enable or disable query access tracing",
26
+ parameters: [
27
+ {
28
+ name: "value",
29
+ description: "on or off",
30
+ type: "string",
31
+ positional: true,
32
+ },
33
+ ],
34
+ meta: {
35
+ examples: [
36
+ "pragma config trace on",
37
+ "pragma config trace off",
38
+ "pragma config trace",
39
+ ],
40
+ extendedHelp:
41
+ "Persists to pragma.config.json. The PRAGMA_TRACE=1 env var overrides this setting.",
42
+ },
43
+ execute: async (
44
+ params: Record<string, unknown>,
45
+ ): Promise<CommandResult> => {
46
+ const value = params.value as string | undefined;
47
+
48
+ // Query mode — no argument
49
+ if (!value) {
50
+ const config = readConfig(ctx.cwd);
51
+ const enabled = config.trace === true;
52
+ const envOverride = process.env.PRAGMA_TRACE === "1";
53
+ const status = envOverride
54
+ ? "on (PRAGMA_TRACE=1 override)"
55
+ : enabled
56
+ ? "on"
57
+ : "off";
58
+ const format = selectFormatter(ctx, traceFormatters.query);
59
+ return createOutputResult(status, { plain: format });
60
+ }
61
+
62
+ // Validate
63
+ if (value !== "on" && value !== "off") {
64
+ throw PragmaError.invalidInput("trace", value, {
65
+ validOptions: ["on", "off"],
66
+ });
67
+ }
68
+
69
+ // Set mode
70
+ const enabled = value === "on";
71
+ await runTask(setTraceTask(ctx.cwd, enabled));
72
+
73
+ const format = selectFormatter(ctx, traceFormatters.set);
74
+ return createOutputResult(
75
+ { field: "trace", value: enabled },
76
+ { plain: format },
77
+ );
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,34 @@
1
+ import type { Formatters } from "../../shared/formatters.js";
2
+
3
+ /**
4
+ * Formatter sets for `pragma config framework` output, grouped by branch:
5
+ *
6
+ * - **set** formats the confirmation after setting a framework.
7
+ * - **reset** formats the confirmation after resetting to default.
8
+ * - **query** formats the current framework value (or "none").
9
+ */
10
+ const frameworkFormatters = {
11
+ set: {
12
+ plain: (d: { field: string; value: string }) =>
13
+ `Set ${d.field} to "${d.value}".`,
14
+ llm: (d: { field: string; value: string }) =>
15
+ `Set ${d.field} to "${d.value}".`,
16
+ json: (d: { field: string; value: string }) =>
17
+ JSON.stringify({ field: d.field, value: d.value }),
18
+ } satisfies Formatters<{ field: string; value: string }>,
19
+
20
+ reset: {
21
+ plain: () => "Reset framework to default.",
22
+ llm: () => "Reset framework to default.",
23
+ json: () => JSON.stringify({ field: "framework", reset: true }),
24
+ } satisfies Formatters<string>,
25
+
26
+ query: {
27
+ plain: (framework: string) => `Current framework: ${framework}`,
28
+ llm: (framework: string) => `Current framework: ${framework}`,
29
+ json: (framework: string) =>
30
+ JSON.stringify({ field: "framework", value: framework }),
31
+ } satisfies Formatters<string>,
32
+ };
33
+
34
+ export default frameworkFormatters;
@@ -1,4 +1,6 @@
1
1
  /** @module Formatter sets for config domain output. */
2
2
  export { default as channelFormatters } from "./channel.js";
3
+ export { default as frameworkFormatters } from "./framework.js";
3
4
  export { default as showFormatters } from "./show.js";
4
5
  export { default as tierFormatters } from "./tier.js";
6
+ export { default as traceFormatters } from "./trace.js";
@@ -0,0 +1,28 @@
1
+ import type { Formatters } from "../../shared/formatters.js";
2
+
3
+ /**
4
+ * Formatter sets for `pragma config trace` output, grouped by branch:
5
+ *
6
+ * - **set** formats the confirmation after enabling/disabling.
7
+ * - **query** formats the current trace status.
8
+ */
9
+ const traceFormatters = {
10
+ set: {
11
+ plain: (d: { field: string; value: boolean }) =>
12
+ d.value ? "Tracing enabled." : "Tracing disabled.",
13
+ llm: (d: { field: string; value: boolean }) =>
14
+ d.value ? "Tracing enabled." : "Tracing disabled.",
15
+ json: (d: { field: string; value: boolean }) =>
16
+ JSON.stringify({ field: d.field, value: d.value }),
17
+ } satisfies Formatters<{ field: string; value: boolean }>,
18
+
19
+ query: {
20
+ plain: (status: string) => status,
21
+ llm: (status: string) => status,
22
+ // Emit the human-readable status under `status`, not `value`, so the JSON
23
+ // shape stays consistent with `set` (where `value` is the boolean state).
24
+ json: (status: string) => JSON.stringify({ field: "trace", status }),
25
+ } satisfies Formatters<string>,
26
+ };
27
+
28
+ export default traceFormatters;
@@ -2,7 +2,13 @@
2
2
 
3
3
  import type { CommandDefinition } from "@canonical/cli-core";
4
4
  import type { PragmaContext } from "../shared/context.js";
5
- import { channelCommand, showCommand, tierCommand } from "./commands/index.js";
5
+ import {
6
+ channelCommand,
7
+ frameworkCommand,
8
+ showCommand,
9
+ tierCommand,
10
+ traceCommand,
11
+ } from "./commands/index.js";
6
12
 
7
13
  /**
8
14
  * Return all config command definitions.
@@ -11,7 +17,13 @@ import { channelCommand, showCommand, tierCommand } from "./commands/index.js";
11
17
  * @returns An array of CommandDefinitions for the config subcommands.
12
18
  */
13
19
  export function commands(ctx: PragmaContext): CommandDefinition[] {
14
- return [tierCommand(ctx), channelCommand(ctx), showCommand(ctx)];
20
+ return [
21
+ tierCommand(ctx),
22
+ channelCommand(ctx),
23
+ traceCommand(ctx),
24
+ frameworkCommand(ctx),
25
+ showCommand(ctx),
26
+ ];
15
27
  }
16
28
 
17
29
  export { specs as mcpSpecs } from "./mcp/index.js";
@@ -19,5 +31,6 @@ export type { ConfigShowData } from "./operations/index.js";
19
31
  export {
20
32
  resolveConfigShow,
21
33
  validateChannel,
34
+ validateFramework,
22
35
  validateTier,
23
36
  } from "./operations/index.js";
@@ -2,4 +2,5 @@
2
2
  export { default as resolveConfigShow } from "./show.js";
3
3
  export type { ConfigShowData } from "./types.js";
4
4
  export { default as validateChannel } from "./validateChannel.js";
5
+ export { default as validateFramework } from "./validateFramework.js";
5
6
  export { default as validateTier } from "./validateTier.js";
@@ -0,0 +1,24 @@
1
+ import { type Framework, VALID_FRAMEWORKS } from "#constants";
2
+ import { PragmaError } from "#error";
3
+
4
+ /**
5
+ * Validate that a string is one of the accepted framework values.
6
+ *
7
+ * @param value - The framework string to validate.
8
+ * @returns The validated Framework value.
9
+ * @throws PragmaError.invalidInput if the value is not a valid framework.
10
+ */
11
+ export default function validateFramework(value: string): Framework {
12
+ if (VALID_FRAMEWORKS.includes(value as Framework)) {
13
+ return value as Framework;
14
+ }
15
+
16
+ throw PragmaError.invalidInput("framework", value, {
17
+ validOptions: [...VALID_FRAMEWORKS],
18
+ recovery: {
19
+ message: "Reset framework configuration.",
20
+ cli: "pragma config framework --reset",
21
+ mcp: { tool: "config_framework", params: { reset: true } },
22
+ },
23
+ });
24
+ }
@@ -1,3 +1,5 @@
1
1
  /** @module Config write tasks. */
2
2
  export { default as setChannelTask } from "./setChannel.js";
3
+ export { default as setFrameworkTask } from "./setFramework.js";
3
4
  export { default as setTierTask } from "./setTier.js";
5
+ export { default as setTraceTask } from "./setTrace.js";
@@ -15,9 +15,15 @@ export default function setChannel(
15
15
  channel: string | undefined,
16
16
  ): Task<{ field: "channel"; value: string | undefined }> {
17
17
  return gen(function* () {
18
+ // Merge onto the full existing config so other fields (e.g. `packages`,
19
+ // `trace`) survive. `readConfig` normalises defaults (tier: undefined,
20
+ // channel: "normal"); strip those to keep the file minimal, then apply
21
+ // the channel change (`undefined` resets it).
18
22
  const current = readConfig(cwd);
23
+ const { tier, channel: _current, ...rest } = current;
19
24
  const next = {
20
- ...(current.tier !== undefined ? { tier: current.tier } : {}),
25
+ ...rest,
26
+ ...(tier !== undefined ? { tier } : {}),
21
27
  ...(channel !== undefined ? { channel } : {}),
22
28
  };
23
29
 
@@ -0,0 +1,40 @@
1
+ import { $, gen, type Task, writeFile } from "@canonical/task";
2
+ import type { Framework } from "#constants";
3
+ import { readConfig } from "../../../config/index.js";
4
+ import resolveConfigPath from "../../../config/resolveConfigPath.js";
5
+
6
+ /**
7
+ * Build a config-write task for setting or resetting the preferred framework.
8
+ *
9
+ * @param cwd - Working directory containing pragma.config.json.
10
+ * @param framework - Framework to persist, or `undefined` to reset.
11
+ * @returns Task yielding the persisted field/value payload.
12
+ * @note - Impure — reads config and writes to the file system.
13
+ */
14
+ export default function setFramework(
15
+ cwd: string,
16
+ framework: Framework | undefined,
17
+ ): Task<{ field: "framework"; value: Framework | undefined }> {
18
+ return gen(function* () {
19
+ // Merge onto the full existing config so other fields (e.g. `packages`,
20
+ // `trace`) survive. `readConfig` normalises defaults (tier: undefined,
21
+ // channel: "normal"); strip those to keep the file minimal, then apply
22
+ // the framework change (`undefined` resets it).
23
+ const current = readConfig(cwd);
24
+ // Destructure out `framework` too, so a reset (framework === undefined)
25
+ // actually removes it rather than letting the old value survive in `rest`.
26
+ const { tier, channel, framework: _current, ...rest } = current;
27
+ const next = {
28
+ ...rest,
29
+ ...(tier !== undefined ? { tier } : {}),
30
+ ...(channel !== "normal" ? { channel } : {}),
31
+ ...(framework !== undefined ? { framework } : {}),
32
+ };
33
+
34
+ yield* $(
35
+ writeFile(resolveConfigPath(cwd), `${JSON.stringify(next, null, 2)}\n`),
36
+ );
37
+
38
+ return { field: "framework", value: framework };
39
+ });
40
+ }
@@ -18,10 +18,16 @@ export default function setTier(
18
18
  value: string | undefined;
19
19
  }> {
20
20
  return gen(function* () {
21
+ // Merge onto the full existing config so other fields (e.g. `packages`,
22
+ // `trace`) survive. `readConfig` normalises defaults (tier: undefined,
23
+ // channel: "normal"); strip those to keep the file minimal, then apply
24
+ // the tier change (`undefined` resets it).
21
25
  const current = readConfig(cwd);
26
+ const { tier: _current, channel, ...rest } = current;
22
27
  const next = {
28
+ ...rest,
23
29
  ...(tier !== undefined ? { tier } : {}),
24
- ...(current.channel !== undefined ? { channel: current.channel } : {}),
30
+ ...(channel !== "normal" ? { channel } : {}),
25
31
  };
26
32
 
27
33
  yield* $(
@@ -0,0 +1,38 @@
1
+ import { $, gen, type Task, writeFile } from "@canonical/task";
2
+ import { readConfig } from "../../../config/index.js";
3
+ import resolveConfigPath from "../../../config/resolveConfigPath.js";
4
+
5
+ /**
6
+ * Build a config-write task for enabling or disabling query tracing.
7
+ *
8
+ * @param cwd - Working directory containing pragma.config.json.
9
+ * @param trace - `true` to enable, `false` to disable.
10
+ * @returns Task yielding the persisted field/value payload.
11
+ */
12
+ export default function setTrace(
13
+ cwd: string,
14
+ trace: boolean,
15
+ ): Task<{ field: "trace"; value: boolean }> {
16
+ return gen(function* () {
17
+ // Merge onto the full existing config so other fields (e.g. `packages`)
18
+ // survive — rebuilding from a fixed field list silently drops everything
19
+ // not listed, and `packages` is runtime-consumed, so dropping it is data
20
+ // loss. `readConfig` normalises defaults (tier: undefined, channel:
21
+ // "normal"); strip those so we keep the file minimal, matching the
22
+ // existing convention of not persisting unset defaults.
23
+ const current = readConfig(cwd);
24
+ const { tier, channel, ...rest } = current;
25
+ const next = {
26
+ ...rest,
27
+ ...(tier !== undefined ? { tier } : {}),
28
+ ...(channel !== "normal" ? { channel } : {}),
29
+ trace,
30
+ };
31
+
32
+ yield* $(
33
+ writeFile(resolveConfigPath(cwd), `${JSON.stringify(next, null, 2)}\n`),
34
+ );
35
+
36
+ return { field: "trace", value: trace };
37
+ });
38
+ }
@@ -14,10 +14,10 @@ export default async function checkKeStore(
14
14
  ctx: CheckContext,
15
15
  ): Promise<CheckResult> {
16
16
  const start = performance.now();
17
- let store: Awaited<ReturnType<typeof bootStore>> | undefined;
17
+ let result: Awaited<ReturnType<typeof bootStore>> | undefined;
18
18
  try {
19
- store = await bootStore({ cwd: ctx.cwd });
20
- const summary = await collectStoreSummary(store);
19
+ result = await bootStore({ cwd: ctx.cwd });
20
+ const summary = await collectStoreSummary(result.store);
21
21
  const elapsed = Math.round(performance.now() - start);
22
22
  return {
23
23
  name: "ke store",
@@ -30,9 +30,9 @@ export default async function checkKeStore(
30
30
  status: "fail",
31
31
  detail: "failed to boot",
32
32
  remedy:
33
- "Ensure design system packages are installed: bun add -D @canonical/ds-global @canonical/code-standards",
33
+ "Ensure design system packages are installed: bun add -D @canonical/design-system @canonical/anatomy-dsl @canonical/code-standards",
34
34
  };
35
35
  } finally {
36
- store?.dispose();
36
+ result?.store.dispose();
37
37
  }
38
38
  }