@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.
- package/README.md +59 -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/ideal/append-nodes.js +11 -6
- package/dist/ideal/get-importer-specs.js +17 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/install.d.ts +23 -8
- package/dist/install.js +24 -1
- package/dist/lockfile/load-edges.js +10 -2
- package/dist/reify/build.d.ts +7 -0
- package/dist/reify/check-needed-build.d.ts +5 -0
- package/dist/reify/check-needed-build.js +13 -0
- package/dist/reify/index.js +4 -38
- package/dist/reify/update-importers-package-json.js +5 -9
- package/dist/skills/dirs.d.ts +58 -0
- package/dist/skills/dirs.js +211 -0
- package/dist/skills/discover.d.ts +73 -0
- package/dist/skills/discover.js +234 -0
- package/dist/skills/gitignore.d.ts +13 -0
- package/dist/skills/gitignore.js +53 -0
- package/dist/skills/index.d.ts +123 -0
- package/dist/skills/index.js +225 -0
- package/dist/skills/lazy.d.ts +8 -0
- package/dist/skills/lazy.js +8 -0
- package/dist/skills/mask.d.ts +26 -0
- package/dist/skills/mask.js +43 -0
- package/dist/skills/mounts.d.ts +64 -0
- package/dist/skills/mounts.js +136 -0
- package/dist/uninstall.d.ts +24 -7
- package/dist/uninstall.js +24 -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
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
|
|
package/dist/actual/load.js
CHANGED
|
@@ -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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/dependencies.d.ts
CHANGED
|
@@ -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.
|
package/dist/dependencies.js
CHANGED
|
@@ -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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Result of {@link install}.
|
|
28
|
+
*/
|
|
29
|
+
export type InstallResult = {
|
|
30
|
+
buildQueue?: DepID[];
|
|
14
31
|
graph: Graph;
|
|
15
|
-
diff
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|