@canonical/pragma-cli 0.24.0 → 0.26.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/pragma +0 -0
- package/package.json +12 -12
- package/src/config/readConfig.test.ts +152 -0
- package/src/config/readConfig.ts +63 -1
- package/src/config/types.ts +9 -0
- package/src/config/writeConfig.ts +6 -0
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +76 -0
- package/src/domains/doctor/operations/checks/index.ts +1 -0
- package/src/domains/doctor/operations/runChecks.test.ts +11 -5
- package/src/domains/doctor/operations/runChecks.ts +2 -0
- package/src/domains/info/formatters/info.test.ts +34 -0
- package/src/domains/info/formatters/info.ts +15 -0
- package/src/domains/info/operations/collectInfo.ts +30 -1
- package/src/domains/info/types.ts +8 -0
- package/src/domains/refs/commands/updateRefs.ts +52 -0
- package/src/domains/refs/formatters/updateRefs.ts +27 -0
- package/src/domains/refs/index.ts +27 -0
- package/src/domains/refs/operations/gitOps.test.ts +155 -0
- package/src/domains/refs/operations/gitOps.ts +92 -0
- package/src/domains/refs/operations/index.ts +10 -0
- package/src/domains/refs/operations/parseRef.test.ts +157 -0
- package/src/domains/refs/operations/parseRef.ts +114 -0
- package/src/domains/refs/operations/paths.test.ts +98 -0
- package/src/domains/refs/operations/paths.ts +63 -0
- package/src/domains/refs/operations/readGlobalRefs.test.ts +63 -0
- package/src/domains/refs/operations/readGlobalRefs.ts +37 -0
- package/src/domains/refs/operations/updateRefs.test.ts +164 -0
- package/src/domains/refs/operations/updateRefs.ts +215 -0
- package/src/domains/shared/bootStore.ts +31 -15
- package/src/domains/shared/packages.ts +99 -49
- package/src/domains/shared/runtime.ts +73 -3
- package/src/domains/skill/helpers/resolveSkillSources.ts +16 -12
- package/src/pipeline/resolveCommandKind.ts +1 -1
- package/src/pipeline/runCli.ts +2 -1
- package/src/testing/integration/boot.test.ts +38 -0
package/dist/pragma
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonical/pragma-cli",
|
|
3
3
|
"description": "CLI and MCP server for Canonical's design system.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.26.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@biomejs/biome": "2.4.9",
|
|
60
|
-
"@canonical/biome-config": "^0.
|
|
61
|
-
"@canonical/typescript-config": "^0.
|
|
62
|
-
"@canonical/webarchitect": "^0.
|
|
60
|
+
"@canonical/biome-config": "^0.26.0",
|
|
61
|
+
"@canonical/typescript-config": "^0.26.0",
|
|
62
|
+
"@canonical/webarchitect": "^0.26.0",
|
|
63
63
|
"@types/bun": "^1.3.10",
|
|
64
64
|
"@types/node": "^24.12.0",
|
|
65
65
|
"@types/react": "^19.2.14",
|
|
@@ -73,13 +73,13 @@
|
|
|
73
73
|
"@canonical/design-system": "^0.1.2"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@canonical/cli-core": "^0.
|
|
77
|
-
"@canonical/harnesses": "^0.
|
|
78
|
-
"@canonical/ke": "^0.
|
|
79
|
-
"@canonical/summon-component": "^0.
|
|
80
|
-
"@canonical/summon-core": "^0.
|
|
81
|
-
"@canonical/summon-package": "^0.
|
|
82
|
-
"@canonical/task": "^0.
|
|
76
|
+
"@canonical/cli-core": "^0.26.0",
|
|
77
|
+
"@canonical/harnesses": "^0.26.0",
|
|
78
|
+
"@canonical/ke": "^0.26.0",
|
|
79
|
+
"@canonical/summon-component": "^0.26.0",
|
|
80
|
+
"@canonical/summon-core": "^0.26.0",
|
|
81
|
+
"@canonical/summon-package": "^0.26.0",
|
|
82
|
+
"@canonical/task": "^0.26.0",
|
|
83
83
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
84
84
|
"chalk": "^5.6.2",
|
|
85
85
|
"commander": "^14.0.3",
|
|
@@ -88,5 +88,5 @@
|
|
|
88
88
|
"react": "^19.2.4",
|
|
89
89
|
"zod": "^3.25.0"
|
|
90
90
|
},
|
|
91
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "55fece1380dba8dcd3a88af443dd509d9aaaccb2"
|
|
92
92
|
}
|
|
@@ -73,6 +73,105 @@ describe("readConfig", () => {
|
|
|
73
73
|
const config = readConfig(dir);
|
|
74
74
|
expect(config.tier).toBeUndefined();
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
describe("packages field", () => {
|
|
78
|
+
it("parses string-only packages array", () => {
|
|
79
|
+
writeFileSync(
|
|
80
|
+
join(dir, "pragma.config.json"),
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
packages: ["@canonical/design-system", "@canonical/code-standards"],
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
const config = readConfig(dir);
|
|
86
|
+
expect(config.packages).toEqual([
|
|
87
|
+
"@canonical/design-system",
|
|
88
|
+
"@canonical/code-standards",
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("parses mixed string and object packages", () => {
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(dir, "pragma.config.json"),
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
packages: [
|
|
97
|
+
"@canonical/design-system",
|
|
98
|
+
{
|
|
99
|
+
name: "@canonical/code-standards",
|
|
100
|
+
source: "file:///home/user/code/code-standards",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
const config = readConfig(dir);
|
|
106
|
+
expect(config.packages).toEqual([
|
|
107
|
+
"@canonical/design-system",
|
|
108
|
+
{
|
|
109
|
+
name: "@canonical/code-standards",
|
|
110
|
+
source: "file:///home/user/code/code-standards",
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("parses object without source as npm", () => {
|
|
116
|
+
writeFileSync(
|
|
117
|
+
join(dir, "pragma.config.json"),
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
packages: [{ name: "@canonical/design-system" }],
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
const config = readConfig(dir);
|
|
123
|
+
expect(config.packages).toEqual([{ name: "@canonical/design-system" }]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns no packages field when absent", () => {
|
|
127
|
+
writeFileSync(join(dir, "pragma.config.json"), '{"tier":"global"}');
|
|
128
|
+
const config = readConfig(dir);
|
|
129
|
+
expect(config.packages).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("throws on non-array packages", () => {
|
|
133
|
+
writeFileSync(
|
|
134
|
+
join(dir, "pragma.config.json"),
|
|
135
|
+
'{"packages":"not-an-array"}',
|
|
136
|
+
);
|
|
137
|
+
expect(() => readConfig(dir)).toThrow(PragmaError);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("throws on object entry with empty name", () => {
|
|
141
|
+
writeFileSync(
|
|
142
|
+
join(dir, "pragma.config.json"),
|
|
143
|
+
JSON.stringify({ packages: [{ name: "" }] }),
|
|
144
|
+
);
|
|
145
|
+
expect(() => readConfig(dir)).toThrow(PragmaError);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("throws on invalid source scheme", () => {
|
|
149
|
+
writeFileSync(
|
|
150
|
+
join(dir, "pragma.config.json"),
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
packages: [
|
|
153
|
+
{ name: "@canonical/ds", source: "https://example.com/foo.tar.gz" },
|
|
154
|
+
],
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
expect(() => readConfig(dir)).toThrow(PragmaError);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("throws on git source without ref", () => {
|
|
161
|
+
writeFileSync(
|
|
162
|
+
join(dir, "pragma.config.json"),
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
packages: [
|
|
165
|
+
{
|
|
166
|
+
name: "@canonical/ds",
|
|
167
|
+
source: "git+https://github.com/canonical/ds.git",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
expect(() => readConfig(dir)).toThrow(PragmaError);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
76
175
|
});
|
|
77
176
|
|
|
78
177
|
describe("writeConfig", () => {
|
|
@@ -149,4 +248,57 @@ describe("writeConfig", () => {
|
|
|
149
248
|
writeFileSync(join(dir, "pragma.config.json"), "{invalid json");
|
|
150
249
|
expect(() => writeConfig(dir, { tier: "apps" })).toThrow();
|
|
151
250
|
});
|
|
251
|
+
|
|
252
|
+
describe("packages field", () => {
|
|
253
|
+
it("writes packages array", () => {
|
|
254
|
+
writeConfig(dir, {
|
|
255
|
+
packages: [
|
|
256
|
+
"@canonical/design-system",
|
|
257
|
+
{
|
|
258
|
+
name: "@canonical/code-standards",
|
|
259
|
+
source: "file:///home/user/code/code-standards",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
const config = readConfig(dir);
|
|
264
|
+
expect(config.packages).toEqual([
|
|
265
|
+
"@canonical/design-system",
|
|
266
|
+
{
|
|
267
|
+
name: "@canonical/code-standards",
|
|
268
|
+
source: "file:///home/user/code/code-standards",
|
|
269
|
+
},
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("merges packages into existing config without touching tier", () => {
|
|
274
|
+
writeFileSync(join(dir, "pragma.config.json"), '{"tier":"global"}');
|
|
275
|
+
writeConfig(dir, { packages: ["@canonical/design-system"] });
|
|
276
|
+
const config = readConfig(dir);
|
|
277
|
+
expect(config.tier).toBe("global");
|
|
278
|
+
expect(config.packages).toEqual(["@canonical/design-system"]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("removes packages when set to undefined", () => {
|
|
282
|
+
writeFileSync(
|
|
283
|
+
join(dir, "pragma.config.json"),
|
|
284
|
+
JSON.stringify({ packages: ["@canonical/design-system"] }),
|
|
285
|
+
);
|
|
286
|
+
writeConfig(dir, { packages: undefined });
|
|
287
|
+
const config = readConfig(dir);
|
|
288
|
+
expect(config.packages).toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("round-trips packages through write then read", () => {
|
|
292
|
+
const packages = [
|
|
293
|
+
"@canonical/design-system",
|
|
294
|
+
{
|
|
295
|
+
name: "@canonical/anatomy-dsl",
|
|
296
|
+
source: "git+https://github.com/canonical/anatomy-dsl.git#main",
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
writeConfig(dir, { packages });
|
|
300
|
+
const config = readConfig(dir);
|
|
301
|
+
expect(config.packages).toEqual(packages);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
152
304
|
});
|
package/src/config/readConfig.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { type Channel, VALID_CHANNELS } from "../constants.js";
|
|
3
|
+
import {
|
|
4
|
+
parsePackageEntry,
|
|
5
|
+
type RawPackageEntry,
|
|
6
|
+
} from "../domains/refs/operations/parseRef.js";
|
|
3
7
|
import { PragmaError } from "../error/PragmaError.js";
|
|
4
8
|
import resolveConfigPath from "./resolveConfigPath.js";
|
|
5
9
|
import type { PragmaConfig } from "./types.js";
|
|
@@ -60,5 +64,63 @@ export default function readConfig(cwd: string = process.cwd()): PragmaConfig {
|
|
|
60
64
|
channel = parsed.channel;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
const packages = parsePackagesField(parsed.packages);
|
|
68
|
+
|
|
69
|
+
return packages ? { tier, channel, packages } : { tier, channel };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Packages field parsing
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate and parse the `packages` config field.
|
|
78
|
+
*
|
|
79
|
+
* @returns The validated array, or `undefined` when the field is absent.
|
|
80
|
+
* @throws PragmaError if the field is present but malformed.
|
|
81
|
+
*/
|
|
82
|
+
function parsePackagesField(
|
|
83
|
+
raw: unknown,
|
|
84
|
+
): ReadonlyArray<RawPackageEntry> | undefined {
|
|
85
|
+
if (raw === undefined || raw === null) return undefined;
|
|
86
|
+
|
|
87
|
+
if (!Array.isArray(raw)) {
|
|
88
|
+
throw PragmaError.configError(
|
|
89
|
+
'"packages" must be an array of strings or { name, source? } objects.',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const entries: RawPackageEntry[] = [];
|
|
94
|
+
for (const item of raw) {
|
|
95
|
+
if (typeof item === "string") {
|
|
96
|
+
entries.push(item);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
100
|
+
const obj = item as Record<string, unknown>;
|
|
101
|
+
if (typeof obj.name !== "string" || obj.name.length === 0) {
|
|
102
|
+
throw PragmaError.configError(
|
|
103
|
+
'Each object in "packages" must have a non-empty "name" string.',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const entry: { name: string; source?: string } = { name: obj.name };
|
|
107
|
+
if (obj.source !== undefined) {
|
|
108
|
+
if (typeof obj.source !== "string") {
|
|
109
|
+
throw PragmaError.configError(
|
|
110
|
+
`Invalid source for "${obj.name}": expected a string.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
entry.source = obj.source;
|
|
114
|
+
}
|
|
115
|
+
// Validate format eagerly — fail fast on bad config
|
|
116
|
+
parsePackageEntry(entry);
|
|
117
|
+
entries.push(entry);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
throw PragmaError.configError(
|
|
121
|
+
'Each entry in "packages" must be a string or { name, source? } object.',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return entries;
|
|
64
126
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Channel } from "../constants.js";
|
|
9
|
+
import type { RawPackageEntry } from "../domains/refs/operations/parseRef.js";
|
|
9
10
|
|
|
10
11
|
/** Parsed contents of pragma.config.json. */
|
|
11
12
|
interface PragmaConfig {
|
|
@@ -13,6 +14,12 @@ interface PragmaConfig {
|
|
|
13
14
|
tier: string | undefined;
|
|
14
15
|
/** Release channel controlling component visibility. */
|
|
15
16
|
channel: Channel;
|
|
17
|
+
/**
|
|
18
|
+
* Semantic package sources. Each entry is a package name (npm) or an
|
|
19
|
+
* object with `{ name, source }` where source is `file://` or `git+https://`.
|
|
20
|
+
* When absent, the hardcoded defaults are used.
|
|
21
|
+
*/
|
|
22
|
+
packages?: ReadonlyArray<RawPackageEntry> | undefined;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/** Partial update payload for writing config changes. */
|
|
@@ -21,6 +28,8 @@ interface ConfigUpdate {
|
|
|
21
28
|
tier?: string | undefined;
|
|
22
29
|
/** New channel, `undefined` to remove the channel field. */
|
|
23
30
|
channel?: Channel | undefined;
|
|
31
|
+
/** New packages list, `undefined` to remove the field. */
|
|
32
|
+
packages?: ReadonlyArray<RawPackageEntry> | undefined;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
export type { ConfigUpdate, PragmaConfig };
|
|
@@ -44,6 +44,12 @@ export default function writeConfig(cwd: string, update: ConfigUpdate): void {
|
|
|
44
44
|
delete existing.channel;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
if (update.packages !== undefined) {
|
|
48
|
+
existing.packages = update.packages;
|
|
49
|
+
} else if ("packages" in update) {
|
|
50
|
+
delete existing.packages;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
writeFileSync(tempPath, `${JSON.stringify(existing, null, 2)}\n`);
|
|
48
54
|
renameSync(tempPath, configPath);
|
|
49
55
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor check: report how each semantic package is resolved.
|
|
3
|
+
*
|
|
4
|
+
* Shows whether packages resolve from npm, file://, or git cache,
|
|
5
|
+
* and warns if a configured ref is missing its cache.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { readConfig } from "#config";
|
|
10
|
+
import {
|
|
11
|
+
parsePackageEntry,
|
|
12
|
+
type RawPackageEntry,
|
|
13
|
+
} from "../../../refs/operations/parseRef.js";
|
|
14
|
+
import { gitCacheDir } from "../../../refs/operations/paths.js";
|
|
15
|
+
import readGlobalRefs from "../../../refs/operations/readGlobalRefs.js";
|
|
16
|
+
import { DEFAULT_PACKAGES } from "../../../shared/packages.js";
|
|
17
|
+
import type { CheckContext, CheckResult } from "../types.js";
|
|
18
|
+
|
|
19
|
+
export default async function checkPackageRefs(
|
|
20
|
+
ctx: CheckContext,
|
|
21
|
+
): Promise<CheckResult> {
|
|
22
|
+
const config = readConfig(ctx.cwd);
|
|
23
|
+
const globalEntries = readGlobalRefs();
|
|
24
|
+
|
|
25
|
+
const hasProjectRefs = config.packages && config.packages.length > 0;
|
|
26
|
+
const hasGlobalRefs = globalEntries.length > 0;
|
|
27
|
+
|
|
28
|
+
if (!hasProjectRefs && !hasGlobalRefs) {
|
|
29
|
+
return {
|
|
30
|
+
name: "package refs",
|
|
31
|
+
status: "pass",
|
|
32
|
+
detail: `${DEFAULT_PACKAGES.length} packages (all npm, defaults)`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const entries: ReadonlyArray<RawPackageEntry> =
|
|
37
|
+
config.packages ?? globalEntries;
|
|
38
|
+
|
|
39
|
+
const details: string[] = [];
|
|
40
|
+
let hasIssue = false;
|
|
41
|
+
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const ref = parsePackageEntry(entry);
|
|
44
|
+
|
|
45
|
+
switch (ref.kind) {
|
|
46
|
+
case "npm":
|
|
47
|
+
details.push(`${ref.pkg}: npm`);
|
|
48
|
+
break;
|
|
49
|
+
case "file":
|
|
50
|
+
if (existsSync(ref.path)) {
|
|
51
|
+
details.push(`${ref.pkg}: file (${ref.path})`);
|
|
52
|
+
} else {
|
|
53
|
+
details.push(`${ref.pkg}: file MISSING (${ref.path})`);
|
|
54
|
+
hasIssue = true;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
case "git": {
|
|
58
|
+
const cached = existsSync(gitCacheDir(ref.pkg, ref.ref));
|
|
59
|
+
if (cached) {
|
|
60
|
+
details.push(`${ref.pkg}: git #${ref.ref} (cached)`);
|
|
61
|
+
} else {
|
|
62
|
+
details.push(`${ref.pkg}: git #${ref.ref} NOT CACHED`);
|
|
63
|
+
hasIssue = true;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
name: "package refs",
|
|
72
|
+
status: hasIssue ? "fail" : "pass",
|
|
73
|
+
detail: details.join("; "),
|
|
74
|
+
...(hasIssue ? { remedy: "pragma update-refs" } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -3,6 +3,7 @@ export { default as checkConfigFile } from "./checkConfigFile.js";
|
|
|
3
3
|
export { default as checkKeStore } from "./checkKeStore.js";
|
|
4
4
|
export { default as checkMcpConfigured } from "./checkMcpConfigured.js";
|
|
5
5
|
export { default as checkNodeVersion } from "./checkNodeVersion.js";
|
|
6
|
+
export { default as checkPackageRefs } from "./checkPackageRefs.js";
|
|
6
7
|
export { default as checkPragmaVersion } from "./checkPragmaVersion.js";
|
|
7
8
|
export { default as checkShellCompletions } from "./checkShellCompletions.js";
|
|
8
9
|
export { default as checkSkillsSymlinked } from "./checkSkillsSymlinked.js";
|
|
@@ -4,6 +4,7 @@ vi.mock("./checks/index.js", () => ({
|
|
|
4
4
|
checkNodeVersion: vi.fn(),
|
|
5
5
|
checkPragmaVersion: vi.fn(),
|
|
6
6
|
checkConfigFile: vi.fn(),
|
|
7
|
+
checkPackageRefs: vi.fn(),
|
|
7
8
|
checkKeStore: vi.fn(),
|
|
8
9
|
checkShellCompletions: vi.fn(),
|
|
9
10
|
checkMcpConfigured: vi.fn(),
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
checkKeStore,
|
|
16
17
|
checkMcpConfigured,
|
|
17
18
|
checkNodeVersion,
|
|
19
|
+
checkPackageRefs,
|
|
18
20
|
checkPragmaVersion,
|
|
19
21
|
checkShellCompletions,
|
|
20
22
|
checkSkillsSymlinked,
|
|
@@ -43,29 +45,31 @@ describe("runChecks", () => {
|
|
|
43
45
|
vi.mocked(checkNodeVersion).mockResolvedValue(pass("Node"));
|
|
44
46
|
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
45
47
|
vi.mocked(checkConfigFile).mockResolvedValue(pass("config"));
|
|
48
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
46
49
|
vi.mocked(checkKeStore).mockResolvedValue(pass("store"));
|
|
47
50
|
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
48
51
|
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
49
52
|
vi.mocked(checkSkillsSymlinked).mockResolvedValue(pass("skills"));
|
|
50
53
|
|
|
51
54
|
const data = await runChecks({ cwd: "/test" });
|
|
52
|
-
expect(data.passed).toBe(
|
|
55
|
+
expect(data.passed).toBe(8);
|
|
53
56
|
expect(data.failed).toBe(0);
|
|
54
57
|
expect(data.skipped).toBe(0);
|
|
55
|
-
expect(data.checks).toHaveLength(
|
|
58
|
+
expect(data.checks).toHaveLength(8);
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
it("counts failures and skips correctly", async () => {
|
|
59
62
|
vi.mocked(checkNodeVersion).mockResolvedValue(pass("Node"));
|
|
60
63
|
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
61
64
|
vi.mocked(checkConfigFile).mockResolvedValue(fail("config"));
|
|
65
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
62
66
|
vi.mocked(checkKeStore).mockResolvedValue(fail("store"));
|
|
63
67
|
vi.mocked(checkShellCompletions).mockResolvedValue(fail("completions"));
|
|
64
68
|
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
65
69
|
vi.mocked(checkSkillsSymlinked).mockResolvedValue(skip("skills"));
|
|
66
70
|
|
|
67
71
|
const data = await runChecks({ cwd: "/test" });
|
|
68
|
-
expect(data.passed).toBe(
|
|
72
|
+
expect(data.passed).toBe(4);
|
|
69
73
|
expect(data.failed).toBe(3);
|
|
70
74
|
expect(data.skipped).toBe(1);
|
|
71
75
|
});
|
|
@@ -74,6 +78,7 @@ describe("runChecks", () => {
|
|
|
74
78
|
vi.mocked(checkNodeVersion).mockResolvedValue(pass("Node"));
|
|
75
79
|
vi.mocked(checkPragmaVersion).mockResolvedValue(pass("pragma"));
|
|
76
80
|
vi.mocked(checkConfigFile).mockResolvedValue(pass("config"));
|
|
81
|
+
vi.mocked(checkPackageRefs).mockResolvedValue(pass("refs"));
|
|
77
82
|
vi.mocked(checkKeStore).mockResolvedValue(pass("store"));
|
|
78
83
|
vi.mocked(checkShellCompletions).mockResolvedValue(pass("completions"));
|
|
79
84
|
vi.mocked(checkMcpConfigured).mockResolvedValue(pass("mcp"));
|
|
@@ -83,7 +88,8 @@ describe("runChecks", () => {
|
|
|
83
88
|
expect(data.checks[0].name).toBe("Node");
|
|
84
89
|
expect(data.checks[1].name).toBe("pragma");
|
|
85
90
|
expect(data.checks[2].name).toBe("config");
|
|
86
|
-
expect(data.checks[3].name).toBe("
|
|
87
|
-
expect(data.checks[
|
|
91
|
+
expect(data.checks[3].name).toBe("refs");
|
|
92
|
+
expect(data.checks[4].name).toBe("store");
|
|
93
|
+
expect(data.checks[7].name).toBe("skills");
|
|
88
94
|
});
|
|
89
95
|
});
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
checkKeStore,
|
|
4
4
|
checkMcpConfigured,
|
|
5
5
|
checkNodeVersion,
|
|
6
|
+
checkPackageRefs,
|
|
6
7
|
checkPragmaVersion,
|
|
7
8
|
checkShellCompletions,
|
|
8
9
|
checkSkillsSymlinked,
|
|
@@ -25,6 +26,7 @@ export default async function runChecks(
|
|
|
25
26
|
checks.push(await checkNodeVersion());
|
|
26
27
|
checks.push(await checkPragmaVersion());
|
|
27
28
|
checks.push(await checkConfigFile(ctx));
|
|
29
|
+
checks.push(await checkPackageRefs(ctx));
|
|
28
30
|
checks.push(await checkKeStore(ctx));
|
|
29
31
|
checks.push(await checkShellCompletions());
|
|
30
32
|
checks.push(await checkMcpConfigured(ctx));
|
|
@@ -98,6 +98,40 @@ describe("renderInfoPlain", () => {
|
|
|
98
98
|
expect(output).not.toContain("Triples");
|
|
99
99
|
expect(output).not.toContain("Graphs");
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
it("includes package refs section", () => {
|
|
103
|
+
const output = renderInfoPlain(
|
|
104
|
+
createInfoData({
|
|
105
|
+
packageRefs: [
|
|
106
|
+
{
|
|
107
|
+
pkg: "@canonical/design-system",
|
|
108
|
+
source: "npm",
|
|
109
|
+
detail: "node_modules",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
pkg: "@canonical/code-standards",
|
|
113
|
+
source: "file",
|
|
114
|
+
detail: "/home/user/code/standards",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
pkg: "@canonical/anatomy-dsl",
|
|
118
|
+
source: "git",
|
|
119
|
+
detail: "https://github.com/canonical/anatomy-dsl.git#main",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
expect(output).toContain("Packages");
|
|
125
|
+
expect(output).toContain("@canonical/design-system");
|
|
126
|
+
expect(output).toContain("npm");
|
|
127
|
+
expect(output).toContain("file");
|
|
128
|
+
expect(output).toContain("git");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("omits package refs section when absent", () => {
|
|
132
|
+
const output = renderInfoPlain(createInfoData());
|
|
133
|
+
expect(output).not.toContain("Packages");
|
|
134
|
+
});
|
|
101
135
|
});
|
|
102
136
|
|
|
103
137
|
describe("renderInfoLlm", () => {
|
|
@@ -43,6 +43,14 @@ function renderInfoPlain(data: InfoData): string {
|
|
|
43
43
|
lines.push(" Up to date.");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
if (data.packageRefs && data.packageRefs.length > 0) {
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push(formatHeading("Packages"));
|
|
49
|
+
for (const ref of data.packageRefs) {
|
|
50
|
+
lines.push(formatField(` ${ref.pkg}:`, `${ref.source} (${ref.detail})`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
if (data.store) {
|
|
47
55
|
lines.push("");
|
|
48
56
|
lines.push(formatHeading("Store"));
|
|
@@ -93,6 +101,13 @@ function renderInfoLlm(data: InfoData): string {
|
|
|
93
101
|
lines.push("- Update: up to date");
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
if (data.packageRefs && data.packageRefs.length > 0) {
|
|
105
|
+
lines.push("## Packages");
|
|
106
|
+
for (const ref of data.packageRefs) {
|
|
107
|
+
lines.push(`- ${ref.pkg}: ${ref.source} (${ref.detail})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
if (data.store) {
|
|
97
112
|
lines.push(`- Store: ${data.store.tripleCount.toLocaleString()} triples`);
|
|
98
113
|
if (data.store.graphNames.length > 0) {
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { readConfig } from "#config";
|
|
2
2
|
import { VERSION } from "#constants";
|
|
3
3
|
import { detectInstallSource, PM_COMMANDS } from "#package-manager";
|
|
4
|
+
import {
|
|
5
|
+
type PackageRef,
|
|
6
|
+
parsePackageEntry,
|
|
7
|
+
} from "../../refs/operations/parseRef.js";
|
|
8
|
+
import readGlobalRefs from "../../refs/operations/readGlobalRefs.js";
|
|
4
9
|
import { bootStore } from "../../shared/bootStore.js";
|
|
5
10
|
import { CHANNEL_RELEASES } from "../../shared/filters/buildChannelFilter.js";
|
|
6
11
|
import { resolveTierChain } from "../../shared/filters/buildTierFilter.js";
|
|
7
|
-
import
|
|
12
|
+
import { DEFAULT_PACKAGES } from "../../shared/packages.js";
|
|
13
|
+
import type { InfoData, PackageRefSummary } from "../types.js";
|
|
8
14
|
import checkRegistryVersion from "./checkRegistryVersion.js";
|
|
9
15
|
import { collectStoreSummary } from "./collectStoreSummary.js";
|
|
10
16
|
|
|
@@ -52,6 +58,9 @@ export default async function collectInfo(cwd: string): Promise<InfoData> {
|
|
|
52
58
|
keStore?.dispose();
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
// Collect package ref summaries
|
|
62
|
+
const packageRefs = collectPackageRefSummaries(config.packages);
|
|
63
|
+
|
|
55
64
|
return {
|
|
56
65
|
version: VERSION,
|
|
57
66
|
pm,
|
|
@@ -64,5 +73,25 @@ export default async function collectInfo(cwd: string): Promise<InfoData> {
|
|
|
64
73
|
update,
|
|
65
74
|
updateSkipped,
|
|
66
75
|
store,
|
|
76
|
+
packageRefs,
|
|
67
77
|
};
|
|
68
78
|
}
|
|
79
|
+
|
|
80
|
+
function collectPackageRefSummaries(
|
|
81
|
+
projectPackages?: ReadonlyArray<
|
|
82
|
+
string | { readonly name: string; readonly source?: string }
|
|
83
|
+
>,
|
|
84
|
+
): PackageRefSummary[] {
|
|
85
|
+
const entries = projectPackages ?? readGlobalRefs();
|
|
86
|
+
const raw = entries.length > 0 ? entries : DEFAULT_PACKAGES.map((pkg) => pkg);
|
|
87
|
+
|
|
88
|
+
return raw.map((entry) => refToSummary(parsePackageEntry(entry)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function refToSummary(ref: PackageRef): PackageRefSummary {
|
|
92
|
+
if (ref.kind === "file")
|
|
93
|
+
return { pkg: ref.pkg, source: "file", detail: ref.path };
|
|
94
|
+
if (ref.kind === "git")
|
|
95
|
+
return { pkg: ref.pkg, source: "git", detail: `${ref.url}#${ref.ref}` };
|
|
96
|
+
return { pkg: ref.pkg, source: "npm", detail: "node_modules" };
|
|
97
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/** Summary of how a single package is resolved. */
|
|
2
|
+
export interface PackageRefSummary {
|
|
3
|
+
readonly pkg: string;
|
|
4
|
+
readonly source: "npm" | "file" | "git";
|
|
5
|
+
readonly detail: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
/** Data collected by `pragma info` for rendering. */
|
|
2
9
|
export interface InfoData {
|
|
3
10
|
readonly version: string;
|
|
@@ -19,6 +26,7 @@ export interface InfoData {
|
|
|
19
26
|
readonly store:
|
|
20
27
|
| { readonly tripleCount: number; readonly graphNames: readonly string[] }
|
|
21
28
|
| undefined;
|
|
29
|
+
readonly packageRefs?: readonly PackageRefSummary[];
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
/** Data collected by `pragma upgrade` for rendering. */
|