@canonical/pragma-cli 0.35.0 → 0.36.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 +2 -2
- package/bundled-skills/add-standard/SKILL.md +338 -0
- package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
- package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
- package/bundled-skills/anatomy-author/SKILL.md +1226 -0
- package/bundled-skills/design-auditor/SKILL.md +455 -0
- package/bundled-skills/implement-component/SKILL.md +333 -0
- package/bundled-skills/specify-component/SKILL.md +331 -0
- package/bundled-skills/specify-pattern/SKILL.md +292 -0
- package/dist/package.json +16 -15
- package/dist/pragma.conf.js +236 -34
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/bin.js +1 -1
- package/dist/src/bin.js.map +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
- package/dist/src/capabilities/create/mount.js +4 -3
- package/dist/src/capabilities/create/mount.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/index.js +6 -6
- package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.render.js +43 -64
- package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
- package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
- package/dist/src/capabilities/doctor/runChecks.js +14 -11
- package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
- package/dist/src/capabilities/graph/query.render.js +2 -2
- package/dist/src/capabilities/graph/query.render.js.map +1 -1
- package/dist/src/capabilities/info/collectInfo.js +8 -2
- package/dist/src/capabilities/info/collectInfo.js.map +1 -1
- package/dist/src/capabilities/info/info.render.js +6 -2
- package/dist/src/capabilities/info/info.render.js.map +1 -1
- package/dist/src/capabilities/prompt/prompt.render.js +2 -2
- package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +43 -39
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/mode.js +2 -2
- package/dist/src/capabilities/setup/operations/index.js +4 -4
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +92 -45
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +88 -36
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +97 -51
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +325 -47
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/plan.render.js +144 -66
- package/dist/src/capabilities/setup/plan.render.js.map +1 -1
- package/dist/src/capabilities/setup/setup.render.js +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +86 -33
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +110 -45
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +6 -5
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/capabilities/shared/packageManager.js +300 -54
- package/dist/src/capabilities/shared/packageManager.js.map +1 -1
- package/dist/src/capabilities/skill/discover.js +97 -11
- package/dist/src/capabilities/skill/discover.js.map +1 -1
- package/dist/src/capabilities/skill/render.js +2 -2
- package/dist/src/capabilities/skill/render.js.map +1 -1
- package/dist/src/capabilities/sources/installSkills.js +145 -3
- package/dist/src/capabilities/sources/installSkills.js.map +1 -1
- package/dist/src/capabilities/sources/runUpdate.js +68 -2
- package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
- package/dist/src/capabilities/sources/update.verb.js +3 -3
- package/dist/src/capabilities/sources/update.verb.js.map +1 -1
- package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
- package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
- package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
- package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
- package/dist/src/constants.js +6 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/index.js +11 -2
- package/dist/src/kernel/index.js.map +1 -1
- package/dist/src/kernel/interactivity.js +68 -0
- package/dist/src/kernel/interactivity.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +3 -3
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +63 -12
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +57 -4
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +54 -2
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +306 -43
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +23 -23
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/globalFlags.js +4 -1
- package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
- package/dist/src/kernel/project/cli/helpFormat.js +1 -2
- package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +34 -4
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +1 -1
- package/dist/src/kernel/render/style.js +29 -9
- package/dist/src/kernel/render/style.js.map +1 -1
- package/dist/src/kernel/render/vocabulary.js +174 -0
- package/dist/src/kernel/render/vocabulary.js.map +1 -0
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +16 -15
- package/pragma.conf.ts +241 -34
- package/src/bin.ts +8 -1
- package/src/capabilities/block.parity.test.ts +25 -11
- package/src/capabilities/block.shipped.exec.test.ts +282 -0
- package/src/capabilities/block.tierRank.exec.test.ts +300 -0
- package/src/capabilities/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/create/createGrammar.test.ts +1 -1
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
- package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
- package/src/capabilities/doctor/checks/index.ts +6 -6
- package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
- package/src/capabilities/doctor/doctor.render.test.ts +108 -32
- package/src/capabilities/doctor/doctor.render.ts +56 -80
- package/src/capabilities/doctor/doctor.test.ts +247 -35
- package/src/capabilities/doctor/doctor.verb.ts +7 -2
- package/src/capabilities/doctor/runChecks.ts +14 -11
- package/src/capabilities/doctor/types.ts +15 -24
- package/src/capabilities/emptyRecovery.test.ts +10 -2
- package/src/capabilities/graph/query.render.ts +2 -2
- package/src/capabilities/info/collectInfo.ts +8 -1
- package/src/capabilities/info/info.render.ts +6 -2
- package/src/capabilities/info/info.test.ts +37 -1
- package/src/capabilities/info/types.ts +10 -2
- package/src/capabilities/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- package/src/capabilities/scopeTypeSync.test.ts +29 -15
- package/src/capabilities/setup/buildPlan.ts +45 -41
- package/src/capabilities/setup/index.ts +1 -1
- package/src/capabilities/setup/mode.ts +2 -2
- package/src/capabilities/setup/operations/index.ts +6 -1
- package/src/capabilities/setup/operations/setupGenerator.ts +144 -70
- package/src/capabilities/setup/operations/setupLsp.ts +129 -37
- package/src/capabilities/setup/operations/setupMcp.ts +132 -61
- package/src/capabilities/setup/operations/setupSkills.ts +392 -58
- package/src/capabilities/setup/plan.render.ts +171 -76
- package/src/capabilities/setup/plan.ts +17 -12
- package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
- package/src/capabilities/setup/setup.render.test.ts +50 -36
- package/src/capabilities/setup/setup.render.ts +1 -1
- package/src/capabilities/setup/setup.test.ts +881 -50
- package/src/capabilities/setup/setup.verb.ts +91 -33
- package/src/capabilities/setup/targets.ts +120 -52
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +17 -6
- package/src/capabilities/shared/packageManager.test.ts +421 -6
- package/src/capabilities/shared/packageManager.ts +394 -59
- package/src/capabilities/skill/bundledSkills.test.ts +347 -0
- package/src/capabilities/skill/discover.ts +100 -11
- package/src/capabilities/skill/render.ts +2 -2
- package/src/capabilities/skill/skill.test.ts +17 -4
- package/src/capabilities/sources/installSkills.ts +181 -3
- package/src/capabilities/sources/runUpdate.ts +78 -2
- package/src/capabilities/sources/sources.test.ts +228 -0
- package/src/capabilities/sources/update.verb.ts +3 -3
- package/src/capabilities/surface.test.ts +2 -2
- package/src/capabilities/upgrade/runUpgrade.ts +45 -6
- package/src/capabilities/upgrade/types.ts +10 -2
- package/src/capabilities/upgrade/upgrade.render.ts +9 -2
- package/src/capabilities/upgrade/upgrade.test.ts +72 -0
- package/src/constants.ts +6 -1
- package/src/identity.test.ts +18 -5
- package/src/kernel/completion/entitySource.test.ts +15 -2
- package/src/kernel/completion/safety.test.ts +16 -9
- package/src/kernel/config/defaults.test.ts +2 -1
- package/src/kernel/index.ts +11 -2
- package/src/kernel/interactivity.test.ts +76 -0
- package/src/kernel/interactivity.ts +69 -0
- package/src/kernel/packs/compile.test.ts +92 -0
- package/src/kernel/packs/renderPack.ts +3 -3
- package/src/kernel/packs/resolveEntity.test.ts +75 -14
- package/src/kernel/packs/resolveEntity.ts +88 -24
- package/src/kernel/packs/runBodies.ts +84 -19
- package/src/kernel/packs/schema.ts +60 -4
- package/src/kernel/packs/sparql/applyFilters.ts +125 -4
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +323 -45
- package/src/kernel/packs/toolDescription.test.ts +35 -1
- package/src/kernel/packs/types.ts +125 -2
- package/src/kernel/project/cli/buildProgram.test.ts +73 -2
- package/src/kernel/project/cli/buildProgram.ts +8 -1
- package/src/kernel/project/cli/dispatch.test.ts +5 -4
- package/src/kernel/project/cli/dispatch.ts +25 -24
- package/src/kernel/project/cli/globalFlags.ts +4 -1
- package/src/kernel/project/cli/helpFormat.ts +1 -2
- package/src/kernel/project/cli/rootHelp.ts +36 -3
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- package/src/kernel/render/renderers.ts +1 -1
- package/src/kernel/render/style.ts +32 -9
- package/src/kernel/render/vocabulary.ts +276 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/types.ts +35 -6
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/docExamples.test.ts +4 -2
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
- package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
- package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
- package/src/testing/eval/cases/readNouns.ts +245 -9
- package/src/testing/fixtures/blockGraph.ts +28 -13
- package/src/testing/fixtures/graph/canonical.ts +95 -12
- package/dist/src/capabilities/shared/bands.js +0 -16
- package/dist/src/capabilities/shared/bands.js.map +0 -1
- package/src/capabilities/shared/bands.ts +0 -16
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* that capability, and the moment something here has a single consumer again
|
|
9
9
|
* it should move back. What survives is the environment the CLI reports on but
|
|
10
10
|
* does not own — how it was installed, what the registry says about it,
|
|
11
|
-
* whether a subprocess it ran actually succeeded
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* whether a subprocess it ran actually succeeded. (The scope labels that keep
|
|
12
|
+
* `setup` and `doctor` calling the two config scopes by the same names used
|
|
13
|
+
* to live here too; they are part of the rendering vocabulary now —
|
|
14
|
+
* `kernel/render/vocabulary.ts` — beside the statuses and glyphs they are
|
|
15
|
+
* printed with.)
|
|
14
16
|
*
|
|
15
17
|
* The exec guard is exported as the pair a caller needs together: `checkExecOk`
|
|
16
18
|
* to classify a result inside a Task and `assertExecOk` to throw on one
|
|
@@ -25,8 +27,17 @@ export {
|
|
|
25
27
|
checkExecOk,
|
|
26
28
|
guardMissingBinary,
|
|
27
29
|
} from "./assertExecOk.js";
|
|
28
|
-
export {
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
export type {
|
|
31
|
+
EphemeralRunner,
|
|
32
|
+
GlobalInstall,
|
|
33
|
+
InstallProbe,
|
|
34
|
+
InstallSource,
|
|
35
|
+
PackageManager,
|
|
36
|
+
} from "./packageManager.js";
|
|
37
|
+
export {
|
|
38
|
+
detectInstallSource,
|
|
39
|
+
pmUpdateCommand,
|
|
40
|
+
updateGuidance,
|
|
41
|
+
} from "./packageManager.js";
|
|
31
42
|
export type { RegistryCheckResult } from "./registry.js";
|
|
32
43
|
export { checkRegistryVersion, PRAGMA_PACKAGE } from "./registry.js";
|
|
@@ -1,14 +1,421 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The install-
|
|
2
|
+
* The install-source detector, driven entirely by fixtures.
|
|
3
3
|
*
|
|
4
|
-
* `detectInstallSource` itself
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `detectInstallSource` itself captures the live process; the decision lives
|
|
5
|
+
* in the pure `classifyInstall`, which takes an {@link InstallProbe} — paths,
|
|
6
|
+
* environment, and injected filesystem readers — so every layout that has
|
|
7
|
+
* ever been wrong is a deterministic case here, independent of the machine
|
|
8
|
+
* the suite runs on (the `PlatformEnv` fixture pattern from
|
|
9
|
+
* `@canonical/harnesses`).
|
|
10
|
+
*
|
|
11
|
+
* The headline case is the LINKED development install (`npm link`/`bun link`):
|
|
12
|
+
* the old env-var heuristic reported it as `node (global)` and would have
|
|
13
|
+
* offered `npm i -g`, which overwrites the link to the worktree being
|
|
14
|
+
* developed. Here it must be identified as linked — and the type of
|
|
15
|
+
* `pmUpdateCommand` (global arm only) makes producing a command for it a
|
|
16
|
+
* compile error, which is the strongest "no upgrade command" assertion there
|
|
17
|
+
* is.
|
|
8
18
|
*/
|
|
9
19
|
|
|
10
20
|
import { describe, expect, it } from "vitest";
|
|
11
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
classifyInstall,
|
|
23
|
+
containsPath,
|
|
24
|
+
ephemeralRunner,
|
|
25
|
+
findInstallRoot,
|
|
26
|
+
type InstallProbe,
|
|
27
|
+
managerFromPath,
|
|
28
|
+
pmUpdateCommand,
|
|
29
|
+
} from "./packageManager.js";
|
|
30
|
+
import { PRAGMA_PACKAGE } from "./registry.js";
|
|
31
|
+
|
|
32
|
+
/** The module-relative suffix the running entry carries under `dist`. */
|
|
33
|
+
const ENTRY = "dist/src/capabilities/shared/packageManager.js";
|
|
34
|
+
|
|
35
|
+
/** Build a probe whose filesystem is the given symlink map + file list. */
|
|
36
|
+
function probeOf(
|
|
37
|
+
over: Partial<InstallProbe>,
|
|
38
|
+
fs: { links?: Record<string, string>; files?: string[] } = {},
|
|
39
|
+
): InstallProbe {
|
|
40
|
+
const links = fs.links ?? {};
|
|
41
|
+
const resolveLink = (path: string): string | undefined => {
|
|
42
|
+
const target = links[path];
|
|
43
|
+
if (target === undefined) return undefined;
|
|
44
|
+
const base = path.slice(0, path.lastIndexOf("/"));
|
|
45
|
+
return target.startsWith("/") ? target : `${base}/${target}`;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
entry: "/unset",
|
|
49
|
+
invoked: undefined,
|
|
50
|
+
cwd: "/somewhere/else",
|
|
51
|
+
userAgent: undefined,
|
|
52
|
+
runtime: "node",
|
|
53
|
+
readLink: (path) => links[path],
|
|
54
|
+
realPath: (path) => {
|
|
55
|
+
// Follow the final component's link chain, normalizing `..` segments —
|
|
56
|
+
// enough realpath for these fixtures.
|
|
57
|
+
let current = path;
|
|
58
|
+
for (let hop = 0; hop < 10; hop++) {
|
|
59
|
+
const next = resolveLink(current);
|
|
60
|
+
if (next === undefined) break;
|
|
61
|
+
current = next;
|
|
62
|
+
}
|
|
63
|
+
const segments: string[] = [];
|
|
64
|
+
for (const part of current.split("/")) {
|
|
65
|
+
if (part === "..") segments.pop();
|
|
66
|
+
else if (part !== "." && part !== "") segments.push(part);
|
|
67
|
+
}
|
|
68
|
+
return `/${segments.join("/")}`;
|
|
69
|
+
},
|
|
70
|
+
exists: (path) => fs.files?.includes(path) ?? false,
|
|
71
|
+
...over,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const G = "/home/u/.npm-global";
|
|
76
|
+
const NPM_PKG = `${G}/lib/node_modules/@canonical/pragma-cli`;
|
|
77
|
+
|
|
78
|
+
describe("classifyInstall — global installs", () => {
|
|
79
|
+
it("npm global: bin symlink into an unmarked global tree", () => {
|
|
80
|
+
const install = classifyInstall(
|
|
81
|
+
probeOf(
|
|
82
|
+
{ entry: `${NPM_PKG}/${ENTRY}`, invoked: `${G}/bin/pragma` },
|
|
83
|
+
{
|
|
84
|
+
links: {
|
|
85
|
+
[`${G}/bin/pragma`]:
|
|
86
|
+
"../lib/node_modules/@canonical/pragma-cli/dist/src/bin.js",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
expect(install).toMatchObject({ kind: "global", pm: "npm" });
|
|
92
|
+
expect(install.label).toBe("npm (global)");
|
|
93
|
+
if (install.kind === "global") {
|
|
94
|
+
expect(pmUpdateCommand(install, PRAGMA_PACKAGE)).toBe(
|
|
95
|
+
`npm i -g ${PRAGMA_PACKAGE}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("the user-agent NEVER overrides the path shape (corroboration only)", () => {
|
|
101
|
+
// `bun run some-script` invoking a globally npm-installed pragma sets a
|
|
102
|
+
// bun user-agent — the install is still npm's.
|
|
103
|
+
const install = classifyInstall(
|
|
104
|
+
probeOf({
|
|
105
|
+
entry: `${NPM_PKG}/${ENTRY}`,
|
|
106
|
+
userAgent: "bun/1.2.19 npm/? node/v24.3.0 linux x64",
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
expect(install).toMatchObject({ kind: "global", pm: "npm" });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("bun global: ~/.bun/install/global shape", () => {
|
|
113
|
+
const pkg =
|
|
114
|
+
"/home/u/.bun/install/global/node_modules/@canonical/pragma-cli";
|
|
115
|
+
const install = classifyInstall(
|
|
116
|
+
probeOf(
|
|
117
|
+
{ entry: `${pkg}/${ENTRY}`, invoked: "/home/u/.bun/bin/pragma" },
|
|
118
|
+
{
|
|
119
|
+
links: {
|
|
120
|
+
"/home/u/.bun/bin/pragma":
|
|
121
|
+
"../install/global/node_modules/@canonical/pragma-cli/dist/src/bin.js",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
expect(install).toMatchObject({ kind: "global", pm: "bun" });
|
|
127
|
+
if (install.kind === "global") {
|
|
128
|
+
expect(pmUpdateCommand(install, PRAGMA_PACKAGE)).toBe(
|
|
129
|
+
`bun add -g ${PRAGMA_PACKAGE}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("pnpm global: the .pnpm store segment names the manager", () => {
|
|
135
|
+
const store =
|
|
136
|
+
"/home/u/.local/share/pnpm/global/5/.pnpm/@canonical+pragma-cli@0.35.0/node_modules/@canonical/pragma-cli";
|
|
137
|
+
const install = classifyInstall(probeOf({ entry: `${store}/${ENTRY}` }));
|
|
138
|
+
expect(install).toMatchObject({ kind: "global", pm: "pnpm" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("volta: ~/.volta/tools/image/packages shape, with volta's own command", () => {
|
|
142
|
+
const pkg =
|
|
143
|
+
"/home/u/.volta/tools/image/packages/pragma-cli/lib/node_modules/@canonical/pragma-cli";
|
|
144
|
+
// Volta's bin shim is a real executable, not a symlink — the chain walk
|
|
145
|
+
// yields nothing and the entry path decides.
|
|
146
|
+
const install = classifyInstall(
|
|
147
|
+
probeOf({
|
|
148
|
+
entry: `${pkg}/${ENTRY}`,
|
|
149
|
+
invoked: "/home/u/.volta/bin/pragma",
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
expect(install).toMatchObject({ kind: "global", pm: "volta" });
|
|
153
|
+
if (install.kind === "global") {
|
|
154
|
+
expect(pmUpdateCommand(install, PRAGMA_PACKAGE)).toBe(
|
|
155
|
+
`volta install ${PRAGMA_PACKAGE}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("asdf hosts an ordinary npm global tree", () => {
|
|
161
|
+
const pkg =
|
|
162
|
+
"/home/u/.asdf/installs/nodejs/22.5.0/lib/node_modules/@canonical/pragma-cli";
|
|
163
|
+
const install = classifyInstall(probeOf({ entry: `${pkg}/${ENTRY}` }));
|
|
164
|
+
expect(install).toMatchObject({ kind: "global", pm: "npm" });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("classifyInstall — the LINKED development install (the headline)", () => {
|
|
169
|
+
// The layout measured on a real machine: `which pragma` →
|
|
170
|
+
// ~/.npm-global/bin/pragma → node_modules/@canonical/pragma-cli, which is
|
|
171
|
+
// itself a symlink OUT of the global root into a development worktree. The
|
|
172
|
+
// module loader realpath-resolves `import.meta.url`, so the entry has
|
|
173
|
+
// already escaped to the worktree — only the argv[1] chain can see the link.
|
|
174
|
+
const worktree =
|
|
175
|
+
"/home/u/code/cn/pragma/.claude/worktrees/try-main/packages/cli/pragma";
|
|
176
|
+
|
|
177
|
+
const linkedProbe = (over: Partial<InstallProbe> = {}): InstallProbe =>
|
|
178
|
+
probeOf(
|
|
179
|
+
{
|
|
180
|
+
entry: `${worktree}/${ENTRY}`,
|
|
181
|
+
invoked: `${G}/bin/pragma`,
|
|
182
|
+
...over,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
links: {
|
|
186
|
+
[`${G}/bin/pragma`]:
|
|
187
|
+
"../lib/node_modules/@canonical/pragma-cli/dist/src/bin.js",
|
|
188
|
+
[NPM_PKG]:
|
|
189
|
+
"../../../../code/cn/pragma/.claude/worktrees/try-main/packages/cli/pragma",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
it("identifies npm link — never 'global'", () => {
|
|
195
|
+
const install = classifyInstall(linkedProbe());
|
|
196
|
+
expect(install.kind).toBe("linked");
|
|
197
|
+
expect(install.label).toBe("linked (development checkout)");
|
|
198
|
+
if (install.kind === "linked") {
|
|
199
|
+
expect(install.target).toBe(worktree);
|
|
200
|
+
}
|
|
201
|
+
// No upgrade command exists for it: `pmUpdateCommand` only accepts the
|
|
202
|
+
// global arm, so the following would not compile —
|
|
203
|
+
// pmUpdateCommand(install, PRAGMA_PACKAGE)
|
|
204
|
+
// — and the runtime data carries guidance instead (see upgrade.test.ts).
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("identifies bun link, naming the hosting manager", () => {
|
|
208
|
+
const pkg =
|
|
209
|
+
"/home/u/.bun/install/global/node_modules/@canonical/pragma-cli";
|
|
210
|
+
const checkout =
|
|
211
|
+
"/home/u/code/cn/pragma/.claude/worktrees/fix-tier-rank/packages/cli/pragma";
|
|
212
|
+
const install = classifyInstall(
|
|
213
|
+
probeOf(
|
|
214
|
+
{
|
|
215
|
+
entry: `${checkout}/${ENTRY}`,
|
|
216
|
+
invoked: "/home/u/.bun/bin/pragma",
|
|
217
|
+
runtime: "bun",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
links: {
|
|
221
|
+
"/home/u/.bun/bin/pragma":
|
|
222
|
+
"../install/global/node_modules/@canonical/pragma-cli/dist/src/bin.js",
|
|
223
|
+
[pkg]: checkout,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
expect(install).toMatchObject({ kind: "linked", pm: "bun" });
|
|
229
|
+
expect(install.label).toBe("bun link (development checkout)");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("a workspace-internal package symlink is NOT a link — it stays inside the root", () => {
|
|
233
|
+
// A monorepo's own `node_modules/<pkg>` → `packages/<pkg>` symlink points
|
|
234
|
+
// outside node_modules but INSIDE the project — a workspace install.
|
|
235
|
+
const root = "/home/u/proj";
|
|
236
|
+
const install = classifyInstall(
|
|
237
|
+
probeOf(
|
|
238
|
+
{
|
|
239
|
+
entry: `${root}/packages/cli/pragma/${ENTRY}`,
|
|
240
|
+
invoked: `${root}/node_modules/.bin/pragma`,
|
|
241
|
+
cwd: `${root}/apps/site`,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
links: {
|
|
245
|
+
[`${root}/node_modules/.bin/pragma`]:
|
|
246
|
+
"../@canonical/pragma-cli/dist/src/bin.js",
|
|
247
|
+
[`${root}/node_modules/@canonical/pragma-cli`]:
|
|
248
|
+
"../../packages/cli/pragma",
|
|
249
|
+
},
|
|
250
|
+
files: [`${root}/bun.lock`],
|
|
251
|
+
},
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
expect(install).toMatchObject({ kind: "workspace", pm: "bun", root });
|
|
255
|
+
expect(install.label).toBe("bun (local project)");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("classifyInstall — ephemeral runners (no upgrade command)", () => {
|
|
260
|
+
it("npx resolves into the ~/.npm/_npx cache", () => {
|
|
261
|
+
const pkg =
|
|
262
|
+
"/home/u/.npm/_npx/beb367dfa21eb3f5/node_modules/@canonical/pragma-cli";
|
|
263
|
+
const install = classifyInstall(probeOf({ entry: `${pkg}/${ENTRY}` }));
|
|
264
|
+
expect(install).toMatchObject({ kind: "ephemeral", runner: "npx" });
|
|
265
|
+
expect(install.label).toBe("npx (ephemeral)");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("bunx resolves into a bunx-* temp dir", () => {
|
|
269
|
+
const pkg =
|
|
270
|
+
"/tmp/bunx-1000-@canonical/pragma-cli@latest/node_modules/@canonical/pragma-cli";
|
|
271
|
+
const install = classifyInstall(
|
|
272
|
+
probeOf({ entry: `${pkg}/${ENTRY}`, runtime: "bun" }),
|
|
273
|
+
);
|
|
274
|
+
expect(install).toMatchObject({ kind: "ephemeral", runner: "bunx" });
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("classifyInstall — workspace installs", () => {
|
|
279
|
+
const root = "/home/u/proj";
|
|
280
|
+
const pkg = `${root}/node_modules/@canonical/pragma-cli`;
|
|
281
|
+
|
|
282
|
+
it("names the manager from the lockfile beside the install root", () => {
|
|
283
|
+
const install = classifyInstall(
|
|
284
|
+
probeOf(
|
|
285
|
+
{ entry: `${pkg}/${ENTRY}`, cwd: `${root}/packages/site` },
|
|
286
|
+
{ files: [`${root}/package-lock.json`] },
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
expect(install).toMatchObject({ kind: "workspace", pm: "npm", root });
|
|
290
|
+
expect(install.label).toBe("npm (local project)");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("falls back to the user-agent when no lockfile marks the tree", () => {
|
|
294
|
+
const install = classifyInstall(
|
|
295
|
+
probeOf({
|
|
296
|
+
entry: `${pkg}/${ENTRY}`,
|
|
297
|
+
cwd: root,
|
|
298
|
+
userAgent: "pnpm/9.1.0 npm/? node/v20.11.0 linux x64",
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
expect(install).toMatchObject({ kind: "workspace", pm: "pnpm" });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("stays honest with no marker at all", () => {
|
|
305
|
+
const install = classifyInstall(
|
|
306
|
+
probeOf({ entry: `${pkg}/${ENTRY}`, cwd: root }),
|
|
307
|
+
);
|
|
308
|
+
expect(install.kind).toBe("workspace");
|
|
309
|
+
expect(install.label).toBe("local project dependency");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("classifyInstall — unknown (the honest fallback)", () => {
|
|
314
|
+
it("a source checkout run directly is unknown, not global", () => {
|
|
315
|
+
const entry =
|
|
316
|
+
"/work/pragma/packages/cli/pragma/src/capabilities/shared/packageManager.ts";
|
|
317
|
+
const install = classifyInstall(
|
|
318
|
+
probeOf({
|
|
319
|
+
entry,
|
|
320
|
+
invoked: "/work/pragma/packages/cli/pragma/src/bin.ts",
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
expect(install.kind).toBe("unknown");
|
|
324
|
+
expect(install.label).toBe("unknown (node runtime)");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("a FOREIGN bin on argv[1] (a test runner) never misattributes the install", () => {
|
|
328
|
+
const repo = "/repo";
|
|
329
|
+
const install = classifyInstall(
|
|
330
|
+
probeOf(
|
|
331
|
+
{
|
|
332
|
+
entry: `${repo}/packages/cli/pragma/src/capabilities/shared/packageManager.ts`,
|
|
333
|
+
invoked: `${repo}/node_modules/.bin/vitest`,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
links: {
|
|
337
|
+
[`${repo}/node_modules/.bin/vitest`]: "../vitest/vitest.mjs",
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
expect(install.kind).toBe("unknown");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("a symlink cycle on the invoked path is bounded, not spun", () => {
|
|
346
|
+
const install = classifyInstall(
|
|
347
|
+
probeOf(
|
|
348
|
+
{ entry: "/src/tree/file.ts", invoked: "/a" },
|
|
349
|
+
{ links: { "/a": "/b", "/b": "/a" } },
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
expect(install.kind).toBe("unknown");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("path-shape helpers (win32 shapes exercised, not host-validated)", () => {
|
|
357
|
+
it("managerFromPath reads win32 volta and posix shapes alike", () => {
|
|
358
|
+
expect(
|
|
359
|
+
managerFromPath(
|
|
360
|
+
"C:\\Users\\u\\AppData\\Local\\Volta\\tools\\image\\packages\\pragma-cli\\node_modules\\@canonical\\pragma-cli\\dist\\src\\bin.js",
|
|
361
|
+
),
|
|
362
|
+
).toBe("volta");
|
|
363
|
+
expect(
|
|
364
|
+
managerFromPath(
|
|
365
|
+
"C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules\\@canonical\\pragma-cli\\dist\\src\\bin.js",
|
|
366
|
+
),
|
|
367
|
+
).toBeUndefined();
|
|
368
|
+
expect(
|
|
369
|
+
managerFromPath("/home/u/.config/yarn/global/node_modules/x/bin.js"),
|
|
370
|
+
).toBe("yarn");
|
|
371
|
+
expect(
|
|
372
|
+
managerFromPath("/home/u/.nvm/versions/node/v20/lib/node_modules/x"),
|
|
373
|
+
).toBe("npm");
|
|
374
|
+
// A project merely NAMED volta must not read as the volta layout.
|
|
375
|
+
expect(
|
|
376
|
+
managerFromPath("/home/u/volta/node_modules/x/bin.js"),
|
|
377
|
+
).toBeUndefined();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("ephemeralRunner ignores ordinary directories that merely share a name (REGRESSION)", () => {
|
|
381
|
+
// The patterns are anchored to each runner's real cache layout. Matching a
|
|
382
|
+
// bare segment anywhere would reclassify a project living under a directory
|
|
383
|
+
// called `dlx`, `_npx` or `bunx-something` as ephemeral — and since that
|
|
384
|
+
// verdict is reached before the workspace check, it wins even with a
|
|
385
|
+
// lockfile beside the root, leaving the user with no upgrade command and no
|
|
386
|
+
// error explaining why.
|
|
387
|
+
expect(
|
|
388
|
+
ephemeralRunner("/work/dlx/app/node_modules/@canonical/pragma-cli"),
|
|
389
|
+
).toBeUndefined();
|
|
390
|
+
expect(
|
|
391
|
+
ephemeralRunner("/srv/_npx/site/node_modules/@canonical/pragma-cli"),
|
|
392
|
+
).toBeUndefined();
|
|
393
|
+
expect(
|
|
394
|
+
ephemeralRunner("/home/u/bunx-tools/node_modules/@canonical/pragma-cli"),
|
|
395
|
+
).toBeUndefined();
|
|
396
|
+
// `dlx` outside a pnpm cache, and `_npx` outside npm's, stay ordinary.
|
|
397
|
+
expect(
|
|
398
|
+
ephemeralRunner("/home/u/.cache/yarn/dlx/abc/node_modules/x/bin.js"),
|
|
399
|
+
).toBeUndefined();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("ephemeralRunner reads npx/bunx/dlx cache shapes on both separator styles", () => {
|
|
403
|
+
expect(
|
|
404
|
+
ephemeralRunner(
|
|
405
|
+
"C:\\Users\\u\\AppData\\Local\\npm-cache\\_npx\\abc\\node_modules\\x\\bin.js",
|
|
406
|
+
),
|
|
407
|
+
).toBe("npx");
|
|
408
|
+
expect(
|
|
409
|
+
ephemeralRunner("/tmp/bunx-1000-x@latest/node_modules/x/bin.js"),
|
|
410
|
+
).toBe("bunx");
|
|
411
|
+
expect(
|
|
412
|
+
ephemeralRunner("/home/u/.cache/pnpm/dlx/abc/node_modules/x/bin.js"),
|
|
413
|
+
).toBe("pnpm dlx");
|
|
414
|
+
expect(
|
|
415
|
+
ephemeralRunner("/home/u/proj/node_modules/x/bin.js"),
|
|
416
|
+
).toBeUndefined();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
12
419
|
|
|
13
420
|
describe("findInstallRoot", () => {
|
|
14
421
|
it("yields the directory the package was installed into", () => {
|
|
@@ -25,6 +432,14 @@ describe("findInstallRoot", () => {
|
|
|
25
432
|
).toBe("/proj/node_modules/a");
|
|
26
433
|
});
|
|
27
434
|
|
|
435
|
+
it("reads a win32 path with its own separator", () => {
|
|
436
|
+
expect(
|
|
437
|
+
findInstallRoot(
|
|
438
|
+
"C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules\\@canonical\\pragma-cli\\dist\\src\\bin.js",
|
|
439
|
+
),
|
|
440
|
+
).toBe("C:\\Users\\u\\AppData\\Roaming\\npm");
|
|
441
|
+
});
|
|
442
|
+
|
|
28
443
|
it("reports no install root for a source checkout", () => {
|
|
29
444
|
expect(findInstallRoot("/work/pragma/packages/cli/pragma/src/bin.ts")).toBe(
|
|
30
445
|
undefined,
|