@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
|
@@ -1,40 +1,146 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Storeless install-source
|
|
2
|
+
* Storeless install-source detector, shared by `info`, `upgrade`, and
|
|
3
3
|
* `doctor`'s `pragma version` check.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Detection is driven by the FILESYSTEM — the resolved entry path, the invoked
|
|
6
|
+
* bin path, and the symlinks between them — not by environment variables. The
|
|
7
|
+
* previous heuristic read `npm_config_user_agent`, which npm/pnpm/yarn/bun set
|
|
8
|
+
* only when THEY run a script; a user typing the bin name in a shell never has
|
|
9
|
+
* it, so on the path that mattered the detector always fell through to the
|
|
10
|
+
* honest runtime and reported `node` for every install. The agent survives
|
|
11
|
+
* only as a last-resort corroboration for a workspace install whose tree
|
|
12
|
+
* carries no other marker.
|
|
13
|
+
*
|
|
14
|
+
* The two paths that matter, and why both are needed:
|
|
15
|
+
*
|
|
16
|
+
* - `import.meta.url` is REALPATH-RESOLVED by the module loader. For a normal
|
|
17
|
+
* install it sits inside `node_modules/<pkg>`; for a linked development
|
|
18
|
+
* install (`npm link` / `bun link`) it has already escaped to the source
|
|
19
|
+
* checkout and contains no `node_modules` at all — which is precisely why an
|
|
20
|
+
* entry-only detector cannot see a link.
|
|
21
|
+
* - `process.argv[1]` preserves the path the user actually invoked, symlinks
|
|
22
|
+
* intact. Walking its link chain finds the `node_modules/<pkg>` entry the
|
|
23
|
+
* bin points through, and whether THAT directory is itself a symlink
|
|
24
|
+
* pointing outside the install root — the linked-install signal. The chain
|
|
25
|
+
* is only trusted when the package directory it names really contains the
|
|
26
|
+
* running module (`realPath` identity check), so a foreign bin on argv[1]
|
|
27
|
+
* (vitest, a wrapper script) never misattributes the install.
|
|
28
|
+
*
|
|
29
|
+
* The manager is identified by path SHAPE (`.bun/`, `.pnpm`/`pnpm`, `.volta`,
|
|
30
|
+
* `.asdf`, yarn's global dir), ephemeral runners by their cache shapes
|
|
31
|
+
* (`_npx`, `bunx-*`, pnpm's `dlx`), and a workspace's manager by the lockfile
|
|
32
|
+
* beside the install root. When no signal is conclusive the detector answers
|
|
33
|
+
* `unknown` — a state with NO update command, because a confidently wrong
|
|
34
|
+
* `npm i -g` is worse than none (against a linked checkout it would overwrite
|
|
35
|
+
* the link to the tree being developed). `pmUpdateCommand` accepts only the
|
|
36
|
+
* {@link GlobalInstall} arm, so offering a command to any other state is a
|
|
37
|
+
* type error, not a convention.
|
|
38
|
+
*
|
|
39
|
+
* Read-only and storeless: a handful of `lstat`/`readlink`/`realpath`/`exists`
|
|
40
|
+
* probes, bounded by the link-chain cap — nothing is ever written. The module
|
|
41
|
+
* sits on the fast-path static graph (via the shared barrel), so all probing
|
|
42
|
+
* happens at call time; module load stays inert.
|
|
43
|
+
*
|
|
44
|
+
* Windows note: segment matching splits on both separators, so the SHAPE
|
|
45
|
+
* checks (`managerFromPath`, `ephemeralRunner`) are exercised against
|
|
46
|
+
* win32-style fixtures — but scope containment goes through the host's
|
|
47
|
+
* `node:path` and, like `platformPaths.ts`, the win32 arm is a conventional
|
|
48
|
+
* best guess that has NOT been validated on a real Windows machine.
|
|
11
49
|
*/
|
|
12
50
|
|
|
13
|
-
import {
|
|
51
|
+
import { existsSync, readlinkSync, realpathSync } from "node:fs";
|
|
52
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
14
53
|
import { fileURLToPath } from "node:url";
|
|
54
|
+
import {
|
|
55
|
+
INSTALL_UPDATE_GUIDANCE,
|
|
56
|
+
installSourceLabel,
|
|
57
|
+
} from "../../kernel/render/vocabulary.js";
|
|
15
58
|
|
|
16
|
-
/** The
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
59
|
+
/** The package managers the detector can name (volta counts: it owns the
|
|
60
|
+
* install and its own update command). */
|
|
61
|
+
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun" | "volta";
|
|
62
|
+
|
|
63
|
+
/** The ephemeral runners — cache-resolved, with no meaningful upgrade. */
|
|
64
|
+
export type EphemeralRunner = "npx" | "bunx" | "pnpm dlx";
|
|
65
|
+
|
|
66
|
+
/** A global install by a known manager — the ONLY updatable state. */
|
|
67
|
+
export interface GlobalInstall {
|
|
68
|
+
readonly kind: "global";
|
|
69
|
+
readonly pm: PackageManager;
|
|
21
70
|
readonly label: string;
|
|
22
71
|
}
|
|
23
72
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
73
|
+
/** Installed as a dependency of the project the command runs in. */
|
|
74
|
+
export interface WorkspaceInstall {
|
|
75
|
+
readonly kind: "workspace";
|
|
76
|
+
/** The project's manager, when a lockfile/shape/agent names one. */
|
|
77
|
+
readonly pm?: PackageManager;
|
|
78
|
+
/** The project directory the package is installed into. */
|
|
79
|
+
readonly root: string;
|
|
80
|
+
readonly label: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A linked development install: `node_modules/<pkg>` is a symlink pointing
|
|
84
|
+
* OUTSIDE the install root (`npm link` / `bun link` / `pnpm link`). */
|
|
85
|
+
export interface LinkedInstall {
|
|
86
|
+
readonly kind: "linked";
|
|
87
|
+
/** The manager whose global tree hosts the link, when its shape names one. */
|
|
88
|
+
readonly pm?: PackageManager;
|
|
89
|
+
/** The resolved link target — the development checkout. */
|
|
90
|
+
readonly target: string;
|
|
91
|
+
readonly label: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** An `npx`/`bunx`/`pnpm dlx` run out of a runner cache. */
|
|
95
|
+
export interface EphemeralInstall {
|
|
96
|
+
readonly kind: "ephemeral";
|
|
97
|
+
readonly runner: EphemeralRunner;
|
|
98
|
+
readonly label: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** No conclusive signal — report the honest runtime and offer nothing. */
|
|
102
|
+
export interface UnknownInstall {
|
|
103
|
+
readonly kind: "unknown";
|
|
104
|
+
readonly runtime: "node" | "bun";
|
|
105
|
+
readonly label: string;
|
|
30
106
|
}
|
|
31
107
|
|
|
108
|
+
/** The install source — a closed union; only {@link GlobalInstall} updates. */
|
|
109
|
+
export type InstallSource =
|
|
110
|
+
| GlobalInstall
|
|
111
|
+
| WorkspaceInstall
|
|
112
|
+
| LinkedInstall
|
|
113
|
+
| EphemeralInstall
|
|
114
|
+
| UnknownInstall;
|
|
115
|
+
|
|
32
116
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
117
|
+
* Everything the classifier reads, captured as a value so tests drive any
|
|
118
|
+
* layout as a fixture — the `PlatformEnv` pattern from
|
|
119
|
+
* `@canonical/harnesses/platformPaths.ts`, with the filesystem probes
|
|
120
|
+
* injected the way `detectWsl` injects its `/proc/version` reader.
|
|
37
121
|
*/
|
|
122
|
+
export interface InstallProbe {
|
|
123
|
+
/** The running module's own path (`import.meta.url`, realpath-resolved). */
|
|
124
|
+
readonly entry: string;
|
|
125
|
+
/** The invoked script path (`process.argv[1]`), symlinks preserved. */
|
|
126
|
+
readonly invoked: string | undefined;
|
|
127
|
+
readonly cwd: string;
|
|
128
|
+
/** `npm_config_user_agent` — corroboration only, absent on a bare run. */
|
|
129
|
+
readonly userAgent: string | undefined;
|
|
130
|
+
readonly runtime: "node" | "bun";
|
|
131
|
+
/** A symlink's raw target, or `undefined` when not a symlink/unreadable. */
|
|
132
|
+
readonly readLink: (path: string) => string | undefined;
|
|
133
|
+
/** Fully-resolved real path, or `undefined` when unresolvable. */
|
|
134
|
+
readonly realPath: (path: string) => string | undefined;
|
|
135
|
+
readonly exists: (path: string) => boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Split on both separators, so shape checks read win32 paths too. */
|
|
139
|
+
const splitPath = (path: string): string[] => path.split(/[\\/]/);
|
|
140
|
+
|
|
141
|
+
/** The separator the input path itself uses (win32 fixtures keep theirs). */
|
|
142
|
+
const sepOf = (path: string): string => (path.includes("\\") ? "\\" : "/");
|
|
143
|
+
|
|
38
144
|
/**
|
|
39
145
|
* The directory a package was installed INTO — the parent of the `node_modules`
|
|
40
146
|
* holding it, so `/proj/node_modules/@scope/x/dist/bin.js` yields `/proj`.
|
|
@@ -48,9 +154,9 @@ function packageManager(): string {
|
|
|
48
154
|
* @remarks Exported for its unit test; not part of the package's public surface.
|
|
49
155
|
*/
|
|
50
156
|
export function findInstallRoot(entry: string): string | undefined {
|
|
51
|
-
const segments = entry
|
|
157
|
+
const segments = splitPath(entry);
|
|
52
158
|
const index = segments.lastIndexOf("node_modules");
|
|
53
|
-
return index === -1 ? undefined : segments.slice(0, index).join(
|
|
159
|
+
return index === -1 ? undefined : segments.slice(0, index).join(sepOf(entry));
|
|
54
160
|
}
|
|
55
161
|
|
|
56
162
|
/**
|
|
@@ -70,49 +176,278 @@ export function containsPath(directory: string, candidate: string): boolean {
|
|
|
70
176
|
return offset === "" || (!offset.startsWith("..") && !isAbsolute(offset));
|
|
71
177
|
}
|
|
72
178
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
return {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
179
|
+
/** A located installed package: its tree's root and its own directory. */
|
|
180
|
+
interface LocatedPackage {
|
|
181
|
+
/** The directory the package was installed into (parent of node_modules). */
|
|
182
|
+
readonly root: string;
|
|
183
|
+
/** The `node_modules/<pkg>` directory itself (scoped-aware). */
|
|
184
|
+
readonly pkgDir: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Locate the `node_modules` package a path sits inside — root and package
|
|
189
|
+
* directory, scope-aware. `node_modules/.bin/...` is NOT a package (the shim
|
|
190
|
+
* there is a symlink the chain walk should follow instead).
|
|
191
|
+
*/
|
|
192
|
+
function locatePackage(path: string): LocatedPackage | undefined {
|
|
193
|
+
const segments = splitPath(path);
|
|
194
|
+
const index = segments.lastIndexOf("node_modules");
|
|
195
|
+
if (index === -1) return undefined;
|
|
196
|
+
const first = segments[index + 1];
|
|
197
|
+
if (first === undefined || first === ".bin") return undefined;
|
|
198
|
+
const span = first.startsWith("@") ? 2 : 1;
|
|
199
|
+
if (span === 2 && segments[index + 2] === undefined) return undefined;
|
|
200
|
+
const sep = sepOf(path);
|
|
201
|
+
return {
|
|
202
|
+
root: segments.slice(0, index).join(sep),
|
|
203
|
+
pkgDir: segments.slice(0, index + 1 + span).join(sep),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The manager a path's shape names, or `undefined` when it names none. */
|
|
208
|
+
export function managerFromPath(path: string): PackageManager | undefined {
|
|
209
|
+
const segments = splitPath(path).map((s) => s.toLowerCase());
|
|
210
|
+
const has = (name: string): boolean => segments.includes(name);
|
|
211
|
+
if (has(".bun")) return "bun";
|
|
212
|
+
if (has(".pnpm") || has("pnpm")) return "pnpm";
|
|
213
|
+
if (
|
|
214
|
+
has(".volta") ||
|
|
215
|
+
segments.some((s, i) => s === "volta" && segments[i + 1] === "tools")
|
|
216
|
+
) {
|
|
217
|
+
return "volta";
|
|
218
|
+
}
|
|
219
|
+
// asdf/nvm host an ordinary npm global tree inside a versioned node install.
|
|
220
|
+
if (has(".asdf") || has(".nvm")) return "npm";
|
|
221
|
+
const yarn = segments.findIndex((s) => s === "yarn" || s === ".yarn");
|
|
222
|
+
if (yarn !== -1 && segments.indexOf("global", yarn) !== -1) return "yarn";
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The ephemeral runner a path's cache shape names, if any.
|
|
228
|
+
*
|
|
229
|
+
* Each pattern is anchored to the runner's ACTUAL cache layout, not to a bare
|
|
230
|
+
* directory name appearing anywhere in the path. A loose match reclassifies an
|
|
231
|
+
* ordinary project — `/work/dlx/app`, or anything under a directory someone
|
|
232
|
+
* named `_npx` — as ephemeral, and because that verdict is reached BEFORE the
|
|
233
|
+
* workspace containment check it wins even when a lockfile sits beside the
|
|
234
|
+
* root. The failure is quiet rather than loud: an ephemeral install correctly
|
|
235
|
+
* offers no upgrade command, so the user simply never learns how to update.
|
|
236
|
+
*/
|
|
237
|
+
export function ephemeralRunner(path: string): EphemeralRunner | undefined {
|
|
238
|
+
const segments = splitPath(path);
|
|
239
|
+
// npx caches at `<npm cache>/_npx/<hash>/…` — `~/.npm` on unix,
|
|
240
|
+
// `%LOCALAPPDATA%\npm-cache` on Windows.
|
|
241
|
+
const npx = segments.indexOf("_npx");
|
|
242
|
+
const npxParent = npx > 0 ? segments[npx - 1] : undefined;
|
|
243
|
+
if (npxParent === ".npm" || npxParent === "npm-cache") return "npx";
|
|
244
|
+
// bunx materialises `$TMPDIR/bunx-<uid>-<pkg>@<ver>/…`; requiring the uid
|
|
245
|
+
// keeps a hand-made `bunx-tools` directory from matching.
|
|
246
|
+
if (segments.some((s) => /^bunx-\d+-/.test(s))) return "bunx";
|
|
247
|
+
// pnpm caches at `<pnpm cache>/dlx/<hash>/…`.
|
|
248
|
+
const dlx = segments.indexOf("dlx");
|
|
249
|
+
if (dlx > 0 && segments[dlx - 1] === "pnpm") return "pnpm dlx";
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The lockfile beside an install root, mapped to its manager. */
|
|
254
|
+
function lockfileManager(
|
|
255
|
+
root: string,
|
|
256
|
+
exists: InstallProbe["exists"],
|
|
257
|
+
sep: string,
|
|
258
|
+
): PackageManager | undefined {
|
|
259
|
+
const beside = (name: string): boolean => exists(`${root}${sep}${name}`);
|
|
260
|
+
if (beside("bun.lock") || beside("bun.lockb")) return "bun";
|
|
261
|
+
if (beside("pnpm-lock.yaml")) return "pnpm";
|
|
262
|
+
if (beside("yarn.lock")) return "yarn";
|
|
263
|
+
if (beside("package-lock.json")) return "npm";
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The manager `npm_config_user_agent` names, when present and recognized. */
|
|
268
|
+
function agentManager(agent: string | undefined): PackageManager | undefined {
|
|
269
|
+
const name = agent?.split("/")[0];
|
|
270
|
+
return name === "npm" || name === "pnpm" || name === "yarn" || name === "bun"
|
|
271
|
+
? name
|
|
272
|
+
: undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The link-chain hop cap — a symlink cycle must not spin the detector. */
|
|
276
|
+
const MAX_LINK_HOPS = 10;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Walk `argv[1]`'s symlink chain to the `node_modules` entry it points
|
|
280
|
+
* through, and trust it only when that package directory really contains the
|
|
281
|
+
* running module. Returns the FIRST chain path inside a package — before the
|
|
282
|
+
* package-level symlink is resolved away, which is where the linked-install
|
|
283
|
+
* signal lives.
|
|
284
|
+
*/
|
|
285
|
+
function resolveInvokedEntry(probe: InstallProbe): string | undefined {
|
|
286
|
+
if (probe.invoked === undefined) return undefined;
|
|
287
|
+
let path = resolve(probe.cwd, probe.invoked);
|
|
288
|
+
for (let hop = 0; hop < MAX_LINK_HOPS; hop++) {
|
|
289
|
+
const located = locatePackage(path);
|
|
290
|
+
if (located !== undefined) {
|
|
291
|
+
// Identity check: only OUR package's bin may classify this process.
|
|
292
|
+
const real = probe.realPath(located.pkgDir);
|
|
293
|
+
return real !== undefined && containsPath(real, probe.entry)
|
|
294
|
+
? path
|
|
295
|
+
: undefined;
|
|
296
|
+
}
|
|
297
|
+
const target = probe.readLink(path);
|
|
298
|
+
if (target === undefined) return undefined;
|
|
299
|
+
path = resolve(dirname(path), target);
|
|
300
|
+
}
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const unknownInstall = (runtime: "node" | "bun"): UnknownInstall => ({
|
|
305
|
+
kind: "unknown",
|
|
306
|
+
runtime,
|
|
307
|
+
label: installSourceLabel("unknown", runtime),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Classify an install from a captured probe — the PURE core of
|
|
312
|
+
* {@link detectInstallSource}, exercised by the fixture matrix in its test.
|
|
313
|
+
*
|
|
314
|
+
* @param probe - The captured paths, environment, and filesystem readers.
|
|
315
|
+
* @returns The classified install source.
|
|
316
|
+
*/
|
|
317
|
+
export function classifyInstall(probe: InstallProbe): InstallSource {
|
|
318
|
+
const invokedEntry = resolveInvokedEntry(probe);
|
|
319
|
+
const installed =
|
|
320
|
+
invokedEntry ??
|
|
321
|
+
(locatePackage(probe.entry) !== undefined ? probe.entry : undefined);
|
|
322
|
+
if (installed === undefined) return unknownInstall(probe.runtime);
|
|
323
|
+
const located = locatePackage(installed);
|
|
324
|
+
/* v8 ignore next -- `installed` is only set when locatePackage succeeded */
|
|
325
|
+
if (located === undefined) return unknownInstall(probe.runtime);
|
|
326
|
+
const { root, pkgDir } = located;
|
|
327
|
+
|
|
328
|
+
// A package dir that is a symlink pointing OUTSIDE its install root is a
|
|
329
|
+
// linked development install (`npm link`/`bun link`). Inside the root it is
|
|
330
|
+
// a store or workspace layout (pnpm's `.pnpm`, a monorepo package) — normal.
|
|
331
|
+
const rawTarget = probe.readLink(pkgDir);
|
|
332
|
+
if (rawTarget !== undefined) {
|
|
333
|
+
const target = resolve(dirname(pkgDir), rawTarget);
|
|
334
|
+
if (!containsPath(root, target)) {
|
|
335
|
+
const pm = managerFromPath(installed);
|
|
336
|
+
return {
|
|
337
|
+
kind: "linked",
|
|
338
|
+
...(pm !== undefined ? { pm } : {}),
|
|
339
|
+
target,
|
|
340
|
+
label: installSourceLabel("linked", pm),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const runner = ephemeralRunner(installed);
|
|
346
|
+
if (runner !== undefined) {
|
|
347
|
+
return {
|
|
348
|
+
kind: "ephemeral",
|
|
349
|
+
runner,
|
|
350
|
+
label: installSourceLabel("ephemeral", runner),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const shaped = managerFromPath(installed);
|
|
355
|
+
if (containsPath(root, resolve(probe.cwd))) {
|
|
356
|
+
// A workspace tree can belong to any manager; the lockfile beside its
|
|
357
|
+
// root is the marker, the user-agent (when a script runner set it) the
|
|
358
|
+
// corroboration of last resort.
|
|
359
|
+
const pm =
|
|
360
|
+
shaped ??
|
|
361
|
+
lockfileManager(root, probe.exists, sepOf(installed)) ??
|
|
362
|
+
agentManager(probe.userAgent);
|
|
363
|
+
return {
|
|
364
|
+
kind: "workspace",
|
|
365
|
+
...(pm !== undefined ? { pm } : {}),
|
|
366
|
+
root,
|
|
367
|
+
label: installSourceLabel("workspace", pm),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// A global tree with none of the distinctive shapes is npm's: bun, pnpm,
|
|
372
|
+
// volta and yarn all mark their global roots, npm (and nvm/asdf node
|
|
373
|
+
// installs, which npm serves) do not. The user-agent is NOT consulted here —
|
|
374
|
+
// it names whoever ran the script (`bun run …`), not who installed the bin.
|
|
375
|
+
const pm = shaped ?? "npm";
|
|
376
|
+
return { kind: "global", pm, label: installSourceLabel("global", pm) };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Capture the live process into a probe (the only impure code here). */
|
|
380
|
+
function liveProbe(): InstallProbe {
|
|
381
|
+
return {
|
|
382
|
+
entry: fileURLToPath(import.meta.url),
|
|
383
|
+
invoked: process.argv[1],
|
|
384
|
+
cwd: process.cwd(),
|
|
385
|
+
userAgent: process.env.npm_config_user_agent,
|
|
386
|
+
runtime: process.versions.bun ? "bun" : "node",
|
|
387
|
+
readLink: (path) => {
|
|
388
|
+
try {
|
|
389
|
+
return readlinkSync(path);
|
|
390
|
+
} catch {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
realPath: (path) => {
|
|
395
|
+
try {
|
|
396
|
+
return realpathSync(path);
|
|
397
|
+
} catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
exists: (path) => existsSync(path),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Detect how the binary was installed.
|
|
407
|
+
*
|
|
408
|
+
* @param probe - Injected probe (tests); defaults to the live process.
|
|
409
|
+
* @returns The classified install source.
|
|
410
|
+
* @note Impure by default — reads `import.meta.url`, `process`, and a bounded
|
|
411
|
+
* handful of symlink/realpath/exists probes. Never writes.
|
|
412
|
+
*/
|
|
413
|
+
export function detectInstallSource(
|
|
414
|
+
probe: InstallProbe = liveProbe(),
|
|
415
|
+
): InstallSource {
|
|
416
|
+
return classifyInstall(probe);
|
|
417
|
+
}
|
|
100
418
|
|
|
101
419
|
/** The global-update command per package manager (install-style). */
|
|
102
|
-
const PM_UPDATE_COMMAND: Record<
|
|
420
|
+
const PM_UPDATE_COMMAND: Record<PackageManager, (pkg: string) => string> = {
|
|
103
421
|
bun: (pkg) => `bun add -g ${pkg}`,
|
|
104
|
-
npm:
|
|
422
|
+
npm: (pkg) => `npm i -g ${pkg}`,
|
|
105
423
|
pnpm: (pkg) => `pnpm add -g ${pkg}`,
|
|
106
424
|
yarn: (pkg) => `yarn global add ${pkg}`,
|
|
425
|
+
volta: (pkg) => `volta install ${pkg}`,
|
|
107
426
|
};
|
|
108
427
|
|
|
109
428
|
/**
|
|
110
|
-
* The command that updates `pkg
|
|
429
|
+
* The command that updates a GLOBAL install of `pkg`. Accepting only the
|
|
430
|
+
* {@link GlobalInstall} arm is the point: a linked, ephemeral, workspace, or
|
|
431
|
+
* unknown install has no sanctioned command, and asking for one is a type
|
|
432
|
+
* error — {@link updateGuidance} is what those states say instead.
|
|
111
433
|
*
|
|
112
|
-
* @param
|
|
434
|
+
* @param install - The detected global install.
|
|
113
435
|
* @param pkg - The package to update.
|
|
114
|
-
* @returns The shell command
|
|
436
|
+
* @returns The shell command.
|
|
437
|
+
*/
|
|
438
|
+
export function pmUpdateCommand(install: GlobalInstall, pkg: string): string {
|
|
439
|
+
return PM_UPDATE_COMMAND[install.pm](pkg);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The honest sentence a command-less install state shows when a newer release
|
|
444
|
+
* exists — see {@link INSTALL_UPDATE_GUIDANCE} for the words.
|
|
445
|
+
*
|
|
446
|
+
* @param install - Any non-global install.
|
|
447
|
+
* @returns The guidance sentence for its state.
|
|
115
448
|
*/
|
|
116
|
-
export function
|
|
117
|
-
|
|
449
|
+
export function updateGuidance(
|
|
450
|
+
install: Exclude<InstallSource, GlobalInstall>,
|
|
451
|
+
): string {
|
|
452
|
+
return INSTALL_UPDATE_GUIDANCE[install.kind];
|
|
118
453
|
}
|