@canonical/pragma-cli 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -35
- package/dist/pragma +0 -0
- package/package.json +15 -13
- package/src/completions/constants.ts +7 -0
- package/src/completions/handleQuery.test.ts +32 -0
- package/src/completions/handleQuery.ts +20 -1
- package/src/completions/queryCompletions.test.ts +5 -0
- package/src/completions/queryCompletions.ts +17 -3
- package/src/completions/querySocket.test.ts +31 -0
- package/src/completions/querySocket.ts +36 -9
- package/src/config/parseConfigValues.ts +2 -0
- package/src/config/readConfigLayers.test.ts +2 -0
- package/src/config/readConfigLayers.ts +3 -0
- package/src/config/types.ts +7 -0
- package/src/constants.ts +3 -1
- package/src/domains/block/commands/sample.ts +2 -1
- package/src/domains/block/formatters/list.test.ts +3 -3
- package/src/domains/block/formatters/lookup.test.ts +5 -5
- package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
- package/src/domains/block/stories.ts +9 -3
- package/src/domains/config/commands/channel.test.ts +16 -0
- package/src/domains/config/commands/channel.ts +14 -1
- package/src/domains/config/commands/framework.test.ts +14 -0
- package/src/domains/config/commands/framework.ts +13 -1
- package/src/domains/config/commands/tier.test.ts +7 -0
- package/src/domains/config/commands/tier.ts +14 -1
- package/src/domains/config/operations/validateTier.test.ts +9 -0
- package/src/domains/config/operations/validateTier.ts +10 -2
- package/src/domains/create/generatorPack/bundled.ts +49 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
- package/src/domains/create/index.ts +13 -7
- package/src/domains/create/mcp/specs.ts +6 -161
- package/src/domains/create/renderGeneratorUi.test.ts +103 -0
- package/src/domains/create/renderGeneratorUi.ts +65 -0
- package/src/domains/doctor/formatters/doctor.test.ts +50 -2
- package/src/domains/doctor/formatters/doctor.ts +90 -38
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
- package/src/domains/doctor/operations/checks.test.ts +6 -4
- package/src/domains/doctor/operations/types.ts +23 -1
- package/src/domains/graph/commands/inspect.test.ts +2 -2
- package/src/domains/graph/commands/query.test.ts +5 -3
- package/src/domains/graph/formatters/inspect.ts +7 -7
- package/src/domains/graph/formatters/query.test.ts +87 -0
- package/src/domains/graph/formatters/query.ts +42 -5
- package/src/domains/graph/operations/inspectUri.test.ts +5 -5
- package/src/domains/graphql/commands/serve.test.ts +12 -6
- package/src/domains/graphql/commands/serve.ts +16 -6
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
- package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
- package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
- package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
- package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
- package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
- package/src/domains/graphql/operations/index.ts +5 -0
- package/src/domains/info/formatters/upgrade.test.ts +7 -4
- package/src/domains/info/formatters/upgrade.ts +3 -1
- package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
- package/src/domains/info/operations/collectEntityCounts.ts +123 -0
- package/src/domains/info/operations/index.ts +1 -0
- package/src/domains/info/types.ts +13 -0
- package/src/domains/llm/capabilities.test.ts +23 -3
- package/src/domains/llm/commands/capabilities.ts +12 -3
- package/src/domains/llm/commands/llm.ts +5 -1
- package/src/domains/llm/data/commandReference.ts +20 -11
- package/src/domains/llm/data/decisionTrees.ts +10 -5
- package/src/domains/llm/data/toolCatalog.ts +49 -18
- package/src/domains/llm/formatters/orientation.ts +5 -1
- package/src/domains/llm/llmCommand.test.ts +11 -3
- package/src/domains/llm/mcp/specs.ts +3 -4
- package/src/domains/llm/operations/collectContext.test.ts +64 -0
- package/src/domains/llm/operations/collectContext.ts +7 -17
- package/src/domains/modifier/commands/sample.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +5 -3
- package/src/domains/modifier/stories.ts +9 -0
- package/src/domains/ontology/commands/list.test.ts +5 -3
- package/src/domains/ontology/commands/show.test.ts +85 -9
- package/src/domains/ontology/formatters/index.ts +1 -0
- package/src/domains/ontology/formatters/list.ts +37 -12
- package/src/domains/ontology/formatters/show.ts +358 -67
- package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
- package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
- package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
- package/src/domains/ontology/helpers/index.ts +14 -0
- package/src/domains/ontology/helpers/queryClasses.ts +32 -16
- package/src/domains/ontology/helpers/queryProperties.ts +39 -6
- package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
- package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
- package/src/domains/ontology/operations/listOntologies.ts +126 -30
- package/src/domains/ontology/operations/showOntology.test.ts +131 -12
- package/src/domains/ontology/operations/showOntology.ts +289 -7
- package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
- package/src/domains/ontology/stories.ts +53 -8
- package/src/domains/refs/operations/updateRefs.ts +0 -1
- package/src/domains/setup/commands/index.ts +1 -0
- package/src/domains/setup/commands/setup.ts +70 -0
- package/src/domains/setup/index.ts +7 -3
- package/src/domains/setup/operations/setupLsp.test.ts +3 -2
- package/src/domains/setup/operations/setupLsp.ts +7 -2
- package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
- package/src/domains/shared/bootStore.test.ts +82 -0
- package/src/domains/shared/bootStore.ts +56 -20
- package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
- package/src/domains/shared/extractLocalName.test.ts +3 -3
- package/src/domains/shared/formatters.ts +23 -2
- package/src/domains/shared/listTiers.ts +40 -0
- package/src/domains/shared/loaders/bundledLoader.ts +48 -0
- package/src/domains/shared/loaders/gitLoader.ts +1 -2
- package/src/domains/shared/loaders/localLoader.ts +2 -18
- package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
- package/src/domains/shared/loaders/readPackageDir.ts +46 -8
- package/src/domains/shared/lookupMany.test.ts +15 -0
- package/src/domains/shared/lookupMany.ts +22 -6
- package/src/domains/shared/namespaces.ts +13 -5
- package/src/domains/shared/parseSampleCount.test.ts +47 -0
- package/src/domains/shared/parseSampleCount.ts +45 -0
- package/src/domains/shared/prefixes.test.ts +211 -0
- package/src/domains/shared/prefixes.ts +182 -15
- package/src/domains/shared/renderers.ts +5 -5
- package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
- package/src/domains/shared/resolveLookupExitCode.ts +42 -0
- package/src/domains/shared/semanticPackage.test.ts +89 -0
- package/src/domains/shared/semanticPackage.ts +17 -2
- package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
- package/src/domains/shared/stories/compileLookupTool.ts +1 -1
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
- package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
- package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
- package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
- package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
- package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
- package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
- package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
- package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
- package/src/domains/shared/stories/pack/index.ts +14 -0
- package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
- package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
- package/src/domains/shared/stories/pack/types.ts +123 -3
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
- package/src/domains/shared/stories/types.ts +9 -1
- package/src/domains/shared/types/index.ts +5 -9
- package/src/domains/shared/types/ontology.ts +119 -24
- package/src/domains/shared/types/standard.ts +9 -52
- package/src/domains/token/commands/lookup.test.ts +8 -0
- package/src/domains/token/commands/lookup.ts +12 -0
- package/src/domains/token/commands/sample.ts +2 -1
- package/src/domains/token/featureFlag.ts +17 -0
- package/src/domains/token/index.ts +5 -0
- package/src/domains/token/mcp/specs.ts +13 -1
- package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
- package/src/domains/trace/commands/show.ts +30 -4
- package/src/index.ts +1 -13
- package/src/mcp/registerResources.test.ts +2 -2
- package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
- package/src/mcp/tools/index.ts +18 -10
- package/src/mcp/tools/registerTools.test.ts +106 -30
- package/src/pipeline/collectCommands.test.ts +151 -1
- package/src/pipeline/collectCommands.ts +34 -19
- package/src/pipeline/createProgram.test.ts +5 -1
- package/src/pipeline/createProgram.ts +2 -2
- package/src/pipeline/firstRun.test.ts +80 -0
- package/src/pipeline/firstRun.ts +99 -0
- package/src/pipeline/parseGlobalFlags.test.ts +102 -1
- package/src/pipeline/parseGlobalFlags.ts +98 -9
- package/src/pipeline/resolveCommandKind.test.ts +12 -0
- package/src/pipeline/resolveCommandKind.ts +4 -0
- package/src/pipeline/rootHelp.test.ts +87 -0
- package/src/pipeline/rootHelp.ts +265 -0
- package/src/pipeline/runCli.orchestration.test.ts +28 -12
- package/src/pipeline/runCli.test.ts +17 -4
- package/src/pipeline/runCli.ts +162 -19
- package/src/testing/cli.ts +30 -2
- package/src/testing/dsFixtures.ts +21 -4
- package/src/testing/index.ts +1 -4
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
- package/src/testing/integration/cli-user-stories.test.ts +63 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
- package/src/testing/integration/edge-cases.test.ts +62 -50
- package/src/testing/integration/executor-execution.test.ts +6 -4
- package/src/testing/integration/executor-golden.test.ts +2 -2
- package/src/testing/integration/mcp-surface.test.ts +40 -15
- package/src/testing/integration/parity.test.ts +119 -70
- package/src/testing/setupXdgIsolation.ts +16 -5
- package/src/testing/standardParityFixtures.ts +26 -83
- package/src/testing/store.ts +4 -4
- package/src/domains/create/commands/component.test.ts +0 -166
- package/src/domains/create/commands/component.ts +0 -123
- package/src/domains/create/commands/index.ts +0 -3
- package/src/domains/create/commands/package.test.ts +0 -35
- package/src/domains/create/commands/package.ts +0 -21
- package/src/domains/create/generators.ts +0 -8
- package/src/domains/standard/commands/categories.test.ts +0 -70
- package/src/domains/standard/commands/categories.ts +0 -48
- package/src/domains/standard/commands/index.ts +0 -6
- package/src/domains/standard/commands/list.test.ts +0 -105
- package/src/domains/standard/commands/list.ts +0 -11
- package/src/domains/standard/commands/lookup.test.ts +0 -135
- package/src/domains/standard/commands/lookup.ts +0 -14
- package/src/domains/standard/commands/sample.ts +0 -57
- package/src/domains/standard/formatters/categories.test.ts +0 -44
- package/src/domains/standard/formatters/categories.ts +0 -38
- package/src/domains/standard/formatters/index.ts +0 -10
- package/src/domains/standard/formatters/list.test.ts +0 -169
- package/src/domains/standard/formatters/list.ts +0 -184
- package/src/domains/standard/formatters/lookup.test.ts +0 -95
- package/src/domains/standard/formatters/lookup.ts +0 -101
- package/src/domains/standard/formatters/sample.ts +0 -15
- package/src/domains/standard/formatters/types.ts +0 -31
- package/src/domains/standard/index.ts +0 -40
- package/src/domains/standard/mcp/index.ts +0 -2
- package/src/domains/standard/mcp/specs.ts +0 -89
- package/src/domains/standard/operations/categories.test.ts +0 -61
- package/src/domains/standard/operations/categories.ts +0 -38
- package/src/domains/standard/operations/index.ts +0 -6
- package/src/domains/standard/operations/list.test.ts +0 -75
- package/src/domains/standard/operations/list.ts +0 -64
- package/src/domains/standard/operations/lookup.test.ts +0 -58
- package/src/domains/standard/operations/lookup.ts +0 -151
- package/src/domains/standard/operations/sample.ts +0 -35
- package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
- package/src/domains/standard/orchestration/index.ts +0 -4
- package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
- package/src/domains/standard/standardConfig.ts +0 -42
- package/src/domains/standard/stories.test.ts +0 -24
- package/src/domains/standard/stories.ts +0 -151
- package/src/domains/standard/types.ts +0 -15
- package/src/domains/tier/commands/index.ts +0 -3
- package/src/domains/tier/commands/list.test.ts +0 -67
- package/src/domains/tier/commands/list.ts +0 -9
- package/src/domains/tier/formatters/index.ts +0 -3
- package/src/domains/tier/formatters/list.test.ts +0 -33
- package/src/domains/tier/formatters/list.ts +0 -45
- package/src/domains/tier/index.ts +0 -17
- package/src/domains/tier/mcp/index.ts +0 -2
- package/src/domains/tier/mcp/specs.ts +0 -12
- package/src/domains/tier/operations/index.ts +0 -3
- package/src/domains/tier/operations/list.test.ts +0 -32
- package/src/domains/tier/operations/list.ts +0 -33
- package/src/domains/tier/stories.test.ts +0 -22
- package/src/domains/tier/stories.ts +0 -52
|
@@ -15,8 +15,13 @@ import {
|
|
|
15
15
|
RECIPE_STORY,
|
|
16
16
|
RECIPE_TTL,
|
|
17
17
|
} from "#testing";
|
|
18
|
+
import { TOKEN_READ_SURFACE_ENABLED } from "../../domains/token/featureFlag.js";
|
|
18
19
|
import type { McpErrorPayload } from "../types.js";
|
|
19
20
|
|
|
21
|
+
// token_list, token_lookup, and token_sample are gated behind the token
|
|
22
|
+
// read-surface feature flag.
|
|
23
|
+
const EXPECTED_TOOL_COUNT = TOKEN_READ_SURFACE_ENABLED ? 34 : 31;
|
|
24
|
+
|
|
20
25
|
let client: Client;
|
|
21
26
|
let cleanup: () => Promise<void>;
|
|
22
27
|
|
|
@@ -57,9 +62,9 @@ function parseData(result: Record<string, unknown>): unknown {
|
|
|
57
62
|
// =============================================================================
|
|
58
63
|
|
|
59
64
|
describe("tool listing", () => {
|
|
60
|
-
it("registers
|
|
65
|
+
it("registers the expected number of tools", async () => {
|
|
61
66
|
const { tools } = await client.listTools();
|
|
62
|
-
expect(tools).toHaveLength(
|
|
67
|
+
expect(tools).toHaveLength(EXPECTED_TOOL_COUNT);
|
|
63
68
|
});
|
|
64
69
|
|
|
65
70
|
it("all tools have descriptions", async () => {
|
|
@@ -80,8 +85,10 @@ describe("tool listing", () => {
|
|
|
80
85
|
expect(names).toContain("standard_categories");
|
|
81
86
|
expect(names).toContain("modifier_list");
|
|
82
87
|
expect(names).toContain("modifier_lookup");
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
if (TOKEN_READ_SURFACE_ENABLED) {
|
|
89
|
+
expect(names).toContain("token_list");
|
|
90
|
+
expect(names).toContain("token_lookup");
|
|
91
|
+
}
|
|
85
92
|
expect(names).toContain("tier_list");
|
|
86
93
|
expect(names).toContain("config_show");
|
|
87
94
|
expect(names).toContain("config_tier");
|
|
@@ -102,7 +109,9 @@ describe("tool listing", () => {
|
|
|
102
109
|
// Sample tools
|
|
103
110
|
expect(names).toContain("block_sample");
|
|
104
111
|
expect(names).toContain("standard_sample");
|
|
105
|
-
|
|
112
|
+
if (TOKEN_READ_SURFACE_ENABLED) {
|
|
113
|
+
expect(names).toContain("token_sample");
|
|
114
|
+
}
|
|
106
115
|
expect(names).toContain("modifier_sample");
|
|
107
116
|
});
|
|
108
117
|
|
|
@@ -112,6 +121,67 @@ describe("tool listing", () => {
|
|
|
112
121
|
expect(tool.name).not.toMatch(/^pragma_/);
|
|
113
122
|
}
|
|
114
123
|
});
|
|
124
|
+
|
|
125
|
+
// Golden surface: the full built-in tool set plus bundled packs, in
|
|
126
|
+
// registration order. The per-(noun, verb) reserved-guard flip must leave
|
|
127
|
+
// this byte-identical AND order-identical — registration order is the tool
|
|
128
|
+
// surface a client sees.
|
|
129
|
+
it("has a stable built-in tool surface", async () => {
|
|
130
|
+
const names = (await client.listTools()).tools.map((tool) => tool.name);
|
|
131
|
+
|
|
132
|
+
expect(names).toEqual([
|
|
133
|
+
"block_list",
|
|
134
|
+
"block_lookup",
|
|
135
|
+
"block_sample",
|
|
136
|
+
"modifier_list",
|
|
137
|
+
"modifier_lookup",
|
|
138
|
+
"modifier_sample",
|
|
139
|
+
...(TOKEN_READ_SURFACE_ENABLED ? ["token_list", "token_lookup"] : []),
|
|
140
|
+
"tokens_add_config",
|
|
141
|
+
...(TOKEN_READ_SURFACE_ENABLED ? ["token_sample"] : []),
|
|
142
|
+
"config_show",
|
|
143
|
+
"config_tier",
|
|
144
|
+
"config_channel",
|
|
145
|
+
"ontology_list",
|
|
146
|
+
"ontology_show",
|
|
147
|
+
"graph_query",
|
|
148
|
+
"graph_inspect",
|
|
149
|
+
"skill_list",
|
|
150
|
+
"skill_lookup",
|
|
151
|
+
"doctor",
|
|
152
|
+
"info",
|
|
153
|
+
"capabilities",
|
|
154
|
+
"llm",
|
|
155
|
+
"create_component",
|
|
156
|
+
"create_package",
|
|
157
|
+
"create_application",
|
|
158
|
+
"create_domain",
|
|
159
|
+
"create_route",
|
|
160
|
+
"create_wrapper",
|
|
161
|
+
// Bundled packs register after the built-in tools: `tier_list` and the
|
|
162
|
+
// whole `standard` noun are served by bundled story packs — the
|
|
163
|
+
// hand-written tier and standard domains were deleted.
|
|
164
|
+
"tier_list",
|
|
165
|
+
"standard_list",
|
|
166
|
+
"standard_lookup",
|
|
167
|
+
"standard_categories",
|
|
168
|
+
"standard_sample",
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("serves the standard tools from the bundled pack, not built-ins", async () => {
|
|
173
|
+
const { tools } = await client.listTools();
|
|
174
|
+
const standardList = tools.find((tool) => tool.name === "standard_list");
|
|
175
|
+
// Pack-compiled tool descriptions carry no story-pack suffix here only
|
|
176
|
+
// because the bundled pack curates them; the giveaway is the derived
|
|
177
|
+
// `search`/`category` params on the pack projection.
|
|
178
|
+
const params = Object.keys(
|
|
179
|
+
(standardList?.inputSchema as { properties?: object })?.properties ?? {},
|
|
180
|
+
);
|
|
181
|
+
expect(params).toContain("category");
|
|
182
|
+
expect(params).toContain("search");
|
|
183
|
+
expect(params).toContain("condensed");
|
|
184
|
+
});
|
|
115
185
|
});
|
|
116
186
|
|
|
117
187
|
// =============================================================================
|
|
@@ -419,7 +489,7 @@ describe("modifier_lookup", () => {
|
|
|
419
489
|
// Token tools
|
|
420
490
|
// =============================================================================
|
|
421
491
|
|
|
422
|
-
describe("token_list", () => {
|
|
492
|
+
describe.skipIf(!TOKEN_READ_SURFACE_ENABLED)("token_list", () => {
|
|
423
493
|
it("returns tokens", async () => {
|
|
424
494
|
const result = await client.callTool({
|
|
425
495
|
name: "token_list",
|
|
@@ -430,7 +500,7 @@ describe("token_list", () => {
|
|
|
430
500
|
});
|
|
431
501
|
});
|
|
432
502
|
|
|
433
|
-
describe("token_lookup", () => {
|
|
503
|
+
describe.skipIf(!TOKEN_READ_SURFACE_ENABLED)("token_lookup", () => {
|
|
434
504
|
it("returns token with theme values", async () => {
|
|
435
505
|
const result = await client.callTool({
|
|
436
506
|
name: "token_lookup",
|
|
@@ -468,10 +538,11 @@ describe("tier_list", () => {
|
|
|
468
538
|
name: "tier_list",
|
|
469
539
|
arguments: {},
|
|
470
540
|
});
|
|
471
|
-
|
|
541
|
+
// The bundled tier pack names the field `name` (its `ds:name` value).
|
|
542
|
+
const data = parseData(result) as { name: string }[];
|
|
472
543
|
expect(data.length).toBeGreaterThan(0);
|
|
473
|
-
const
|
|
474
|
-
expect(
|
|
544
|
+
const names = data.map((t) => t.name);
|
|
545
|
+
expect(names).toContain("global");
|
|
475
546
|
});
|
|
476
547
|
});
|
|
477
548
|
|
|
@@ -714,7 +785,7 @@ describe("create_component", () => {
|
|
|
714
785
|
const result = await client.callTool({
|
|
715
786
|
name: "create_component",
|
|
716
787
|
arguments: {
|
|
717
|
-
|
|
788
|
+
variant: "react",
|
|
718
789
|
componentPath: "src/components/Button",
|
|
719
790
|
},
|
|
720
791
|
});
|
|
@@ -729,7 +800,7 @@ describe("create_component", () => {
|
|
|
729
800
|
const result = await client.callTool({
|
|
730
801
|
name: "create_component",
|
|
731
802
|
arguments: {
|
|
732
|
-
|
|
803
|
+
variant: "svelte",
|
|
733
804
|
componentPath: "src/lib/components/Toggle",
|
|
734
805
|
},
|
|
735
806
|
});
|
|
@@ -738,11 +809,11 @@ describe("create_component", () => {
|
|
|
738
809
|
expect((data.generator as { name: string }).name).toBe("component/svelte");
|
|
739
810
|
});
|
|
740
811
|
|
|
741
|
-
it("returns error for invalid
|
|
812
|
+
it("returns error for invalid variant", async () => {
|
|
742
813
|
const result = await client.callTool({
|
|
743
814
|
name: "create_component",
|
|
744
815
|
arguments: {
|
|
745
|
-
|
|
816
|
+
variant: "angular",
|
|
746
817
|
componentPath: "src/components/Button",
|
|
747
818
|
},
|
|
748
819
|
});
|
|
@@ -784,7 +855,7 @@ describe("llm", () => {
|
|
|
784
855
|
expect(data.context.counts.blocks).toBeGreaterThan(0);
|
|
785
856
|
expect(data.context.counts.standards).toBeGreaterThan(0);
|
|
786
857
|
expect(data.context.namespaces.length).toBeGreaterThan(0);
|
|
787
|
-
expect(data.decisionTrees).toHaveLength(5);
|
|
858
|
+
expect(data.decisionTrees).toHaveLength(TOKEN_READ_SURFACE_ENABLED ? 5 : 4);
|
|
788
859
|
expect(data.commandReference.length).toBeGreaterThan(0);
|
|
789
860
|
});
|
|
790
861
|
});
|
|
@@ -798,7 +869,9 @@ describe("error handling", () => {
|
|
|
798
869
|
const notFoundCalls = [
|
|
799
870
|
{ name: "block_lookup", arguments: { names: ["X"] } },
|
|
800
871
|
{ name: "modifier_lookup", arguments: { names: ["X"] } },
|
|
801
|
-
|
|
872
|
+
...(TOKEN_READ_SURFACE_ENABLED
|
|
873
|
+
? [{ name: "token_lookup", arguments: { names: ["X"] } }]
|
|
874
|
+
: []),
|
|
802
875
|
{ name: "standard_lookup", arguments: { names: ["X"] } },
|
|
803
876
|
] as const;
|
|
804
877
|
|
|
@@ -872,7 +945,7 @@ describe("ontology_show", () => {
|
|
|
872
945
|
const envelope = parseEnvelope(result);
|
|
873
946
|
expect(envelope.ok).toBe(true);
|
|
874
947
|
expect(envelope.condensed).toBe(true);
|
|
875
|
-
expect(envelope.text).toEqual(expect.stringContaining("## ds:"));
|
|
948
|
+
expect(envelope.text).toEqual(expect.stringContaining("## Ontology ds:"));
|
|
876
949
|
});
|
|
877
950
|
});
|
|
878
951
|
|
|
@@ -995,7 +1068,7 @@ describe("story-pack tools", () => {
|
|
|
995
1068
|
const tools = (await scoped.client.listTools()).tools.map(
|
|
996
1069
|
(tool) => tool.name,
|
|
997
1070
|
);
|
|
998
|
-
expect(tools).toHaveLength(
|
|
1071
|
+
expect(tools).toHaveLength(EXPECTED_TOOL_COUNT + 2);
|
|
999
1072
|
expect(tools).toContain("recipe_list");
|
|
1000
1073
|
expect(tools).toContain("recipe_lookup");
|
|
1001
1074
|
|
|
@@ -1105,9 +1178,9 @@ describe("capabilities", () => {
|
|
|
1105
1178
|
expect(toolNames).toContain("block_list");
|
|
1106
1179
|
expect(toolNames).toContain("capabilities");
|
|
1107
1180
|
expect(data.tools.every((t) => t.use_when.length > 0)).toBe(true);
|
|
1108
|
-
expect(data.counts.total).toBe(
|
|
1181
|
+
expect(data.counts.total).toBe(EXPECTED_TOOL_COUNT);
|
|
1109
1182
|
expect(data.counts.read).toBeGreaterThan(0);
|
|
1110
|
-
expect(data.counts.write).toBe(
|
|
1183
|
+
expect(data.counts.write).toBe(9);
|
|
1111
1184
|
expect(data.counts.orientation).toBe(2);
|
|
1112
1185
|
expect(data.counts.diagnostic).toBe(2);
|
|
1113
1186
|
expect(data.version).toBeDefined();
|
|
@@ -1188,14 +1261,17 @@ describe("condensed parameter", () => {
|
|
|
1188
1261
|
expect((envelope.text as string).length).toBeGreaterThan(0);
|
|
1189
1262
|
});
|
|
1190
1263
|
|
|
1191
|
-
it(
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1264
|
+
it.skipIf(!TOKEN_READ_SURFACE_ENABLED)(
|
|
1265
|
+
"token_lookup returns condensed text",
|
|
1266
|
+
async () => {
|
|
1267
|
+
const result = await client.callTool({
|
|
1268
|
+
name: "token_lookup",
|
|
1269
|
+
arguments: { names: ["color.primary"], condensed: true },
|
|
1270
|
+
});
|
|
1271
|
+
const envelope = parseEnvelope(result);
|
|
1272
|
+
expect(envelope.ok).toBe(true);
|
|
1273
|
+
expect(envelope.condensed).toBe(true);
|
|
1274
|
+
expect(typeof envelope.text).toBe("string");
|
|
1275
|
+
},
|
|
1276
|
+
);
|
|
1201
1277
|
});
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
2
|
import type { PragmaContext } from "../domains/shared/context.js";
|
|
3
3
|
import type { PragmaRuntime } from "../domains/shared/runtime.js";
|
|
4
|
+
import {
|
|
5
|
+
deriveReservedVerbs,
|
|
6
|
+
isReserved,
|
|
7
|
+
nounVerbFromPath,
|
|
8
|
+
nounVerbFromToolName,
|
|
9
|
+
} from "../domains/shared/stories/pack/index.js";
|
|
10
|
+
import { TOKEN_READ_SURFACE_ENABLED } from "../domains/token/featureFlag.js";
|
|
11
|
+
import { allSpecs } from "../mcp/tools/index.js";
|
|
4
12
|
import createTestRuntime from "../testing/helpers/createTestRuntime.js";
|
|
5
|
-
import collectCommands from "./collectCommands.js";
|
|
13
|
+
import collectCommands, { builtInCommands } from "./collectCommands.js";
|
|
6
14
|
|
|
7
15
|
let runtime: PragmaRuntime;
|
|
8
16
|
|
|
@@ -34,4 +42,146 @@ describe("collectCommands", () => {
|
|
|
34
42
|
expect(paths).toContain("graph query");
|
|
35
43
|
expect(paths).toContain("graph inspect");
|
|
36
44
|
});
|
|
45
|
+
|
|
46
|
+
// Golden surface: the full built-in command set, in emission order. The
|
|
47
|
+
// per-(noun, verb) reserved-guard flip must not add, drop, rename, or REORDER
|
|
48
|
+
// a single built-in command — order drives help output and registration.
|
|
49
|
+
// `tier list` and the whole `standard` noun are no longer here: the
|
|
50
|
+
// hand-written domains were deleted and are served by bundled story packs
|
|
51
|
+
// (asserted below).
|
|
52
|
+
it("has a stable built-in command surface", () => {
|
|
53
|
+
const paths = builtInCommands(makeCtx()).map((command) =>
|
|
54
|
+
command.path.join(" "),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(paths).toEqual([
|
|
58
|
+
"config tier",
|
|
59
|
+
"config channel",
|
|
60
|
+
"config trace",
|
|
61
|
+
"config framework",
|
|
62
|
+
"config show",
|
|
63
|
+
"create component",
|
|
64
|
+
"create package",
|
|
65
|
+
"create application",
|
|
66
|
+
"create domain",
|
|
67
|
+
"create route",
|
|
68
|
+
"create wrapper",
|
|
69
|
+
"setup",
|
|
70
|
+
"setup lsp",
|
|
71
|
+
"setup mcp",
|
|
72
|
+
"setup completions",
|
|
73
|
+
"setup skills",
|
|
74
|
+
"modifier list",
|
|
75
|
+
"modifier lookup",
|
|
76
|
+
"modifier sample",
|
|
77
|
+
...(TOKEN_READ_SURFACE_ENABLED ? ["token list", "token lookup"] : []),
|
|
78
|
+
"tokens add-config",
|
|
79
|
+
...(TOKEN_READ_SURFACE_ENABLED ? ["token sample"] : []),
|
|
80
|
+
"block list",
|
|
81
|
+
"block lookup",
|
|
82
|
+
"block sample",
|
|
83
|
+
"ontology list",
|
|
84
|
+
"ontology show",
|
|
85
|
+
"graph query",
|
|
86
|
+
"graph inspect",
|
|
87
|
+
"graphql build",
|
|
88
|
+
"graphql check",
|
|
89
|
+
"graphql serve",
|
|
90
|
+
"skill list",
|
|
91
|
+
"skill lookup",
|
|
92
|
+
"doctor",
|
|
93
|
+
"info",
|
|
94
|
+
"upgrade",
|
|
95
|
+
"llm",
|
|
96
|
+
"capabilities",
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("serves `tier list` from the bundled pack, not a built-in", () => {
|
|
101
|
+
const builtInPaths = builtInCommands(makeCtx()).map((c) =>
|
|
102
|
+
c.path.join(" "),
|
|
103
|
+
);
|
|
104
|
+
const allPaths = collectCommands(makeCtx()).map((c) => c.path.join(" "));
|
|
105
|
+
|
|
106
|
+
expect(builtInPaths).not.toContain("tier list");
|
|
107
|
+
expect(allPaths).toContain("tier list");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("serves the whole `standard` noun from the bundled pack", () => {
|
|
111
|
+
const builtInPaths = builtInCommands(makeCtx()).map((c) =>
|
|
112
|
+
c.path.join(" "),
|
|
113
|
+
);
|
|
114
|
+
const allPaths = collectCommands(makeCtx()).map((c) => c.path.join(" "));
|
|
115
|
+
|
|
116
|
+
const standardVerbs = [
|
|
117
|
+
"standard list",
|
|
118
|
+
"standard lookup",
|
|
119
|
+
"standard categories",
|
|
120
|
+
"standard sample",
|
|
121
|
+
];
|
|
122
|
+
for (const path of standardVerbs) {
|
|
123
|
+
expect(builtInPaths).not.toContain(path);
|
|
124
|
+
expect(allPaths).toContain(path);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Cross-surface skew invariant (FIX 2 / R4-F1): the CLI and MCP surfaces both
|
|
130
|
+
// derive their reserved map through the single shared deriveReservedVerbs, so
|
|
131
|
+
// they MUST agree on every leaf read (noun, verb) reservation. If they drifted,
|
|
132
|
+
// a pack migrating a `list`/`lookup` verb could be admitted on one surface and
|
|
133
|
+
// blocked on the other. Operational nouns legitimately differ (graphql/setup/
|
|
134
|
+
// upgrade are CLI-only), so the invariant is scoped to the read verbs that
|
|
135
|
+
// packs actually emit.
|
|
136
|
+
describe("cross-surface reserved-verb parity", () => {
|
|
137
|
+
const READ_VERBS = ["list", "lookup"] as const;
|
|
138
|
+
|
|
139
|
+
it("CLI and MCP reserve the same list/lookup verbs for every leaf read noun", () => {
|
|
140
|
+
// Both surfaces derive reservations from BUILT-INS only (production reserves
|
|
141
|
+
// from built-ins, before appending packs), so a bundled/config/package pack
|
|
142
|
+
// never inflates the reserved map. Using collectCommands here would fold the
|
|
143
|
+
// bundled tier pack into the CLI side and break the symmetry.
|
|
144
|
+
const cliPairs = builtInCommands(makeCtx()).map((command) =>
|
|
145
|
+
nounVerbFromPath(command.path),
|
|
146
|
+
);
|
|
147
|
+
const mcpPairs = allSpecs.map((spec) => nounVerbFromToolName(spec.name));
|
|
148
|
+
|
|
149
|
+
const cliReserved = deriveReservedVerbs(cliPairs);
|
|
150
|
+
const mcpReserved = deriveReservedVerbs(mcpPairs);
|
|
151
|
+
|
|
152
|
+
// FIX 1 on the real CLI surface: operational nouns own no list/lookup, so
|
|
153
|
+
// they are reserved wholesale — a pack emitting `list` stays blocked.
|
|
154
|
+
for (const noun of ["config", "graph", "graphql", "setup", "create"]) {
|
|
155
|
+
expect(isReserved(cliReserved, noun, "list")).toBe(true);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Leaf read nouns: those that own a `list`/`lookup` verb on either surface.
|
|
159
|
+
const readNouns = new Set(
|
|
160
|
+
[...cliPairs, ...mcpPairs]
|
|
161
|
+
.filter(([, verb]) => verb === "list" || verb === "lookup")
|
|
162
|
+
.map(([noun]) => noun),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Sanity: the real leaf-migration targets are present, so this asserts
|
|
166
|
+
// something (a silently empty set would make the test vacuous). `tier`
|
|
167
|
+
// and `standard` are no longer here — both were cut over to bundled
|
|
168
|
+
// packs, so they are correctly absent from the built-in reserved
|
|
169
|
+
// surface on both sides. The token noun only owns read verbs while its
|
|
170
|
+
// feature flag is on.
|
|
171
|
+
const expectedReadNouns = ["block", "modifier"];
|
|
172
|
+
if (TOKEN_READ_SURFACE_ENABLED) expectedReadNouns.push("token");
|
|
173
|
+
for (const noun of expectedReadNouns) {
|
|
174
|
+
expect(readNouns.has(noun)).toBe(true);
|
|
175
|
+
}
|
|
176
|
+
expect(readNouns.has("standard")).toBe(false);
|
|
177
|
+
|
|
178
|
+
for (const noun of readNouns) {
|
|
179
|
+
for (const verb of READ_VERBS) {
|
|
180
|
+
expect(
|
|
181
|
+
isReserved(cliReserved, noun, verb),
|
|
182
|
+
`CLI/MCP disagree on ${noun} ${verb}`,
|
|
183
|
+
).toBe(isReserved(mcpReserved, noun, verb));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
37
187
|
});
|
|
@@ -14,31 +14,27 @@ import { commands as modifierCommands } from "../domains/modifier/index.js";
|
|
|
14
14
|
import { commands as ontologyCommands } from "../domains/ontology/index.js";
|
|
15
15
|
import { commands as setupCommands } from "../domains/setup/index.js";
|
|
16
16
|
import type { PragmaContext } from "../domains/shared/context.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
compilePackCommands,
|
|
19
|
+
deriveReservedVerbs,
|
|
20
|
+
nounVerbFromPath,
|
|
21
|
+
} from "../domains/shared/stories/pack/index.js";
|
|
18
22
|
import { commands as skillCommands } from "../domains/skill/index.js";
|
|
19
|
-
import { commands as standardCommands } from "../domains/standard/index.js";
|
|
20
|
-
import { commands as tierCommands } from "../domains/tier/index.js";
|
|
21
23
|
import { commands as tokenCommands } from "../domains/token/index.js";
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Extracted from runCli so the completions server can import
|
|
27
|
-
* the command list independently without running the full CLI pipeline.
|
|
26
|
+
* The built-in (hand-written domain) command surface, in emission order.
|
|
28
27
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* Excludes story-pack commands (bundled, config, or package). Exported so the
|
|
29
|
+
* reserved-verb derivation and its tests share the exact input production uses
|
|
30
|
+
* — the reserved map is built from built-ins only, never from packs.
|
|
31
31
|
*/
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
): CommandDefinition[] {
|
|
35
|
-
const builtIn: CommandDefinition[] = [
|
|
32
|
+
export function builtInCommands(ctx: PragmaContext): CommandDefinition[] {
|
|
33
|
+
return [
|
|
36
34
|
...configCommands(ctx),
|
|
37
35
|
...createCommands(),
|
|
38
36
|
...setupCommands(),
|
|
39
|
-
...standardCommands(ctx),
|
|
40
37
|
...modifierCommands(ctx),
|
|
41
|
-
...tierCommands(ctx),
|
|
42
38
|
...tokenCommands(ctx),
|
|
43
39
|
...blockCommands(ctx),
|
|
44
40
|
...ontologyCommands(ctx),
|
|
@@ -51,10 +47,29 @@ export default function collectCommands(
|
|
|
51
47
|
buildLlmCommand(ctx),
|
|
52
48
|
buildCapabilitiesCommand(),
|
|
53
49
|
];
|
|
50
|
+
}
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Collect all CLI command definitions: built-in domains plus story packs.
|
|
54
|
+
*
|
|
55
|
+
* Extracted from runCli so the completions server can import the command list
|
|
56
|
+
* independently without running the full CLI pipeline.
|
|
57
|
+
*
|
|
58
|
+
* Story packs (bundled transitional packs, plus config- and package-declared
|
|
59
|
+
* stories) project onto the same surface. Leaf read nouns reserve only the
|
|
60
|
+
* (noun, verb) pairs they own, so a pack can add a verb no built-in owns and,
|
|
61
|
+
* once a built-in leaf is deleted, its bundled pack takes over the freed noun;
|
|
62
|
+
* operational nouns (config, graph, setup, …) stay reserved wholesale.
|
|
63
|
+
*
|
|
64
|
+
* @param ctx - The pragma context providing store, config, and global flags.
|
|
65
|
+
* @returns The full array of command definitions for all domains.
|
|
66
|
+
*/
|
|
67
|
+
export default function collectCommands(
|
|
68
|
+
ctx: PragmaContext,
|
|
69
|
+
): CommandDefinition[] {
|
|
70
|
+
const builtIn = builtInCommands(ctx);
|
|
71
|
+
const reserved = deriveReservedVerbs(
|
|
72
|
+
builtIn.map((command) => nounVerbFromPath(command.path)),
|
|
58
73
|
);
|
|
59
|
-
return [...builtIn, ...compilePackCommands(ctx,
|
|
74
|
+
return [...builtIn, ...compilePackCommands(ctx, reserved)];
|
|
60
75
|
}
|
|
@@ -36,9 +36,13 @@ describe("createProgram", () => {
|
|
|
36
36
|
}
|
|
37
37
|
)._events.beforeAllHelp({ command: program, write });
|
|
38
38
|
|
|
39
|
+
// Assert on single-chalk-span strings so the check is robust whether or
|
|
40
|
+
// not color is enabled (CI renders with color; ANSI resets would otherwise
|
|
41
|
+
// split a multi-word phrase like "Usage: pragma").
|
|
39
42
|
expect(write).toHaveBeenCalledWith(
|
|
40
|
-
expect.stringContaining("
|
|
43
|
+
expect.stringContaining("For AI agents"),
|
|
41
44
|
);
|
|
45
|
+
expect(write).toHaveBeenCalledWith(expect.stringContaining("Global flags"));
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
it("uses llm root help when llm is enabled", async () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type CommandDefinition,
|
|
3
3
|
detectRenderMode,
|
|
4
|
-
formatHelp,
|
|
5
4
|
formatLlmHelp,
|
|
6
5
|
type HandleResultOptions,
|
|
7
6
|
registerAll,
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
import { Command } from "commander";
|
|
10
9
|
import { PROGRAM_DESCRIPTION, PROGRAM_NAME, VERSION } from "../constants.js";
|
|
11
10
|
import type { PragmaContext } from "../domains/shared/context.js";
|
|
11
|
+
import formatRootHelp from "./rootHelp.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Build and configure the top-level Commander program.
|
|
@@ -57,7 +57,7 @@ export default function createProgram(
|
|
|
57
57
|
if (ctx.globalFlags.llm) {
|
|
58
58
|
return formatLlmHelp(PROGRAM_NAME, commands);
|
|
59
59
|
}
|
|
60
|
-
return
|
|
60
|
+
return formatRootHelp(PROGRAM_NAME, PROGRAM_DESCRIPTION, commands);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
program.helpOption("-h, --help", "Show help");
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import ensureFirstRun from "./firstRun.js";
|
|
6
|
+
|
|
7
|
+
let xdg: string;
|
|
8
|
+
let savedXdg: string | undefined;
|
|
9
|
+
let lines: string[];
|
|
10
|
+
|
|
11
|
+
const write = (line: string) => {
|
|
12
|
+
lines.push(line);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
savedXdg = process.env.XDG_CONFIG_HOME;
|
|
17
|
+
xdg = mkdtempSync(join(tmpdir(), "pragma-firstrun-"));
|
|
18
|
+
process.env.XDG_CONFIG_HOME = xdg;
|
|
19
|
+
lines = [];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
process.env.XDG_CONFIG_HOME = savedXdg;
|
|
24
|
+
rmSync(xdg, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const configPath = () => join(xdg, "pragma", "config.json");
|
|
28
|
+
|
|
29
|
+
describe("ensureFirstRun", () => {
|
|
30
|
+
it("creates the global config with defaults and greets on first run", async () => {
|
|
31
|
+
await ensureFirstRun(write);
|
|
32
|
+
|
|
33
|
+
expect(existsSync(configPath())).toBe(true);
|
|
34
|
+
expect(JSON.parse(readFileSync(configPath(), "utf-8"))).toEqual({});
|
|
35
|
+
|
|
36
|
+
const text = lines.join("\n");
|
|
37
|
+
expect(text).toContain("pre-release pragma CLI");
|
|
38
|
+
expect(text).toContain("github.com/canonical/pragma");
|
|
39
|
+
expect(text).toContain(configPath());
|
|
40
|
+
expect(text).toContain("pragma.config.json");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("is silent and leaves the config untouched on subsequent runs", async () => {
|
|
44
|
+
await ensureFirstRun(write);
|
|
45
|
+
lines = [];
|
|
46
|
+
|
|
47
|
+
await ensureFirstRun(write);
|
|
48
|
+
|
|
49
|
+
expect(lines).toEqual([]);
|
|
50
|
+
expect(readFileSync(configPath(), "utf-8")).toBe("{}\n");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("never overwrites an existing config", async () => {
|
|
54
|
+
await ensureFirstRun(write);
|
|
55
|
+
const custom = '{ "channel": "experimental" }\n';
|
|
56
|
+
const { writeFileSync } = await import("node:fs");
|
|
57
|
+
writeFileSync(configPath(), custom);
|
|
58
|
+
lines = [];
|
|
59
|
+
|
|
60
|
+
await ensureFirstRun(write);
|
|
61
|
+
|
|
62
|
+
expect(readFileSync(configPath(), "utf-8")).toBe(custom);
|
|
63
|
+
expect(lines).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("degrades to a warning instead of throwing when the config cannot be created", async () => {
|
|
67
|
+
// Point XDG at a path whose parent is a FILE, so mkdir must fail.
|
|
68
|
+
const blocker = join(xdg, "blocker");
|
|
69
|
+
const { writeFileSync } = await import("node:fs");
|
|
70
|
+
writeFileSync(blocker, "not a directory");
|
|
71
|
+
process.env.XDG_CONFIG_HOME = join(blocker, "nested");
|
|
72
|
+
|
|
73
|
+
await expect(ensureFirstRun(write)).resolves.toBeUndefined();
|
|
74
|
+
// Exactly one warning line and no welcome banner: creation is attempted
|
|
75
|
+
// before the greeting, so a failure degrades to a single stderr warning.
|
|
76
|
+
expect(lines).toHaveLength(1);
|
|
77
|
+
expect(lines[0]).toContain("could not create the global pragma config");
|
|
78
|
+
expect(lines.join("\n")).not.toContain("pre-release pragma CLI");
|
|
79
|
+
});
|
|
80
|
+
});
|