@vltpkg/graph 1.3.8 → 1.5.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 +59 -3
- package/dist/actual/load.js +28 -12
- package/dist/build.js +30 -5
- package/dist/dependencies.d.ts +11 -0
- package/dist/dependencies.js +47 -7
- package/dist/filter-nodes-by-query.d.ts +39 -0
- package/dist/filter-nodes-by-query.js +100 -0
- package/dist/ideal/append-nodes.js +19 -6
- package/dist/ideal/build-ideal-from-starting-graph.js +8 -0
- package/dist/ideal/get-importer-specs.d.ts +1 -0
- package/dist/ideal/get-importer-specs.js +27 -7
- package/dist/ideal/refresh-ideal-graph.js +6 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/install.d.ts +23 -8
- package/dist/install.js +46 -1
- package/dist/lockfile/load-edges.js +10 -4
- package/dist/modifiers.d.ts +7 -0
- package/dist/modifiers.js +24 -0
- package/dist/reify/build.d.ts +7 -0
- package/dist/reify/check-needed-build.d.ts +5 -0
- package/dist/reify/check-needed-build.js +13 -0
- package/dist/reify/index.js +9 -39
- package/dist/reify/update-importers-package-json.js +5 -9
- package/dist/skills/dirs.d.ts +58 -0
- package/dist/skills/dirs.js +211 -0
- package/dist/skills/discover.d.ts +73 -0
- package/dist/skills/discover.js +234 -0
- package/dist/skills/gitignore.d.ts +13 -0
- package/dist/skills/gitignore.js +53 -0
- package/dist/skills/index.d.ts +123 -0
- package/dist/skills/index.js +225 -0
- package/dist/skills/lazy.d.ts +8 -0
- package/dist/skills/lazy.js +8 -0
- package/dist/skills/mask.d.ts +26 -0
- package/dist/skills/mask.js +43 -0
- package/dist/skills/mounts.d.ts +64 -0
- package/dist/skills/mounts.js +136 -0
- package/dist/uninstall.d.ts +24 -7
- package/dist/uninstall.js +26 -1
- package/dist/update.d.ts +24 -4
- package/dist/update.js +24 -1
- package/dist/visualization/mermaid-output.d.ts +8 -0
- package/dist/visualization/mermaid-output.js +25 -8
- package/package.json +21 -21
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { load as loadActual } from "../actual/load.js";
|
|
3
|
+
import { filterNodesByQueryParts } from "../filter-nodes-by-query.js";
|
|
4
|
+
import { unshare } from "../reify/unshare.js";
|
|
5
|
+
import { skillsDirs } from "./dirs.js";
|
|
6
|
+
import { discoverSkills } from "./discover.js";
|
|
7
|
+
import { editGitignore } from "./gitignore.js";
|
|
8
|
+
import { maskSkills } from "./mask.js";
|
|
9
|
+
import { mountSkill, packageKey, readMounts, samePath, unmountSkill, } from "./mounts.js";
|
|
10
|
+
export * from "./discover.js";
|
|
11
|
+
const lc = (s) => s.toLowerCase();
|
|
12
|
+
const selectNodes = async (o) => {
|
|
13
|
+
const graph = o.graph ?? loadActual({ ...o, loadManifests: true });
|
|
14
|
+
const { ids, parts, unscanned } = await filterNodesByQueryParts(graph, o.target ?? '*');
|
|
15
|
+
const nodes = [...graph.nodes.values()].filter(n => ids.has(n.id));
|
|
16
|
+
return { graph, nodes, parts, unscanned };
|
|
17
|
+
};
|
|
18
|
+
const dirsOf = (o, create = false) => skillsDirs({
|
|
19
|
+
projectRoot: o.projectRoot,
|
|
20
|
+
configured: o.skillsDirs,
|
|
21
|
+
env: o.env,
|
|
22
|
+
create,
|
|
23
|
+
});
|
|
24
|
+
// packages behind live vlt links
|
|
25
|
+
const liveOwners = (mounts) => mounts.flatMap(m => m.owner && existsSync(m.target) ? [m.owner] : []);
|
|
26
|
+
// private copies of store-hardlinked files: edits via skill links
|
|
27
|
+
// must not reach the global store
|
|
28
|
+
const unshareSkills = async (graph, ids, scurry) => {
|
|
29
|
+
for (const id of new Set(ids)) {
|
|
30
|
+
const node = graph.nodes.get(id);
|
|
31
|
+
if (node?.inVltStore())
|
|
32
|
+
await unshare(node.resolvedLocation(scurry));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
// `vlt ls` style
|
|
36
|
+
const label = (s) => {
|
|
37
|
+
const pkg = s.version ? `${s.name}@${s.version}` : s.name;
|
|
38
|
+
return s.alias ? `${s.alias} (${pkg})` : pkg;
|
|
39
|
+
};
|
|
40
|
+
// apply the mask; fail closed: skills of `unscanned` pkgs are dropped,
|
|
41
|
+
// and reported unless another package's skill takes the name
|
|
42
|
+
const pick = (found, parts, unscanned) => {
|
|
43
|
+
const { skills: masked, unmatched } = maskSkills(found, parts);
|
|
44
|
+
const ok = (s) => !unscanned?.ids.has(s.id);
|
|
45
|
+
const picked = masked.filter(ok);
|
|
46
|
+
const names = new Set(picked.map(s => lc(s.skill)));
|
|
47
|
+
const dropped = masked.filter(s => !ok(s) && !names.has(lc(s.skill)));
|
|
48
|
+
return { skills: found.filter(ok), picked, unmatched, dropped };
|
|
49
|
+
};
|
|
50
|
+
const report = (dropped, unscanned) => unscanned && dropped.length ?
|
|
51
|
+
{
|
|
52
|
+
unscanned: dropped.map(s => `${s.skill} (${label(s)})`),
|
|
53
|
+
unscannedReason: unscanned.reason,
|
|
54
|
+
}
|
|
55
|
+
: {};
|
|
56
|
+
/**
|
|
57
|
+
* List the agent skills shipped by installed packages (`:skill()`
|
|
58
|
+
* applied, `unscanned` left out), and the agent skill dirs. Creates
|
|
59
|
+
* nothing.
|
|
60
|
+
*/
|
|
61
|
+
export const listSkills = async (o) => {
|
|
62
|
+
const { graph, nodes, parts, unscanned } = await selectNodes(o);
|
|
63
|
+
const { targets, scan, skipped } = dirsOf(o);
|
|
64
|
+
const { skills } = discoverSkills(nodes, o.scurry, readMounts(scan));
|
|
65
|
+
const { picked, dropped } = pick(skills, parts, unscanned);
|
|
66
|
+
return {
|
|
67
|
+
project: graph.mainImporter.name,
|
|
68
|
+
graph,
|
|
69
|
+
skills: picked,
|
|
70
|
+
dirs: targets.map(d => d.dir),
|
|
71
|
+
...(skipped.length ? { skipped } : null),
|
|
72
|
+
...report(dropped, unscanned),
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Link the agent skills of the selected packages into each of the
|
|
77
|
+
* project's agent skill dirs (see `skillsDirs`), created only when a
|
|
78
|
+
* skill is picked. `target` selectors may end with `:skill()`. One
|
|
79
|
+
* package per name, among picked skills, in every dir: the package of
|
|
80
|
+
* the first existing link (in dir order), else rank (main importer
|
|
81
|
+
* dep, importer dep, transitive), then DepID; the others are
|
|
82
|
+
* reported. Installs keep a dir's link to another installed skill.
|
|
83
|
+
* A link follows its package to the picked version, in every scanned
|
|
84
|
+
* dir. Then remove vlt links whose skill is no longer installed (with
|
|
85
|
+
* `apply`: every vlt link not made here), and keep each dir's
|
|
86
|
+
* `.gitignore` block in sync. Linked packages get private copies of
|
|
87
|
+
* files hardlinked from the global store. Security queries fail
|
|
88
|
+
* closed: skills of packages with no security data are not linked,
|
|
89
|
+
* and reported in `unscanned` unless still linked.
|
|
90
|
+
*/
|
|
91
|
+
export const linkSkills = async (o) => {
|
|
92
|
+
const { graph, nodes, parts, unscanned } = await selectNodes(o);
|
|
93
|
+
const { skills: found, scanned } = discoverSkills(nodes, o.scurry);
|
|
94
|
+
const { skills, picked, unmatched, dropped } = pick(found, parts, unscanned);
|
|
95
|
+
const inMask = new Set(picked);
|
|
96
|
+
const { targets, scan, skipped } = dirsOf(o, picked.length > 0);
|
|
97
|
+
const res = {
|
|
98
|
+
linked: [],
|
|
99
|
+
unchanged: [],
|
|
100
|
+
removed: [],
|
|
101
|
+
conflicts: [],
|
|
102
|
+
dirs: targets.map(d => d.dir),
|
|
103
|
+
};
|
|
104
|
+
const mounts = readMounts(scan);
|
|
105
|
+
const byName = new Map(mounts.map(m => [`${m.dir.dir}\0${lc(m.name)}`, m]));
|
|
106
|
+
// the link's skill is no longer installed: dangling, orphan owner,
|
|
107
|
+
// or a scanned owner that no longer ships it
|
|
108
|
+
const stale = (m) => !m.owner ||
|
|
109
|
+
!graph.nodes.has(m.owner) ||
|
|
110
|
+
!existsSync(m.target) ||
|
|
111
|
+
scanned
|
|
112
|
+
.get(m.owner)
|
|
113
|
+
?.some(f => lc(f.skill) === lc(m.name) && samePath(f.path, m.target)) === false;
|
|
114
|
+
const groups = new Map();
|
|
115
|
+
for (const s of skills) {
|
|
116
|
+
const k = lc(s.skill);
|
|
117
|
+
groups.set(k, [...(groups.get(k) ?? []), s]);
|
|
118
|
+
}
|
|
119
|
+
// the link at `<dir>/<k>`, and its package's skill in `group`
|
|
120
|
+
const at = (dir, k, group) => {
|
|
121
|
+
const m = byName.get(`${dir.dir}\0${k}`);
|
|
122
|
+
const key = m?.owner && packageKey(m.owner);
|
|
123
|
+
return { m, own: group.find(s => packageKey(s.id) === key) };
|
|
124
|
+
};
|
|
125
|
+
// picked skills only: the others never take a name
|
|
126
|
+
const cands = (group) => group.filter(s => inMask.has(s));
|
|
127
|
+
// one per name: the first linked package keeps it, re-pointed if needed
|
|
128
|
+
const first = new Map([...groups].map(([k, group]) => {
|
|
129
|
+
const c = cands(group);
|
|
130
|
+
return [
|
|
131
|
+
k,
|
|
132
|
+
targets.map(d => at(d, k, c).own).find(s => !!s) ?? c[0],
|
|
133
|
+
];
|
|
134
|
+
}));
|
|
135
|
+
const isTarget = new Set(targets);
|
|
136
|
+
const picks = [];
|
|
137
|
+
for (const dir of scan) {
|
|
138
|
+
for (const [k, group] of groups) {
|
|
139
|
+
const { m, own } = at(dir, k, group);
|
|
140
|
+
if (isTarget.has(dir)) {
|
|
141
|
+
// installs keep a link to another installed skill
|
|
142
|
+
const winner = o.apply ?
|
|
143
|
+
first.get(k)
|
|
144
|
+
: (own ?? (m && !stale(m) ? undefined : first.get(k)));
|
|
145
|
+
picks.push({
|
|
146
|
+
dir,
|
|
147
|
+
winner,
|
|
148
|
+
losers: cands(group).filter(s => s !== winner),
|
|
149
|
+
m,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else if (own && !o.apply) {
|
|
153
|
+
// no longer a target: only re-pointed
|
|
154
|
+
picks.push({ dir, winner: own, losers: [], m });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await unshareSkills(graph, [
|
|
159
|
+
...picks.flatMap(({ winner }) => (winner ? [winner.id] : [])),
|
|
160
|
+
...liveOwners(mounts),
|
|
161
|
+
], o.scurry);
|
|
162
|
+
const gone = [];
|
|
163
|
+
// names taken over by entries not vlt's
|
|
164
|
+
const foreign = [];
|
|
165
|
+
const kept = new Set();
|
|
166
|
+
const where = new Map();
|
|
167
|
+
for (const { dir, winner, losers, m } of picks) {
|
|
168
|
+
if (winner) {
|
|
169
|
+
// replaced link named in another case (case-sensitive fs)
|
|
170
|
+
if (m && m.name !== winner.skill) {
|
|
171
|
+
unmountSkill(m);
|
|
172
|
+
gone.push(m);
|
|
173
|
+
}
|
|
174
|
+
const mount = `${dir.dir}/${winner.skill}`;
|
|
175
|
+
const r = mountSkill(winner, dir);
|
|
176
|
+
if (r === 'conflict') {
|
|
177
|
+
res.conflicts.push(mount);
|
|
178
|
+
foreign.push({ dir, name: winner.skill });
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const k = `${winner.id}\0${winner.skill}`;
|
|
182
|
+
const linked = where.get(k) ?? [];
|
|
183
|
+
linked.push(dir.dir);
|
|
184
|
+
where.set(k, linked);
|
|
185
|
+
res[r].push({ ...winner, linked, dir: dir.dir, mount });
|
|
186
|
+
kept.add(mount);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const s of losers)
|
|
190
|
+
res.conflicts.push(`${dir.dir}/${s.skill} (${label(s)})`);
|
|
191
|
+
}
|
|
192
|
+
const after = readMounts(scan);
|
|
193
|
+
const present = [];
|
|
194
|
+
for (const m of after) {
|
|
195
|
+
if (o.apply ? kept.has(m.mount) : !stale(m)) {
|
|
196
|
+
present.push(m);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
unmountSkill(m);
|
|
200
|
+
gone.push(m);
|
|
201
|
+
res.removed.push(m.mount);
|
|
202
|
+
}
|
|
203
|
+
const gitignore = [];
|
|
204
|
+
for (const dir of scan) {
|
|
205
|
+
const add = present.filter(m => m.dir === dir).map(m => m.name);
|
|
206
|
+
const remove = [...gone, ...foreign]
|
|
207
|
+
.filter(m => m.dir === dir && !add.includes(m.name))
|
|
208
|
+
.map(m => m.name);
|
|
209
|
+
if ((add.length || remove.length) &&
|
|
210
|
+
editGitignore(dir.real, { add, remove }) === 'refused') {
|
|
211
|
+
gitignore.push(`${dir.dir}/.gitignore`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (skipped.length)
|
|
215
|
+
res.skipped = skipped;
|
|
216
|
+
if (gitignore.length)
|
|
217
|
+
res.gitignore = gitignore;
|
|
218
|
+
if (unmatched.length)
|
|
219
|
+
res.unmatched = unmatched;
|
|
220
|
+
// not left out: still linked in every dir (installs keep links)
|
|
221
|
+
const left = dropped.filter(s => !targets.every(d => present.some(m => m.dir === d &&
|
|
222
|
+
m.owner === s.id &&
|
|
223
|
+
lc(m.name) === lc(s.skill))));
|
|
224
|
+
return Object.assign(res, report(left, unscanned));
|
|
225
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HasSkills } from '@vltpkg/query';
|
|
2
|
+
import type * as Skills from './index.ts';
|
|
3
|
+
/** `listSkills`, loading the skills code on first use. */
|
|
4
|
+
export declare const listSkills: typeof Skills.listSkills;
|
|
5
|
+
/** `linkSkills`, loading the skills code on first use. */
|
|
6
|
+
export declare const linkSkills: typeof Skills.linkSkills;
|
|
7
|
+
/** `:skills` provider: loads the skills code on first `Node`. */
|
|
8
|
+
export declare const hasSkills: HasSkills;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isNode } from "../node.js";
|
|
2
|
+
const load = () => import("./index.js");
|
|
3
|
+
/** `listSkills`, loading the skills code on first use. */
|
|
4
|
+
export const listSkills = async (o) => (await load()).listSkills(o);
|
|
5
|
+
/** `linkSkills`, loading the skills code on first use. */
|
|
6
|
+
export const linkSkills = async (o) => (await load()).linkSkills(o);
|
|
7
|
+
/** `:skills` provider: loads the skills code on first `Node`. */
|
|
8
|
+
export const hasSkills = async (node) => isNode(node) && (await load()).shipsSkills(node);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { QueryPartIds } from '../filter-nodes-by-query.ts';
|
|
2
|
+
import type { Skill } from './discover.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A `:skill()` pattern that matched no skill of its selector's
|
|
5
|
+
* packages.
|
|
6
|
+
*/
|
|
7
|
+
export type UnmatchedPattern = {
|
|
8
|
+
pattern: string;
|
|
9
|
+
/** skills those packages ship, sorted; unset: no package selected */
|
|
10
|
+
skills?: string[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Whole-name, case-insensitive; `*` = any run of characters (incl.
|
|
14
|
+
* none).
|
|
15
|
+
*/
|
|
16
|
+
export declare const globRegExp: (pattern: string) => RegExp;
|
|
17
|
+
/**
|
|
18
|
+
* Apply an `allow-skills` mask. A skill is picked when some part matched
|
|
19
|
+
* its package (any version) and has no patterns, or one of its patterns
|
|
20
|
+
* matches the skill (parts are a union). Also returns the patterns that
|
|
21
|
+
* matched nothing.
|
|
22
|
+
*/
|
|
23
|
+
export declare const maskSkills: (skills: Skill[], parts: QueryPartIds[]) => {
|
|
24
|
+
skills: Skill[];
|
|
25
|
+
unmatched: UnmatchedPattern[];
|
|
26
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { packageKey } from "./mounts.js";
|
|
2
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3
|
+
/**
|
|
4
|
+
* Whole-name, case-insensitive; `*` = any run of characters (incl.
|
|
5
|
+
* none).
|
|
6
|
+
*/
|
|
7
|
+
export const globRegExp = (pattern) => new RegExp(`^${pattern.split('*').map(escape).join('.*')}$`, 'i');
|
|
8
|
+
const matches = (re, s) => re.test(s.skill) ||
|
|
9
|
+
(!!s.frontmatterName && re.test(s.frontmatterName));
|
|
10
|
+
/**
|
|
11
|
+
* Apply an `allow-skills` mask. A skill is picked when some part matched
|
|
12
|
+
* its package (any version) and has no patterns, or one of its patterns
|
|
13
|
+
* matches the skill (parts are a union). Also returns the patterns that
|
|
14
|
+
* matched nothing.
|
|
15
|
+
*/
|
|
16
|
+
export const maskSkills = (skills, parts) => {
|
|
17
|
+
const compiled = parts.map(p => ({
|
|
18
|
+
keys: new Set([...p.ids].map(packageKey)),
|
|
19
|
+
patterns: p.patterns?.map(pattern => ({
|
|
20
|
+
pattern,
|
|
21
|
+
re: globRegExp(pattern),
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
const picked = skills.filter(s => compiled.some(p => p.keys.has(packageKey(s.id)) &&
|
|
25
|
+
(!p.patterns || p.patterns.some(({ re }) => matches(re, s)))));
|
|
26
|
+
const unmatched = [];
|
|
27
|
+
for (const p of compiled) {
|
|
28
|
+
if (!p.patterns)
|
|
29
|
+
continue;
|
|
30
|
+
const own = skills.filter(s => p.keys.has(packageKey(s.id)));
|
|
31
|
+
for (const { pattern, re } of p.patterns) {
|
|
32
|
+
if (own.some(s => matches(re, s)))
|
|
33
|
+
continue;
|
|
34
|
+
unmatched.push({
|
|
35
|
+
pattern,
|
|
36
|
+
...(p.keys.size > 0 && {
|
|
37
|
+
skills: [...new Set(own.map(s => s.skill))].sort((a, b) => a.localeCompare(b, 'en')),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { skills: picked, unmatched };
|
|
43
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Stats } from 'node:fs';
|
|
2
|
+
import type { DepID } from '@vltpkg/dep-id';
|
|
3
|
+
import type { SkillsDir } from './dirs.ts';
|
|
4
|
+
import type { Skill } from './discover.ts';
|
|
5
|
+
/**
|
|
6
|
+
* A vlt link in an agent skills dir.
|
|
7
|
+
*/
|
|
8
|
+
export type SkillMount = {
|
|
9
|
+
/** the dir holding the link */
|
|
10
|
+
dir: SkillsDir;
|
|
11
|
+
/** entry name */
|
|
12
|
+
name: string;
|
|
13
|
+
/** `<dir>/<name>`, `/`-separated, relative to projectRoot */
|
|
14
|
+
mount: string;
|
|
15
|
+
/** absolute link path, in the real dir */
|
|
16
|
+
link: string;
|
|
17
|
+
/** absolute resolved link target */
|
|
18
|
+
target: string;
|
|
19
|
+
/** DepID of the store dir the link points into */
|
|
20
|
+
owner?: DepID;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Outcome of mounting a single skill.
|
|
24
|
+
*/
|
|
25
|
+
export type MountResult = 'linked' | 'unchanged' | 'conflict';
|
|
26
|
+
/** `lstat`, undefined when missing or unreadable (eg, ENOTDIR) */
|
|
27
|
+
export declare const lstat: (path: string) => Stats | undefined;
|
|
28
|
+
/** Resolved path, lowercased on win32. */
|
|
29
|
+
export declare const norm: (p: string) => string;
|
|
30
|
+
/** realpath, else the resolved path when missing. */
|
|
31
|
+
export declare const realOr: (p: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Same real path? Case-insensitive on win32.
|
|
34
|
+
*/
|
|
35
|
+
export declare const samePath: (a: string, b: string) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The target of `link` if it is a symlink (or junction) pointing into
|
|
38
|
+
* a `node_modules/.vlt` store, otherwise undefined. Any store, not just
|
|
39
|
+
* the project's: win32 junctions are absolute, so they point to the old
|
|
40
|
+
* store after the project is moved or copied.
|
|
41
|
+
*/
|
|
42
|
+
export declare const mountTarget: (link: string) => string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The DepID of the last `node_modules/.vlt/<DepID>` segment of a raw
|
|
45
|
+
* link target, if valid.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ownerOf: (target: string) => DepID | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* One key per package: registry + name, else the DepID w/o its extra.
|
|
50
|
+
*/
|
|
51
|
+
export declare const packageKey: (id: DepID) => string;
|
|
52
|
+
/**
|
|
53
|
+
* All vlt links in the given dirs. Anything else is ignored.
|
|
54
|
+
*/
|
|
55
|
+
export declare const readMounts: (scan: SkillsDir[]) => SkillMount[];
|
|
56
|
+
/**
|
|
57
|
+
* Link a skill at `<dir>/<skill>`. Never touches anything not managed by
|
|
58
|
+
* vlt: returns `'conflict'` instead.
|
|
59
|
+
*/
|
|
60
|
+
export declare const mountSkill: (skill: Pick<Skill, "skill" | "path">, dir: SkillsDir) => MountResult;
|
|
61
|
+
/**
|
|
62
|
+
* Remove a vlt link. Its dir is kept.
|
|
63
|
+
*/
|
|
64
|
+
export declare const unmountSkill: (m: SkillMount) => void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { lstatSync, readdirSync, readlinkSync, realpathSync, unlinkSync, } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { baseDepID, isDepID, joinDepIDTuple, splitDepID, } from '@vltpkg/dep-id';
|
|
4
|
+
import { symlinkSyncMkdirp } from "../reify/symlink-sync.js";
|
|
5
|
+
/** `lstat`, undefined when missing or unreadable (eg, ENOTDIR) */
|
|
6
|
+
export const lstat = (path) => {
|
|
7
|
+
try {
|
|
8
|
+
return lstatSync(path, { throwIfNoEntry: false });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/** Resolved path, lowercased on win32. */
|
|
15
|
+
export const norm = (p) => {
|
|
16
|
+
const r = resolve(p);
|
|
17
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
18
|
+
};
|
|
19
|
+
/** realpath, else the resolved path when missing. */
|
|
20
|
+
export const realOr = (p) => {
|
|
21
|
+
try {
|
|
22
|
+
return realpathSync(p);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return resolve(p);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Same real path? Case-insensitive on win32.
|
|
30
|
+
*/
|
|
31
|
+
export const samePath = (a, b) => norm(realOr(a)) === norm(realOr(b));
|
|
32
|
+
const storeSeg = `${sep}node_modules${sep}.vlt${sep}`;
|
|
33
|
+
/**
|
|
34
|
+
* The target of `link` if it is a symlink (or junction) pointing into
|
|
35
|
+
* a `node_modules/.vlt` store, otherwise undefined. Any store, not just
|
|
36
|
+
* the project's: win32 junctions are absolute, so they point to the old
|
|
37
|
+
* store after the project is moved or copied.
|
|
38
|
+
*/
|
|
39
|
+
export const mountTarget = (link) => {
|
|
40
|
+
if (!lstat(link)?.isSymbolicLink())
|
|
41
|
+
return undefined;
|
|
42
|
+
let raw = readlinkSync(link);
|
|
43
|
+
if (raw.startsWith('\\\\?\\'))
|
|
44
|
+
raw = raw.slice(4);
|
|
45
|
+
const target = resolve(dirname(link), raw);
|
|
46
|
+
return norm(target).includes(storeSeg) ? target : undefined;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The DepID of the last `node_modules/.vlt/<DepID>` segment of a raw
|
|
50
|
+
* link target, if valid.
|
|
51
|
+
*/
|
|
52
|
+
export const ownerOf = (target) => {
|
|
53
|
+
const parts = target.split(sep);
|
|
54
|
+
const seg = (i) => process.platform === 'win32' ? parts[i]?.toLowerCase() : parts[i];
|
|
55
|
+
for (let i = parts.length - 3; i >= 0; i--) {
|
|
56
|
+
if (seg(i) === 'node_modules' && seg(i + 1) === '.vlt') {
|
|
57
|
+
const id = parts[i + 2];
|
|
58
|
+
return isDepID(id) ? id : undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* One key per package: registry + name, else the DepID w/o its extra.
|
|
65
|
+
*/
|
|
66
|
+
export const packageKey = (id) => {
|
|
67
|
+
const t = splitDepID(id);
|
|
68
|
+
if (t[0] !== 'registry')
|
|
69
|
+
return baseDepID(id);
|
|
70
|
+
const spec = t[2];
|
|
71
|
+
const at = spec.lastIndexOf('@');
|
|
72
|
+
return joinDepIDTuple([
|
|
73
|
+
'registry',
|
|
74
|
+
t[1],
|
|
75
|
+
at > 0 ? spec.slice(0, at) : spec,
|
|
76
|
+
]);
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* All vlt links in the given dirs. Anything else is ignored.
|
|
80
|
+
*/
|
|
81
|
+
export const readMounts = (scan) => {
|
|
82
|
+
const mounts = [];
|
|
83
|
+
for (const dir of scan) {
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = readdirSync(dir.real);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const name of entries) {
|
|
92
|
+
const link = join(dir.real, name);
|
|
93
|
+
const target = mountTarget(link);
|
|
94
|
+
if (target) {
|
|
95
|
+
mounts.push({
|
|
96
|
+
dir,
|
|
97
|
+
name,
|
|
98
|
+
mount: `${dir.dir}/${name}`,
|
|
99
|
+
link,
|
|
100
|
+
target,
|
|
101
|
+
owner: ownerOf(target),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return mounts.sort((a, b) => a.mount.localeCompare(b.mount, 'en'));
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Link a skill at `<dir>/<skill>`. Never touches anything not managed by
|
|
110
|
+
* vlt: returns `'conflict'` instead.
|
|
111
|
+
*/
|
|
112
|
+
export const mountSkill = (skill, dir) => {
|
|
113
|
+
const link = join(dir.real, skill.skill);
|
|
114
|
+
if (lstat(link)) {
|
|
115
|
+
const target = mountTarget(link);
|
|
116
|
+
if (!target)
|
|
117
|
+
return 'conflict';
|
|
118
|
+
if (samePath(target, skill.path))
|
|
119
|
+
return 'unchanged';
|
|
120
|
+
unlinkSync(link);
|
|
121
|
+
}
|
|
122
|
+
// junctions need no admin rights, but want an absolute target
|
|
123
|
+
if (process.platform === 'win32') {
|
|
124
|
+
symlinkSyncMkdirp(skill.path, link, 'junction');
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
symlinkSyncMkdirp(relative(dir.real, skill.path), link, 'dir');
|
|
128
|
+
}
|
|
129
|
+
return 'linked';
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Remove a vlt link. Its dir is kept.
|
|
133
|
+
*/
|
|
134
|
+
export const unmountSkill = (m) => {
|
|
135
|
+
unlinkSync(m.link);
|
|
136
|
+
};
|
package/dist/uninstall.d.ts
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import type { PackageInfoClient } from '@vltpkg/package-info';
|
|
2
2
|
import type { LoadOptions } from './actual/load.ts';
|
|
3
3
|
import type { RemoveImportersDependenciesMap } from './dependencies.ts';
|
|
4
|
+
import type { ReifyResult } from './reify/index.ts';
|
|
5
|
+
import type { Graph } from './graph.ts';
|
|
6
|
+
import type { LinkSkillsResult } from './skills/index.ts';
|
|
4
7
|
export type UninstallOptions = LoadOptions & {
|
|
5
8
|
packageInfo: PackageInfoClient;
|
|
6
9
|
allowScripts: string;
|
|
10
|
+
/**
|
|
11
|
+
* DSS query: packages whose agent skills get linked into the
|
|
12
|
+
* project's agent skill dirs after uninstall. Selectors may end with
|
|
13
|
+
* `:skill()`. Unset or `:not(*)`: none.
|
|
14
|
+
*/
|
|
15
|
+
allowSkills?: string;
|
|
16
|
+
/**
|
|
17
|
+
* agent skill dirs, relative to projectRoot. Unset: detect; `[]`:
|
|
18
|
+
* none
|
|
19
|
+
*/
|
|
20
|
+
skillsDirs?: string[];
|
|
7
21
|
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
graph:
|
|
13
|
-
diff
|
|
14
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Result of {@link uninstall}.
|
|
24
|
+
*/
|
|
25
|
+
export type UninstallResult = {
|
|
26
|
+
graph: Graph;
|
|
27
|
+
diff?: ReifyResult;
|
|
28
|
+
/** skill links added or removed, if any */
|
|
29
|
+
skills?: LinkSkillsResult;
|
|
30
|
+
};
|
|
31
|
+
export declare const uninstall: (options: UninstallOptions, remove?: RemoveImportersDependenciesMap) => Promise<UninstallResult>;
|
package/dist/uninstall.js
CHANGED
|
@@ -8,7 +8,19 @@ import { RollbackRemove } from '@vltpkg/rollback-remove';
|
|
|
8
8
|
import { Monorepo } from '@vltpkg/workspaces';
|
|
9
9
|
import { existsSync, rmSync } from 'node:fs';
|
|
10
10
|
import { resolve } from 'node:path';
|
|
11
|
+
import { assertQuery } from "./filter-nodes-by-query.js";
|
|
12
|
+
import { assertSkillsDirs } from "./skills/dirs.js";
|
|
11
13
|
export const uninstall = async (options, remove) => {
|
|
14
|
+
// unset or `:not(*)`: no skills work at all, the skills code never
|
|
15
|
+
// loads
|
|
16
|
+
const allowSkills = options.allowSkills === ':not(*)' ?
|
|
17
|
+
undefined
|
|
18
|
+
: options.allowSkills;
|
|
19
|
+
// fail before uninstalling, not after
|
|
20
|
+
if (allowSkills) {
|
|
21
|
+
await assertQuery(allowSkills, 'allow-skills', ['skill']);
|
|
22
|
+
assertSkillsDirs(options.projectRoot, options.skillsDirs);
|
|
23
|
+
}
|
|
12
24
|
const mainManifest = options.packageJson.read(options.projectRoot);
|
|
13
25
|
const modifiers = GraphModifier.maybeLoad(options);
|
|
14
26
|
const remover = new RollbackRemove();
|
|
@@ -17,6 +29,7 @@ export const uninstall = async (options, remove) => {
|
|
|
17
29
|
packageJson: options.packageJson,
|
|
18
30
|
scurry: options.scurry,
|
|
19
31
|
});
|
|
32
|
+
let result;
|
|
20
33
|
try {
|
|
21
34
|
// Load the actual graph before building the ideal graph so that
|
|
22
35
|
// manifest data from installed packages can be used to hydrate
|
|
@@ -38,6 +51,7 @@ export const uninstall = async (options, remove) => {
|
|
|
38
51
|
remove,
|
|
39
52
|
mainManifest,
|
|
40
53
|
loadManifests: true,
|
|
54
|
+
modifiers,
|
|
41
55
|
remover,
|
|
42
56
|
});
|
|
43
57
|
// If lockfileOnly is enabled, skip reify and only save the lockfile
|
|
@@ -67,9 +81,10 @@ export const uninstall = async (options, remove) => {
|
|
|
67
81
|
actual: act,
|
|
68
82
|
graph,
|
|
69
83
|
loadManifests: true,
|
|
84
|
+
modifiers,
|
|
70
85
|
remover,
|
|
71
86
|
});
|
|
72
|
-
|
|
87
|
+
result = { graph, diff };
|
|
73
88
|
/* c8 ignore start */
|
|
74
89
|
}
|
|
75
90
|
catch (err) {
|
|
@@ -85,4 +100,14 @@ export const uninstall = async (options, remove) => {
|
|
|
85
100
|
throw err;
|
|
86
101
|
}
|
|
87
102
|
/* c8 ignore stop */
|
|
103
|
+
// after the try: a link failure must not roll back the uninstall
|
|
104
|
+
if (!allowSkills)
|
|
105
|
+
return result;
|
|
106
|
+
const { linkSkills } = await import("./skills/index.js");
|
|
107
|
+
result.skills = await linkSkills({
|
|
108
|
+
...options,
|
|
109
|
+
graph: result.graph,
|
|
110
|
+
target: allowSkills,
|
|
111
|
+
});
|
|
112
|
+
return result;
|
|
88
113
|
};
|
package/dist/update.d.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import type { PackageInfoClient } from '@vltpkg/package-info';
|
|
2
2
|
import type { LoadOptions } from './actual/load.ts';
|
|
3
3
|
import { Graph } from './graph.ts';
|
|
4
|
+
import type { DepID } from '@vltpkg/dep-id';
|
|
5
|
+
import type { Diff } from './diff.ts';
|
|
6
|
+
import type { LinkSkillsResult } from './skills/index.ts';
|
|
4
7
|
export type UpdateOptions = LoadOptions & {
|
|
5
8
|
packageInfo: PackageInfoClient;
|
|
6
9
|
allowScripts: string;
|
|
10
|
+
/**
|
|
11
|
+
* DSS query: packages whose agent skills get linked into the
|
|
12
|
+
* project's agent skill dirs after update. Selectors may end with
|
|
13
|
+
* `:skill()`. Unset or `:not(*)`: none.
|
|
14
|
+
*/
|
|
15
|
+
allowSkills?: string;
|
|
16
|
+
/**
|
|
17
|
+
* agent skill dirs, relative to projectRoot. Unset: detect; `[]`:
|
|
18
|
+
* none
|
|
19
|
+
*/
|
|
20
|
+
skillsDirs?: string[];
|
|
7
21
|
};
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Result of {@link update}.
|
|
24
|
+
*/
|
|
25
|
+
export type UpdateResult = {
|
|
26
|
+
buildQueue?: DepID[];
|
|
10
27
|
graph: Graph;
|
|
11
|
-
diff
|
|
12
|
-
|
|
28
|
+
diff?: Diff;
|
|
29
|
+
/** skill links added or removed, if any */
|
|
30
|
+
skills?: LinkSkillsResult;
|
|
31
|
+
};
|
|
32
|
+
export declare const update: (options: UpdateOptions) => Promise<UpdateResult>;
|