@vltpkg/graph 1.4.0 → 1.6.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.
Files changed (53) hide show
  1. package/README.md +62 -3
  2. package/dist/actual/load.js +24 -12
  3. package/dist/build.js +30 -5
  4. package/dist/dependencies.d.ts +11 -0
  5. package/dist/dependencies.js +47 -7
  6. package/dist/filter-nodes-by-query.d.ts +39 -0
  7. package/dist/filter-nodes-by-query.js +100 -0
  8. package/dist/graph.js +3 -0
  9. package/dist/ideal/append-nodes.js +29 -11
  10. package/dist/ideal/get-importer-specs.js +17 -7
  11. package/dist/ideal/remove-satisfied-specs.js +18 -10
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +2 -0
  14. package/dist/install.d.ts +25 -8
  15. package/dist/install.js +25 -1
  16. package/dist/lockfile/load-edges.js +10 -2
  17. package/dist/modifiers.d.ts +11 -0
  18. package/dist/modifiers.js +105 -21
  19. package/dist/reify/add-edge.d.ts +4 -0
  20. package/dist/reify/add-edge.js +13 -7
  21. package/dist/reify/build.d.ts +7 -0
  22. package/dist/reify/check-needed-build.d.ts +5 -0
  23. package/dist/reify/check-needed-build.js +13 -0
  24. package/dist/reify/delete-edge.js +16 -14
  25. package/dist/reify/delete-nodes.d.ts +2 -1
  26. package/dist/reify/delete-nodes.js +4 -2
  27. package/dist/reify/index.d.ts +2 -0
  28. package/dist/reify/index.js +11 -39
  29. package/dist/reify/relink-bins.d.ts +9 -0
  30. package/dist/reify/relink-bins.js +39 -0
  31. package/dist/reify/update-importers-package-json.d.ts +6 -1
  32. package/dist/reify/update-importers-package-json.js +16 -15
  33. package/dist/skills/dirs.d.ts +58 -0
  34. package/dist/skills/dirs.js +211 -0
  35. package/dist/skills/discover.d.ts +73 -0
  36. package/dist/skills/discover.js +238 -0
  37. package/dist/skills/gitignore.d.ts +18 -0
  38. package/dist/skills/gitignore.js +82 -0
  39. package/dist/skills/index.d.ts +124 -0
  40. package/dist/skills/index.js +254 -0
  41. package/dist/skills/lazy.d.ts +8 -0
  42. package/dist/skills/lazy.js +8 -0
  43. package/dist/skills/mask.d.ts +26 -0
  44. package/dist/skills/mask.js +43 -0
  45. package/dist/skills/mounts.d.ts +78 -0
  46. package/dist/skills/mounts.js +162 -0
  47. package/dist/uninstall.d.ts +27 -7
  48. package/dist/uninstall.js +25 -1
  49. package/dist/update.d.ts +24 -4
  50. package/dist/update.js +24 -1
  51. package/dist/visualization/mermaid-output.d.ts +8 -0
  52. package/dist/visualization/mermaid-output.js +25 -8
  53. package/package.json +21 -21
@@ -0,0 +1,18 @@
1
+ export declare const GITIGNORE_BEGIN = "# >>> vlt agent skills (links into node_modules)";
2
+ export declare const GITIGNORE_END = "# <<< vlt agent skills";
3
+ /**
4
+ * Keep a `/<name>` line per vlt link in a marked block of
5
+ * `<dir>/.gitignore`: add `add`, drop `remove`. Other lines are kept.
6
+ * Written only when changed, created only to add a line, never
7
+ * removed. `'refused'`: `.gitignore` is not a regular file, or an fs
8
+ * error.
9
+ */
10
+ export declare const editGitignore: (dir: string, edit: {
11
+ add: string[];
12
+ remove: string[];
13
+ }) => "ok" | "refused";
14
+ /**
15
+ * Names in vlt's block of `<dir>/.gitignore`: vlt's record of its links
16
+ * in `dir`. Empty if none or unreadable.
17
+ */
18
+ export declare const gitignoreNames: (dir: string) => Set<string>;
@@ -0,0 +1,82 @@
1
+ import { lstatSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export const GITIGNORE_BEGIN = '# >>> vlt agent skills (links into node_modules)';
4
+ export const GITIGNORE_END = '# <<< vlt agent skills';
5
+ const sorted = (names) => [...names]
6
+ .sort((a, b) => a.localeCompare(b, 'en'))
7
+ .map(n => `/${n}`);
8
+ // vlt's block: `end` is -1 if none
9
+ const findBlock = (lines) => {
10
+ const begin = lines.findIndex(l => l.trim() === GITIGNORE_BEGIN);
11
+ const end = lines.findIndex((l, i) => begin !== -1 && i > begin && l.trim() === GITIGNORE_END);
12
+ return { begin, end };
13
+ };
14
+ const update = (file, { add, remove }) => {
15
+ const st = lstatSync(file, { throwIfNoEntry: false });
16
+ if (st && !st.isFile())
17
+ return 'refused';
18
+ const text = st ? readFileSync(file, 'utf8') : '';
19
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
20
+ const lines = text.split(eol);
21
+ const { begin, end } = findBlock(lines);
22
+ let out;
23
+ if (end === -1) {
24
+ if (!add.length)
25
+ return 'ok';
26
+ out =
27
+ (text && !text.endsWith(eol) ? text + eol : text) +
28
+ [GITIGNORE_BEGIN, ...sorted(add), GITIGNORE_END].join(eol) +
29
+ eol;
30
+ }
31
+ else {
32
+ const rm = new Set(sorted(remove));
33
+ const block = lines
34
+ .slice(begin + 1, end)
35
+ .filter(l => !rm.has(l.trim()));
36
+ const have = new Set(block.map(l => l.trim()));
37
+ lines.splice(begin + 1, end - begin - 1, ...block, ...sorted(add).filter(l => !have.has(l)));
38
+ out = lines.join(eol);
39
+ }
40
+ if (out !== text)
41
+ writeFileSync(file, out);
42
+ return 'ok';
43
+ };
44
+ /**
45
+ * Keep a `/<name>` line per vlt link in a marked block of
46
+ * `<dir>/.gitignore`: add `add`, drop `remove`. Other lines are kept.
47
+ * Written only when changed, created only to add a line, never
48
+ * removed. `'refused'`: `.gitignore` is not a regular file, or an fs
49
+ * error.
50
+ */
51
+ export const editGitignore = (dir, edit) => {
52
+ try {
53
+ return update(join(dir, '.gitignore'), edit);
54
+ }
55
+ catch {
56
+ return 'refused';
57
+ }
58
+ };
59
+ /**
60
+ * Names in vlt's block of `<dir>/.gitignore`: vlt's record of its links
61
+ * in `dir`. Empty if none or unreadable.
62
+ */
63
+ export const gitignoreNames = (dir) => {
64
+ try {
65
+ const file = join(dir, '.gitignore');
66
+ if (!lstatSync(file, { throwIfNoEntry: false })?.isFile()) {
67
+ return new Set();
68
+ }
69
+ const lines = readFileSync(file, 'utf8').split(/\r?\n/);
70
+ const { begin, end } = findBlock(lines);
71
+ if (end === -1)
72
+ return new Set();
73
+ return new Set(lines
74
+ .slice(begin + 1, end)
75
+ .map(l => l.trim())
76
+ .filter(l => l.startsWith('/'))
77
+ .map(l => l.slice(1)));
78
+ }
79
+ catch {
80
+ return new Set();
81
+ }
82
+ };
@@ -0,0 +1,124 @@
1
+ import type { LoadOptions } from '../actual/load.ts';
2
+ import type { Graph } from '../graph.ts';
3
+ import type { Skill } from './discover.ts';
4
+ import type { UnmatchedPattern } from './mask.ts';
5
+ export * from './discover.ts';
6
+ export type { SkillMount, MountResult } from './mounts.ts';
7
+ export type { SkillsDir } from './dirs.ts';
8
+ export type { UnmatchedPattern } from './mask.ts';
9
+ /**
10
+ * Options for {@link listSkills} and {@link linkSkills}.
11
+ */
12
+ export type SkillsOptions = LoadOptions & {
13
+ /**
14
+ * DSS query selecting packages, selectors may end with `:skill()`;
15
+ * default `*`
16
+ */
17
+ target?: string;
18
+ /** graph to use instead of loading the actual one */
19
+ graph?: Graph;
20
+ /**
21
+ * agent skill dirs, relative to projectRoot. Unset: detect; `[]`:
22
+ * none
23
+ */
24
+ skillsDirs?: string[];
25
+ /** env read by detection; default `process.env` */
26
+ env?: Record<string, string | undefined>;
27
+ };
28
+ /**
29
+ * Options for {@link linkSkills}.
30
+ */
31
+ export type LinkSkillsOptions = SkillsOptions & {
32
+ /** DSS query selecting packages */
33
+ target: string;
34
+ /**
35
+ * also remove every vlt link this selection does not make, in every
36
+ * scanned dir (`vlt skills link`). Default: only add links and remove
37
+ * links whose skill is gone (installs)
38
+ */
39
+ apply?: boolean;
40
+ };
41
+ /**
42
+ * A skill linked in one dir.
43
+ */
44
+ export type SkillRow = Skill & {
45
+ /** the dir */
46
+ dir: string;
47
+ /** `<dir>/<skill>` */
48
+ mount: string;
49
+ };
50
+ /**
51
+ * Skills left out as their packages have no security data (fail
52
+ * closed).
53
+ */
54
+ export type UnscannedSkills = {
55
+ /** `<skill> (<package>)` */
56
+ unscanned?: string[];
57
+ /** why: the request timed out, or socket.dev has not scanned them */
58
+ unscannedReason?: 'timeout' | 'unscanned';
59
+ };
60
+ /**
61
+ * Result of {@link listSkills}.
62
+ */
63
+ export type ListSkillsResult = UnscannedSkills & {
64
+ /** main importer name, the tree root */
65
+ project: string;
66
+ /** the graph the skills were found in */
67
+ graph: Graph;
68
+ skills: Skill[];
69
+ /** target dirs */
70
+ dirs: string[];
71
+ /** targets dropped: `<dir> (<reason>)` */
72
+ skipped?: string[];
73
+ };
74
+ /**
75
+ * Result of {@link linkSkills}.
76
+ */
77
+ export type LinkSkillsResult = UnscannedSkills & {
78
+ /** links created or re-pointed */
79
+ linked: SkillRow[];
80
+ /** selected skills already linked */
81
+ unchanged: SkillRow[];
82
+ /**
83
+ * vlt links removed (skill gone, or with `apply`: not selected or dir
84
+ * no longer a target): `/`-separated relative paths
85
+ */
86
+ removed: string[];
87
+ /**
88
+ * skipped per dir: path in use (`<mount>`), or name taken by another
89
+ * package (`<mount> (<package>)`)
90
+ */
91
+ conflicts: string[];
92
+ /** target dirs */
93
+ dirs: string[];
94
+ /** targets dropped: `<dir> (<reason>)` */
95
+ skipped?: string[];
96
+ /** `<dir>/.gitignore` files not updated: not a regular file, fs error */
97
+ gitignore?: string[];
98
+ /** `:skill()` patterns that matched no skill of their packages */
99
+ unmatched?: UnmatchedPattern[];
100
+ };
101
+ /**
102
+ * List the agent skills shipped by installed packages (`:skill()`
103
+ * applied, `unscanned` left out), and the agent skill dirs. Creates
104
+ * nothing.
105
+ */
106
+ export declare const listSkills: (o: SkillsOptions) => Promise<ListSkillsResult>;
107
+ /**
108
+ * Link the agent skills of the selected packages into each of the
109
+ * project's agent skill dirs (see `skillsDirs`), created only when a
110
+ * skill is picked. `target` selectors may end with `:skill()`. One
111
+ * package per name, among picked skills, in every dir: the package of
112
+ * the first existing link (in dir order), else rank (workspace or main
113
+ * importer dep, importer dep, transitive), then DepID; the others are
114
+ * reported. Installs keep a dir's link to another installed skill.
115
+ * A link follows its package to the picked version, in every scanned
116
+ * dir. Then remove vlt links whose skill is no longer installed (with
117
+ * `apply`: every vlt link not made here), and keep each dir's
118
+ * `.gitignore` block in sync (also vlt's record of workspace links).
119
+ * Linked packages get private copies of files hardlinked from the
120
+ * global store. Security queries fail closed: skills of packages with
121
+ * no security data are not linked, and reported in `unscanned` unless
122
+ * still linked.
123
+ */
124
+ export declare const linkSkills: (o: LinkSkillsOptions) => Promise<LinkSkillsResult>;
@@ -0,0 +1,254 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { Monorepo } from '@vltpkg/workspaces';
3
+ import { load as loadActual } from "../actual/load.js";
4
+ import { filterNodesByQueryParts } from "../filter-nodes-by-query.js";
5
+ import { unshare } from "../reify/unshare.js";
6
+ import { skillsDirs } from "./dirs.js";
7
+ import { discoverSkills } from "./discover.js";
8
+ import { editGitignore } from "./gitignore.js";
9
+ import { maskSkills } from "./mask.js";
10
+ import { mountSkill, packageKey, readMounts, realOr, samePath, unmountSkill, } from "./mounts.js";
11
+ export * from "./discover.js";
12
+ const lc = (s) => s.toLowerCase();
13
+ const selectNodes = async (o) => {
14
+ const graph = o.graph ?? loadActual({ ...o, loadManifests: true });
15
+ const { ids, parts, unscanned } = await filterNodesByQueryParts(graph, o.target ?? '*');
16
+ const nodes = [...graph.nodes.values()].filter(n => ids.has(n.id));
17
+ return { graph, nodes, parts, unscanned };
18
+ };
19
+ const dirsOf = (o, create = false) => skillsDirs({
20
+ projectRoot: o.projectRoot,
21
+ configured: o.skillsDirs,
22
+ env: o.env,
23
+ create,
24
+ });
25
+ // every workspace, selected or not: owners of vlt links
26
+ const workspaceDirs = (graph, scurry) => [...graph.importers]
27
+ .filter(n => !n.mainImporter)
28
+ .map(n => ({
29
+ id: n.id,
30
+ path: realOr(n.resolvedLocation(scurry)),
31
+ }));
32
+ // every workspace, loaded or not (`-w`)
33
+ const allWorkspaces = (m) => m ?
34
+ [
35
+ ...Monorepo.load(m.projectRoot, {
36
+ config: m.config,
37
+ scurry: m.scurry,
38
+ packageJson: m.packageJson,
39
+ }).values(),
40
+ ].map(ws => ({ id: ws.id, path: realOr(ws.fullpath) }))
41
+ : [];
42
+ // packages behind live vlt links
43
+ const liveOwners = (mounts) => mounts.flatMap(m => m.owner && existsSync(m.target) ? [m.owner] : []);
44
+ // private copies of store-hardlinked files: edits via skill links
45
+ // must not reach the global store
46
+ const unshareSkills = async (graph, ids, scurry) => {
47
+ for (const id of new Set(ids)) {
48
+ const node = graph.nodes.get(id);
49
+ if (node?.inVltStore())
50
+ await unshare(node.resolvedLocation(scurry));
51
+ }
52
+ };
53
+ // `vlt ls` style
54
+ const label = (s) => {
55
+ const pkg = s.version ? `${s.name}@${s.version}` : s.name;
56
+ return s.alias ? `${s.alias} (${pkg})` : pkg;
57
+ };
58
+ // apply the mask; fail closed: skills of `unscanned` pkgs are dropped,
59
+ // and reported unless another package's skill takes the name
60
+ const pick = (found, parts, unscanned) => {
61
+ const { skills: masked, unmatched } = maskSkills(found, parts);
62
+ const ok = (s) => !unscanned?.ids.has(s.id);
63
+ const picked = masked.filter(ok);
64
+ const names = new Set(picked.map(s => lc(s.skill)));
65
+ const dropped = masked.filter(s => !ok(s) && !names.has(lc(s.skill)));
66
+ return { skills: found.filter(ok), picked, unmatched, dropped };
67
+ };
68
+ const report = (dropped, unscanned) => unscanned && dropped.length ?
69
+ {
70
+ unscanned: dropped.map(s => `${s.skill} (${label(s)})`),
71
+ unscannedReason: unscanned.reason,
72
+ }
73
+ : {};
74
+ /**
75
+ * List the agent skills shipped by installed packages (`:skill()`
76
+ * applied, `unscanned` left out), and the agent skill dirs. Creates
77
+ * nothing.
78
+ */
79
+ export const listSkills = async (o) => {
80
+ const { graph, nodes, parts, unscanned } = await selectNodes(o);
81
+ const { targets, scan, skipped } = dirsOf(o);
82
+ const { skills } = discoverSkills(nodes, o.scurry, readMounts(scan, { workspaces: workspaceDirs(graph, o.scurry) }));
83
+ const { picked, dropped } = pick(skills, parts, unscanned);
84
+ return {
85
+ project: graph.mainImporter.name,
86
+ graph,
87
+ skills: picked,
88
+ dirs: targets.map(d => d.dir),
89
+ ...(skipped.length ? { skipped } : null),
90
+ ...report(dropped, unscanned),
91
+ };
92
+ };
93
+ /**
94
+ * Link the agent skills of the selected packages into each of the
95
+ * project's agent skill dirs (see `skillsDirs`), created only when a
96
+ * skill is picked. `target` selectors may end with `:skill()`. One
97
+ * package per name, among picked skills, in every dir: the package of
98
+ * the first existing link (in dir order), else rank (workspace or main
99
+ * importer dep, importer dep, transitive), then DepID; the others are
100
+ * reported. Installs keep a dir's link to another installed skill.
101
+ * A link follows its package to the picked version, in every scanned
102
+ * dir. Then remove vlt links whose skill is no longer installed (with
103
+ * `apply`: every vlt link not made here), and keep each dir's
104
+ * `.gitignore` block in sync (also vlt's record of workspace links).
105
+ * Linked packages get private copies of files hardlinked from the
106
+ * global store. Security queries fail closed: skills of packages with
107
+ * no security data are not linked, and reported in `unscanned` unless
108
+ * still linked.
109
+ */
110
+ export const linkSkills = async (o) => {
111
+ const { graph, nodes, parts, unscanned } = await selectNodes(o);
112
+ const { skills: found, scanned } = discoverSkills(nodes, o.scurry);
113
+ const { skills, picked, unmatched, dropped } = pick(found, parts, unscanned);
114
+ const inMask = new Set(picked);
115
+ const { targets, scan, skipped } = dirsOf(o, picked.length > 0);
116
+ const res = {
117
+ linked: [],
118
+ unchanged: [],
119
+ removed: [],
120
+ conflicts: [],
121
+ dirs: targets.map(d => d.dir),
122
+ };
123
+ const workspaces = workspaceDirs(graph, o.scurry);
124
+ const mounts = readMounts(scan, { workspaces });
125
+ const byName = new Map(mounts.map(m => [`${m.dir.dir}\0${lc(m.name)}`, m]));
126
+ // the link's skill is no longer installed: dangling, orphan owner,
127
+ // or a scanned owner that no longer ships it
128
+ const stale = (m) => !m.owner ||
129
+ !graph.nodes.has(m.owner) ||
130
+ !existsSync(m.target) ||
131
+ scanned
132
+ .get(m.owner)
133
+ ?.some(f => lc(f.skill) === lc(m.name) && samePath(f.path, m.target)) === false;
134
+ const groups = new Map();
135
+ for (const s of skills) {
136
+ const k = lc(s.skill);
137
+ groups.set(k, [...(groups.get(k) ?? []), s]);
138
+ }
139
+ // the link at `<dir>/<k>`, and its package's skill in `group`
140
+ const at = (dir, k, group) => {
141
+ const m = byName.get(`${dir.dir}\0${k}`);
142
+ const key = m?.owner && packageKey(m.owner);
143
+ return { m, own: group.find(s => packageKey(s.id) === key) };
144
+ };
145
+ // picked skills only: the others never take a name
146
+ const cands = (group) => group.filter(s => inMask.has(s));
147
+ // one per name: the first linked package keeps it, re-pointed if needed
148
+ const first = new Map([...groups].map(([k, group]) => {
149
+ const c = cands(group);
150
+ return [
151
+ k,
152
+ targets.map(d => at(d, k, c).own).find(s => !!s) ?? c[0],
153
+ ];
154
+ }));
155
+ const isTarget = new Set(targets);
156
+ const picks = [];
157
+ for (const dir of scan) {
158
+ for (const [k, group] of groups) {
159
+ const { m, own } = at(dir, k, group);
160
+ if (isTarget.has(dir)) {
161
+ // installs keep a link to another installed skill
162
+ const winner = o.apply ?
163
+ first.get(k)
164
+ : (own ?? (m && !stale(m) ? undefined : first.get(k)));
165
+ picks.push({
166
+ dir,
167
+ winner,
168
+ losers: cands(group).filter(s => s !== winner),
169
+ m,
170
+ });
171
+ }
172
+ else if (own && !o.apply) {
173
+ // no longer a target: only re-pointed
174
+ picks.push({ dir, winner: own, losers: [], m });
175
+ }
176
+ }
177
+ }
178
+ await unshareSkills(graph, [
179
+ ...picks.flatMap(({ winner }) => (winner ? [winner.id] : [])),
180
+ ...liveOwners(mounts),
181
+ ], o.scurry);
182
+ const gone = [];
183
+ // names taken over by entries not vlt's
184
+ const foreign = [];
185
+ const kept = new Set();
186
+ // links made here, recorded even if not yet in `.gitignore`
187
+ const made = new Set();
188
+ const where = new Map();
189
+ let every;
190
+ for (const { dir, winner, losers, m } of picks) {
191
+ if (winner) {
192
+ // replaced link named in another case (case-sensitive fs)
193
+ if (m && m.name !== winner.skill) {
194
+ unmountSkill(m);
195
+ gone.push(m);
196
+ }
197
+ const mount = `${dir.dir}/${winner.skill}`;
198
+ const r = mountSkill(winner, dir, m?.name === winner.skill);
199
+ if (r === 'linked')
200
+ made.add(mount);
201
+ if (r === 'conflict') {
202
+ res.conflicts.push(mount);
203
+ // vlt's link into a workspace not loaded (`-w`) keeps its line
204
+ every ??= allWorkspaces(o.monorepo);
205
+ if (!readMounts([dir], { workspaces: every }).some(l => l.name === winner.skill)) {
206
+ foreign.push({ dir, name: winner.skill });
207
+ }
208
+ }
209
+ else {
210
+ const k = `${winner.id}\0${winner.skill}`;
211
+ const linked = where.get(k) ?? [];
212
+ linked.push(dir.dir);
213
+ where.set(k, linked);
214
+ res[r].push({ ...winner, linked, dir: dir.dir, mount });
215
+ kept.add(mount);
216
+ }
217
+ }
218
+ for (const s of losers)
219
+ res.conflicts.push(`${dir.dir}/${s.skill} (${label(s)})`);
220
+ }
221
+ const after = readMounts(scan, { workspaces, made });
222
+ const present = [];
223
+ for (const m of after) {
224
+ if (o.apply ? kept.has(m.mount) : !stale(m)) {
225
+ present.push(m);
226
+ continue;
227
+ }
228
+ unmountSkill(m);
229
+ gone.push(m);
230
+ res.removed.push(m.mount);
231
+ }
232
+ const gitignore = [];
233
+ for (const dir of scan) {
234
+ const add = present.filter(m => m.dir === dir).map(m => m.name);
235
+ const remove = [...gone, ...foreign]
236
+ .filter(m => m.dir === dir && !add.includes(m.name))
237
+ .map(m => m.name);
238
+ if ((add.length || remove.length) &&
239
+ editGitignore(dir.real, { add, remove }) === 'refused') {
240
+ gitignore.push(`${dir.dir}/.gitignore`);
241
+ }
242
+ }
243
+ if (skipped.length)
244
+ res.skipped = skipped;
245
+ if (gitignore.length)
246
+ res.gitignore = gitignore;
247
+ if (unmatched.length)
248
+ res.unmatched = unmatched;
249
+ // not left out: still linked in every dir (installs keep links)
250
+ const left = dropped.filter(s => !targets.every(d => present.some(m => m.dir === d &&
251
+ m.owner === s.id &&
252
+ lc(m.name) === lc(s.skill))));
253
+ return Object.assign(res, report(left, unscanned));
254
+ };
@@ -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,78 @@
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 or workspace the link points into */
20
+ owner?: DepID;
21
+ };
22
+ /**
23
+ * A workspace of the project.
24
+ */
25
+ export type WorkspaceDir = {
26
+ id: DepID;
27
+ /** realpath */
28
+ path: string;
29
+ };
30
+ /**
31
+ * Outcome of mounting a single skill.
32
+ */
33
+ export type MountResult = 'linked' | 'unchanged' | 'conflict';
34
+ /** `lstat`, undefined when missing or unreadable (eg, ENOTDIR) */
35
+ export declare const lstat: (path: string) => Stats | undefined;
36
+ /** Resolved path, lowercased on win32. */
37
+ export declare const norm: (p: string) => string;
38
+ /** realpath, else the resolved path when missing. */
39
+ export declare const realOr: (p: string) => string;
40
+ /**
41
+ * Same real path? Case-insensitive on win32.
42
+ */
43
+ export declare const samePath: (a: string, b: string) => boolean;
44
+ /**
45
+ * The target of `link` if it is a symlink (or junction) pointing into
46
+ * a `node_modules/.vlt` store, otherwise undefined. Any store, not just
47
+ * the project's: win32 junctions are absolute, so they point to the old
48
+ * store after the project is moved or copied.
49
+ */
50
+ export declare const mountTarget: (link: string) => string | undefined;
51
+ /**
52
+ * The DepID of the last `node_modules/.vlt/<DepID>` segment of a raw
53
+ * link target, if valid.
54
+ */
55
+ export declare const ownerOf: (target: string) => DepID | undefined;
56
+ /**
57
+ * One key per package: registry + name, else the DepID w/o its extra.
58
+ */
59
+ export declare const packageKey: (id: DepID) => string;
60
+ /**
61
+ * All vlt links in the given dirs: links into a `node_modules/.vlt`
62
+ * store, and links listed in the dir's `.gitignore` block (or `made`)
63
+ * into a workspace or dangling. Anything else is ignored.
64
+ */
65
+ export declare const readMounts: (scan: SkillsDir[], { workspaces, made, }?: {
66
+ workspaces?: WorkspaceDir[];
67
+ made?: Set<string>;
68
+ }) => SkillMount[];
69
+ /**
70
+ * Link a skill at `<dir>/<skill>`. `'unchanged'` if the entry already
71
+ * is the skill. Never touches anything not managed by vlt (a store
72
+ * link, or `owned`): returns `'conflict'` instead.
73
+ */
74
+ export declare const mountSkill: (skill: Pick<Skill, "skill" | "path">, dir: SkillsDir, owned?: boolean) => MountResult;
75
+ /**
76
+ * Remove a vlt link. Its dir is kept.
77
+ */
78
+ export declare const unmountSkill: (m: SkillMount) => void;