@vltpkg/cli-sdk 1.3.6 → 1.3.8

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.
@@ -44,6 +44,11 @@ export const usage = () => commandUsage({
44
44
  If the appropriate excutable cannot be determined, then
45
45
  an error will be raised.
46
46
 
47
+ If the package is a path to a local directory (eg, \`.\`
48
+ or \`../pkg\`, relative to the current directory), its
49
+ bins are run in place. Nothing is installed, and its own
50
+ \`node_modules\` is used as-is.
51
+
47
52
  At no point will \`vlt exec\` change the locally installed
48
53
  dependencies. Any installs it performs is done in vlt's XDG
49
54
  data directory.
@@ -58,6 +63,9 @@ export const usage = () => commandUsage({
58
63
  'eslint@9.24 src/file.js': {
59
64
  description: 'Run the default bin provided by eslint version 9.24',
60
65
  },
66
+ '.': {
67
+ description: 'Run the default bin of the package in the current directory',
68
+ },
61
69
  'create-react-app --call="echo $PWD"': {
62
70
  description: 'Install create-react-app and run an arbitrary command with its bins in PATH',
63
71
  },
@@ -0,0 +1,91 @@
1
+ import type { PackageAlert } from '@vltpkg/security-archive';
2
+ import type { DependencyTypeShort } from '@vltpkg/types';
3
+ import type { CommandFn, CommandUsage } from '../index.ts';
4
+ export declare const needsRegistry = true;
5
+ export declare const usage: CommandUsage;
6
+ /** How far the newest available version is from the installed one. */
7
+ export type OutdatedKind = 'missing' | 'major' | 'minor' | 'patch' | 'prerelease';
8
+ /** What the security archive knows about one version of a package. */
9
+ export type VersionSecurity = {
10
+ /** The overall security score, from 0 to 100 */
11
+ score: number;
12
+ alerts: PackageAlert[];
13
+ };
14
+ /**
15
+ * A transitive dependent whose declared range does not admit the latest
16
+ * version. Importers are left out: their ranges are the user's to
17
+ * change, and the `action` field says how.
18
+ */
19
+ export type HeldBy = {
20
+ dependent: string;
21
+ spec: string;
22
+ };
23
+ /**
24
+ * One dependency that is missing, behind its wanted version, or behind
25
+ * the latest published version.
26
+ */
27
+ export type OutdatedEntry = {
28
+ /** The dependency name as it appears in the dependent's manifest */
29
+ name: string;
30
+ /** The declared spec, for example `^1.2.3` */
31
+ spec: string;
32
+ /** The dependency type in the dependent's manifest */
33
+ type: DependencyTypeShort;
34
+ /** The installed version, absent when the dependency is missing */
35
+ current?: string;
36
+ /**
37
+ * The highest published version that satisfies the declared spec,
38
+ * i.e. the version that `vlt install` would pick. Absent when no
39
+ * published version satisfies the spec.
40
+ */
41
+ wanted?: string;
42
+ /** The version the registry points its `latest` dist-tag at */
43
+ latest?: string;
44
+ /**
45
+ * The package depending on it: the name of an importer, or
46
+ * `name@version` of a transitive dependent.
47
+ */
48
+ dependent: string;
49
+ /** The dependent's location relative to the project root */
50
+ location: string;
51
+ /** How far latest (or wanted, when latest is not ahead) is from current */
52
+ kind: OutdatedKind;
53
+ /**
54
+ * True when a newer version satisfies the declared range, so that
55
+ * `vlt update` picks it up without a manifest change.
56
+ */
57
+ inRange: boolean;
58
+ /** The deprecation message of the installed version, when deprecated */
59
+ deprecated?: string;
60
+ /** Other dependents whose ranges do not admit the latest version */
61
+ heldBy?: HeldBy[];
62
+ /** Requirements of the latest version that this project does not meet */
63
+ requires?: {
64
+ /** The `engines.node` range of latest, when the current node fails it */
65
+ node?: string;
66
+ /**
67
+ * Peer dependency ranges of latest that the dependent's installed
68
+ * peers do not satisfy, by peer name.
69
+ */
70
+ peers?: Record<string, string>;
71
+ };
72
+ /** Security data for each version the archive has a report for */
73
+ security?: {
74
+ current?: VersionSecurity;
75
+ wanted?: VersionSecurity;
76
+ latest?: VersionSecurity;
77
+ };
78
+ /**
79
+ * How to upgrade: a `vlt` command for dependencies of an importer,
80
+ * or the catalog edit to make. Absent for transitive dependents,
81
+ * where the upgrade belongs to the dependent's own release.
82
+ */
83
+ action?: string;
84
+ };
85
+ export type OutdatedResult = OutdatedEntry[];
86
+ export declare const views: {
87
+ readonly json: (result: OutdatedResult) => OutdatedResult;
88
+ readonly count: (result: OutdatedResult) => number;
89
+ readonly human: (result: OutdatedResult) => string;
90
+ };
91
+ export declare const command: CommandFn<OutdatedResult>;
@@ -0,0 +1,554 @@
1
+ import { actual, Edge, GraphModifier } from '@vltpkg/graph';
2
+ import { PackageInfoClient } from '@vltpkg/package-info';
3
+ import { pickManifest } from '@vltpkg/pick-manifest';
4
+ import { Query } from '@vltpkg/query';
5
+ import { SecurityArchive } from '@vltpkg/security-archive';
6
+ import { gt, parse, rsort, satisfies, stable } from '@vltpkg/semver';
7
+ import { commandUsage } from "../config/usage.js";
8
+ import { assertVltInstalled } from "../is-vlt-installed.js";
9
+ import { createGetAuthHeader } from "../query-auth.js";
10
+ import { createHostContextsMap } from "../query-host-contexts.js";
11
+ import { createStubNode } from "../stub-node.js";
12
+ export const needsRegistry = true;
13
+ export const usage = () => commandUsage({
14
+ command: 'outdated',
15
+ usage: [
16
+ '',
17
+ '[package-names...]',
18
+ '[--target=<query>] [--workspace=<path>]',
19
+ '[--view=human | json | count]',
20
+ ],
21
+ description: `List dependencies that have newer versions available, and
22
+ what is keeping them from being upgraded.
23
+
24
+ For every registry dependency, compare the installed version
25
+ against the highest version that satisfies the declared range
26
+ (wanted) and against the registry's \`latest\` dist-tag (latest).
27
+ Only dependencies that are missing or behind on either count are
28
+ reported.
29
+
30
+ Each report goes beyond the version numbers:
31
+
32
+ - the size of the jump (major, minor, patch)
33
+ - whether the installed version is deprecated
34
+ - security scores and alerts for the installed, wanted and latest
35
+ versions, so an upgrade that fixes a vulnerability or one that
36
+ introduces a flagged version stands out
37
+ - other dependents whose ranges hold the package back
38
+ - engine and peer dependency requirements of the latest version
39
+ that this project does not meet
40
+ - the command that performs the upgrade
41
+
42
+ By default the direct dependencies of the project root and its
43
+ workspaces are checked; --workspace and --workspace-group narrow
44
+ that to the given workspaces. The --target option accepts a DSS
45
+ query selector instead, which can reach any dependency in the
46
+ graph. Package names given as positional arguments filter either
47
+ selection.
48
+
49
+ The installed versions come from the graph built by
50
+ \`vlt install\`, so the project must be installed by vlt first.
51
+ Dependencies that do not resolve against a registry (git, file,
52
+ remote tarball or workspace specs) are not checked. Catalog specs
53
+ are checked against the range the catalog resolves them to.`,
54
+ examples: {
55
+ '': {
56
+ description: 'Report outdated direct dependencies of the project and all its workspaces',
57
+ },
58
+ 'react react-dom': {
59
+ description: `Only check the dependencies named 'react' and 'react-dom'`,
60
+ },
61
+ '--workspace=packages/app': {
62
+ description: 'Only check the dependencies of a single workspace',
63
+ },
64
+ '--target="*"': {
65
+ description: 'Check every dependency in the graph, transitive ones included',
66
+ },
67
+ '--target=":root > *:dev"': {
68
+ description: 'Only check the dev dependencies of the project root',
69
+ },
70
+ '--target="*:vuln"': {
71
+ description: 'Only check dependencies with known vulnerabilities',
72
+ },
73
+ '--view=json': {
74
+ description: 'Print the report as JSON',
75
+ },
76
+ },
77
+ options: {
78
+ target: {
79
+ value: '<query>',
80
+ description: 'DSS query selector choosing the dependencies to check, in place of the direct dependencies of the selected importers.',
81
+ },
82
+ workspace: {
83
+ value: '<path>',
84
+ description: 'Limit the report to the dependencies of the given workspace(s).',
85
+ },
86
+ 'workspace-group': {
87
+ value: '<name>',
88
+ description: 'Limit the report to the dependencies of the workspaces in the given group(s).',
89
+ },
90
+ view: {
91
+ value: '[human | json | count]',
92
+ description: 'Output format. Defaults to human-readable or json if no tty. Count outputs the number of outdated dependencies.',
93
+ },
94
+ },
95
+ });
96
+ const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
97
+ const missingVersion = (version) => version ?? 'missing';
98
+ /**
99
+ * Lay out columns of cells as a text table, each column padded to its
100
+ * widest cell and separated from the next by two spaces.
101
+ */
102
+ const formatTable = (columns) => {
103
+ const lines = [];
104
+ for (const { title, cells } of columns) {
105
+ const width = Math.max(title.length, ...cells.map(c => c.length));
106
+ const column = [title, ...cells].map(cell => cell.padEnd(width));
107
+ column.forEach((cell, i) => {
108
+ const line = lines[i];
109
+ lines[i] = line === undefined ? cell : `${line} ${cell}`;
110
+ });
111
+ }
112
+ // the last column carries no trailing padding
113
+ return lines.map(line => line.trimEnd()).join('\n');
114
+ };
115
+ const alertsNotIn = (alerts, other) => alerts.filter(a => !other.some(o => o.key === a.key));
116
+ /**
117
+ * The alerts worth a mention in a one-line summary. Low and medium
118
+ * severity alerts (network access, env vars, minified files) come by
119
+ * the dozen on any sizeable package and would drown out the rest.
120
+ */
121
+ const serious = (alerts) => alerts.filter(a => a.severity === 'high' || a.severity === 'critical');
122
+ /**
123
+ * A short explanation of what the upgrade means, for the human table.
124
+ */
125
+ const explain = (entry) => {
126
+ const parts = [];
127
+ if (entry.kind !== 'missing')
128
+ parts.push(entry.kind);
129
+ if (entry.deprecated !== undefined)
130
+ parts.push('deprecated');
131
+ const { current } = entry.security ?? {};
132
+ if (current) {
133
+ const targets = [
134
+ ['wanted', entry.inRange ? entry.security?.wanted : undefined],
135
+ ['latest', entry.security?.latest],
136
+ ];
137
+ for (const [label, target] of targets) {
138
+ if (!target)
139
+ continue;
140
+ const fixed = alertsNotIn(serious(current.alerts), target.alerts).length;
141
+ const added = alertsNotIn(serious(target.alerts), current.alerts).length;
142
+ if (fixed) {
143
+ parts.push(`${label} fixes ${plural(fixed, 'serious alert')}`);
144
+ }
145
+ if (added) {
146
+ parts.push(`${label} adds ${plural(added, 'serious alert')}`);
147
+ }
148
+ if (current.score - target.score >= 10) {
149
+ parts.push(`${label} score ${target.score} (from ${current.score})`);
150
+ }
151
+ }
152
+ }
153
+ if (entry.heldBy?.length) {
154
+ parts.push(`held by ${entry.heldBy.map(h => h.dependent).join(', ')}`);
155
+ }
156
+ if (entry.requires?.node) {
157
+ parts.push(`latest needs node ${entry.requires.node}`);
158
+ }
159
+ if (entry.requires?.peers) {
160
+ const peers = Object.entries(entry.requires.peers)
161
+ .map(([name, range]) => `${name}@${range}`)
162
+ .join(', ');
163
+ parts.push(`latest needs ${peers}`);
164
+ }
165
+ return parts.join(', ');
166
+ };
167
+ /**
168
+ * The commands that perform the upgrades, one `vlt install` per set of
169
+ * flags so that the specs can be combined. Several entries can call
170
+ * for the same step, such as every workspace that shares a catalog
171
+ * entry, so each step is listed once.
172
+ */
173
+ const suggestActions = (result) => {
174
+ const lines = [];
175
+ const updates = result.filter(e => e.inRange).length;
176
+ if (updates) {
177
+ lines.push(`Run \`vlt update\` to pick up ${plural(updates, 'in-range update')}.`);
178
+ }
179
+ const installs = new Map();
180
+ const edits = new Set();
181
+ for (const { action } of result) {
182
+ if (!action)
183
+ continue;
184
+ if (action.startsWith('vlt install ')) {
185
+ const [spec = '', ...flags] = action
186
+ .slice('vlt install '.length)
187
+ .split(' ');
188
+ const key = flags.join(' ');
189
+ const specs = installs.get(key) ?? new Set();
190
+ specs.add(spec);
191
+ installs.set(key, specs);
192
+ }
193
+ else if (!action.startsWith('vlt ')) {
194
+ edits.add(`${action.charAt(0).toUpperCase()}${action.slice(1)}.`);
195
+ }
196
+ }
197
+ for (const [flags, specs] of installs) {
198
+ const args = [...specs, ...(flags ? [flags] : [])].join(' ');
199
+ lines.push(`Run \`vlt install ${args}\` to move to latest.`);
200
+ }
201
+ lines.push(...edits);
202
+ return lines;
203
+ };
204
+ export const views = {
205
+ json: result => result,
206
+ count: result => result.length,
207
+ human: result => {
208
+ if (result.length === 0) {
209
+ return 'All dependencies are up to date.';
210
+ }
211
+ const columns = [
212
+ { title: 'Package', cells: result.map(e => e.name) },
213
+ {
214
+ title: 'Current',
215
+ cells: result.map(e => missingVersion(e.current)),
216
+ },
217
+ {
218
+ title: 'Wanted',
219
+ cells: result.map(e => missingVersion(e.wanted)),
220
+ },
221
+ {
222
+ title: 'Latest',
223
+ cells: result.map(e => missingVersion(e.latest)),
224
+ },
225
+ { title: 'Type', cells: result.map(e => e.type) },
226
+ ];
227
+ // the dependent column only earns its space when the entries do
228
+ // not all belong to the same importer
229
+ if (new Set(result.map(e => e.dependent)).size > 1) {
230
+ columns.push({
231
+ title: 'Dependent',
232
+ cells: result.map(e => e.dependent),
233
+ });
234
+ }
235
+ columns.push({ title: 'Why', cells: result.map(explain) });
236
+ const actions = suggestActions(result);
237
+ return [
238
+ formatTable(columns),
239
+ ...(actions.length ? ['', ...actions] : []),
240
+ ].join('\n');
241
+ },
242
+ };
243
+ /**
244
+ * The version a registry calls `latest`. Registries that have no
245
+ * `latest` tag fall back to the highest stable published version.
246
+ */
247
+ const latestVersion = (packument) => {
248
+ const tagged = packument['dist-tags'].latest;
249
+ if (tagged)
250
+ return tagged;
251
+ const [highest] = rsort(stable(Object.keys(packument.versions)));
252
+ return highest;
253
+ };
254
+ const gapKind = (current, target) => {
255
+ const c = parse(current);
256
+ const t = parse(target);
257
+ /* c8 ignore next - versions that reached the graph always parse */
258
+ if (!c || !t)
259
+ return 'patch';
260
+ if (t.major > c.major)
261
+ return 'major';
262
+ if (t.minor > c.minor)
263
+ return 'minor';
264
+ if (t.patch > c.patch)
265
+ return 'patch';
266
+ return 'prerelease';
267
+ };
268
+ /** An importer by name, anything else by name and version. */
269
+ const dependentName = (node) => node.importer ? node.name : `${node.name}@${node.version}`;
270
+ /**
271
+ * The other dependents of the installed node whose ranges do not admit
272
+ * the latest version, i.e. what keeps a transitive dependency back
273
+ * even after this dependent's range is widened.
274
+ */
275
+ const heldBy = (edge, latest) => {
276
+ const holders = [];
277
+ for (const other of edge.to?.edgesIn ?? []) {
278
+ if (other === edge || other.from.importer)
279
+ continue;
280
+ const { range } = other.spec.final;
281
+ if (range && !satisfies(latest, range)) {
282
+ holders.push({
283
+ dependent: dependentName(other.from),
284
+ spec: other.spec.bareSpec,
285
+ });
286
+ }
287
+ }
288
+ return holders;
289
+ };
290
+ /**
291
+ * Requirements the latest version makes that the dependent does not
292
+ * meet: a node engine range this process fails, and peer dependency
293
+ * ranges that the dependent's installed peers fall outside of.
294
+ */
295
+ const unmetRequirements = (edge, latest, nodeVersion) => {
296
+ const requires = {};
297
+ const engine = latest.engines?.node;
298
+ if (engine && !satisfies(nodeVersion, engine))
299
+ requires.node = engine;
300
+ const peers = {};
301
+ for (const [peer, range] of Object.entries(latest.peerDependencies ?? {})) {
302
+ const installed = edge.from.edgesOut.get(peer)?.to?.version;
303
+ if (installed && !satisfies(installed, range))
304
+ peers[peer] = range;
305
+ }
306
+ if (Object.keys(peers).length)
307
+ requires.peers = peers;
308
+ return requires.node || requires.peers ? requires : undefined;
309
+ };
310
+ /**
311
+ * The spec to hand `vlt install` so that an alias or a named registry
312
+ * prefix survives the range change, e.g. `foo@npm:bar@^2.0.0`.
313
+ */
314
+ const installSpec = (edge, range) => {
315
+ const { bareSpec, final } = edge.spec;
316
+ const prefix = bareSpec.slice(0, bareSpec.length - final.bareSpec.length);
317
+ return `${edge.name}@${prefix}${range}`;
318
+ };
319
+ const saveFlags = {
320
+ prod: '',
321
+ dev: ' --save-dev',
322
+ optional: ' --save-optional',
323
+ peer: ' --save-peer',
324
+ peerOptional: ' --save-peer',
325
+ };
326
+ /**
327
+ * How an importer upgrades this dependency. A version within the
328
+ * declared range is one `vlt update` away. Moving past the range means
329
+ * changing the spec, with `vlt install` or in the catalog.
330
+ */
331
+ const actionFor = (edge, entry, { savePrefix }) => {
332
+ if (!edge.from.importer)
333
+ return undefined;
334
+ if (!entry.current)
335
+ return 'vlt install';
336
+ const { latest } = entry;
337
+ const { range } = edge.spec.final;
338
+ if (latest && gt(latest, entry.current) && range) {
339
+ if (satisfies(latest, range))
340
+ return 'vlt update';
341
+ const newRange = `${savePrefix}${latest}`;
342
+ if (edge.spec.type === 'catalog') {
343
+ const catalog = edge.spec.catalog ?
344
+ `"${edge.spec.catalog}" catalog`
345
+ : 'catalog';
346
+ return `set the ${catalog} entry for ${edge.name} in vlt.json to ${newRange}`;
347
+ }
348
+ const workspace = edge.from.mainImporter ?
349
+ ''
350
+ : ` --workspace=${edge.from.location.replace(/^\.\//, '')}`;
351
+ return `vlt install ${installSpec(edge, newRange)}${saveFlags[edge.type]}${workspace}`;
352
+ }
353
+ return 'vlt update';
354
+ };
355
+ /**
356
+ * Describe the dependency `edge` declares, or nothing when its
357
+ * installed version is as new as the registry offers.
358
+ */
359
+ const buildEntry = (edge, packument, options) => {
360
+ const current = edge.to?.version;
361
+ const wanted = pickManifest(packument, edge.spec, options.pick)?.version;
362
+ const latest = latestVersion(packument);
363
+ const wantedAhead = !!wanted && (!current || gt(wanted, current));
364
+ const latestAhead = !!latest && (!current || gt(latest, current));
365
+ if (current && !wantedAhead && !latestAhead)
366
+ return undefined;
367
+ const entry = {
368
+ name: edge.name,
369
+ spec: edge.spec.bareSpec,
370
+ type: edge.type,
371
+ current,
372
+ wanted,
373
+ latest,
374
+ dependent: dependentName(edge.from),
375
+ location: edge.from.location,
376
+ kind: !current ? 'missing'
377
+ : latestAhead ? gapKind(current, latest)
378
+ : gapKind(current, String(wanted)),
379
+ inRange: wantedAhead,
380
+ };
381
+ const deprecated = current ? packument.versions[current]?.deprecated : undefined;
382
+ if (deprecated)
383
+ entry.deprecated = deprecated;
384
+ if (latestAhead) {
385
+ const holders = heldBy(edge, latest);
386
+ if (holders.length)
387
+ entry.heldBy = holders;
388
+ const manifest = packument.versions[latest];
389
+ const requires = manifest &&
390
+ unmetRequirements(edge, manifest, options.nodeVersion);
391
+ if (requires)
392
+ entry.requires = requires;
393
+ }
394
+ const action = actionFor(edge, entry, options);
395
+ if (action)
396
+ entry.action = action;
397
+ return entry;
398
+ };
399
+ /**
400
+ * Pick the importers whose dependencies are reported. All importers by
401
+ * default, or only the selected workspaces when `--workspace` or
402
+ * `--workspace-group` is set in a monorepo.
403
+ */
404
+ const selectImporters = (conf, graph) => {
405
+ const { monorepo } = conf.options;
406
+ const selected = 'workspace' in conf.values || 'workspace-group' in conf.values;
407
+ if (monorepo && selected) {
408
+ const importers = [];
409
+ for (const workspace of monorepo.filter(conf.values)) {
410
+ const node = graph.nodes.get(workspace.id);
411
+ /* c8 ignore next - every loaded workspace has a node */
412
+ if (node)
413
+ importers.push(node);
414
+ }
415
+ return importers;
416
+ }
417
+ return [...graph.importers];
418
+ };
419
+ /**
420
+ * The edges to check: those a `--target` query selects, or else the
421
+ * direct dependencies of the selected importers.
422
+ */
423
+ const selectEdges = async (conf, graph, archive) => {
424
+ const target = conf.get('target');
425
+ if (!target) {
426
+ return selectImporters(conf, graph).flatMap(importer => [
427
+ ...importer.edgesOut.values(),
428
+ ]);
429
+ }
430
+ const query = new Query({
431
+ nodes: new Set(graph.nodes.values()),
432
+ edges: graph.edges,
433
+ importers: graph.importers,
434
+ securityArchive: archive,
435
+ hostContexts: await createHostContextsMap(conf),
436
+ getAuthHeader: createGetAuthHeader(conf),
437
+ });
438
+ const { edges } = await query.search(target, {
439
+ signal: new AbortController().signal,
440
+ });
441
+ return edges.filter(edge => edge instanceof Edge);
442
+ };
443
+ /**
444
+ * The security archive is a bonus, never a requirement: when it cannot
445
+ * be reached the report simply carries no security data.
446
+ */
447
+ const startArchive = async (nodes) => {
448
+ try {
449
+ return await SecurityArchive.start({ nodes });
450
+ }
451
+ catch {
452
+ return undefined;
453
+ }
454
+ };
455
+ const securityOf = (archive, id) => {
456
+ const report = archive.get(id);
457
+ if (!report)
458
+ return undefined;
459
+ return {
460
+ score: Math.round(report.score.overall * 100),
461
+ alerts: report.alerts,
462
+ };
463
+ };
464
+ export const command = async (conf) => {
465
+ const { projectRoot, packageJson, monorepo } = conf.options;
466
+ assertVltInstalled(projectRoot, 'outdated');
467
+ const mainManifest = packageJson.read(projectRoot);
468
+ const graph = actual.load({
469
+ ...conf.options,
470
+ mainManifest,
471
+ modifiers: GraphModifier.maybeLoad(conf.options),
472
+ monorepo,
473
+ loadManifests: true,
474
+ });
475
+ // a query may use security selectors, which need the archive to
476
+ // know every node in the graph before the search runs
477
+ let archive = conf.get('target') ?
478
+ await startArchive([...graph.nodes.values()])
479
+ : undefined;
480
+ const names = new Set(conf.positionals);
481
+ const edges = (await selectEdges(conf, graph, archive)).filter(edge => (names.size === 0 || names.has(edge.name)) &&
482
+ edge.spec.final.type === 'registry');
483
+ const pic = new PackageInfoClient(conf.options);
484
+ // one packument request per registry package, however many
485
+ // dependents it has
486
+ const packuments = new Map();
487
+ const fetchPackument = (edge) => {
488
+ // the final spec of a registry dependency always carries its
489
+ // registry, so the key is unique per package per registry
490
+ const { registry, name } = edge.spec.final;
491
+ const key = `${String(registry)}:${name}`;
492
+ let p = packuments.get(key);
493
+ if (!p) {
494
+ p = pic.packument(edge.spec);
495
+ packuments.set(key, p);
496
+ }
497
+ return p;
498
+ };
499
+ const options = {
500
+ nodeVersion: conf.options['node-version'],
501
+ savePrefix: conf.options['save-exact'] ? '' : conf.options['save-prefix'],
502
+ pick: conf.options,
503
+ };
504
+ const found = [];
505
+ for (const edge of edges) {
506
+ const entry = buildEntry(edge, await fetchPackument(edge), options);
507
+ if (entry)
508
+ found.push({ edge, entry });
509
+ }
510
+ // look up the installed, wanted and latest versions in the security
511
+ // archive, the two candidates as stand-in nodes
512
+ const stub = (edge, version) => createStubNode(edge.spec, edge.spec.final.name, version, conf.options);
513
+ const securityNodes = new Map();
514
+ const candidates = [];
515
+ for (const { edge, entry } of found) {
516
+ const ids = {};
517
+ if (edge.to) {
518
+ ids.current = edge.to.id;
519
+ securityNodes.set(edge.to.id, edge.to);
520
+ }
521
+ for (const which of ['wanted', 'latest']) {
522
+ const version = entry[which];
523
+ if (!version)
524
+ continue;
525
+ const node = stub(edge, version);
526
+ ids[which] = node.id;
527
+ securityNodes.set(node.id, node);
528
+ }
529
+ candidates.push({ entry, ids });
530
+ }
531
+ const nodes = [...securityNodes.values()];
532
+ if (archive) {
533
+ await archive.refresh({ nodes }).catch(() => {
534
+ archive = undefined;
535
+ });
536
+ }
537
+ else if (nodes.length) {
538
+ archive = await startArchive(nodes);
539
+ }
540
+ if (archive) {
541
+ for (const { entry, ids } of candidates) {
542
+ const security = {};
543
+ for (const which of ['current', 'wanted', 'latest']) {
544
+ const id = ids[which];
545
+ const report = id && securityOf(archive, id);
546
+ if (report)
547
+ security[which] = report;
548
+ }
549
+ if (Object.keys(security).length)
550
+ entry.security = security;
551
+ }
552
+ }
553
+ return found.map(({ entry }) => entry);
554
+ };
@@ -10,8 +10,17 @@ export const usage = () => commandUsage({
10
10
  command: 'update',
11
11
  usage: '',
12
12
  description: `Update dependencies to their latest in-range versions.
13
- Discards the lockfile and resolves dependencies from scratch.`,
13
+ Discards the lockfile and resolves dependencies from scratch
14
+ (with -w: only matching workspaces' dependencies).`,
14
15
  options: {
16
+ workspace: {
17
+ value: '<path|glob>',
18
+ description: 'Limit update to matching workspaces. Others, and deps they share, keep their locked versions.',
19
+ },
20
+ 'workspace-group': {
21
+ value: '<name>',
22
+ description: 'Limit update to workspace groups. Others, and deps they share, keep their locked versions.',
23
+ },
15
24
  'save-config': {
16
25
  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.',
17
26
  },
@@ -1,10 +1,10 @@
1
1
  import * as dotProp from '@vltpkg/dot-prop';
2
2
  import { error } from '@vltpkg/error-cause';
3
- import { getId } from '@vltpkg/dep-id';
4
3
  import { PackageInfoClient } from '@vltpkg/package-info';
5
4
  import { SecurityArchive } from '@vltpkg/security-archive';
6
5
  import { Spec } from '@vltpkg/spec';
7
6
  import { commandUsage } from "../config/usage.js";
7
+ import { createStubNode } from "../stub-node.js";
8
8
  export const needsRegistry = true;
9
9
  export const usage = () => commandUsage({
10
10
  command: 'view',
@@ -22,7 +22,11 @@ export const usage = () => commandUsage({
22
22
  (e.g., \`dist-tags.latest\`, \`dependencies.lodash\`).
23
23
 
24
24
  Security data from the vlt security archive is shown when
25
- available, including scores and alerts.`,
25
+ available, including scores and alerts.
26
+
27
+ Use \`.\` (or no args) for the package in the current directory
28
+ (nearest package.json), \`.@<version>\` to pick a version.
29
+ Private packages show local package.json info.`,
26
30
  examples: {
27
31
  express: {
28
32
  description: 'View info about the latest version of express',
@@ -42,6 +46,12 @@ export const usage = () => commandUsage({
42
46
  'express dist-tags.latest': {
43
47
  description: 'Show the latest dist-tag value',
44
48
  },
49
+ '.': {
50
+ description: 'View the package in the current directory',
51
+ },
52
+ '. version': {
53
+ description: 'Show the latest published version of the current package',
54
+ },
45
55
  },
46
56
  options: {
47
57
  view: {
@@ -114,7 +124,7 @@ const formatHuman = (result) => {
114
124
  // Name and version
115
125
  const name = manifest.name ?? packument.name;
116
126
  const version = manifest.version ?? '';
117
- lines.push(`${name}@${version}`);
127
+ lines.push(version ? `${name}@${version}` : name);
118
128
  // Description
119
129
  if (manifest.description) {
120
130
  lines.push(manifest.description);
@@ -227,32 +237,6 @@ export const views = {
227
237
  };
228
238
  },
229
239
  };
230
- /**
231
- * Create a minimal NodeLike for SecurityArchive lookup.
232
- * Only the fields used by SecurityArchive.start() are needed.
233
- */
234
- const createFakeNode = (spec, name, version, options) => ({
235
- id: getId(spec, { name, version }),
236
- name,
237
- version,
238
- confused: false,
239
- edgesIn: new Set(),
240
- edgesOut: new Map(),
241
- workspaces: undefined,
242
- importer: false,
243
- mainImporter: false,
244
- projectRoot: '',
245
- dev: false,
246
- optional: false,
247
- graph: {},
248
- options,
249
- /* c8 ignore next 5 - stub methods for NodeLike interface */
250
- toJSON: () => ({}),
251
- toString: () => `${name}@${version}`,
252
- setResolved: () => { },
253
- setConfusedManifest: () => { },
254
- maybeSetConfusedManifest: () => { },
255
- });
256
240
  /**
257
241
  * Lookup fields from a combined packument+manifest view.
258
242
  *
@@ -289,15 +273,30 @@ const lookupField = (result, path) => {
289
273
  // Default: look up in manifest
290
274
  return dotProp.get(result.manifest, path);
291
275
  };
292
- export const command = async (conf) => {
293
- const specArg = conf.positionals[0];
294
- if (!specArg) {
295
- throw error('view requires a package spec argument', {
276
+ const readLocalManifest = (conf) => {
277
+ const file = conf.options.packageJson.find(process.cwd());
278
+ if (!file) {
279
+ throw error('No local package.json found', {
296
280
  code: 'EUSAGE',
281
+ path: process.cwd(),
297
282
  });
298
283
  }
299
- const fieldPath = conf.positionals[1];
300
- const spec = Spec.parseArgs(specArg, conf.options);
284
+ const manifest = conf.options.packageJson.read(file);
285
+ const { name } = manifest;
286
+ if (!name) {
287
+ throw error('No package name found in package.json', {
288
+ code: 'EUSAGE',
289
+ found: file,
290
+ });
291
+ }
292
+ return { name, manifest };
293
+ };
294
+ // nothing published: no dist-tags or versions
295
+ const localView = (name, manifest) => ({
296
+ manifest,
297
+ packument: { name, 'dist-tags': {}, versions: {} },
298
+ });
299
+ const fetchView = async (spec, conf) => {
301
300
  const pic = new PackageInfoClient(conf.options);
302
301
  // Fetch the full packument (needs time, maintainers) and resolved manifest
303
302
  const [packument, resolvedManifest] = await Promise.all([
@@ -311,7 +310,7 @@ export const command = async (conf) => {
311
310
  const version = manifest.version;
312
311
  if (name && version) {
313
312
  try {
314
- const node = createFakeNode(spec, name, version, conf.options);
313
+ const node = createStubNode(spec, name, version, conf.options);
315
314
  const archive = await SecurityArchive.start({
316
315
  nodes: [node],
317
316
  });
@@ -321,11 +320,34 @@ export const command = async (conf) => {
321
320
  // Security data is optional, don't fail the command
322
321
  }
323
322
  }
324
- const result = {
325
- packument,
326
- manifest,
327
- security,
328
- };
323
+ return { packument, manifest, security };
324
+ };
325
+ export const command = async (conf) => {
326
+ // `.` (or no args) is the package in the current directory
327
+ const specArg = conf.positionals[0] || '.';
328
+ const fieldPath = conf.positionals[1];
329
+ let result;
330
+ if (specArg === '.' || specArg.startsWith('.@')) {
331
+ const { name, manifest } = readLocalManifest(conf);
332
+ const rest = specArg.slice(1);
333
+ if (manifest.private) {
334
+ // never query the registry for private pkgs
335
+ if (rest) {
336
+ throw error('Cannot select a version of a private package', {
337
+ code: 'EUSAGE',
338
+ found: specArg,
339
+ });
340
+ }
341
+ result = localView(name, manifest);
342
+ }
343
+ else {
344
+ // keep name literal; rest is '' or '@<spec>'
345
+ result = await fetchView(Spec.parse(name, rest.slice(1), conf.options), conf);
346
+ }
347
+ }
348
+ else {
349
+ result = await fetchView(Spec.parseArgs(specArg, conf.options), conf);
350
+ }
329
351
  // If a field path is provided, resolve it
330
352
  if (fieldPath !== undefined) {
331
353
  result.fieldPath = fieldPath;
@@ -41,6 +41,7 @@ export declare const commands: {
41
41
  readonly login: "login";
42
42
  readonly logout: "logout";
43
43
  readonly list: "list";
44
+ readonly outdated: "outdated";
44
45
  readonly pack: "pack";
45
46
  readonly ping: "ping";
46
47
  readonly pkg: "pkg";
@@ -34,6 +34,7 @@ const canonicalCommands = {
34
34
  logout: 'logout',
35
35
  list: 'list',
36
36
  ls: 'ls',
37
+ outdated: 'outdated',
37
38
  pack: 'pack',
38
39
  ping: 'ping',
39
40
  pkg: 'pkg',
@@ -129,6 +129,10 @@ const commandHelp = {
129
129
  args: '',
130
130
  desc: 'Log out from a registry',
131
131
  },
132
+ outdated: {
133
+ args: '[package-names...]',
134
+ desc: 'List dependencies with newer versions available',
135
+ },
132
136
  pack: {
133
137
  args: '',
134
138
  desc: 'Create a tarball from a package',
@@ -2,7 +2,8 @@ import type { GetAuthHeader } from '@vltpkg/query';
2
2
  import type { LoadedConfig } from './config/index.ts';
3
3
  /**
4
4
  * Returns a {@link GetAuthHeader} implementation that looks up
5
- * authorization tokens from the local keychain / env, used by
5
+ * authorization tokens from the local keychain / env (incl.
6
+ * `VLT_TOKEN_<key>` for configured registries), used by
6
7
  * network-reliant query selectors, e.g: `:outdated()`.
7
8
  */
8
9
  export declare const createGetAuthHeader: (conf: LoadedConfig) => GetAuthHeader;
@@ -1,7 +1,11 @@
1
- import { getTokenByURL } from '@vltpkg/registry-client';
1
+ import { getTokenByURL, registryKeys } from '@vltpkg/registry-client';
2
2
  /**
3
3
  * Returns a {@link GetAuthHeader} implementation that looks up
4
- * authorization tokens from the local keychain / env, used by
4
+ * authorization tokens from the local keychain / env (incl.
5
+ * `VLT_TOKEN_<key>` for configured registries), used by
5
6
  * network-reliant query selectors, e.g: `:outdated()`.
6
7
  */
7
- export const createGetAuthHeader = (conf) => async (url) => getTokenByURL(url, conf.options.identity);
8
+ export const createGetAuthHeader = (conf) => {
9
+ const keys = registryKeys(conf.options);
10
+ return async (url) => getTokenByURL(url, conf.options.identity, keys);
11
+ };
@@ -0,0 +1,8 @@
1
+ import type { Spec, SpecOptions } from '@vltpkg/spec';
2
+ import type { NodeLike } from '@vltpkg/types';
3
+ /**
4
+ * Create a minimal NodeLike for a package version that is not part of
5
+ * any graph, so that it can be looked up in the security archive.
6
+ * Only the fields used by `SecurityArchive.start()` are needed.
7
+ */
8
+ export declare const createStubNode: (spec: Spec, name: string, version: string, options: SpecOptions) => NodeLike;
@@ -0,0 +1,28 @@
1
+ import { getId } from '@vltpkg/dep-id';
2
+ /**
3
+ * Create a minimal NodeLike for a package version that is not part of
4
+ * any graph, so that it can be looked up in the security archive.
5
+ * Only the fields used by `SecurityArchive.start()` are needed.
6
+ */
7
+ export const createStubNode = (spec, name, version, options) => ({
8
+ id: getId(spec, { name, version }),
9
+ name,
10
+ version,
11
+ confused: false,
12
+ edgesIn: new Set(),
13
+ edgesOut: new Map(),
14
+ workspaces: undefined,
15
+ importer: false,
16
+ mainImporter: false,
17
+ projectRoot: '',
18
+ dev: false,
19
+ optional: false,
20
+ graph: {},
21
+ options,
22
+ /* c8 ignore next 5 - stub methods for NodeLike interface */
23
+ toJSON: () => ({}),
24
+ toString: () => `${name}@${version}`,
25
+ setResolved: () => { },
26
+ setConfusedManifest: () => { },
27
+ maybeSetConfusedManifest: () => { },
28
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/cli-sdk",
3
3
  "description": "The source for the vlt CLI",
4
- "version": "1.3.6",
4
+ "version": "1.3.8",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -14,31 +14,33 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@resvg/resvg-wasm": "^2.6.2",
17
- "@vltpkg/config": "1.3.6",
18
- "@vltpkg/dep-id": "1.3.6",
19
- "@vltpkg/dot-prop": "1.3.6",
20
- "@vltpkg/error-cause": "1.3.6",
21
- "@vltpkg/git": "1.3.6",
22
- "@vltpkg/graph": "1.3.6",
23
- "@vltpkg/graph-run": "1.3.6",
24
- "@vltpkg/init": "1.3.6",
25
- "@vltpkg/output": "1.3.6",
26
- "@vltpkg/package-info": "1.3.6",
27
- "@vltpkg/package-json": "1.3.6",
28
- "@vltpkg/promise-spawn": "1.3.6",
29
- "@vltpkg/query": "1.3.6",
30
- "@vltpkg/registry-client": "1.3.6",
31
- "@vltpkg/rollback-remove": "1.3.6",
32
- "@vltpkg/run": "1.3.6",
33
- "@vltpkg/security-archive": "1.3.6",
34
- "@vltpkg/spec": "1.3.6",
35
- "@vltpkg/tar": "1.3.6",
36
- "@vltpkg/types": "1.3.6",
37
- "@vltpkg/url-open": "1.3.6",
38
- "@vltpkg/vlt-json": "1.3.6",
39
- "@vltpkg/vlx": "1.3.6",
40
- "@vltpkg/workspaces": "1.3.6",
41
- "@vltpkg/xdg": "1.3.6",
17
+ "@vltpkg/config": "1.3.8",
18
+ "@vltpkg/dep-id": "1.3.8",
19
+ "@vltpkg/dot-prop": "1.3.8",
20
+ "@vltpkg/error-cause": "1.3.8",
21
+ "@vltpkg/git": "1.3.8",
22
+ "@vltpkg/graph": "1.3.8",
23
+ "@vltpkg/graph-run": "1.3.8",
24
+ "@vltpkg/init": "1.3.8",
25
+ "@vltpkg/output": "1.3.8",
26
+ "@vltpkg/package-info": "1.3.8",
27
+ "@vltpkg/package-json": "1.3.8",
28
+ "@vltpkg/pick-manifest": "1.3.8",
29
+ "@vltpkg/promise-spawn": "1.3.8",
30
+ "@vltpkg/query": "1.3.8",
31
+ "@vltpkg/registry-client": "1.3.8",
32
+ "@vltpkg/rollback-remove": "1.3.8",
33
+ "@vltpkg/run": "1.3.8",
34
+ "@vltpkg/security-archive": "1.3.8",
35
+ "@vltpkg/semver": "1.3.8",
36
+ "@vltpkg/spec": "1.3.8",
37
+ "@vltpkg/tar": "1.3.8",
38
+ "@vltpkg/types": "1.3.8",
39
+ "@vltpkg/url-open": "1.3.8",
40
+ "@vltpkg/vlt-json": "1.3.8",
41
+ "@vltpkg/vlx": "1.3.8",
42
+ "@vltpkg/workspaces": "1.3.8",
43
+ "@vltpkg/xdg": "1.3.8",
42
44
  "ansi-to-pre": "^1.0.6",
43
45
  "beautiful-mermaid": "^1.1.3",
44
46
  "chalk": "^5.6.2",