@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
package/README.md CHANGED
@@ -78,15 +78,71 @@ minimal filesystem changes (creating/deleting links, writing
78
78
  lockfiles, hoisting, lifecycle scripts) to make the on-disk install
79
79
  match the Ideal graph. Returns `{ diff, buildQueue }`.
80
80
 
81
- ### `install(options, add?): Promise<{ graph, diff, buildQueue }>`
81
+ ### `install(options, add?): Promise<{ graph, diff, buildQueue, skills? }>`
82
82
 
83
83
  High-level install orchestration that handles graph building, reify,
84
84
  and lockfile management. Supports `--frozen-lockfile`,
85
- `--clean-install`, and `--lockfile-only` modes.
85
+ `--clean-install`, and `--lockfile-only` modes. With `allowSkills` (a
86
+ DSS query whose selectors may end with `:skill()`, validated up
87
+ front), links the agent skills of matching packages afterwards (see
88
+ `linkSkills`) into `skillsDirs` (unset: detected) and returns the
89
+ result as `skills`. Unset or `:not(*)`: no skills work, the skills
90
+ code is not loaded. `update` and `uninstall` take `allowSkills` too.
91
+
92
+ ### `listSkills(options): Promise<{ project, graph, skills, dirs, skipped? }>`
93
+
94
+ Lists the agent skills (dirs with a `SKILL.md`, see
95
+ https://agentskills.io/specification) shipped by installed packages in
96
+ the vlt store (a root `SKILL.md`, then `<dir>/SKILL.md` in `skills/`,
97
+ `dist/skills/` and `.agents/skills/`; the first skill of a name wins),
98
+ from one version per package (a direct dep, else the highest), with
99
+ the agent skill dirs each is linked in, `project`: the main importer
100
+ name, and the `graph`. `options.target` is a DSS query selecting
101
+ packages (default `*`); a selector ending with `:skill(<name>, …)`
102
+ keeps only those skills of its packages. Creates nothing.
103
+
104
+ ### `linkSkills(options): Promise<LinkSkillsResult>`
105
+
106
+ Links the skills picked by `options.target` (as in `listSkills`; comma
107
+ lists are a union) into each agent skill dir (junctions on Windows):
108
+ `options.skillsDirs`, else `.agents/skills`, plus `<folder>/skills`
109
+ for each agent folder at the project root, `.claude/skills` inside
110
+ Claude Code (`options.env`), and existing `.<name>/skills` dirs. Dirs
111
+ must resolve inside the project, not in `node_modules` (else
112
+ `skipped`, as are fs errors) and are created only when a skill is
113
+ picked. One package per name, among picked skills, in every dir: the
114
+ one the first existing link points to while its skill is installed,
115
+ else a direct dep, then DepID; the others are reported in `conflicts`
116
+ as `<mount> (<package>)`. Installs keep a dir's link to another
117
+ installed skill. A link follows its package to the picked version.
118
+ Then removes links whose skill is no longer installed: dangling,
119
+ package not in the graph, or no longer shipped. With `options.apply`,
120
+ also removes every other vlt link in the scanned dirs. Only links
121
+ pointing into a `node_modules/.vlt` store are ever replaced or
122
+ removed; anything else in the way is reported in `conflicts`. Keeps a
123
+ marked block of link names in each dir's `.gitignore` (`gitignore`
124
+ lists the ones it could not edit). Linked packages get private copies
125
+ of files hardlinked from the global store. `:skill()` patterns that
126
+ match no skill of their packages are returned in `unmatched`. Security
127
+ queries fail closed: skills of npm registry packages with no security
128
+ data are left out (in `listSkills` too) and returned in `unscanned`
129
+ with `unscannedReason` (by `linkSkills`: unless still linked).
130
+
131
+ ### `assertQuery(selector, flag, partSelectors?): Promise<void>`
132
+
133
+ Throws `EUSAGE` unless `selector` is a valid DSS query.
134
+ `partSelectors` (e.g. `['skill']`) allows `:skill()`.
135
+
136
+ ### `hasSkills(node): Promise<boolean>`
137
+
138
+ `:skills` provider for `Query`: true when an installed package ships
139
+ agent skills, as `listSkills` finds them.
86
140
 
87
141
  ### `mermaidOutput(graph): string`
88
142
 
89
- Generates Mermaid flowchart syntax from graph data.
143
+ Generates Mermaid flowchart syntax from graph data. `graph.children`
144
+ (`Map<DepID, { label, edge? }[]>`) draws leaves under nodes, e.g.
145
+ `vlt skills list` skills.
90
146
 
91
147
  ### `humanReadableOutput(graph, options): string`
92
148
 
@@ -2,7 +2,7 @@ import { asDepID, hydrate, joinDepIDTuple, joinExtra, splitDepID, splitExtra, }
2
2
  import { Spec } from '@vltpkg/spec';
3
3
  import { graphStep } from '@vltpkg/output';
4
4
  import { isObject } from '@vltpkg/types';
5
- import { shorten, getRawDependencies, getDependencies, } from "../dependencies.js";
5
+ import { shorten, getRawDependencies, getDependencies, asDependencySpecError, } from "../dependencies.js";
6
6
  import { Graph } from "../graph.js";
7
7
  import { loadHidden } from "../lockfile/load.js";
8
8
  import { saveHidden } from "../lockfile/save.js";
@@ -178,13 +178,19 @@ const parseDir = (options, scurry, packageJson, depsFound, graph, fromNode, curr
178
178
  const type = deps?.type || 'dependencies';
179
179
  const bareSpec = deps?.bareSpec || '*';
180
180
  const depType = shorten(type, alias, fromNode.manifest);
181
- let spec = Spec.parse(alias, bareSpec, {
182
- ...options,
183
- // fall back to options.registry (which the lockfile merges into)
184
- // so importer-level edges don't silently revert to the default
185
- // npm registry. see vltpkg/vltpkg#1580.
186
- registry: fromNode.registry ?? options.registry,
187
- });
181
+ let spec;
182
+ try {
183
+ spec = Spec.parse(alias, bareSpec, {
184
+ ...options,
185
+ // fall back to options.registry (which the lockfile merges into)
186
+ // so importer-level edges don't silently revert to the default
187
+ // npm registry. see vltpkg/vltpkg#1580.
188
+ registry: fromNode.registry ?? options.registry,
189
+ });
190
+ }
191
+ catch (er) {
192
+ throw asDependencySpecError(er, fromNode, depType);
193
+ }
188
194
  // Check for active modifiers and replace spec if a modifier is complete
189
195
  const { spec: modifiedSpec, queryModifier } = maybeApplyModifierToSpec(spec, alias, modifierRefs);
190
196
  spec = modifiedSpec;
@@ -240,10 +246,16 @@ const parseDir = (options, scurry, packageJson, depsFound, graph, fromNode, curr
240
246
  for (const { name, type, bareSpec } of dependencies.values()) {
241
247
  if (!seenDeps.has(name)) {
242
248
  const depType = shorten(type, name, fromNode.manifest);
243
- let spec = Spec.parse(name, bareSpec, {
244
- ...options,
245
- registry: fromNode.registry ?? options.registry,
246
- });
249
+ let spec;
250
+ try {
251
+ spec = Spec.parse(name, bareSpec, {
252
+ ...options,
253
+ registry: fromNode.registry ?? options.registry,
254
+ });
255
+ }
256
+ catch (er) {
257
+ throw asDependencySpecError(er, fromNode, depType);
258
+ }
247
259
  // Check for active modifiers and replace spec for missing dependencies
248
260
  const { spec: modifiedSpec, queryModifier } = maybeApplyModifierToSpec(spec, name, modifierRefs);
249
261
  // a `-` modifier removes the dependency, so it is not missing
package/dist/build.js CHANGED
@@ -4,10 +4,12 @@ import { build as reifyBuild } from "./reify/build.js";
4
4
  import { Diff } from "./diff.js";
5
5
  import { Graph } from "./graph.js";
6
6
  import { Query } from '@vltpkg/query';
7
- import { SecurityArchive } from '@vltpkg/security-archive';
7
+ import { SecurityArchive, usesNpmRegistry, } from '@vltpkg/security-archive';
8
8
  import { saveHidden } from "./lockfile/save.js";
9
+ import { hasSkills } from "./skills/lazy.js";
9
10
  /**
10
- * Filter nodes using a DSS query string
11
+ * Filter nodes using a DSS query string. `skipped` holds matched nodes
12
+ * left out because they have no security data, `skippedReason` says why.
11
13
  */
12
14
  const filterNodesByQuery = async (targetQuery, graph) => {
13
15
  /* c8 ignore start */
@@ -25,11 +27,30 @@ const filterNodesByQuery = async (targetQuery, graph) => {
25
27
  nodes,
26
28
  importers,
27
29
  securityArchive,
30
+ hasSkills,
28
31
  });
29
32
  const { nodes: resultNodes } = await query.search(targetQuery, {
30
33
  signal: new AbortController().signal,
31
34
  });
32
- return new Set(resultNodes.map(node => node.id));
35
+ // fail closed: never run scripts of pkgs with no security data, be it
36
+ // from a timed out request or not scanned by socket.dev yet
37
+ const allowed = new Set();
38
+ const skipped = new Set();
39
+ for (const node of resultNodes) {
40
+ if (securityArchive &&
41
+ !securityArchive.has(node.id) &&
42
+ usesNpmRegistry(node)) {
43
+ skipped.add(node.id);
44
+ }
45
+ else {
46
+ allowed.add(node.id);
47
+ }
48
+ }
49
+ return {
50
+ allowed,
51
+ skipped,
52
+ skippedReason: securityArchive?.timedOut ? 'timeout' : 'unscanned',
53
+ };
33
54
  };
34
55
  /**
35
56
  * Build the project based on actual graph state and build state from lockfile
@@ -54,7 +75,7 @@ export const build = async (options) => {
54
75
  loadManifests: true,
55
76
  });
56
77
  // Filter nodes using target query provided
57
- const targetFilteredNodes = await filterNodesByQuery(target, actualGraph);
78
+ const { allowed: targetFilteredNodes, skipped, skippedReason, } = await filterNodesByQuery(target, actualGraph);
58
79
  // Create a total diff including the actual graph as 'to'
59
80
  const diff = new Diff(new Graph({
60
81
  ...options,
@@ -83,5 +104,9 @@ export const build = async (options) => {
83
104
  graph: actualGraph,
84
105
  });
85
106
  }
86
- return buildResult;
107
+ // needed builds left out for lack of security data
108
+ const skippedNodes = [...diff.nodes.add].filter(node => skipped.has(node.id));
109
+ return skippedNodes.length ?
110
+ { ...buildResult, skipped: skippedNodes, skippedReason }
111
+ : buildResult;
87
112
  };
@@ -74,6 +74,17 @@ export declare const shorten: (typeLong: DependencyTypeLong, name?: string, mani
74
74
  */
75
75
  export declare const shouldInstallDepType: (node: NodeLike, depType: DependencyTypeLong) => boolean;
76
76
  export declare const getRawDependencies: (node: NodeLike) => Map<string, RawDependency>;
77
+ /**
78
+ * Maps short dependency types to the `package.json` field declaring them.
79
+ */
80
+ export declare const longDependencyTypeNames: Record<DependencyTypeShort, DependencyTypeLong>;
81
+ /**
82
+ * Point an `ECONFIG` spec error (eg: an undefined `catalog:`) at the
83
+ * package.json & dependency type that declared it. `source` names the
84
+ * file the spec was read from when not the manifest (eg: a lockfile).
85
+ * Other values are returned as is.
86
+ */
87
+ export declare const asDependencySpecError: (er: unknown, from: NodeLike, type: DependencyTypeShort, source?: string) => unknown;
77
88
  /**
78
89
  * Retrieves a map of all dependencies, of all types, that can be inferred
79
90
  * from a given node manifest, including missing dependencies.
@@ -1,6 +1,6 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
2
  import { Spec } from '@vltpkg/spec/browser';
3
- import { dependencyTypes, longDependencyTypes, shortDependencyTypes, } from '@vltpkg/types';
3
+ import { isErrorWithCause, isObject, dependencyTypes, longDependencyTypes, shortDependencyTypes, } from '@vltpkg/types';
4
4
  export const isDependencyTypeShort = (obj) => shortDependencyTypes.has(obj);
5
5
  export const isDependencySaveType = (obj) => shortDependencyTypes.has(obj) ||
6
6
  obj === 'implicit';
@@ -113,6 +113,40 @@ export const getRawDependencies = (node) => {
113
113
  }
114
114
  return dependencies;
115
115
  };
116
+ /**
117
+ * Maps short dependency types to the `package.json` field declaring them.
118
+ */
119
+ export const longDependencyTypeNames = {
120
+ prod: 'dependencies',
121
+ dev: 'devDependencies',
122
+ optional: 'optionalDependencies',
123
+ peer: 'peerDependencies',
124
+ peerOptional: 'peerDependencies',
125
+ };
126
+ // `./`-relative unless already relative or absolute
127
+ const manifestPath = ({ location = '.' }) => `${/^(?:\.\.?(?:\/|$)|\/|[a-z]:)/i.test(location) ? '' : './'}${location}/package.json`;
128
+ /**
129
+ * Point an `ECONFIG` spec error (eg: an undefined `catalog:`) at the
130
+ * package.json & dependency type that declared it. `source` names the
131
+ * file the spec was read from when not the manifest (eg: a lockfile).
132
+ * Other values are returned as is.
133
+ */
134
+ export const asDependencySpecError = (er, from, type, source) => {
135
+ if (!isErrorWithCause(er) ||
136
+ !isObject(er.cause) ||
137
+ er.cause.code !== 'ECONFIG') {
138
+ return er;
139
+ }
140
+ const pj = manifestPath(from);
141
+ const depType = longDependencyTypeNames[type];
142
+ return error(er.message, {
143
+ ...er.cause,
144
+ from: source ?
145
+ `${source} (${pj} ${depType})`
146
+ : `${pj} (${depType})`,
147
+ cause: er,
148
+ }, asDependencySpecError);
149
+ };
116
150
  /**
117
151
  * Retrieves a map of all dependencies, of all types, that can be inferred
118
152
  * from a given node manifest, including missing dependencies.
@@ -122,12 +156,18 @@ export const getDependencies = (node, options) => {
122
156
  const dependencies = getRawDependencies(node);
123
157
  for (const { name, type, bareSpec } of dependencies.values()) {
124
158
  const depType = shorten(type, name, node.manifest);
125
- const spec = Spec.parse(name, bareSpec, {
126
- ...options,
127
- // fall back to options.registry so deps don't lose the configured
128
- // registry when the node itself has none. see vltpkg/vltpkg#1580.
129
- registry: node.registry ?? options.registry,
130
- });
159
+ let spec;
160
+ try {
161
+ spec = Spec.parse(name, bareSpec, {
162
+ ...options,
163
+ // fall back to options.registry so deps don't lose the configured
164
+ // registry when the node itself has none. see vltpkg/vltpkg#1580.
165
+ registry: node.registry ?? options.registry,
166
+ });
167
+ }
168
+ catch (er) {
169
+ throw asDependencySpecError(er, node, depType);
170
+ }
131
171
  res.set(name, {
132
172
  spec,
133
173
  type: depType,
@@ -0,0 +1,39 @@
1
+ import type { QueryOptions } from '@vltpkg/query';
2
+ import type { DepID } from '@vltpkg/dep-id';
3
+ import type { Graph } from './graph.ts';
4
+ /**
5
+ * One top-level selector of a query: the ids it matched, and its
6
+ * `:skill()` patterns (unset: every skill).
7
+ */
8
+ export type QueryPartIds = {
9
+ ids: Set<DepID>;
10
+ patterns?: string[];
11
+ };
12
+ /**
13
+ * Matched npm registry pkgs with no security data, and why: the
14
+ * request timed out, or socket.dev has not scanned them yet.
15
+ */
16
+ export type Unscanned = {
17
+ ids: Set<DepID>;
18
+ reason: 'timeout' | 'unscanned';
19
+ };
20
+ /**
21
+ * Filter nodes using a DSS query string
22
+ */
23
+ export declare const filterNodesByQuery: (graph: Graph, selector?: string) => Promise<Set<DepID>>;
24
+ /**
25
+ * {@link filterNodesByQuery} for `allow-skills`: accepts `:skill()`,
26
+ * and also returns one entry per top-level selector. Unlike it, keeps
27
+ * `unscanned` pkgs: callers must drop them (fail closed).
28
+ */
29
+ export declare const filterNodesByQueryParts: (graph: Graph, selector?: string) => Promise<{
30
+ ids: Set<DepID>;
31
+ parts: QueryPartIds[];
32
+ unscanned?: Unscanned;
33
+ }>;
34
+ /**
35
+ * Throw `EUSAGE` unless `selector` is a valid DSS query. Runs it on an
36
+ * empty graph: no fs, network or security data needed. `partSelectors`
37
+ * allows e.g. `:skill()`.
38
+ */
39
+ export declare const assertQuery: (selector: string, flag: string, partSelectors?: QueryOptions["partSelectors"]) => Promise<void>;
@@ -0,0 +1,100 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { Query } from '@vltpkg/query';
3
+ import { SecurityArchive, usesNpmRegistry, } from '@vltpkg/security-archive';
4
+ import { asError } from '@vltpkg/types';
5
+ import { hasSkills } from "./skills/lazy.js";
6
+ const toIds = (nodes) => new Set(nodes.map(n => n.id));
7
+ const search = async (graph, selector, partSelectors) => {
8
+ /* c8 ignore start */
9
+ const securityArchive = Query.hasSecuritySelectors(selector) ?
10
+ await SecurityArchive.start({
11
+ nodes: [...graph.nodes.values()],
12
+ })
13
+ : undefined;
14
+ /* c8 ignore stop */
15
+ const res = await new Query({
16
+ edges: graph.edges,
17
+ nodes: new Set(graph.nodes.values()),
18
+ importers: graph.importers,
19
+ securityArchive,
20
+ hasSkills,
21
+ partSelectors,
22
+ }).search(selector, { signal: new AbortController().signal });
23
+ if (!securityArchive)
24
+ return res;
25
+ // fail closed: callers skip pkgs with no security data, be it from a
26
+ // timed out request or not scanned by socket.dev yet
27
+ const ids = toIds(res.nodes.filter(n => !securityArchive.has(n.id) && usesNpmRegistry(n)));
28
+ if (!ids.size)
29
+ return res;
30
+ const reason = securityArchive.timedOut ? 'timeout' : 'unscanned';
31
+ return { ...res, unscanned: { ids, reason } };
32
+ };
33
+ /**
34
+ * Filter nodes using a DSS query string
35
+ */
36
+ export const filterNodesByQuery = async (graph, selector) => {
37
+ // shortcut no packages included
38
+ if (selector === ':not(*)' /* c8 ignore next */ || !selector) {
39
+ return new Set();
40
+ }
41
+ // shortcut all packages included
42
+ if (selector === '*') {
43
+ return new Set(graph.nodes.keys());
44
+ }
45
+ const { nodes, unscanned } = await search(graph, selector);
46
+ return toIds(nodes.filter(n => !unscanned?.ids.has(n.id)));
47
+ };
48
+ /**
49
+ * {@link filterNodesByQuery} for `allow-skills`: accepts `:skill()`,
50
+ * and also returns one entry per top-level selector. Unlike it, keeps
51
+ * `unscanned` pkgs: callers must drop them (fail closed).
52
+ */
53
+ export const filterNodesByQueryParts = async (graph, selector) => {
54
+ if (!selector || selector === ':not(*)') {
55
+ return { ids: new Set(), parts: [] };
56
+ }
57
+ if (selector === '*') {
58
+ const ids = new Set(graph.nodes.keys());
59
+ return { ids, parts: [{ ids }] };
60
+ }
61
+ const res = await search(graph, selector, ['skill']);
62
+ return {
63
+ ids: toIds(res.nodes),
64
+ /* c8 ignore next -- always set when opted in */
65
+ parts: (res.parts ?? []).map(p => ({
66
+ ids: toIds(p.nodes),
67
+ ...(p.patterns && { patterns: p.patterns }),
68
+ })),
69
+ ...(res.unscanned && { unscanned: res.unscanned }),
70
+ };
71
+ };
72
+ /**
73
+ * Throw `EUSAGE` unless `selector` is a valid DSS query. Runs it on an
74
+ * empty graph: no fs, network or security data needed. `partSelectors`
75
+ * allows e.g. `:skill()`.
76
+ */
77
+ export const assertQuery = async (selector, flag, partSelectors) => {
78
+ try {
79
+ await new Query({
80
+ edges: new Set(),
81
+ nodes: new Set(),
82
+ importers: new Set(),
83
+ securityArchive: new Map(),
84
+ hasSkills,
85
+ partSelectors,
86
+ }).search(selector, { signal: new AbortController().signal });
87
+ }
88
+ catch (cause) {
89
+ const { message, cause: c } = asError(cause);
90
+ // a typo's suggestions, e.g. `:skill` for `:skills`
91
+ const validOptions = c?.validOptions ??
92
+ (c?.wanted === undefined ? undefined : [c.wanted]);
93
+ throw error(`Invalid --${flag} query: ${message}`, {
94
+ code: 'EUSAGE',
95
+ found: selector,
96
+ cause,
97
+ ...(validOptions && { validOptions }),
98
+ }, assertQuery);
99
+ }
100
+ };
@@ -5,7 +5,7 @@ import { satisfies } from '@vltpkg/satisfies';
5
5
  import { parse as parseVersion } from '@vltpkg/semver';
6
6
  import { longDependencyTypes, normalizeManifest } from '@vltpkg/types';
7
7
  import { fixupAddedNames } from "../fixup-added-names.js";
8
- import { addKey, shorten, shouldInstallDepType, } from "../dependencies.js";
8
+ import { addKey, asDependencySpecError, shorten, shouldInstallDepType, } from "../dependencies.js";
9
9
  import { removeOptionalSubgraph } from "../remove-optional-subgraph.js";
10
10
  import { isPathSecurityError } from "../path-security-error.js";
11
11
  import { extractNode } from "../reify/extract-node.js";
@@ -549,15 +549,20 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
549
549
  // might need to skip already placed peer deps here
550
550
  if (bundled.has(name))
551
551
  continue;
552
- const dep = {
553
- type: shorten(depTypeName, name, manifest),
554
- spec: Spec.parse(name, bareSpec, {
552
+ const type = shorten(depTypeName, name, manifest);
553
+ let depSpec;
554
+ try {
555
+ depSpec = Spec.parse(name, bareSpec, {
555
556
  ...options,
556
557
  // git/file/remote parents have no spec.registry, so fall
557
558
  // back to the configured one. see vltpkg/vltpkg#1580.
558
559
  registry: spec.registry ?? options.registry,
559
- }),
560
- };
560
+ });
561
+ }
562
+ catch (er) {
563
+ throw asDependencySpecError(er, node, type);
564
+ }
565
+ const dep = { type, spec: depSpec };
561
566
  if (depTypeName === 'peerDependencies') {
562
567
  nextPeerDeps.set(name, dep);
563
568
  }
@@ -1,5 +1,5 @@
1
1
  import { longDependencyTypes } from '@vltpkg/types';
2
- import { shorten, asDependency } from "../dependencies.js";
2
+ import { shorten, asDependency, asDependencySpecError, } from "../dependencies.js";
3
3
  import { removeSatisfiedSpecs } from "./remove-satisfied-specs.js";
4
4
  import { Spec } from '@vltpkg/spec';
5
5
  const hasDepName = (importer, edge) => {
@@ -72,7 +72,13 @@ export const getImporterSpecs = (options) => {
72
72
  edge.type === type) {
73
73
  continue;
74
74
  }
75
- const spec = Spec.parse(depName, depSpec, options);
75
+ let spec;
76
+ try {
77
+ spec = Spec.parse(depName, depSpec, options);
78
+ }
79
+ catch (er) {
80
+ throw asDependencySpecError(er, importer, type);
81
+ }
76
82
  // a `-` modifier removes this dependency, nothing to place; an
77
83
  // edge from before (e.g. a qualifier now matching) has to go
78
84
  if (options.modifiers?.removesImporterEdge(importer, spec)) {
@@ -137,11 +143,15 @@ export const getImporterSpecs = (options) => {
137
143
  if (edge?.to && edge.spec.bareSpec === depSpec)
138
144
  continue;
139
145
  // add the dependency to the addDeps map
140
- const dependency = asDependency({
141
- spec: Spec.parse(depName, depSpec, options),
142
- type: shorten(depType, depName, manifest),
143
- });
144
- addDeps.set(depName, dependency);
146
+ const type = shorten(depType, depName, manifest);
147
+ let spec;
148
+ try {
149
+ spec = Spec.parse(depName, depSpec, options);
150
+ }
151
+ catch (er) {
152
+ throw asDependencySpecError(er, node, type);
153
+ }
154
+ addDeps.set(depName, asDependency({ spec, type }));
145
155
  }
146
156
  }
147
157
  // store in transientAdd if there are any dependencies
package/dist/index.d.ts CHANGED
@@ -15,6 +15,9 @@ export * from './update.ts';
15
15
  export * from './diff.ts';
16
16
  export * from './modifiers.ts';
17
17
  export * from './virtual-root.ts';
18
+ export type { DiscoveredSkills, LinkSkillsOptions, LinkSkillsResult, ListSkillsResult, MountResult, Skill, SkillFrontmatter, SkillMount, SkillRow, SkillsDir, SkillsOptions, UnmatchedPattern, UnscannedSkills, } from './skills/index.ts';
19
+ export { hasSkills, linkSkills, listSkills } from './skills/lazy.ts';
20
+ export { assertQuery } from './filter-nodes-by-query.ts';
18
21
  import type { LoadOptions as ActualLoadOptions } from './actual/load.ts';
19
22
  export declare const actual: {
20
23
  load: (options: ActualLoadOptions) => import("./graph.ts").Graph;
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ export * from "./update.js";
15
15
  export * from "./diff.js";
16
16
  export * from "./modifiers.js";
17
17
  export * from "./virtual-root.js";
18
+ export { hasSkills, linkSkills, listSkills } from "./skills/lazy.js";
19
+ export { assertQuery } from "./filter-nodes-by-query.js";
18
20
  import { load as actualLoad } from "./actual/load.js";
19
21
  export const actual = { load: actualLoad };
20
22
  import { load as lockfileLoad, loadData as lockfileLoadData, } from "./lockfile/load.js";
package/dist/install.d.ts CHANGED
@@ -3,19 +3,34 @@ import type { LoadOptions } from './actual/load.ts';
3
3
  import type { AddImportersDependenciesMap } from './dependencies.ts';
4
4
  import type { DepID } from '@vltpkg/dep-id';
5
5
  import type { Graph } from './index.ts';
6
+ import type { Diff } from './diff.ts';
7
+ import type { LinkSkillsResult } from './skills/index.ts';
6
8
  export type InstallOptions = LoadOptions & {
7
9
  packageInfo: PackageInfoClient;
8
10
  cleanInstall?: boolean;
9
11
  allowScripts: string;
10
12
  saveExact?: boolean;
11
13
  savePrefix?: string;
14
+ /**
15
+ * DSS query: packages whose agent skills get linked into the
16
+ * project's agent skill dirs after install. Selectors may end with
17
+ * `:skill()`. Unset or `:not(*)`: none.
18
+ */
19
+ allowSkills?: string;
20
+ /**
21
+ * agent skill dirs, relative to projectRoot. Unset: detect; `[]`:
22
+ * none
23
+ */
24
+ skillsDirs?: string[];
12
25
  };
13
- export declare const install: (options: InstallOptions, add?: AddImportersDependenciesMap) => Promise<{
26
+ /**
27
+ * Result of {@link install}.
28
+ */
29
+ export type InstallResult = {
30
+ buildQueue?: DepID[];
14
31
  graph: Graph;
15
- diff: undefined;
16
- buildQueue?: undefined;
17
- } | {
18
- buildQueue: DepID[] | undefined;
19
- graph: Graph;
20
- diff: import("./diff.ts").Diff;
21
- }>;
32
+ diff?: Diff;
33
+ /** skill links added or removed, if any */
34
+ skills?: LinkSkillsResult;
35
+ };
36
+ export declare const install: (options: InstallOptions, add?: AddImportersDependenciesMap) => Promise<InstallResult>;
package/dist/install.js CHANGED
@@ -16,11 +16,23 @@ import { getImporterSpecs } from "./ideal/get-importer-specs.js";
16
16
  import { lockfile } from "./index.js";
17
17
  import { updatePackageJson } from "./reify/update-importers-package-json.js";
18
18
  import { Monorepo } from '@vltpkg/workspaces';
19
+ import { assertSkillsDirs } from "./skills/dirs.js";
20
+ import { assertQuery } from "./filter-nodes-by-query.js";
19
21
  export const install = async (options, add) => {
20
22
  // Validate incompatible options
21
23
  if (options.lockfileOnly && options.cleanInstall) {
22
24
  throw error('Cannot use --lockfile-only with --clean-install (ci command). Clean install requires filesystem operations.');
23
25
  }
26
+ // unset or `:not(*)`: no skills work at all, the skills code never
27
+ // loads
28
+ const allowSkills = options.allowSkills === ':not(*)' ?
29
+ undefined
30
+ : options.allowSkills;
31
+ // fail before installing, not after
32
+ if (allowSkills) {
33
+ await assertQuery(allowSkills, 'allow-skills', ['skill']);
34
+ assertSkillsDirs(options.projectRoot, options.skillsDirs);
35
+ }
24
36
  if (options.expectLockfile || options.frozenLockfile) {
25
37
  const lockfilePath = resolve(options.projectRoot, 'vlt-lock.json');
26
38
  if (!existsSync(lockfilePath)) {
@@ -178,6 +190,7 @@ export const install = async (options, add) => {
178
190
  remover.confirm();
179
191
  }
180
192
  }
193
+ let result;
181
194
  try {
182
195
  const remove = Object.assign(new Map(), {
183
196
  modifiedDependencies: false,
@@ -247,7 +260,7 @@ export const install = async (options, add) => {
247
260
  remove,
248
261
  remover,
249
262
  });
250
- return { buildQueue, graph, diff };
263
+ result = { buildQueue, graph, diff };
251
264
  }
252
265
  catch (err) {
253
266
  /* c8 ignore next */
@@ -262,4 +275,14 @@ export const install = async (options, add) => {
262
275
  catch { }
263
276
  throw err;
264
277
  }
278
+ // after the try: a link failure must not roll back the install
279
+ if (!allowSkills)
280
+ return result;
281
+ const { linkSkills } = await import("./skills/index.js");
282
+ result.skills = await linkSkills({
283
+ ...options,
284
+ graph: result.graph,
285
+ target: allowSkills,
286
+ });
287
+ return result;
265
288
  };
@@ -3,7 +3,7 @@ import { error } from '@vltpkg/error-cause';
3
3
  import { fastSplit } from '@vltpkg/fast-split';
4
4
  import { Spec } from '@vltpkg/spec/browser';
5
5
  import { longDependencyTypes } from '@vltpkg/types';
6
- import { isDependencyTypeShort } from "../dependencies.js";
6
+ import { asDependencySpecError, isDependencyTypeShort, } from "../dependencies.js";
7
7
  const retrieveNodeFromGraph = (key, value, graph, fromId, seenNodes) => {
8
8
  const foundNode = graph.nodes.get(asDepID(fromId));
9
9
  if (!foundNode) {
@@ -104,7 +104,15 @@ export const loadEdges = (graph, edges, options, cache) => {
104
104
  // loaded), so overriding here would always inject `undefined`
105
105
  // and silently revert to the default npm registry.
106
106
  // See vltpkg/vltpkg#1580.
107
- const spec = parseEdgeSpec(specName, valRest.substring(0, vrSplit), options, cache);
107
+ let spec;
108
+ try {
109
+ spec = parseEdgeSpec(specName, valRest.substring(0, vrSplit), options, cache);
110
+ }
111
+ catch (er) {
112
+ // spec text is the lockfile's; hidden lockfile errors are
113
+ // swallowed, so only vlt-lock.json surfaces here
114
+ throw asDependencySpecError(er, fromNode, depType, 'vlt-lock.json');
115
+ }
108
116
  if (useOptimizations) {
109
117
  edgeProcessingQueue.push({
110
118
  fromNode,