@canonical/pragma-cli 0.27.1-experimental.0 → 0.30.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 +67 -15
- package/dist/pragma +0 -0
- package/package.json +16 -17
- package/src/config/defaultConfig.test.ts +33 -0
- package/src/config/defaultConfig.ts +59 -0
- package/src/config/defaults.json +17 -0
- package/src/config/findProjectConfigPath.test.ts +53 -0
- package/src/config/findProjectConfigPath.ts +36 -0
- package/src/config/index.ts +17 -2
- package/src/config/mergeConfigFileUpdate.ts +58 -0
- package/src/config/parseConfigValues.ts +215 -0
- package/src/config/readConfig.test.ts +104 -13
- package/src/config/readConfig.ts +12 -117
- package/src/config/readConfigLayers.test.ts +107 -0
- package/src/config/readConfigLayers.ts +115 -0
- package/src/config/resolveGlobalConfigPath.ts +13 -0
- package/src/config/resolveWriteConfigPath.test.ts +53 -0
- package/src/config/resolveWriteConfigPath.ts +31 -0
- package/src/config/types.ts +88 -2
- package/src/config/writeConfig.ts +25 -43
- package/src/constants.ts +12 -1
- package/src/domains/block/commands/index.ts +2 -1
- package/src/domains/block/commands/list.ts +5 -90
- package/src/domains/block/commands/lookup.ts +8 -167
- 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 +36 -168
- 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 +11 -3
- package/src/domains/block/stories.test.ts +28 -0
- package/src/domains/block/stories.ts +254 -0
- package/src/domains/config/commands/channel.test.ts +14 -3
- package/src/domains/config/commands/channel.ts +11 -7
- package/src/domains/config/commands/configScope.ts +46 -0
- package/src/domains/config/commands/framework.test.ts +105 -0
- package/src/domains/config/commands/framework.ts +91 -0
- package/src/domains/config/commands/index.ts +2 -0
- package/src/domains/config/commands/show.ts +5 -45
- package/src/domains/config/commands/tier.test.ts +13 -2
- package/src/domains/config/commands/tier.ts +11 -7
- package/src/domains/config/commands/trace.test.ts +165 -0
- package/src/domains/config/commands/trace.ts +83 -0
- package/src/domains/config/formatters/channel.test.ts +20 -6
- package/src/domains/config/formatters/channel.ts +14 -11
- package/src/domains/config/formatters/framework.ts +37 -0
- package/src/domains/config/formatters/index.ts +2 -0
- package/src/domains/config/formatters/show.test.ts +27 -0
- package/src/domains/config/formatters/show.ts +27 -4
- package/src/domains/config/formatters/tier.test.ts +26 -10
- package/src/domains/config/formatters/tier.ts +19 -11
- package/src/domains/config/formatters/trace.ts +28 -0
- package/src/domains/config/index.ts +15 -2
- package/src/domains/config/mcp/specs.ts +33 -53
- package/src/domains/config/operations/index.ts +1 -0
- package/src/domains/config/operations/show.test.ts +18 -0
- package/src/domains/config/operations/show.ts +7 -1
- package/src/domains/config/operations/types.ts +7 -0
- package/src/domains/config/operations/validateFramework.ts +24 -0
- package/src/domains/config/stories.ts +42 -0
- package/src/domains/config/tasks/index.ts +3 -0
- package/src/domains/config/tasks/setChannel.ts +13 -22
- package/src/domains/config/tasks/setFramework.ts +21 -0
- package/src/domains/config/tasks/setTier.ts +10 -23
- package/src/domains/config/tasks/setTrace.ts +20 -0
- package/src/domains/config/tasks/writeConfigField.ts +41 -0
- package/src/domains/create/commands/component.ts +5 -5
- package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
- package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
- package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -48
- package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
- package/src/domains/doctor/operations/checks/index.ts +1 -0
- package/src/domains/doctor/operations/checks.test.ts +162 -8
- package/src/domains/doctor/operations/runChecks.test.ts +85 -4
- package/src/domains/doctor/operations/runChecks.ts +50 -12
- package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
- package/src/domains/graph/helpers/resolveUri.ts +28 -21
- package/src/domains/graphql/commands/build.test.ts +196 -0
- package/src/domains/graphql/commands/build.ts +117 -0
- package/src/domains/graphql/commands/check.test.ts +141 -0
- package/src/domains/graphql/commands/check.ts +81 -0
- package/src/domains/graphql/commands/index.ts +5 -0
- package/src/domains/graphql/commands/serve.test.ts +40 -0
- package/src/domains/graphql/commands/serve.ts +131 -0
- package/src/domains/graphql/constants.ts +10 -0
- package/src/domains/graphql/formatters/index.ts +3 -0
- package/src/domains/graphql/formatters/report.ts +78 -0
- package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
- package/src/domains/graphql/helpers/gatherSources.ts +58 -0
- package/src/domains/graphql/helpers/index.ts +6 -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/helpers/routeGraphql.test.ts +43 -0
- package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
- package/src/domains/graphql/index.ts +24 -0
- package/src/domains/graphql/operations/compileSchema.test.ts +95 -0
- package/src/domains/graphql/operations/compileSchema.ts +107 -0
- package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
- package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
- package/src/domains/graphql/operations/index.ts +12 -0
- package/src/domains/graphql/types.ts +29 -0
- package/src/domains/info/operations/collectInfo.test.ts +16 -5
- package/src/domains/info/operations/collectInfo.ts +16 -19
- 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 +190 -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/list.ts +5 -38
- package/src/domains/modifier/commands/lookup.ts +8 -79
- 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 +38 -70
- 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 +15 -2
- package/src/domains/modifier/stories.test.ts +20 -0
- package/src/domains/modifier/stories.ts +77 -0
- package/src/domains/ontology/commands/list.ts +5 -58
- package/src/domains/ontology/commands/show.ts +6 -58
- package/src/domains/ontology/formatters/show.ts +7 -1
- package/src/domains/ontology/mcp/specs.ts +7 -84
- package/src/domains/ontology/stories.test.ts +36 -0
- package/src/domains/ontology/stories.ts +108 -0
- 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 +2 -36
- package/src/domains/setup/commands/skills.test.ts +6 -2
- package/src/domains/setup/commands/skills.ts +6 -2
- package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
- package/src/domains/setup/helpers/runSetupTask.ts +62 -70
- package/src/domains/setup/operations/setupMcp.test.ts +6 -1
- package/src/domains/shared/bootStore.ts +92 -50
- package/src/domains/shared/compactUri.test.ts +11 -0
- package/src/domains/shared/compactUri.ts +10 -5
- package/src/domains/shared/context.ts +2 -2
- package/src/domains/shared/contracts.ts +12 -0
- package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
- package/src/domains/shared/createInteractivePromptSession.ts +236 -0
- package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
- package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
- package/src/domains/shared/loaders/bundledLoader.ts +206 -0
- package/src/domains/shared/loaders/gitLoader.ts +31 -0
- package/src/domains/shared/loaders/index.ts +9 -0
- package/src/domains/shared/loaders/localLoader.ts +87 -0
- package/src/domains/shared/loaders/readPackageDir.ts +147 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
- package/src/domains/shared/lookupMany.test.ts +120 -0
- package/src/domains/shared/lookupMany.ts +43 -3
- package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
- package/src/domains/shared/mergeAndParseRefs.ts +44 -0
- package/src/domains/shared/packages.ts +14 -120
- package/src/domains/shared/pickRandom.ts +22 -0
- package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
- package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
- package/src/domains/shared/runtime.ts +7 -63
- package/src/domains/shared/sampleFormatters.ts +47 -0
- package/src/domains/shared/semanticPackage.ts +112 -0
- package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
- package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
- package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
- package/src/domains/shared/stories/compileLookupTool.ts +96 -0
- package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
- package/src/domains/shared/stories/compileReadCommand.ts +48 -0
- package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
- package/src/domains/shared/stories/compileReadTool.ts +38 -0
- package/src/domains/shared/stories/condense.test.ts +18 -0
- package/src/domains/shared/stories/condense.ts +15 -0
- package/src/domains/shared/stories/index.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.ts +22 -0
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
- package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
- package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
- package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
- package/src/domains/shared/stories/pack/index.ts +28 -0
- package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
- package/src/domains/shared/stories/pack/types.ts +104 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
- package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
- package/src/domains/shared/stories/projectCliParameters.ts +57 -0
- package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
- package/src/domains/shared/stories/projectToolParams.ts +32 -0
- package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
- package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
- package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
- package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
- package/src/domains/shared/stories/types.ts +171 -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 +52 -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 +130 -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/commands/index.ts +3 -1
- package/src/domains/skill/commands/list.test.ts +6 -2
- package/src/domains/skill/commands/list.ts +5 -63
- package/src/domains/skill/commands/lookup.test.ts +118 -0
- package/src/domains/skill/commands/lookup.ts +14 -0
- package/src/domains/skill/formatters/index.ts +1 -0
- package/src/domains/skill/formatters/lookup.test.ts +46 -0
- package/src/domains/skill/formatters/lookup.ts +57 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +48 -16
- package/src/domains/skill/index.ts +7 -3
- package/src/domains/skill/mcp/specs.ts +10 -29
- package/src/domains/skill/operations/discover.test.ts +61 -30
- package/src/domains/skill/operations/discover.ts +28 -37
- package/src/domains/skill/operations/index.ts +2 -1
- package/src/domains/skill/operations/list.test.ts +15 -7
- package/src/domains/skill/operations/lookup.test.ts +91 -0
- package/src/domains/skill/operations/lookup.ts +70 -0
- package/src/domains/skill/stories.test.ts +43 -0
- package/src/domains/skill/stories.ts +103 -0
- package/src/domains/skill/types.ts +8 -0
- package/src/domains/standard/commands/index.ts +2 -1
- package/src/domains/standard/commands/list.ts +5 -84
- package/src/domains/standard/commands/lookup.ts +8 -93
- 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 +44 -152
- 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 +15 -2
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
- package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
- package/src/domains/standard/stories.test.ts +24 -0
- package/src/domains/standard/stories.ts +151 -0
- package/src/domains/tier/commands/list.ts +5 -55
- package/src/domains/tier/mcp/specs.ts +5 -31
- package/src/domains/tier/stories.test.ts +22 -0
- package/src/domains/tier/stories.ts +52 -0
- 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 +52 -2
- 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 +15 -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 +405 -75
- package/src/mcp/registerResources.ts +5 -299
- package/src/mcp/resources/buildGraphIndex.ts +167 -0
- package/src/mcp/resources/buildResourceList.test.ts +97 -0
- package/src/mcp/resources/buildResourceList.ts +176 -0
- package/src/mcp/resources/classifyEntity.test.ts +40 -0
- package/src/mcp/resources/classifyEntity.ts +27 -0
- package/src/mcp/resources/constants.ts +105 -0
- package/src/mcp/resources/displayPredicates.ts +62 -0
- package/src/mcp/resources/pickFirstValue.test.ts +29 -0
- package/src/mcp/resources/pickFirstValue.ts +26 -0
- package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
- package/src/mcp/resources/pickPrimaryType.ts +26 -0
- package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
- package/src/mcp/resources/rankUriCompletions.ts +64 -0
- package/src/mcp/resources/readEntity.ts +169 -0
- package/src/mcp/resources/registerResources.ts +126 -0
- package/src/mcp/resources/types.ts +128 -0
- package/src/mcp/tools/index.ts +9 -0
- package/src/mcp/tools/registerTools.test.ts +84 -12
- package/src/mcp/types.ts +4 -0
- package/src/mcp/utils/wrapTool.test.ts +1 -0
- package/src/pipeline/collectCommands.ts +15 -2
- package/src/pipeline/renderError.ts +15 -0
- package/src/pipeline/resolveCommandKind.ts +8 -1
- package/src/pipeline/runCli.ts +51 -6
- package/src/testing/createTestMcpClient.ts +5 -1
- package/src/testing/graphqlFixtures.ts +72 -0
- package/src/testing/index.ts +17 -0
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
- package/src/testing/integration/executor-execution.test.ts +101 -0
- package/src/testing/integration/executor-golden.test.ts +145 -0
- package/src/testing/integration/mcp-surface.test.ts +11 -5
- package/src/testing/integration/parity.test.ts +17 -12
- package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
- package/src/testing/setupXdgIsolation.ts +15 -0
- package/src/testing/standardParityFixtures.ts +92 -0
- package/src/testing/storyFixtures.ts +63 -0
- package/src/pipeline/runInteractiveCommand.ts +0 -198
|
@@ -8,12 +8,14 @@ vi.mock("./checks/index.js", () => ({
|
|
|
8
8
|
checkKeStore: vi.fn(),
|
|
9
9
|
checkShellCompletions: vi.fn(),
|
|
10
10
|
checkMcpConfigured: vi.fn(),
|
|
11
|
+
checkMcpCommands: vi.fn(),
|
|
11
12
|
checkSkillsSymlinked: vi.fn(),
|
|
12
13
|
}));
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
checkConfigFile,
|
|
16
17
|
checkKeStore,
|
|
18
|
+
checkMcpCommands,
|
|
17
19
|
checkMcpConfigured,
|
|
18
20
|
checkNodeVersion,
|
|
19
21
|
checkPackageRefs,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
checkSkillsSymlinked,
|
|
23
25
|
} from "./checks/index.js";
|
|
24
26
|
import runChecks from "./runChecks.js";
|
|
27
|
+
import type { CheckResult } from "./types.js";
|
|
25
28
|
|
|
26
29
|
const pass = (name: string) => ({
|
|
27
30
|
name,
|
|
@@ -49,13 +52,14 @@ describe("runChecks", () => {
|
|
|
49
52
|
vi.mocked(checkKeStore).mockResolvedValue(pass("store"));
|
|
50
53
|
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
51
54
|
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
55
|
+
vi.mocked(checkMcpCommands).mockResolvedValue(pass("mcp commands"));
|
|
52
56
|
vi.mocked(checkSkillsSymlinked).mockResolvedValue(pass("skills"));
|
|
53
57
|
|
|
54
58
|
const data = await runChecks({ cwd: "/test" });
|
|
55
|
-
expect(data.passed).toBe(
|
|
59
|
+
expect(data.passed).toBe(9);
|
|
56
60
|
expect(data.failed).toBe(0);
|
|
57
61
|
expect(data.skipped).toBe(0);
|
|
58
|
-
expect(data.checks).toHaveLength(
|
|
62
|
+
expect(data.checks).toHaveLength(9);
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
it("counts failures and skips correctly", async () => {
|
|
@@ -66,10 +70,11 @@ describe("runChecks", () => {
|
|
|
66
70
|
vi.mocked(checkKeStore).mockResolvedValue(fail("store"));
|
|
67
71
|
vi.mocked(checkShellCompletions).mockResolvedValue(fail("completions"));
|
|
68
72
|
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
73
|
+
vi.mocked(checkMcpCommands).mockResolvedValue(pass("mcp commands"));
|
|
69
74
|
vi.mocked(checkSkillsSymlinked).mockResolvedValue(skip("skills"));
|
|
70
75
|
|
|
71
76
|
const data = await runChecks({ cwd: "/test" });
|
|
72
|
-
expect(data.passed).toBe(
|
|
77
|
+
expect(data.passed).toBe(5);
|
|
73
78
|
expect(data.failed).toBe(3);
|
|
74
79
|
expect(data.skipped).toBe(1);
|
|
75
80
|
});
|
|
@@ -82,6 +87,7 @@ describe("runChecks", () => {
|
|
|
82
87
|
vi.mocked(checkKeStore).mockResolvedValue(pass("store"));
|
|
83
88
|
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
84
89
|
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
90
|
+
vi.mocked(checkMcpCommands).mockResolvedValue(pass("mcp commands"));
|
|
85
91
|
vi.mocked(checkSkillsSymlinked).mockResolvedValue(pass("skills"));
|
|
86
92
|
|
|
87
93
|
const data = await runChecks({ cwd: "/test" });
|
|
@@ -90,6 +96,81 @@ describe("runChecks", () => {
|
|
|
90
96
|
expect(data.checks[2].name).toBe("config");
|
|
91
97
|
expect(data.checks[3].name).toBe("refs");
|
|
92
98
|
expect(data.checks[4].name).toBe("store");
|
|
93
|
-
expect(data.checks[
|
|
99
|
+
expect(data.checks[8].name).toBe("skills");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("preserves order even when checks resolve out of order", async () => {
|
|
103
|
+
const delayed = (result: CheckResult, ms: number) =>
|
|
104
|
+
new Promise<CheckResult>((resolve) =>
|
|
105
|
+
setTimeout(() => resolve(result), ms),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Resolve the first check slowest and the last fastest to prove the report
|
|
109
|
+
// order follows declaration order, not completion order.
|
|
110
|
+
vi.mocked(checkNodeVersion).mockReturnValue(delayed(pass("Node"), 30));
|
|
111
|
+
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
112
|
+
vi.mocked(checkConfigFile).mockResolvedValue(pass("config"));
|
|
113
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
114
|
+
vi.mocked(checkKeStore).mockResolvedValue(pass("store"));
|
|
115
|
+
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
116
|
+
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
117
|
+
vi.mocked(checkMcpCommands).mockResolvedValue(pass("mcp commands"));
|
|
118
|
+
vi.mocked(checkSkillsSymlinked).mockReturnValue(delayed(pass("skills"), 0));
|
|
119
|
+
|
|
120
|
+
const data = await runChecks({ cwd: "/test" });
|
|
121
|
+
expect(data.checks.map((c) => c.name)).toEqual([
|
|
122
|
+
"Node",
|
|
123
|
+
"pragma",
|
|
124
|
+
"config",
|
|
125
|
+
"refs",
|
|
126
|
+
"store",
|
|
127
|
+
"completions",
|
|
128
|
+
"mcp",
|
|
129
|
+
"mcp commands",
|
|
130
|
+
"skills",
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("surfaces a thrown check as an attributable fail instead of aborting", async () => {
|
|
135
|
+
vi.mocked(checkNodeVersion).mockResolvedValue(pass("Node"));
|
|
136
|
+
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
137
|
+
vi.mocked(checkConfigFile).mockRejectedValue(new Error("disk on fire"));
|
|
138
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
139
|
+
vi.mocked(checkKeStore).mockResolvedValue(pass("store"));
|
|
140
|
+
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
141
|
+
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
142
|
+
vi.mocked(checkMcpCommands).mockResolvedValue(pass("mcp commands"));
|
|
143
|
+
vi.mocked(checkSkillsSymlinked).mockResolvedValue(pass("skills"));
|
|
144
|
+
|
|
145
|
+
const data = await runChecks({ cwd: "/test" });
|
|
146
|
+
|
|
147
|
+
// The whole run still completes; the thrown check becomes a fail in place.
|
|
148
|
+
expect(data.checks).toHaveLength(9);
|
|
149
|
+
expect(data.passed).toBe(8);
|
|
150
|
+
expect(data.failed).toBe(1);
|
|
151
|
+
const failed = data.checks[2];
|
|
152
|
+
expect(failed.status).toBe("fail");
|
|
153
|
+
// Fallback name matches checkConfigFile's own `name` so a thrown check
|
|
154
|
+
// reports the same label as a normal pass/fail.
|
|
155
|
+
expect(failed.name).toBe("pragma.config.json");
|
|
156
|
+
expect(failed.detail).toContain("disk on fire");
|
|
157
|
+
expect(failed.remedy).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("handles a non-Error rejection without crashing", async () => {
|
|
161
|
+
vi.mocked(checkNodeVersion).mockResolvedValue(pass("Node"));
|
|
162
|
+
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
163
|
+
vi.mocked(checkConfigFile).mockResolvedValue(pass("config"));
|
|
164
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
165
|
+
vi.mocked(checkKeStore).mockRejectedValue("string failure");
|
|
166
|
+
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
167
|
+
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
168
|
+
vi.mocked(checkMcpCommands).mockResolvedValue(pass("mcp commands"));
|
|
169
|
+
vi.mocked(checkSkillsSymlinked).mockResolvedValue(pass("skills"));
|
|
170
|
+
|
|
171
|
+
const data = await runChecks({ cwd: "/test" });
|
|
172
|
+
expect(data.failed).toBe(1);
|
|
173
|
+
expect(data.checks[4].name).toBe("ke store");
|
|
174
|
+
expect(data.checks[4].detail).toContain("string failure");
|
|
94
175
|
});
|
|
95
176
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
checkConfigFile,
|
|
3
3
|
checkKeStore,
|
|
4
|
+
checkMcpCommands,
|
|
4
5
|
checkMcpConfigured,
|
|
5
6
|
checkNodeVersion,
|
|
6
7
|
checkPackageRefs,
|
|
@@ -11,8 +12,36 @@ import {
|
|
|
11
12
|
import type { CheckContext, CheckResult, DoctorData } from "./types.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* Each check paired with the display name used if it rejects. The checks are
|
|
16
|
+
* mutually independent, so they run concurrently; this list also fixes the
|
|
17
|
+
* stable display order of the results. The fallback name must match the
|
|
18
|
+
* `name` each check returns on success, so a thrown check reports the same
|
|
19
|
+
* label as a normal pass/fail and stays correlatable.
|
|
20
|
+
*/
|
|
21
|
+
function buildChecks(
|
|
22
|
+
ctx: CheckContext,
|
|
23
|
+
): readonly [string, Promise<CheckResult>][] {
|
|
24
|
+
return [
|
|
25
|
+
["Node version", checkNodeVersion()],
|
|
26
|
+
["pragma version", checkPragmaVersion()],
|
|
27
|
+
["pragma.config.json", checkConfigFile(ctx)],
|
|
28
|
+
["package refs", checkPackageRefs(ctx)],
|
|
29
|
+
["ke store", checkKeStore(ctx)],
|
|
30
|
+
["Shell completions", checkShellCompletions()],
|
|
31
|
+
["MCP configured", checkMcpConfigured(ctx)],
|
|
32
|
+
["MCP commands", checkMcpCommands(ctx)],
|
|
33
|
+
["Skills symlinked", checkSkillsSymlinked(ctx)],
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Orchestrate all environment health checks concurrently and return a
|
|
39
|
+
* DoctorData summary with pass/fail/skip counts.
|
|
40
|
+
*
|
|
41
|
+
* Checks run concurrently; results are collected in declaration order so the
|
|
42
|
+
* report is deterministic. A check that throws is surfaced as a fail (rather
|
|
43
|
+
* than aborting `pragma doctor`), tagged with the check's name so the failure
|
|
44
|
+
* remains attributable.
|
|
16
45
|
*
|
|
17
46
|
* @param ctx - Check context containing the working directory.
|
|
18
47
|
* @returns Aggregated check results.
|
|
@@ -21,16 +50,25 @@ import type { CheckContext, CheckResult, DoctorData } from "./types.js";
|
|
|
21
50
|
export default async function runChecks(
|
|
22
51
|
ctx: CheckContext,
|
|
23
52
|
): Promise<DoctorData> {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
checks
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
// buildChecks starts every check eagerly, so they run concurrently; each is
|
|
54
|
+
// guarded so a thrown check becomes an attributable fail rather than
|
|
55
|
+
// rejecting the whole run. Mapping preserves declaration order.
|
|
56
|
+
const checks: CheckResult[] = await Promise.all(
|
|
57
|
+
buildChecks(ctx).map(async ([name, promise]): Promise<CheckResult> => {
|
|
58
|
+
try {
|
|
59
|
+
return await promise;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
62
|
+
return {
|
|
63
|
+
name,
|
|
64
|
+
status: "fail",
|
|
65
|
+
detail: `Check threw an unexpected error: ${reason}`,
|
|
66
|
+
remedy:
|
|
67
|
+
"Re-run `pragma doctor`; if it persists, report this as a bug.",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
34
72
|
|
|
35
73
|
const passed = checks.filter((c) => c.status === "pass").length;
|
|
36
74
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PragmaError } from "#error";
|
|
3
|
+
import resolveUri from "./resolveUri.js";
|
|
4
|
+
|
|
5
|
+
const prefixes = {
|
|
6
|
+
ds: "https://ds.canonical.com/",
|
|
7
|
+
cs: "http://pragma.canonical.com/codestandards#",
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
describe("resolveUri", () => {
|
|
11
|
+
it("expands a registered prefix", () => {
|
|
12
|
+
expect(resolveUri("ds:UIBlock", prefixes)).toBe(
|
|
13
|
+
"https://ds.canonical.com/UIBlock",
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("passes through a full http(s) IRI", () => {
|
|
18
|
+
expect(resolveUri("https://ds.canonical.com/global", prefixes)).toBe(
|
|
19
|
+
"https://ds.canonical.com/global",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("passes through a urn: IRI", () => {
|
|
24
|
+
expect(resolveUri("urn:isbn:0451450523", prefixes)).toBe(
|
|
25
|
+
"urn:isbn:0451450523",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("passes through a foreign scheme:// IRI", () => {
|
|
30
|
+
expect(resolveUri("ftp://example.org/thing", prefixes)).toBe(
|
|
31
|
+
"ftp://example.org/thing",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("throws for an unknown prefix", () => {
|
|
36
|
+
expect(() => resolveUri("unknown:something", prefixes)).toThrow(
|
|
37
|
+
PragmaError,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("throws for a bare string without a prefix", () => {
|
|
42
|
+
expect(() => resolveUri("button", prefixes)).toThrow(PragmaError);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("throws for an IRI with unsafe characters", () => {
|
|
46
|
+
expect(() =>
|
|
47
|
+
resolveUri("https://example.com/<injected>", prefixes),
|
|
48
|
+
).toThrow(PragmaError);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -6,12 +6,20 @@ import { PragmaError } from "#error";
|
|
|
6
6
|
*/
|
|
7
7
|
const UNSAFE_IRI_PATTERN = /[<>"{}|\\^`\s]/;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Matches a general absolute IRI: any scheme with an authority (`scheme://…`,
|
|
11
|
+
* covering http, https, ftp, ws, and foreign schemes) or a `urn:` opaque IRI.
|
|
12
|
+
* Broader than an http(s)-only check so foreign ontologies resolve.
|
|
13
|
+
*/
|
|
14
|
+
const ABSOLUTE_IRI_PATTERN = /^(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/|urn:)\S/i;
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Resolves a potentially prefixed URI (e.g. `ds:UIBlock`) to a full URI.
|
|
11
18
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
19
|
+
* A registered prefix always wins: `ds:UIBlock` expands via the prefix map.
|
|
20
|
+
* Otherwise a general absolute IRI (`scheme://…` or `urn:…`) is validated and
|
|
21
|
+
* returned as-is, so foreign ontologies are not rejected as unknown prefixes.
|
|
22
|
+
* Any other `token:rest` is treated as an unknown prefix.
|
|
15
23
|
*
|
|
16
24
|
* @param uri - A full or prefixed URI string.
|
|
17
25
|
* @param prefixes - Registered prefix-to-namespace mapping from the store.
|
|
@@ -22,11 +30,6 @@ export default function resolveUri(
|
|
|
22
30
|
uri: string,
|
|
23
31
|
prefixes: Readonly<Record<string, string>>,
|
|
24
32
|
): string {
|
|
25
|
-
if (uri.startsWith("http://") || uri.startsWith("https://")) {
|
|
26
|
-
assertSafeIri(uri);
|
|
27
|
-
return uri;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
33
|
const colonIdx = uri.indexOf(":");
|
|
31
34
|
if (colonIdx === -1) {
|
|
32
35
|
throw PragmaError.invalidInput("uri", uri, {
|
|
@@ -38,23 +41,27 @@ export default function resolveUri(
|
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
const prefix = uri.slice(0, colonIdx);
|
|
41
|
-
const localName = uri.slice(colonIdx + 1);
|
|
42
44
|
const namespace = prefixes[prefix];
|
|
43
45
|
|
|
44
|
-
if (namespace
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
if (namespace !== undefined) {
|
|
47
|
+
const resolved = `${namespace}${uri.slice(colonIdx + 1)}`;
|
|
48
|
+
assertSafeIri(resolved);
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (ABSOLUTE_IRI_PATTERN.test(uri)) {
|
|
53
|
+
assertSafeIri(uri);
|
|
54
|
+
return uri;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
throw PragmaError.invalidInput("prefix", prefix, {
|
|
58
|
+
validOptions: Object.keys(prefixes),
|
|
59
|
+
recovery: {
|
|
60
|
+
message: "List known ontology prefixes.",
|
|
61
|
+
cli: "pragma ontology list",
|
|
62
|
+
mcp: { tool: "ontology_list" },
|
|
63
|
+
},
|
|
64
|
+
});
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
@@ -0,0 +1,196 @@
|
|
|
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 a structured error when explicit sources match nothing", async () => {
|
|
185
|
+
const ctx = makeCtx();
|
|
186
|
+
|
|
187
|
+
await expect(
|
|
188
|
+
buildCommand.execute({ sources: ["no-such-*.ttl"] }, ctx),
|
|
189
|
+
).rejects.toMatchObject({
|
|
190
|
+
code: "INVALID_INPUT",
|
|
191
|
+
recovery: {
|
|
192
|
+
message: "No files matched the given paths or glob patterns.",
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
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 { selectFormatter } from "../../shared/formatters.js";
|
|
11
|
+
import { DEFAULT_EXTRACTION_PATH, DEFAULT_SDL_PATH } from "../constants.js";
|
|
12
|
+
import { reportFormatters } from "../formatters/index.js";
|
|
13
|
+
import { gatherSources, parsePrefixes } from "../helpers/index.js";
|
|
14
|
+
import { compileSchema } from "../operations/index.js";
|
|
15
|
+
import type { GraphqlCompileReport } from "../types.js";
|
|
16
|
+
|
|
17
|
+
/** Narrow an unknown param to a string array. */
|
|
18
|
+
function readStringArray(value: unknown): string[] {
|
|
19
|
+
return Array.isArray(value)
|
|
20
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
21
|
+
: [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds the `pragma graphql build` command definition.
|
|
26
|
+
*
|
|
27
|
+
* Compiles TTL sources into a GraphQL schema, writes the SDL and the
|
|
28
|
+
* extraction artifact, and prints all compiler diagnostics. Exits
|
|
29
|
+
* non-zero when schema composition fails.
|
|
30
|
+
*/
|
|
31
|
+
const buildCommand: CommandDefinition = {
|
|
32
|
+
path: ["graphql", "build"],
|
|
33
|
+
description: "Compile TTL sources into GraphQL schema artifacts",
|
|
34
|
+
parameters: [
|
|
35
|
+
{
|
|
36
|
+
name: "sources",
|
|
37
|
+
description:
|
|
38
|
+
"TTL files or globs (default: the configured semantic packages)",
|
|
39
|
+
type: "multiselect",
|
|
40
|
+
positional: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "sdl",
|
|
44
|
+
description: "Output path for the GraphQL SDL",
|
|
45
|
+
type: "string",
|
|
46
|
+
default: DEFAULT_SDL_PATH,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "extraction",
|
|
50
|
+
description: "Output path for the extraction artifact",
|
|
51
|
+
type: "string",
|
|
52
|
+
default: DEFAULT_EXTRACTION_PATH,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "prefix",
|
|
56
|
+
description: "Ontology prefix as name=namespace (repeatable)",
|
|
57
|
+
type: "multiselect",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
meta: {
|
|
61
|
+
examples: [
|
|
62
|
+
"pragma graphql build ontology.ttl",
|
|
63
|
+
'pragma graphql build "data/*.ttl" --sdl out/schema.graphql --extraction out/extraction.json',
|
|
64
|
+
"pragma graphql build ontology.ttl --prefix ds=https://ds.canonical.com/",
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
async execute(params, ctx): Promise<CommandResult> {
|
|
68
|
+
const sources = await gatherSources(
|
|
69
|
+
readStringArray(params.sources),
|
|
70
|
+
ctx.cwd,
|
|
71
|
+
);
|
|
72
|
+
const prefixes = parsePrefixes(readStringArray(params.prefix));
|
|
73
|
+
const outcome = await compileSchema({ sources, prefixes, cwd: ctx.cwd });
|
|
74
|
+
const format = selectFormatter(ctx, reportFormatters);
|
|
75
|
+
|
|
76
|
+
if (outcome.status === "failed" || !outcome.compiled) {
|
|
77
|
+
const report: GraphqlCompileReport = {
|
|
78
|
+
diagnostics: outcome.diagnostics,
|
|
79
|
+
files: outcome.files,
|
|
80
|
+
sourcesHash: outcome.sourcesHash,
|
|
81
|
+
};
|
|
82
|
+
process.stdout.write(`${format(report)}\n`);
|
|
83
|
+
return createExitResult(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sdlPath = resolve(
|
|
87
|
+
ctx.cwd,
|
|
88
|
+
typeof params.sdl === "string" ? params.sdl : DEFAULT_SDL_PATH,
|
|
89
|
+
);
|
|
90
|
+
const extractionPath = resolve(
|
|
91
|
+
ctx.cwd,
|
|
92
|
+
typeof params.extraction === "string"
|
|
93
|
+
? params.extraction
|
|
94
|
+
: DEFAULT_EXTRACTION_PATH,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
mkdirSync(dirname(sdlPath), { recursive: true });
|
|
98
|
+
writeFileSync(sdlPath, outcome.compiled.sdl, "utf-8");
|
|
99
|
+
mkdirSync(dirname(extractionPath), { recursive: true });
|
|
100
|
+
writeFileSync(
|
|
101
|
+
extractionPath,
|
|
102
|
+
serializeExtraction(outcome.compiled.extraction, outcome.sourcesHash),
|
|
103
|
+
"utf-8",
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const report: GraphqlCompileReport = {
|
|
107
|
+
diagnostics: outcome.diagnostics,
|
|
108
|
+
files: outcome.files,
|
|
109
|
+
sourcesHash: outcome.sourcesHash,
|
|
110
|
+
artifacts: { sdl: sdlPath, extraction: extractionPath },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return createOutputResult(report, { plain: format });
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default buildCommand;
|