@vltpkg/graph 1.3.8 → 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 +28 -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 +19 -6
- package/dist/ideal/build-ideal-from-starting-graph.js +8 -0
- package/dist/ideal/get-importer-specs.d.ts +1 -0
- package/dist/ideal/get-importer-specs.js +27 -7
- package/dist/ideal/refresh-ideal-graph.js +6 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/install.d.ts +23 -8
- package/dist/install.js +46 -1
- package/dist/lockfile/load-edges.js +10 -4
- package/dist/modifiers.d.ts +7 -0
- package/dist/modifiers.js +24 -0
- 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 +9 -39
- 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 +26 -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/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
|
@@ -7,6 +7,7 @@ import { error } from '@vltpkg/error-cause';
|
|
|
7
7
|
import { asError } from '@vltpkg/types';
|
|
8
8
|
import { getDependencies } from "./dependencies.js";
|
|
9
9
|
import { RollbackRemove } from '@vltpkg/rollback-remove';
|
|
10
|
+
import { joinDepIDTuple } from '@vltpkg/dep-id';
|
|
10
11
|
import { existsSync, rmSync } from 'node:fs';
|
|
11
12
|
import { resolve } from 'node:path';
|
|
12
13
|
import { load as loadVirtual, loadData } from "./lockfile/load.js";
|
|
@@ -15,11 +16,23 @@ import { getImporterSpecs } from "./ideal/get-importer-specs.js";
|
|
|
15
16
|
import { lockfile } from "./index.js";
|
|
16
17
|
import { updatePackageJson } from "./reify/update-importers-package-json.js";
|
|
17
18
|
import { Monorepo } from '@vltpkg/workspaces';
|
|
19
|
+
import { assertSkillsDirs } from "./skills/dirs.js";
|
|
20
|
+
import { assertQuery } from "./filter-nodes-by-query.js";
|
|
18
21
|
export const install = async (options, add) => {
|
|
19
22
|
// Validate incompatible options
|
|
20
23
|
if (options.lockfileOnly && options.cleanInstall) {
|
|
21
24
|
throw error('Cannot use --lockfile-only with --clean-install (ci command). Clean install requires filesystem operations.');
|
|
22
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
|
+
}
|
|
23
36
|
if (options.expectLockfile || options.frozenLockfile) {
|
|
24
37
|
const lockfilePath = resolve(options.projectRoot, 'vlt-lock.json');
|
|
25
38
|
if (!existsSync(lockfilePath)) {
|
|
@@ -109,6 +122,7 @@ export const install = async (options, add) => {
|
|
|
109
122
|
}
|
|
110
123
|
if (importerSpecs.add.modifiedDependencies ||
|
|
111
124
|
importerSpecs.remove.modifiedDependencies ||
|
|
125
|
+
importerSpecs.removedEdges.size > 0 ||
|
|
112
126
|
specChanges.length > 0 ||
|
|
113
127
|
lockfileGraph.optionsChanged) {
|
|
114
128
|
const details = [];
|
|
@@ -141,6 +155,9 @@ export const install = async (options, add) => {
|
|
|
141
155
|
details.push(` ${location}: ${deps.size} ${depLabelRemove} to remove (${depNames.join(', ')})`);
|
|
142
156
|
}
|
|
143
157
|
}
|
|
158
|
+
for (const edge of importerSpecs.removedEdges) {
|
|
159
|
+
details.push(` ${edge.from.location}: ${edge.name} is removed by a graph modifier`);
|
|
160
|
+
}
|
|
144
161
|
const lockfilePath = resolve(options.projectRoot, 'vlt-lock.json');
|
|
145
162
|
throw error('Lockfile is out of sync with package.json. Run "vlt install" to update.\n' +
|
|
146
163
|
details.join('\n'), {
|
|
@@ -148,6 +165,23 @@ export const install = async (options, add) => {
|
|
|
148
165
|
});
|
|
149
166
|
}
|
|
150
167
|
}
|
|
168
|
+
// an explicit add of a dep a `-` modifier removes fails before
|
|
169
|
+
// anything on disk is touched
|
|
170
|
+
if (modifiers && add?.modifiedDependencies) {
|
|
171
|
+
const wsIds = new Set([...(fullMonorepo?.values() ?? [])].map(ws => ws.id));
|
|
172
|
+
for (const [id, deps] of add) {
|
|
173
|
+
const mainImporter = id === joinDepIDTuple(['file', '.']);
|
|
174
|
+
// nested folders are no importers, appendNodes checks those
|
|
175
|
+
if (!mainImporter && !wsIds.has(id))
|
|
176
|
+
continue;
|
|
177
|
+
for (const { spec } of deps.values()) {
|
|
178
|
+
const found = modifiers.removesImporterEdge({ importer: true, mainImporter }, spec);
|
|
179
|
+
if (found) {
|
|
180
|
+
throw error(`Cannot add ${spec.name}: it is removed by a graph modifier`, { code: 'ECONFIG', found });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
151
185
|
const remover = new RollbackRemove();
|
|
152
186
|
if (options.cleanInstall) {
|
|
153
187
|
const nodeModulesPath = resolve(options.projectRoot, 'node_modules');
|
|
@@ -156,6 +190,7 @@ export const install = async (options, add) => {
|
|
|
156
190
|
remover.confirm();
|
|
157
191
|
}
|
|
158
192
|
}
|
|
193
|
+
let result;
|
|
159
194
|
try {
|
|
160
195
|
const remove = Object.assign(new Map(), {
|
|
161
196
|
modifiedDependencies: false,
|
|
@@ -225,7 +260,7 @@ export const install = async (options, add) => {
|
|
|
225
260
|
remove,
|
|
226
261
|
remover,
|
|
227
262
|
});
|
|
228
|
-
|
|
263
|
+
result = { buildQueue, graph, diff };
|
|
229
264
|
}
|
|
230
265
|
catch (err) {
|
|
231
266
|
/* c8 ignore next */
|
|
@@ -240,4 +275,14 @@ export const install = async (options, add) => {
|
|
|
240
275
|
catch { }
|
|
241
276
|
throw err;
|
|
242
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;
|
|
243
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) {
|
|
@@ -52,11 +52,9 @@ export const loadEdges = (graph, edges, options, cache) => {
|
|
|
52
52
|
const [depType, valRest] = fastSplit(value, ' ', 2);
|
|
53
53
|
const vrSplit = valRest?.lastIndexOf(' ') ?? -1;
|
|
54
54
|
// not a valid edge record
|
|
55
|
-
/* c8 ignore start */
|
|
56
55
|
if (!valRest || !depType || !fromId || !specName || vrSplit < 1) {
|
|
57
56
|
continue;
|
|
58
57
|
}
|
|
59
|
-
/* c8 ignore stop */
|
|
60
58
|
// Validate dependency type early
|
|
61
59
|
if (!isDependencyTypeShort(depType)) {
|
|
62
60
|
throw error('Found unsupported dependency type in lockfile', {
|
|
@@ -106,7 +104,15 @@ export const loadEdges = (graph, edges, options, cache) => {
|
|
|
106
104
|
// loaded), so overriding here would always inject `undefined`
|
|
107
105
|
// and silently revert to the default npm registry.
|
|
108
106
|
// See vltpkg/vltpkg#1580.
|
|
109
|
-
|
|
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
|
+
}
|
|
110
116
|
if (useOptimizations) {
|
|
111
117
|
edgeProcessingQueue.push({
|
|
112
118
|
fromNode,
|
package/dist/modifiers.d.ts
CHANGED
|
@@ -126,6 +126,13 @@ export declare class GraphModifier {
|
|
|
126
126
|
* root's own b edge is still the manifest's to validate and heal.
|
|
127
127
|
*/
|
|
128
128
|
targetsImporterEdge(importer: Node, spec: Spec): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Selector of the modifier that wins the direct `importer -> spec.name`
|
|
131
|
+
* edge, if it removes it (value `-`). Same pick as tryNewDependency:
|
|
132
|
+
* complete entries whose qualifier accepts the spec, highest
|
|
133
|
+
* specificity, importer-anchored before lone on ties.
|
|
134
|
+
*/
|
|
135
|
+
removesImporterEdge(importer: Pick<Node, 'importer' | 'mainImporter'>, spec: Spec): string | undefined;
|
|
129
136
|
/**
|
|
130
137
|
* Loads the modifiers defined in `vlt.json` into memory.
|
|
131
138
|
*/
|
package/dist/modifiers.js
CHANGED
|
@@ -115,6 +115,30 @@ export class GraphModifier {
|
|
|
115
115
|
}
|
|
116
116
|
return false;
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Selector of the modifier that wins the direct `importer -> spec.name`
|
|
120
|
+
* edge, if it removes it (value `-`). Same pick as tryNewDependency:
|
|
121
|
+
* complete entries whose qualifier accepts the spec, highest
|
|
122
|
+
* specificity, importer-anchored before lone on ties.
|
|
123
|
+
*/
|
|
124
|
+
removesImporterEdge(importer, spec) {
|
|
125
|
+
const anchored = [];
|
|
126
|
+
const lone = [];
|
|
127
|
+
for (const mod of this.#modifiers) {
|
|
128
|
+
const { last } = mod.breadcrumb;
|
|
129
|
+
if (last.name !== spec.name || !matchesQualifier(last, spec))
|
|
130
|
+
continue;
|
|
131
|
+
const { prev } = last;
|
|
132
|
+
if (!prev)
|
|
133
|
+
lone.push(mod);
|
|
134
|
+
else if (!prev.prev && matchesImporter(prev, importer))
|
|
135
|
+
anchored.push(mod);
|
|
136
|
+
}
|
|
137
|
+
const all = [...anchored, ...lone];
|
|
138
|
+
const [winner] = specificitySort(all.map(m => m.breadcrumb));
|
|
139
|
+
const mod = all.find(m => m.breadcrumb === winner);
|
|
140
|
+
return mod?.value === '-' ? mod.query : undefined;
|
|
141
|
+
}
|
|
118
142
|
/**
|
|
119
143
|
* Loads the modifiers defined in `vlt.json` into memory.
|
|
120
144
|
*/
|
package/dist/reify/build.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/dist/reify/index.js
CHANGED
|
@@ -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
|
|
@@ -89,7 +55,11 @@ export const reify = async (options) => {
|
|
|
89
55
|
// Otherwise a first install of an optional-only tree would never extract.
|
|
90
56
|
const hasLockfiles = !!scurry.lstatSync('vlt-lock.json') &&
|
|
91
57
|
!!scurry.lstatSync('node_modules/.vlt-lock.json');
|
|
92
|
-
|
|
58
|
+
// a removal is never optional-only: deleting is what reify is for
|
|
59
|
+
const skipOptionalOnly = noModifiedDependencies &&
|
|
60
|
+
diff.optionalOnly &&
|
|
61
|
+
!diff.nodes.delete.size &&
|
|
62
|
+
!diff.edges.delete.size;
|
|
93
63
|
const skippable = skipOptionalOnly && !options.update && hasLockfiles;
|
|
94
64
|
const res = { diff };
|
|
95
65
|
if (!diff.hasChanges() || skippable) {
|
|
@@ -169,7 +139,7 @@ const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
|
|
|
169
139
|
await build(diff, packageJson, scurry, allowScriptsNodes);
|
|
170
140
|
// set the buildQueue on the result object containing
|
|
171
141
|
// an array with all the ids of nodes that need building
|
|
172
|
-
res.buildQueue = [...diff.nodes.
|
|
142
|
+
res.buildQueue = [...diff.to.nodes.values()]
|
|
173
143
|
.filter(node => node.buildState === 'needed')
|
|
174
144
|
.map(node => node.id);
|
|
175
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 =
|
|
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
|
}
|
|
@@ -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
|
+
};
|