@canonical/pragma-cli 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -47
- package/dist/pragma +0 -0
- package/package.json +15 -13
- package/src/completions/constants.ts +7 -0
- package/src/completions/handleQuery.test.ts +32 -0
- package/src/completions/handleQuery.ts +20 -1
- package/src/completions/queryCompletions.test.ts +5 -0
- package/src/completions/queryCompletions.ts +17 -3
- package/src/completions/querySocket.test.ts +31 -0
- package/src/completions/querySocket.ts +36 -9
- package/src/config/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 +217 -0
- package/src/config/readConfig.test.ts +104 -13
- package/src/config/readConfig.ts +12 -153
- package/src/config/readConfigLayers.test.ts +109 -0
- package/src/config/readConfigLayers.ts +118 -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 +83 -1
- package/src/config/writeConfig.ts +25 -55
- package/src/constants.ts +3 -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 +2 -1
- package/src/domains/block/formatters/list.test.ts +3 -3
- package/src/domains/block/formatters/lookup.test.ts +5 -5
- package/src/domains/block/mcp/specs.ts +16 -196
- package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
- package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
- package/src/domains/block/stories.test.ts +28 -0
- package/src/domains/block/stories.ts +260 -0
- package/src/domains/config/commands/channel.test.ts +30 -3
- package/src/domains/config/commands/channel.ts +25 -8
- package/src/domains/config/commands/configScope.ts +46 -0
- package/src/domains/config/commands/framework.test.ts +32 -1
- package/src/domains/config/commands/framework.ts +26 -8
- package/src/domains/config/commands/show.ts +5 -45
- package/src/domains/config/commands/tier.test.ts +20 -2
- package/src/domains/config/commands/tier.ts +25 -8
- package/src/domains/config/commands/trace.test.ts +19 -2
- package/src/domains/config/commands/trace.ts +6 -3
- 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 +14 -11
- 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 +7 -7
- package/src/domains/config/mcp/specs.ts +33 -53
- 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/validateTier.test.ts +9 -0
- package/src/domains/config/operations/validateTier.ts +10 -2
- package/src/domains/config/stories.ts +42 -0
- package/src/domains/config/tasks/index.ts +1 -0
- package/src/domains/config/tasks/setChannel.ts +13 -28
- package/src/domains/config/tasks/setFramework.ts +11 -30
- package/src/domains/config/tasks/setTier.ts +10 -29
- package/src/domains/config/tasks/setTrace.ts +10 -28
- package/src/domains/config/tasks/writeConfigField.ts +41 -0
- package/src/domains/create/generatorPack/bundled.ts +49 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
- package/src/domains/create/index.ts +13 -7
- package/src/domains/create/mcp/specs.ts +6 -161
- package/src/domains/create/renderGeneratorUi.test.ts +103 -0
- package/src/domains/create/renderGeneratorUi.ts +65 -0
- package/src/domains/doctor/formatters/doctor.test.ts +50 -2
- package/src/domains/doctor/formatters/doctor.ts +90 -38
- package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +133 -0
- package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +32 -26
- 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 +160 -7
- package/src/domains/doctor/operations/runChecks.test.ts +15 -6
- package/src/domains/doctor/operations/runChecks.ts +2 -0
- package/src/domains/doctor/operations/types.ts +23 -1
- package/src/domains/graph/commands/inspect.test.ts +2 -2
- package/src/domains/graph/commands/query.test.ts +5 -3
- package/src/domains/graph/formatters/inspect.ts +7 -7
- package/src/domains/graph/formatters/query.test.ts +87 -0
- package/src/domains/graph/formatters/query.ts +42 -5
- package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
- package/src/domains/graph/helpers/resolveUri.ts +28 -21
- package/src/domains/graph/operations/inspectUri.test.ts +5 -5
- package/src/domains/graphql/commands/build.test.ts +5 -3
- package/src/domains/graphql/commands/build.ts +8 -16
- package/src/domains/graphql/commands/check.test.ts +5 -3
- package/src/domains/graphql/commands/check.ts +7 -13
- package/src/domains/graphql/commands/index.ts +1 -0
- package/src/domains/graphql/commands/serve.test.ts +46 -0
- package/src/domains/graphql/commands/serve.ts +141 -0
- package/src/domains/graphql/constants.ts +10 -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 +2 -0
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
- package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
- package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
- package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
- package/src/domains/graphql/index.ts +9 -6
- package/src/domains/graphql/operations/compileSchema.test.ts +23 -47
- package/src/domains/graphql/operations/compileSchema.ts +23 -25
- package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
- package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
- package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
- package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
- package/src/domains/graphql/operations/index.ts +10 -0
- package/src/domains/info/formatters/upgrade.test.ts +7 -4
- package/src/domains/info/formatters/upgrade.ts +3 -1
- package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
- package/src/domains/info/operations/collectEntityCounts.ts +123 -0
- package/src/domains/info/operations/collectInfo.test.ts +14 -3
- package/src/domains/info/operations/collectInfo.ts +12 -15
- package/src/domains/info/operations/index.ts +1 -0
- package/src/domains/info/types.ts +13 -0
- package/src/domains/llm/capabilities.test.ts +24 -4
- package/src/domains/llm/commands/capabilities.ts +12 -3
- package/src/domains/llm/commands/llm.ts +5 -1
- package/src/domains/llm/data/commandReference.ts +20 -11
- package/src/domains/llm/data/decisionTrees.ts +10 -5
- package/src/domains/llm/data/toolCatalog.ts +55 -18
- package/src/domains/llm/formatters/orientation.ts +5 -1
- package/src/domains/llm/llmCommand.test.ts +11 -3
- package/src/domains/llm/mcp/specs.ts +3 -4
- package/src/domains/llm/operations/collectContext.test.ts +64 -0
- package/src/domains/llm/operations/collectContext.ts +7 -17
- package/src/domains/modifier/commands/list.ts +5 -38
- package/src/domains/modifier/commands/lookup.ts +8 -79
- package/src/domains/modifier/commands/sample.ts +2 -1
- package/src/domains/modifier/mcp/specs.ts +15 -94
- package/src/domains/modifier/operations/lookup.ts +5 -3
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
- package/src/domains/modifier/stories.test.ts +20 -0
- package/src/domains/modifier/stories.ts +86 -0
- package/src/domains/ontology/commands/list.test.ts +5 -3
- package/src/domains/ontology/commands/list.ts +5 -58
- package/src/domains/ontology/commands/show.test.ts +85 -9
- package/src/domains/ontology/commands/show.ts +6 -58
- package/src/domains/ontology/formatters/index.ts +1 -0
- package/src/domains/ontology/formatters/list.ts +37 -12
- package/src/domains/ontology/formatters/show.ts +361 -64
- package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
- package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
- package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
- package/src/domains/ontology/helpers/index.ts +14 -0
- package/src/domains/ontology/helpers/queryClasses.ts +32 -16
- package/src/domains/ontology/helpers/queryProperties.ts +39 -6
- package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
- package/src/domains/ontology/mcp/specs.ts +7 -84
- package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
- package/src/domains/ontology/operations/listOntologies.ts +126 -30
- package/src/domains/ontology/operations/showOntology.test.ts +131 -12
- package/src/domains/ontology/operations/showOntology.ts +289 -7
- package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
- package/src/domains/ontology/stories.test.ts +36 -0
- package/src/domains/ontology/stories.ts +153 -0
- package/src/domains/refs/operations/updateRefs.ts +2 -38
- package/src/domains/setup/commands/index.ts +1 -0
- package/src/domains/setup/commands/setup.ts +70 -0
- 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/index.ts +7 -3
- package/src/domains/setup/operations/setupLsp.test.ts +3 -2
- package/src/domains/setup/operations/setupLsp.ts +7 -2
- package/src/domains/setup/operations/setupMcp.test.ts +6 -1
- package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
- package/src/domains/shared/bootStore.test.ts +82 -0
- package/src/domains/shared/bootStore.ts +57 -18
- 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 +11 -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 +23 -26
- package/src/domains/shared/extractLocalName.test.ts +3 -3
- package/src/domains/shared/formatters.ts +23 -2
- package/src/domains/shared/listTiers.ts +40 -0
- package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
- package/src/domains/shared/loaders/bundledLoader.ts +158 -12
- package/src/domains/shared/loaders/gitLoader.ts +1 -2
- package/src/domains/shared/loaders/index.ts +4 -1
- package/src/domains/shared/loaders/localLoader.ts +2 -4
- package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
- package/src/domains/shared/loaders/readPackageDir.ts +82 -9
- 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 +135 -0
- package/src/domains/shared/lookupMany.ts +50 -2
- package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
- package/src/domains/shared/mergeAndParseRefs.ts +44 -0
- package/src/domains/shared/namespaces.ts +13 -5
- package/src/domains/shared/packages.ts +10 -17
- package/src/domains/shared/parseSampleCount.test.ts +47 -0
- package/src/domains/shared/parseSampleCount.ts +45 -0
- package/src/domains/shared/prefixes.test.ts +211 -0
- package/src/domains/shared/prefixes.ts +182 -15
- package/src/domains/shared/renderers.ts +5 -5
- package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
- package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
- package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
- package/src/domains/shared/resolveLookupExitCode.ts +42 -0
- package/src/domains/shared/runtime.ts +4 -63
- package/src/domains/shared/semanticPackage.test.ts +89 -0
- package/src/domains/shared/semanticPackage.ts +34 -3
- package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
- package/src/domains/shared/stories/compileLookupCommand.ts +102 -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 +151 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +111 -0
- package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
- package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +81 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +154 -0
- package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
- package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
- package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +406 -0
- package/src/domains/shared/stories/pack/collectPackStories.ts +158 -0
- package/src/domains/shared/stories/pack/compilePackCommands.ts +50 -0
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +751 -0
- package/src/domains/shared/stories/pack/compilePackStories.ts +724 -0
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +55 -0
- package/src/domains/shared/stories/pack/index.ts +42 -0
- package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
- package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
- package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +381 -0
- package/src/domains/shared/stories/pack/types.ts +224 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +742 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +859 -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 +179 -0
- package/src/domains/shared/suggestions/expandGlob.ts +1 -0
- package/src/domains/shared/suggestions/suggestNames.ts +43 -10
- package/src/domains/shared/types/index.ts +5 -9
- package/src/domains/shared/types/ontology.ts +119 -24
- package/src/domains/shared/types/standard.ts +9 -52
- package/src/domains/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 +0 -1
- 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/token/commands/lookup.test.ts +8 -0
- package/src/domains/token/commands/lookup.ts +12 -0
- package/src/domains/token/commands/sample.ts +2 -1
- package/src/domains/token/featureFlag.ts +17 -0
- package/src/domains/token/index.ts +5 -0
- package/src/domains/token/mcp/specs.ts +18 -2
- package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
- package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
- package/src/domains/trace/commands/show.ts +30 -4
- package/src/domains/trace/tracePlugin.test.ts +1 -1
- package/src/index.ts +1 -13
- package/src/mcp/registerResources.test.ts +382 -74
- package/src/mcp/registerResources.ts +5 -348
- 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 +22 -5
- package/src/mcp/tools/registerTools.test.ts +167 -34
- package/src/mcp/types.ts +4 -0
- package/src/pipeline/collectCommands.test.ts +151 -1
- package/src/pipeline/collectCommands.ts +35 -13
- package/src/pipeline/createProgram.test.ts +5 -1
- package/src/pipeline/createProgram.ts +2 -2
- package/src/pipeline/firstRun.test.ts +80 -0
- package/src/pipeline/firstRun.ts +99 -0
- package/src/pipeline/parseGlobalFlags.test.ts +102 -1
- package/src/pipeline/parseGlobalFlags.ts +98 -9
- package/src/pipeline/resolveCommandKind.test.ts +12 -0
- package/src/pipeline/resolveCommandKind.ts +4 -0
- package/src/pipeline/rootHelp.test.ts +87 -0
- package/src/pipeline/rootHelp.ts +265 -0
- package/src/pipeline/runCli.orchestration.test.ts +28 -12
- package/src/pipeline/runCli.test.ts +17 -4
- package/src/pipeline/runCli.ts +202 -25
- package/src/testing/cli.ts +30 -2
- package/src/testing/createTestMcpClient.ts +4 -1
- package/src/testing/dsFixtures.ts +21 -4
- package/src/testing/index.ts +7 -0
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
- package/src/testing/integration/cli-user-stories.test.ts +63 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
- package/src/testing/integration/edge-cases.test.ts +62 -50
- package/src/testing/integration/executor-execution.test.ts +103 -0
- package/src/testing/integration/executor-golden.test.ts +145 -0
- package/src/testing/integration/mcp-surface.test.ts +40 -15
- package/src/testing/integration/parity.test.ts +136 -82
- package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
- package/src/testing/setupXdgIsolation.ts +26 -0
- package/src/testing/standardParityFixtures.ts +35 -0
- package/src/testing/store.ts +4 -4
- package/src/testing/storyFixtures.ts +63 -0
- package/src/domains/create/commands/component.test.ts +0 -166
- package/src/domains/create/commands/component.ts +0 -123
- package/src/domains/create/commands/index.ts +0 -3
- package/src/domains/create/commands/package.test.ts +0 -35
- package/src/domains/create/commands/package.ts +0 -21
- package/src/domains/create/generators.ts +0 -8
- package/src/domains/standard/commands/categories.test.ts +0 -70
- package/src/domains/standard/commands/categories.ts +0 -48
- package/src/domains/standard/commands/index.ts +0 -6
- package/src/domains/standard/commands/list.test.ts +0 -105
- package/src/domains/standard/commands/list.ts +0 -90
- package/src/domains/standard/commands/lookup.test.ts +0 -135
- package/src/domains/standard/commands/lookup.ts +0 -99
- package/src/domains/standard/commands/sample.ts +0 -57
- package/src/domains/standard/formatters/categories.test.ts +0 -44
- package/src/domains/standard/formatters/categories.ts +0 -38
- package/src/domains/standard/formatters/index.ts +0 -10
- package/src/domains/standard/formatters/list.test.ts +0 -169
- package/src/domains/standard/formatters/list.ts +0 -184
- package/src/domains/standard/formatters/lookup.test.ts +0 -95
- package/src/domains/standard/formatters/lookup.ts +0 -101
- package/src/domains/standard/formatters/sample.ts +0 -15
- package/src/domains/standard/formatters/types.ts +0 -31
- package/src/domains/standard/index.ts +0 -40
- package/src/domains/standard/mcp/index.ts +0 -2
- package/src/domains/standard/mcp/specs.ts +0 -244
- package/src/domains/standard/operations/categories.test.ts +0 -61
- package/src/domains/standard/operations/categories.ts +0 -38
- package/src/domains/standard/operations/index.ts +0 -6
- package/src/domains/standard/operations/list.test.ts +0 -75
- package/src/domains/standard/operations/list.ts +0 -64
- package/src/domains/standard/operations/lookup.test.ts +0 -58
- package/src/domains/standard/operations/lookup.ts +0 -151
- package/src/domains/standard/operations/sample.ts +0 -35
- package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
- package/src/domains/standard/orchestration/index.ts +0 -4
- package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -32
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
- package/src/domains/standard/standardConfig.ts +0 -42
- package/src/domains/standard/types.ts +0 -15
- package/src/domains/tier/commands/index.ts +0 -3
- package/src/domains/tier/commands/list.test.ts +0 -67
- package/src/domains/tier/commands/list.ts +0 -59
- package/src/domains/tier/formatters/index.ts +0 -3
- package/src/domains/tier/formatters/list.test.ts +0 -33
- package/src/domains/tier/formatters/list.ts +0 -45
- package/src/domains/tier/index.ts +0 -17
- package/src/domains/tier/mcp/index.ts +0 -2
- package/src/domains/tier/mcp/specs.ts +0 -38
- package/src/domains/tier/operations/index.ts +0 -3
- package/src/domains/tier/operations/list.test.ts +0 -32
- package/src/domains/tier/operations/list.ts +0 -33
- package/src/pipeline/runInteractiveCommand.ts +0 -198
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
2
8
|
import { tmpdir } from "node:os";
|
|
3
9
|
import { join } from "node:path";
|
|
4
10
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
@@ -6,11 +12,22 @@ import { PragmaError } from "../error/PragmaError.js";
|
|
|
6
12
|
import readConfig from "./readConfig.js";
|
|
7
13
|
import writeConfig from "./writeConfig.js";
|
|
8
14
|
|
|
15
|
+
/** Write to `<dir>/pragma.config.json` — these tests pin file mechanics. */
|
|
16
|
+
function writeConfigLocal(
|
|
17
|
+
dir: string,
|
|
18
|
+
update: Parameters<typeof writeConfig>[1],
|
|
19
|
+
): string {
|
|
20
|
+
return writeConfig(dir, update, "local");
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
describe("readConfig", () => {
|
|
10
24
|
let dir: string;
|
|
11
25
|
|
|
12
26
|
beforeEach(() => {
|
|
13
27
|
dir = mkdtempSync(join(tmpdir(), "pragma-config-"));
|
|
28
|
+
// Bound the project-config walk at this fixture root so a stray
|
|
29
|
+
// pragma.config.json in an ancestor directory can never leak in.
|
|
30
|
+
mkdirSync(join(dir, ".git"));
|
|
14
31
|
});
|
|
15
32
|
|
|
16
33
|
afterEach(() => {
|
|
@@ -174,6 +191,64 @@ describe("readConfig", () => {
|
|
|
174
191
|
});
|
|
175
192
|
});
|
|
176
193
|
|
|
194
|
+
describe("readConfig — stories and prefixes fields", () => {
|
|
195
|
+
let dir: string;
|
|
196
|
+
|
|
197
|
+
beforeEach(() => {
|
|
198
|
+
dir = mkdtempSync(join(tmpdir(), "pragma-config-stories-"));
|
|
199
|
+
mkdirSync(join(dir, ".git"));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
afterEach(() => {
|
|
203
|
+
rmSync(dir, { recursive: true, force: true });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("parses a valid stories array and prefixes map", () => {
|
|
207
|
+
writeFileSync(
|
|
208
|
+
join(dir, "pragma.config.json"),
|
|
209
|
+
JSON.stringify({
|
|
210
|
+
prefixes: { ex: "http://example.org/" },
|
|
211
|
+
stories: [
|
|
212
|
+
{
|
|
213
|
+
noun: "recipe",
|
|
214
|
+
list: {
|
|
215
|
+
query: "SELECT ?uri WHERE { ?uri a ex:Recipe }",
|
|
216
|
+
columns: [{ field: "uri" }],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
const config = readConfig(dir);
|
|
223
|
+
expect(config.prefixes).toEqual({ ex: "http://example.org/" });
|
|
224
|
+
expect(config.stories?.at(0)?.noun).toBe("recipe");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("throws on a non-array stories field", () => {
|
|
228
|
+
writeFileSync(
|
|
229
|
+
join(dir, "pragma.config.json"),
|
|
230
|
+
'{"stories": {"noun": "x"}}',
|
|
231
|
+
);
|
|
232
|
+
expect(() => readConfig(dir)).toThrow(/stories/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("throws on an invalid story definition", () => {
|
|
236
|
+
writeFileSync(
|
|
237
|
+
join(dir, "pragma.config.json"),
|
|
238
|
+
'{"stories": [{"noun": "Bad Noun"}]}',
|
|
239
|
+
);
|
|
240
|
+
expect(() => readConfig(dir)).toThrow(PragmaError);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("throws on a prefixes value that is not an IRI", () => {
|
|
244
|
+
writeFileSync(
|
|
245
|
+
join(dir, "pragma.config.json"),
|
|
246
|
+
'{"prefixes": {"ex": "not-an-iri"}}',
|
|
247
|
+
);
|
|
248
|
+
expect(() => readConfig(dir)).toThrow(/prefix/);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
177
252
|
describe("writeConfig", () => {
|
|
178
253
|
let dir: string;
|
|
179
254
|
|
|
@@ -186,14 +261,14 @@ describe("writeConfig", () => {
|
|
|
186
261
|
});
|
|
187
262
|
|
|
188
263
|
it("creates a new config file with tier", () => {
|
|
189
|
-
|
|
264
|
+
writeConfigLocal(dir, { tier: "apps/lxd" });
|
|
190
265
|
const config = readConfig(dir);
|
|
191
266
|
expect(config.tier).toBe("apps/lxd");
|
|
192
267
|
expect(config.channel).toBe("normal");
|
|
193
268
|
});
|
|
194
269
|
|
|
195
270
|
it("creates a new config file with channel", () => {
|
|
196
|
-
|
|
271
|
+
writeConfigLocal(dir, { channel: "experimental" });
|
|
197
272
|
const config = readConfig(dir);
|
|
198
273
|
expect(config.channel).toBe("experimental");
|
|
199
274
|
expect(config.tier).toBeUndefined();
|
|
@@ -201,7 +276,7 @@ describe("writeConfig", () => {
|
|
|
201
276
|
|
|
202
277
|
it("merges tier into existing config", () => {
|
|
203
278
|
writeFileSync(join(dir, "pragma.config.json"), '{"channel":"prerelease"}');
|
|
204
|
-
|
|
279
|
+
writeConfigLocal(dir, { tier: "global" });
|
|
205
280
|
const config = readConfig(dir);
|
|
206
281
|
expect(config.tier).toBe("global");
|
|
207
282
|
expect(config.channel).toBe("prerelease");
|
|
@@ -212,7 +287,7 @@ describe("writeConfig", () => {
|
|
|
212
287
|
join(dir, "pragma.config.json"),
|
|
213
288
|
'{"tier":"apps","channel":"normal"}',
|
|
214
289
|
);
|
|
215
|
-
|
|
290
|
+
writeConfigLocal(dir, { tier: undefined });
|
|
216
291
|
const config = readConfig(dir);
|
|
217
292
|
expect(config.tier).toBeUndefined();
|
|
218
293
|
expect(config.channel).toBe("normal");
|
|
@@ -223,7 +298,7 @@ describe("writeConfig", () => {
|
|
|
223
298
|
join(dir, "pragma.config.json"),
|
|
224
299
|
'{"tier":"apps","channel":"experimental"}',
|
|
225
300
|
);
|
|
226
|
-
|
|
301
|
+
writeConfigLocal(dir, { channel: undefined });
|
|
227
302
|
const config = readConfig(dir);
|
|
228
303
|
expect(config.tier).toBe("apps");
|
|
229
304
|
// Channel defaults to "normal" when absent from file
|
|
@@ -231,7 +306,7 @@ describe("writeConfig", () => {
|
|
|
231
306
|
});
|
|
232
307
|
|
|
233
308
|
it("round-trips tier and channel through write then read", () => {
|
|
234
|
-
|
|
309
|
+
writeConfigLocal(dir, { tier: "apps/lxd", channel: "prerelease" });
|
|
235
310
|
const config = readConfig(dir);
|
|
236
311
|
expect(config.tier).toBe("apps/lxd");
|
|
237
312
|
expect(config.channel).toBe("prerelease");
|
|
@@ -239,19 +314,19 @@ describe("writeConfig", () => {
|
|
|
239
314
|
|
|
240
315
|
it("writes valid empty JSON when all fields removed", () => {
|
|
241
316
|
writeFileSync(join(dir, "pragma.config.json"), '{"tier":"apps"}');
|
|
242
|
-
|
|
317
|
+
writeConfigLocal(dir, { tier: undefined });
|
|
243
318
|
const raw = readFileSync(join(dir, "pragma.config.json"), "utf-8");
|
|
244
319
|
expect(raw).toBe("{}\n");
|
|
245
320
|
});
|
|
246
321
|
|
|
247
322
|
it("throws on unparseable existing config instead of silently overwriting", () => {
|
|
248
323
|
writeFileSync(join(dir, "pragma.config.json"), "{invalid json");
|
|
249
|
-
expect(() =>
|
|
324
|
+
expect(() => writeConfigLocal(dir, { tier: "apps" })).toThrow();
|
|
250
325
|
});
|
|
251
326
|
|
|
252
327
|
describe("packages field", () => {
|
|
253
328
|
it("writes packages array", () => {
|
|
254
|
-
|
|
329
|
+
writeConfigLocal(dir, {
|
|
255
330
|
packages: [
|
|
256
331
|
"@canonical/design-system",
|
|
257
332
|
{
|
|
@@ -272,7 +347,7 @@ describe("writeConfig", () => {
|
|
|
272
347
|
|
|
273
348
|
it("merges packages into existing config without touching tier", () => {
|
|
274
349
|
writeFileSync(join(dir, "pragma.config.json"), '{"tier":"global"}');
|
|
275
|
-
|
|
350
|
+
writeConfigLocal(dir, { packages: ["@canonical/design-system"] });
|
|
276
351
|
const config = readConfig(dir);
|
|
277
352
|
expect(config.tier).toBe("global");
|
|
278
353
|
expect(config.packages).toEqual(["@canonical/design-system"]);
|
|
@@ -283,7 +358,7 @@ describe("writeConfig", () => {
|
|
|
283
358
|
join(dir, "pragma.config.json"),
|
|
284
359
|
JSON.stringify({ packages: ["@canonical/design-system"] }),
|
|
285
360
|
);
|
|
286
|
-
|
|
361
|
+
writeConfigLocal(dir, { packages: undefined });
|
|
287
362
|
const config = readConfig(dir);
|
|
288
363
|
expect(config.packages).toBeUndefined();
|
|
289
364
|
});
|
|
@@ -296,9 +371,25 @@ describe("writeConfig", () => {
|
|
|
296
371
|
source: "git+https://github.com/canonical/anatomy-dsl.git#main",
|
|
297
372
|
},
|
|
298
373
|
];
|
|
299
|
-
|
|
374
|
+
writeConfigLocal(dir, { packages });
|
|
300
375
|
const config = readConfig(dir);
|
|
301
376
|
expect(config.packages).toEqual(packages);
|
|
302
377
|
});
|
|
303
378
|
});
|
|
304
379
|
});
|
|
380
|
+
|
|
381
|
+
describe("readConfig — empty packages normalization", () => {
|
|
382
|
+
it("treats an explicitly empty packages list as not configured", () => {
|
|
383
|
+
const dir = mkdtempSync(join(tmpdir(), "pragma-empty-pkgs-"));
|
|
384
|
+
mkdirSync(join(dir, ".git"), { recursive: true });
|
|
385
|
+
writeFileSync(
|
|
386
|
+
join(dir, "pragma.config.json"),
|
|
387
|
+
JSON.stringify({ channel: "normal", packages: [] }),
|
|
388
|
+
);
|
|
389
|
+
const config = readConfig(dir);
|
|
390
|
+
// The field is dropped at parse time so provenance matches the ref
|
|
391
|
+
// resolution behavior (empty list falls back to defaults/global refs).
|
|
392
|
+
expect(config.packages).toBeUndefined();
|
|
393
|
+
rmSync(dir, { recursive: true, force: true });
|
|
394
|
+
});
|
|
395
|
+
});
|
package/src/config/readConfig.ts
CHANGED
|
@@ -1,162 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
type Channel,
|
|
4
|
-
type Framework,
|
|
5
|
-
VALID_CHANNELS,
|
|
6
|
-
VALID_FRAMEWORKS,
|
|
7
|
-
} from "../constants.js";
|
|
8
|
-
import {
|
|
9
|
-
parsePackageEntry,
|
|
10
|
-
type RawPackageEntry,
|
|
11
|
-
} from "../domains/refs/operations/parseRef.js";
|
|
12
|
-
import { PragmaError } from "../error/PragmaError.js";
|
|
13
|
-
import resolveConfigPath from "./resolveConfigPath.js";
|
|
1
|
+
import readConfigLayers from "./readConfigLayers.js";
|
|
14
2
|
import type { PragmaConfig } from "./types.js";
|
|
15
3
|
|
|
16
4
|
/**
|
|
17
|
-
*
|
|
5
|
+
* Read the effective pragma configuration for a directory.
|
|
18
6
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Type guard: check whether a value is a recognized framework string.
|
|
28
|
-
*
|
|
29
|
-
* @param value - Candidate value.
|
|
30
|
-
* @returns `true` if value is a valid Framework.
|
|
31
|
-
*/
|
|
32
|
-
function isValidFramework(value: unknown): value is Framework {
|
|
33
|
-
return (
|
|
34
|
-
typeof value === "string" && VALID_FRAMEWORKS.includes(value as Framework)
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Read pragma config from `pragma.config.json` in the given directory.
|
|
7
|
+
* Layered resolution: built-in defaults, then the global XDG file
|
|
8
|
+
* (`~/.config/pragma/config.json`), then the nearest project
|
|
9
|
+
* `pragma.config.json` at or above `cwd` — each field won by the most
|
|
10
|
+
* specific layer that sets it. Returns defaults (no tier, `"normal"`
|
|
11
|
+
* channel) when no layer configures anything.
|
|
40
12
|
*
|
|
41
|
-
*
|
|
13
|
+
* @param cwd - Directory the project layer is resolved from (defaults to `process.cwd()`).
|
|
14
|
+
* @returns The effective merged configuration.
|
|
15
|
+
* @throws PragmaError with code `CONFIG_ERROR` if a layer file has invalid JSON or values.
|
|
42
16
|
*
|
|
43
|
-
* @
|
|
44
|
-
* @returns Parsed configuration.
|
|
45
|
-
* @throws PragmaError with code `CONFIG_ERROR` if JSON is invalid or channel is unrecognized.
|
|
46
|
-
*
|
|
47
|
-
* @note Impure — reads config from filesystem.
|
|
17
|
+
* @note Impure — reads config files from the filesystem.
|
|
48
18
|
*/
|
|
49
19
|
export default function readConfig(cwd: string = process.cwd()): PragmaConfig {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
let raw: string;
|
|
53
|
-
try {
|
|
54
|
-
raw = readFileSync(configPath, "utf-8");
|
|
55
|
-
} catch {
|
|
56
|
-
return { tier: undefined, channel: "normal" };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const trimmed = raw.trim();
|
|
60
|
-
if (trimmed === "") {
|
|
61
|
-
return { tier: undefined, channel: "normal" };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let parsed: Record<string, unknown>;
|
|
65
|
-
try {
|
|
66
|
-
parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
67
|
-
} catch {
|
|
68
|
-
throw PragmaError.configError(`Invalid JSON in ${configPath}.`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const tier = typeof parsed.tier === "string" ? parsed.tier : undefined;
|
|
72
|
-
|
|
73
|
-
let channel: Channel = "normal";
|
|
74
|
-
if (parsed.channel !== undefined) {
|
|
75
|
-
if (!isValidChannel(parsed.channel)) {
|
|
76
|
-
throw PragmaError.configError(
|
|
77
|
-
`Invalid channel "${String(parsed.channel)}".`,
|
|
78
|
-
{ validOptions: [...VALID_CHANNELS] },
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
channel = parsed.channel;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const trace = typeof parsed.trace === "boolean" ? parsed.trace : undefined;
|
|
85
|
-
|
|
86
|
-
let framework: Framework | undefined;
|
|
87
|
-
if (parsed.framework !== undefined) {
|
|
88
|
-
if (!isValidFramework(parsed.framework)) {
|
|
89
|
-
throw PragmaError.configError(
|
|
90
|
-
`Invalid framework "${String(parsed.framework)}".`,
|
|
91
|
-
{ validOptions: [...VALID_FRAMEWORKS] },
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
framework = parsed.framework;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const packages = parsePackagesField(parsed.packages);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
tier,
|
|
101
|
-
channel,
|
|
102
|
-
...(packages ? { packages } : {}),
|
|
103
|
-
...(trace !== undefined ? { trace } : {}),
|
|
104
|
-
...(framework !== undefined ? { framework } : {}),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// Packages field parsing
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Validate and parse the `packages` config field.
|
|
114
|
-
*
|
|
115
|
-
* @returns The validated array, or `undefined` when the field is absent.
|
|
116
|
-
* @throws PragmaError if the field is present but malformed.
|
|
117
|
-
*/
|
|
118
|
-
function parsePackagesField(
|
|
119
|
-
raw: unknown,
|
|
120
|
-
): ReadonlyArray<RawPackageEntry> | undefined {
|
|
121
|
-
if (raw === undefined || raw === null) return undefined;
|
|
122
|
-
|
|
123
|
-
if (!Array.isArray(raw)) {
|
|
124
|
-
throw PragmaError.configError(
|
|
125
|
-
'"packages" must be an array of strings or { name, source? } objects.',
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const entries: RawPackageEntry[] = [];
|
|
130
|
-
for (const item of raw) {
|
|
131
|
-
if (typeof item === "string") {
|
|
132
|
-
entries.push(item);
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
136
|
-
const obj = item as Record<string, unknown>;
|
|
137
|
-
if (typeof obj.name !== "string" || obj.name.length === 0) {
|
|
138
|
-
throw PragmaError.configError(
|
|
139
|
-
'Each object in "packages" must have a non-empty "name" string.',
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
const entry: { name: string; source?: string } = { name: obj.name };
|
|
143
|
-
if (obj.source !== undefined) {
|
|
144
|
-
if (typeof obj.source !== "string") {
|
|
145
|
-
throw PragmaError.configError(
|
|
146
|
-
`Invalid source for "${obj.name}": expected a string.`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
entry.source = obj.source;
|
|
150
|
-
}
|
|
151
|
-
// Validate format eagerly — fail fast on bad config
|
|
152
|
-
parsePackageEntry(entry);
|
|
153
|
-
entries.push(entry);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
throw PragmaError.configError(
|
|
157
|
-
'Each entry in "packages" must be a string or { name, source? } object.',
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return entries;
|
|
20
|
+
return readConfigLayers(cwd).config;
|
|
162
21
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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 { PragmaError } from "../error/PragmaError.js";
|
|
6
|
+
import readConfigLayers from "./readConfigLayers.js";
|
|
7
|
+
|
|
8
|
+
describe("readConfigLayers", () => {
|
|
9
|
+
let xdgDir: string;
|
|
10
|
+
let projectDir: string;
|
|
11
|
+
let originalXdg: string | undefined;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
originalXdg = process.env.XDG_CONFIG_HOME;
|
|
15
|
+
xdgDir = mkdtempSync(join(tmpdir(), "pragma-layers-xdg-"));
|
|
16
|
+
process.env.XDG_CONFIG_HOME = xdgDir;
|
|
17
|
+
projectDir = mkdtempSync(join(tmpdir(), "pragma-layers-project-"));
|
|
18
|
+
mkdirSync(join(projectDir, ".git"));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
process.env.XDG_CONFIG_HOME = originalXdg;
|
|
23
|
+
rmSync(xdgDir, { recursive: true, force: true });
|
|
24
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function writeGlobal(content: string): void {
|
|
28
|
+
mkdirSync(join(xdgDir, "pragma"), { recursive: true });
|
|
29
|
+
writeFileSync(join(xdgDir, "pragma/config.json"), content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeProject(content: string): void {
|
|
33
|
+
writeFileSync(join(projectDir, "pragma.config.json"), content);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it("returns defaults with default origins when no layer exists", () => {
|
|
37
|
+
const layers = readConfigLayers(projectDir);
|
|
38
|
+
expect(layers.config).toEqual({ tier: undefined, channel: "normal" });
|
|
39
|
+
expect(layers.origins).toEqual({
|
|
40
|
+
tier: "default",
|
|
41
|
+
channel: "default",
|
|
42
|
+
packages: "default",
|
|
43
|
+
trace: "default",
|
|
44
|
+
framework: "default",
|
|
45
|
+
stories: "default",
|
|
46
|
+
prefixes: "default",
|
|
47
|
+
detail: "default",
|
|
48
|
+
});
|
|
49
|
+
expect(layers.global.exists).toBe(false);
|
|
50
|
+
expect(layers.project.exists).toBe(false);
|
|
51
|
+
expect(layers.project.path).toBe(join(projectDir, "pragma.config.json"));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("reads the global layer when no project file exists", () => {
|
|
55
|
+
writeGlobal('{"channel":"experimental","tier":"global"}');
|
|
56
|
+
const layers = readConfigLayers(projectDir);
|
|
57
|
+
expect(layers.config.channel).toBe("experimental");
|
|
58
|
+
expect(layers.config.tier).toBe("global");
|
|
59
|
+
expect(layers.origins.channel).toBe("global");
|
|
60
|
+
expect(layers.origins.tier).toBe("global");
|
|
61
|
+
expect(layers.global.exists).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("merges per field — project wins only the fields it sets", () => {
|
|
65
|
+
writeGlobal('{"channel":"experimental","trace":true}');
|
|
66
|
+
writeProject('{"tier":"apps/lxd"}');
|
|
67
|
+
const layers = readConfigLayers(projectDir);
|
|
68
|
+
expect(layers.config).toEqual({
|
|
69
|
+
tier: "apps/lxd",
|
|
70
|
+
channel: "experimental",
|
|
71
|
+
trace: true,
|
|
72
|
+
});
|
|
73
|
+
expect(layers.origins).toEqual({
|
|
74
|
+
tier: "project",
|
|
75
|
+
channel: "global",
|
|
76
|
+
packages: "default",
|
|
77
|
+
trace: "global",
|
|
78
|
+
framework: "default",
|
|
79
|
+
stories: "default",
|
|
80
|
+
prefixes: "default",
|
|
81
|
+
detail: "default",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("lets the project override a global field wholesale", () => {
|
|
86
|
+
writeGlobal('{"packages":["@canonical/a"],"channel":"experimental"}');
|
|
87
|
+
writeProject('{"packages":["@canonical/b"],"channel":"normal"}');
|
|
88
|
+
const layers = readConfigLayers(projectDir);
|
|
89
|
+
expect(layers.config.packages).toEqual(["@canonical/b"]);
|
|
90
|
+
expect(layers.config.channel).toBe("normal");
|
|
91
|
+
expect(layers.origins.packages).toBe("project");
|
|
92
|
+
expect(layers.origins.channel).toBe("project");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("finds the project layer from a subdirectory", () => {
|
|
96
|
+
writeProject('{"tier":"apps/lxd"}');
|
|
97
|
+
const sub = join(projectDir, "packages/app");
|
|
98
|
+
mkdirSync(sub, { recursive: true });
|
|
99
|
+
const layers = readConfigLayers(sub);
|
|
100
|
+
expect(layers.config.tier).toBe("apps/lxd");
|
|
101
|
+
expect(layers.project.path).toBe(join(projectDir, "pragma.config.json"));
|
|
102
|
+
expect(layers.project.exists).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("throws on invalid JSON in the global layer", () => {
|
|
106
|
+
writeGlobal("{not json");
|
|
107
|
+
expect(() => readConfigLayers(projectDir)).toThrow(PragmaError);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { DEFAULT_CONFIG } from "./defaultConfig.js";
|
|
3
|
+
import findProjectConfigPath from "./findProjectConfigPath.js";
|
|
4
|
+
import parseConfigValues from "./parseConfigValues.js";
|
|
5
|
+
import resolveConfigPath from "./resolveConfigPath.js";
|
|
6
|
+
import resolveGlobalConfigPath from "./resolveGlobalConfigPath.js";
|
|
7
|
+
import type {
|
|
8
|
+
ConfigFileValues,
|
|
9
|
+
ConfigLayers,
|
|
10
|
+
ConfigOrigin,
|
|
11
|
+
PragmaConfig,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fields the built-in defaults document contributes to the layer merge.
|
|
16
|
+
*
|
|
17
|
+
* `packages` is deliberately absent: it reaches the ref merge through
|
|
18
|
+
* `DEFAULT_PACKAGES` instead, so global `refs.json` entries keep merging
|
|
19
|
+
* over the default packages by name. Surfacing it here would make the
|
|
20
|
+
* defaults look user-declared and replace the merged set wholesale.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_LAYER: ConfigFileValues = {
|
|
23
|
+
channel: DEFAULT_CONFIG.channel,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the layered configuration with per-field provenance.
|
|
28
|
+
*
|
|
29
|
+
* Layer order, least to most specific: built-in defaults, the global XDG
|
|
30
|
+
* file (`~/.config/pragma/config.json`), and the nearest project
|
|
31
|
+
* `pragma.config.json` up the tree. Merging is per field — a field set by
|
|
32
|
+
* a more specific layer wins wholesale (`packages` replaces as a whole,
|
|
33
|
+
* preserving the established project-pins-exactly semantics).
|
|
34
|
+
*
|
|
35
|
+
* @param cwd - Directory the project layer is resolved from.
|
|
36
|
+
* @returns The effective config, per-field origins, and both layers.
|
|
37
|
+
* @throws PragmaError with code `CONFIG_ERROR` when either file is invalid.
|
|
38
|
+
* @note Impure — reads config files from the filesystem.
|
|
39
|
+
*/
|
|
40
|
+
export default function readConfigLayers(
|
|
41
|
+
cwd: string = process.cwd(),
|
|
42
|
+
): ConfigLayers {
|
|
43
|
+
const globalPath = resolveGlobalConfigPath();
|
|
44
|
+
const projectPath = findProjectConfigPath(cwd) ?? resolveConfigPath(cwd);
|
|
45
|
+
|
|
46
|
+
const globalFile = readConfigFile(globalPath);
|
|
47
|
+
const projectFile = readConfigFile(projectPath);
|
|
48
|
+
|
|
49
|
+
const pick = <K extends keyof ConfigFileValues>(
|
|
50
|
+
field: K,
|
|
51
|
+
): { value: ConfigFileValues[K] | undefined; origin: ConfigOrigin } => {
|
|
52
|
+
if (field in projectFile.values) {
|
|
53
|
+
return { value: projectFile.values[field], origin: "project" };
|
|
54
|
+
}
|
|
55
|
+
if (field in globalFile.values) {
|
|
56
|
+
return { value: globalFile.values[field], origin: "global" };
|
|
57
|
+
}
|
|
58
|
+
if (field in DEFAULT_LAYER) {
|
|
59
|
+
return { value: DEFAULT_LAYER[field], origin: "default" };
|
|
60
|
+
}
|
|
61
|
+
return { value: undefined, origin: "default" };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const tier = pick("tier");
|
|
65
|
+
const channel = pick("channel");
|
|
66
|
+
const packages = pick("packages");
|
|
67
|
+
const trace = pick("trace");
|
|
68
|
+
const framework = pick("framework");
|
|
69
|
+
const stories = pick("stories");
|
|
70
|
+
const prefixes = pick("prefixes");
|
|
71
|
+
const detail = pick("detail");
|
|
72
|
+
|
|
73
|
+
const config: PragmaConfig = {
|
|
74
|
+
tier: tier.value,
|
|
75
|
+
channel: channel.value ?? DEFAULT_CONFIG.channel,
|
|
76
|
+
...(packages.value ? { packages: packages.value } : {}),
|
|
77
|
+
...(trace.value !== undefined ? { trace: trace.value } : {}),
|
|
78
|
+
...(framework.value !== undefined ? { framework: framework.value } : {}),
|
|
79
|
+
...(stories.value ? { stories: stories.value } : {}),
|
|
80
|
+
...(prefixes.value ? { prefixes: prefixes.value } : {}),
|
|
81
|
+
...(detail.value !== undefined ? { detail: detail.value } : {}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
config,
|
|
86
|
+
origins: {
|
|
87
|
+
tier: tier.origin,
|
|
88
|
+
channel: channel.origin,
|
|
89
|
+
packages: packages.origin,
|
|
90
|
+
trace: trace.origin,
|
|
91
|
+
framework: framework.origin,
|
|
92
|
+
stories: stories.origin,
|
|
93
|
+
prefixes: prefixes.origin,
|
|
94
|
+
detail: detail.origin,
|
|
95
|
+
},
|
|
96
|
+
global: { path: globalPath, exists: globalFile.exists },
|
|
97
|
+
project: { path: projectPath, exists: projectFile.exists },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Read and parse one config file layer.
|
|
103
|
+
*
|
|
104
|
+
* A missing or unreadable file contributes no values — the layer simply
|
|
105
|
+
* does not participate in the merge.
|
|
106
|
+
*/
|
|
107
|
+
function readConfigFile(path: string): {
|
|
108
|
+
values: ConfigFileValues;
|
|
109
|
+
exists: boolean;
|
|
110
|
+
} {
|
|
111
|
+
let raw: string;
|
|
112
|
+
try {
|
|
113
|
+
raw = readFileSync(path, "utf-8");
|
|
114
|
+
} catch {
|
|
115
|
+
return { values: {}, exists: false };
|
|
116
|
+
}
|
|
117
|
+
return { values: parseConfigValues(raw, path), exists: true };
|
|
118
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { globalConfigDir } from "../domains/refs/operations/paths.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the absolute path of the global config file.
|
|
6
|
+
*
|
|
7
|
+
* Lives next to `refs.json` in the pragma XDG config directory:
|
|
8
|
+
* `$XDG_CONFIG_HOME/pragma/config.json`, defaulting to
|
|
9
|
+
* `~/.config/pragma/config.json`.
|
|
10
|
+
*/
|
|
11
|
+
export default function resolveGlobalConfigPath(): string {
|
|
12
|
+
return join(globalConfigDir(), "config.json");
|
|
13
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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 resolveWriteConfigPath from "./resolveWriteConfigPath.js";
|
|
6
|
+
|
|
7
|
+
describe("resolveWriteConfigPath", () => {
|
|
8
|
+
let xdgDir: string;
|
|
9
|
+
let projectDir: string;
|
|
10
|
+
let originalXdg: string | undefined;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
originalXdg = process.env.XDG_CONFIG_HOME;
|
|
14
|
+
xdgDir = mkdtempSync(join(tmpdir(), "pragma-write-xdg-"));
|
|
15
|
+
process.env.XDG_CONFIG_HOME = xdgDir;
|
|
16
|
+
projectDir = mkdtempSync(join(tmpdir(), "pragma-write-project-"));
|
|
17
|
+
mkdirSync(join(projectDir, ".git"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.env.XDG_CONFIG_HOME = originalXdg;
|
|
22
|
+
rmSync(xdgDir, { recursive: true, force: true });
|
|
23
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("targets the nearest existing project file", () => {
|
|
27
|
+
writeFileSync(join(projectDir, "pragma.config.json"), "{}");
|
|
28
|
+
const sub = join(projectDir, "packages/app");
|
|
29
|
+
mkdirSync(sub, { recursive: true });
|
|
30
|
+
expect(resolveWriteConfigPath(sub)).toBe(
|
|
31
|
+
join(projectDir, "pragma.config.json"),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("falls back to the global file when no project file exists", () => {
|
|
36
|
+
expect(resolveWriteConfigPath(projectDir)).toBe(
|
|
37
|
+
join(xdgDir, "pragma/config.json"),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("honors an explicit local scope even without an existing file", () => {
|
|
42
|
+
expect(resolveWriteConfigPath(projectDir, "local")).toBe(
|
|
43
|
+
join(projectDir, "pragma.config.json"),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("honors an explicit global scope over an existing project file", () => {
|
|
48
|
+
writeFileSync(join(projectDir, "pragma.config.json"), "{}");
|
|
49
|
+
expect(resolveWriteConfigPath(projectDir, "global")).toBe(
|
|
50
|
+
join(xdgDir, "pragma/config.json"),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|