@canonical/pragma-cli 0.32.0 → 0.33.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 (140) hide show
  1. package/README.md +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +14 -13
  4. package/src/bin.ts +2 -2
  5. package/src/capabilities/block/blockList.test.ts +79 -0
  6. package/src/capabilities/block/blockList.verb.ts +18 -5
  7. package/src/capabilities/block/pack.ts +12 -1
  8. package/src/capabilities/block/parity.test.ts +9 -2
  9. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  10. package/src/capabilities/capabilities/capabilities.verb.ts +1 -1
  11. package/src/capabilities/capabilities/catalog.ts +7 -3
  12. package/src/capabilities/capabilities/hints.ts +8 -17
  13. package/src/capabilities/capabilities/types.ts +2 -0
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +4 -1
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/pragmaColophon.ts +4 -4
  20. package/src/capabilities/colophon/types.ts +1 -1
  21. package/src/capabilities/config/fields.ts +21 -92
  22. package/src/capabilities/config/set.test.ts +6 -6
  23. package/src/capabilities/config/set.verb.ts +5 -5
  24. package/src/capabilities/config/show.render.test.ts +65 -0
  25. package/src/capabilities/config/show.render.ts +56 -10
  26. package/src/capabilities/config/show.verb.ts +5 -5
  27. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  28. package/src/capabilities/create/byteEquality.test.ts +7 -4
  29. package/src/capabilities/create/compiledCreate.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/create.test.ts +32 -3
  31. package/src/capabilities/create/create.verb.ts +69 -11
  32. package/src/capabilities/doctor/checks/checkConfigFile.ts +1 -1
  33. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  34. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +10 -4
  35. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  36. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  37. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  38. package/src/capabilities/doctor/doctor.render.ts +116 -34
  39. package/src/capabilities/doctor/doctor.test.ts +31 -1
  40. package/src/capabilities/doctor/runChecks.ts +28 -4
  41. package/src/capabilities/doctor/types.ts +15 -0
  42. package/src/capabilities/ontology/index.ts +10 -3
  43. package/src/capabilities/ontology/ontology.test.ts +157 -5
  44. package/src/capabilities/ontology/verbs.ts +192 -93
  45. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  46. package/src/capabilities/prompt/prompt.render.ts +23 -6
  47. package/src/capabilities/prompt/prompt.test.ts +61 -0
  48. package/src/capabilities/reference.test.ts +32 -0
  49. package/src/capabilities/resources/provider.ts +28 -14
  50. package/src/capabilities/resources/resources.test.ts +99 -0
  51. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  52. package/src/capabilities/setup/index.ts +7 -1
  53. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  54. package/src/capabilities/setup/operations/setupGenerator.ts +157 -61
  55. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  56. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  57. package/src/capabilities/setup/operations/setupSkills.ts +7 -1
  58. package/src/capabilities/setup/setup.render.test.ts +109 -0
  59. package/src/capabilities/setup/setup.render.ts +61 -14
  60. package/src/capabilities/setup/setup.test.ts +352 -1
  61. package/src/capabilities/setup/setup.verb.ts +57 -5
  62. package/src/capabilities/setup/types.ts +59 -2
  63. package/src/capabilities/shared/bands.ts +16 -0
  64. package/src/capabilities/sources/collectStatus.ts +19 -4
  65. package/src/capabilities/sources/runUpdate.ts +58 -1
  66. package/src/capabilities/sources/sources.test.ts +187 -1
  67. package/src/capabilities/sources/status.render.test.ts +65 -0
  68. package/src/capabilities/sources/status.render.ts +50 -23
  69. package/src/capabilities/surface.test.ts +34 -9
  70. package/src/capabilities/tier/lookup.render.test.ts +72 -0
  71. package/src/capabilities/tier/lookup.render.ts +33 -8
  72. package/src/capabilities/tier/lookup.test.ts +15 -0
  73. package/src/capabilities/tier/runLookup.ts +28 -0
  74. package/src/constants.test.ts +1 -1
  75. package/src/constants.ts +2 -2
  76. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +13 -14
  77. package/src/kernel/completion/bashDrive.test.ts +3 -3
  78. package/src/kernel/completion/complete.test.ts +3 -5
  79. package/src/kernel/completion/entitySource.ts +14 -6
  80. package/src/kernel/completion/model.test.ts +2 -2
  81. package/src/kernel/completion/model.ts +0 -1
  82. package/src/kernel/completion/parse.test.ts +2 -4
  83. package/src/kernel/error/PragmaError.test.ts +4 -4
  84. package/src/kernel/error/renderError.ts +1 -1
  85. package/src/kernel/packs/compile.ts +1 -1
  86. package/src/kernel/project/cli/buildProgram.test.ts +51 -7
  87. package/src/kernel/project/cli/buildProgram.ts +11 -0
  88. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  89. package/src/kernel/project/cli/globalFlags.ts +14 -14
  90. package/src/kernel/project/cli/rootHelp.ts +4 -2
  91. package/src/kernel/project/mcp/instructions.ts +1 -1
  92. package/src/kernel/project/mcp/mcpError.ts +52 -0
  93. package/src/kernel/project/mcp/prompts/prompts.test.ts +8 -0
  94. package/src/kernel/project/mcp/prompts/provider.ts +36 -6
  95. package/src/kernel/render/formatters.ts +5 -4
  96. package/src/kernel/render/renderers.test.ts +34 -0
  97. package/src/kernel/render/renderers.ts +25 -7
  98. package/src/kernel/render/style.test.ts +31 -0
  99. package/src/kernel/render/style.ts +74 -0
  100. package/src/kernel/runtime/graphpack/build.ts +28 -0
  101. package/src/kernel/runtime/graphpack/buildIndex.test.ts +103 -0
  102. package/src/kernel/runtime/graphpack/buildIndex.ts +124 -36
  103. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +3 -6
  104. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -2
  105. package/src/kernel/runtime/graphpack/graphpack.test.ts +69 -1
  106. package/src/kernel/runtime/graphpack/read.ts +28 -13
  107. package/src/kernel/runtime/graphpack/types.ts +14 -0
  108. package/src/kernel/runtime/loadSession.ts +4 -12
  109. package/src/kernel/runtime/refs/resolve.ts +63 -3
  110. package/src/kernel/runtime/storeReadiness.ts +61 -0
  111. package/src/kernel/runtime/types.ts +1 -1
  112. package/src/kernel/runtime/wasmEmbed.test.ts +3 -1
  113. package/src/kernel/spec/emitReference.test.ts +196 -0
  114. package/src/kernel/spec/emitReference.ts +491 -0
  115. package/src/kernel/spec/emitSurface.ts +4 -2
  116. package/src/kernel/spec/index.ts +2 -0
  117. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  118. package/src/testing/PARITY_GAPS.ts +1 -1
  119. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  120. package/src/testing/behavioral/completion.test.ts +2 -2
  121. package/src/testing/behavioral/disclosure.test.ts +12 -0
  122. package/src/testing/behavioral/docExamples.test.ts +283 -0
  123. package/src/testing/behavioral/errorMatrix.storeless.test.ts +2 -1
  124. package/src/testing/behavioral/journeys.cli.test.ts +3 -1
  125. package/src/testing/behavioral/journeys.defaultPack.test.ts +571 -0
  126. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  127. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  128. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +176 -0
  129. package/src/testing/behavioral/mcpSurface.test.ts +1 -1
  130. package/src/testing/behavioral/staleCommands.test.ts +76 -0
  131. package/src/testing/eval/__snapshots__/eval.test.ts.snap +2 -2
  132. package/src/testing/eval/cases/assembly.ts +3 -3
  133. package/src/testing/eval/cases/effectful.ts +6 -3
  134. package/src/testing/eval/cases/readNouns.ts +4 -1
  135. package/src/testing/fixtures/fixtureCapability.ts +156 -0
  136. package/src/testing/fixtures/graph/defaultPack.ts +178 -0
  137. package/src/testing/fixtures/graph/owlExport.ts +95 -0
  138. package/src/testing/helpers/extractSnippets.ts +29 -0
  139. package/src/testing/helpers/golden.ts +1 -1
  140. package/src/capabilities/config/field.test.ts +0 -188
@@ -7,11 +7,14 @@
7
7
  *
8
8
  * Single consumer today (`pragma colophon`); a promotion candidate to
9
9
  * `kernel/render/` if another verb ever wants the same helper. Determinism note:
10
- * styling routes entirely through `chalk`, so a test forcing `chalk.level = 0`
11
- * gets a stable, color-free snapshot of the structural transforms.
10
+ * styling routes through the shared {@link RenderStyle} seam (color only on a
11
+ * color-capable TTY), so piped / redirected / CI output — where `supports-color`
12
+ * can report a level with no TTY — stays byte-for-byte the color-free structural
13
+ * transform, and a test can force either mode by passing an explicit style.
12
14
  */
13
15
 
14
16
  import chalk from "chalk";
17
+ import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
15
18
 
16
19
  /** Full ANSI escape sequences (`ESC [ … final-letter`) — colour/cursor codes. */
17
20
  // biome-ignore lint/suspicious/noControlCharactersInRegex: deliberate control-char scrub
@@ -33,25 +36,35 @@ function stripControl(line: string): string {
33
36
  return line.replace(ANSI_ESCAPE, "").replace(C0_CONTROLS, "");
34
37
  }
35
38
 
36
- /** Style inline `**bold**` and `` `code` `` spans within one line. */
37
- function styleInline(text: string): string {
39
+ /**
40
+ * Style inline `**bold**` and `` `code` `` spans within one line. The `**` / `` ` ``
41
+ * markers are ALWAYS consumed; off a TTY the styler is inert, so the markers drop
42
+ * with no color (byte-identical to the pre-seam `chalk.level = 0` behavior).
43
+ */
44
+ function styleInline(text: string, style: RenderStyle): string {
38
45
  return text
39
- .replace(/\*\*([^*]+)\*\*/g, (_match, inner: string) => chalk.bold(inner))
40
- .replace(/`([^`]+)`/g, (_match, inner: string) => chalk.cyan(inner));
46
+ .replace(/\*\*([^*]+)\*\*/g, (_match, inner: string) => style.bold(inner))
47
+ .replace(/`([^`]+)`/g, (_match, inner: string) => style.cyan(inner));
41
48
  }
42
49
 
43
50
  /**
44
51
  * Render a Markdown string to styled terminal text.
45
52
  *
46
53
  * @param markdown - The authored Markdown body.
47
- * @returns The chalk-styled terminal string (no trailing newline).
54
+ * @param style - The TTY styler; defaults to the process style. On a color-capable
55
+ * terminal headings/bullets/inline spans are tinted; off a TTY the styler is
56
+ * inert, so the output is byte-identical to the color-free structural form.
57
+ * @returns The styled terminal string (no trailing newline).
48
58
  */
49
- export function renderMarkdownToTerminal(markdown: string): string {
59
+ export function renderMarkdownToTerminal(
60
+ markdown: string,
61
+ style: RenderStyle = defaultStyle(),
62
+ ): string {
50
63
  const lines: string[] = [];
51
64
  let inFence = false;
52
65
 
53
66
  for (const rawLine of markdown.split("\n")) {
54
- // Scrub pack-authored terminal control codes BEFORE our own chalk styling
67
+ // Scrub pack-authored terminal control codes BEFORE our own styling
55
68
  // (colophon bodies are domain-as-data — never a channel for raw ANSI).
56
69
  const raw = stripControl(rawLine);
57
70
  // A fence toggles code mode; the ``` markers themselves are dropped.
@@ -60,25 +73,31 @@ export function renderMarkdownToTerminal(markdown: string): string {
60
73
  continue;
61
74
  }
62
75
  if (inFence) {
63
- lines.push(chalk.dim(` ${raw}`));
76
+ lines.push(style.dim(` ${raw}`));
64
77
  continue;
65
78
  }
66
79
 
67
80
  const heading = /^(#{1,6})\s+(.+)$/.exec(raw);
68
81
  if (heading) {
69
82
  const level = heading[1]?.length ?? 1;
70
- const text = styleInline(heading[2] ?? "");
71
- lines.push(level === 1 ? chalk.bold.underline(text) : chalk.bold(text));
83
+ const text = styleInline(heading[2] ?? "", style);
84
+ // H1 earns bold+underline on a TTY; underline is outside the RenderStyle
85
+ // seam, so reach for chalk only when the styler is enabled.
86
+ lines.push(
87
+ level === 1 && style.enabled
88
+ ? chalk.bold.underline(text)
89
+ : style.bold(text),
90
+ );
72
91
  continue;
73
92
  }
74
93
 
75
94
  const bullet = /^\s*[-*]\s+(.+)$/.exec(raw);
76
95
  if (bullet) {
77
- lines.push(` ${chalk.cyan("•")} ${styleInline(bullet[1] ?? "")}`);
96
+ lines.push(` ${style.cyan("•")} ${styleInline(bullet[1] ?? "", style)}`);
78
97
  continue;
79
98
  }
80
99
 
81
- lines.push(styleInline(raw));
100
+ lines.push(styleInline(raw, style));
82
101
  }
83
102
 
84
103
  return lines.join("\n");
@@ -4,7 +4,7 @@
4
4
  * colophon` as the first section, before any active pack's domain colophon.
5
5
  *
6
6
  * `PRAGMA_COLOPHON` is the full narrative for humans; `PRAGMA_COLOPHON_SUMMARY`
7
- * is the condensed form `--llm` emits (Markdown IS the agent-optimal shape).
7
+ * is the condensed form `--format llm` emits (Markdown IS the agent-optimal shape).
8
8
  * Both are BODIES with no leading H1 — the renderer supplies the "pragma"
9
9
  * heading, so the section is never double-titled.
10
10
  */
@@ -32,7 +32,7 @@ source of truth a test asserts the live grammar still emits, tool for tool.
32
32
  ## LLM-optimized output
33
33
 
34
34
  Each verb renders three ways — \`plain\` for a terminal, \`json\` for the machine
35
- envelope, and \`llm\` for condensed Markdown. \`--llm\` (or a non-interactive
35
+ envelope, and \`llm\` for condensed Markdown. \`--format llm\` (or a non-interactive
36
36
  stdout) selects the agent form: the same data, shaped for a model to read. This
37
37
  colophon is itself a showcase of that render model.
38
38
 
@@ -56,11 +56,11 @@ queries) compiled into verbs, backed by a content-addressed graphpack that
56
56
  \`sources update\` builds once. Swap the pack and the same pragma serves a
57
57
  different domain — including the domain colophon printed below this one.`;
58
58
 
59
- /** The condensed colophon for `--llm` (Markdown body, no leading H1). */
59
+ /** The condensed colophon for `--format llm` (Markdown body, no leading H1). */
60
60
  export const PRAGMA_COLOPHON_SUMMARY = `pragma is a domain-based toolchain: one CLI + MCP server projected from a single \`VerbSpec\` grammar.
61
61
 
62
62
  - **Effect monad** (\`@canonical/task\`): reads are async; a mutation returns an interpreted \`Task\`, so \`--dry-run\` and \`--undo\` are free. The dispatcher branches on \`capability.mutates\`.
63
63
  - **One grammar, many projections**: CLI, MCP tools, completion, and docs all project one \`VerbSpec\`; the emitted surface is frozen in a covenant so the projections never drift.
64
- - **LLM-optimized output**: every verb renders \`plain\` / \`json\` / \`llm\`; \`--llm\` emits condensed Markdown for agents.
64
+ - **LLM-optimized output**: every verb renders \`plain\` / \`json\` / \`llm\`; \`--format llm\` (or a piped stdout) emits condensed Markdown for agents.
65
65
  - **Modular + storeless**: capability modules; the triple store boots only for \`needsStore\` verbs.
66
66
  - **Domain as data**: a pack is a declarative \`PackDefinition\` compiled to verbs over a content-addressed graph built by \`sources update\`.`;
@@ -17,7 +17,7 @@ export interface ColophonSection {
17
17
  * heading from {@link title}, so a section is never double-titled).
18
18
  */
19
19
  readonly markdown: string;
20
- /** Optional condensed body for `--llm` (pragma supplies one). */
20
+ /** Optional condensed body for `--format llm` (pragma supplies one). */
21
21
  readonly summary?: string;
22
22
  /** Provenance: `"built-in"`, or `"pack:<name>"`. */
23
23
  readonly source?: string;
@@ -1,28 +1,19 @@
1
1
  /**
2
- * The config field table — one declarative row per writable field drives all
3
- * three setters (`tier` / `channel` / `detail`), the way the pack grammar
4
- * compiles read verbs from a table.
5
- *
6
- * Covenant-exact emission is the hard gate (surfaceConformance): each verb must
7
- * `emitVerb` to `{ v:<field>, args:["<positional>"], mutates:true,
8
- * mcp:"config_<field>" }` — one REQUIRED positional, NO flags, NO needsStore
9
- * (config setters are storeless; the old store-backed `validateTier` is dropped
10
- * — PARITY_GAP `config-tier-no-ontology-validation`). The factory produces
11
- * exactly that shape; the `field.test.ts` deep-equals the golden slice.
2
+ * The config field table — one declarative row per writable field. Since AV-228
3
+ * B3 retired the per-field `config tier`/`channel`/`detail` verbs, this table no
4
+ * longer emits verbs of its own: it is the shared source of truth that drives
5
+ * `config set <key> <value>` (via `runSet` → `runField`), giving `set` its
6
+ * `<key>` enum, per-field reset sentinels, enum validation, and positional
7
+ * shaping for free.
12
8
  */
13
9
 
14
- import type { Task } from "@canonical/task";
15
10
  import { DETAIL_LEVELS } from "../../constants.js";
16
11
  import type { RawConfig } from "../../kernel/config/types.js";
17
12
  import { CHANNELS } from "../../kernel/config/types.js";
18
- import { asVerb } from "../../kernel/spec/asVerb.js";
19
- import type { ParamSpec, VerbSpec } from "../../kernel/spec/types.js";
20
- import { configFieldFormatters } from "./field.render.js";
21
- import type { ConfigFieldResult } from "./types.js";
22
13
 
23
14
  /** One writable config field and how its single positional is shaped/validated. */
24
15
  export interface ConfigFieldSpec {
25
- /** The `RawConfig` key written (also the verb label + `config_<field>` tool). */
16
+ /** The `RawConfig` key written (also the `config set` `<key>` enum member). */
26
17
  readonly field: keyof RawConfig & ("tier" | "channel" | "detail");
27
18
  /** The positional param NAME — its usage token is `<positional>` (covenant). */
28
19
  readonly positional: string;
@@ -30,24 +21,22 @@ export interface ConfigFieldSpec {
30
21
  readonly kind: "string" | "enum";
31
22
  /** The allowed values for an enum field. */
32
23
  readonly values?: readonly string[];
33
- /** One-line verb summary. */
24
+ /** One-line field summary. */
34
25
  readonly summary: string;
35
26
  /** Extended help doc. */
36
27
  readonly doc: string;
37
28
  /** Reserved values that REMOVE the field instead of setting it. */
38
29
  readonly resetSentinel?: readonly string[];
39
- /** Verb help examples. */
30
+ /** Field help examples. */
40
31
  readonly examples: readonly { cmd: string; note?: string }[];
41
32
  }
42
33
 
43
34
  /**
44
35
  * The three writable fields. Only `tier` (a free string with a meaningful
45
36
  * "no value") carries reset sentinels; `channel`/`detail` reset by setting
46
- * their default (`normal`/`standard`) — no `--reset` flag exists, because a
47
- * flag would emit `flags:[...]` and break the covenant (PARITY_GAP
48
- * `config-field-reset-sentinel`). Query is `config show` — the covenant gives
49
- * each setter exactly ONE required positional, so a no-arg "print current" mode
50
- * is impossible here (PARITY_GAP `config-field-query-via-show`).
37
+ * their default (`normal`/`standard`). The migration path for the retired
38
+ * field-verbs is `config set <field> <value>` (e.g. `config set tier apps/lxd`,
39
+ * `config set tier none` to clear it).
51
40
  */
52
41
  export const CONFIG_FIELDS: readonly ConfigFieldSpec[] = [
53
42
  {
@@ -58,8 +47,11 @@ export const CONFIG_FIELDS: readonly ConfigFieldSpec[] = [
58
47
  summary: "Set the active design-system tier.",
59
48
  doc: "Writes the `tier` field to the global config. Pass `none`, `default`, or `-` to clear it. Written to the global layer only — project configs are authored by hand.",
60
49
  examples: [
61
- { cmd: "pragma config tier apps/lxd", note: "scope reads to a tier" },
62
- { cmd: "pragma config tier none", note: "clear the tier" },
50
+ {
51
+ cmd: "pragma config set tier apps/lxd",
52
+ note: "scope reads to a tier",
53
+ },
54
+ { cmd: "pragma config set tier none", note: "clear the tier" },
63
55
  ],
64
56
  },
65
57
  {
@@ -70,8 +62,8 @@ export const CONFIG_FIELDS: readonly ConfigFieldSpec[] = [
70
62
  summary: "Set the release channel controlling component visibility.",
71
63
  doc: "Writes the `channel` field to the global config. Reset by setting the default, `normal`.",
72
64
  examples: [
73
- { cmd: "pragma config channel experimental" },
74
- { cmd: "pragma config channel normal", note: "back to the default" },
65
+ { cmd: "pragma config set channel experimental" },
66
+ { cmd: "pragma config set channel normal", note: "back to the default" },
75
67
  ],
76
68
  },
77
69
  {
@@ -82,71 +74,8 @@ export const CONFIG_FIELDS: readonly ConfigFieldSpec[] = [
82
74
  summary: "Set the default progressive-disclosure level.",
83
75
  doc: "Writes the `detail` field to the global config. Reset by setting the default, `standard`.",
84
76
  examples: [
85
- { cmd: "pragma config detail detailed" },
86
- { cmd: "pragma config detail standard", note: "back to the default" },
77
+ { cmd: "pragma config set detail detailed" },
78
+ { cmd: "pragma config set detail standard", note: "back to the default" },
87
79
  ],
88
80
  },
89
81
  ];
90
-
91
- /** The single required positional for a field, shaped per its `kind`. */
92
- function fieldPositional(spec: ConfigFieldSpec): ParamSpec {
93
- // Name the positional by its usage token (`<path>`/`<name>`/`<level>`) so the
94
- // doc and the token agree (was "The tier value to write." for a `<path>`).
95
- const doc = `The ${spec.field} ${spec.positional} to write.`;
96
- if (spec.kind === "enum") {
97
- return {
98
- kind: "enum",
99
- name: spec.positional,
100
- doc,
101
- values: spec.values ?? [],
102
- required: true,
103
- positional: true,
104
- };
105
- }
106
- return {
107
- kind: "string",
108
- name: spec.positional,
109
- doc,
110
- required: true,
111
- positional: true,
112
- };
113
- }
114
-
115
- /**
116
- * Build one storeless, mutating config setter from a field spec.
117
- *
118
- * `run` lazily imports the write body and presents `Promise<Task<R>>` through
119
- * the `Task<R>` arm by the honest cast (the same seam `sources update` uses) —
120
- * a literal `Promise<Task>` union arm would poison async read-verb inference.
121
- *
122
- * @param spec - The field-table row.
123
- * @returns The verb spec for `config <field>`.
124
- */
125
- export function fieldVerb(
126
- spec: ConfigFieldSpec,
127
- ): VerbSpec<Record<string, unknown>, ConfigFieldResult> {
128
- return {
129
- path: ["config", spec.field],
130
- summary: spec.summary,
131
- doc: spec.doc,
132
- params: [fieldPositional(spec)],
133
- output: { formatters: configFieldFormatters },
134
- examples: [...spec.examples],
135
- capability: {
136
- needsStore: false,
137
- mutates: true,
138
- mcp: {
139
- expose: true,
140
- annotations: { readOnlyHint: false, openWorldHint: false },
141
- },
142
- },
143
- run: (params) =>
144
- import("./runField.js").then((m) =>
145
- m.runField(spec, params),
146
- ) as unknown as Task<ConfigFieldResult>,
147
- };
148
- }
149
-
150
- /** The three config setters, in covenant order (tier, channel, detail). */
151
- export const configFieldVerbs: VerbSpec[] =
152
- CONFIG_FIELDS.map(fieldVerb).map(asVerb);
@@ -1,12 +1,12 @@
1
1
  /**
2
- * `config set <key> <value>` — the additive one-command setter (PR9, COVENANT).
2
+ * `config set <key> <value>` — the one-command config setter (PR9, COVENANT).
3
3
  *
4
4
  * Pins the emitted covenant slice (`{ v:"set", args:["<key>","<value>"],
5
- * mutates:true, mcp:"config_set" }`), proves it shares the per-field write path
6
- * (set / reset-sentinel / enum-backstop), rejects an unknown key and an
7
- * out-of-set enum value with INVALID_INPUT, and holds MCP plan-first/confirm
8
- * parity. The frozen `config tier/channel/detail` verbs are UNTOUCHED — this is
9
- * purely additive.
5
+ * mutates:true, mcp:"config_set" }`), proves it drives the shared per-field
6
+ * write path (set / reset-sentinel / enum-backstop), rejects an unknown key and
7
+ * an out-of-set enum value with INVALID_INPUT, and holds MCP plan-first/confirm
8
+ * parity. Since AV-228 B3 retired `config tier/channel/detail`, `config set` is
9
+ * the sole config setter — `config set <field> <value>` is the migration path.
10
10
  */
11
11
 
12
12
  import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * `config set <key> <value>` — the one-command config setter (ADDITIVE).
3
3
  *
4
- * The user-facing primary form for writing config: `pragma config set tier
5
- * apps/lxd` instead of the per-field `pragma config tier apps/lxd`. It is purely
6
- * additive — the frozen `config tier`/`channel`/`detail` verbs stay — and shares
7
- * their exact write path (`runField` via `runSet`), so it inherits reset
8
- * sentinels, enum re-validation, and the global-layer-only write for free.
4
+ * The single form for writing config: `pragma config set tier apps/lxd`. Since
5
+ * AV-228 B3 retired the per-field `config tier`/`channel`/`detail` verbs, this
6
+ * is now the ONLY config setter. It drives the shared write path (`runField` via
7
+ * `runSet`), so it inherits reset sentinels, enum re-validation, and the
8
+ * global-layer-only write from the {@link CONFIG_FIELDS} table.
9
9
  *
10
10
  * Covenant shape: `<key>` is an ENUM over the field names (better completion +
11
11
  * validation; the token still emits as `<key>`), `<value>` a free string, so
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { type RenderStyle, styleFor } from "../../kernel/render/style.js";
3
+ import { renderConfigShowPlain } from "./show.render.js";
4
+ import type { ConfigShowData } from "./types.js";
5
+
6
+ const DATA: ConfigShowData = {
7
+ config: {
8
+ tier: "apps/lxd",
9
+ channel: "normal",
10
+ detail: "standard",
11
+ packages: ["@canonical/ds"],
12
+ },
13
+ origins: {
14
+ tier: "project",
15
+ channel: "default",
16
+ detail: "default",
17
+ packages: "global",
18
+ stories: "default",
19
+ prefixes: "default",
20
+ prompts: "default",
21
+ },
22
+ globalConfigPath: "/home/u/.config/pragma/config.json",
23
+ projectConfigPath: "/repo/pragma.config.ts",
24
+ projectExists: true,
25
+ globalExists: true,
26
+ };
27
+
28
+ /** A sentinel styler — enabled, each color wrapped in a visible tag. */
29
+ const TAGGED: RenderStyle = {
30
+ enabled: true,
31
+ bold: (t) => `<b>${t}</b>`,
32
+ dim: (t) => `<d>${t}</d>`,
33
+ cyan: (t) => `<c>${t}</c>`,
34
+ green: (t) => `<g>${t}</g>`,
35
+ yellow: (t) => `<y>${t}</y>`,
36
+ };
37
+
38
+ describe("renderConfigShowPlain", () => {
39
+ it("is byte-identical to the plain form when the styler is disabled", () => {
40
+ expect(renderConfigShowPlain(DATA, styleFor(false))).toBe(
41
+ [
42
+ "tier: apps/lxd [project]",
43
+ "channel: normal",
44
+ "detail: standard",
45
+ "packages: @canonical/ds [global]",
46
+ "global config: /home/u/.config/pragma/config.json",
47
+ "project config: /repo/pragma.config.ts",
48
+ ].join("\n"),
49
+ );
50
+ });
51
+
52
+ it("aligns the key column and colorizes values on a TTY", () => {
53
+ const out = renderConfigShowPlain(DATA, TAGGED);
54
+ const keyWidth = "project config:".length;
55
+ // Dim, padded key; cyan value; dim `[layer]` marker.
56
+ expect(out).toContain(
57
+ `<d>${"tier:".padEnd(keyWidth)}</d> <c>apps/lxd</c><d> [project]</d>`,
58
+ );
59
+ // A default-origin value carries no marker.
60
+ expect(out).toContain(
61
+ `<d>${"channel:".padEnd(keyWidth)}</d> <c>normal</c>`,
62
+ );
63
+ expect(out).not.toContain("<d> [default]</d>");
64
+ });
65
+ });
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { ConfigOrigin } from "../../kernel/config/types.js";
10
+ import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
10
11
  import type { Formatters } from "../../kernel/spec/types.js";
11
12
  import type { ConfigShowData } from "./types.js";
12
13
 
@@ -24,18 +25,63 @@ function packageNames(data: ConfigShowData): string {
24
25
  return names.length > 0 ? names.join(", ") : "(none)";
25
26
  }
26
27
 
28
+ /** One resolved row: its label, value, and `[layer]` marker (blank for a default). */
29
+ type ConfigRow = readonly [label: string, value: string, marker: string];
30
+
31
+ /** The resolved rows, in display order, shared by the plain + beautified forms. */
32
+ function configRows(data: ConfigShowData): readonly ConfigRow[] {
33
+ const { config, origins } = data;
34
+ return [
35
+ [
36
+ "tier",
37
+ config.tier ?? "(none — all tiers visible)",
38
+ originMarker(origins.tier),
39
+ ],
40
+ ["channel", config.channel, originMarker(origins.channel)],
41
+ ["detail", config.detail ?? "standard", originMarker(origins.detail)],
42
+ ["packages", packageNames(data), originMarker(origins.packages)],
43
+ [
44
+ "global config",
45
+ `${data.globalConfigPath}${data.globalExists ? "" : " (not found)"}`,
46
+ "",
47
+ ],
48
+ ["project config", data.projectConfigPath ?? "(not found)", ""],
49
+ ];
50
+ }
51
+
52
+ /**
53
+ * Render `config show` as plain text.
54
+ *
55
+ * @param data - The resolved config-show payload.
56
+ * @param style - TTY styling; defaults to the process style. On a color-capable
57
+ * terminal the `key:` column is aligned + dim, values cyan, and `[layer]`
58
+ * markers dim; off a TTY the styler is inert, so the output is byte-identical
59
+ * to the pre-beautify `key: value[marker]` form.
60
+ * @returns The formatted config block.
61
+ */
62
+ export function renderConfigShowPlain(
63
+ data: ConfigShowData,
64
+ style: RenderStyle = defaultStyle(),
65
+ ): string {
66
+ const rows = configRows(data);
67
+ if (!style.enabled) {
68
+ return rows
69
+ .map(([label, value, marker]) => `${label}: ${value}${marker}`)
70
+ .join("\n");
71
+ }
72
+ const keyWidth = Math.max(...rows.map(([label]) => `${label}:`.length));
73
+ return rows
74
+ .map(([label, value, marker]) => {
75
+ const key = style.dim(`${label}:`.padEnd(keyWidth));
76
+ const tail = marker ? style.dim(marker) : "";
77
+ return `${key} ${style.cyan(value)}${tail}`;
78
+ })
79
+ .join("\n");
80
+ }
81
+
27
82
  export const configShowFormatters: Formatters<ConfigShowData> = {
28
83
  plain(data) {
29
- const { config, origins } = data;
30
- const lines = [
31
- `tier: ${config.tier ?? "(none — all tiers visible)"}${originMarker(origins.tier)}`,
32
- `channel: ${config.channel}${originMarker(origins.channel)}`,
33
- `detail: ${config.detail ?? "standard"}${originMarker(origins.detail)}`,
34
- `packages: ${packageNames(data)}${originMarker(origins.packages)}`,
35
- `global config: ${data.globalConfigPath}${data.globalExists ? "" : " (not found)"}`,
36
- `project config: ${data.projectConfigPath ?? "(not found)"}`,
37
- ];
38
- return lines.join("\n");
84
+ return renderConfigShowPlain(data);
39
85
  },
40
86
 
41
87
  llm(data) {
@@ -7,7 +7,6 @@
7
7
 
8
8
  import { asVerb } from "../../kernel/spec/asVerb.js";
9
9
  import type { CapabilityModule, VerbSpec } from "../../kernel/spec/types.js";
10
- import { configFieldVerbs } from "./fields.js";
11
10
  import { configSetVerb } from "./set.verb.js";
12
11
  import { configShowFormatters } from "./show.render.js";
13
12
  import type { ConfigShowData } from "./types.js";
@@ -35,11 +34,12 @@ const showVerb: VerbSpec<Record<string, unknown>, ConfigShowData> = {
35
34
  };
36
35
 
37
36
  /**
38
- * The `config` capability module: the `show` reader (PR1), the storeless
39
- * `tier`/`channel`/`detail` setters (PR6), and the additive one-command `set`
40
- * verb (PR9), emitted in covenant order.
37
+ * The `config` capability module: the `show` reader (PR1) and the one-command
38
+ * `set <key> <value>` setter (PR9), emitted in covenant order. The per-field
39
+ * `tier`/`channel`/`detail` verbs were retired in favour of `config set`
40
+ * (AV-228 B3).
41
41
  */
42
42
  export const configModule: CapabilityModule = {
43
43
  name: "config",
44
- verbs: [asVerb(showVerb), ...configFieldVerbs, configSetVerb],
44
+ verbs: [asVerb(showVerb), configSetVerb],
45
45
  };
@@ -20,7 +20,7 @@ import { describe, expect, it } from "vitest";
20
20
 
21
21
  const here = dirname(fileURLToPath(import.meta.url));
22
22
  const repoRoot = resolve(here, "../../../../../..");
23
- const pragmaBin = join(repoRoot, "packages/cli/next/src/bin.ts");
23
+ const pragmaBin = join(repoRoot, "packages/cli/pragma/src/bin.ts");
24
24
  const summonCore = join(repoRoot, "packages/summon/core/dist/esm/index.js");
25
25
  const pickGen = join(here, "pickGenerator.ts");
26
26
  const freshCwd = (): string => mkdtempSync(join(tmpdir(), "pragma-xbin-"));
@@ -184,12 +184,15 @@ const cases: Case[] = [
184
184
  name: "application",
185
185
  kind: "application",
186
186
  framework: "",
187
+ // The pragma path receives the CLI `--with-X` params (AV-228 B8); the summon
188
+ // path receives the generator's bare prompt names. runCreate's boundary remap
189
+ // (`withSsr` -> `ssr`, ...) makes both produce the byte-identical tree.
187
190
  params: {
188
191
  appPath: "my-app",
189
- ssr: true,
190
- router: true,
191
- forms: true,
192
- relay: false,
192
+ withSsr: true,
193
+ withRouter: true,
194
+ withForms: true,
195
+ withRelay: false,
193
196
  runInstall: false,
194
197
  },
195
198
  answers: {
@@ -28,7 +28,7 @@ import { beforeAll, describe, expect, it } from "vitest";
28
28
 
29
29
  const here = dirname(fileURLToPath(import.meta.url));
30
30
  const repoRoot = resolve(here, "../../../../../..");
31
- const cliNextDir = join(repoRoot, "packages/cli/next");
31
+ const cliNextDir = join(repoRoot, "packages/cli/pragma");
32
32
  const pragmaBin = join(cliNextDir, "src/bin.ts");
33
33
  const compiledBin = join(cliNextDir, "dist/pragma");
34
34
  const freshCwd = (): string => mkdtempSync(join(tmpdir(), "pragma-compiled-"));
@@ -29,6 +29,7 @@ import {
29
29
  createApplicationVerb,
30
30
  createComponentVerb,
31
31
  createPackageVerb,
32
+ INCLUDE_FLAG_ALIASES,
32
33
  isModuleNotFound,
33
34
  } from "./create.verb.js";
34
35
  import { generatorToParams } from "./generatorToVerbSpec.js";
@@ -350,6 +351,18 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
350
351
  });
351
352
  });
352
353
 
354
+ /** Map a mirror's CLI `--with-X` include-flag names back to the generator
355
+ * prompt names (AV-228 B8) so the parity comparison still checks kinds and
356
+ * defaults against the real generator despite the deliberate flag rename. */
357
+ const toGeneratorNames = (
358
+ kind: "application",
359
+ params: readonly ParamSpec[],
360
+ ): ParamSpec[] =>
361
+ params.map((p) => {
362
+ const generatorName = INCLUDE_FLAG_ALIASES[kind][p.name];
363
+ return generatorName ? ({ ...p, name: generatorName } as ParamSpec) : p;
364
+ });
365
+
353
366
  it("application: mirror matches the application/react generator (defaults)", async () => {
354
367
  const { generators } = await import("@canonical/summon-application");
355
368
  const real = generatorToParams(
@@ -357,9 +370,25 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
357
370
  "application/react"
358
371
  ].prompts as never,
359
372
  );
360
- expect(behavioural(createApplicationVerb.params)).toEqual(
361
- behavioural(applyRunInstallOverride(real)),
362
- );
373
+ // Bridge the B8 include-flag rename before comparing: the CLI grammar
374
+ // exposes `withSsr`/`withRouter`/`withForms`, which map back to the
375
+ // generator's `ssr`/`router`/`forms`.
376
+ expect(
377
+ behavioural(
378
+ toGeneratorNames("application", createApplicationVerb.params),
379
+ ),
380
+ ).toEqual(behavioural(applyRunInstallOverride(real)));
381
+
382
+ // Guard the B8 rename itself: ALL application include-flags ARE on the
383
+ // `--with-X` convention (params `withSsr`/`withRouter`/`withForms`/
384
+ // `withRelay`), not the bare `ssr`/`router`/`forms`/`relay` booleans.
385
+ const names = createApplicationVerb.params.map((p) => p.name);
386
+ expect(names).toContain("withSsr");
387
+ expect(names).toContain("withRouter");
388
+ expect(names).toContain("withForms");
389
+ expect(names).toContain("withRelay");
390
+ expect(names).not.toContain("ssr");
391
+ expect(names).not.toContain("relay");
363
392
 
364
393
  const realRunInstall = real.find((p) => p.name === "runInstall") as
365
394
  | Record<string, unknown>