@canonical/pragma-cli 0.36.0 → 0.37.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/dist/package.json +22 -19
- package/dist/pragma.conf.js +35 -5
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +1 -1
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +64 -13
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +59 -13
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +4 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +79 -30
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +12 -6
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +11 -7
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +23 -1
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +14 -9
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +9 -3
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/package.json +22 -19
- package/pragma.conf.ts +36 -5
- package/src/capabilities/capabilities/hints.ts +1 -1
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/lazy.test.ts +26 -2
- package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +83 -14
- package/src/capabilities/setup/operations/setupLsp.ts +95 -22
- package/src/capabilities/setup/operations/setupMcp.ts +7 -1
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/setup.test.ts +247 -2
- package/src/capabilities/setup/setup.verb.ts +83 -35
- package/src/capabilities/setup/targets.ts +18 -8
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/surface.test.ts +12 -4
- package/src/constants.ts +8 -0
- package/src/kernel/config/defaults.test.ts +17 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/packs/sparql/buildLookupQuery.ts +11 -7
- package/src/kernel/project/cli/dispatch.ts +23 -1
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +14 -12
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +9 -3
- package/src/kernel/runtime/types.ts +17 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle.ts — do not edit by hand.
|
|
2
|
+
// Regenerate from the packs pragma.conf.ts declares: `bun run bundle`.
|
|
3
|
+
export const manifestJson = "{\"name\":\"pragma\",\"version\":\"0.36.0\",\"sourceRef\":\"@canonical/design-system@git:ce00818852628857da9c4a67bf8556c7cfda287d, @canonical/anatomy-dsl@npm:0.3.0, @canonical/token-ontology@npm:0.8.1, @canonical/code-standards@git:fcd3ac2dea458775e5b79344b3237a3fcb37add9, @canonical/ds-implementations@self:v0.36.0\",\"contentHash\":\"7ff2bbd5fd059584ad3285c7b1c7d59c734efef8006fb0bba726db03254392e8\",\"prefixes\":{\"rdf\":\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\"rdfs\":\"http://www.w3.org/2000/01/rdf-schema#\",\"owl\":\"http://www.w3.org/2002/07/owl#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\",\"skos\":\"http://www.w3.org/2004/02/skos/core#\",\"sh\":\"http://www.w3.org/ns/shacl#\",\"dcterms\":\"http://purl.org/dc/terms/\",\"ds\":\"https://ds.canonical.com/\",\"anatomy\":\"http://anatomy-dsl.example.org/ontology#\",\"graphql\":\"https://pragma.canonical.com/graphql#\",\"w3c-tokens\":\"https://dt.canonical.com/w3c-tokens/\",\"dt\":\"https://dt.canonical.com/\",\"cs\":\"http://pragma.canonical.com/codestandards#\",\"dso\":\"https://ds.canonical.com/ontology#\"},\"createdAt\":\"2026-09-01T20:29:05.995Z\",\"tripleCount\":30346,\"entityCount\":2906}";
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
*
|
|
19
19
|
* The inlined strings are materialized into the ordinary content-addressed pack
|
|
20
20
|
* cache on first use and then read back through {@link readPack}, so the
|
|
21
|
-
* embedded pack and a built pack share one boot path. The story records
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* embedded pack and a built pack share one boot path. The story records, the
|
|
22
|
+
* index and the MANIFEST each live in their own generated module: every one of
|
|
23
|
+
* them is read on a path that does not need the n-quads, and leaving any of
|
|
24
|
+
* them beside `pack.generated.ts` would load its ~2.1 MB with them — a measured
|
|
25
|
+
* +28 ms on every invocation for the stories, and ~370 ms per CLI invocation
|
|
26
|
+
* for the manifest at a 30,340-triple pack. Inlining as JS strings
|
|
25
27
|
* (rather than file assets) means the pack travels with the emitted modules —
|
|
26
28
|
* it needs no asset step in any build, and no file lookup at run time.
|
|
27
29
|
*/
|
|
@@ -36,8 +38,8 @@ import {
|
|
|
36
38
|
} from "node:fs";
|
|
37
39
|
import { join } from "node:path";
|
|
38
40
|
import { packDir, packsCacheDir } from "../paths.js";
|
|
39
|
-
import { dataNq, manifestJson, schemaJson } from "./embedded/pack.generated.js";
|
|
40
41
|
import { indexJson } from "./embedded/pack.index.generated.js";
|
|
42
|
+
import { manifestJson } from "./embedded/pack.manifest.generated.js";
|
|
41
43
|
import { storiesJson } from "./embedded/pack.stories.generated.js";
|
|
42
44
|
import { packIsComplete } from "./manifest.js";
|
|
43
45
|
import { manifestSchema } from "./schemas.js";
|
|
@@ -72,10 +74,16 @@ export function embeddedManifest(): Manifest {
|
|
|
72
74
|
* @returns The pack directory, ready for {@link readPack}.
|
|
73
75
|
* @note Impure — writes the inlined pack files into the cache.
|
|
74
76
|
*/
|
|
75
|
-
export function materializeEmbeddedPack(): string {
|
|
77
|
+
export async function materializeEmbeddedPack(): Promise<string> {
|
|
76
78
|
const dir = packDir(embeddedManifest().contentHash);
|
|
77
79
|
if (packIsComplete(dir)) return dir;
|
|
78
80
|
|
|
81
|
+
// Loaded HERE, not at module scope: this is the only branch that needs the
|
|
82
|
+
// payload, and it is the branch that does NOT run once the pack is cached.
|
|
83
|
+
// A static import would parse ~2.1 MB of inlined n-quads on every boot to
|
|
84
|
+
// reach a return statement six lines above.
|
|
85
|
+
const { dataNq, schemaJson } = await import("./embedded/pack.generated.js");
|
|
86
|
+
|
|
79
87
|
mkdirSync(packsCacheDir(), { recursive: true });
|
|
80
88
|
const temp = mkdtempSync(join(packsCacheDir(), ".embed-"));
|
|
81
89
|
try {
|
|
@@ -144,15 +144,21 @@ describe("the committed embedded pack (PROTECTED)", () => {
|
|
|
144
144
|
// directory holds — which the next build then reuses, silently dropping the
|
|
145
145
|
// difference. Comparing the two directories catches that on the day it lands.
|
|
146
146
|
const built = await build([{ path: "a.ttl", content: TTL }]);
|
|
147
|
-
expect(readdirSync(materializeEmbeddedPack()).sort()).toEqual(
|
|
147
|
+
expect(readdirSync(await materializeEmbeddedPack()).sort()).toEqual(
|
|
148
148
|
readdirSync(built.dir).sort(),
|
|
149
149
|
);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
// 60s, not the 5s default. This test does real work twice: it BUILDS a
|
|
151
|
+
// pack from source, and it materialises the embedded one — which on a cold
|
|
152
|
+
// cache takes the miss branch and imports the multi-megabyte payload. Both
|
|
153
|
+
// costs grow with the pack, and the pack grows whenever a source is added.
|
|
154
|
+
// A default that a pack size can outgrow turns a real assertion into an
|
|
155
|
+
// intermittent one.
|
|
156
|
+
}, 60_000);
|
|
157
|
+
|
|
158
|
+
it("is self-consistent: complete, content-addressed, and non-empty", async () => {
|
|
153
159
|
// No network, so CI runs it: the committed strings really do materialize a
|
|
154
160
|
// bootable pack whose parts agree with each other.
|
|
155
|
-
const dir = materializeEmbeddedPack();
|
|
161
|
+
const dir = await materializeEmbeddedPack();
|
|
156
162
|
const manifest = embeddedManifest();
|
|
157
163
|
expect(packIsComplete(dir)).toBe(true);
|
|
158
164
|
expect(basename(dir)).toBe(manifest.contentHash);
|
|
@@ -162,6 +168,26 @@ describe("the committed embedded pack (PROTECTED)", () => {
|
|
|
162
168
|
expect(index.contentHash).toBe(manifest.contentHash);
|
|
163
169
|
expect(manifest.tripleCount ?? 0).toBeGreaterThan(0);
|
|
164
170
|
expect(manifest.entityCount ?? 0).toBeGreaterThan(0);
|
|
171
|
+
|
|
172
|
+
// EXACT, not "> 0", and it earns its keep twice.
|
|
173
|
+
//
|
|
174
|
+
// The manifest and the n-quads now live in SEPARATE generated modules, so
|
|
175
|
+
// a partial regeneration or a one-file `git add` can leave them describing
|
|
176
|
+
// different packs — a skew that could not exist while they shared a
|
|
177
|
+
// module. `read.ts` tolerates a superset deliberately, so it would serve
|
|
178
|
+
// the skew rather than refuse it.
|
|
179
|
+
//
|
|
180
|
+
// It also pins the equivalence `read.ts` depends on but cannot state:
|
|
181
|
+
// `store.size` (quads) against a `tripleCount` that `build.ts` computes
|
|
182
|
+
// with `SELECT (COUNT(*))` over the union graph. They agree on oxigraph
|
|
183
|
+
// 0.5.6; oxigraph is pinned by caret, so this is where a future version
|
|
184
|
+
// that deduplicates the union would be caught.
|
|
185
|
+
const session = await readPack(dir);
|
|
186
|
+
try {
|
|
187
|
+
expect(session.store.size).toBe(manifest.tripleCount);
|
|
188
|
+
} finally {
|
|
189
|
+
session.store.dispose();
|
|
190
|
+
}
|
|
165
191
|
});
|
|
166
192
|
});
|
|
167
193
|
|
|
@@ -80,15 +80,25 @@ export async function readPack(dir: string): Promise<StoreSession> {
|
|
|
80
80
|
// "populated index but empty store" check (via the now-empty dump,
|
|
81
81
|
// `packIsComplete` turns false so recovery rebuilds) rather than serving an
|
|
82
82
|
// empty graph as the pack.
|
|
83
|
-
|
|
83
|
+
// `store.size` is a field oxigraph maintains. The equivalent query —
|
|
84
|
+
// `SELECT (COUNT(*)) WHERE { ?s ?p ?o }`, which is what `build.ts` still runs
|
|
85
|
+
// to WRITE `tripleCount` — is a scan, and it ran on every boot to read a
|
|
86
|
+
// number the store already knew.
|
|
87
|
+
//
|
|
88
|
+
// The two agree: ke queries with `use_default_graph_as_union`, and oxigraph
|
|
89
|
+
// does not deduplicate across that union, so a quad count and the union's
|
|
90
|
+
// solution count are the same number even for a triple repeated across
|
|
91
|
+
// graphs. Verified against 0.5.6 rather than assumed — and the guard is `<`,
|
|
92
|
+
// which tolerates the direction a set-semantic union would drift.
|
|
93
|
+
const actualQuads = store.size;
|
|
84
94
|
if (manifest.tripleCount !== undefined) {
|
|
85
|
-
if (
|
|
95
|
+
if (actualQuads < manifest.tripleCount) {
|
|
86
96
|
store.dispose();
|
|
87
97
|
throw packUnavailable(
|
|
88
|
-
`The pack at ${dir} has a corrupt data cache (expected at least ${manifest.tripleCount} triples, loaded ${
|
|
98
|
+
`The pack at ${dir} has a corrupt data cache (expected at least ${manifest.tripleCount} triples, loaded ${actualQuads}).`,
|
|
89
99
|
);
|
|
90
100
|
}
|
|
91
|
-
} else if (index.entities.length > 0 &&
|
|
101
|
+
} else if (index.entities.length > 0 && actualQuads === 0) {
|
|
92
102
|
store.dispose();
|
|
93
103
|
throw packUnavailable(`The pack at ${dir} has a corrupt data cache.`);
|
|
94
104
|
}
|
|
@@ -101,11 +111,3 @@ export async function readPack(dir: string): Promise<StoreSession> {
|
|
|
101
111
|
index,
|
|
102
112
|
};
|
|
103
113
|
}
|
|
104
|
-
|
|
105
|
-
/** Count the booted store's triples (a cheap aggregate over the union graph). */
|
|
106
|
-
async function countTriples(store: StoreSession["store"]): Promise<number> {
|
|
107
|
-
const result = (await store.query(
|
|
108
|
-
"SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" as never,
|
|
109
|
-
)) as import("@canonical/ke").SelectResult;
|
|
110
|
-
return Number(result.bindings.at(0)?.n ?? 0);
|
|
111
|
-
}
|
|
@@ -21,7 +21,7 @@ export async function runStoreProbe(): Promise<string> {
|
|
|
21
21
|
import("./graphpack/embedded.js"),
|
|
22
22
|
import("./graphpack/read.js"),
|
|
23
23
|
]);
|
|
24
|
-
const session = await readPack(materializeEmbeddedPack());
|
|
24
|
+
const session = await readPack(await materializeEmbeddedPack());
|
|
25
25
|
try {
|
|
26
26
|
const result = await session.store.query(
|
|
27
27
|
"SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" as never,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { redactUrl } from "./parseRef.js";
|
|
2
|
+
import { parsePackDeclaration, redactUrl } from "./parseRef.js";
|
|
3
3
|
|
|
4
4
|
describe("redactUrl", () => {
|
|
5
5
|
it("strips user:password userinfo so an inlined token never reaches output", () => {
|
|
@@ -32,3 +32,51 @@ describe("redactUrl", () => {
|
|
|
32
32
|
);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
describe("parsePackDeclaration — a git source may name a subdirectory", () => {
|
|
37
|
+
const url = "git+https://github.com/org/repo.git";
|
|
38
|
+
|
|
39
|
+
it("reaches a package nested in a monorepo", () => {
|
|
40
|
+
expect(
|
|
41
|
+
parsePackDeclaration({
|
|
42
|
+
name: "@org/thing",
|
|
43
|
+
source: `${url}#main:pkgs/thing`,
|
|
44
|
+
}),
|
|
45
|
+
).toEqual({
|
|
46
|
+
kind: "git",
|
|
47
|
+
pkg: "@org/thing",
|
|
48
|
+
url: "https://github.com/org/repo.git",
|
|
49
|
+
ref: "main",
|
|
50
|
+
subdir: "pkgs/thing",
|
|
51
|
+
source: `${url}#main:pkgs/thing`,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("leaves a plain ref untouched, with no subdir key", () => {
|
|
56
|
+
const ref = parsePackDeclaration({
|
|
57
|
+
name: "@org/thing",
|
|
58
|
+
source: `${url}#main`,
|
|
59
|
+
});
|
|
60
|
+
expect(ref).toEqual({
|
|
61
|
+
kind: "git",
|
|
62
|
+
pkg: "@org/thing",
|
|
63
|
+
url: "https://github.com/org/repo.git",
|
|
64
|
+
ref: "main",
|
|
65
|
+
source: `${url}#main`,
|
|
66
|
+
});
|
|
67
|
+
expect("subdir" in ref).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("splits on the FIRST colon, so a nested path survives", () => {
|
|
71
|
+
// The ref cannot contain a colon; the subdirectory can contain slashes.
|
|
72
|
+
expect(
|
|
73
|
+
parsePackDeclaration({ name: "@org/t", source: `${url}#v1.2:a/b/c` }),
|
|
74
|
+
).toMatchObject({ ref: "v1.2", subdir: "a/b/c" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("rejects an empty subdirectory rather than silently ignoring it", () => {
|
|
78
|
+
expect(() =>
|
|
79
|
+
parsePackDeclaration({ name: "@org/thing", source: `${url}#main:` }),
|
|
80
|
+
).toThrow(/subdirectory after : is empty/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -25,6 +25,8 @@ export type PackageRef =
|
|
|
25
25
|
readonly pkg: string;
|
|
26
26
|
readonly url: string;
|
|
27
27
|
readonly ref: string;
|
|
28
|
+
/** Package root WITHIN the clone, for a package nested in a monorepo. */
|
|
29
|
+
readonly subdir?: string;
|
|
28
30
|
readonly source: string;
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -72,11 +74,33 @@ export function parsePackDeclaration(entry: PackDeclaration): PackageRef {
|
|
|
72
74
|
},
|
|
73
75
|
);
|
|
74
76
|
}
|
|
77
|
+
// `#<ref>` or `#<ref>:<subdir>`. A git ref cannot contain a colon
|
|
78
|
+
// (git check-ref-format), so the split is unambiguous.
|
|
79
|
+
const fragment = source.slice(hashIdx + 1);
|
|
80
|
+
const colonIdx = fragment.indexOf(":");
|
|
81
|
+
if (colonIdx !== -1 && colonIdx === fragment.length - 1) {
|
|
82
|
+
throw PragmaError.configError(
|
|
83
|
+
`Invalid source for "${name}": the subdirectory after : is empty.`,
|
|
84
|
+
{
|
|
85
|
+
recovery: {
|
|
86
|
+
message:
|
|
87
|
+
"Example: git+https://github.com/org/repo.git#main:packages/thing",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const ref = colonIdx === -1 ? fragment : fragment.slice(0, colonIdx);
|
|
93
|
+
if (ref.length === 0) {
|
|
94
|
+
throw PragmaError.configError(
|
|
95
|
+
`Invalid source for "${name}": git URL must include a ref after #.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
75
98
|
return {
|
|
76
99
|
kind: "git",
|
|
77
100
|
pkg: name,
|
|
78
101
|
url: source.slice("git+".length, hashIdx),
|
|
79
|
-
ref
|
|
102
|
+
ref,
|
|
103
|
+
...(colonIdx === -1 ? {} : { subdir: fragment.slice(colonIdx + 1) }),
|
|
80
104
|
source,
|
|
81
105
|
};
|
|
82
106
|
}
|
|
@@ -383,13 +383,19 @@ export async function resolvePackage(
|
|
|
383
383
|
},
|
|
384
384
|
);
|
|
385
385
|
}
|
|
386
|
+
// The clone lives at `dir`; the PACKAGE may be nested within it. A git
|
|
387
|
+
// source names a repository, so a package inside a monorepo is reached
|
|
388
|
+
// by its subdirectory — without which `readTtlSources` would scan a
|
|
389
|
+
// repository root that has no `definitions/` or `data/` and contribute
|
|
390
|
+
// nothing at all.
|
|
391
|
+
const packageRoot = ref.subdir ? join(dir, ref.subdir) : dir;
|
|
386
392
|
return {
|
|
387
393
|
name: ref.pkg,
|
|
388
394
|
source: ref.source,
|
|
389
395
|
resolved,
|
|
390
|
-
root:
|
|
391
|
-
sources: readTtlSources(
|
|
392
|
-
stories: readStorySources(
|
|
396
|
+
root: packageRoot,
|
|
397
|
+
sources: readTtlSources(packageRoot, ref.pkg),
|
|
398
|
+
stories: readStorySources(packageRoot, ref.pkg),
|
|
393
399
|
};
|
|
394
400
|
}
|
|
395
401
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* that are actually populated by the time it runs.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type { UndoOutcome } from "@canonical/task/node";
|
|
11
12
|
import type { DetailLevel, OutputFormat } from "../../constants.js";
|
|
12
13
|
import type { ConfigLayers } from "../config/types.js";
|
|
13
14
|
|
|
@@ -221,4 +222,20 @@ export interface PragmaRuntime {
|
|
|
221
222
|
* plan of its own, which is what keeps the raw-effect render the default.
|
|
222
223
|
*/
|
|
223
224
|
planData?: unknown;
|
|
225
|
+
/**
|
|
226
|
+
* The post-undo projection seam. `--undo` is two-phase: the undo
|
|
227
|
+
* interpreter WALKS the verb's task with every effect mocked to collect the
|
|
228
|
+
* reversals, and only then executes what it collected — so nothing the task
|
|
229
|
+
* body says while it is interpreted describes what the reversals did (the
|
|
230
|
+
* body only ever runs under the mocked walk). What DOES describe them is
|
|
231
|
+
* the interpreter's own per-undo outcomes, correlated by the `undoKey` each
|
|
232
|
+
* reversal was declared with. A verb that reports per-target results sets
|
|
233
|
+
* this (the same way it sets `exec`/`planData`, before returning its Task);
|
|
234
|
+
* the projector's undo branch awaits it with those outcomes AFTER the
|
|
235
|
+
* collected undos executed and before the `Undid N step(s).` line, so the
|
|
236
|
+
* verb projects them onto its own rows through the same path its forward
|
|
237
|
+
* run uses. A throw here is rendered as the run's error (nonzero exit) —
|
|
238
|
+
* the honest outcome when a reversal did not take.
|
|
239
|
+
*/
|
|
240
|
+
undoReport?: (outcomes: readonly UndoOutcome[]) => void | Promise<void>;
|
|
224
241
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins how `runCli` reports a child that does not finish.
|
|
3
|
+
*
|
|
4
|
+
* `spawnSync` reports a timeout the same way it reports a failure to spawn —
|
|
5
|
+
* through `error` — and a caller that does not look will see only
|
|
6
|
+
* `status: null`, which reads downstream as an ordinary non-zero exit or, worse,
|
|
7
|
+
* as a content mismatch. These tests hold the boundary where that distinction
|
|
8
|
+
* is made, because the cost of losing it is paid in diagnosis rather than in a
|
|
9
|
+
* red test.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { runCli } from "./runCli.js";
|
|
14
|
+
|
|
15
|
+
describe("runCli — a child that does not finish", () => {
|
|
16
|
+
it("reports a timeout as a timeout, with the duration", async () => {
|
|
17
|
+
// `--help` on a 1ms budget cannot complete; any real CLI start-up exceeds it.
|
|
18
|
+
await expect(async () =>
|
|
19
|
+
runCli(["--help"], { timeoutMs: 1 }),
|
|
20
|
+
).rejects.toThrow(/timed out after 1ms/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("names the signal that killed it, so a hang is distinguishable", async () => {
|
|
24
|
+
await expect(async () =>
|
|
25
|
+
runCli(["--help"], { timeoutMs: 1 }),
|
|
26
|
+
).rejects.toThrow(/killed by SIG/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does not describe a timeout as a failure to spawn", async () => {
|
|
30
|
+
// The previous message said "failed to spawn", which sent a reader looking
|
|
31
|
+
// for a missing binary or a bad path instead of a slow or hung command.
|
|
32
|
+
await expect(async () =>
|
|
33
|
+
runCli(["--help"], { timeoutMs: 1 }),
|
|
34
|
+
).rejects.not.toThrow(/failed to spawn/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("attaches whatever the child managed to write", async () => {
|
|
38
|
+
// Empty streams are still reported explicitly: "(empty)" is evidence, an
|
|
39
|
+
// absent section is an omission the reader has to interpret.
|
|
40
|
+
await expect(async () =>
|
|
41
|
+
runCli(["--help"], { timeoutMs: 1 }),
|
|
42
|
+
).rejects.toThrow(/stdout:[\s\S]*stderr:/);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -46,10 +46,17 @@ export interface RunCliOptions {
|
|
|
46
46
|
* test the shipped-entry boundary itself.
|
|
47
47
|
*/
|
|
48
48
|
readonly mode?: "shipped" | "source";
|
|
49
|
-
/** Spawn timeout in milliseconds (default
|
|
49
|
+
/** Spawn timeout in milliseconds (default {@link DEFAULT_TIMEOUT_MS}). */
|
|
50
50
|
readonly timeoutMs?: number;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* How long a spawned CLI may run before it is killed. Twenty seconds is far
|
|
55
|
+
* past any assertion these tests make; a run that reaches it has hung, not
|
|
56
|
+
* been slow.
|
|
57
|
+
*/
|
|
58
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
59
|
+
|
|
53
60
|
/** The captured outcome of a `runCli` invocation. */
|
|
54
61
|
export interface RunCliResult {
|
|
55
62
|
readonly stdout: string;
|
|
@@ -114,12 +121,29 @@ export function runCli(
|
|
|
114
121
|
cwd: options.cwd,
|
|
115
122
|
env: { ...env, ...options.env },
|
|
116
123
|
encoding: "utf-8",
|
|
117
|
-
timeout: options.timeoutMs ??
|
|
124
|
+
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
118
125
|
});
|
|
119
126
|
|
|
120
127
|
if (result.error) {
|
|
128
|
+
// A timeout is not a spawn failure, and saying so costs real diagnosis
|
|
129
|
+
// time: `spawnSync` reports both through `error`, but a timed-out child
|
|
130
|
+
// has already produced output worth seeing, and the duration is the whole
|
|
131
|
+
// finding. Separated here so the message names what happened, and carries
|
|
132
|
+
// the evidence rather than making the reader go and get it.
|
|
133
|
+
const timedOutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
134
|
+
const invocation = `${command} ${spawnArgs.join(" ")}`;
|
|
135
|
+
const captured = [
|
|
136
|
+
result.stdout ? `stdout:\n${result.stdout}` : "stdout: (empty)",
|
|
137
|
+
result.stderr ? `stderr:\n${result.stderr}` : "stderr: (empty)",
|
|
138
|
+
].join("\n");
|
|
139
|
+
|
|
140
|
+
if ((result.error as NodeJS.ErrnoException).code === "ETIMEDOUT") {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`runCli: ${invocation} timed out after ${timedOutMs}ms (killed by ${result.signal ?? "signal"}).\n${captured}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
121
145
|
throw new Error(
|
|
122
|
-
`runCli: failed to spawn ${
|
|
146
|
+
`runCli: failed to spawn ${invocation} — ${result.error.message}.\n${captured}`,
|
|
123
147
|
);
|
|
124
148
|
}
|
|
125
149
|
|