@canonical/pragma-cli 0.33.0 → 0.34.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 +7 -7
- package/dist/pragma +0 -0
- package/package.json +18 -15
- package/pragma.conf.ts +664 -0
- package/src/bin.ts +13 -4
- package/src/capabilities/block.parity.test.ts +250 -0
- package/src/capabilities/capabilities/capabilities.render.ts +3 -2
- package/src/capabilities/capabilities/capabilities.verb.ts +31 -13
- package/src/capabilities/capabilities/catalog.ts +18 -10
- package/src/capabilities/capabilities/hints.ts +13 -19
- package/src/capabilities/colophon/collectColophon.ts +34 -19
- package/src/capabilities/colophon/colophon.verb.ts +8 -4
- package/src/capabilities/colophon/types.ts +16 -7
- package/src/capabilities/config/collectConfigShow.test.ts +57 -0
- package/src/capabilities/config/collectConfigShow.ts +41 -4
- package/src/capabilities/config/fields.ts +0 -27
- package/src/capabilities/config/set.verb.ts +7 -3
- package/src/capabilities/config/show.render.test.ts +77 -5
- package/src/capabilities/config/show.render.ts +7 -12
- package/src/capabilities/config/show.verb.ts +4 -3
- package/src/capabilities/config/types.ts +8 -0
- package/src/capabilities/create/byteEquality.test.ts +78 -100
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
- package/src/capabilities/create/constants.ts +75 -0
- package/src/capabilities/create/create.mcp.test.ts +2 -2
- package/src/capabilities/create/create.test.ts +162 -15
- package/src/capabilities/create/create.verb.ts +140 -69
- package/src/capabilities/create/index.ts +2 -10
- package/src/capabilities/create/pickGenerator.ts +34 -28
- package/src/capabilities/create/templates.embedded.generated.ts +4 -29
- package/src/capabilities/create/types.ts +10 -6
- package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
- package/src/capabilities/distribution.test.ts +120 -0
- package/src/capabilities/distribution.ts +59 -0
- package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
- package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
- package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
- package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
- package/src/capabilities/doctor/doctor.render.test.ts +6 -6
- package/src/capabilities/doctor/doctor.render.ts +2 -1
- package/src/capabilities/doctor/doctor.test.ts +37 -8
- package/src/capabilities/doctor/doctor.verb.ts +6 -2
- package/src/capabilities/doctor/runChecks.ts +5 -5
- package/src/capabilities/doctor/types.ts +1 -1
- package/src/capabilities/emptyRecovery.test.ts +32 -15
- package/src/capabilities/graph/inspect.verb.ts +3 -2
- package/src/capabilities/graph/query.verb.ts +3 -2
- package/src/capabilities/graph/runQuery.ts +8 -5
- package/src/capabilities/index.ts +22 -18
- package/src/capabilities/info/collectInfo.ts +10 -6
- package/src/capabilities/info/info.render.ts +3 -2
- package/src/capabilities/info/info.verb.ts +3 -2
- package/src/capabilities/lazy.test.ts +93 -2
- package/src/capabilities/ontology/verbs.ts +21 -21
- package/src/capabilities/prompt/prompt.test.ts +12 -6
- package/src/capabilities/prompt/verbs.ts +24 -11
- package/src/capabilities/resources/provider.ts +30 -11
- package/src/capabilities/resources/resources.test.ts +85 -29
- package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
- package/src/capabilities/setup/operations/setupMcp.ts +2 -2
- package/src/capabilities/setup/operations/setupSkills.ts +3 -3
- package/src/capabilities/setup/setup.verb.ts +12 -5
- package/src/capabilities/setup/shell.ts +29 -3
- package/src/capabilities/skill/discover.ts +11 -5
- package/src/capabilities/skill/render.ts +4 -5
- package/src/capabilities/skill/verbs.ts +7 -7
- package/src/capabilities/sources/collectStatus.ts +55 -50
- package/src/capabilities/sources/index.ts +1 -1
- package/src/capabilities/sources/installSkills.ts +2 -2
- package/src/capabilities/sources/runUpdate.ts +113 -122
- package/src/capabilities/sources/sources.test.ts +111 -117
- package/src/capabilities/sources/status.render.test.ts +53 -24
- package/src/capabilities/sources/status.render.ts +24 -29
- package/src/capabilities/sources/status.verb.ts +15 -8
- package/src/capabilities/sources/types.ts +18 -25
- package/src/capabilities/sources/update.render.ts +9 -2
- package/src/capabilities/sources/update.verb.ts +12 -23
- package/src/capabilities/surface.test.ts +33 -10
- package/src/capabilities/tier.parity.test.ts +144 -0
- package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
- package/src/constants.test.ts +8 -6
- package/src/constants.ts +47 -21
- package/src/identity.test.ts +333 -0
- package/src/index.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
- package/src/kernel/completion/emitScripts.test.ts +1 -1
- package/src/kernel/completion/emitScripts.ts +4 -2
- package/src/kernel/completion/entitySource.test.ts +185 -41
- package/src/kernel/completion/entitySource.ts +117 -107
- package/src/kernel/completion/model.test.ts +1 -1
- package/src/kernel/completion/model.ts +11 -0
- package/src/kernel/completion/safety.test.ts +82 -36
- package/src/kernel/completion/shellDrive.test.ts +592 -0
- package/src/kernel/completion/templates/fish.ts +28 -6
- package/src/kernel/completion/types.ts +3 -3
- package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
- package/src/kernel/config/defaults.test.ts +85 -0
- package/src/kernel/config/defaults.ts +12 -23
- package/src/kernel/config/evaluateProjectConfig.ts +2 -3
- package/src/kernel/config/findProjectConfig.ts +4 -3
- package/src/kernel/config/firstRun.ts +12 -5
- package/src/kernel/config/globalConfig.ts +2 -2
- package/src/kernel/config/index.ts +4 -5
- package/src/kernel/config/paths.ts +17 -17
- package/src/kernel/config/readConfig.test.ts +242 -6
- package/src/kernel/config/readConfig.ts +8 -8
- package/src/kernel/config/schema.test.ts +50 -0
- package/src/kernel/config/schema.ts +97 -10
- package/src/kernel/config/types.ts +73 -19
- package/src/kernel/copy.test.ts +453 -0
- package/src/kernel/error/PragmaError.test.ts +1 -1
- package/src/kernel/error/constants.ts +3 -2
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +22 -45
- package/src/kernel/error/recovery.ts +22 -27
- package/src/kernel/error/types.ts +5 -4
- package/src/kernel/packs/collect.test.ts +195 -11
- package/src/kernel/packs/collect.ts +213 -58
- package/src/kernel/packs/compile.test.ts +163 -7
- package/src/kernel/packs/compile.ts +15 -12
- package/src/kernel/packs/disclosure.test.ts +5 -2
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
- package/src/kernel/packs/graphqlSource.test.ts +6 -3
- package/src/kernel/packs/iri.ts +5 -7
- package/src/kernel/packs/renderPack.ts +5 -3
- package/src/kernel/packs/resolveEntity.ts +21 -17
- package/src/kernel/packs/runBodies.ts +15 -14
- package/src/kernel/packs/schema.ts +96 -10
- package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
- package/src/kernel/packs/sparql/runSelect.ts +73 -34
- package/src/kernel/packs/toolDescription.test.ts +10 -2
- package/src/kernel/packs/types.ts +39 -4
- package/src/kernel/packs/uniqueness.ts +4 -4
- package/src/kernel/project/cli/buildProgram.test.ts +9 -4
- package/src/kernel/project/cli/dispatch.test.ts +67 -0
- package/src/kernel/project/cli/dispatch.ts +34 -12
- package/src/kernel/project/cli/helpFormat.ts +1 -1
- package/src/kernel/project/cli/rootHelp.ts +105 -105
- package/src/kernel/project/mcp/buildServer.ts +11 -6
- package/src/kernel/project/mcp/instructions.ts +11 -6
- package/src/kernel/project/mcp/parity.test.ts +39 -1
- package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
- package/src/kernel/project/mcp/prompts/provider.ts +22 -8
- package/src/kernel/project/mcp/prompts/source.ts +68 -67
- package/src/kernel/project/mcp/registerVerb.ts +25 -6
- package/src/kernel/project/mcp/serve.ts +11 -3
- package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
- package/src/kernel/render/disclosure.ts +4 -2
- package/src/kernel/render/prefixes.test.ts +36 -0
- package/src/kernel/render/prefixes.ts +40 -13
- package/src/kernel/render/renderers.test.ts +1 -1
- package/src/kernel/runtime/graphpack/build.ts +29 -3
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
- package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +37 -11
- package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
- package/src/kernel/runtime/graphpack/hash.ts +3 -2
- package/src/kernel/runtime/graphpack/index.ts +1 -1
- package/src/kernel/runtime/graphpack/manifest.ts +114 -12
- package/src/kernel/runtime/graphpack/read.ts +4 -9
- package/src/kernel/runtime/graphpack/schemas.ts +74 -0
- package/src/kernel/runtime/graphpack/stories.ts +84 -0
- package/src/kernel/runtime/graphpack/types.ts +61 -48
- package/src/kernel/runtime/paths.ts +64 -15
- package/src/kernel/runtime/probe.test.ts +8 -4
- package/src/kernel/runtime/readEntity.ts +6 -5
- package/src/kernel/runtime/refs/gitOps.ts +1 -19
- package/src/kernel/runtime/refs/parseRef.ts +7 -7
- package/src/kernel/runtime/refs/resolve.ts +78 -62
- package/src/kernel/runtime/resolveSources.test.ts +63 -24
- package/src/kernel/runtime/resolveSources.ts +36 -25
- package/src/kernel/runtime/storeReadiness.ts +9 -8
- package/src/kernel/runtime/storeless.test.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
- package/src/kernel/spec/emitReference.test.ts +3 -3
- package/src/kernel/spec/emitReference.ts +144 -11
- package/src/kernel/spec/emitSurface.ts +9 -5
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/surfaceConformance.ts +8 -0
- package/src/kernel/spec/types.ts +29 -9
- package/src/kernel/spec/validate.ts +2 -1
- package/src/kernel/vocabulary.test.ts +164 -0
- package/src/kernel/vocabulary.ts +146 -0
- package/src/testing/PARITY_GAPS.ts +8 -2
- package/src/testing/behavioral/README.md +3 -3
- package/src/testing/behavioral/completion.test.ts +19 -6
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
- package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
- package/src/testing/behavioral/harness.foundation.test.ts +14 -5
- package/src/testing/behavioral/journeys.cli.test.ts +116 -75
- package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
- package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
- package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
- package/src/testing/behavioral/liveReadSurface.ts +4 -3
- package/src/testing/behavioral/mcpSurface.test.ts +9 -4
- package/src/testing/behavioral/staleCommands.test.ts +10 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
- package/src/testing/eval/cases/assembly.ts +13 -6
- package/src/testing/eval/cases/effectful.ts +25 -13
- package/src/testing/eval/cases/readNouns.ts +22 -20
- package/src/testing/eval/cases/stable.ts +5 -1
- package/src/testing/eval/eval.test.ts +7 -1
- package/src/testing/fixtures/fixtureCapability.ts +45 -1
- package/src/testing/fixtures/graph/canonical.ts +6 -3
- package/src/testing/fixtures/graph/defaultPack.ts +6 -5
- package/src/testing/fixtures/graph/owlExport.ts +7 -5
- package/src/testing/helpers/fixtureGraph.ts +33 -16
- package/src/testing/helpers/packRuntime.ts +2 -3
- package/src/testing/helpers/projectCli.ts +2 -1
- package/src/testing/helpers/projectMcp.ts +6 -0
- package/src/testing/perf/budgets.test.ts +4 -0
- package/src/testing/perf/budgets.ts +22 -7
- package/src/testing/perf/globalSetup.ts +121 -6
- package/src/types.ts +2 -2
- package/src/capabilities/block/blockList.test.ts +0 -79
- package/src/capabilities/block/blockList.verb.ts +0 -163
- package/src/capabilities/block/index.ts +0 -25
- package/src/capabilities/block/pack.ts +0 -166
- package/src/capabilities/block/parity.test.ts +0 -187
- package/src/capabilities/block/tierChain.ts +0 -78
- package/src/capabilities/colophon/pragmaColophon.ts +0 -66
- package/src/capabilities/modifier/index.ts +0 -15
- package/src/capabilities/modifier/pack.ts +0 -64
- package/src/capabilities/standard/index.ts +0 -18
- package/src/capabilities/standard/pack.ts +0 -128
- package/src/capabilities/tier/index.ts +0 -19
- package/src/capabilities/tier/lookup.render.test.ts +0 -72
- package/src/capabilities/tier/lookup.render.ts +0 -59
- package/src/capabilities/tier/lookup.test.ts +0 -83
- package/src/capabilities/tier/lookup.verb.ts +0 -44
- package/src/capabilities/tier/pack.ts +0 -28
- package/src/capabilities/tier/runLookup.ts +0 -87
- package/src/capabilities/token/addConfig.render.ts +0 -27
- package/src/capabilities/token/addConfig.test.ts +0 -62
- package/src/capabilities/token/addConfig.verb.ts +0 -40
- package/src/capabilities/token/index.ts +0 -19
- package/src/capabilities/token/pack.ts +0 -58
- package/src/capabilities/token/runAddConfig.ts +0 -95
- package/src/kernel/completion/bashDrive.test.ts +0 -73
- package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
- package/src/kernel/runtime/lock.ts +0 -90
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import defaults from "./defaults.js";
|
|
3
|
+
|
|
4
|
+
describe("defaults — the validated distribution config (pragma.conf.ts)", () => {
|
|
5
|
+
it("ships the distribution identity", () => {
|
|
6
|
+
expect(defaults.name).toBe("pragma");
|
|
7
|
+
expect(defaults.help).toBe("Explore the design system");
|
|
8
|
+
expect(defaults.issuesUrl).toBe(
|
|
9
|
+
"https://github.com/canonical/pragma/issues",
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("declares the toolchain colophon as content (markdown body + summary)", () => {
|
|
14
|
+
// The narrative is a DECLARATION the `colophon` verb renders, not code:
|
|
15
|
+
// `collectColophon` reads exactly this object. Pin the shape and the two
|
|
16
|
+
// stable fragments — the story's own subject and the maker line the old
|
|
17
|
+
// one-line `colophon` string carried (folded in when the field went live).
|
|
18
|
+
expect(defaults.colophon?.markdown).toContain("domain-based toolchain");
|
|
19
|
+
expect(defaults.colophon?.markdown).toContain(
|
|
20
|
+
"Made by the Canonical Webteam — https://canonical.com.",
|
|
21
|
+
);
|
|
22
|
+
expect(defaults.colophon?.summary).toContain("domain-based toolchain");
|
|
23
|
+
// Bodies, not documents: the renderer supplies the H1 from the name.
|
|
24
|
+
expect(defaults.colophon?.markdown.startsWith("#")).toBe(false);
|
|
25
|
+
expect(defaults.colophon?.summary?.startsWith("#")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("ships the three canonical default packs (git+https sources)", () => {
|
|
29
|
+
expect(
|
|
30
|
+
defaults.packs?.map((pack) =>
|
|
31
|
+
typeof pack === "string"
|
|
32
|
+
? pack
|
|
33
|
+
: { name: pack.name, source: pack.source },
|
|
34
|
+
),
|
|
35
|
+
).toEqual([
|
|
36
|
+
{
|
|
37
|
+
name: "@canonical/design-system",
|
|
38
|
+
source: "git+https://github.com/canonical/design-system.git#main",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "@canonical/anatomy-dsl",
|
|
42
|
+
source: "git+https://github.com/canonical/anatomy-dsl.git#main",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "@canonical/code-standards",
|
|
46
|
+
source: "git+https://github.com/canonical/web-code-standards.git#main",
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("carries the declared read stories through validation, on their packs", () => {
|
|
52
|
+
// The zod layer must not strip `stories` (unknown keys ARE stripped for
|
|
53
|
+
// forward compatibility). Content is owned by `capabilities/distribution.test.ts`;
|
|
54
|
+
// this pins only that the field survives `parseRawConfig` on the right pack.
|
|
55
|
+
const storyCounts = defaults.packs?.map((pack) =>
|
|
56
|
+
typeof pack === "string" ? 0 : (pack.stories?.length ?? 0),
|
|
57
|
+
);
|
|
58
|
+
expect(storyCounts).toEqual([4, 0, 1]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("declares no removed field — the validator would refuse to load one", () => {
|
|
62
|
+
// `generators` was deleted (L-OPEN-1 ruling): validated, layered, and read
|
|
63
|
+
// by nothing. The distribution config goes through the same strict
|
|
64
|
+
// `parseRawConfig` as every layer, so a `generators:` reintroduced here
|
|
65
|
+
// would fail the eager validation at import — this pins the honest state.
|
|
66
|
+
expect(defaults).not.toHaveProperty("generators");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("declares the domain namespaces the store is built with and the CLI resolves", () => {
|
|
70
|
+
// Two consumers, one declaration. (1) The config layer wins every prefix
|
|
71
|
+
// harvest, so this is what keeps `ds:` entity names stable when the design
|
|
72
|
+
// system declares `ds:` with two IRIs. (2) It is the domain half of
|
|
73
|
+
// `DEFAULT_PREFIX_MAP`, so it also decides which prefixed names a lookup
|
|
74
|
+
// can expand — `cs:` is here for that reader.
|
|
75
|
+
expect(defaults.prefixes).toEqual({
|
|
76
|
+
ds: "https://ds.canonical.com/",
|
|
77
|
+
cs: "http://pragma.canonical.com/codestandards#",
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("ships the normal channel and standard detail level", () => {
|
|
82
|
+
expect(defaults.channel).toBe("normal");
|
|
83
|
+
expect(defaults.detail).toBe("standard");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in configuration defaults
|
|
2
|
+
* Built-in configuration defaults — the validated distribution config.
|
|
3
3
|
*
|
|
4
|
-
* The lowest config layer
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The lowest config layer is the distribution's own `pragma.conf.ts` (package
|
|
5
|
+
* root), STATICALLY imported so `bun build --compile` inlines it (no fs read —
|
|
6
|
+
* `evaluateProjectConfig` stats/imports from disk, impossible inside the
|
|
7
|
+
* compiled binary) and validated through the same `parseRawConfig` as every
|
|
8
|
+
* other layer. Statically imported by `readConfig` (itself dynamic-imported,
|
|
9
|
+
* off the `--help`/`__complete` fast path) and by its own test, so the eager
|
|
10
|
+
* validation and the zod import here add nothing to the storeless surfaces —
|
|
11
|
+
* the lazy.test.ts module-graph probe covers that boundary.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
|
-
import
|
|
14
|
+
import rawConfig from "../../../pragma.conf.js";
|
|
15
|
+
import { parseRawConfig } from "./schema.js";
|
|
10
16
|
|
|
11
|
-
export default
|
|
12
|
-
channel: "normal",
|
|
13
|
-
detail: "standard",
|
|
14
|
-
packages: [
|
|
15
|
-
{
|
|
16
|
-
name: "@canonical/design-system",
|
|
17
|
-
source: "git+https://github.com/canonical/design-system.git#main",
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: "@canonical/anatomy-dsl",
|
|
21
|
-
source: "git+https://github.com/canonical/anatomy-dsl.git#main",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "@canonical/code-standards",
|
|
25
|
-
source: "git+https://github.com/canonical/web-code-standards.git#main",
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
} satisfies PragmaConfig;
|
|
17
|
+
export default parseRawConfig(rawConfig, "pragma.conf.ts");
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "node:fs";
|
|
34
34
|
import { join } from "node:path";
|
|
35
35
|
import { pathToFileURL } from "node:url";
|
|
36
|
-
import { VERSION } from "../../constants.js";
|
|
36
|
+
import { PROJECT_CONFIG_FILENAME, VERSION } from "../../constants.js";
|
|
37
37
|
import { PragmaError } from "../error/PragmaError.js";
|
|
38
38
|
import { configCacheDir } from "./paths.js";
|
|
39
39
|
import { parseRawConfig } from "./schema.js";
|
|
@@ -107,8 +107,7 @@ export async function evaluateProjectConfig(path: string): Promise<RawConfig> {
|
|
|
107
107
|
`Could not load project config ${path}: ${reason}`,
|
|
108
108
|
{
|
|
109
109
|
recovery: {
|
|
110
|
-
message:
|
|
111
|
-
"Fix the error in your pragma.config.ts (it must evaluate to a default export), then try again.",
|
|
110
|
+
message: `Fix the error in your ${PROJECT_CONFIG_FILENAME} (it must evaluate to a default export), then try again.`,
|
|
112
111
|
},
|
|
113
112
|
},
|
|
114
113
|
);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Locate the nearest project config by walking up from `cwd`.
|
|
3
3
|
*
|
|
4
|
-
* Looks for
|
|
5
|
-
*
|
|
4
|
+
* Looks for `<bin>.config.ts` (or, as a compiled-binary fallback,
|
|
5
|
+
* `<bin>.config.js`) at or above `cwd`. The walk is bounded: it stops after
|
|
6
6
|
* the first directory containing `.git` (the repo root), after the home
|
|
7
7
|
* directory, and at the filesystem root — a config outside the repo or home
|
|
8
8
|
* never leaks in. Ported from the v1 `findProjectConfigPath`.
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import { BIN_NAME, PROJECT_CONFIG_FILENAME } from "../../constants.js";
|
|
14
15
|
|
|
15
16
|
/** Candidate project-config filenames, in preference order. */
|
|
16
|
-
const CANDIDATES = [
|
|
17
|
+
const CANDIDATES = [PROJECT_CONFIG_FILENAME, `${BIN_NAME}.config.js`] as const;
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Find the nearest project config file at or above `cwd`.
|
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
writeFile,
|
|
22
22
|
} from "@canonical/task";
|
|
23
23
|
import { runTask } from "@canonical/task/node";
|
|
24
|
+
import {
|
|
25
|
+
BIN_NAME,
|
|
26
|
+
ISSUES_URL,
|
|
27
|
+
PROJECT_CONFIG_FILENAME,
|
|
28
|
+
} from "../../constants.js";
|
|
24
29
|
import { globalConfigPath } from "./paths.js";
|
|
25
30
|
|
|
26
31
|
/** Seed content: an empty object, so nothing is pinned the user did not choose. */
|
|
@@ -29,10 +34,10 @@ const SEED_CONFIG = "{}\n";
|
|
|
29
34
|
/** Build the welcome note; the resolved path is shown so it is copyable. */
|
|
30
35
|
function welcomeLines(path: string): string[] {
|
|
31
36
|
return [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
`Hello! Thanks for taking the time to try the pre-release ${BIN_NAME} CLI.`,
|
|
38
|
+
`Please file issues and feedback at ${ISSUES_URL}.`,
|
|
39
|
+
`${BIN_NAME} stores its configuration in ${path} (just created with defaults).`,
|
|
40
|
+
`A \`${PROJECT_CONFIG_FILENAME}\` in this directory or above it overrides it per project.`,
|
|
36
41
|
"",
|
|
37
42
|
];
|
|
38
43
|
}
|
|
@@ -86,6 +91,8 @@ export async function ensureFirstRun(
|
|
|
86
91
|
});
|
|
87
92
|
} catch (error) {
|
|
88
93
|
const reason = error instanceof Error ? error.message : String(error);
|
|
89
|
-
write(
|
|
94
|
+
write(
|
|
95
|
+
`Warning: could not create the global ${BIN_NAME} config — ${reason}`,
|
|
96
|
+
);
|
|
90
97
|
}
|
|
91
98
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read the global machine config: `$XDG_CONFIG_HOME/pragma/config.json`.
|
|
3
3
|
*
|
|
4
|
-
* The global layer holds cross-project machine state (channel, detail, tier
|
|
5
|
-
*
|
|
4
|
+
* The global layer holds cross-project machine state (channel, detail, tier).
|
|
5
|
+
* A missing or empty file contributes no values — the layer simply
|
|
6
6
|
* does not participate in the merge. A *malformed* file (a torn write) must
|
|
7
7
|
* never brick every command (the old loud `CONFIG_ERROR` throw) nor be silently
|
|
8
8
|
* discarded (writeConfigField's old reset-to-`{}`): it is backed up to a
|
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `schema.ts` (zod) is intentionally not re-exported here: it is imported
|
|
5
5
|
* directly and lazily by the readers, keeping this barrel — and any storeless
|
|
6
|
-
* consumer — off the zod fast path.
|
|
6
|
+
* consumer — off the zod fast path. `defaults` is likewise not re-exported:
|
|
7
|
+
* it statically imports the distribution config and the validating schema.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
export { default as defaults } from "./defaults.js";
|
|
10
10
|
export { evaluateProjectConfig } from "./evaluateProjectConfig.js";
|
|
11
11
|
export { findProjectConfig } from "./findProjectConfig.js";
|
|
12
12
|
export { ensureFirstRun, firstRunTask } from "./firstRun.js";
|
|
13
13
|
export type { GlobalConfigRead } from "./globalConfig.js";
|
|
14
14
|
export { readGlobalConfig } from "./globalConfig.js";
|
|
15
15
|
export {
|
|
16
|
-
cacheDir,
|
|
17
16
|
configCacheDir,
|
|
18
17
|
configDir,
|
|
19
18
|
globalConfigPath,
|
|
@@ -26,8 +25,8 @@ export type {
|
|
|
26
25
|
ConfigLayers,
|
|
27
26
|
ConfigOrigin,
|
|
28
27
|
ConfigOrigins,
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
PackDeclaration,
|
|
29
|
+
PackSource,
|
|
31
30
|
PragmaConfig,
|
|
32
31
|
RawConfig,
|
|
33
32
|
} from "./types.js";
|
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* XDG base-directory resolution for config
|
|
2
|
+
* XDG base-directory resolution for config and state.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Every directory is namespaced by the distribution's own {@link BIN_NAME}, so a
|
|
5
|
+
* fork stores its state beside this one instead of colliding in it — the
|
|
6
|
+
* greeting that quotes the resolved path then names one program, not two. Config
|
|
7
|
+
* (the global JSON) lives under `$XDG_CONFIG_HOME/<bin>`; the evaluated
|
|
8
|
+
* project-config cache lives under `$XDG_STATE_HOME/<bin>/config-cache`. The
|
|
9
|
+
* store layer's cache root (`$XDG_CACHE_HOME/<bin>`) is resolved by
|
|
10
|
+
* `kernel/runtime/paths.ts` instead — it must stay leaf-clean for the storeless
|
|
11
|
+
* fast path. Tests point these env vars at per-run temp directories (see
|
|
12
|
+
* setupXdgIsolation) so they never touch the developer's real state.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { homedir } from "node:os";
|
|
11
16
|
import { join } from "node:path";
|
|
17
|
+
import { BIN_NAME } from "../../constants.js";
|
|
12
18
|
|
|
13
|
-
/** `$XDG_CONFIG_HOME
|
|
19
|
+
/** `$XDG_CONFIG_HOME/<bin>` (default `~/.config/<bin>`). */
|
|
14
20
|
export function configDir(): string {
|
|
15
21
|
const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
16
|
-
return join(base,
|
|
22
|
+
return join(base, BIN_NAME);
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
/** `$
|
|
20
|
-
export function cacheDir(): string {
|
|
21
|
-
const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
|
|
22
|
-
return join(base, "pragma");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** `$XDG_STATE_HOME/pragma` (default `~/.local/state/pragma`). */
|
|
25
|
+
/** `$XDG_STATE_HOME/<bin>` (default `~/.local/state/<bin>`). */
|
|
26
26
|
export function stateDir(): string {
|
|
27
27
|
const base = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
28
|
-
return join(base,
|
|
28
|
+
return join(base, BIN_NAME);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** The global config file: `$XDG_CONFIG_HOME
|
|
31
|
+
/** The global config file: `$XDG_CONFIG_HOME/<bin>/config.json`. */
|
|
32
32
|
export function globalConfigPath(): string {
|
|
33
33
|
return join(configDir(), "config.json");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** The evaluated-project-config cache dir: `$XDG_STATE_HOME
|
|
36
|
+
/** The evaluated-project-config cache dir: `$XDG_STATE_HOME/<bin>/config-cache`. */
|
|
37
37
|
export function configCacheDir(): string {
|
|
38
38
|
return join(stateDir(), "config-cache");
|
|
39
39
|
}
|
|
@@ -10,6 +10,7 @@ import { tmpdir } from "node:os";
|
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { runTask } from "@canonical/task/node";
|
|
12
12
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { BIN_NAME, PROGRAM_DESCRIPTION } from "../../constants.js";
|
|
13
14
|
import { evaluateProjectConfig } from "./evaluateProjectConfig.js";
|
|
14
15
|
import { findProjectConfig } from "./findProjectConfig.js";
|
|
15
16
|
import { readGlobalConfig } from "./globalConfig.js";
|
|
@@ -58,13 +59,13 @@ describe("readConfig — layering + provenance", () => {
|
|
|
58
59
|
expect(config.tier).toBe("core");
|
|
59
60
|
expect(config.channel).toBe("experimental");
|
|
60
61
|
expect(config.detail).toBe("detailed");
|
|
61
|
-
expect(config.
|
|
62
|
+
expect(config.packs).toHaveLength(3); // from defaults
|
|
62
63
|
|
|
63
64
|
expect(origins).toMatchObject({
|
|
64
65
|
tier: "project",
|
|
65
66
|
channel: "global",
|
|
66
67
|
detail: "project", // project wins over global
|
|
67
|
-
|
|
68
|
+
packs: "default",
|
|
68
69
|
});
|
|
69
70
|
expect(project.exists).toBe(true);
|
|
70
71
|
expect(global.exists).toBe(true);
|
|
@@ -81,14 +82,43 @@ describe("readConfig — layering + provenance", () => {
|
|
|
81
82
|
expect(project.exists).toBe(false);
|
|
82
83
|
});
|
|
83
84
|
|
|
84
|
-
it("replaces
|
|
85
|
+
it("replaces packs wholesale from the project layer", async () => {
|
|
85
86
|
freshXdg();
|
|
86
|
-
const dir = projectWith('export default {
|
|
87
|
+
const dir = projectWith('export default { packs: ["@acme/only"] };');
|
|
88
|
+
|
|
89
|
+
const { config, origins } = await readConfig(dir);
|
|
90
|
+
|
|
91
|
+
expect(config.packs).toEqual(["@acme/only"]);
|
|
92
|
+
expect(origins.packs).toBe("project");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("accepts a layer's identity fields and gives them NO effect", async () => {
|
|
96
|
+
// Identity is read from `pragma.conf.ts` by `src/constants.ts` at module
|
|
97
|
+
// load, so a layer declaring it can only ever be silent. The validator
|
|
98
|
+
// still accepts the keys (the distribution config shares the schema), and
|
|
99
|
+
// the merged config now carries neither the value nor a provenance for it
|
|
100
|
+
// — which is what `config show` used to report and did not honour.
|
|
101
|
+
freshXdg();
|
|
102
|
+
writeGlobal('{"help":"Global help"}');
|
|
103
|
+
const dir = projectWith(
|
|
104
|
+
'export default { name: "acme", colophon: { markdown: "By Acme." }, issuesUrl: "https://acme.test/issues", tier: "core" };',
|
|
105
|
+
);
|
|
87
106
|
|
|
88
107
|
const { config, origins } = await readConfig(dir);
|
|
89
108
|
|
|
90
|
-
expect(config
|
|
91
|
-
expect(
|
|
109
|
+
expect(config).not.toHaveProperty("name");
|
|
110
|
+
expect(config).not.toHaveProperty("help");
|
|
111
|
+
expect(config).not.toHaveProperty("colophon");
|
|
112
|
+
expect(config).not.toHaveProperty("issuesUrl");
|
|
113
|
+
expect(origins).not.toHaveProperty("name");
|
|
114
|
+
// The positive control, from the SAME project file: a layerable field
|
|
115
|
+
// declared beside them IS merged and reported, so the assertions above
|
|
116
|
+
// cannot pass by the project layer having failed to load at all.
|
|
117
|
+
expect(config.tier).toBe("core");
|
|
118
|
+
expect(origins.tier).toBe("project");
|
|
119
|
+
// And the projected identity is unmoved by any of it.
|
|
120
|
+
expect(BIN_NAME).toBe("pragma");
|
|
121
|
+
expect(PROGRAM_DESCRIPTION).not.toBe("Global help");
|
|
92
122
|
});
|
|
93
123
|
|
|
94
124
|
it("merges the completion policy into the effective config", async () => {
|
|
@@ -106,6 +136,212 @@ describe("readConfig — layering + provenance", () => {
|
|
|
106
136
|
});
|
|
107
137
|
});
|
|
108
138
|
|
|
139
|
+
describe("`detail` — a closed enum at load", () => {
|
|
140
|
+
it("a project config declaring an unknown level throws CONFIG_ERROR naming the file and the three levels", async () => {
|
|
141
|
+
freshXdg();
|
|
142
|
+
// Before the enum, `detail: "banana"` passed validation, was reported as
|
|
143
|
+
// `[project]` by `config show`, and silently rendered at `standard`.
|
|
144
|
+
const dir = projectWith('export default { detail: "banana" };');
|
|
145
|
+
const path = join(dir, "pragma.config.ts");
|
|
146
|
+
|
|
147
|
+
let caught: unknown;
|
|
148
|
+
try {
|
|
149
|
+
await evaluateProjectConfig(path);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
caught = error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
|
|
155
|
+
const message = (caught as { message: string }).message;
|
|
156
|
+
expect(message).toContain(path);
|
|
157
|
+
expect(message).toContain("detail");
|
|
158
|
+
for (const level of ["summary", "standard", "detailed"]) {
|
|
159
|
+
expect(message).toContain(level);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("a global JSON declaring an unknown level throws the same loud error", () => {
|
|
164
|
+
freshXdg();
|
|
165
|
+
writeGlobal('{"detail": "digest"}');
|
|
166
|
+
|
|
167
|
+
let caught: unknown;
|
|
168
|
+
try {
|
|
169
|
+
readGlobalConfig();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
caught = error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
|
|
175
|
+
const message = (caught as { message: string }).message;
|
|
176
|
+
expect(message).toContain(globalConfigPath());
|
|
177
|
+
for (const level of ["summary", "standard", "detailed"]) {
|
|
178
|
+
expect(message).toContain(level);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("legacy config shapes — loud rename/removal errors", () => {
|
|
184
|
+
it("a project config declaring `packages` throws CONFIG_ERROR naming the rename", async () => {
|
|
185
|
+
freshXdg();
|
|
186
|
+
// Unknown keys are stripped by the schema, so the legacy key must be
|
|
187
|
+
// DETECTED — a silent ignore would boot the default packs while the user
|
|
188
|
+
// believes their own list is in force.
|
|
189
|
+
const dir = projectWith('export default { packages: ["@acme/only"] };');
|
|
190
|
+
const path = join(dir, "pragma.config.ts");
|
|
191
|
+
|
|
192
|
+
let caught: unknown;
|
|
193
|
+
try {
|
|
194
|
+
await evaluateProjectConfig(path);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
caught = error;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
|
|
200
|
+
expect((caught as { message: string }).message).toContain(
|
|
201
|
+
`Invalid config in ${path}: the "packages" field was renamed to "packs". The entry shape is unchanged.`,
|
|
202
|
+
);
|
|
203
|
+
// The recovery carries the path; the message drops the instruction.
|
|
204
|
+
expect(
|
|
205
|
+
(caught as { recovery?: { message: string } }).recovery?.message,
|
|
206
|
+
).toBe(`In ${path}, rename "packages:" to "packs".`);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("a legacy global JSON declaring `packages` throws the same rename error", () => {
|
|
210
|
+
freshXdg();
|
|
211
|
+
writeGlobal('{"packages": []}');
|
|
212
|
+
|
|
213
|
+
let caught: unknown;
|
|
214
|
+
try {
|
|
215
|
+
readGlobalConfig();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
caught = error;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
|
|
221
|
+
expect((caught as { message: string }).message).toContain(
|
|
222
|
+
'the "packages" field was renamed to "packs". The entry shape is unchanged.',
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("a removed `generators` field throws CONFIG_ERROR naming the field", async () => {
|
|
227
|
+
freshXdg();
|
|
228
|
+
// Ruled dead (L-OPEN-1): `resolveSources` never read it, `create` resolves
|
|
229
|
+
// its generators statically, and its only consumers were the merge and
|
|
230
|
+
// `config show`'s name display. A config still declaring it must fail
|
|
231
|
+
// naming the file — the schema's unknown-key stripping would otherwise
|
|
232
|
+
// swallow the deletion in silence.
|
|
233
|
+
const dir = projectWith(
|
|
234
|
+
'export default { generators: [{ name: "@acme/gen", source: "npm:@acme/gen@^1.0.0" }] };',
|
|
235
|
+
);
|
|
236
|
+
const path = join(dir, "pragma.config.ts");
|
|
237
|
+
|
|
238
|
+
let caught: unknown;
|
|
239
|
+
try {
|
|
240
|
+
await evaluateProjectConfig(path);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
caught = error;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
|
|
246
|
+
const message = (caught as { message: string }).message;
|
|
247
|
+
expect(message).toContain(path);
|
|
248
|
+
expect(message).toContain('"generators"');
|
|
249
|
+
expect(message).toContain("removed");
|
|
250
|
+
expect(
|
|
251
|
+
(caught as { recovery?: { message: string } }).recovery?.message,
|
|
252
|
+
).toBe(`In ${path}, delete the "generators" field.`);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("a global JSON still declaring generators throws the same removal error", () => {
|
|
256
|
+
freshXdg();
|
|
257
|
+
writeGlobal('{"generators": []}');
|
|
258
|
+
|
|
259
|
+
let caught: unknown;
|
|
260
|
+
try {
|
|
261
|
+
readGlobalConfig();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
caught = error;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
|
|
267
|
+
expect((caught as { message: string }).message).toContain('"generators"');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("a removed `completion.caseSensitive` throws CONFIG_ERROR naming the field", async () => {
|
|
271
|
+
freshXdg();
|
|
272
|
+
// The field was validated and read by NOTHING. Removing it silently would
|
|
273
|
+
// leave a config author believing case-sensitivity is in force; the schema
|
|
274
|
+
// strips unknown keys, so the removal must be DETECTED before validation.
|
|
275
|
+
const dir = projectWith(
|
|
276
|
+
"export default { completion: { minChars: 3, caseSensitive: true } };",
|
|
277
|
+
);
|
|
278
|
+
const path = join(dir, "pragma.config.ts");
|
|
279
|
+
|
|
280
|
+
let caught: unknown;
|
|
281
|
+
try {
|
|
282
|
+
await evaluateProjectConfig(path);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
caught = error;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
|
|
288
|
+
const message = (caught as { message: string }).message;
|
|
289
|
+
expect(message).toContain(path);
|
|
290
|
+
expect(message).toContain('"completion.caseSensitive"');
|
|
291
|
+
expect(message).toContain("removed");
|
|
292
|
+
expect(
|
|
293
|
+
(caught as { recovery?: { message: string } }).recovery?.message,
|
|
294
|
+
).toBe(`In ${path}, delete "caseSensitive" from "completion".`);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("a global JSON still setting completion.caseSensitive throws the same removal error", () => {
|
|
298
|
+
freshXdg();
|
|
299
|
+
writeGlobal('{"completion": {"caseSensitive": false}}');
|
|
300
|
+
|
|
301
|
+
let caught: unknown;
|
|
302
|
+
try {
|
|
303
|
+
readGlobalConfig();
|
|
304
|
+
} catch (error) {
|
|
305
|
+
caught = error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
|
|
309
|
+
expect((caught as { message: string }).message).toContain(
|
|
310
|
+
'"completion.caseSensitive"',
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("a TOP-LEVEL `caseSensitive` key is an ordinary unknown key, not the removed field", async () => {
|
|
315
|
+
freshXdg();
|
|
316
|
+
// The detection reads exactly `completion.caseSensitive` — a stray
|
|
317
|
+
// top-level key keeps the forward-compatible unknown-key stripping.
|
|
318
|
+
const dir = projectWith(
|
|
319
|
+
"export default { caseSensitive: true, tier: 'core' };",
|
|
320
|
+
);
|
|
321
|
+
const path = join(dir, "pragma.config.ts");
|
|
322
|
+
|
|
323
|
+
await expect(evaluateProjectConfig(path)).resolves.toEqual({
|
|
324
|
+
tier: "core",
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("a `packages` key NESTED under another field does NOT trip the detection", async () => {
|
|
329
|
+
freshXdg();
|
|
330
|
+
// The rename detection is deliberately SHALLOW (top-level keys only): a
|
|
331
|
+
// `packages` key inside `prefixes` is a namespace prefix named `packages`,
|
|
332
|
+
// not the legacy field. Pin that against a future "helpful" deep check,
|
|
333
|
+
// which would false-positive here.
|
|
334
|
+
const dir = projectWith(
|
|
335
|
+
'export default { prefixes: { packages: "https://example.com/packages/" } };',
|
|
336
|
+
);
|
|
337
|
+
const path = join(dir, "pragma.config.ts");
|
|
338
|
+
|
|
339
|
+
await expect(evaluateProjectConfig(path)).resolves.toEqual({
|
|
340
|
+
prefixes: { packages: "https://example.com/packages/" },
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
109
345
|
describe("global config — corrupt-file recovery", () => {
|
|
110
346
|
/** Backup siblings the recovery created next to the config file. */
|
|
111
347
|
function corruptBackups(): string[] {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Layer order, least to most specific: the built-in {@link defaults}, the
|
|
5
5
|
* global XDG JSON, and the nearest evaluated `pragma.config.ts`. Merging is
|
|
6
|
-
* per field — a more specific layer wins wholesale (so `
|
|
6
|
+
* per field — a more specific layer wins wholesale (so `packs` *replaces*,
|
|
7
7
|
* preserving the "project pins exactly" semantics) — and each field records
|
|
8
8
|
* the layer that supplied it. Async because the project layer is evaluated
|
|
9
9
|
* (and content-hash cached).
|
|
@@ -37,7 +37,7 @@ export async function readConfig(
|
|
|
37
37
|
const project: RawConfig = projectPath
|
|
38
38
|
? await evaluateProjectConfig(projectPath)
|
|
39
39
|
: {};
|
|
40
|
-
const defaultValues = defaults
|
|
40
|
+
const defaultValues = defaults;
|
|
41
41
|
|
|
42
42
|
const pick = <K extends keyof RawConfig>(
|
|
43
43
|
field: K,
|
|
@@ -51,13 +51,15 @@ export async function readConfig(
|
|
|
51
51
|
return { value: defaultValues[field], origin: "default" };
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
// `name`/`help`/`colophon`/`issuesUrl` are DELIBERATELY not picked: identity
|
|
55
|
+
// is read from `pragma.conf.ts` statically (`src/constants.ts`), so merging a
|
|
56
|
+
// layer's value here would report a provenance nothing honours.
|
|
54
57
|
const tier = pick("tier");
|
|
55
58
|
const channel = pick("channel");
|
|
56
59
|
const detail = pick("detail");
|
|
57
|
-
const
|
|
60
|
+
const packs = pick("packs");
|
|
58
61
|
const stories = pick("stories");
|
|
59
62
|
const prefixes = pick("prefixes");
|
|
60
|
-
const prompts = pick("prompts");
|
|
61
63
|
// `completion` is read at `setup completions` emit time (not on any fast
|
|
62
64
|
// path) and carries no `config show` provenance, so it merges into the
|
|
63
65
|
// effective config but is deliberately absent from ConfigOrigins.
|
|
@@ -67,10 +69,9 @@ export async function readConfig(
|
|
|
67
69
|
channel: (channel.value ?? defaults.channel) as Channel,
|
|
68
70
|
...(tier.value !== undefined ? { tier: tier.value } : {}),
|
|
69
71
|
...(detail.value !== undefined ? { detail: detail.value } : {}),
|
|
70
|
-
...(
|
|
72
|
+
...(packs.value !== undefined ? { packs: packs.value } : {}),
|
|
71
73
|
...(stories.value !== undefined ? { stories: stories.value } : {}),
|
|
72
74
|
...(prefixes.value !== undefined ? { prefixes: prefixes.value } : {}),
|
|
73
|
-
...(prompts.value !== undefined ? { prompts: prompts.value } : {}),
|
|
74
75
|
...(completion.value !== undefined ? { completion: completion.value } : {}),
|
|
75
76
|
};
|
|
76
77
|
|
|
@@ -80,10 +81,9 @@ export async function readConfig(
|
|
|
80
81
|
tier: tier.origin,
|
|
81
82
|
channel: channel.origin,
|
|
82
83
|
detail: detail.origin,
|
|
83
|
-
|
|
84
|
+
packs: packs.origin,
|
|
84
85
|
stories: stories.origin,
|
|
85
86
|
prefixes: prefixes.origin,
|
|
86
|
-
prompts: prompts.origin,
|
|
87
87
|
},
|
|
88
88
|
global: { path: global.path, exists: global.exists },
|
|
89
89
|
project: {
|