@vltpkg/cli-sdk 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.
Files changed (50) hide show
  1. package/dist/allow-skills.d.ts +36 -0
  2. package/dist/allow-skills.js +49 -0
  3. package/dist/commands/build.d.ts +7 -1
  4. package/dist/commands/build.js +19 -9
  5. package/dist/commands/cache.d.ts +2 -2
  6. package/dist/commands/cache.js +22 -7
  7. package/dist/commands/ci.d.ts +1 -1
  8. package/dist/commands/ci.js +10 -2
  9. package/dist/commands/init.js +7 -8
  10. package/dist/commands/install/reporter.js +46 -0
  11. package/dist/commands/install.d.ts +29 -1
  12. package/dist/commands/install.js +35 -1
  13. package/dist/commands/list.js +3 -1
  14. package/dist/commands/pack.js +2 -1
  15. package/dist/commands/pkg.js +4 -1
  16. package/dist/commands/publish.d.ts +0 -1
  17. package/dist/commands/publish.js +31 -17
  18. package/dist/commands/query.js +3 -1
  19. package/dist/commands/skills.d.ts +56 -0
  20. package/dist/commands/skills.js +268 -0
  21. package/dist/commands/uninstall.d.ts +5 -1
  22. package/dist/commands/uninstall.js +4 -2
  23. package/dist/commands/unpublish.d.ts +0 -1
  24. package/dist/commands/unpublish.js +17 -4
  25. package/dist/commands/update.d.ts +12 -0
  26. package/dist/commands/update.js +18 -1
  27. package/dist/commands/version.js +4 -1
  28. package/dist/config/definition.d.ts +26 -2
  29. package/dist/config/definition.js +75 -11
  30. package/dist/config/index.d.ts +4 -0
  31. package/dist/config/index.js +22 -6
  32. package/dist/config/spec-aliases.d.ts +22 -0
  33. package/dist/config/spec-aliases.js +47 -0
  34. package/dist/custom-help.js +4 -0
  35. package/dist/exec-command.js +12 -6
  36. package/dist/index.js +18 -8
  37. package/dist/load-command.d.ts +2 -0
  38. package/dist/migrate-workspaces.d.ts +16 -0
  39. package/dist/migrate-workspaces.js +44 -0
  40. package/dist/output.js +2 -1
  41. package/dist/print-err.js +4 -1
  42. package/dist/publish-config.d.ts +9 -0
  43. package/dist/publish-config.js +8 -0
  44. package/dist/save-types.d.ts +20 -0
  45. package/dist/save-types.js +105 -0
  46. package/dist/skills-prompt.d.ts +28 -0
  47. package/dist/skills-prompt.js +78 -0
  48. package/dist/sort-scope-locations.d.ts +11 -0
  49. package/dist/sort-scope-locations.js +57 -0
  50. package/package.json +28 -28
@@ -0,0 +1,36 @@
1
+ import type { UnmatchedPattern, UnscannedSkills } from '@vltpkg/graph';
2
+ import type { WhichConfig } from '@vltpkg/vlt-json';
3
+ import type { LoadedConfig } from './config/index.ts';
4
+ /** An `allow-skills` query stored in a config file. */
5
+ export type StoredAllowSkills = {
6
+ query: string;
7
+ which: WhichConfig;
8
+ };
9
+ /**
10
+ * Outcome of saving `--allow-skills` after install/update. `stored`
11
+ * set: another query is stored and was kept.
12
+ */
13
+ export type AllowSkillsSave = {
14
+ query: string;
15
+ stored?: StoredAllowSkills;
16
+ };
17
+ /**
18
+ * The top-level `allow-skills` of the project vlt.json, else the user
19
+ * one. A project `null` or `''` drops the user's.
20
+ */
21
+ export declare const storedAllowSkills: (conf: Pick<LoadedConfig, "layers">) => StoredAllowSkills | undefined;
22
+ /** The file's name in messages. */
23
+ export declare const configLabel: (which: WhichConfig) => string;
24
+ /**
25
+ * After a successful install/update: save an explicit `--allow-skills`
26
+ * to the project vlt.json, unless another query is stored.
27
+ */
28
+ export declare const saveAllowSkills: (conf: LoadedConfig) => Promise<AllowSkillsSave | undefined>;
29
+ /** Warnings for `:skill()` patterns that matched nothing. */
30
+ export declare const unmatchedLines: (unmatched?: UnmatchedPattern[]) => string[];
31
+ /** Skills left out: their packages have no security data. */
32
+ export declare const unscannedLines: ({ unscanned, unscannedReason, }: UnscannedSkills) => string[];
33
+ /** `allow-skills` saved to a config file. */
34
+ export declare const savedLine: (query: string, which: WhichConfig) => string;
35
+ /** `--allow-skills` not saved: another query is stored. */
36
+ export declare const keptLine: (s: StoredAllowSkills) => string;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The top-level `allow-skills` of the project vlt.json, else the user
3
+ * one. A project `null` or `''` drops the user's.
4
+ */
5
+ export const storedAllowSkills = (conf) => {
6
+ const { project, user } = conf.layers;
7
+ const p = project?.['allow-skills'];
8
+ if (p)
9
+ return { query: p, which: 'project' };
10
+ if (project && 'allow-skills' in project)
11
+ return undefined;
12
+ const u = user?.['allow-skills'];
13
+ return u ? { query: u, which: 'user' } : undefined;
14
+ };
15
+ /** The file's name in messages. */
16
+ export const configLabel = (which) => which === 'user' ? 'user vlt.json' : 'vlt.json';
17
+ /**
18
+ * After a successful install/update: save an explicit `--allow-skills`
19
+ * to the project vlt.json, unless another query is stored.
20
+ */
21
+ export const saveAllowSkills = async (conf) => {
22
+ const query = conf.explicit['allow-skills'];
23
+ if (!query || query === ':not(*)')
24
+ return undefined;
25
+ const stored = storedAllowSkills(conf);
26
+ if (stored?.query === query)
27
+ return undefined;
28
+ if (stored)
29
+ return { query, stored };
30
+ await conf.addConfigToFile('project', { 'allow-skills': query });
31
+ return { query };
32
+ };
33
+ /** Warnings for `:skill()` patterns that matched nothing. */
34
+ export const unmatchedLines = (unmatched = []) => unmatched.map(({ pattern, skills }) => `⚠️ No skill matches "${pattern}"; ${!skills ? 'no installed package matches its selector'
35
+ : skills.length ?
36
+ `the selected packages ship: ${skills.join(', ')}`
37
+ : 'the selected packages ship no skills'}`);
38
+ /** Skills left out: their packages have no security data. */
39
+ export const unscannedLines = ({ unscanned = [], unscannedReason, }) => unscanned.length ?
40
+ [
41
+ `⏳ ${unscanned.length} skill${unscanned.length === 1 ? '' : 's'} left out, ${unscannedReason === 'timeout' ?
42
+ 'security data timed out'
43
+ : 'Socket has not scanned their packages yet'}: ${unscanned.join(', ')}. Try again later.`,
44
+ ]
45
+ : [];
46
+ /** `allow-skills` saved to a config file. */
47
+ export const savedLine = (query, which) => `Saved allow-skills="${query}" to ${configLabel(which)}`;
48
+ /** `--allow-skills` not saved: another query is stored. */
49
+ export const keptLine = (s) => `allow-skills is already set (${configLabel(s.which)}): "${s.query}"; not replaced. Run vlt skills link to change it, or edit ${configLabel(s.which)}`;
@@ -3,6 +3,13 @@ import type { CommandFn, CommandUsage } from '../index.ts';
3
3
  export declare const views: {
4
4
  readonly human: (result: BuildResult) => string;
5
5
  readonly json: (result: BuildResult) => {
6
+ message: string;
7
+ skipped?: {
8
+ id: import("@vltpkg/dep-id").DepID;
9
+ name: string;
10
+ version: string | undefined;
11
+ }[] | undefined;
12
+ skippedReason?: "timeout" | "unscanned" | undefined;
6
13
  success: {
7
14
  id: import("@vltpkg/dep-id").DepID;
8
15
  name: string;
@@ -13,7 +20,6 @@ export declare const views: {
13
20
  name: string;
14
21
  version: string | undefined;
15
22
  }[];
16
- message: string;
17
23
  };
18
24
  };
19
25
  export declare const needsRegistry = true;
@@ -2,39 +2,49 @@ import { build } from '@vltpkg/graph';
2
2
  import { error } from '@vltpkg/error-cause';
3
3
  import { commandUsage } from "../config/usage.js";
4
4
  import { isErrorWithCause } from '@vltpkg/types';
5
+ const skippedMessage = (count, reason) => `${count} package${count === 1 ? '' : 's'} not built, ${reason === 'unscanned' ?
6
+ 'Socket has not scanned them yet'
7
+ : 'security data timed out'}. Run "vlt build" again later.`;
5
8
  export const views = {
6
9
  human: (result) => {
7
10
  const successCount = result.success.length;
8
11
  const failureCount = result.failure.length;
12
+ const skippedCount = result.skipped?.length ?? 0;
9
13
  const messages = [];
10
14
  if (successCount > 0) {
11
15
  messages.push(`🔨 Built ${successCount} package${successCount === 1 ? '' : 's'} successfully.`);
12
16
  }
13
- else {
17
+ else if (!skippedCount) {
14
18
  messages.push('📦 All packages are already built.');
15
19
  }
16
20
  if (failureCount > 0) {
17
21
  messages.push(`🔎 ${failureCount} optional package${ /* c8 ignore next */failureCount === 1 ? '' : 's'} failed to build.`);
18
22
  }
23
+ if (skippedCount > 0) {
24
+ messages.push(`⏳ ${skippedMessage(skippedCount, result.skippedReason)}`);
25
+ }
19
26
  return messages.join('\n');
20
27
  },
21
28
  json: (result) => {
22
- const successList = result.success.map(node => ({
29
+ const toJSON = (node) => ({
23
30
  id: node.id,
24
31
  name: node.name,
25
32
  version: node.version,
26
- }));
27
- const failureList = result.failure.map(node => ({
28
- id: node.id,
29
- name: node.name,
30
- version: node.version,
31
- }));
33
+ });
34
+ const successList = result.success.map(toJSON);
35
+ const failureList = result.failure.map(toJSON);
36
+ const skippedList = result.skipped?.map(toJSON) ?? [];
32
37
  return {
33
38
  success: successList,
34
39
  failure: failureList,
40
+ ...(skippedList.length ?
41
+ { skipped: skippedList, skippedReason: result.skippedReason }
42
+ : {}),
35
43
  message: successList.length > 0 ?
36
44
  `Built ${successList.length} package${successList.length === 1 ? '' : 's'}.`
37
- : 'No packages needed building.',
45
+ : skippedList.length ?
46
+ skippedMessage(skippedList.length, result.skippedReason)
47
+ : 'No packages needed building.',
38
48
  };
39
49
  },
40
50
  };
@@ -56,8 +56,8 @@ declare const usageDef: {
56
56
  readonly description: "Check global store entries against their cached\n tarballs (file list, contents, exec bits) and remove\n any that differ or have no cached tarball, e.g. after\n a file in `node_modules` was edited in place.";
57
57
  };
58
58
  readonly 'prune-store': {
59
- readonly usage: "";
60
- readonly description: "Remove global store entries that no `node_modules`\n folder links to. Entries ever copied from (install\n scripts, `store-linker=copy`, a cache on another\n drive) stay: copies do not show use.";
59
+ readonly usage: "[<date>]";
60
+ readonly description: "Remove global store entries that no `node_modules`\n folder links to. Copies do not show use: an entry\n copied or cloned from (install scripts,\n `store-linker=copy`, macOS, a cache on another\n drive) stays if that last happened after\n `<date>`, or in the last 30 days\n without one.";
61
61
  };
62
62
  };
63
63
  readonly examples: {
@@ -1,7 +1,7 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
2
  import { CacheEntry, assertOk } from '@vltpkg/registry-client';
3
3
  import { Spec } from '@vltpkg/spec';
4
- import { removeStoreEntry, storeEntryCopied, storeEntryLinked, storeEntryNames, storeEntryTime, verifyStoreEntry, } from '@vltpkg/tar/store-entry';
4
+ import { removeStoreEntry, storeEntryCopied, storeEntryLastCopied, storeEntryLinked, storeEntryNames, storeEntryTime, verifyStoreEntry, } from '@vltpkg/tar/store-entry';
5
5
  import { readStoreIndex } from '@vltpkg/tar/store-index';
6
6
  import { integrityHex } from '@vltpkg/types';
7
7
  import { readFileSync } from 'node:fs';
@@ -24,6 +24,8 @@ export class CacheView extends ViewClass {
24
24
  export const views = {
25
25
  human: CacheView,
26
26
  };
27
+ /** `prune-store`: days a copied entry stays, by default */
28
+ const pruneDays = 30;
27
29
  const usageDef = {
28
30
  command: 'cache',
29
31
  usage: '<command> [flags]',
@@ -76,11 +78,14 @@ const usageDef = {
76
78
  a file in \`node_modules\` was edited in place.`,
77
79
  },
78
80
  'prune-store': {
79
- usage: '',
81
+ usage: '[<date>]',
80
82
  description: `Remove global store entries that no \`node_modules\`
81
- folder links to. Entries ever copied from (install
82
- scripts, \`store-linker=copy\`, a cache on another
83
- drive) stay: copies do not show use.`,
83
+ folder links to. Copies do not show use: an entry
84
+ copied or cloned from (install scripts,
85
+ \`store-linker=copy\`, macOS, a cache on another
86
+ drive) stays if that last happened after
87
+ \`<date>\`, or in the last ${pruneDays} days
88
+ without one.`,
84
89
  },
85
90
  },
86
91
  examples: {
@@ -335,14 +340,24 @@ const verify = async (conf, specs, view) => {
335
340
  ...(!all && { missing }),
336
341
  };
337
342
  };
338
- const pruneStore = async (conf, _, view) => {
343
+ const pruneStore = async (conf, args, view) => {
344
+ const since = args.length ?
345
+ new Date(args.join(' ')).getTime()
346
+ : Date.now() - pruneDays * 24 * 60 * 60 * 1000;
347
+ if (Number.isNaN(since)) {
348
+ throw error('Invalid date', {
349
+ code: 'EUSAGE',
350
+ found: args.join(' '),
351
+ });
352
+ }
339
353
  const { storeRoot } = conf.options;
340
354
  const names = storeEntryNames(storeRoot);
341
355
  const removed = {};
342
356
  for (const hex of names) {
343
357
  const entry = resolve(storeRoot, hex);
344
358
  const index = readStoreIndex(entry);
345
- if (storeEntryCopied(entry, index) ||
359
+ if ((storeEntryCopied(entry, index) &&
360
+ storeEntryLastCopied(entry) >= since) ||
346
361
  storeEntryLinked(entry, index)) {
347
362
  continue;
348
363
  }
@@ -1,6 +1,6 @@
1
1
  import type { CommandFn, CommandUsage } from '../index.ts';
2
2
  import type { InstallResult } from './install.ts';
3
- export type CIResult = Pick<InstallResult, 'buildQueue' | 'graph'>;
3
+ export type CIResult = Pick<InstallResult, 'buildQueue' | 'graph' | 'skills'>;
4
4
  export declare const needsRegistry = true;
5
5
  export declare const needsNpmRegistry = true;
6
6
  export declare const usage: CommandUsage;
@@ -1,5 +1,6 @@
1
1
  import { install } from '@vltpkg/graph';
2
2
  import { commandUsage } from "../config/usage.js";
3
+ import { warnIgnoredWorkspaces } from "../migrate-workspaces.js";
3
4
  import { lazyView } from "../view.js";
4
5
  export const needsRegistry = true;
5
6
  export const needsNpmRegistry = true;
@@ -22,6 +23,10 @@ export const usage = () => commandUsage({
22
23
  value: '<query>',
23
24
  description: 'Filter which packages are allowed to run lifecycle scripts using DSS query syntax.',
24
25
  },
26
+ 'allow-skills': {
27
+ value: '<query>',
28
+ description: "Link agent skills of packages matching this DSS query into the project's agent skill dirs.",
29
+ },
25
30
  'lockfile-only': {
26
31
  description: 'Only update lockfile and package.json files; skip node_modules operations.',
27
32
  },
@@ -32,15 +37,18 @@ export const views = {
32
37
  human: lazyView(async () => (await import('./install/reporter.ts')).InstallReporter),
33
38
  };
34
39
  export const command = async (conf) => {
40
+ warnIgnoredWorkspaces(conf);
35
41
  const ciOptions = {
36
42
  ...conf.options,
37
43
  // same default as install: no scripts
38
44
  allowScripts: conf.get('allow-scripts') ?? ':not(*)',
45
+ allowSkills: conf.get('allow-skills'),
46
+ skillsDirs: conf.get('skills-dir'),
39
47
  expectLockfile: true,
40
48
  frozenLockfile: true,
41
49
  cleanInstall: true,
42
50
  lockfileOnly: conf.options['lockfile-only'],
43
51
  };
44
- const { buildQueue, graph } = await install(ciOptions);
45
- return { buildQueue, graph };
52
+ const { buildQueue, graph, skills } = await install(ciOptions);
53
+ return { buildQueue, graph, ...(skills ? { skills } : null) };
46
54
  };
@@ -4,7 +4,7 @@ import { minimatch } from 'minimatch';
4
4
  import { init } from '@vltpkg/init';
5
5
  import { install } from '@vltpkg/graph';
6
6
  import { load, save } from '@vltpkg/vlt-json';
7
- import { assertWSConfig, resolveWSConfig, splitNegatedPatterns, } from '@vltpkg/workspaces';
7
+ import { assertWSConfig, migratableWSConfig, resolveWSConfig, splitNegatedPatterns, } from '@vltpkg/workspaces';
8
8
  import { commandUsage } from "../config/usage.js";
9
9
  export const usage = () => commandUsage({
10
10
  command: 'init',
@@ -57,16 +57,15 @@ export const command = async (conf) => {
57
57
  if (conf.values.workspace?.length) {
58
58
  // the *effective* config, which may come from the root package.json
59
59
  // rather than vlt.json
60
- const { config: parsedWSConfig, source } = resolveWSConfig(conf.options.projectRoot, conf.options.packageJson);
60
+ const resolved = resolveWSConfig(conf.options.projectRoot, conf.options.packageJson);
61
+ const parsedWSConfig = resolved.config;
61
62
  // what gets written back to vlt.json. The raw vlt.json value is
62
63
  // reused as-is so its existing shape is preserved. When the
63
- // definitions live in package.json instead, seed vlt.json with them
64
- // first -- otherwise adding a `workspaces` field here would shadow
65
- // every workspace package.json already declares.
64
+ // definitions live in package.json or pnpm-workspace.yaml instead,
65
+ // seed vlt.json with them first -- otherwise adding a `workspaces`
66
+ // field here would shadow every workspace already declared there.
66
67
  const workspacesConfig = load('workspaces', assertWSConfig) ??
67
- (source === 'package.json' ?
68
- Object.values(parsedWSConfig).flat()
69
- : undefined);
68
+ migratableWSConfig(resolved);
70
69
  const results = [];
71
70
  const addToConfig = [];
72
71
  // create a new package.json file for every workspace
@@ -5,6 +5,7 @@ import { createElement as $, Fragment, useEffect, useState, } from 'react';
5
5
  import { ViewClass } from "../../view.js";
6
6
  import { asError } from '@vltpkg/types';
7
7
  import { persistedEntries } from "../../persist-spec-config.js";
8
+ import { keptLine, savedLine, unmatchedLines, unscannedLines, } from "../../allow-skills.js";
8
9
  const labels = {
9
10
  build: 'resolving dependencies',
10
11
  actual: '',
@@ -75,6 +76,17 @@ const App = ({ trailer }) => {
75
76
  export class InstallReporter extends ViewClass {
76
77
  #instance = null;
77
78
  start() {
79
+ // Ink measures the terminal on every render, and when stdout has no
80
+ // width (a pipe, a CI log) it does so by spawning `tput cols` and
81
+ // `tput lines`: thousands of processes over one install. Give a
82
+ // non-TTY stdout the width Ink would fall back to anyway, so the
83
+ // output is the same and nothing is spawned.
84
+ if (!process.stdout.isTTY) {
85
+ // only a sane COLUMNS counts; -1 or Infinity would be truthy
86
+ const cols = Number(process.env.COLUMNS);
87
+ process.stdout.columns ||=
88
+ Number.isInteger(cols) && cols > 0 ? cols : 80;
89
+ }
78
90
  this.#instance = render($(App));
79
91
  }
80
92
  async done(_result, { time }) {
@@ -87,12 +99,46 @@ export class InstallReporter extends ViewClass {
87
99
  out +=
88
100
  '🔨 Run `vlt build` to run all required scripts to build installed packages.\n';
89
101
  }
102
+ const { linked = [], removed = [], conflicts = [], skipped = [], gitignore = [], unmatched = [], } = _result.skills ?? {};
103
+ const s = (n) => (n === 1 ? '' : 's');
104
+ const skills = [];
105
+ if (linked.length) {
106
+ // one per skill, linked in any number of dirs
107
+ const n = new Set(linked.map(r => `${r.id}\0${r.skill}`)).size;
108
+ const dirs = [...new Set(linked.map(r => r.dir))].join(', ');
109
+ skills.push(`🔗 Linked ${n} agent skill${s(n)} into ${dirs}`);
110
+ }
111
+ if (removed.length) {
112
+ skills.push(`🧹 Removed ${removed.length} stale skill link${s(removed.length)}: ${removed.join(', ')}`);
113
+ }
114
+ if (conflicts.length) {
115
+ skills.push(`⚠️ ${conflicts.length} skill link${s(conflicts.length)} skipped, path in use or taken by another package: ${conflicts.join(', ')}`);
116
+ }
117
+ skills.push(...unmatchedLines(unmatched));
118
+ if (skipped.length) {
119
+ skills.push(`⚠️ Skipped ${skipped.join(', ')}`);
120
+ }
121
+ if (gitignore.length) {
122
+ skills.push(`⚠️ Could not update ${gitignore.join(', ')}`);
123
+ }
124
+ skills.push(...unscannedLines(_result.skills ?? {}));
125
+ if (skills.length)
126
+ out += `\n${skills.join('\n')}\n`;
90
127
  const saved = _result.persistedConfig;
91
128
  if (saved) {
92
129
  for (const e of persistedEntries(saved.values)) {
93
130
  out += `\nSaved ${e} to ${saved.which} vlt.json`;
94
131
  }
95
132
  }
133
+ const migrated = _result.migratedWorkspaces;
134
+ if (migrated) {
135
+ out += `\nSaved workspaces from ${migrated.source} to vlt.json`;
136
+ }
137
+ const a = _result.allowSkills;
138
+ if (a?.stored)
139
+ out += `\n${keptLine(a.stored)}`;
140
+ else if (a)
141
+ out += `\n${savedLine(a.query, 'project')}`;
96
142
  this.#instance?.rerender($(App, { trailer: out }));
97
143
  return undefined;
98
144
  }
@@ -1,6 +1,8 @@
1
+ import type { AllowSkillsSave } from '../allow-skills.ts';
2
+ import type { MigratedWorkspaces } from '../migrate-workspaces.ts';
1
3
  import type { SpecConfigPersistPlan } from '../persist-spec-config.ts';
2
4
  import type { DepID } from '@vltpkg/dep-id';
3
- import type { Diff, Graph } from '@vltpkg/graph';
5
+ import type { Diff, Graph, LinkSkillsResult } from '@vltpkg/graph';
4
6
  import type { CommandFn, CommandUsage } from '../index.ts';
5
7
  /**
6
8
  * The resulting object of an install operation. To be used by the view impl.
@@ -22,12 +24,38 @@ export type InstallResult = {
22
24
  * Spec config from the cli / env that was saved to a config file.
23
25
  */
24
26
  persistedConfig?: SpecConfigPersistPlan;
27
+ /**
28
+ * Workspaces saved to vlt.json from package.json or pnpm-workspace.yaml.
29
+ */
30
+ migratedWorkspaces?: MigratedWorkspaces;
31
+ /**
32
+ * Agent skill links added or removed.
33
+ */
34
+ skills?: LinkSkillsResult;
35
+ /**
36
+ * `--allow-skills` saved to vlt.json, or kept because another query
37
+ * is stored.
38
+ */
39
+ allowSkills?: AllowSkillsSave;
25
40
  };
26
41
  export declare const needsRegistry = true;
27
42
  export declare const needsNpmRegistry = true;
28
43
  export declare const usage: CommandUsage;
29
44
  export declare const views: {
30
45
  readonly json: (i: InstallResult) => {
46
+ allowSkills?: AllowSkillsSave | undefined;
47
+ skills?: {
48
+ unscanned?: string[];
49
+ unscannedReason?: "timeout" | "unscanned";
50
+ removed: string[];
51
+ conflicts: string[];
52
+ dirs: string[];
53
+ skipped?: string[];
54
+ gitignore?: string[];
55
+ unmatched?: import("@vltpkg/graph").UnmatchedPattern[];
56
+ linked: string[];
57
+ } | undefined;
58
+ migratedWorkspaces?: MigratedWorkspaces | undefined;
31
59
  persistedConfig?: SpecConfigPersistPlan | undefined;
32
60
  buildQueue?: DepID[] | undefined;
33
61
  message?: string | undefined;
@@ -1,8 +1,11 @@
1
1
  import { commandUsage } from "../config/usage.js";
2
2
  import { install } from '@vltpkg/graph';
3
+ import { saveAllowSkills } from "../allow-skills.js";
3
4
  import { parseAddArgs } from "../parse-add-remove-args.js";
4
5
  import { planSpecConfigPersist } from "../persist-spec-config.js";
5
6
  import { asUnknownSpecPrefix } from "../require-registry.js";
7
+ import { addTypesDeps } from "../save-types.js";
8
+ import { migrateWorkspaces } from "../migrate-workspaces.js";
6
9
  import { trackInstall } from "../telemetry.js";
7
10
  import { lazyView } from "../view.js";
8
11
  export const needsRegistry = true;
@@ -32,9 +35,15 @@ export const usage = () => commandUsage({
32
35
  'save-prod': {
33
36
  description: 'Save installed packages to package.json as dependencies.',
34
37
  },
38
+ 'save-types': {
39
+ description: 'Also save matching @types/* packages to devDependencies for added packages that ship no types.',
40
+ },
35
41
  'save-config': {
36
42
  description: 'Save registry and git host options given on the command line or via env (e.g. `--registries name=url`) to the project vlt.json, or the user vlt.json with --config=user.',
37
43
  },
44
+ 'no-migrate-workspaces': {
45
+ description: "Don't save workspaces read from package.json or pnpm-workspace.yaml to vlt.json.",
46
+ },
38
47
  workspace: {
39
48
  value: '<path|glob>',
40
49
  description: 'Limit installation targets to matching workspaces.',
@@ -56,8 +65,17 @@ export const usage = () => commandUsage({
56
65
  value: '<query>',
57
66
  description: 'Filter which packages are allowed to run lifecycle scripts using DSS query syntax.',
58
67
  },
68
+ 'allow-skills': {
69
+ value: '<query>',
70
+ description: "Link agent skills of packages matching this DSS query into the project's agent skill dirs. Saved to vlt.json.",
71
+ },
59
72
  },
60
73
  });
74
+ // link mounts; `unchanged` left out
75
+ const skillsJson = ({ linked, unchanged: _, ...rest }) => ({
76
+ linked: linked.map(s => s.mount),
77
+ ...rest,
78
+ });
61
79
  export const views = {
62
80
  json: i => {
63
81
  const added = i.diff?.nodes.add ?? new Set();
@@ -119,6 +137,11 @@ export const views = {
119
137
  ...(i.persistedConfig ?
120
138
  { persistedConfig: i.persistedConfig }
121
139
  : null),
140
+ ...(i.migratedWorkspaces ?
141
+ { migratedWorkspaces: i.migratedWorkspaces }
142
+ : null),
143
+ ...(i.skills ? { skills: skillsJson(i.skills) } : null),
144
+ ...(i.allowSkills ? { allowSkills: i.allowSkills } : null),
122
145
  };
123
146
  },
124
147
  human: lazyView(async () => (await import('./install/reporter.ts')).InstallReporter),
@@ -130,6 +153,8 @@ export const command = async (conf) => {
130
153
  const scurry = conf.options.scurry;
131
154
  const { add } = parseAddArgs(conf, scurry, monorepo);
132
155
  const persist = planSpecConfigPersist(conf);
156
+ if (conf.values['save-types'])
157
+ await addTypesDeps(conf, add);
133
158
  const frozenLockfile = conf.options['frozen-lockfile'];
134
159
  const expectLockfile = conf.options['expect-lockfile'];
135
160
  const lockfileOnly = conf.options['lockfile-only'];
@@ -140,20 +165,26 @@ export const command = async (conf) => {
140
165
  String(conf.get('allow-scripts'))
141
166
  : ':not(*)';
142
167
  /* c8 ignore stop */
168
+ const allowSkills = conf.get('allow-skills');
143
169
  const installStart = Date.now();
144
- const { buildQueue, graph, diff } = await install({
170
+ const { buildQueue, graph, diff, skills } = await install({
145
171
  ...conf.options,
146
172
  frozenLockfile,
147
173
  expectLockfile,
148
174
  allowScripts,
175
+ allowSkills,
176
+ skillsDirs: conf.get('skills-dir'),
149
177
  lockfileOnly,
150
178
  saveExact,
151
179
  savePrefix,
152
180
  }, add).catch((er) => {
153
181
  throw asUnknownSpecPrefix(er);
154
182
  });
183
+ const migrated = frozenLockfile || expectLockfile ? undefined : (migrateWorkspaces(conf));
155
184
  if (persist)
156
185
  await conf.addConfigToFile(persist.which, persist.values);
186
+ // nothing linked on --lockfile-only
187
+ const allowSkillsSave = lockfileOnly ? undefined : await saveAllowSkills(conf);
157
188
  /* c8 ignore next 9 - telemetry is best-effort */
158
189
  try {
159
190
  trackInstall({
@@ -167,5 +198,8 @@ export const command = async (conf) => {
167
198
  graph,
168
199
  diff,
169
200
  ...(persist ? { persistedConfig: persist } : null),
201
+ ...(migrated ? { migratedWorkspaces: migrated } : null),
202
+ ...(skills ? { skills } : null),
203
+ ...(allowSkillsSave ? { allowSkills: allowSkillsSave } : null),
170
204
  };
171
205
  };
@@ -1,4 +1,4 @@
1
- import { actual, asNode, humanReadableOutput, jsonOutput, mermaidOutput, GraphModifier, } from '@vltpkg/graph';
1
+ import { actual, asNode, hasSkills, humanReadableOutput, jsonOutput, mermaidOutput, GraphModifier, } from '@vltpkg/graph';
2
2
  import { Query } from '@vltpkg/query';
3
3
  import { SecurityArchive } from '@vltpkg/security-archive';
4
4
  import { error } from '@vltpkg/error-cause';
@@ -138,6 +138,7 @@ export const command = async (conf) => {
138
138
  securityArchive,
139
139
  hostContexts,
140
140
  getAuthHeader,
141
+ hasSkills,
141
142
  });
142
143
  const { nodes: resultNodes } = await scopeQuery.search(scopeQueryString, {
143
144
  signal: new AbortController().signal,
@@ -190,6 +191,7 @@ export const command = async (conf) => {
190
191
  securityArchive,
191
192
  hostContexts,
192
193
  getAuthHeader,
194
+ hasSkills,
193
195
  });
194
196
  const query = queryString ||
195
197
  /* c8 ignore next */
@@ -9,6 +9,7 @@ import { actual } from '@vltpkg/graph';
9
9
  import { Query } from '@vltpkg/query';
10
10
  import { error } from '@vltpkg/error-cause';
11
11
  import { createHostContextsMap } from "../query-host-contexts.js";
12
+ import { sortScopeLocations } from "../sort-scope-locations.js";
12
13
  import { minimatch } from 'minimatch';
13
14
  export const needsRegistry = true;
14
15
  export const usage = () => commandUsage({
@@ -97,7 +98,7 @@ const scopeLocations = async (queryString, conf) => {
97
98
  }));
98
99
  locations.push(resolve(projectRoot, location));
99
100
  }
100
- return locations;
101
+ return sortScopeLocations(locations, options.monorepo);
101
102
  };
102
103
  export const command = async (conf) => {
103
104
  const { options, projectRoot } = conf;
@@ -9,6 +9,7 @@ import { SecurityArchive } from '@vltpkg/security-archive';
9
9
  import { views as initViews } from "./init.js";
10
10
  import { commandUsage } from "../config/usage.js";
11
11
  import { createHostContextsMap } from "../query-host-contexts.js";
12
+ import { sortScopeLocations } from "../sort-scope-locations.js";
12
13
  import { minimatch } from 'minimatch';
13
14
  export const views = {
14
15
  human: (results, _, config) => {
@@ -129,15 +130,17 @@ export const command = async (conf) => {
129
130
  const { nodes: resultNodes } = await query.search(queryString, {
130
131
  signal: new AbortController().signal,
131
132
  });
133
+ const found = [];
132
134
  for (const node of resultNodes) {
133
135
  const location = node.location;
134
136
  assert(location, error(`node ${node.id} has no location`, {
135
137
  found: node,
136
138
  }));
137
139
  if (node.id !== VIRTUAL_ROOT_ID) {
138
- locations.push(resolve(node.projectRoot, location));
140
+ found.push(resolve(node.projectRoot, location));
139
141
  }
140
142
  }
143
+ locations.push(...sortScopeLocations(found, options.monorepo));
141
144
  }
142
145
  else if (paths?.length || groups?.length || recursive) {
143
146
  for (const workspace of options.monorepo ?? []) {
@@ -1,5 +1,4 @@
1
1
  import type { CommandFn, CommandUsage } from '../index.ts';
2
- export declare const needsRegistry = true;
3
2
  export declare const usage: CommandUsage;
4
3
  export type CommandResultSingle = {
5
4
  id: string;