@canonical/pragma-cli 0.18.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +166 -0
- package/dist/pragma +0 -0
- package/package.json +31 -18
- package/src/constants.ts +1 -1
- package/src/domains/block/blockConfig.ts +47 -3
- package/src/domains/block/commands/list.test.ts +1 -1
- package/src/domains/block/commands/list.ts +27 -0
- package/src/domains/block/commands/lookup.ts +26 -52
- package/src/domains/block/formatters/index.ts +1 -1
- package/src/domains/block/formatters/list.test.ts +1 -1
- package/src/domains/block/formatters/list.ts +28 -42
- package/src/domains/block/formatters/lookup.test.ts +3 -2
- package/src/domains/block/formatters/lookup.ts +91 -296
- package/src/domains/block/formatters/types.ts +1 -1
- package/src/domains/block/operations/list.test.ts +1 -1
- package/src/domains/block/operations/list.ts +1 -1
- package/src/domains/block/operations/lookup.test.ts +29 -8
- package/src/domains/block/operations/lookup.ts +94 -16
- package/src/domains/block/orchestration/blockEmptyError.ts +1 -1
- package/src/domains/block/orchestration/buildBlockFilters.ts +1 -1
- package/src/domains/block/orchestration/enrichBlocks.ts +6 -2
- package/src/domains/block/orchestration/resolveBlockList.ts +1 -1
- package/src/domains/block/orchestration/resolveBlockLookup.ts +8 -2
- package/src/domains/block/types.ts +1 -1
- package/src/domains/config/commands/channel.ts +6 -4
- package/src/domains/config/commands/tier.ts +6 -4
- package/src/domains/config/mcp/specs.ts +18 -6
- package/src/domains/config/operations/validateTier.ts +1 -1
- package/src/domains/config/tasks/index.ts +3 -0
- package/src/domains/config/tasks/setChannel.ts +30 -0
- package/src/domains/config/tasks/setTier.ts +33 -0
- package/src/domains/create/commands/package.test.ts +2 -2
- package/src/domains/doctor/mcp/index.ts +1 -1
- package/src/domains/doctor/mcp/specs.ts +2 -49
- package/src/domains/graph/commands/inspect.test.ts +1 -1
- package/src/domains/graph/formatters/inspect.ts +12 -7
- package/src/domains/graph/operations/inspectUri.ts +4 -1
- package/src/domains/info/commands/upgrade.ts +6 -3
- package/src/domains/info/index.ts +3 -1
- package/src/domains/info/mcp/index.ts +2 -0
- package/src/domains/info/mcp/specs.ts +65 -0
- package/src/domains/llm/data/commandReference.ts +4 -4
- package/src/domains/llm/data/decisionTrees.ts +14 -12
- package/src/domains/llm/operations/collectContext.ts +1 -1
- package/src/domains/modifier/commands/list.ts +9 -0
- package/src/domains/modifier/commands/lookup.ts +22 -42
- package/src/domains/modifier/formatters/index.ts +4 -1
- package/src/domains/modifier/formatters/list.test.ts +1 -1
- package/src/domains/modifier/formatters/list.ts +13 -26
- package/src/domains/modifier/formatters/lookup.test.ts +1 -1
- package/src/domains/modifier/formatters/lookup.ts +41 -29
- package/src/domains/modifier/index.ts +1 -0
- package/src/domains/modifier/modifierConfig.ts +36 -0
- package/src/domains/modifier/operations/list.ts +1 -1
- package/src/domains/modifier/operations/lookup.ts +4 -3
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +1 -1
- package/src/domains/modifier/types.ts +1 -1
- package/src/domains/ontology/commands/list.test.ts +1 -1
- package/src/domains/ontology/commands/list.ts +18 -0
- package/src/domains/ontology/commands/show.test.ts +1 -1
- package/src/domains/ontology/formatters/list.ts +5 -3
- package/src/domains/ontology/formatters/show.ts +38 -13
- package/src/domains/ontology/helpers/queryClasses.ts +1 -1
- package/src/domains/ontology/helpers/queryProperties.ts +1 -1
- package/src/domains/ontology/operations/listOntologies.ts +1 -1
- package/src/domains/ontology/operations/showOntology.ts +1 -1
- package/src/domains/setup/helpers/completionScripts.test.ts +3 -3
- package/src/domains/setup/helpers/completionScripts.ts +4 -4
- package/src/domains/shared/ToolSpec.ts +1 -1
- package/src/domains/shared/context.ts +3 -2
- package/src/domains/shared/contracts.ts +40 -1
- package/src/domains/shared/filters/buildFilterConfig.ts +1 -2
- package/src/domains/shared/filters/buildFilters.ts +1 -1
- package/src/domains/shared/filters/describeFilters.ts +1 -1
- package/src/domains/shared/formatters.ts +47 -0
- package/src/domains/shared/renderers.ts +410 -0
- package/src/domains/shared/runtime.ts +7 -22
- package/src/domains/shared/types/block.ts +107 -0
- package/src/domains/shared/types/common.ts +33 -0
- package/src/domains/shared/types/graph.ts +17 -0
- package/src/domains/shared/types/index.ts +49 -0
- package/src/domains/shared/types/modifier.ts +22 -0
- package/src/domains/shared/types/ontology.ts +50 -0
- package/src/domains/shared/types/runtime.ts +21 -0
- package/src/domains/shared/types/standard.ts +64 -0
- package/src/domains/shared/types/tier.ts +15 -0
- package/src/domains/shared/types/token.ts +30 -0
- package/src/domains/standard/commands/list.ts +9 -0
- package/src/domains/standard/commands/lookup.ts +22 -50
- package/src/domains/standard/formatters/categories.test.ts +1 -1
- package/src/domains/standard/formatters/categories.ts +1 -1
- package/src/domains/standard/formatters/index.ts +4 -1
- package/src/domains/standard/formatters/list.test.ts +4 -1
- package/src/domains/standard/formatters/list.ts +112 -84
- package/src/domains/standard/formatters/lookup.test.ts +1 -1
- package/src/domains/standard/formatters/lookup.ts +85 -70
- package/src/domains/standard/formatters/types.ts +1 -1
- package/src/domains/standard/index.ts +1 -0
- package/src/domains/standard/operations/categories.ts +1 -1
- package/src/domains/standard/operations/list.ts +1 -1
- package/src/domains/standard/operations/lookup.ts +1 -1
- package/src/domains/standard/orchestration/buildStandardFilters.ts +1 -1
- package/src/domains/standard/orchestration/resolveStandardList.ts +4 -1
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +1 -1
- package/src/domains/standard/orchestration/standardEmptyError.ts +1 -1
- package/src/domains/standard/standardConfig.ts +42 -0
- package/src/domains/standard/types.ts +1 -1
- package/src/domains/tier/commands/list.ts +16 -0
- package/src/domains/tier/formatters/list.test.ts +1 -1
- package/src/domains/tier/formatters/list.ts +11 -6
- package/src/domains/tier/operations/list.ts +1 -1
- package/src/domains/token/commands/list.ts +9 -0
- package/src/domains/token/commands/lookup.ts +22 -48
- package/src/domains/token/formatters/index.ts +4 -1
- package/src/domains/token/formatters/list.test.ts +1 -1
- package/src/domains/token/formatters/list.ts +15 -28
- package/src/domains/token/formatters/lookup.test.ts +2 -2
- package/src/domains/token/formatters/lookup.ts +80 -45
- package/src/domains/token/index.ts +1 -0
- package/src/domains/token/operations/list.ts +1 -1
- package/src/domains/token/operations/lookup.ts +1 -1
- package/src/domains/token/orchestration/resolveTokenLookup.ts +1 -1
- package/src/domains/token/tokenConfig.ts +38 -0
- package/src/domains/token/types.ts +1 -1
- package/src/index.ts +1 -1
- package/src/mcp/runMcpServer.test.ts +2 -2
- package/src/mcp/tools/index.ts +2 -0
- package/src/mcp/tools/registerTools.test.ts +8 -3
- package/src/package-manager/types.ts +1 -3
- package/src/pipeline/createProgram.ts +20 -1
- package/src/pipeline/index.ts +4 -1
- package/src/pipeline/parseGlobalFlags.test.ts +92 -0
- package/src/pipeline/parseGlobalFlags.ts +25 -0
- package/src/pipeline/runCli.orchestration.test.ts +1 -0
- package/src/pipeline/runCli.ts +11 -4
- package/src/pipeline/runInteractiveCommand.ts +198 -0
- package/src/testing/cli.ts +2 -2
- package/src/testing/fixtures.ts +2 -2
- package/src/testing/integration/boot.test.ts +2 -2
- package/src/testing/integration/cli-user-stories.test.ts +198 -0
- package/src/testing/integration/edge-cases.test.ts +263 -0
- package/src/testing/integration/parity.test.ts +377 -27
- package/src/testing/store.ts +1 -1
- package/src/tui/components/BulletList/BulletList.tsx +46 -0
- package/src/tui/components/BulletList/index.ts +2 -0
- package/src/tui/components/BulletList/types.ts +6 -0
- package/src/tui/components/Card/Card.tsx +73 -0
- package/src/tui/components/Card/index.ts +1 -0
- package/src/tui/components/Card/types.ts +12 -0
- package/src/tui/components/CodeBlock/CodeBlock.tsx +37 -0
- package/src/tui/components/CodeBlock/index.ts +2 -0
- package/src/tui/components/CodeBlock/types.ts +6 -0
- package/src/tui/components/DataRow/DataRow.tsx +80 -0
- package/src/tui/components/DataRow/index.ts +2 -0
- package/src/tui/components/DataRow/types.ts +14 -0
- package/src/tui/components/FieldRow/FieldRow.tsx +24 -0
- package/src/tui/components/FieldRow/index.ts +2 -0
- package/src/tui/components/FieldRow/types.ts +10 -0
- package/src/tui/components/HeaderRow/HeaderRow.tsx +32 -0
- package/src/tui/components/HeaderRow/index.ts +2 -0
- package/src/tui/components/HeaderRow/types.ts +8 -0
- package/src/tui/components/MarkdownText/MarkdownText.tsx +19 -0
- package/src/tui/components/MarkdownText/index.ts +2 -0
- package/src/tui/components/MarkdownText/types.ts +4 -0
- package/src/tui/components/MiniTable/MiniTable.tsx +82 -0
- package/src/tui/components/MiniTable/index.ts +2 -0
- package/src/tui/components/MiniTable/types.ts +6 -0
- package/src/tui/components/SectionRenderer/SectionRenderer.tsx +91 -0
- package/src/tui/components/SectionRenderer/index.ts +2 -0
- package/src/tui/components/SectionRenderer/types.ts +15 -0
- package/src/tui/components/TreeView/TreeView.tsx +84 -0
- package/src/tui/components/TreeView/index.ts +2 -0
- package/src/tui/components/TreeView/types.ts +6 -0
- package/src/tui/components/index.ts +19 -0
- package/src/tui/constants.ts +39 -0
- package/src/tui/createListView.ts +23 -0
- package/src/tui/createLookupView.ts +26 -0
- package/src/tui/helpers/computeWidths.ts +53 -0
- package/src/tui/helpers/fitColumns.ts +33 -0
- package/src/tui/helpers/formatMarkdown.ts +19 -0
- package/src/tui/helpers/getTerminalSize.ts +16 -0
- package/src/tui/helpers/index.ts +6 -0
- package/src/tui/helpers/syntaxColor.ts +67 -0
- package/src/tui/helpers/truncateText.ts +12 -0
- package/src/tui/hooks/index.ts +3 -0
- package/src/tui/hooks/types.ts +7 -0
- package/src/tui/hooks/useAutoExit.ts +17 -0
- package/src/tui/hooks/useTerminalSize.ts +19 -0
- package/src/tui/index.ts +15 -0
- package/src/tui/renderInk.tsx +18 -0
- package/src/tui/types.ts +12 -0
- package/src/tui/views/ListView/ListView.tsx +104 -0
- package/src/tui/views/ListView/index.ts +2 -0
- package/src/tui/views/ListView/types.ts +14 -0
- package/src/tui/views/LookupView/LookupView.tsx +244 -0
- package/src/tui/views/LookupView/index.ts +2 -0
- package/src/tui/views/LookupView/types.ts +16 -0
- package/src/tui/views/index.ts +4 -0
- package/src/domains/shared/types.test-d.ts +0 -127
- package/src/domains/shared/types.ts +0 -348
|
@@ -3,28 +3,68 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each test calls the same query through both the operation layer
|
|
5
5
|
* (what CLI uses) and the MCP tool (what agents use), then asserts
|
|
6
|
-
* data equality.
|
|
6
|
+
* data equality. Condensed-mode tests verify that the MCP condensed
|
|
7
|
+
* text matches what the LLM formatter produces for the same data.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
10
11
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
11
|
-
|
|
12
|
+
// ---- Shared utilities ----
|
|
13
|
+
import { VERSION } from "#constants";
|
|
14
|
+
import { detectInstallSource } from "#package-manager";
|
|
15
|
+
// ---- Formatters ----
|
|
16
|
+
import {
|
|
17
|
+
listFormatters as blockListFmt,
|
|
18
|
+
lookupFormatters as blockLookupFmt,
|
|
19
|
+
} from "../../domains/block/formatters/index.js";
|
|
20
|
+
// ---- Operations ----
|
|
12
21
|
import {
|
|
13
22
|
listBlocks,
|
|
14
23
|
lookupBlock,
|
|
15
24
|
} from "../../domains/block/operations/index.js";
|
|
25
|
+
import { showFormatters as configShowFmt } from "../../domains/config/formatters/index.js";
|
|
26
|
+
import { resolveConfigShow } from "../../domains/config/operations/index.js";
|
|
27
|
+
import { renderInfoLlm } from "../../domains/info/formatters/index.js";
|
|
28
|
+
import { collectStoreSummary } from "../../domains/info/operations/index.js";
|
|
29
|
+
import type { InfoData } from "../../domains/info/types.js";
|
|
30
|
+
import {
|
|
31
|
+
listFormatters as modifierListFmt,
|
|
32
|
+
lookupFormatters as modifierLookupFmt,
|
|
33
|
+
} from "../../domains/modifier/formatters/index.js";
|
|
16
34
|
import {
|
|
17
35
|
listModifiers,
|
|
18
36
|
lookupModifier,
|
|
19
37
|
} from "../../domains/modifier/operations/index.js";
|
|
20
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
listOntologies,
|
|
40
|
+
showOntology,
|
|
41
|
+
} from "../../domains/ontology/operations/index.js";
|
|
42
|
+
import { CHANNEL_RELEASES } from "../../domains/shared/filters/buildChannelFilter.js";
|
|
43
|
+
import { resolveTierChain } from "../../domains/shared/filters/buildTierFilter.js";
|
|
21
44
|
import type { PragmaRuntime } from "../../domains/shared/runtime.js";
|
|
22
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
type StandardListOutput,
|
|
47
|
+
categoriesFormatters as standardCatFmt,
|
|
48
|
+
listFormatters as standardListFmt,
|
|
49
|
+
lookupFormatters as standardLookupFmt,
|
|
50
|
+
} from "../../domains/standard/formatters/index.js";
|
|
51
|
+
import {
|
|
52
|
+
listCategories,
|
|
53
|
+
listStandards,
|
|
54
|
+
lookupStandard,
|
|
55
|
+
} from "../../domains/standard/operations/index.js";
|
|
56
|
+
import { listFormatters as tierListFmt } from "../../domains/tier/formatters/index.js";
|
|
23
57
|
import { listTiers } from "../../domains/tier/operations/index.js";
|
|
58
|
+
import {
|
|
59
|
+
createLookupFormatters as createTokenLookupFmt,
|
|
60
|
+
listFormatters as tokenListFmt,
|
|
61
|
+
} from "../../domains/token/formatters/index.js";
|
|
24
62
|
import {
|
|
25
63
|
listTokens,
|
|
26
64
|
lookupToken,
|
|
27
65
|
} from "../../domains/token/operations/index.js";
|
|
66
|
+
|
|
67
|
+
// ---- Test helpers ----
|
|
28
68
|
import assertParity from "../helpers/assertParity.js";
|
|
29
69
|
import createTestMcpClient from "../helpers/createTestMcpClient.js";
|
|
30
70
|
import createTestRuntime from "../helpers/createTestRuntime.js";
|
|
@@ -45,6 +85,30 @@ afterAll(async () => {
|
|
|
45
85
|
rt.dispose();
|
|
46
86
|
});
|
|
47
87
|
|
|
88
|
+
/** Parse condensed MCP response envelope. */
|
|
89
|
+
function parseCondensed(mcpResponse: Record<string, unknown>): {
|
|
90
|
+
text: string;
|
|
91
|
+
condensed: boolean;
|
|
92
|
+
tokens: string;
|
|
93
|
+
} {
|
|
94
|
+
const content = mcpResponse.content as unknown[];
|
|
95
|
+
const first = content[0] as { text: string };
|
|
96
|
+
return JSON.parse(first.text) as {
|
|
97
|
+
text: string;
|
|
98
|
+
condensed: boolean;
|
|
99
|
+
tokens: string;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Parse MCP response envelope. */
|
|
104
|
+
function parseEnvelope(
|
|
105
|
+
mcpResponse: Record<string, unknown>,
|
|
106
|
+
): Record<string, unknown> {
|
|
107
|
+
const content = mcpResponse.content as unknown[];
|
|
108
|
+
const first = content[0] as { text: string };
|
|
109
|
+
return JSON.parse(first.text) as Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
48
112
|
// ---------------------------------------------------------------------------
|
|
49
113
|
// Block parity
|
|
50
114
|
// ---------------------------------------------------------------------------
|
|
@@ -67,24 +131,43 @@ describe("block parity", () => {
|
|
|
67
131
|
name: "block_list",
|
|
68
132
|
arguments: { condensed: true },
|
|
69
133
|
});
|
|
70
|
-
const
|
|
71
|
-
const first = content[0] as { text: string };
|
|
72
|
-
const body = JSON.parse(first.text) as { text: string };
|
|
134
|
+
const body = parseCondensed(mcpRes);
|
|
73
135
|
expect(body.text).toBe(expectedText);
|
|
74
136
|
});
|
|
75
137
|
|
|
76
|
-
it("block_lookup: detailed
|
|
77
|
-
const
|
|
138
|
+
it("block_lookup: detailed result matches", async () => {
|
|
139
|
+
const opResults = await lookupBlock(rt.store, "Button", rt.config);
|
|
140
|
+
expect(opResults.length).toBeGreaterThan(0);
|
|
141
|
+
const opResult = opResults[0];
|
|
78
142
|
const mcpRes = await client.callTool({
|
|
79
143
|
name: "block_lookup",
|
|
80
144
|
arguments: { names: ["Button"] },
|
|
81
145
|
});
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
146
|
+
const body = parseEnvelope(mcpRes);
|
|
147
|
+
expect(body.data).toEqual({ results: [opResult], errors: [] });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("block_lookup condensed: matches llm formatter", async () => {
|
|
151
|
+
const opResults = await lookupBlock(rt.store, "Button", rt.config);
|
|
152
|
+
expect(opResults.length).toBeGreaterThan(0);
|
|
153
|
+
const opResult = opResults[0];
|
|
154
|
+
const expectedText = blockLookupFmt.llm({
|
|
155
|
+
block: opResult,
|
|
156
|
+
detailed: true,
|
|
157
|
+
aspects: {
|
|
158
|
+
anatomy: true,
|
|
159
|
+
modifiers: true,
|
|
160
|
+
tokens: true,
|
|
161
|
+
implementations: true,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const mcpRes = await client.callTool({
|
|
166
|
+
name: "block_lookup",
|
|
167
|
+
arguments: { names: ["Button"], condensed: true },
|
|
168
|
+
});
|
|
169
|
+
const body = parseCondensed(mcpRes);
|
|
170
|
+
expect(body.text).toBe(expectedText);
|
|
88
171
|
});
|
|
89
172
|
});
|
|
90
173
|
|
|
@@ -101,6 +184,80 @@ describe("standard parity", () => {
|
|
|
101
184
|
});
|
|
102
185
|
assertParity(opResult, mcpRes);
|
|
103
186
|
});
|
|
187
|
+
|
|
188
|
+
it("standard_list condensed: matches llm formatter", async () => {
|
|
189
|
+
const opResult = await listStandards(rt.store);
|
|
190
|
+
const output: StandardListOutput = {
|
|
191
|
+
items: opResult,
|
|
192
|
+
details: undefined,
|
|
193
|
+
disclosure: { level: "summary" },
|
|
194
|
+
};
|
|
195
|
+
const expectedText = standardListFmt.llm(output);
|
|
196
|
+
|
|
197
|
+
const mcpRes = await client.callTool({
|
|
198
|
+
name: "standard_list",
|
|
199
|
+
arguments: { condensed: true },
|
|
200
|
+
});
|
|
201
|
+
const body = parseCondensed(mcpRes);
|
|
202
|
+
expect(body.text).toBe(expectedText);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("standard_lookup: detailed fields match", async () => {
|
|
206
|
+
const opResult = await lookupStandard(
|
|
207
|
+
rt.store,
|
|
208
|
+
"react/component/folder-structure",
|
|
209
|
+
);
|
|
210
|
+
const mcpRes = await client.callTool({
|
|
211
|
+
name: "standard_lookup",
|
|
212
|
+
arguments: { names: ["react/component/folder-structure"] },
|
|
213
|
+
});
|
|
214
|
+
const body = parseEnvelope(mcpRes);
|
|
215
|
+
const data = body.data as { results: unknown[]; errors: unknown[] };
|
|
216
|
+
expect(data.results[0]).toEqual(opResult);
|
|
217
|
+
expect(data.errors).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("standard_lookup condensed: matches llm formatter", async () => {
|
|
221
|
+
const opResult = await lookupStandard(
|
|
222
|
+
rt.store,
|
|
223
|
+
"react/component/folder-structure",
|
|
224
|
+
);
|
|
225
|
+
const expectedText = standardLookupFmt.llm({
|
|
226
|
+
standard: opResult,
|
|
227
|
+
detailed: true,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const mcpRes = await client.callTool({
|
|
231
|
+
name: "standard_lookup",
|
|
232
|
+
arguments: {
|
|
233
|
+
names: ["react/component/folder-structure"],
|
|
234
|
+
condensed: true,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
const body = parseCondensed(mcpRes);
|
|
238
|
+
expect(body.text).toBe(expectedText);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("standard_categories: data matches", async () => {
|
|
242
|
+
const opResult = await listCategories(rt.store);
|
|
243
|
+
const mcpRes = await client.callTool({
|
|
244
|
+
name: "standard_categories",
|
|
245
|
+
arguments: {},
|
|
246
|
+
});
|
|
247
|
+
assertParity(opResult, mcpRes);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("standard_categories condensed: matches llm formatter", async () => {
|
|
251
|
+
const opResult = await listCategories(rt.store);
|
|
252
|
+
const expectedText = standardCatFmt.llm(opResult);
|
|
253
|
+
|
|
254
|
+
const mcpRes = await client.callTool({
|
|
255
|
+
name: "standard_categories",
|
|
256
|
+
arguments: { condensed: true },
|
|
257
|
+
});
|
|
258
|
+
const body = parseCondensed(mcpRes);
|
|
259
|
+
expect(body.text).toBe(expectedText);
|
|
260
|
+
});
|
|
104
261
|
});
|
|
105
262
|
|
|
106
263
|
// ---------------------------------------------------------------------------
|
|
@@ -116,6 +273,18 @@ describe("tier parity", () => {
|
|
|
116
273
|
});
|
|
117
274
|
assertParity(opResult, mcpRes);
|
|
118
275
|
});
|
|
276
|
+
|
|
277
|
+
it("tier_list condensed: matches llm formatter", async () => {
|
|
278
|
+
const opResult = await listTiers(rt.store);
|
|
279
|
+
const expectedText = tierListFmt.llm(opResult);
|
|
280
|
+
|
|
281
|
+
const mcpRes = await client.callTool({
|
|
282
|
+
name: "tier_list",
|
|
283
|
+
arguments: { condensed: true },
|
|
284
|
+
});
|
|
285
|
+
const body = parseCondensed(mcpRes);
|
|
286
|
+
expect(body.text).toBe(expectedText);
|
|
287
|
+
});
|
|
119
288
|
});
|
|
120
289
|
|
|
121
290
|
// ---------------------------------------------------------------------------
|
|
@@ -132,18 +301,39 @@ describe("modifier parity", () => {
|
|
|
132
301
|
assertParity(opResult, mcpRes);
|
|
133
302
|
});
|
|
134
303
|
|
|
304
|
+
it("modifier_list condensed: matches llm formatter", async () => {
|
|
305
|
+
const opResult = await listModifiers(rt.store);
|
|
306
|
+
const expectedText = modifierListFmt.llm([...opResult]);
|
|
307
|
+
|
|
308
|
+
const mcpRes = await client.callTool({
|
|
309
|
+
name: "modifier_list",
|
|
310
|
+
arguments: { condensed: true },
|
|
311
|
+
});
|
|
312
|
+
const body = parseCondensed(mcpRes);
|
|
313
|
+
expect(body.text).toBe(expectedText);
|
|
314
|
+
});
|
|
315
|
+
|
|
135
316
|
it("modifier_lookup: values match", async () => {
|
|
136
317
|
const opResult = await lookupModifier(rt.store, "importance");
|
|
137
318
|
const mcpRes = await client.callTool({
|
|
138
319
|
name: "modifier_lookup",
|
|
139
320
|
arguments: { names: ["importance"] },
|
|
140
321
|
});
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
322
|
+
const body = parseEnvelope(mcpRes);
|
|
323
|
+
const data = body.data as { results: unknown[]; errors: unknown[] };
|
|
324
|
+
expect(data.results[0]).toEqual(opResult);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("modifier_lookup condensed: matches llm formatter", async () => {
|
|
328
|
+
const opResult = await lookupModifier(rt.store, "importance");
|
|
329
|
+
const expectedText = modifierLookupFmt.llm(opResult);
|
|
330
|
+
|
|
331
|
+
const mcpRes = await client.callTool({
|
|
332
|
+
name: "modifier_lookup",
|
|
333
|
+
arguments: { names: ["importance"], condensed: true },
|
|
334
|
+
});
|
|
335
|
+
const body = parseCondensed(mcpRes);
|
|
336
|
+
expect(body.text).toBe(expectedText);
|
|
147
337
|
});
|
|
148
338
|
});
|
|
149
339
|
|
|
@@ -161,18 +351,40 @@ describe("token parity", () => {
|
|
|
161
351
|
assertParity(opResult, mcpRes);
|
|
162
352
|
});
|
|
163
353
|
|
|
354
|
+
it("token_list condensed: matches llm formatter", async () => {
|
|
355
|
+
const opResult = await listTokens(rt.store);
|
|
356
|
+
const expectedText = tokenListFmt.llm([...opResult]);
|
|
357
|
+
|
|
358
|
+
const mcpRes = await client.callTool({
|
|
359
|
+
name: "token_list",
|
|
360
|
+
arguments: { condensed: true },
|
|
361
|
+
});
|
|
362
|
+
const body = parseCondensed(mcpRes);
|
|
363
|
+
expect(body.text).toBe(expectedText);
|
|
364
|
+
});
|
|
365
|
+
|
|
164
366
|
it("token_lookup: values match", async () => {
|
|
165
367
|
const opResult = await lookupToken(rt.store, "color.primary");
|
|
166
368
|
const mcpRes = await client.callTool({
|
|
167
369
|
name: "token_lookup",
|
|
168
370
|
arguments: { names: ["color.primary"] },
|
|
169
371
|
});
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
372
|
+
const body = parseEnvelope(mcpRes);
|
|
373
|
+
const data = body.data as { results: unknown[]; errors: unknown[] };
|
|
374
|
+
expect(data.results[0]).toEqual(opResult);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("token_lookup condensed: matches llm formatter", async () => {
|
|
378
|
+
const opResult = await lookupToken(rt.store, "color.primary");
|
|
379
|
+
const fmt = createTokenLookupFmt({ detailed: true });
|
|
380
|
+
const expectedText = fmt.llm(opResult);
|
|
381
|
+
|
|
382
|
+
const mcpRes = await client.callTool({
|
|
383
|
+
name: "token_lookup",
|
|
384
|
+
arguments: { names: ["color.primary"], condensed: true },
|
|
385
|
+
});
|
|
386
|
+
const body = parseCondensed(mcpRes);
|
|
387
|
+
expect(body.text).toBe(expectedText);
|
|
176
388
|
});
|
|
177
389
|
});
|
|
178
390
|
|
|
@@ -189,4 +401,142 @@ describe("ontology parity", () => {
|
|
|
189
401
|
});
|
|
190
402
|
assertParity(opResult, mcpRes);
|
|
191
403
|
});
|
|
404
|
+
|
|
405
|
+
it("ontology_list condensed: text contains ontology data", async () => {
|
|
406
|
+
const mcpRes = await client.callTool({
|
|
407
|
+
name: "ontology_list",
|
|
408
|
+
arguments: { condensed: true },
|
|
409
|
+
});
|
|
410
|
+
const body = parseCondensed(mcpRes);
|
|
411
|
+
expect(body.condensed).toBe(true);
|
|
412
|
+
expect(body.text).toContain("Ontologies");
|
|
413
|
+
expect(body.text).toContain("ds:");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("ontology_show: data matches operation", async () => {
|
|
417
|
+
const opResult = await showOntology(rt.store, "ds");
|
|
418
|
+
const mcpRes = await client.callTool({
|
|
419
|
+
name: "ontology_show",
|
|
420
|
+
arguments: { prefix: "ds" },
|
|
421
|
+
});
|
|
422
|
+
const body = parseEnvelope(mcpRes);
|
|
423
|
+
expect(body.data).toEqual(opResult);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("ontology_show condensed: text contains schema data", async () => {
|
|
427
|
+
const mcpRes = await client.callTool({
|
|
428
|
+
name: "ontology_show",
|
|
429
|
+
arguments: { prefix: "ds", condensed: true },
|
|
430
|
+
});
|
|
431
|
+
const body = parseCondensed(mcpRes);
|
|
432
|
+
expect(body.condensed).toBe(true);
|
|
433
|
+
expect(body.text).toContain("ds:");
|
|
434
|
+
expect(body.text).toContain("Classes");
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// Config parity
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
describe("config parity", () => {
|
|
443
|
+
it("config_show: data matches resolveConfigShow", async () => {
|
|
444
|
+
const install = detectInstallSource();
|
|
445
|
+
const opResult = resolveConfigShow(rt.config, {
|
|
446
|
+
packageManager: install.packageManager,
|
|
447
|
+
installSource: install.label,
|
|
448
|
+
configFilePath: "pragma.config.json",
|
|
449
|
+
configFileExists: true,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const mcpRes = await client.callTool({
|
|
453
|
+
name: "config_show",
|
|
454
|
+
arguments: {},
|
|
455
|
+
});
|
|
456
|
+
const body = parseEnvelope(mcpRes);
|
|
457
|
+
expect(body.data).toEqual(opResult);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("config_show condensed: matches llm formatter", async () => {
|
|
461
|
+
const install = detectInstallSource();
|
|
462
|
+
const opResult = resolveConfigShow(rt.config, {
|
|
463
|
+
packageManager: install.packageManager,
|
|
464
|
+
installSource: install.label,
|
|
465
|
+
configFilePath: "pragma.config.json",
|
|
466
|
+
configFileExists: true,
|
|
467
|
+
});
|
|
468
|
+
const expectedText = configShowFmt.llm(opResult);
|
|
469
|
+
|
|
470
|
+
const mcpRes = await client.callTool({
|
|
471
|
+
name: "config_show",
|
|
472
|
+
arguments: { condensed: true },
|
|
473
|
+
});
|
|
474
|
+
const body = parseCondensed(mcpRes);
|
|
475
|
+
expect(body.text).toBe(expectedText);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
// Info parity
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
describe("info parity", () => {
|
|
484
|
+
it("info: data matches shared operations", async () => {
|
|
485
|
+
const install = detectInstallSource();
|
|
486
|
+
const tierChain =
|
|
487
|
+
rt.config.tier !== undefined ? resolveTierChain(rt.config.tier) : [];
|
|
488
|
+
const channelReleases = CHANNEL_RELEASES[rt.config.channel];
|
|
489
|
+
const storeSummary = await collectStoreSummary(rt.store);
|
|
490
|
+
|
|
491
|
+
const expected: InfoData = {
|
|
492
|
+
version: VERSION,
|
|
493
|
+
pm: install.packageManager,
|
|
494
|
+
installSource: install.label,
|
|
495
|
+
configPath: "pragma.config.json",
|
|
496
|
+
tier: rt.config.tier,
|
|
497
|
+
tierChain,
|
|
498
|
+
channel: rt.config.channel,
|
|
499
|
+
channelReleases: [...channelReleases],
|
|
500
|
+
update: undefined,
|
|
501
|
+
updateSkipped: true,
|
|
502
|
+
store: storeSummary,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const mcpRes = await client.callTool({
|
|
506
|
+
name: "info",
|
|
507
|
+
arguments: {},
|
|
508
|
+
});
|
|
509
|
+
const body = parseEnvelope(mcpRes);
|
|
510
|
+
expect(body.data).toEqual(expected);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("info condensed: matches llm formatter", async () => {
|
|
514
|
+
const install = detectInstallSource();
|
|
515
|
+
const tierChain =
|
|
516
|
+
rt.config.tier !== undefined ? resolveTierChain(rt.config.tier) : [];
|
|
517
|
+
const channelReleases = CHANNEL_RELEASES[rt.config.channel];
|
|
518
|
+
const storeSummary = await collectStoreSummary(rt.store);
|
|
519
|
+
|
|
520
|
+
const data: InfoData = {
|
|
521
|
+
version: VERSION,
|
|
522
|
+
pm: install.packageManager,
|
|
523
|
+
installSource: install.label,
|
|
524
|
+
configPath: "pragma.config.json",
|
|
525
|
+
tier: rt.config.tier,
|
|
526
|
+
tierChain,
|
|
527
|
+
channel: rt.config.channel,
|
|
528
|
+
channelReleases: [...channelReleases],
|
|
529
|
+
update: undefined,
|
|
530
|
+
updateSkipped: true,
|
|
531
|
+
store: storeSummary,
|
|
532
|
+
};
|
|
533
|
+
const expectedText = renderInfoLlm(data);
|
|
534
|
+
|
|
535
|
+
const mcpRes = await client.callTool({
|
|
536
|
+
name: "info",
|
|
537
|
+
arguments: { condensed: true },
|
|
538
|
+
});
|
|
539
|
+
const body = parseCondensed(mcpRes);
|
|
540
|
+
expect(body.text).toBe(expectedText);
|
|
541
|
+
});
|
|
192
542
|
});
|
package/src/testing/store.ts
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import compactUri from "../../../domains/shared/compactUri.js";
|
|
3
|
+
import { PREFIX_MAP } from "../../../domains/shared/prefixes.js";
|
|
4
|
+
import type { BulletListProps } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A simple bulleted list with `•` prefix.
|
|
8
|
+
*
|
|
9
|
+
* Formats each item as a string, compacting URIs when detected.
|
|
10
|
+
*/
|
|
11
|
+
export default function BulletList({ items, prefixes }: BulletListProps) {
|
|
12
|
+
const prefixMap = prefixes ?? PREFIX_MAP;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Box flexDirection="column" paddingLeft={2}>
|
|
16
|
+
{items.map((item) => {
|
|
17
|
+
const text = formatItem(item, prefixMap);
|
|
18
|
+
return <Text key={text}>• {text}</Text>;
|
|
19
|
+
})}
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatItem(
|
|
25
|
+
value: unknown,
|
|
26
|
+
prefixes: Readonly<Record<string, string>>,
|
|
27
|
+
): string {
|
|
28
|
+
if (value === null || value === undefined) return "";
|
|
29
|
+
if (typeof value === "string") {
|
|
30
|
+
if (
|
|
31
|
+
value.startsWith("http://") ||
|
|
32
|
+
value.startsWith("https://") ||
|
|
33
|
+
/^[a-z][a-z0-9+.-]*:/i.test(value)
|
|
34
|
+
) {
|
|
35
|
+
return compactUri(value, prefixes);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
40
|
+
return Object.entries(value as Record<string, unknown>)
|
|
41
|
+
.filter(([, v]) => v !== null && v !== undefined)
|
|
42
|
+
.map(([k, v]) => `${k}: ${String(v)}`)
|
|
43
|
+
.join(" | ");
|
|
44
|
+
}
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { BOX, DOMAIN_COLORS } from "../../constants.js";
|
|
3
|
+
import { truncateText } from "../../helpers/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a bordered card string for a single lookup result.
|
|
7
|
+
*
|
|
8
|
+
* Uses double-line box-drawing characters for the border. The title
|
|
9
|
+
* renders in the domain's instance color. When a badge is present
|
|
10
|
+
* (e.g., "[1 of 2]"), it appears right-aligned in the header.
|
|
11
|
+
* Every body line is wrapped with `║` side borders.
|
|
12
|
+
*
|
|
13
|
+
* @param title - Card title for the header bar.
|
|
14
|
+
* @param domain - Domain name for color lookup.
|
|
15
|
+
* @param bodyLines - Pre-formatted body lines to wrap with borders.
|
|
16
|
+
* @param termWidth - Terminal width in columns.
|
|
17
|
+
* @param badge - Optional right-aligned badge text (e.g., "[1 of 2]").
|
|
18
|
+
* @returns A fully bordered card as a single string.
|
|
19
|
+
*/
|
|
20
|
+
export default function buildCard(
|
|
21
|
+
title: string,
|
|
22
|
+
domain: string,
|
|
23
|
+
bodyLines: readonly string[],
|
|
24
|
+
termWidth: number,
|
|
25
|
+
badge?: string,
|
|
26
|
+
): string {
|
|
27
|
+
const innerWidth = Math.max(termWidth - 4, 20);
|
|
28
|
+
const colors = DOMAIN_COLORS[domain];
|
|
29
|
+
const colorFn = resolveChalkColor(colors?.instanceFg);
|
|
30
|
+
|
|
31
|
+
const badgeText = badge ? ` ${badge}` : "";
|
|
32
|
+
const maxTitleWidth = innerWidth - badgeText.length;
|
|
33
|
+
const displayTitle = truncateText(title, maxTitleWidth);
|
|
34
|
+
const titlePad = innerWidth - displayTitle.length - badgeText.length;
|
|
35
|
+
|
|
36
|
+
const topBorder = `${BOX.topLeft}${BOX.horizontal.repeat(innerWidth + 2)}${BOX.topRight}`;
|
|
37
|
+
const badgePart = badge ? chalk.dim(badgeText) : "";
|
|
38
|
+
const titleLine = `${BOX.vertical} ${chalk.bold(colorFn(displayTitle))}${" ".repeat(Math.max(titlePad, 0))}${badgePart} ${BOX.vertical}`;
|
|
39
|
+
const divider = `${BOX.dividerLeft}${BOX.dividerHorizontal.repeat(innerWidth + 2)}${BOX.dividerRight}`;
|
|
40
|
+
const bottomBorder = `${BOX.bottomLeft}${BOX.horizontal.repeat(innerWidth + 2)}${BOX.bottomRight}`;
|
|
41
|
+
|
|
42
|
+
const expandedLines = bodyLines.flatMap((line) => line.split("\n"));
|
|
43
|
+
const borderedBody = expandedLines.map((line) => {
|
|
44
|
+
const visibleWidth = stripAnsi(line).length;
|
|
45
|
+
const pad = Math.max(innerWidth - visibleWidth, 0);
|
|
46
|
+
return `${BOX.vertical} ${line}${" ".repeat(pad)} ${BOX.vertical}`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return [topBorder, titleLine, divider, ...borderedBody, bottomBorder].join(
|
|
50
|
+
"\n",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveChalkColor(
|
|
55
|
+
colorName: string | undefined,
|
|
56
|
+
): (text: string) => string {
|
|
57
|
+
if (!colorName) return (text) => text;
|
|
58
|
+
const fn = chalk[colorName as keyof typeof chalk];
|
|
59
|
+
return typeof fn === "function"
|
|
60
|
+
? (fn as (text: string) => string)
|
|
61
|
+
: (text) => text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Strip ANSI escape codes from a string to measure visible width.
|
|
66
|
+
*/
|
|
67
|
+
function stripAnsi(text: string): string {
|
|
68
|
+
return text.replace(
|
|
69
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escapes
|
|
70
|
+
/\x1b\[[0-9;]*m/g,
|
|
71
|
+
"",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as buildCard } from "./Card.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
|
|
3
|
+
export interface CardProps {
|
|
4
|
+
/** Card title displayed in the header bar. */
|
|
5
|
+
readonly title: string;
|
|
6
|
+
/** Domain name — indexes into DOMAIN_COLORS for the title color. */
|
|
7
|
+
readonly domain: string;
|
|
8
|
+
/** Optional badge text displayed right-aligned in the header (e.g., "[1 of 2]"). */
|
|
9
|
+
readonly badge?: string;
|
|
10
|
+
/** Card body content. */
|
|
11
|
+
readonly children: React.ReactNode;
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { BOX } from "../../constants.js";
|
|
3
|
+
import { syntaxColor } from "../../helpers/index.js";
|
|
4
|
+
import { useTerminalSize } from "../../hooks/index.js";
|
|
5
|
+
import type { CodeBlockProps } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A bordered code block with lightweight syntax coloring.
|
|
9
|
+
*
|
|
10
|
+
* Renders code inside a single-line bordered sub-box using thin
|
|
11
|
+
* box-drawing characters. Applies keyword-level colorization via
|
|
12
|
+
* the syntaxColor helper.
|
|
13
|
+
*/
|
|
14
|
+
export default function CodeBlock({ code, language }: CodeBlockProps) {
|
|
15
|
+
const { columns: termWidth } = useTerminalSize();
|
|
16
|
+
const innerWidth = Math.max(termWidth - 8, 20);
|
|
17
|
+
|
|
18
|
+
const colored = syntaxColor(code, language);
|
|
19
|
+
const lines = colored.split("\n");
|
|
20
|
+
|
|
21
|
+
const topBorder = ` ${BOX.thinVertical === "│" ? "┌" : BOX.thinVertical}${"─".repeat(innerWidth)}${"┐"}`;
|
|
22
|
+
const bottomBorder = ` ${"└"}${"─".repeat(innerWidth)}${"┘"}`;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box flexDirection="column">
|
|
26
|
+
<Text dimColor>{topBorder}</Text>
|
|
27
|
+
{lines.map((line, i) => (
|
|
28
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: static code lines
|
|
29
|
+
<Text key={i}>
|
|
30
|
+
{" │ "}
|
|
31
|
+
{line}
|
|
32
|
+
</Text>
|
|
33
|
+
))}
|
|
34
|
+
<Text dimColor>{bottomBorder}</Text>
|
|
35
|
+
</Box>
|
|
36
|
+
);
|
|
37
|
+
}
|