@vltpkg/graph 1.4.0 → 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.
Files changed (40) hide show
  1. package/README.md +59 -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/ideal/append-nodes.js +11 -6
  9. package/dist/ideal/get-importer-specs.js +17 -7
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.js +2 -0
  12. package/dist/install.d.ts +23 -8
  13. package/dist/install.js +24 -1
  14. package/dist/lockfile/load-edges.js +10 -2
  15. package/dist/reify/build.d.ts +7 -0
  16. package/dist/reify/check-needed-build.d.ts +5 -0
  17. package/dist/reify/check-needed-build.js +13 -0
  18. package/dist/reify/index.js +4 -38
  19. package/dist/reify/update-importers-package-json.js +5 -9
  20. package/dist/skills/dirs.d.ts +58 -0
  21. package/dist/skills/dirs.js +211 -0
  22. package/dist/skills/discover.d.ts +73 -0
  23. package/dist/skills/discover.js +234 -0
  24. package/dist/skills/gitignore.d.ts +13 -0
  25. package/dist/skills/gitignore.js +53 -0
  26. package/dist/skills/index.d.ts +123 -0
  27. package/dist/skills/index.js +225 -0
  28. package/dist/skills/lazy.d.ts +8 -0
  29. package/dist/skills/lazy.js +8 -0
  30. package/dist/skills/mask.d.ts +26 -0
  31. package/dist/skills/mask.js +43 -0
  32. package/dist/skills/mounts.d.ts +64 -0
  33. package/dist/skills/mounts.js +136 -0
  34. package/dist/uninstall.d.ts +24 -7
  35. package/dist/uninstall.js +24 -1
  36. package/dist/update.d.ts +24 -4
  37. package/dist/update.js +24 -1
  38. package/dist/visualization/mermaid-output.d.ts +8 -0
  39. package/dist/visualization/mermaid-output.js +25 -8
  40. package/package.json +21 -21
@@ -9,5 +9,12 @@ import type { Node } from '../node.ts';
9
9
  export type BuildResult = {
10
10
  success: Node[];
11
11
  failure: Node[];
12
+ /** needed builds skipped as they have no security data */
13
+ skipped?: Node[];
14
+ /**
15
+ * why `skipped` builds have no security data: the request timed
16
+ * out, or socket.dev has not scanned them yet
17
+ */
18
+ skippedReason?: 'timeout' | 'unscanned';
12
19
  };
13
20
  export declare const build: (diff: Diff, packageJson: PackageJson, scurry: PathScurry, allowScriptsNodes: Set<DepID>) => Promise<BuildResult>;
@@ -32,3 +32,8 @@ export type CheckNeededBuildOptions = {
32
32
  * @returns {BuildData} The BuildData object containing Node objects that need building
33
33
  */
34
34
  export declare const checkNeededBuild: (options: CheckNeededBuildOptions) => BuildData;
35
+ /**
36
+ * Copy build state from the actual graph onto ideal nodes that are
37
+ * not re-extracted, since `vlt-lock.json` does not store it.
38
+ */
39
+ export declare const carryBuildState: (diff: Diff) => void;
@@ -70,3 +70,16 @@ export const checkNeededBuild = (options) => {
70
70
  };
71
71
  return buildData;
72
72
  };
73
+ /**
74
+ * Copy build state from the actual graph onto ideal nodes that are
75
+ * not re-extracted, since `vlt-lock.json` does not store it.
76
+ */
77
+ export const carryBuildState = (diff) => {
78
+ for (const [id, node] of diff.to.nodes) {
79
+ if (diff.nodes.add.has(node))
80
+ continue;
81
+ const prev = diff.from.nodes.get(id);
82
+ if (prev)
83
+ node.buildState = prev.buildState;
84
+ }
85
+ };
@@ -8,51 +8,16 @@ import { addEdges } from "./add-edges.js";
8
8
  import { addNodes } from "./add-nodes.js";
9
9
  import { build } from "./build.js";
10
10
  import { deleteEdges } from "./delete-edges.js";
11
- import { checkNeededBuild } from "./check-needed-build.js";
11
+ import { carryBuildState, checkNeededBuild, } from "./check-needed-build.js";
12
12
  import { deleteNodes } from "./delete-nodes.js";
13
13
  import { internalHoist } from "./internal-hoist.js";
14
14
  import { pruneUnsupportedOptional } from "./optional-fail.js";
15
15
  import { rollback } from "./rollback.js";
16
16
  import { updatePackageJson } from "./update-importers-package-json.js";
17
17
  import { copyFileSync } from 'node:fs';
18
- import { Query } from '@vltpkg/query';
19
- import { SecurityArchive } from '@vltpkg/security-archive';
20
18
  import { binChmodAll } from "./bin-chmod.js";
19
+ import { filterNodesByQuery } from "../filter-nodes-by-query.js";
21
20
  const limit = Math.max(availableParallelism() - 1, 1) * 8;
22
- /**
23
- * Filter nodes using a DSS query string
24
- */
25
- const filterNodesByQuery = async (graph, allowScriptsQuery) => {
26
- // shortcut no packages included
27
- if (allowScriptsQuery === ':not(*)' /* c8 ignore next */ ||
28
- !allowScriptsQuery) {
29
- return new Set();
30
- }
31
- // shortcut all packages included
32
- if (allowScriptsQuery === '*') {
33
- return new Set(graph.nodes.keys());
34
- }
35
- /* c8 ignore start */
36
- const securityArchive = Query.hasSecuritySelectors(allowScriptsQuery) ?
37
- await SecurityArchive.start({
38
- nodes: [...graph.nodes.values()],
39
- })
40
- : undefined;
41
- /* c8 ignore stop */
42
- const edges = graph.edges;
43
- const nodes = new Set(graph.nodes.values());
44
- const importers = graph.importers;
45
- const query = new Query({
46
- edges,
47
- nodes,
48
- importers,
49
- securityArchive,
50
- });
51
- const { nodes: resultNodes } = await query.search(allowScriptsQuery, {
52
- signal: new AbortController().signal,
53
- });
54
- return new Set(resultNodes.map(node => node.id));
55
- };
56
21
  /**
57
22
  * Make the current project match the supplied graph.
58
23
  */
@@ -65,6 +30,7 @@ export const reify = async (options) => {
65
30
  loadManifests: true,
66
31
  });
67
32
  const diff = new Diff(actual, graph);
33
+ carryBuildState(diff);
68
34
  // normalise importer edge specs and compute the package.json changes
69
35
  // before deciding whether anything needs reifying: an add whose target
70
36
  // is already in place has no node diff but still has a saved value to
@@ -173,7 +139,7 @@ const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
173
139
  await build(diff, packageJson, scurry, allowScriptsNodes);
174
140
  // set the buildQueue on the result object containing
175
141
  // an array with all the ids of nodes that need building
176
- res.buildQueue = [...diff.nodes.add]
142
+ res.buildQueue = [...diff.to.nodes.values()]
177
143
  .filter(node => node.buildState === 'needed')
178
144
  .map(node => node.id);
179
145
  // if we had to change the actual graph along the way,
@@ -2,15 +2,9 @@ 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 depTypesMap = new Map([
8
- ['prod', 'dependencies'],
9
- ['dev', 'devDependencies'],
10
- ['peer', 'peerDependencies'],
11
- ['peerOptional', 'peerDependencies'],
12
- ['optional', 'optionalDependencies'],
13
- ]);
14
8
  const addOrRemoveDeps = (nodeId, graph, addOrRemove, saveExact, savePrefix) => {
15
9
  const node = graph.nodes.get(nodeId);
16
10
  if (!node) {
@@ -51,10 +45,12 @@ 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 = depTypesMap.get(depTypeShort);
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: [...depTypesMap.keys()],
53
+ validOptions: Object.keys(longDependencyTypeNames),
58
54
  found: dep.type,
59
55
  });
60
56
  }
@@ -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 are scanned, for a root `SKILL.md`,
65
+ * then `<dir>/SKILL.md` in `skills/`, `dist/skills/` and
66
+ * `.agents/skills/`: the first skill of a name wins. One version per
67
+ * package is picked, even if it ships none: a dep of the main
68
+ * importer, else of any importer, else the highest version, then
69
+ * DepID. Same-name skills of different packages are all returned,
70
+ * sorted by rank then DepID (`linkSkills` resolves them). `mounts`
71
+ * set each skill's `linked` dirs.
72
+ */
73
+ export declare const discoverSkills: (nodes: Iterable<Node>, scurry: PathScurry, mounts?: SkillMount[]) => DiscoveredSkills;