@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.
- package/README.md +62 -3
- package/dist/actual/load.js +24 -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/graph.js +3 -0
- package/dist/ideal/append-nodes.js +29 -11
- package/dist/ideal/get-importer-specs.js +17 -7
- package/dist/ideal/remove-satisfied-specs.js +18 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/install.d.ts +25 -8
- package/dist/install.js +25 -1
- package/dist/lockfile/load-edges.js +10 -2
- package/dist/modifiers.d.ts +11 -0
- package/dist/modifiers.js +105 -21
- package/dist/reify/add-edge.d.ts +4 -0
- package/dist/reify/add-edge.js +13 -7
- 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/delete-edge.js +16 -14
- package/dist/reify/delete-nodes.d.ts +2 -1
- package/dist/reify/delete-nodes.js +4 -2
- package/dist/reify/index.d.ts +2 -0
- package/dist/reify/index.js +11 -39
- package/dist/reify/relink-bins.d.ts +9 -0
- package/dist/reify/relink-bins.js +39 -0
- package/dist/reify/update-importers-package-json.d.ts +6 -1
- package/dist/reify/update-importers-package-json.js +16 -15
- 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 +238 -0
- package/dist/skills/gitignore.d.ts +18 -0
- package/dist/skills/gitignore.js +82 -0
- package/dist/skills/index.d.ts +124 -0
- package/dist/skills/index.js +254 -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 +78 -0
- package/dist/skills/mounts.js +162 -0
- package/dist/uninstall.d.ts +27 -7
- package/dist/uninstall.js +25 -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PackageJson } from '@vltpkg/package-json';
|
|
2
2
|
import type { AddImportersDependenciesMap, RemoveImportersDependenciesMap } from '../dependencies.ts';
|
|
3
3
|
import type { Graph } from '../graph.ts';
|
|
4
|
+
import type { GraphModifier } from '../modifiers.ts';
|
|
4
5
|
export type UpdatePackageJsonOptions = {
|
|
5
6
|
/**
|
|
6
7
|
* A `Map` in which keys are {@link DepID} linking to another `Map` in which
|
|
@@ -37,9 +38,13 @@ export type UpdatePackageJsonOptions = {
|
|
|
37
38
|
* Defaults to `^` if not specified. Ignored when `saveExact` is true.
|
|
38
39
|
*/
|
|
39
40
|
savePrefix?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Graph modifiers, importer edges they govern keep the modifier value.
|
|
43
|
+
*/
|
|
44
|
+
modifiers?: GraphModifier;
|
|
40
45
|
};
|
|
41
46
|
/**
|
|
42
47
|
* Updates nodes of a provided {@link Graph} accordingly to the
|
|
43
48
|
* provided add or remove arguments.
|
|
44
49
|
*/
|
|
45
|
-
export declare const updatePackageJson: ({ add, graph, packageJson, remove, saveExact, savePrefix, }: UpdatePackageJsonOptions) => () => void;
|
|
50
|
+
export declare const updatePackageJson: ({ add, graph, packageJson, remove, saveExact, savePrefix, modifiers, }: UpdatePackageJsonOptions) => () => void;
|
|
@@ -2,16 +2,10 @@ import { splitDepID } from '@vltpkg/dep-id';
|
|
|
2
2
|
import { error } from '@vltpkg/error-cause';
|
|
3
3
|
import { Spec } from '@vltpkg/spec';
|
|
4
4
|
import { longDependencyTypes } from '@vltpkg/types';
|
|
5
|
+
import { longDependencyTypeNames } from "../dependencies.js";
|
|
5
6
|
import { resolveSaveType } from "../resolve-save-type.js";
|
|
6
7
|
import { calculateSaveValue } from "./calculate-save-value.js";
|
|
7
|
-
const
|
|
8
|
-
['prod', 'dependencies'],
|
|
9
|
-
['dev', 'devDependencies'],
|
|
10
|
-
['peer', 'peerDependencies'],
|
|
11
|
-
['peerOptional', 'peerDependencies'],
|
|
12
|
-
['optional', 'optionalDependencies'],
|
|
13
|
-
]);
|
|
14
|
-
const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
|
|
8
|
+
const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix, modifiers) => {
|
|
15
9
|
const node = graph.nodes.get(nodeId);
|
|
16
10
|
if (!node) {
|
|
17
11
|
throw error('Failed to retrieve node', {
|
|
@@ -51,16 +45,21 @@ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
|
|
|
51
45
|
const [name, dep] = deleteNameOrAddItem;
|
|
52
46
|
// peerOptional also needs to add peerDependenciesMeta entry
|
|
53
47
|
const depTypeShort = resolveSaveType(node, name, dep.type);
|
|
54
|
-
const depType =
|
|
48
|
+
const depType = Object.hasOwn(longDependencyTypeNames, depTypeShort) ?
|
|
49
|
+
longDependencyTypeNames[depTypeShort]
|
|
50
|
+
: undefined;
|
|
55
51
|
if (!depType) {
|
|
56
52
|
throw error('Failed to retrieve dependency type', {
|
|
57
|
-
validOptions:
|
|
53
|
+
validOptions: Object.keys(longDependencyTypeNames),
|
|
58
54
|
found: dep.type,
|
|
59
55
|
});
|
|
60
56
|
}
|
|
61
57
|
const edge = node.edgesOut.get(name);
|
|
62
58
|
if (!edge?.to) {
|
|
63
|
-
throw error(
|
|
59
|
+
throw error(`Dependency node could not be found: ${name}`, {
|
|
60
|
+
spec: dep.spec,
|
|
61
|
+
from: node.location,
|
|
62
|
+
});
|
|
64
63
|
}
|
|
65
64
|
const n = edge.to;
|
|
66
65
|
const [nodeType] = splitDepID(n.id);
|
|
@@ -94,8 +93,10 @@ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
|
|
|
94
93
|
// the lockfile has to carry the value package.json carries, or a
|
|
95
94
|
// later install reads back `latest` where the manifest says
|
|
96
95
|
// `^1.2.3`. anything keyed by the old spec from here on must be
|
|
97
|
-
// invalidated at this point.
|
|
98
|
-
|
|
96
|
+
// invalidated at this point. governed edges keep the modifier
|
|
97
|
+
// value instead.
|
|
98
|
+
if (saveValue !== edge.spec.bareSpec &&
|
|
99
|
+
!modifiers?.targetsImporterEdge(node, dep.spec)) {
|
|
99
100
|
edge.spec = Spec.parse(name, saveValue, dep.spec.options);
|
|
100
101
|
graph.lockfileStale = true;
|
|
101
102
|
}
|
|
@@ -107,7 +108,7 @@ const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
|
|
|
107
108
|
* Updates nodes of a provided {@link Graph} accordingly to the
|
|
108
109
|
* provided add or remove arguments.
|
|
109
110
|
*/
|
|
110
|
-
export const updatePackageJson = ({ add, graph, packageJson, remove, saveExact, savePrefix, }) => {
|
|
111
|
+
export const updatePackageJson = ({ add, graph, packageJson, remove, saveExact, savePrefix, modifiers, }) => {
|
|
111
112
|
const manifestsToUpdate = new Set();
|
|
112
113
|
const operations = new Set([add, remove]);
|
|
113
114
|
for (const operation of operations) {
|
|
@@ -116,7 +117,7 @@ export const updatePackageJson = ({ add, graph, packageJson, remove, saveExact,
|
|
|
116
117
|
// that are nested folders from which the user can also add new
|
|
117
118
|
// dependencies to
|
|
118
119
|
for (const nodeId of operation.keys()) {
|
|
119
|
-
const manifest = addOrRemoveDeps(nodeId, graph, operation, saveExact, savePrefix);
|
|
120
|
+
const manifest = addOrRemoveDeps(nodeId, graph, operation, saveExact, savePrefix, modifiers);
|
|
120
121
|
if (manifest) {
|
|
121
122
|
manifestsToUpdate.add(manifest);
|
|
122
123
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Always a target when detecting: read by most agents. */
|
|
2
|
+
export declare const AGENTS_SKILLS = ".agents/skills";
|
|
3
|
+
/** Set (non-empty) inside Claude Code. */
|
|
4
|
+
export declare const CLAUDE_ENV: readonly ["CLAUDECODE", "CLAUDE_CODE"];
|
|
5
|
+
/**
|
|
6
|
+
* Agent folder at the project root → its skills dir.
|
|
7
|
+
*/
|
|
8
|
+
export declare const AGENT_ROOTS: readonly (readonly [
|
|
9
|
+
marker: string,
|
|
10
|
+
dir: string
|
|
11
|
+
])[];
|
|
12
|
+
/**
|
|
13
|
+
* An agent skills dir.
|
|
14
|
+
*/
|
|
15
|
+
export type SkillsDir = {
|
|
16
|
+
/** `/`-separated, relative to projectRoot */
|
|
17
|
+
dir: string;
|
|
18
|
+
/** realpath (a missing tail appended as is) */
|
|
19
|
+
real: string;
|
|
20
|
+
/** `<dev>:<ino>` (no inode: the path); unset when missing */
|
|
21
|
+
id?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Result of {@link skillsDirs}.
|
|
25
|
+
*/
|
|
26
|
+
export type SkillsDirs = {
|
|
27
|
+
/** dirs to link into */
|
|
28
|
+
targets: SkillsDir[];
|
|
29
|
+
/** existing dirs to look for vlt links in: targets first */
|
|
30
|
+
scan: SkillsDir[];
|
|
31
|
+
/** targets dropped: `<dir> (<reason>)` */
|
|
32
|
+
skipped: string[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Options for {@link skillsDirs}.
|
|
36
|
+
*/
|
|
37
|
+
export type SkillsDirsOptions = {
|
|
38
|
+
projectRoot: string;
|
|
39
|
+
/** `skills-dir`: replaces detection; `[]` none */
|
|
40
|
+
configured?: string[];
|
|
41
|
+
/** default `process.env` */
|
|
42
|
+
env?: Record<string, string | undefined>;
|
|
43
|
+
/** create missing targets */
|
|
44
|
+
create?: boolean;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Throw EUSAGE unless each entry is a dir inside projectRoot, not in
|
|
48
|
+
* `node_modules`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const assertSkillsDirs: (projectRoot: string, list?: string[]) => void;
|
|
51
|
+
/**
|
|
52
|
+
* The project's agent skill dirs. `configured` replaces detection:
|
|
53
|
+
* `.agents/skills`, plus `<folder>/skills` for each agent folder at
|
|
54
|
+
* the root, `.claude/skills` inside Claude Code, and existing
|
|
55
|
+
* `.<name>/skills` dirs. Dirs must resolve inside projectRoot, not in
|
|
56
|
+
* `node_modules`; the same dir is listed once.
|
|
57
|
+
*/
|
|
58
|
+
export declare const skillsDirs: ({ projectRoot, configured, env, create, }: SkillsDirsOptions) => SkillsDirs;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { error } from '@vltpkg/error-cause';
|
|
2
|
+
import { lstatSync, mkdirSync, readdirSync, realpathSync, statSync, } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { norm } from "./mounts.js";
|
|
5
|
+
/** Always a target when detecting: read by most agents. */
|
|
6
|
+
export const AGENTS_SKILLS = '.agents/skills';
|
|
7
|
+
/** Set (non-empty) inside Claude Code. */
|
|
8
|
+
export const CLAUDE_ENV = ['CLAUDECODE', 'CLAUDE_CODE'];
|
|
9
|
+
/**
|
|
10
|
+
* Agent folder at the project root → its skills dir.
|
|
11
|
+
*/
|
|
12
|
+
// vercel-labs/skills v1.7.2 src/agents.ts: skillsDir per agent (line), dot roots only
|
|
13
|
+
export const AGENT_ROOTS = [
|
|
14
|
+
['.adal', '.adal/skills'], // 813
|
|
15
|
+
['.aider-desk', '.aider-desk/skills'], // 84
|
|
16
|
+
['.augment', '.augment/skills'], // 140
|
|
17
|
+
['.autohand', '.autohand/skills'], // 131
|
|
18
|
+
['.bob', '.bob/skills'], // 149
|
|
19
|
+
['.claude', '.claude/skills'], // 158
|
|
20
|
+
['.codeartsdoer', '.codeartsdoer/skills'], // 191
|
|
21
|
+
['.codebuddy', '.codebuddy/skills'], // 200
|
|
22
|
+
['.codemaker', '.codemaker/skills'], // 209
|
|
23
|
+
['.codestudio', '.codestudio/skills'], // 218
|
|
24
|
+
['.commandcode', '.commandcode/skills'], // 236
|
|
25
|
+
['.continue', '.continue/skills'], // 245
|
|
26
|
+
['.cortex', '.cortex/skills'], // 254
|
|
27
|
+
['.crush', '.crush/skills'], // 263
|
|
28
|
+
['.devin', '.devin/skills'], // 291
|
|
29
|
+
['.forge', '.forge/skills'], // 345
|
|
30
|
+
['.goose', '.goose/skills'], // 382
|
|
31
|
+
['.grok', '.grok/skills'], // 391
|
|
32
|
+
['.hermes', '.hermes/skills'], // 400
|
|
33
|
+
['.iflow', '.iflow/skills'], // 436
|
|
34
|
+
['.inferencesh', '.inferencesh/skills'], // 409
|
|
35
|
+
['.jazz', '.jazz/skills'], // 418
|
|
36
|
+
['.junie', '.junie/skills'], // 427
|
|
37
|
+
['.kimchi', '.kimchi/skills'], // 456
|
|
38
|
+
['.kiro', '.kiro/skills'], // 474
|
|
39
|
+
['.kode', '.kode/skills'], // 483
|
|
40
|
+
['.lingma', '.lingma/skills'], // 492
|
|
41
|
+
['.mcpjam', '.mcpjam/skills'], // 511
|
|
42
|
+
['.minimax', '.minimax/skills'], // 520
|
|
43
|
+
['.moxby', '.moxby/skills'], // 538
|
|
44
|
+
['.mux', '.mux/skills'], // 547
|
|
45
|
+
['.neovate', '.neovate/skills'], // 782
|
|
46
|
+
['.ona', '.ona/skills'], // 574
|
|
47
|
+
['.openhands', '.openhands/skills'], // 565
|
|
48
|
+
['.pochi', '.pochi/skills'], // 791
|
|
49
|
+
['.posit/assistant', '.posit/assistant/skills'], // 595
|
|
50
|
+
['.qoder', '.qoder/skills'], // 604,613
|
|
51
|
+
['.qwen', '.qwen/skills'], // 622
|
|
52
|
+
['.reasonix', '.reasonix/skills'], // 641
|
|
53
|
+
['.roo', '.roo/skills'], // 659
|
|
54
|
+
['.rovodev', '.rovodev/skills'], // 650
|
|
55
|
+
['.tabnine', '.tabnine/agent/skills'], // 678
|
|
56
|
+
['.terramind', '.terramind/skills'], // 687
|
|
57
|
+
['.tinycloud', '.tinycloud/skills'], // 696
|
|
58
|
+
['.trae', '.trae/skills'], // 705,714
|
|
59
|
+
['.vibe', '.vibe/skills'], // 529
|
|
60
|
+
['.windsurf', '.windsurf/skills'], // 732
|
|
61
|
+
['.zcode', '.zcode/skills'], // 755
|
|
62
|
+
['.zencoder', '.zencoder/skills'], // 764,773
|
|
63
|
+
];
|
|
64
|
+
// strictly below root
|
|
65
|
+
const inside = (root, p) => {
|
|
66
|
+
const r = relative(root, p);
|
|
67
|
+
return (!!r && r !== '..' && !r.startsWith('..' + sep) && !isAbsolute(r));
|
|
68
|
+
};
|
|
69
|
+
// a `node_modules` segment below root
|
|
70
|
+
const inNodeModules = (root, p) => relative(root, p)
|
|
71
|
+
.split(sep)
|
|
72
|
+
.some(s => s.toLowerCase() === 'node_modules');
|
|
73
|
+
const isDir = (path) => {
|
|
74
|
+
try {
|
|
75
|
+
return statSync(path).isDirectory();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Throw EUSAGE unless each entry is a dir inside projectRoot, not in
|
|
83
|
+
* `node_modules`.
|
|
84
|
+
*/
|
|
85
|
+
export const assertSkillsDirs = (projectRoot, list = []) => {
|
|
86
|
+
for (const e of list) {
|
|
87
|
+
const p = resolve(projectRoot, e);
|
|
88
|
+
if (e &&
|
|
89
|
+
(!inside(projectRoot, p) || inNodeModules(projectRoot, p))) {
|
|
90
|
+
throw error('Invalid --skills-dir: must be a dir inside the project, not in node_modules', { code: 'EUSAGE', found: e }, assertSkillsDirs);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
// walk `dir` from the real root, one component at a time, so a
|
|
95
|
+
// symlink out of the project is caught before anything is made in it.
|
|
96
|
+
// A string is the reason it was skipped (fs errors: the code)
|
|
97
|
+
const walk = (realRoot, dir, create) => {
|
|
98
|
+
const parts = dir.split('/');
|
|
99
|
+
let real = realRoot;
|
|
100
|
+
try {
|
|
101
|
+
for (const [i, part] of parts.entries()) {
|
|
102
|
+
const next = join(real, part);
|
|
103
|
+
let st = lstatSync(next, { throwIfNoEntry: false });
|
|
104
|
+
if (!st) {
|
|
105
|
+
if (!create)
|
|
106
|
+
return { dir, real: join(real, ...parts.slice(i)) };
|
|
107
|
+
try {
|
|
108
|
+
mkdirSync(next);
|
|
109
|
+
}
|
|
110
|
+
catch (er) {
|
|
111
|
+
if (er.code !== 'EEXIST')
|
|
112
|
+
throw er;
|
|
113
|
+
}
|
|
114
|
+
st = lstatSync(next);
|
|
115
|
+
}
|
|
116
|
+
if (st.isSymbolicLink()) {
|
|
117
|
+
let r;
|
|
118
|
+
try {
|
|
119
|
+
r = realpathSync(next);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return 'not a directory';
|
|
123
|
+
}
|
|
124
|
+
if (!inside(realRoot, r))
|
|
125
|
+
return 'outside the project';
|
|
126
|
+
if (inNodeModules(realRoot, r))
|
|
127
|
+
return 'inside node_modules';
|
|
128
|
+
if (!isDir(r))
|
|
129
|
+
return 'not a directory';
|
|
130
|
+
real = r;
|
|
131
|
+
}
|
|
132
|
+
else if (st.isDirectory()) {
|
|
133
|
+
real = next;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
return 'not a directory';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const { dev, ino } = statSync(real, { bigint: true });
|
|
140
|
+
// some filesystems report no inode: compare paths
|
|
141
|
+
return { dir, real, id: ino ? `${dev}:${ino}` : norm(real) };
|
|
142
|
+
}
|
|
143
|
+
catch (er) {
|
|
144
|
+
return String(er.code);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* The project's agent skill dirs. `configured` replaces detection:
|
|
149
|
+
* `.agents/skills`, plus `<folder>/skills` for each agent folder at
|
|
150
|
+
* the root, `.claude/skills` inside Claude Code, and existing
|
|
151
|
+
* `.<name>/skills` dirs. Dirs must resolve inside projectRoot, not in
|
|
152
|
+
* `node_modules`; the same dir is listed once.
|
|
153
|
+
*/
|
|
154
|
+
export const skillsDirs = ({ projectRoot, configured, env = process.env, create = false, }) => {
|
|
155
|
+
const realRoot = realpathSync(projectRoot);
|
|
156
|
+
const res = { targets: [], scan: [], skipped: [] };
|
|
157
|
+
const dots = readdirSync(realRoot)
|
|
158
|
+
.filter(e => e.startsWith('.'))
|
|
159
|
+
.sort((a, b) => a.localeCompare(b, 'en'))
|
|
160
|
+
.map(e => `${e}/skills`);
|
|
161
|
+
let list;
|
|
162
|
+
if (configured) {
|
|
163
|
+
assertSkillsDirs(projectRoot, configured);
|
|
164
|
+
list = configured.filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const found = new Set();
|
|
168
|
+
for (const [marker, d] of AGENT_ROOTS) {
|
|
169
|
+
if (isDir(join(realRoot, marker)))
|
|
170
|
+
found.add(d);
|
|
171
|
+
}
|
|
172
|
+
if (CLAUDE_ENV.some(k => env[k]))
|
|
173
|
+
found.add('.claude/skills');
|
|
174
|
+
for (const d of dots) {
|
|
175
|
+
if (isDir(join(realRoot, d)))
|
|
176
|
+
found.add(d);
|
|
177
|
+
}
|
|
178
|
+
list = [
|
|
179
|
+
AGENTS_SKILLS,
|
|
180
|
+
...[...found].sort((a, b) => a.localeCompare(b, 'en')),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
const dirs = new Set(list.map(e => relative(projectRoot, resolve(projectRoot, e))
|
|
184
|
+
.split(sep)
|
|
185
|
+
.join('/')));
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
for (const dir of dirs) {
|
|
188
|
+
const d = walk(realRoot, dir, create);
|
|
189
|
+
if (typeof d === 'string') {
|
|
190
|
+
res.skipped.push(`${dir} (${d})`);
|
|
191
|
+
}
|
|
192
|
+
else if (!seen.has(d.id ?? d.real)) {
|
|
193
|
+
seen.add(d.id ?? d.real);
|
|
194
|
+
res.targets.push(d);
|
|
195
|
+
if (d.id)
|
|
196
|
+
res.scan.push(d);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const dir of [
|
|
200
|
+
AGENTS_SKILLS,
|
|
201
|
+
...AGENT_ROOTS.map(([, d]) => d),
|
|
202
|
+
...dots,
|
|
203
|
+
]) {
|
|
204
|
+
const d = walk(realRoot, dir, false);
|
|
205
|
+
if (typeof d !== 'string' && d.id && !seen.has(d.id)) {
|
|
206
|
+
seen.add(d.id);
|
|
207
|
+
res.scan.push(d);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return res;
|
|
211
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { DepID } from '@vltpkg/dep-id';
|
|
2
|
+
import type { PathScurry } from 'path-scurry';
|
|
3
|
+
import type { Node } from '../node.ts';
|
|
4
|
+
import type { SkillMount } from './mounts.ts';
|
|
5
|
+
/**
|
|
6
|
+
* An agent skill (a dir with a `SKILL.md`) shipped by an installed
|
|
7
|
+
* package. See https://agentskills.io/specification
|
|
8
|
+
*/
|
|
9
|
+
export type Skill = {
|
|
10
|
+
/** package name, e.g. `@s/bar` */
|
|
11
|
+
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* name a direct dep is installed under, when not `name`, as `vlt ls`
|
|
14
|
+
* shows it
|
|
15
|
+
*/
|
|
16
|
+
alias?: string;
|
|
17
|
+
/** package version */
|
|
18
|
+
version?: string;
|
|
19
|
+
/** package DepID */
|
|
20
|
+
id: DepID;
|
|
21
|
+
/**
|
|
22
|
+
* skill name: its dir name (`skills/<dir>`, `dist/skills/<dir>`,
|
|
23
|
+
* `.agents/skills/<dir>`), or the root SKILL.md `name`
|
|
24
|
+
*/
|
|
25
|
+
skill: string;
|
|
26
|
+
/**
|
|
27
|
+
* `SKILL.md` frontmatter `name`, when it differs from `skill`
|
|
28
|
+
* (matched by `:skill()`)
|
|
29
|
+
*/
|
|
30
|
+
frontmatterName?: string;
|
|
31
|
+
/** frontmatter description (folded to one line, control chars stripped) */
|
|
32
|
+
description?: string;
|
|
33
|
+
/** realpath of the dir holding SKILL.md */
|
|
34
|
+
path: string;
|
|
35
|
+
/** dirs holding a vlt link named `skill` to `path` */
|
|
36
|
+
linked: string[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Result of `discoverSkills`.
|
|
40
|
+
*/
|
|
41
|
+
export type DiscoveredSkills = {
|
|
42
|
+
/** skills of the picked version of each package, sorted by name */
|
|
43
|
+
skills: Skill[];
|
|
44
|
+
/** every scanned package version, w/ the skills it ships */
|
|
45
|
+
scanned: Map<DepID, Pick<Skill, 'skill' | 'path'>[]>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The `SKILL.md` frontmatter fields vlt reads.
|
|
49
|
+
*/
|
|
50
|
+
export type SkillFrontmatter = {
|
|
51
|
+
name?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Read `name` and `description` from a `SKILL.md` YAML frontmatter.
|
|
56
|
+
* Minimal line reader: top-level keys, plain/quoted/folded values.
|
|
57
|
+
*/
|
|
58
|
+
export declare const readFrontmatter: (file: string) => SkillFrontmatter;
|
|
59
|
+
/** Ships agent skills, as `discoverSkills` finds them. */
|
|
60
|
+
export declare const shipsSkills: (node: Node) => boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Find the agent skills shipped by the given installed packages.
|
|
63
|
+
*
|
|
64
|
+
* Only packages in the vlt store and the project's workspaces (not its
|
|
65
|
+
* root) are scanned, for a root `SKILL.md`, then `<dir>/SKILL.md` in
|
|
66
|
+
* `skills/`, `dist/skills/` and `.agents/skills/`: the first skill of a
|
|
67
|
+
* name wins. One version per package is picked, even if it ships none:
|
|
68
|
+
* a workspace or dep of the main importer, else a dep of any importer,
|
|
69
|
+
* else the highest version, then DepID. Same-name skills of different
|
|
70
|
+
* packages are all returned, sorted by rank then DepID (`linkSkills`
|
|
71
|
+
* resolves them). `mounts` set each skill's `linked` dirs.
|
|
72
|
+
*/
|
|
73
|
+
export declare const discoverSkills: (nodes: Iterable<Node>, scurry: PathScurry, mounts?: SkillMount[]) => DiscoveredSkills;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { splitDepID } from '@vltpkg/dep-id';
|
|
4
|
+
import { parse } from '@vltpkg/semver';
|
|
5
|
+
import { lstat, norm, packageKey, realOr } from "./mounts.js";
|
|
6
|
+
// safe single path segment, no dotfiles or scopes
|
|
7
|
+
const skillDirName = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
8
|
+
// agentskills.io `name`
|
|
9
|
+
const specName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const topKey = /^([A-Za-z0-9_-]+):/;
|
|
11
|
+
const blockIndicator = /^[>|][-+1-9]*$/;
|
|
12
|
+
const fold = (inline, more) => {
|
|
13
|
+
let value = [blockIndicator.test(inline) ? '' : inline, ...more]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(' ');
|
|
16
|
+
const q = value[0];
|
|
17
|
+
if (value.length > 1 &&
|
|
18
|
+
(q === '"' || q === "'") &&
|
|
19
|
+
value.endsWith(q)) {
|
|
20
|
+
value = value.slice(1, -1).trim();
|
|
21
|
+
}
|
|
22
|
+
return value || undefined;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Read `name` and `description` from a `SKILL.md` YAML frontmatter.
|
|
26
|
+
* Minimal line reader: top-level keys, plain/quoted/folded values.
|
|
27
|
+
*/
|
|
28
|
+
export const readFrontmatter = (file) => {
|
|
29
|
+
let text;
|
|
30
|
+
try {
|
|
31
|
+
text = readFileSync(file, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
const lines = text.split(/\r?\n/);
|
|
37
|
+
if (lines[0]?.trim() !== '---')
|
|
38
|
+
return {};
|
|
39
|
+
const end = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
|
|
40
|
+
if (end === -1)
|
|
41
|
+
return {};
|
|
42
|
+
const fm = {};
|
|
43
|
+
let key;
|
|
44
|
+
let inline = '';
|
|
45
|
+
let more = [];
|
|
46
|
+
const flush = () => {
|
|
47
|
+
if (key)
|
|
48
|
+
fm[key] = fold(inline, more);
|
|
49
|
+
key = undefined;
|
|
50
|
+
};
|
|
51
|
+
for (const line of lines.slice(1, end)) {
|
|
52
|
+
if (!line.trim())
|
|
53
|
+
continue;
|
|
54
|
+
const m = topKey.exec(line);
|
|
55
|
+
if (m) {
|
|
56
|
+
flush();
|
|
57
|
+
const k = m[1];
|
|
58
|
+
if (k === 'name' || k === 'description') {
|
|
59
|
+
key = k;
|
|
60
|
+
inline = line.slice(k.length + 1).trim();
|
|
61
|
+
// drop a ` # comment` from unquoted values
|
|
62
|
+
const hash = inline.search(/(?:^|\s)#/);
|
|
63
|
+
if (hash !== -1 && !/^["']/.test(inline)) {
|
|
64
|
+
inline = inline.slice(0, hash).trim();
|
|
65
|
+
}
|
|
66
|
+
more = [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (/^\s/.test(line)) {
|
|
70
|
+
if (key)
|
|
71
|
+
more.push(line.trim());
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
flush();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
flush();
|
|
78
|
+
return fm;
|
|
79
|
+
};
|
|
80
|
+
// package-controlled text printed to terminals
|
|
81
|
+
const sanitize = (s) => {
|
|
82
|
+
const clean = s
|
|
83
|
+
?.replace(/\p{Cc}/gu, ' ')
|
|
84
|
+
.replace(/\s+/g, ' ')
|
|
85
|
+
.trim();
|
|
86
|
+
return clean ? clean.slice(0, 1024) : undefined;
|
|
87
|
+
};
|
|
88
|
+
const lc = (s) => s.toLowerCase();
|
|
89
|
+
const isDir = (path) => !!lstat(path)?.isDirectory();
|
|
90
|
+
const isFile = (path) => !!lstat(path)?.isFile();
|
|
91
|
+
// `skills/SKILL.md` would make the dir look like a skill
|
|
92
|
+
const isSkillMd = (name) => name.toLowerCase() === 'skill.md';
|
|
93
|
+
// where packages ship skills, in precedence order (a name found first
|
|
94
|
+
// wins), after a root SKILL.md
|
|
95
|
+
const shipDirs = ['skills', 'dist/skills', '.agents/skills'];
|
|
96
|
+
// a real dir, no symlink on the way: skills stay inside the package
|
|
97
|
+
const subdir = (dir, sub) => {
|
|
98
|
+
let path = dir;
|
|
99
|
+
for (const seg of sub.split('/')) {
|
|
100
|
+
path = resolve(path, seg);
|
|
101
|
+
if (!isDir(path))
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return path;
|
|
105
|
+
};
|
|
106
|
+
const scan = (dir, pkgName) => {
|
|
107
|
+
const found = [];
|
|
108
|
+
const has = (skill) => found.some(f => lc(f.skill) === lc(skill));
|
|
109
|
+
const root = resolve(dir, 'SKILL.md');
|
|
110
|
+
if (isFile(root)) {
|
|
111
|
+
const { name, description } = readFrontmatter(root);
|
|
112
|
+
const skill = name && name.length <= 64 && specName.test(name) ?
|
|
113
|
+
name
|
|
114
|
+
: pkgName.slice(pkgName.lastIndexOf('/') + 1);
|
|
115
|
+
if (skillDirName.test(skill) && !isSkillMd(skill)) {
|
|
116
|
+
found.push({ skill, path: dir, name, description });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const sub of shipDirs) {
|
|
120
|
+
const skillsDir = subdir(dir, sub);
|
|
121
|
+
if (!skillsDir)
|
|
122
|
+
continue;
|
|
123
|
+
// one per name, any case
|
|
124
|
+
for (const d of readdirSync(skillsDir).sort()) {
|
|
125
|
+
if (!skillDirName.test(d) || isSkillMd(d) || has(d))
|
|
126
|
+
continue;
|
|
127
|
+
const path = resolve(skillsDir, d);
|
|
128
|
+
const file = resolve(path, 'SKILL.md');
|
|
129
|
+
if (!isDir(path) || !isFile(file))
|
|
130
|
+
continue;
|
|
131
|
+
const { name, description } = readFrontmatter(file);
|
|
132
|
+
found.push({ skill: d, path, name, description });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return found;
|
|
136
|
+
};
|
|
137
|
+
// 0: a workspace or dep of the main importer, 1: dep of any
|
|
138
|
+
// importer, 2: transitive
|
|
139
|
+
const rank = (node) => {
|
|
140
|
+
if (node.importer)
|
|
141
|
+
return 0;
|
|
142
|
+
let r = 2;
|
|
143
|
+
for (const { from } of node.edgesIn) {
|
|
144
|
+
if (from.mainImporter)
|
|
145
|
+
return 0;
|
|
146
|
+
if (from.importer)
|
|
147
|
+
r = 1;
|
|
148
|
+
}
|
|
149
|
+
return r;
|
|
150
|
+
};
|
|
151
|
+
const ver = (node) => node.version ? parse(node.version) : undefined;
|
|
152
|
+
// higher version first, if both valid
|
|
153
|
+
const byVersion = (a, b) => {
|
|
154
|
+
const va = ver(a);
|
|
155
|
+
const vb = ver(b);
|
|
156
|
+
return va && vb ? vb.compare(va) : 0;
|
|
157
|
+
};
|
|
158
|
+
const compare = (a, b) => rank(a) - rank(b) ||
|
|
159
|
+
byVersion(a, b) ||
|
|
160
|
+
a.id.localeCompare(b.id, 'en');
|
|
161
|
+
// the name a direct dep is installed under, if not its own
|
|
162
|
+
const aliasOf = (node) => {
|
|
163
|
+
const edges = [...node.edgesIn];
|
|
164
|
+
const n = (edges.find(e => e.from.mainImporter) ??
|
|
165
|
+
edges.find(e => e.from.importer))?.name;
|
|
166
|
+
return n === node.name ? undefined : n;
|
|
167
|
+
};
|
|
168
|
+
// store pkgs and workspaces, not the project root; lockfile keeps a
|
|
169
|
+
// tarball's path as its location
|
|
170
|
+
const scannable = (node) => node.importer ?
|
|
171
|
+
!node.mainImporter
|
|
172
|
+
: node.inVltStore() && splitDepID(node.id)[0] !== 'file';
|
|
173
|
+
/** Ships agent skills, as `discoverSkills` finds them. */
|
|
174
|
+
export const shipsSkills = (node) => scannable(node) &&
|
|
175
|
+
scan(realOr(resolve(node.projectRoot, node.location)), node.name)
|
|
176
|
+
.length > 0;
|
|
177
|
+
/**
|
|
178
|
+
* Find the agent skills shipped by the given installed packages.
|
|
179
|
+
*
|
|
180
|
+
* Only packages in the vlt store and the project's workspaces (not its
|
|
181
|
+
* root) are scanned, for a root `SKILL.md`, then `<dir>/SKILL.md` in
|
|
182
|
+
* `skills/`, `dist/skills/` and `.agents/skills/`: the first skill of a
|
|
183
|
+
* name wins. One version per package is picked, even if it ships none:
|
|
184
|
+
* a workspace or dep of the main importer, else a dep of any importer,
|
|
185
|
+
* else the highest version, then DepID. Same-name skills of different
|
|
186
|
+
* packages are all returned, sorted by rank then DepID (`linkSkills`
|
|
187
|
+
* resolves them). `mounts` set each skill's `linked` dirs.
|
|
188
|
+
*/
|
|
189
|
+
export const discoverSkills = (nodes, scurry, mounts = []) => {
|
|
190
|
+
const key = (name, path) => `${lc(name)}\0${norm(path)}`;
|
|
191
|
+
const linkedIn = new Map();
|
|
192
|
+
for (const m of mounts) {
|
|
193
|
+
const k = key(m.name, realOr(m.target));
|
|
194
|
+
const dirs = linkedIn.get(k) ?? [];
|
|
195
|
+
if (!dirs.includes(m.dir.dir))
|
|
196
|
+
dirs.push(m.dir.dir);
|
|
197
|
+
linkedIn.set(k, dirs);
|
|
198
|
+
}
|
|
199
|
+
const scanned = new Map();
|
|
200
|
+
const byPkg = new Map();
|
|
201
|
+
for (const node of nodes) {
|
|
202
|
+
if (!scannable(node))
|
|
203
|
+
continue;
|
|
204
|
+
const found = scan(realOr(node.resolvedLocation(scurry)), node.name);
|
|
205
|
+
scanned.set(node.id, found.map(({ skill, path }) => ({ skill, path })));
|
|
206
|
+
const key = packageKey(node.id);
|
|
207
|
+
const list = byPkg.get(key) ?? [];
|
|
208
|
+
list.push({ node, found });
|
|
209
|
+
byPkg.set(key, list);
|
|
210
|
+
}
|
|
211
|
+
const rows = [];
|
|
212
|
+
for (const list of byPkg.values()) {
|
|
213
|
+
const { node, found } = list.reduce((a, b) => compare(a.node, b.node) <= 0 ? a : b);
|
|
214
|
+
for (const { skill, path, name, description } of found) {
|
|
215
|
+
const fm = sanitize(name);
|
|
216
|
+
rows.push([
|
|
217
|
+
{
|
|
218
|
+
name: node.name,
|
|
219
|
+
alias: aliasOf(node),
|
|
220
|
+
version: node.version,
|
|
221
|
+
id: node.id,
|
|
222
|
+
skill,
|
|
223
|
+
...(fm && lc(fm) !== lc(skill) && { frontmatterName: fm }),
|
|
224
|
+
description: sanitize(description),
|
|
225
|
+
path,
|
|
226
|
+
linked: linkedIn.get(key(skill, path)) ?? [],
|
|
227
|
+
},
|
|
228
|
+
rank(node),
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const skills = rows
|
|
233
|
+
.sort(([a, ra], [b, rb]) => lc(a.skill).localeCompare(lc(b.skill), 'en') ||
|
|
234
|
+
ra - rb ||
|
|
235
|
+
a.id.localeCompare(b.id, 'en'))
|
|
236
|
+
.map(([s]) => s);
|
|
237
|
+
return { skills, scanned };
|
|
238
|
+
};
|