@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read semantic content from a package directory on disk.
|
|
3
|
+
*
|
|
4
|
+
* Shared by LocalLoader and GitLoader. Given a directory path, reads
|
|
5
|
+
* package.json for version, globs TTL files from definitions/ and data/,
|
|
6
|
+
* and scans skills/ for subdirectories.
|
|
7
|
+
*
|
|
8
|
+
* @note Impure — reads filesystem.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
globSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
statSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import type {
|
|
20
|
+
GraphContent,
|
|
21
|
+
SkillEntry,
|
|
22
|
+
StoryFileEntry,
|
|
23
|
+
} from "../semanticPackage.js";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// TTL discovery convention
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const TTL_DIRS: readonly { dir: string; glob: string }[] = [
|
|
30
|
+
{ dir: "definitions", glob: "definitions/**/*.ttl" },
|
|
31
|
+
{ dir: "data", glob: "data/**/*.ttl" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Public API
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export interface PackageDirContents {
|
|
39
|
+
readonly version: string;
|
|
40
|
+
readonly graphs: GraphContent[];
|
|
41
|
+
readonly skills: SkillEntry[];
|
|
42
|
+
readonly stories: StoryFileEntry[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read semantic package content from a directory.
|
|
47
|
+
*
|
|
48
|
+
* @param dir - Absolute path to the package root directory.
|
|
49
|
+
* @returns Package version, TTL graph content, and skill entries.
|
|
50
|
+
*/
|
|
51
|
+
export default function readPackageDir(dir: string): PackageDirContents {
|
|
52
|
+
return {
|
|
53
|
+
version: readVersion(dir),
|
|
54
|
+
graphs: readGraphs(dir),
|
|
55
|
+
skills: readSkills(dir),
|
|
56
|
+
stories: readStories(dir),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function readVersion(dir: string): string {
|
|
65
|
+
const pjsonPath = join(dir, "package.json");
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(pjsonPath, "utf-8");
|
|
68
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
69
|
+
return parsed.version ?? "0.0.0";
|
|
70
|
+
} catch {
|
|
71
|
+
return "0.0.0";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readGraphs(dir: string): GraphContent[] {
|
|
76
|
+
const graphs: GraphContent[] = [];
|
|
77
|
+
|
|
78
|
+
for (const entry of TTL_DIRS) {
|
|
79
|
+
if (!existsSync(join(dir, entry.dir))) continue;
|
|
80
|
+
|
|
81
|
+
const files = globSync(entry.glob, { cwd: dir });
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const absPath = join(dir, file);
|
|
84
|
+
try {
|
|
85
|
+
const content = readFileSync(absPath, "utf-8");
|
|
86
|
+
graphs.push({ path: absPath, content, format: "turtle" });
|
|
87
|
+
} catch {
|
|
88
|
+
// Unreadable file — skip silently
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return graphs;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readStories(dir: string): StoryFileEntry[] {
|
|
97
|
+
const storiesDir = join(dir, "stories");
|
|
98
|
+
if (!existsSync(storiesDir)) return [];
|
|
99
|
+
|
|
100
|
+
const stories: StoryFileEntry[] = [];
|
|
101
|
+
try {
|
|
102
|
+
// Sorted: readdir order is filesystem-dependent, and story collision
|
|
103
|
+
// resolution must be deterministic across platforms.
|
|
104
|
+
for (const entry of [...readdirSync(storiesDir)].sort()) {
|
|
105
|
+
if (!entry.endsWith(".json")) continue;
|
|
106
|
+
const entryPath = join(storiesDir, entry);
|
|
107
|
+
try {
|
|
108
|
+
stories.push({
|
|
109
|
+
path: entryPath,
|
|
110
|
+
definition: JSON.parse(readFileSync(entryPath, "utf-8")),
|
|
111
|
+
});
|
|
112
|
+
} catch {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`Warning: invalid JSON in story file ${entryPath}\n`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// readdir failed — no stories
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return stories;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readSkills(dir: string): SkillEntry[] {
|
|
126
|
+
const skillsDir = join(dir, "skills");
|
|
127
|
+
if (!existsSync(skillsDir)) return [];
|
|
128
|
+
|
|
129
|
+
const skills: SkillEntry[] = [];
|
|
130
|
+
try {
|
|
131
|
+
const entries = readdirSync(skillsDir);
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const entryPath = join(skillsDir, entry);
|
|
134
|
+
try {
|
|
135
|
+
if (statSync(entryPath).isDirectory()) {
|
|
136
|
+
skills.push({ dir: entryPath, folderName: entry });
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// stat failed — skip
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// readdir failed — no skills
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return skills;
|
|
147
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { BunPlugin } from "bun";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import createStoryAssetsPlugin, {
|
|
7
|
+
STORY_ASSET_PATH_PATTERN,
|
|
8
|
+
} from "./storyAssetsPlugin.js";
|
|
9
|
+
|
|
10
|
+
interface OnLoadRegistration {
|
|
11
|
+
readonly filter: RegExp;
|
|
12
|
+
readonly callback: (args: { path: string }) => unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Run the plugin's setup against a stub builder and capture its onLoad. */
|
|
16
|
+
function captureOnLoadRegistration(): OnLoadRegistration {
|
|
17
|
+
let captured: OnLoadRegistration | undefined;
|
|
18
|
+
const builder = {
|
|
19
|
+
onLoad(
|
|
20
|
+
constraints: { filter: RegExp },
|
|
21
|
+
callback: OnLoadRegistration["callback"],
|
|
22
|
+
): void {
|
|
23
|
+
captured = { filter: constraints.filter, callback };
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
createStoryAssetsPlugin().setup(
|
|
28
|
+
builder as unknown as Parameters<BunPlugin["setup"]>[0],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!captured) throw new Error("plugin did not register an onLoad handler");
|
|
32
|
+
return captured;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("STORY_ASSET_PATH_PATTERN", () => {
|
|
36
|
+
it("matches JSON directly inside a stories directory", () => {
|
|
37
|
+
expect(
|
|
38
|
+
STORY_ASSET_PATH_PATTERN.test(
|
|
39
|
+
"/repo/node_modules/@canonical/design-system/stories/color.json",
|
|
40
|
+
),
|
|
41
|
+
).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("rejects manifests, nested files, near-miss directories, and non-JSON", () => {
|
|
45
|
+
const nonStories = [
|
|
46
|
+
"/repo/node_modules/@canonical/design-system/package.json",
|
|
47
|
+
"/repo/node_modules/@canonical/design-system/stories/nested/color.json",
|
|
48
|
+
"/repo/node_modules/@canonical/design-system/histories/color.json",
|
|
49
|
+
"/repo/node_modules/@canonical/design-system/stories/color.jsonc",
|
|
50
|
+
"/repo/node_modules/@canonical/design-system/stories/color.ttl",
|
|
51
|
+
];
|
|
52
|
+
for (const path of nonStories) {
|
|
53
|
+
expect(STORY_ASSET_PATH_PATTERN.test(path), path).toBe(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("createStoryAssetsPlugin", () => {
|
|
59
|
+
let tmpDir: string | undefined;
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
63
|
+
tmpDir = undefined;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("registers its load hook under the story path filter", () => {
|
|
67
|
+
const { filter } = captureOnLoadRegistration();
|
|
68
|
+
expect(filter).toBe(STORY_ASSET_PATH_PATTERN);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("loads story JSON contents under the file loader", () => {
|
|
72
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pragma-story-assets-"));
|
|
73
|
+
const storyPath = join(tmpDir, "color.json");
|
|
74
|
+
const content = JSON.stringify({ noun: "color" });
|
|
75
|
+
writeFileSync(storyPath, content);
|
|
76
|
+
|
|
77
|
+
const { callback } = captureOnLoadRegistration();
|
|
78
|
+
|
|
79
|
+
expect(callback({ path: storyPath })).toEqual({
|
|
80
|
+
contents: content,
|
|
81
|
+
loader: "file",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time bundler plugin: embed story-pack JSON as file assets.
|
|
3
|
+
*
|
|
4
|
+
* Bun compiles `.json` build inputs into JSON modules, which never
|
|
5
|
+
* surface in `Bun.embeddedFiles`. Story-pack files must instead be
|
|
6
|
+
* embedded as file assets — like TTL graphs — so the bundled loader can
|
|
7
|
+
* serve them inside the compiled binary. This plugin scopes the `file`
|
|
8
|
+
* loader to story paths only, leaving every other JSON input (e.g.
|
|
9
|
+
* `package.json` imported for the CLI version) compiled as a module.
|
|
10
|
+
*
|
|
11
|
+
* Contract with the bundled loader: story files are the only `.json`
|
|
12
|
+
* content embedded as file assets, so every `.json` blob in
|
|
13
|
+
* `Bun.embeddedFiles` that is not named like a package manifest is a
|
|
14
|
+
* story definition. Consequence: no module in the bundle may import a
|
|
15
|
+
* `stories/*.json` file as a JSON module — the plugin would rewrite
|
|
16
|
+
* such an import into a file-path string.
|
|
17
|
+
*
|
|
18
|
+
* Consumed by `scripts/build.ts` only; never bundled into the binary.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import type { BunPlugin } from "bun";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Matches story-pack files: a `.json` directly inside a `stories/`
|
|
26
|
+
* directory — the same non-recursive scope the local loader scans.
|
|
27
|
+
*/
|
|
28
|
+
export const STORY_ASSET_PATH_PATTERN = /[\\/]stories[\\/][^\\/]+\.json$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create the bundler plugin that embeds `stories/*.json` as file assets.
|
|
32
|
+
*
|
|
33
|
+
* @returns A `Bun.build` plugin for the compiled-binary build.
|
|
34
|
+
* @note Impure — the registered load callback reads the filesystem.
|
|
35
|
+
*/
|
|
36
|
+
export default function createStoryAssetsPlugin(): BunPlugin {
|
|
37
|
+
return {
|
|
38
|
+
name: "story-assets",
|
|
39
|
+
setup(build) {
|
|
40
|
+
build.onLoad({ filter: STORY_ASSET_PATH_PATTERN }, (args) => ({
|
|
41
|
+
contents: readFileSync(args.path, "utf-8"),
|
|
42
|
+
loader: "file",
|
|
43
|
+
}));
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PragmaError } from "#error";
|
|
3
|
+
import lookupMany, { lookupToolMeta } from "./lookupMany.js";
|
|
4
|
+
|
|
5
|
+
describe("lookupMany", () => {
|
|
6
|
+
it("returns all results when every query resolves", async () => {
|
|
7
|
+
const result = await lookupMany(["a", "b"], async (q) => q.toUpperCase());
|
|
8
|
+
|
|
9
|
+
expect(result.results).toEqual(["A", "B"]);
|
|
10
|
+
expect(result.errors).toEqual([]);
|
|
11
|
+
expect(result.meta).toEqual({ internalErrorCount: 0 });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("collects PragmaError failures as structured error entries", async () => {
|
|
15
|
+
const result = await lookupMany(["Button", "Buton"], async (q) => {
|
|
16
|
+
if (q === "Buton") {
|
|
17
|
+
throw PragmaError.notFound("block", q, { suggestions: ["Button"] });
|
|
18
|
+
}
|
|
19
|
+
return q;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(result.results).toEqual(["Button"]);
|
|
23
|
+
expect(result.errors).toEqual([
|
|
24
|
+
{
|
|
25
|
+
query: "Buton",
|
|
26
|
+
code: "ENTITY_NOT_FOUND",
|
|
27
|
+
message: 'block "Buton" not found.',
|
|
28
|
+
suggestions: ["Button"],
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
expect(result.meta).toEqual({ internalErrorCount: 0 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps N-1 results when one query throws a non-PragmaError", async () => {
|
|
35
|
+
const result = await lookupMany(
|
|
36
|
+
["a", "poisoned", "c"],
|
|
37
|
+
async (q): Promise<string> => {
|
|
38
|
+
if (q === "poisoned") {
|
|
39
|
+
throw new TypeError("cannot read properties of undefined");
|
|
40
|
+
}
|
|
41
|
+
return q.toUpperCase();
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(result.results).toEqual(["A", "C"]);
|
|
46
|
+
expect(result.errors).toEqual([
|
|
47
|
+
{
|
|
48
|
+
query: "poisoned",
|
|
49
|
+
code: "INTERNAL_ERROR",
|
|
50
|
+
message: "Internal error: cannot read properties of undefined",
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
expect(result.meta).toEqual({ internalErrorCount: 1 });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("stringifies non-Error rejection values", async () => {
|
|
57
|
+
const result = await lookupMany(["a"], async () => {
|
|
58
|
+
throw "string failure";
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.results).toEqual([]);
|
|
62
|
+
expect(result.errors).toEqual([
|
|
63
|
+
{
|
|
64
|
+
query: "a",
|
|
65
|
+
code: "INTERNAL_ERROR",
|
|
66
|
+
message: "Internal error: string failure",
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
expect(result.meta).toEqual({ internalErrorCount: 1 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("counts each internal error and mixes with PragmaError entries", async () => {
|
|
73
|
+
const result = await lookupMany(
|
|
74
|
+
["ok", "missing", "boom-1", "boom-2"],
|
|
75
|
+
async (q): Promise<string> => {
|
|
76
|
+
if (q === "missing") throw PragmaError.notFound("token", q);
|
|
77
|
+
if (q.startsWith("boom")) throw new Error(q);
|
|
78
|
+
return q;
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result.results).toEqual(["ok"]);
|
|
83
|
+
expect(result.errors).toHaveLength(3);
|
|
84
|
+
expect(result.errors.map((e) => e.code)).toEqual([
|
|
85
|
+
"ENTITY_NOT_FOUND",
|
|
86
|
+
"INTERNAL_ERROR",
|
|
87
|
+
"INTERNAL_ERROR",
|
|
88
|
+
]);
|
|
89
|
+
expect(result.meta).toEqual({ internalErrorCount: 2 });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("lookupToolMeta", () => {
|
|
94
|
+
it("reports count and omits internalErrorCount when zero", async () => {
|
|
95
|
+
const result = await lookupMany(["a", "b"], async (q) => q);
|
|
96
|
+
|
|
97
|
+
expect(lookupToolMeta(result)).toEqual({ count: 2 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("includes internalErrorCount when at least one query failed unexpectedly", async () => {
|
|
101
|
+
const result = await lookupMany(
|
|
102
|
+
["a", "boom"],
|
|
103
|
+
async (q): Promise<string> => {
|
|
104
|
+
if (q === "boom") throw new Error("boom");
|
|
105
|
+
return q;
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(lookupToolMeta(result)).toEqual({
|
|
110
|
+
count: 1,
|
|
111
|
+
internalErrorCount: 1,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("tolerates hand-built results without meta", () => {
|
|
116
|
+
expect(lookupToolMeta({ results: ["x"], errors: [] })).toEqual({
|
|
117
|
+
count: 1,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -6,16 +6,27 @@ import type { LookupResult } from "./contracts.js";
|
|
|
6
6
|
*
|
|
7
7
|
* Used by MCP lookup tools so a single call can return multiple results
|
|
8
8
|
* without failing the entire request when one query is missing.
|
|
9
|
+
*
|
|
10
|
+
* The collection loop never rejects: a `PragmaError` becomes an error entry
|
|
11
|
+
* with its own code, and any other thrown value becomes an `INTERNAL_ERROR`
|
|
12
|
+
* entry — so one poisoned query cannot discard the other queries' results.
|
|
13
|
+
* `meta.internalErrorCount` reports how many entries are internal errors.
|
|
9
14
|
*/
|
|
10
15
|
export default async function lookupMany<TResult>(
|
|
11
16
|
queries: readonly string[],
|
|
12
17
|
lookup: (query: string) => Promise<TResult>,
|
|
13
18
|
): Promise<LookupResult<TResult>> {
|
|
14
|
-
const errors: Array<{
|
|
19
|
+
const errors: Array<{
|
|
20
|
+
query: string;
|
|
21
|
+
code: string;
|
|
22
|
+
message: string;
|
|
23
|
+
suggestions?: readonly string[];
|
|
24
|
+
}> = [];
|
|
15
25
|
const settled = await Promise.allSettled(
|
|
16
26
|
queries.map((query) => lookup(query)),
|
|
17
27
|
);
|
|
18
28
|
const results: TResult[] = [];
|
|
29
|
+
let internalErrorCount = 0;
|
|
19
30
|
|
|
20
31
|
for (const [index, outcome] of settled.entries()) {
|
|
21
32
|
const query = queries[index];
|
|
@@ -35,11 +46,40 @@ export default async function lookupMany<TResult>(
|
|
|
35
46
|
query,
|
|
36
47
|
code: error.code,
|
|
37
48
|
message: error.message,
|
|
49
|
+
...(error.suggestions.length > 0 && {
|
|
50
|
+
suggestions: error.suggestions,
|
|
51
|
+
}),
|
|
38
52
|
});
|
|
39
53
|
} else {
|
|
40
|
-
|
|
54
|
+
internalErrorCount += 1;
|
|
55
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
56
|
+
errors.push({
|
|
57
|
+
query,
|
|
58
|
+
code: "INTERNAL_ERROR",
|
|
59
|
+
message: `Internal error: ${reason}`,
|
|
60
|
+
});
|
|
41
61
|
}
|
|
42
62
|
}
|
|
43
63
|
|
|
44
|
-
return { results, errors };
|
|
64
|
+
return { results, errors, meta: { internalErrorCount } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build MCP envelope metadata for a lookup result.
|
|
69
|
+
*
|
|
70
|
+
* Always reports `count`; includes `internalErrorCount` only when at least
|
|
71
|
+
* one query failed unexpectedly, so healthy responses stay unchanged.
|
|
72
|
+
*
|
|
73
|
+
* @param result - A lookup result, typically produced via {@link lookupMany}.
|
|
74
|
+
* @returns Meta object for the `{ ok, data, meta }` tool envelope.
|
|
75
|
+
*/
|
|
76
|
+
export function lookupToolMeta<TResult>(result: LookupResult<TResult>): {
|
|
77
|
+
count: number;
|
|
78
|
+
internalErrorCount?: number;
|
|
79
|
+
} {
|
|
80
|
+
const internalErrorCount = result.meta?.internalErrorCount ?? 0;
|
|
81
|
+
return {
|
|
82
|
+
count: result.results.length,
|
|
83
|
+
...(internalErrorCount > 0 && { internalErrorCount }),
|
|
84
|
+
};
|
|
45
85
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import type { RawPackageEntry } from "../refs/operations/parseRef.js";
|
|
6
|
+
import { mergeAndParseRefs } from "./mergeAndParseRefs.js";
|
|
7
|
+
import { DEFAULT_PACKAGES } from "./packages.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_NAMES = DEFAULT_PACKAGES.map((entry) =>
|
|
10
|
+
typeof entry === "string" ? entry : entry.name,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
function writeGlobalRefs(
|
|
14
|
+
xdgDir: string,
|
|
15
|
+
packages: ReadonlyArray<RawPackageEntry>,
|
|
16
|
+
): void {
|
|
17
|
+
const dir = join(xdgDir, "pragma");
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
writeFileSync(join(dir, "refs.json"), JSON.stringify({ packages }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("mergeAndParseRefs", () => {
|
|
23
|
+
let tmpDir: string;
|
|
24
|
+
const origEnv = { ...process.env };
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pragma-merge-refs-"));
|
|
28
|
+
// Point XDG_CONFIG_HOME to our temp dir so readGlobalRefs() resolves there
|
|
29
|
+
process.env.XDG_CONFIG_HOME = tmpDir;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
process.env = { ...origEnv };
|
|
34
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns parsed defaults when config and global refs are absent", () => {
|
|
38
|
+
const refs = mergeAndParseRefs(undefined);
|
|
39
|
+
expect(refs.map((ref) => ref.pkg)).toEqual(DEFAULT_NAMES);
|
|
40
|
+
expect(refs.every((ref) => ref.kind === "git")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("treats an explicitly empty config list as not configured", () => {
|
|
44
|
+
expect(mergeAndParseRefs([])).toEqual(mergeAndParseRefs(undefined));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("falls back to global refs when the config list is empty", () => {
|
|
48
|
+
writeGlobalRefs(tmpDir, [
|
|
49
|
+
{ name: "@canonical/design-system", source: "file:///tmp/ds" },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const refs = mergeAndParseRefs([]);
|
|
53
|
+
|
|
54
|
+
expect(refs.map((ref) => ref.pkg)).toEqual(DEFAULT_NAMES);
|
|
55
|
+
expect(refs.at(0)).toEqual({
|
|
56
|
+
kind: "file",
|
|
57
|
+
pkg: "@canonical/design-system",
|
|
58
|
+
path: "/tmp/ds",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("merges global refs over defaults by package name", () => {
|
|
63
|
+
writeGlobalRefs(tmpDir, [
|
|
64
|
+
{ name: "@canonical/design-system", source: "file:///tmp/ds" },
|
|
65
|
+
"@acme/extra",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const refs = mergeAndParseRefs(undefined);
|
|
69
|
+
|
|
70
|
+
expect(refs.map((ref) => ref.pkg)).toEqual([
|
|
71
|
+
...DEFAULT_NAMES,
|
|
72
|
+
"@acme/extra",
|
|
73
|
+
]);
|
|
74
|
+
expect(refs.at(0)?.kind).toBe("file");
|
|
75
|
+
expect(refs.at(-1)).toEqual({ kind: "npm", pkg: "@acme/extra" });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("replaces defaults and global refs with a non-empty config list", () => {
|
|
79
|
+
writeGlobalRefs(tmpDir, ["@acme/global-only"]);
|
|
80
|
+
|
|
81
|
+
const refs = mergeAndParseRefs([
|
|
82
|
+
{ name: "@acme/only", source: "file:///tmp/only" },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(refs).toEqual([
|
|
86
|
+
{ kind: "file", pkg: "@acme/only", path: "/tmp/only" },
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PackageRef,
|
|
3
|
+
parsePackageEntry,
|
|
4
|
+
type RawPackageEntry,
|
|
5
|
+
} from "../refs/operations/parseRef.js";
|
|
6
|
+
import readGlobalRefs from "../refs/operations/readGlobalRefs.js";
|
|
7
|
+
import { DEFAULT_PACKAGES } from "./packages.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Merge config packages, global refs, and the built-in defaults into a
|
|
11
|
+
* final parsed `PackageRef` array.
|
|
12
|
+
*
|
|
13
|
+
* Priority: config `packages` > global `refs.json` > hardcoded defaults.
|
|
14
|
+
* A non-empty config list is a full replacement — only the listed packages
|
|
15
|
+
* resolve. An empty or absent list means "not configured": global refs are
|
|
16
|
+
* merged over the defaults by package name, so a global entry for
|
|
17
|
+
* "@canonical/foo" overrides the default entry for the same package.
|
|
18
|
+
*
|
|
19
|
+
* Kept free of store imports so config-only consumers (e.g.
|
|
20
|
+
* `resolveConfiguredGraphs`) can merge refs without loading the ke/WASM
|
|
21
|
+
* runtime.
|
|
22
|
+
*
|
|
23
|
+
* @note Impure — reads global refs from `~/.config/pragma/refs.json`.
|
|
24
|
+
*
|
|
25
|
+
* @param configPackages - The `packages` field from `pragma.config.json`.
|
|
26
|
+
* @returns Parsed refs ready for the loader chain.
|
|
27
|
+
* @throws PragmaError with code CONFIG_ERROR when an entry is malformed.
|
|
28
|
+
*/
|
|
29
|
+
export function mergeAndParseRefs(
|
|
30
|
+
configPackages?: ReadonlyArray<RawPackageEntry>,
|
|
31
|
+
): PackageRef[] {
|
|
32
|
+
// A non-empty config list replaces the package set entirely.
|
|
33
|
+
if (configPackages && configPackages.length > 0) {
|
|
34
|
+
return configPackages.map((entry) => parsePackageEntry(entry));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Empty or absent → merge global refs over the defaults, keyed by name.
|
|
38
|
+
const merged = new Map<string, RawPackageEntry>();
|
|
39
|
+
for (const entry of [...DEFAULT_PACKAGES, ...readGlobalRefs()]) {
|
|
40
|
+
const name = typeof entry === "string" ? entry : entry.name;
|
|
41
|
+
merged.set(name, entry);
|
|
42
|
+
}
|
|
43
|
+
return [...merged.values()].map((entry) => parsePackageEntry(entry));
|
|
44
|
+
}
|