@canonical/pragma-cli 0.30.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 +64 -35
- 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/parseConfigValues.ts +2 -0
- package/src/config/readConfigLayers.test.ts +2 -0
- package/src/config/readConfigLayers.ts +3 -0
- package/src/config/types.ts +7 -0
- package/src/constants.ts +3 -1
- 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/orchestration/enrichBlocks.ts +6 -3
- package/src/domains/block/stories.ts +9 -3
- package/src/domains/config/commands/channel.test.ts +16 -0
- package/src/domains/config/commands/channel.ts +14 -1
- package/src/domains/config/commands/framework.test.ts +14 -0
- package/src/domains/config/commands/framework.ts +13 -1
- package/src/domains/config/commands/tier.test.ts +7 -0
- package/src/domains/config/commands/tier.ts +14 -1
- package/src/domains/config/operations/validateTier.test.ts +9 -0
- package/src/domains/config/operations/validateTier.ts +10 -2
- 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/checkMcpCommands.ts +9 -6
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
- package/src/domains/doctor/operations/checks.test.ts +6 -4
- 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/operations/inspectUri.test.ts +5 -5
- package/src/domains/graphql/commands/serve.test.ts +12 -6
- package/src/domains/graphql/commands/serve.ts +16 -6
- 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/operations/compileSchema.test.ts +2 -2
- package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
- 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 +5 -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/index.ts +1 -0
- package/src/domains/info/types.ts +13 -0
- package/src/domains/llm/capabilities.test.ts +23 -3
- 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 +49 -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/sample.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +5 -3
- package/src/domains/modifier/stories.ts +9 -0
- package/src/domains/ontology/commands/list.test.ts +5 -3
- package/src/domains/ontology/commands/show.test.ts +85 -9
- 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 +358 -67
- 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/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.ts +53 -8
- package/src/domains/refs/operations/updateRefs.ts +0 -1
- package/src/domains/setup/commands/index.ts +1 -0
- package/src/domains/setup/commands/setup.ts +70 -0
- 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/shared/bootStore.bareCore.test.ts +63 -0
- package/src/domains/shared/bootStore.test.ts +82 -0
- package/src/domains/shared/bootStore.ts +56 -20
- 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.ts +48 -0
- package/src/domains/shared/loaders/gitLoader.ts +1 -2
- package/src/domains/shared/loaders/localLoader.ts +2 -18
- package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
- package/src/domains/shared/loaders/readPackageDir.ts +46 -8
- package/src/domains/shared/lookupMany.test.ts +15 -0
- package/src/domains/shared/lookupMany.ts +22 -6
- package/src/domains/shared/namespaces.ts +13 -5
- 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/resolveLookupExitCode.test.ts +48 -0
- package/src/domains/shared/resolveLookupExitCode.ts +42 -0
- package/src/domains/shared/semanticPackage.test.ts +89 -0
- package/src/domains/shared/semanticPackage.ts +17 -2
- package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
- package/src/domains/shared/stories/compileLookupTool.ts +1 -1
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
- 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 +30 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
- 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 +305 -7
- package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
- package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
- package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
- package/src/domains/shared/stories/pack/index.ts +14 -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/standardParity.test.ts +293 -220
- package/src/domains/shared/stories/pack/types.ts +123 -3
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
- package/src/domains/shared/stories/types.ts +9 -1
- 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/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 +13 -1
- package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
- package/src/domains/trace/commands/show.ts +30 -4
- package/src/index.ts +1 -13
- package/src/mcp/registerResources.test.ts +2 -2
- package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
- package/src/mcp/tools/index.ts +18 -10
- package/src/mcp/tools/registerTools.test.ts +106 -30
- package/src/pipeline/collectCommands.test.ts +151 -1
- package/src/pipeline/collectCommands.ts +34 -19
- 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 +162 -19
- package/src/testing/cli.ts +30 -2
- package/src/testing/dsFixtures.ts +21 -4
- package/src/testing/index.ts +1 -4
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
- package/src/testing/integration/cli-user-stories.test.ts +63 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
- package/src/testing/integration/edge-cases.test.ts +62 -50
- package/src/testing/integration/executor-execution.test.ts +6 -4
- package/src/testing/integration/executor-golden.test.ts +2 -2
- package/src/testing/integration/mcp-surface.test.ts +40 -15
- package/src/testing/integration/parity.test.ts +119 -70
- package/src/testing/setupXdgIsolation.ts +16 -5
- package/src/testing/standardParityFixtures.ts +26 -83
- package/src/testing/store.ts +4 -4
- 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 -11
- package/src/domains/standard/commands/lookup.test.ts +0 -135
- package/src/domains/standard/commands/lookup.ts +0 -14
- 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 -89
- 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 -33
- 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/stories.test.ts +0 -24
- package/src/domains/standard/stories.ts +0 -151
- 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 -9
- 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 -12
- 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/domains/tier/stories.test.ts +0 -22
- package/src/domains/tier/stories.ts +0 -52
|
@@ -2,7 +2,7 @@ import type { Store } from "@canonical/ke";
|
|
|
2
2
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
3
3
|
import { PragmaError } from "#error";
|
|
4
4
|
import { createTestStore, DS_ALL_TTL } from "#testing";
|
|
5
|
-
import {
|
|
5
|
+
import { DEFAULT_PREFIX_MAP, P } from "../../shared/prefixes.js";
|
|
6
6
|
import inspectUri from "./inspectUri.js";
|
|
7
7
|
|
|
8
8
|
let store: Store;
|
|
@@ -19,7 +19,7 @@ afterAll(() => cleanup());
|
|
|
19
19
|
describe("inspectUri", () => {
|
|
20
20
|
it("returns grouped triples for a known URI", async () => {
|
|
21
21
|
const result = await inspectUri(store, `${P.ds}global.component.button`);
|
|
22
|
-
expect(result.uri).toBe(`${
|
|
22
|
+
expect(result.uri).toBe(`${DEFAULT_PREFIX_MAP.ds}global.component.button`);
|
|
23
23
|
expect(result.groups.length).toBeGreaterThan(0);
|
|
24
24
|
|
|
25
25
|
const predicates = result.groups.map((g) => g.predicate);
|
|
@@ -30,15 +30,15 @@ describe("inspectUri", () => {
|
|
|
30
30
|
|
|
31
31
|
it("resolves prefixed URIs", async () => {
|
|
32
32
|
const result = await inspectUri(store, `${P.ds}global.component.button`);
|
|
33
|
-
expect(result.uri).toBe(`${
|
|
33
|
+
expect(result.uri).toBe(`${DEFAULT_PREFIX_MAP.ds}global.component.button`);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("accepts full URIs", async () => {
|
|
37
37
|
const result = await inspectUri(
|
|
38
38
|
store,
|
|
39
|
-
`${
|
|
39
|
+
`${DEFAULT_PREFIX_MAP.ds}global.component.button`,
|
|
40
40
|
);
|
|
41
|
-
expect(result.uri).toBe(`${
|
|
41
|
+
expect(result.uri).toBe(`${DEFAULT_PREFIX_MAP.ds}global.component.button`);
|
|
42
42
|
expect(result.groups.length).toBeGreaterThan(0);
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { PragmaError } from "#error";
|
|
2
3
|
import type { PragmaContext } from "../../shared/context.js";
|
|
3
4
|
import serveCommand, { readPort } from "./serve.js";
|
|
4
5
|
|
|
@@ -7,15 +8,20 @@ describe("readPort", () => {
|
|
|
7
8
|
expect(readPort("4001")).toBe(4001);
|
|
8
9
|
});
|
|
9
10
|
|
|
10
|
-
it("
|
|
11
|
+
it("uses the default when the flag is absent", () => {
|
|
11
12
|
expect(readPort(undefined)).toBe(4000);
|
|
12
|
-
expect(readPort(
|
|
13
|
+
expect(readPort("")).toBe(4000);
|
|
13
14
|
});
|
|
14
15
|
|
|
15
|
-
it("
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
it("rejects an out-of-range or unparseable provided port", () => {
|
|
17
|
+
for (const value of ["0", "70000", "not-a-port"]) {
|
|
18
|
+
expect(() => readPort(value)).toThrowError(PragmaError);
|
|
19
|
+
try {
|
|
20
|
+
readPort(value);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
expect((error as PragmaError).code).toBe("INVALID_INPUT");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
19
25
|
});
|
|
20
26
|
});
|
|
21
27
|
|
|
@@ -18,15 +18,25 @@ function readStringArray(value: unknown): string[] {
|
|
|
18
18
|
: [];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Parse the `--port` flag. A missing flag uses the default; a provided value
|
|
23
|
+
* must be a valid port (1–65535), otherwise it is rejected rather than
|
|
24
|
+
* silently falling back and masking the user's mistake.
|
|
25
|
+
*
|
|
26
|
+
* @throws PragmaError INVALID_INPUT when a provided `--port` is not a valid port.
|
|
27
|
+
*/
|
|
22
28
|
export function readPort(value: unknown): number {
|
|
23
|
-
if (
|
|
29
|
+
if (value === undefined || value === null || value === "") {
|
|
24
30
|
return DEFAULT_PORT;
|
|
25
31
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const text = String(value).trim();
|
|
33
|
+
const port = Number(text);
|
|
34
|
+
if (!/^\d+$/.test(text) || port < 1 || port > 65535) {
|
|
35
|
+
throw PragmaError.invalidInput("port", String(value), {
|
|
36
|
+
recovery: { message: "Provide a port between 1 and 65535." },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return port;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
/**
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
|
|
3
3
|
import parsePrefixes from "./parsePrefixes.js";
|
|
4
4
|
|
|
5
5
|
describe("parsePrefixes", () => {
|
|
6
6
|
it("returns the default prefix map for no entries", () => {
|
|
7
7
|
const prefixes = parsePrefixes([]);
|
|
8
|
-
expect(prefixes).toEqual(
|
|
8
|
+
expect(prefixes).toEqual(DEFAULT_PREFIX_MAP);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it("merges entries on top of the defaults", () => {
|
|
12
12
|
const prefixes = parsePrefixes(["ex=http://example.org/"]);
|
|
13
13
|
expect(prefixes.ex).toBe("http://example.org/");
|
|
14
|
-
expect(prefixes.ds).toBe(
|
|
14
|
+
expect(prefixes.ds).toBe(DEFAULT_PREFIX_MAP.ds);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it("lets entries override default prefixes", () => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { PragmaError } from "#error";
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Parse repeated `--prefix name=namespace` entries into a prefix map.
|
|
6
6
|
*
|
|
7
7
|
* User-supplied prefixes are merged on top of pragma's default
|
|
8
|
-
* {@link
|
|
8
|
+
* {@link DEFAULT_PREFIX_MAP}, so `ds:` and the W3C vocabularies stay available
|
|
9
9
|
* unless explicitly overridden.
|
|
10
10
|
*
|
|
11
11
|
* @param entries - Raw `name=namespace` strings from the CLI.
|
|
@@ -15,7 +15,7 @@ import { PREFIX_MAP } from "../../shared/prefixes.js";
|
|
|
15
15
|
export default function parsePrefixes(
|
|
16
16
|
entries: readonly string[],
|
|
17
17
|
): Record<string, string> {
|
|
18
|
-
const prefixes: Record<string, string> = { ...
|
|
18
|
+
const prefixes: Record<string, string> = { ...DEFAULT_PREFIX_MAP };
|
|
19
19
|
|
|
20
20
|
for (const entry of entries) {
|
|
21
21
|
const separator = entry.indexOf("=");
|
|
@@ -53,4 +53,13 @@ describe("resolveSourceFiles", () => {
|
|
|
53
53
|
const files = resolveSourceFiles(["b.ttl", "nested/*.ttl"], dir);
|
|
54
54
|
expect(files).toEqual([join(dir, "b.ttl"), join(dir, "nested", "c.ttl")]);
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it("skips a directory path instead of treating it as a file", () => {
|
|
58
|
+
// A directory would later crash readFileSync with EISDIR.
|
|
59
|
+
expect(resolveSourceFiles(["nested"], dir)).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("resolves the empty string (the cwd directory) to no files", () => {
|
|
63
|
+
expect(resolveSourceFiles([""], dir)).toEqual([]);
|
|
64
|
+
});
|
|
56
65
|
});
|
|
@@ -8,9 +8,27 @@
|
|
|
8
8
|
* @note Impure — reads filesystem to expand globs and check existence.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { globSync, statSync } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Whether a path points at an existing regular file.
|
|
16
|
+
*
|
|
17
|
+
* Guards against a directory (or the empty-string path, which resolves to the
|
|
18
|
+
* cwd directory) slipping through as a "source" and later crashing readFileSync
|
|
19
|
+
* with EISDIR.
|
|
20
|
+
*
|
|
21
|
+
* @param absPath - Absolute path to check.
|
|
22
|
+
* @returns True when the path exists and is a regular file.
|
|
23
|
+
*/
|
|
24
|
+
function isRegularFile(absPath: string): boolean {
|
|
25
|
+
try {
|
|
26
|
+
return statSync(absPath).isFile();
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
/**
|
|
15
33
|
* Expand file paths and glob patterns into absolute file paths.
|
|
16
34
|
*
|
|
@@ -31,11 +49,14 @@ export default function resolveSourceFiles(
|
|
|
31
49
|
pattern.includes("[")
|
|
32
50
|
) {
|
|
33
51
|
for (const file of globSync(pattern, { cwd })) {
|
|
34
|
-
|
|
52
|
+
const absPath = resolve(cwd, file);
|
|
53
|
+
if (isRegularFile(absPath)) {
|
|
54
|
+
files.push(absPath);
|
|
55
|
+
}
|
|
35
56
|
}
|
|
36
57
|
} else {
|
|
37
58
|
const absPath = resolve(cwd, pattern);
|
|
38
|
-
if (
|
|
59
|
+
if (isRegularFile(absPath)) {
|
|
39
60
|
files.push(absPath);
|
|
40
61
|
}
|
|
41
62
|
}
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
GRAPHQL_ERROR_TTL,
|
|
7
7
|
GRAPHQL_FATAL_TTL,
|
|
8
8
|
} from "#testing";
|
|
9
|
-
import {
|
|
9
|
+
import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
|
|
10
10
|
import compileSchema from "./compileSchema.js";
|
|
11
11
|
|
|
12
|
-
const PREFIXES = { ...
|
|
12
|
+
const PREFIXES = { ...DEFAULT_PREFIX_MAP, ex: EX_NAMESPACE };
|
|
13
13
|
|
|
14
14
|
const inline = (content: string, path: string) => ({
|
|
15
15
|
content,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { EX_NAMESPACE, GRAPHQL_CLEAN_TTL } from "#testing";
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
|
|
4
4
|
import createSchemaServer, { type SchemaServer } from "./createSchemaServer.js";
|
|
5
5
|
|
|
6
|
-
const PREFIXES = { ...
|
|
6
|
+
const PREFIXES = { ...DEFAULT_PREFIX_MAP, ex: EX_NAMESPACE };
|
|
7
7
|
|
|
8
8
|
describe("createSchemaServer", () => {
|
|
9
9
|
let server: SchemaServer | undefined;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
12
|
+
import { EX_NAMESPACE, GRAPHQL_CLEAN_TTL, GRAPHQL_FATAL_TTL } from "#testing";
|
|
13
|
+
import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
|
|
14
|
+
import type { SemanticPackage } from "../../shared/semanticPackage.js";
|
|
15
|
+
import ensureSchemaArtifact, {
|
|
16
|
+
resolveSchemaArtifactPath,
|
|
17
|
+
} from "./ensureSchemaArtifact.js";
|
|
18
|
+
|
|
19
|
+
const PREFIXES = { ...DEFAULT_PREFIX_MAP, ex: EX_NAMESPACE };
|
|
20
|
+
|
|
21
|
+
let dataDir: string;
|
|
22
|
+
let cwd: string;
|
|
23
|
+
let originalXdgData: string | undefined;
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
dataDir = mkdtempSync(join(tmpdir(), "pragma-schema-artifact-data-"));
|
|
27
|
+
cwd = mkdtempSync(join(tmpdir(), "pragma-schema-artifact-cwd-"));
|
|
28
|
+
originalXdgData = process.env.XDG_DATA_HOME;
|
|
29
|
+
process.env.XDG_DATA_HOME = dataDir;
|
|
30
|
+
// The global test setup disables artifact emission; this suite tests it.
|
|
31
|
+
delete process.env.PRAGMA_SCHEMA_ARTIFACT;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
if (originalXdgData === undefined) {
|
|
36
|
+
delete process.env.XDG_DATA_HOME;
|
|
37
|
+
} else {
|
|
38
|
+
process.env.XDG_DATA_HOME = originalXdgData;
|
|
39
|
+
}
|
|
40
|
+
process.env.PRAGMA_SCHEMA_ARTIFACT = "off";
|
|
41
|
+
rmSync(dataDir, { recursive: true, force: true });
|
|
42
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
delete process.env.PRAGMA_SCHEMA_ARTIFACT;
|
|
47
|
+
rmSync(join(dataDir, "pragma"), { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function makeRuntime(ttl: string): Parameters<typeof ensureSchemaArtifact>[0] {
|
|
51
|
+
const pkg: SemanticPackage = {
|
|
52
|
+
name: "@test/ontology",
|
|
53
|
+
version: "1.0.0",
|
|
54
|
+
source: "local",
|
|
55
|
+
graphs: [{ path: "ontology.ttl", content: ttl, format: "turtle" }],
|
|
56
|
+
skills: [],
|
|
57
|
+
stories: [],
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
cwd,
|
|
61
|
+
packages: [pkg],
|
|
62
|
+
store: { prefixes: PREFIXES } as never,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("resolveSchemaArtifactPath", () => {
|
|
67
|
+
it("uses the XDG data dir when no project config is in tree", () => {
|
|
68
|
+
expect(resolveSchemaArtifactPath(cwd)).toBe(
|
|
69
|
+
join(dataDir, "pragma", "schema.graphql"),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("uses .pragma/ beside the project config when one is in tree", () => {
|
|
74
|
+
const project = mkdtempSync(join(tmpdir(), "pragma-schema-project-"));
|
|
75
|
+
try {
|
|
76
|
+
writeFileSync(join(project, "pragma.config.json"), "{}", "utf-8");
|
|
77
|
+
const nested = join(project, "src");
|
|
78
|
+
mkdirSync(nested);
|
|
79
|
+
expect(resolveSchemaArtifactPath(nested)).toBe(
|
|
80
|
+
join(project, ".pragma", "schema.graphql"),
|
|
81
|
+
);
|
|
82
|
+
} finally {
|
|
83
|
+
rmSync(project, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("ensureSchemaArtifact", () => {
|
|
89
|
+
it("writes the artifact with a source-hash header on first use", async () => {
|
|
90
|
+
const result = await ensureSchemaArtifact(makeRuntime(GRAPHQL_CLEAN_TTL));
|
|
91
|
+
|
|
92
|
+
expect(result.status).toBe("written");
|
|
93
|
+
expect(existsSync(result.path)).toBe(true);
|
|
94
|
+
|
|
95
|
+
const content = readFileSync(result.path, "utf-8");
|
|
96
|
+
expect(content.startsWith("# pragma:sources ")).toBe(true);
|
|
97
|
+
expect(content).toContain("type Thing");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("is fresh on a second run with unchanged sources", async () => {
|
|
101
|
+
const runtime = makeRuntime(GRAPHQL_CLEAN_TTL);
|
|
102
|
+
await ensureSchemaArtifact(runtime);
|
|
103
|
+
|
|
104
|
+
const second = await ensureSchemaArtifact(runtime);
|
|
105
|
+
expect(second.status).toBe("fresh");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("rewrites when the sources change", async () => {
|
|
109
|
+
await ensureSchemaArtifact(makeRuntime(GRAPHQL_CLEAN_TTL));
|
|
110
|
+
|
|
111
|
+
const changed = await ensureSchemaArtifact(
|
|
112
|
+
makeRuntime(`${GRAPHQL_CLEAN_TTL}\n# changed`),
|
|
113
|
+
);
|
|
114
|
+
expect(changed.status).toBe("written");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("leaves a stale artifact in place when compilation fails", async () => {
|
|
118
|
+
const first = await ensureSchemaArtifact(makeRuntime(GRAPHQL_CLEAN_TTL));
|
|
119
|
+
const before = readFileSync(first.path, "utf-8");
|
|
120
|
+
|
|
121
|
+
const failed = await ensureSchemaArtifact(makeRuntime(GRAPHQL_FATAL_TTL));
|
|
122
|
+
expect(failed.status).toBe("failed");
|
|
123
|
+
expect(readFileSync(first.path, "utf-8")).toBe(before);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("compiles from the parseable sources when one is malformed", async () => {
|
|
127
|
+
const pkg: SemanticPackage = {
|
|
128
|
+
name: "@test/ontology",
|
|
129
|
+
version: "1.0.0",
|
|
130
|
+
source: "local",
|
|
131
|
+
graphs: [
|
|
132
|
+
{ path: "clean.ttl", content: GRAPHQL_CLEAN_TTL, format: "turtle" },
|
|
133
|
+
{
|
|
134
|
+
path: "broken.ttl",
|
|
135
|
+
content: "this is not turtle .",
|
|
136
|
+
format: "turtle",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
skills: [],
|
|
140
|
+
stories: [],
|
|
141
|
+
};
|
|
142
|
+
const result = await ensureSchemaArtifact({
|
|
143
|
+
cwd,
|
|
144
|
+
packages: [pkg],
|
|
145
|
+
store: { prefixes: PREFIXES } as never,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.status).toBe("written");
|
|
149
|
+
expect(readFileSync(result.path, "utf-8")).toContain("type Thing");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("skips when no sources resolve", async () => {
|
|
153
|
+
const result = await ensureSchemaArtifact({
|
|
154
|
+
cwd,
|
|
155
|
+
packages: [],
|
|
156
|
+
store: { prefixes: PREFIXES } as never,
|
|
157
|
+
});
|
|
158
|
+
expect(result.status).toBe("skipped");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("skips when disabled via PRAGMA_SCHEMA_ARTIFACT=off", async () => {
|
|
162
|
+
process.env.PRAGMA_SCHEMA_ARTIFACT = "off";
|
|
163
|
+
const result = await ensureSchemaArtifact(makeRuntime(GRAPHQL_CLEAN_TTL));
|
|
164
|
+
expect(result.status).toBe("skipped");
|
|
165
|
+
expect(existsSync(result.path)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-gated `schema.graphql` artifact.
|
|
3
|
+
*
|
|
4
|
+
* pragma keeps one compiled GraphQL schema per data set so downstream
|
|
5
|
+
* tooling (codegen, IDEs, `graphql serve` clients) always has a current
|
|
6
|
+
* SDL without anyone running `pragma graphql build` by hand:
|
|
7
|
+
*
|
|
8
|
+
* - **Location** — next to the project's `pragma.config.json`
|
|
9
|
+
* (`<projectRoot>/.pragma/schema.graphql`) when one is in tree, else the
|
|
10
|
+
* XDG data dir (`$XDG_DATA_HOME/pragma/schema.graphql`).
|
|
11
|
+
* - **Emission** — on first use of pragma (any command that boots the
|
|
12
|
+
* runtime) the artifact is generated if missing.
|
|
13
|
+
* - **Renewal** — the artifact's first line records the hash of the TTL
|
|
14
|
+
* sources it was compiled from (`# pragma:sources <hash>`); it is
|
|
15
|
+
* recompiled only when the resolved sources no longer match that hash.
|
|
16
|
+
*
|
|
17
|
+
* Failure is never fatal: a schema that does not compile leaves the
|
|
18
|
+
* previous artifact in place and the boot proceeds with a warning.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
closeSync,
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
openSync,
|
|
26
|
+
readSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import { createStore, definePlugin, type InlineSource } from "@canonical/ke";
|
|
31
|
+
import { hashSources } from "@canonical/ke-graphql";
|
|
32
|
+
import { findProjectConfigPath } from "#config";
|
|
33
|
+
import { dataRoot } from "../../refs/operations/paths.js";
|
|
34
|
+
import type { PragmaRuntime } from "../../shared/runtime.js";
|
|
35
|
+
import compileSchema from "./compileSchema.js";
|
|
36
|
+
|
|
37
|
+
/** First-line marker carrying the source fingerprint. */
|
|
38
|
+
const HASH_HEADER = "# pragma:sources ";
|
|
39
|
+
|
|
40
|
+
/** Environment kill-switch (CI, tests, air-gapped boots). */
|
|
41
|
+
const DISABLE_ENV = "PRAGMA_SCHEMA_ARTIFACT";
|
|
42
|
+
|
|
43
|
+
export interface SchemaArtifactResult {
|
|
44
|
+
/** Absolute path of the artifact. */
|
|
45
|
+
readonly path: string;
|
|
46
|
+
/**
|
|
47
|
+
* What happened: `fresh` (hash matched, nothing to do), `written`
|
|
48
|
+
* (compiled and saved), `failed` (compilation failed, artifact left
|
|
49
|
+
* as-is), `skipped` (no sources, or disabled via env).
|
|
50
|
+
*/
|
|
51
|
+
readonly status: "fresh" | "written" | "failed" | "skipped";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Where the schema artifact lives for a working directory: project-local
|
|
56
|
+
* (`.pragma/` beside the nearest `pragma.config.json`) when the project is
|
|
57
|
+
* configured, global XDG data dir otherwise.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveSchemaArtifactPath(cwd: string): string {
|
|
60
|
+
const configPath = findProjectConfigPath(cwd);
|
|
61
|
+
return configPath
|
|
62
|
+
? join(dirname(configPath), ".pragma", "schema.graphql")
|
|
63
|
+
: join(dataRoot(), "schema.graphql");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ensure the schema artifact exists and matches the runtime's resolved
|
|
68
|
+
* TTL sources, recompiling only on hash mismatch.
|
|
69
|
+
*
|
|
70
|
+
* @note Impure — reads and writes the artifact; compiles via a throwaway
|
|
71
|
+
* ke store when stale.
|
|
72
|
+
*/
|
|
73
|
+
export default async function ensureSchemaArtifact(
|
|
74
|
+
runtime: Pick<PragmaRuntime, "cwd" | "packages" | "store">,
|
|
75
|
+
): Promise<SchemaArtifactResult> {
|
|
76
|
+
const path = resolveSchemaArtifactPath(runtime.cwd);
|
|
77
|
+
|
|
78
|
+
if (process.env[DISABLE_ENV] === "off") {
|
|
79
|
+
return { path, status: "skipped" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Sort by provenance path so the fingerprint is stable across boots
|
|
83
|
+
// regardless of package resolution order.
|
|
84
|
+
const sources: InlineSource[] = runtime.packages
|
|
85
|
+
.flatMap((pkg) =>
|
|
86
|
+
pkg.graphs.map((graph) => ({
|
|
87
|
+
content: graph.content,
|
|
88
|
+
format: "turtle" as const,
|
|
89
|
+
path: graph.path,
|
|
90
|
+
})),
|
|
91
|
+
)
|
|
92
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
93
|
+
|
|
94
|
+
if (sources.length === 0) {
|
|
95
|
+
return { path, status: "skipped" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const hash = hashSources(sources.map((source) => source.content));
|
|
99
|
+
|
|
100
|
+
if (existsSync(path) && readFirstLine(path) === `${HASH_HEADER}${hash}`) {
|
|
101
|
+
return { path, status: "fresh" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Mirror boot's "one bad file cannot break boot": compile from the
|
|
105
|
+
// parseable sources only. The runtime already warned about any malformed
|
|
106
|
+
// graph while booting the main store, so the filter here is silent.
|
|
107
|
+
// The fingerprint above covers ALL sources, so fixing a malformed file
|
|
108
|
+
// changes the hash and triggers recompilation.
|
|
109
|
+
const parseable = await filterParseableSources(sources);
|
|
110
|
+
if (parseable.length === 0) {
|
|
111
|
+
return { path, status: "failed" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const outcome = await compileSchema({
|
|
115
|
+
sources: parseable,
|
|
116
|
+
prefixes: runtime.store.prefixes,
|
|
117
|
+
cwd: runtime.cwd,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (outcome.status === "failed" || !outcome.compiled) {
|
|
121
|
+
return { path, status: "failed" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
125
|
+
writeFileSync(
|
|
126
|
+
path,
|
|
127
|
+
`${HASH_HEADER}${hash}\n${outcome.compiled.sdl}`,
|
|
128
|
+
"utf-8",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return { path, status: "written" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Read just the artifact's header line — the freshness probe runs on every
|
|
136
|
+
* booted invocation, so it must not scale with the SDL's size.
|
|
137
|
+
*/
|
|
138
|
+
function readFirstLine(path: string, max = 96): string {
|
|
139
|
+
const fd = openSync(path, "r");
|
|
140
|
+
try {
|
|
141
|
+
const buffer = Buffer.alloc(max);
|
|
142
|
+
const bytes = readSync(fd, buffer, 0, max, 0);
|
|
143
|
+
const text = buffer.subarray(0, bytes).toString("utf-8");
|
|
144
|
+
const newline = text.indexOf("\n");
|
|
145
|
+
return newline === -1 ? text : text.slice(0, newline);
|
|
146
|
+
} finally {
|
|
147
|
+
closeSync(fd);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Drop sources the Turtle parser rejects, keeping the rest — the same
|
|
153
|
+
* tolerance the main store's graph loader applies at boot. Probes with a
|
|
154
|
+
* throwaway store so the strict `graphql check` semantics of
|
|
155
|
+
* {@link compileSchema} stay untouched.
|
|
156
|
+
*/
|
|
157
|
+
async function filterParseableSources(
|
|
158
|
+
sources: readonly InlineSource[],
|
|
159
|
+
): Promise<InlineSource[]> {
|
|
160
|
+
const parseable: InlineSource[] = [];
|
|
161
|
+
const probe = definePlugin({
|
|
162
|
+
name: "pragma-schema-artifact-probe",
|
|
163
|
+
onReady(ctx) {
|
|
164
|
+
for (const source of sources) {
|
|
165
|
+
try {
|
|
166
|
+
ctx.load(source.content, { format: source.format });
|
|
167
|
+
parseable.push(source);
|
|
168
|
+
} catch {
|
|
169
|
+
// Already warned at boot by the main store's graph loader.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const store = await createStore({ sources: [], plugins: [probe] });
|
|
176
|
+
store.dispose();
|
|
177
|
+
return parseable;
|
|
178
|
+
}
|
|
@@ -10,3 +10,8 @@ export type {
|
|
|
10
10
|
SchemaServer,
|
|
11
11
|
} from "./createSchemaServer.js";
|
|
12
12
|
export { default as createSchemaServer } from "./createSchemaServer.js";
|
|
13
|
+
export type { SchemaArtifactResult } from "./ensureSchemaArtifact.js";
|
|
14
|
+
export {
|
|
15
|
+
default as ensureSchemaArtifact,
|
|
16
|
+
resolveSchemaArtifactPath,
|
|
17
|
+
} from "./ensureSchemaArtifact.js";
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
|
|
9
9
|
function createUpgradeData(overrides: Partial<UpgradeData> = {}): UpgradeData {
|
|
10
10
|
return {
|
|
11
|
-
pm
|
|
11
|
+
// `pm` is the install label, which already carries the scope (as the
|
|
12
|
+
// command populates it from source.label).
|
|
13
|
+
pm: "bun (global)",
|
|
12
14
|
current: "0.18.0",
|
|
13
15
|
latest: "0.19.0",
|
|
14
16
|
command: "bun update -g @canonical/pragma-cli",
|
|
@@ -21,9 +23,10 @@ function createUpgradeData(overrides: Partial<UpgradeData> = {}): UpgradeData {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
describe("renderUpgradePlain", () => {
|
|
24
|
-
it("shows PM info", () => {
|
|
26
|
+
it("shows PM info without doubling the scope suffix", () => {
|
|
25
27
|
const output = renderUpgradePlain(createUpgradeData());
|
|
26
|
-
expect(output).toContain("bun (global)");
|
|
28
|
+
expect(output).toContain("Installed via: bun (global)");
|
|
29
|
+
expect(output).not.toContain("(global) (global)");
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
it("shows version transition", () => {
|
|
@@ -107,7 +110,7 @@ describe("renderUpgradeJson", () => {
|
|
|
107
110
|
it("produces valid JSON", () => {
|
|
108
111
|
const data = createUpgradeData();
|
|
109
112
|
const parsed = JSON.parse(renderUpgradeJson(data));
|
|
110
|
-
expect(parsed.pm).toBe("bun");
|
|
113
|
+
expect(parsed.pm).toBe("bun (global)");
|
|
111
114
|
expect(parsed.current).toBe("0.18.0");
|
|
112
115
|
expect(parsed.latest).toBe("0.19.0");
|
|
113
116
|
});
|
|
@@ -12,7 +12,9 @@ import type { UpgradeData } from "../types.js";
|
|
|
12
12
|
function renderUpgradePlain(data: UpgradeData): string {
|
|
13
13
|
const lines: string[] = [];
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// `data.pm` is the install label (e.g. "bun (global)"), which already carries
|
|
16
|
+
// the scope — do not append another "(global)".
|
|
17
|
+
lines.push(`Installed via: ${data.pm}`);
|
|
16
18
|
|
|
17
19
|
if (data.offline) {
|
|
18
20
|
lines.push("Could not reach registry");
|