@canonical/pragma-cli 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pragma +0 -0
- package/package.json +19 -17
- package/src/config/readConfig.ts +38 -2
- package/src/config/types.ts +12 -1
- package/src/config/writeConfig.ts +12 -0
- package/src/constants.ts +12 -1
- package/src/domains/block/commands/index.ts +2 -1
- package/src/domains/block/commands/sample.ts +55 -0
- package/src/domains/block/formatters/index.ts +2 -1
- package/src/domains/block/formatters/sample.ts +21 -0
- package/src/domains/block/index.ts +3 -3
- package/src/domains/block/mcp/specs.ts +48 -0
- package/src/domains/block/operations/index.ts +2 -1
- package/src/domains/block/operations/lookup.ts +9 -0
- package/src/domains/block/operations/sample.ts +41 -0
- package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
- package/src/domains/block/orchestration/resolveBlockLookup.ts +10 -3
- package/src/domains/config/commands/framework.test.ts +88 -0
- package/src/domains/config/commands/framework.ts +85 -0
- package/src/domains/config/commands/index.ts +2 -0
- package/src/domains/config/commands/trace.test.ts +148 -0
- package/src/domains/config/commands/trace.ts +80 -0
- package/src/domains/config/formatters/framework.ts +34 -0
- package/src/domains/config/formatters/index.ts +2 -0
- package/src/domains/config/formatters/trace.ts +28 -0
- package/src/domains/config/index.ts +15 -2
- package/src/domains/config/operations/index.ts +1 -0
- package/src/domains/config/operations/validateFramework.ts +24 -0
- package/src/domains/config/tasks/index.ts +2 -0
- package/src/domains/config/tasks/setChannel.ts +7 -1
- package/src/domains/config/tasks/setFramework.ts +40 -0
- package/src/domains/config/tasks/setTier.ts +7 -1
- package/src/domains/config/tasks/setTrace.ts +38 -0
- package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +35 -41
- package/src/domains/doctor/operations/checks.test.ts +4 -1
- package/src/domains/doctor/operations/runChecks.test.ts +72 -0
- package/src/domains/doctor/operations/runChecks.ts +48 -12
- package/src/domains/graphql/commands/build.test.ts +194 -0
- package/src/domains/graphql/commands/build.ts +125 -0
- package/src/domains/graphql/commands/check.test.ts +139 -0
- package/src/domains/graphql/commands/check.ts +87 -0
- package/src/domains/graphql/commands/index.ts +4 -0
- package/src/domains/graphql/formatters/index.ts +3 -0
- package/src/domains/graphql/formatters/report.ts +78 -0
- package/src/domains/graphql/helpers/index.ts +4 -0
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
- package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
- package/src/domains/graphql/index.ts +21 -0
- package/src/domains/graphql/operations/compileSchema.test.ts +119 -0
- package/src/domains/graphql/operations/compileSchema.ts +109 -0
- package/src/domains/graphql/operations/index.ts +7 -0
- package/src/domains/graphql/types.ts +29 -0
- package/src/domains/info/operations/collectInfo.test.ts +2 -2
- package/src/domains/info/operations/collectInfo.ts +4 -4
- package/src/domains/llm/capabilities.test.ts +91 -0
- package/src/domains/llm/commands/capabilities.ts +28 -0
- package/src/domains/llm/commands/index.ts +1 -0
- package/src/domains/llm/data/commandReference.ts +90 -18
- package/src/domains/llm/data/decisionTrees.ts +15 -13
- package/src/domains/llm/data/index.ts +2 -0
- package/src/domains/llm/data/toolCatalog.ts +184 -0
- package/src/domains/llm/formatters/capabilities.ts +50 -0
- package/src/domains/llm/formatters/index.ts +1 -0
- package/src/domains/llm/index.ts +3 -2
- package/src/domains/llm/mcp/index.ts +1 -1
- package/src/domains/llm/mcp/specs.ts +59 -52
- package/src/domains/llm/types.ts +40 -0
- package/src/domains/modifier/commands/index.ts +2 -1
- package/src/domains/modifier/commands/sample.ts +53 -0
- package/src/domains/modifier/formatters/index.ts +2 -1
- package/src/domains/modifier/formatters/sample.ts +15 -0
- package/src/domains/modifier/index.ts +7 -3
- package/src/domains/modifier/mcp/specs.ts +47 -0
- package/src/domains/modifier/operations/index.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +9 -0
- package/src/domains/modifier/operations/sample.ts +32 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +14 -2
- package/src/domains/refs/operations/index.ts +1 -1
- package/src/domains/refs/operations/paths.ts +29 -1
- package/src/domains/refs/operations/updateRefs.test.ts +6 -3
- package/src/domains/refs/operations/updateRefs.ts +3 -2
- package/src/domains/shared/bootStore.ts +88 -49
- package/src/domains/shared/contracts.ts +1 -0
- package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
- package/src/domains/shared/loaders/bundledLoader.ts +108 -0
- package/src/domains/shared/loaders/gitLoader.ts +31 -0
- package/src/domains/shared/loaders/index.ts +6 -0
- package/src/domains/shared/loaders/localLoader.ts +73 -0
- package/src/domains/shared/loaders/readPackageDir.ts +112 -0
- package/src/domains/shared/lookupMany.ts +9 -1
- package/src/domains/shared/packages.ts +21 -120
- package/src/domains/shared/pickRandom.ts +22 -0
- package/src/domains/shared/runtime.ts +6 -3
- package/src/domains/shared/sampleFormatters.ts +47 -0
- package/src/domains/shared/semanticPackage.ts +96 -0
- package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
- package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
- package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
- package/src/domains/shared/suggestions/expandGlob.ts +51 -0
- package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
- package/src/domains/shared/suggestions/index.ts +7 -0
- package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
- package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
- package/src/domains/shared/suggestions/suggestNames.ts +97 -0
- package/src/domains/shared/types/index.ts +1 -0
- package/src/domains/shared/types/runtime.ts +4 -1
- package/src/domains/shared/types/sample.ts +17 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +49 -16
- package/src/domains/standard/commands/index.ts +2 -1
- package/src/domains/standard/commands/sample.ts +57 -0
- package/src/domains/standard/formatters/index.ts +2 -1
- package/src/domains/standard/formatters/sample.ts +15 -0
- package/src/domains/standard/index.ts +8 -1
- package/src/domains/standard/mcp/specs.ts +48 -1
- package/src/domains/standard/operations/index.ts +2 -1
- package/src/domains/standard/operations/lookup.ts +9 -0
- package/src/domains/standard/operations/sample.ts +35 -0
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +14 -2
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
- package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
- package/src/domains/token/commands/index.ts +2 -1
- package/src/domains/token/commands/sample.ts +53 -0
- package/src/domains/token/formatters/index.ts +2 -1
- package/src/domains/token/formatters/sample.ts +16 -0
- package/src/domains/token/index.ts +8 -1
- package/src/domains/token/mcp/specs.ts +47 -1
- package/src/domains/token/operations/index.ts +2 -1
- package/src/domains/token/operations/lookup.ts +9 -0
- package/src/domains/token/operations/sample.ts +32 -0
- package/src/domains/token/orchestration/resolveTokenLookup.ts +14 -2
- package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
- package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
- package/src/domains/trace/commands/clear.ts +71 -0
- package/src/domains/trace/commands/index.ts +5 -0
- package/src/domains/trace/commands/sessions.ts +32 -0
- package/src/domains/trace/commands/show.ts +145 -0
- package/src/domains/trace/formatters/index.ts +9 -0
- package/src/domains/trace/formatters/sessions.ts +80 -0
- package/src/domains/trace/formatters/show.ts +122 -0
- package/src/domains/trace/index.ts +17 -0
- package/src/domains/trace/operations/index.ts +8 -0
- package/src/domains/trace/operations/listSessions.test.ts +55 -0
- package/src/domains/trace/operations/listSessions.ts +52 -0
- package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
- package/src/domains/trace/operations/readTraceLog.ts +104 -0
- package/src/domains/trace/tracePlugin.test.ts +171 -0
- package/src/domains/trace/tracePlugin.ts +159 -0
- package/src/domains/trace/types.ts +65 -0
- package/src/error/PragmaError.ts +15 -2
- package/src/error/types.ts +15 -1
- package/src/mcp/error/serializeErrorPayload.ts +7 -0
- package/src/mcp/error/types.ts +8 -0
- package/src/mcp/registerResources.test.ts +38 -16
- package/src/mcp/registerResources.ts +57 -8
- package/src/mcp/tools/registerTools.test.ts +22 -7
- package/src/mcp/utils/wrapTool.test.ts +1 -0
- package/src/pipeline/collectCommands.ts +7 -1
- package/src/pipeline/renderError.ts +15 -0
- package/src/pipeline/resolveCommandKind.ts +8 -1
- package/src/pipeline/runCli.ts +12 -1
- package/src/testing/createTestMcpClient.ts +1 -0
- package/src/testing/graphqlFixtures.ts +72 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/integration/mcp-surface.test.ts +11 -5
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Doctor check: report how each semantic package is resolved.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Uses the loader chain (local > git > bundled) to show where each
|
|
5
|
+
* package resolves from, its version, and graph/skill counts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
8
|
import { readConfig } from "#config";
|
|
10
|
-
import {
|
|
11
|
-
parsePackageEntry,
|
|
12
|
-
type RawPackageEntry,
|
|
13
|
-
} from "../../../refs/operations/parseRef.js";
|
|
14
|
-
import { gitCacheDir } from "../../../refs/operations/paths.js";
|
|
9
|
+
import { parsePackageEntry } from "../../../refs/operations/parseRef.js";
|
|
15
10
|
import readGlobalRefs from "../../../refs/operations/readGlobalRefs.js";
|
|
11
|
+
import {
|
|
12
|
+
createBundledLoader,
|
|
13
|
+
createGitLoader,
|
|
14
|
+
createLocalLoader,
|
|
15
|
+
} from "../../../shared/loaders/index.js";
|
|
16
16
|
import { DEFAULT_PACKAGES } from "../../../shared/packages.js";
|
|
17
|
+
import { resolveSemanticPackages } from "../../../shared/semanticPackage.js";
|
|
17
18
|
import type { CheckContext, CheckResult } from "../types.js";
|
|
18
19
|
|
|
19
20
|
export default async function checkPackageRefs(
|
|
@@ -23,46 +24,39 @@ export default async function checkPackageRefs(
|
|
|
23
24
|
const globalEntries = readGlobalRefs();
|
|
24
25
|
|
|
25
26
|
const hasProjectRefs = config.packages && config.packages.length > 0;
|
|
26
|
-
const hasGlobalRefs = globalEntries.length > 0;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
};
|
|
34
|
-
}
|
|
28
|
+
const rawEntries = hasProjectRefs
|
|
29
|
+
? config.packages!
|
|
30
|
+
: globalEntries.length > 0
|
|
31
|
+
? globalEntries
|
|
32
|
+
: DEFAULT_PACKAGES;
|
|
35
33
|
|
|
36
|
-
const
|
|
37
|
-
|
|
34
|
+
const refs = [...rawEntries].map(parsePackageEntry);
|
|
35
|
+
const loaders = [
|
|
36
|
+
createLocalLoader(),
|
|
37
|
+
createGitLoader(),
|
|
38
|
+
createBundledLoader(),
|
|
39
|
+
];
|
|
40
|
+
const packages = await resolveSemanticPackages(refs, loaders);
|
|
38
41
|
|
|
39
42
|
const details: string[] = [];
|
|
40
43
|
let hasIssue = false;
|
|
41
44
|
|
|
42
|
-
for (const
|
|
43
|
-
const
|
|
45
|
+
for (const ref of refs) {
|
|
46
|
+
const resolved = packages.find((p) => p.name === ref.pkg);
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
case "git": {
|
|
58
|
-
const cached = existsSync(gitCacheDir(ref.pkg, ref.ref));
|
|
59
|
-
if (cached) {
|
|
60
|
-
details.push(`${ref.pkg}: git #${ref.ref} (cached)`);
|
|
61
|
-
} else {
|
|
62
|
-
details.push(`${ref.pkg}: git #${ref.ref} NOT CACHED`);
|
|
63
|
-
hasIssue = true;
|
|
64
|
-
}
|
|
65
|
-
break;
|
|
48
|
+
if (resolved) {
|
|
49
|
+
details.push(
|
|
50
|
+
`${ref.pkg}: ${resolved.source} v${resolved.version} (${resolved.graphs.length} graphs, ${resolved.skills.length} skills)`,
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
// Check if covered by bundled fallback
|
|
54
|
+
const bundled = packages.find((p) => p.name === "(bundled)");
|
|
55
|
+
if (bundled) {
|
|
56
|
+
details.push(`${ref.pkg}: bundled v${bundled.version}`);
|
|
57
|
+
} else {
|
|
58
|
+
details.push(`${ref.pkg}: NOT RESOLVED`);
|
|
59
|
+
hasIssue = true;
|
|
66
60
|
}
|
|
67
61
|
}
|
|
68
62
|
}
|
|
@@ -75,7 +75,10 @@ describe("checkKeStore", () => {
|
|
|
75
75
|
query: vi.fn(),
|
|
76
76
|
dispose: vi.fn(),
|
|
77
77
|
};
|
|
78
|
-
vi.mocked(bootStore).mockResolvedValue(
|
|
78
|
+
vi.mocked(bootStore).mockResolvedValue({
|
|
79
|
+
store: mockStore,
|
|
80
|
+
packages: [],
|
|
81
|
+
} as never);
|
|
79
82
|
vi.mocked(collectStoreSummary).mockResolvedValue({
|
|
80
83
|
tripleCount: 12847,
|
|
81
84
|
graphNames: [],
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
checkSkillsSymlinked,
|
|
23
23
|
} from "./checks/index.js";
|
|
24
24
|
import runChecks from "./runChecks.js";
|
|
25
|
+
import type { CheckResult } from "./types.js";
|
|
25
26
|
|
|
26
27
|
const pass = (name: string) => ({
|
|
27
28
|
name,
|
|
@@ -92,4 +93,75 @@ describe("runChecks", () => {
|
|
|
92
93
|
expect(data.checks[4].name).toBe("store");
|
|
93
94
|
expect(data.checks[7].name).toBe("skills");
|
|
94
95
|
});
|
|
96
|
+
|
|
97
|
+
it("preserves order even when checks resolve out of order", async () => {
|
|
98
|
+
const delayed = (result: CheckResult, ms: number) =>
|
|
99
|
+
new Promise<CheckResult>((resolve) =>
|
|
100
|
+
setTimeout(() => resolve(result), ms),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Resolve the first check slowest and the last fastest to prove the report
|
|
104
|
+
// order follows declaration order, not completion order.
|
|
105
|
+
vi.mocked(checkNodeVersion).mockReturnValue(delayed(pass("Node"), 30));
|
|
106
|
+
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
107
|
+
vi.mocked(checkConfigFile).mockResolvedValue(pass("config"));
|
|
108
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
109
|
+
vi.mocked(checkKeStore).mockResolvedValue(pass("store"));
|
|
110
|
+
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
111
|
+
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
112
|
+
vi.mocked(checkSkillsSymlinked).mockReturnValue(delayed(pass("skills"), 0));
|
|
113
|
+
|
|
114
|
+
const data = await runChecks({ cwd: "/test" });
|
|
115
|
+
expect(data.checks.map((c) => c.name)).toEqual([
|
|
116
|
+
"Node",
|
|
117
|
+
"pragma",
|
|
118
|
+
"config",
|
|
119
|
+
"refs",
|
|
120
|
+
"store",
|
|
121
|
+
"completions",
|
|
122
|
+
"mcp",
|
|
123
|
+
"skills",
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("surfaces a thrown check as an attributable fail instead of aborting", async () => {
|
|
128
|
+
vi.mocked(checkNodeVersion).mockResolvedValue(pass("Node"));
|
|
129
|
+
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
130
|
+
vi.mocked(checkConfigFile).mockRejectedValue(new Error("disk on fire"));
|
|
131
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
132
|
+
vi.mocked(checkKeStore).mockResolvedValue(pass("store"));
|
|
133
|
+
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
134
|
+
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
135
|
+
vi.mocked(checkSkillsSymlinked).mockResolvedValue(pass("skills"));
|
|
136
|
+
|
|
137
|
+
const data = await runChecks({ cwd: "/test" });
|
|
138
|
+
|
|
139
|
+
// The whole run still completes; the thrown check becomes a fail in place.
|
|
140
|
+
expect(data.checks).toHaveLength(8);
|
|
141
|
+
expect(data.passed).toBe(7);
|
|
142
|
+
expect(data.failed).toBe(1);
|
|
143
|
+
const failed = data.checks[2];
|
|
144
|
+
expect(failed.status).toBe("fail");
|
|
145
|
+
// Fallback name matches checkConfigFile's own `name` so a thrown check
|
|
146
|
+
// reports the same label as a normal pass/fail.
|
|
147
|
+
expect(failed.name).toBe("pragma.config.json");
|
|
148
|
+
expect(failed.detail).toContain("disk on fire");
|
|
149
|
+
expect(failed.remedy).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("handles a non-Error rejection without crashing", async () => {
|
|
153
|
+
vi.mocked(checkNodeVersion).mockResolvedValue(pass("Node"));
|
|
154
|
+
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
155
|
+
vi.mocked(checkConfigFile).mockResolvedValue(pass("config"));
|
|
156
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
157
|
+
vi.mocked(checkKeStore).mockRejectedValue("string failure");
|
|
158
|
+
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
159
|
+
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
160
|
+
vi.mocked(checkSkillsSymlinked).mockResolvedValue(pass("skills"));
|
|
161
|
+
|
|
162
|
+
const data = await runChecks({ cwd: "/test" });
|
|
163
|
+
expect(data.failed).toBe(1);
|
|
164
|
+
expect(data.checks[4].name).toBe("ke store");
|
|
165
|
+
expect(data.checks[4].detail).toContain("string failure");
|
|
166
|
+
});
|
|
95
167
|
});
|
|
@@ -11,8 +11,35 @@ import {
|
|
|
11
11
|
import type { CheckContext, CheckResult, DoctorData } from "./types.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Each check paired with the display name used if it rejects. The checks are
|
|
15
|
+
* mutually independent, so they run concurrently; this list also fixes the
|
|
16
|
+
* stable display order of the results. The fallback name must match the
|
|
17
|
+
* `name` each check returns on success, so a thrown check reports the same
|
|
18
|
+
* label as a normal pass/fail and stays correlatable.
|
|
19
|
+
*/
|
|
20
|
+
function buildChecks(
|
|
21
|
+
ctx: CheckContext,
|
|
22
|
+
): readonly [string, Promise<CheckResult>][] {
|
|
23
|
+
return [
|
|
24
|
+
["Node version", checkNodeVersion()],
|
|
25
|
+
["pragma version", checkPragmaVersion()],
|
|
26
|
+
["pragma.config.json", checkConfigFile(ctx)],
|
|
27
|
+
["package refs", checkPackageRefs(ctx)],
|
|
28
|
+
["ke store", checkKeStore(ctx)],
|
|
29
|
+
["Shell completions", checkShellCompletions()],
|
|
30
|
+
["MCP configured", checkMcpConfigured(ctx)],
|
|
31
|
+
["Skills symlinked", checkSkillsSymlinked(ctx)],
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Orchestrate all environment health checks concurrently and return a
|
|
37
|
+
* DoctorData summary with pass/fail/skip counts.
|
|
38
|
+
*
|
|
39
|
+
* Checks run concurrently; results are collected in declaration order so the
|
|
40
|
+
* report is deterministic. A check that throws is surfaced as a fail (rather
|
|
41
|
+
* than aborting `pragma doctor`), tagged with the check's name so the failure
|
|
42
|
+
* remains attributable.
|
|
16
43
|
*
|
|
17
44
|
* @param ctx - Check context containing the working directory.
|
|
18
45
|
* @returns Aggregated check results.
|
|
@@ -21,16 +48,25 @@ import type { CheckContext, CheckResult, DoctorData } from "./types.js";
|
|
|
21
48
|
export default async function runChecks(
|
|
22
49
|
ctx: CheckContext,
|
|
23
50
|
): Promise<DoctorData> {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
checks
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
// buildChecks starts every check eagerly, so they run concurrently; each is
|
|
52
|
+
// guarded so a thrown check becomes an attributable fail rather than
|
|
53
|
+
// rejecting the whole run. Mapping preserves declaration order.
|
|
54
|
+
const checks: CheckResult[] = await Promise.all(
|
|
55
|
+
buildChecks(ctx).map(async ([name, promise]): Promise<CheckResult> => {
|
|
56
|
+
try {
|
|
57
|
+
return await promise;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
status: "fail",
|
|
63
|
+
detail: `Check threw an unexpected error: ${reason}`,
|
|
64
|
+
remedy:
|
|
65
|
+
"Re-run `pragma doctor`; if it persists, report this as a bug.",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
34
70
|
|
|
35
71
|
const passed = checks.filter((c) => c.status === "pass").length;
|
|
36
72
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import type { CommandDefinition } from "@canonical/cli-core";
|
|
11
|
+
import { hashSources } from "@canonical/ke-graphql";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import {
|
|
14
|
+
EX_PREFIX_ENTRY,
|
|
15
|
+
GRAPHQL_CLEAN_TTL,
|
|
16
|
+
GRAPHQL_ERROR_TTL,
|
|
17
|
+
GRAPHQL_FATAL_TTL,
|
|
18
|
+
} from "#testing";
|
|
19
|
+
import type { PragmaContext } from "../../shared/context.js";
|
|
20
|
+
import type { GraphqlCompileReport } from "../types.js";
|
|
21
|
+
import buildCommand from "./build.js";
|
|
22
|
+
|
|
23
|
+
let dir: string;
|
|
24
|
+
let stdoutSpy: ReturnType<typeof vi.spyOn>;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
dir = mkdtempSync(join(tmpdir(), "pragma-graphql-build-"));
|
|
28
|
+
writeFileSync(join(dir, "clean.ttl"), GRAPHQL_CLEAN_TTL);
|
|
29
|
+
writeFileSync(join(dir, "error.ttl"), GRAPHQL_ERROR_TTL);
|
|
30
|
+
writeFileSync(join(dir, "fatal.ttl"), GRAPHQL_FATAL_TTL);
|
|
31
|
+
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
stdoutSpy.mockRestore();
|
|
36
|
+
rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function makeCtx(overrides: Partial<PragmaContext> = {}): PragmaContext {
|
|
40
|
+
return {
|
|
41
|
+
cwd: dir,
|
|
42
|
+
globalFlags: { llm: false, format: "text" as const, verbose: false },
|
|
43
|
+
store: {} as PragmaContext["store"],
|
|
44
|
+
config: { tier: undefined, channel: "normal" },
|
|
45
|
+
...overrides,
|
|
46
|
+
} as PragmaContext;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function executeOutput(
|
|
50
|
+
cmd: CommandDefinition,
|
|
51
|
+
params: Record<string, unknown>,
|
|
52
|
+
ctx: PragmaContext,
|
|
53
|
+
): Promise<{ value: GraphqlCompileReport; text: string }> {
|
|
54
|
+
const result = await cmd.execute(params, ctx);
|
|
55
|
+
expect(result.tag).toBe("output");
|
|
56
|
+
if (result.tag !== "output") throw new Error("Expected output result");
|
|
57
|
+
return {
|
|
58
|
+
value: result.value as GraphqlCompileReport,
|
|
59
|
+
text: result.render.plain(result.value),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("graphql build command", () => {
|
|
64
|
+
it("writes the SDL and extraction artifacts to the default paths", async () => {
|
|
65
|
+
const ctx = makeCtx();
|
|
66
|
+
const { value } = await executeOutput(
|
|
67
|
+
buildCommand,
|
|
68
|
+
{ sources: ["clean.ttl"], prefix: [EX_PREFIX_ENTRY] },
|
|
69
|
+
ctx,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const sdl = readFileSync(join(dir, "schema.graphql"), "utf-8");
|
|
73
|
+
expect(sdl).toContain("type Thing");
|
|
74
|
+
|
|
75
|
+
const artifact = JSON.parse(
|
|
76
|
+
readFileSync(join(dir, "extraction.json"), "utf-8"),
|
|
77
|
+
) as { version: number; sourcesHash: string };
|
|
78
|
+
expect(artifact.version).toBe(1);
|
|
79
|
+
|
|
80
|
+
const raw = readFileSync(join(dir, "clean.ttl"), "utf-8");
|
|
81
|
+
expect(artifact.sourcesHash).toBe(hashSources([raw]));
|
|
82
|
+
expect(value.artifacts).toEqual({
|
|
83
|
+
sdl: join(dir, "schema.graphql"),
|
|
84
|
+
extraction: join(dir, "extraction.json"),
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("writes artifacts to custom --sdl and --extraction paths", async () => {
|
|
89
|
+
const ctx = makeCtx();
|
|
90
|
+
await executeOutput(
|
|
91
|
+
buildCommand,
|
|
92
|
+
{
|
|
93
|
+
sources: ["clean.ttl"],
|
|
94
|
+
prefix: [EX_PREFIX_ENTRY],
|
|
95
|
+
sdl: "out/api.graphql",
|
|
96
|
+
extraction: "out/api-extraction.json",
|
|
97
|
+
},
|
|
98
|
+
ctx,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(readFileSync(join(dir, "out", "api.graphql"), "utf-8")).toContain(
|
|
102
|
+
"type Thing",
|
|
103
|
+
);
|
|
104
|
+
expect(
|
|
105
|
+
JSON.parse(
|
|
106
|
+
readFileSync(join(dir, "out", "api-extraction.json"), "utf-8"),
|
|
107
|
+
),
|
|
108
|
+
).toMatchObject({ version: 1 });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("renders plain output with a summary and artifact paths", async () => {
|
|
112
|
+
const ctx = makeCtx();
|
|
113
|
+
const { text } = await executeOutput(
|
|
114
|
+
buildCommand,
|
|
115
|
+
{ sources: ["clean.ttl"], prefix: [EX_PREFIX_ENTRY] },
|
|
116
|
+
ctx,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(text).toContain("1 source file: 0 errors, 0 warnings, 0 info");
|
|
120
|
+
expect(text).toContain(`Wrote ${join(dir, "schema.graphql")}`);
|
|
121
|
+
expect(text).toContain(`Wrote ${join(dir, "extraction.json")}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("prints error diagnostics but still writes artifacts", async () => {
|
|
125
|
+
const ctx = makeCtx();
|
|
126
|
+
const { text } = await executeOutput(
|
|
127
|
+
buildCommand,
|
|
128
|
+
{ sources: ["error.ttl"], prefix: [EX_PREFIX_ENTRY] },
|
|
129
|
+
ctx,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(text).toContain("M001");
|
|
133
|
+
expect(text).toContain("1 error");
|
|
134
|
+
expect(existsSync(join(dir, "schema.graphql"))).toBe(true);
|
|
135
|
+
expect(existsSync(join(dir, "extraction.json"))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("renders llm output", async () => {
|
|
139
|
+
const ctx = makeCtx({
|
|
140
|
+
globalFlags: { llm: true, format: "text" as const, verbose: false },
|
|
141
|
+
});
|
|
142
|
+
const { text } = await executeOutput(
|
|
143
|
+
buildCommand,
|
|
144
|
+
{ sources: ["clean.ttl"], prefix: [EX_PREFIX_ENTRY] },
|
|
145
|
+
ctx,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(text).toContain("## GraphQL Compile");
|
|
149
|
+
expect(text).toContain("**Summary:**");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("renders json output", async () => {
|
|
153
|
+
const ctx = makeCtx({
|
|
154
|
+
globalFlags: { llm: false, format: "json" as const, verbose: false },
|
|
155
|
+
});
|
|
156
|
+
const { text } = await executeOutput(
|
|
157
|
+
buildCommand,
|
|
158
|
+
{ sources: ["clean.ttl"], prefix: [EX_PREFIX_ENTRY] },
|
|
159
|
+
ctx,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const parsed = JSON.parse(text) as GraphqlCompileReport;
|
|
163
|
+
expect(parsed.diagnostics).toEqual([]);
|
|
164
|
+
expect(parsed.files).toEqual([join(dir, "clean.ttl")]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("exits non-zero and writes no artifacts when compilation fails", async () => {
|
|
168
|
+
const ctx = makeCtx();
|
|
169
|
+
const result = await buildCommand.execute(
|
|
170
|
+
{ sources: ["fatal.ttl"], prefix: [EX_PREFIX_ENTRY] },
|
|
171
|
+
ctx,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(result.tag).toBe("exit");
|
|
175
|
+
if (result.tag !== "exit") throw new Error("Expected exit result");
|
|
176
|
+
expect(result.code).toBe(1);
|
|
177
|
+
expect(existsSync(join(dir, "schema.graphql"))).toBe(false);
|
|
178
|
+
expect(existsSync(join(dir, "extraction.json"))).toBe(false);
|
|
179
|
+
|
|
180
|
+
const printed = stdoutSpy.mock.calls.map((call) => call[0]).join("");
|
|
181
|
+
expect(printed).toContain("C003");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("throws structured error when sources are missing", async () => {
|
|
185
|
+
const ctx = makeCtx();
|
|
186
|
+
|
|
187
|
+
await expect(buildCommand.execute({}, ctx)).rejects.toMatchObject({
|
|
188
|
+
code: "INVALID_INPUT",
|
|
189
|
+
recovery: {
|
|
190
|
+
message: "Provide at least one TTL file or glob pattern.",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type CommandDefinition,
|
|
5
|
+
type CommandResult,
|
|
6
|
+
createExitResult,
|
|
7
|
+
createOutputResult,
|
|
8
|
+
} from "@canonical/cli-core";
|
|
9
|
+
import { serializeExtraction } from "@canonical/ke-graphql";
|
|
10
|
+
import { PragmaError } from "#error";
|
|
11
|
+
import { selectFormatter } from "../../shared/formatters.js";
|
|
12
|
+
import { reportFormatters } from "../formatters/index.js";
|
|
13
|
+
import { parsePrefixes } from "../helpers/index.js";
|
|
14
|
+
import { compileSchema } from "../operations/index.js";
|
|
15
|
+
import type { GraphqlCompileReport } from "../types.js";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_SDL_PATH = "./schema.graphql";
|
|
18
|
+
const DEFAULT_EXTRACTION_PATH = "./extraction.json";
|
|
19
|
+
|
|
20
|
+
/** Narrow an unknown param to a string array. */
|
|
21
|
+
function readStringArray(value: unknown): string[] {
|
|
22
|
+
return Array.isArray(value)
|
|
23
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
24
|
+
: [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the `pragma graphql build` command definition.
|
|
29
|
+
*
|
|
30
|
+
* Compiles TTL sources into a GraphQL schema, writes the SDL and the
|
|
31
|
+
* extraction artifact, and prints all compiler diagnostics. Exits
|
|
32
|
+
* non-zero when schema composition fails.
|
|
33
|
+
*/
|
|
34
|
+
const buildCommand: CommandDefinition = {
|
|
35
|
+
path: ["graphql", "build"],
|
|
36
|
+
description: "Compile TTL sources into GraphQL schema artifacts",
|
|
37
|
+
parameters: [
|
|
38
|
+
{
|
|
39
|
+
name: "sources",
|
|
40
|
+
description: "TTL source files or glob patterns",
|
|
41
|
+
type: "multiselect",
|
|
42
|
+
positional: true,
|
|
43
|
+
required: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "sdl",
|
|
47
|
+
description: "Output path for the GraphQL SDL",
|
|
48
|
+
type: "string",
|
|
49
|
+
default: DEFAULT_SDL_PATH,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "extraction",
|
|
53
|
+
description: "Output path for the extraction artifact",
|
|
54
|
+
type: "string",
|
|
55
|
+
default: DEFAULT_EXTRACTION_PATH,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "prefix",
|
|
59
|
+
description: "Ontology prefix as name=namespace (repeatable)",
|
|
60
|
+
type: "multiselect",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
meta: {
|
|
64
|
+
examples: [
|
|
65
|
+
"pragma graphql build ontology.ttl",
|
|
66
|
+
'pragma graphql build "data/*.ttl" --sdl out/schema.graphql --extraction out/extraction.json',
|
|
67
|
+
"pragma graphql build ontology.ttl --prefix ds=https://ds.canonical.com/",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
async execute(params, ctx): Promise<CommandResult> {
|
|
71
|
+
const sources = readStringArray(params.sources);
|
|
72
|
+
if (sources.length === 0) {
|
|
73
|
+
throw PragmaError.invalidInput("sources", "(empty)", {
|
|
74
|
+
recovery: {
|
|
75
|
+
message: "Provide at least one TTL file or glob pattern.",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const prefixes = parsePrefixes(readStringArray(params.prefix));
|
|
81
|
+
const outcome = await compileSchema({ sources, prefixes, cwd: ctx.cwd });
|
|
82
|
+
const format = selectFormatter(ctx, reportFormatters);
|
|
83
|
+
|
|
84
|
+
if (outcome.status === "failed" || !outcome.compiled) {
|
|
85
|
+
const report: GraphqlCompileReport = {
|
|
86
|
+
diagnostics: outcome.diagnostics,
|
|
87
|
+
files: outcome.files,
|
|
88
|
+
sourcesHash: outcome.sourcesHash,
|
|
89
|
+
};
|
|
90
|
+
process.stdout.write(`${format(report)}\n`);
|
|
91
|
+
return createExitResult(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const sdlPath = resolve(
|
|
95
|
+
ctx.cwd,
|
|
96
|
+
typeof params.sdl === "string" ? params.sdl : DEFAULT_SDL_PATH,
|
|
97
|
+
);
|
|
98
|
+
const extractionPath = resolve(
|
|
99
|
+
ctx.cwd,
|
|
100
|
+
typeof params.extraction === "string"
|
|
101
|
+
? params.extraction
|
|
102
|
+
: DEFAULT_EXTRACTION_PATH,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
mkdirSync(dirname(sdlPath), { recursive: true });
|
|
106
|
+
writeFileSync(sdlPath, outcome.compiled.sdl, "utf-8");
|
|
107
|
+
mkdirSync(dirname(extractionPath), { recursive: true });
|
|
108
|
+
writeFileSync(
|
|
109
|
+
extractionPath,
|
|
110
|
+
serializeExtraction(outcome.compiled.extraction, outcome.sourcesHash),
|
|
111
|
+
"utf-8",
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const report: GraphqlCompileReport = {
|
|
115
|
+
diagnostics: outcome.diagnostics,
|
|
116
|
+
files: outcome.files,
|
|
117
|
+
sourcesHash: outcome.sourcesHash,
|
|
118
|
+
artifacts: { sdl: sdlPath, extraction: extractionPath },
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return createOutputResult(report, { plain: format });
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default buildCommand;
|