@vltpkg/cli-sdk 1.3.5 → 1.3.7

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.
@@ -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,547 @@
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.
170
+ */
171
+ const suggestActions = (result) => {
172
+ const lines = [];
173
+ const updates = result.filter(e => e.inRange).length;
174
+ if (updates) {
175
+ lines.push(`Run \`vlt update\` to pick up ${plural(updates, 'in-range update')}.`);
176
+ }
177
+ const installs = new Map();
178
+ for (const { action } of result) {
179
+ if (!action?.startsWith('vlt install '))
180
+ continue;
181
+ const [spec = '', ...flags] = action
182
+ .slice('vlt install '.length)
183
+ .split(' ');
184
+ const key = flags.join(' ');
185
+ const specs = installs.get(key) ?? [];
186
+ specs.push(spec);
187
+ installs.set(key, specs);
188
+ }
189
+ for (const [flags, specs] of installs) {
190
+ const args = [...specs, ...(flags ? [flags] : [])].join(' ');
191
+ lines.push(`Run \`vlt install ${args}\` to move to latest.`);
192
+ }
193
+ for (const { action } of result) {
194
+ if (action && !action.startsWith('vlt ')) {
195
+ lines.push(`${action.charAt(0).toUpperCase()}${action.slice(1)}.`);
196
+ }
197
+ }
198
+ return lines;
199
+ };
200
+ export const views = {
201
+ json: result => result,
202
+ count: result => result.length,
203
+ human: result => {
204
+ if (result.length === 0) {
205
+ return 'All dependencies are up to date.';
206
+ }
207
+ const columns = [
208
+ { title: 'Package', cells: result.map(e => e.name) },
209
+ {
210
+ title: 'Current',
211
+ cells: result.map(e => missingVersion(e.current)),
212
+ },
213
+ {
214
+ title: 'Wanted',
215
+ cells: result.map(e => missingVersion(e.wanted)),
216
+ },
217
+ {
218
+ title: 'Latest',
219
+ cells: result.map(e => missingVersion(e.latest)),
220
+ },
221
+ { title: 'Type', cells: result.map(e => e.type) },
222
+ ];
223
+ // the dependent column only earns its space when the entries do
224
+ // not all belong to the same importer
225
+ if (new Set(result.map(e => e.dependent)).size > 1) {
226
+ columns.push({
227
+ title: 'Dependent',
228
+ cells: result.map(e => e.dependent),
229
+ });
230
+ }
231
+ columns.push({ title: 'Why', cells: result.map(explain) });
232
+ const actions = suggestActions(result);
233
+ return [
234
+ formatTable(columns),
235
+ ...(actions.length ? ['', ...actions] : []),
236
+ ].join('\n');
237
+ },
238
+ };
239
+ /**
240
+ * The version a registry calls `latest`. Registries that have no
241
+ * `latest` tag fall back to the highest stable published version.
242
+ */
243
+ const latestVersion = (packument) => {
244
+ const tagged = packument['dist-tags'].latest;
245
+ if (tagged)
246
+ return tagged;
247
+ const [highest] = rsort(stable(Object.keys(packument.versions)));
248
+ return highest;
249
+ };
250
+ const gapKind = (current, target) => {
251
+ const c = parse(current);
252
+ const t = parse(target);
253
+ /* c8 ignore next - versions that reached the graph always parse */
254
+ if (!c || !t)
255
+ return 'patch';
256
+ if (t.major > c.major)
257
+ return 'major';
258
+ if (t.minor > c.minor)
259
+ return 'minor';
260
+ if (t.patch > c.patch)
261
+ return 'patch';
262
+ return 'prerelease';
263
+ };
264
+ /** An importer by name, anything else by name and version. */
265
+ const dependentName = (node) => node.importer ? node.name : `${node.name}@${node.version}`;
266
+ /**
267
+ * The other dependents of the installed node whose ranges do not admit
268
+ * the latest version, i.e. what keeps a transitive dependency back
269
+ * even after this dependent's range is widened.
270
+ */
271
+ const heldBy = (edge, latest) => {
272
+ const holders = [];
273
+ for (const other of edge.to?.edgesIn ?? []) {
274
+ if (other === edge || other.from.importer)
275
+ continue;
276
+ const { range } = other.spec.final;
277
+ if (range && !satisfies(latest, range)) {
278
+ holders.push({
279
+ dependent: dependentName(other.from),
280
+ spec: other.spec.bareSpec,
281
+ });
282
+ }
283
+ }
284
+ return holders;
285
+ };
286
+ /**
287
+ * Requirements the latest version makes that the dependent does not
288
+ * meet: a node engine range this process fails, and peer dependency
289
+ * ranges that the dependent's installed peers fall outside of.
290
+ */
291
+ const unmetRequirements = (edge, latest, nodeVersion) => {
292
+ const requires = {};
293
+ const engine = latest.engines?.node;
294
+ if (engine && !satisfies(nodeVersion, engine))
295
+ requires.node = engine;
296
+ const peers = {};
297
+ for (const [peer, range] of Object.entries(latest.peerDependencies ?? {})) {
298
+ const installed = edge.from.edgesOut.get(peer)?.to?.version;
299
+ if (installed && !satisfies(installed, range))
300
+ peers[peer] = range;
301
+ }
302
+ if (Object.keys(peers).length)
303
+ requires.peers = peers;
304
+ return requires.node || requires.peers ? requires : undefined;
305
+ };
306
+ /**
307
+ * The spec to hand `vlt install` so that an alias or a named registry
308
+ * prefix survives the range change, e.g. `foo@npm:bar@^2.0.0`.
309
+ */
310
+ const installSpec = (edge, range) => {
311
+ const { bareSpec, final } = edge.spec;
312
+ const prefix = bareSpec.slice(0, bareSpec.length - final.bareSpec.length);
313
+ return `${edge.name}@${prefix}${range}`;
314
+ };
315
+ const saveFlags = {
316
+ prod: '',
317
+ dev: ' --save-dev',
318
+ optional: ' --save-optional',
319
+ peer: ' --save-peer',
320
+ peerOptional: ' --save-peer',
321
+ };
322
+ /**
323
+ * How an importer upgrades this dependency. A version within the
324
+ * declared range is one `vlt update` away. Moving past the range means
325
+ * changing the spec, with `vlt install` or in the catalog.
326
+ */
327
+ const actionFor = (edge, entry, { savePrefix }) => {
328
+ if (!edge.from.importer)
329
+ return undefined;
330
+ if (!entry.current)
331
+ return 'vlt install';
332
+ const { latest } = entry;
333
+ const { range } = edge.spec.final;
334
+ if (latest && gt(latest, entry.current) && range) {
335
+ if (satisfies(latest, range))
336
+ return 'vlt update';
337
+ const newRange = `${savePrefix}${latest}`;
338
+ if (edge.spec.type === 'catalog') {
339
+ return `set the catalog entry for ${edge.name} in vlt.json to ${newRange}`;
340
+ }
341
+ const workspace = edge.from.mainImporter ?
342
+ ''
343
+ : ` --workspace=${edge.from.location.replace(/^\.\//, '')}`;
344
+ return `vlt install ${installSpec(edge, newRange)}${saveFlags[edge.type]}${workspace}`;
345
+ }
346
+ return 'vlt update';
347
+ };
348
+ /**
349
+ * Describe the dependency `edge` declares, or nothing when its
350
+ * installed version is as new as the registry offers.
351
+ */
352
+ const buildEntry = (edge, packument, options) => {
353
+ const current = edge.to?.version;
354
+ const wanted = pickManifest(packument, edge.spec, options.pick)?.version;
355
+ const latest = latestVersion(packument);
356
+ const wantedAhead = !!wanted && (!current || gt(wanted, current));
357
+ const latestAhead = !!latest && (!current || gt(latest, current));
358
+ if (current && !wantedAhead && !latestAhead)
359
+ return undefined;
360
+ const entry = {
361
+ name: edge.name,
362
+ spec: edge.spec.bareSpec,
363
+ type: edge.type,
364
+ current,
365
+ wanted,
366
+ latest,
367
+ dependent: dependentName(edge.from),
368
+ location: edge.from.location,
369
+ kind: !current ? 'missing'
370
+ : latestAhead ? gapKind(current, latest)
371
+ : gapKind(current, String(wanted)),
372
+ inRange: wantedAhead,
373
+ };
374
+ const deprecated = current ? packument.versions[current]?.deprecated : undefined;
375
+ if (deprecated)
376
+ entry.deprecated = deprecated;
377
+ if (latestAhead) {
378
+ const holders = heldBy(edge, latest);
379
+ if (holders.length)
380
+ entry.heldBy = holders;
381
+ const manifest = packument.versions[latest];
382
+ const requires = manifest &&
383
+ unmetRequirements(edge, manifest, options.nodeVersion);
384
+ if (requires)
385
+ entry.requires = requires;
386
+ }
387
+ const action = actionFor(edge, entry, options);
388
+ if (action)
389
+ entry.action = action;
390
+ return entry;
391
+ };
392
+ /**
393
+ * Pick the importers whose dependencies are reported. All importers by
394
+ * default, or only the selected workspaces when `--workspace` or
395
+ * `--workspace-group` is set in a monorepo.
396
+ */
397
+ const selectImporters = (conf, graph) => {
398
+ const { monorepo } = conf.options;
399
+ const selected = 'workspace' in conf.values || 'workspace-group' in conf.values;
400
+ if (monorepo && selected) {
401
+ const importers = [];
402
+ for (const workspace of monorepo.filter(conf.values)) {
403
+ const node = graph.nodes.get(workspace.id);
404
+ /* c8 ignore next - every loaded workspace has a node */
405
+ if (node)
406
+ importers.push(node);
407
+ }
408
+ return importers;
409
+ }
410
+ return [...graph.importers];
411
+ };
412
+ /**
413
+ * The edges to check: those a `--target` query selects, or else the
414
+ * direct dependencies of the selected importers.
415
+ */
416
+ const selectEdges = async (conf, graph, archive) => {
417
+ const target = conf.get('target');
418
+ if (!target) {
419
+ return selectImporters(conf, graph).flatMap(importer => [
420
+ ...importer.edgesOut.values(),
421
+ ]);
422
+ }
423
+ const query = new Query({
424
+ nodes: new Set(graph.nodes.values()),
425
+ edges: graph.edges,
426
+ importers: graph.importers,
427
+ securityArchive: archive,
428
+ hostContexts: await createHostContextsMap(conf),
429
+ getAuthHeader: createGetAuthHeader(conf),
430
+ });
431
+ const { edges } = await query.search(target, {
432
+ signal: new AbortController().signal,
433
+ });
434
+ return edges.filter(edge => edge instanceof Edge);
435
+ };
436
+ /**
437
+ * The security archive is a bonus, never a requirement: when it cannot
438
+ * be reached the report simply carries no security data.
439
+ */
440
+ const startArchive = async (nodes) => {
441
+ try {
442
+ return await SecurityArchive.start({ nodes });
443
+ }
444
+ catch {
445
+ return undefined;
446
+ }
447
+ };
448
+ const securityOf = (archive, id) => {
449
+ const report = archive.get(id);
450
+ if (!report)
451
+ return undefined;
452
+ return {
453
+ score: Math.round(report.score.overall * 100),
454
+ alerts: report.alerts,
455
+ };
456
+ };
457
+ export const command = async (conf) => {
458
+ const { projectRoot, packageJson, monorepo } = conf.options;
459
+ assertVltInstalled(projectRoot, 'outdated');
460
+ const mainManifest = packageJson.read(projectRoot);
461
+ const graph = actual.load({
462
+ ...conf.options,
463
+ mainManifest,
464
+ modifiers: GraphModifier.maybeLoad(conf.options),
465
+ monorepo,
466
+ loadManifests: true,
467
+ });
468
+ // a query may use security selectors, which need the archive to
469
+ // know every node in the graph before the search runs
470
+ let archive = conf.get('target') ?
471
+ await startArchive([...graph.nodes.values()])
472
+ : undefined;
473
+ const names = new Set(conf.positionals);
474
+ const edges = (await selectEdges(conf, graph, archive)).filter(edge => (names.size === 0 || names.has(edge.name)) &&
475
+ edge.spec.final.type === 'registry');
476
+ const pic = new PackageInfoClient(conf.options);
477
+ // one packument request per registry package, however many
478
+ // dependents it has
479
+ const packuments = new Map();
480
+ const fetchPackument = (edge) => {
481
+ // the final spec of a registry dependency always carries its
482
+ // registry, so the key is unique per package per registry
483
+ const { registry, name } = edge.spec.final;
484
+ const key = `${String(registry)}:${name}`;
485
+ let p = packuments.get(key);
486
+ if (!p) {
487
+ p = pic.packument(edge.spec);
488
+ packuments.set(key, p);
489
+ }
490
+ return p;
491
+ };
492
+ const options = {
493
+ nodeVersion: conf.options['node-version'],
494
+ savePrefix: conf.options['save-exact'] ? '' : conf.options['save-prefix'],
495
+ pick: conf.options,
496
+ };
497
+ const found = [];
498
+ for (const edge of edges) {
499
+ const entry = buildEntry(edge, await fetchPackument(edge), options);
500
+ if (entry)
501
+ found.push({ edge, entry });
502
+ }
503
+ // look up the installed, wanted and latest versions in the security
504
+ // archive, the two candidates as stand-in nodes
505
+ const stub = (edge, version) => createStubNode(edge.spec, edge.spec.final.name, version, conf.options);
506
+ const securityNodes = new Map();
507
+ const candidates = [];
508
+ for (const { edge, entry } of found) {
509
+ const ids = {};
510
+ if (edge.to) {
511
+ ids.current = edge.to.id;
512
+ securityNodes.set(edge.to.id, edge.to);
513
+ }
514
+ for (const which of ['wanted', 'latest']) {
515
+ const version = entry[which];
516
+ if (!version)
517
+ continue;
518
+ const node = stub(edge, version);
519
+ ids[which] = node.id;
520
+ securityNodes.set(node.id, node);
521
+ }
522
+ candidates.push({ entry, ids });
523
+ }
524
+ const nodes = [...securityNodes.values()];
525
+ if (archive) {
526
+ await archive.refresh({ nodes }).catch(() => {
527
+ archive = undefined;
528
+ });
529
+ }
530
+ else if (nodes.length) {
531
+ archive = await startArchive(nodes);
532
+ }
533
+ if (archive) {
534
+ for (const { entry, ids } of candidates) {
535
+ const security = {};
536
+ for (const which of ['current', 'wanted', 'latest']) {
537
+ const id = ids[which];
538
+ const report = id && securityOf(archive, id);
539
+ if (report)
540
+ security[which] = report;
541
+ }
542
+ if (Object.keys(security).length)
543
+ entry.security = security;
544
+ }
545
+ }
546
+ return found.map(({ entry }) => entry);
547
+ };
@@ -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',
@@ -227,32 +227,6 @@ export const views = {
227
227
  };
228
228
  },
229
229
  };
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
230
  /**
257
231
  * Lookup fields from a combined packument+manifest view.
258
232
  *
@@ -311,7 +285,7 @@ export const command = async (conf) => {
311
285
  const version = manifest.version;
312
286
  if (name && version) {
313
287
  try {
314
- const node = createFakeNode(spec, name, version, conf.options);
288
+ const node = createStubNode(spec, name, version, conf.options);
315
289
  const archive = await SecurityArchive.start({
316
290
  nodes: [node],
317
291
  });
@@ -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',
@@ -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.5",
4
+ "version": "1.3.7",
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.5",
18
- "@vltpkg/dep-id": "1.3.5",
19
- "@vltpkg/dot-prop": "1.3.5",
20
- "@vltpkg/error-cause": "1.3.5",
21
- "@vltpkg/git": "1.3.5",
22
- "@vltpkg/graph": "1.3.5",
23
- "@vltpkg/graph-run": "1.3.5",
24
- "@vltpkg/init": "1.3.5",
25
- "@vltpkg/output": "1.3.5",
26
- "@vltpkg/package-info": "1.3.5",
27
- "@vltpkg/package-json": "1.3.5",
28
- "@vltpkg/promise-spawn": "1.3.5",
29
- "@vltpkg/query": "1.3.5",
30
- "@vltpkg/registry-client": "1.3.5",
31
- "@vltpkg/rollback-remove": "1.3.5",
32
- "@vltpkg/run": "1.3.5",
33
- "@vltpkg/security-archive": "1.3.5",
34
- "@vltpkg/spec": "1.3.5",
35
- "@vltpkg/tar": "1.3.5",
36
- "@vltpkg/types": "1.3.5",
37
- "@vltpkg/url-open": "1.3.5",
38
- "@vltpkg/vlt-json": "1.3.5",
39
- "@vltpkg/vlx": "1.3.5",
40
- "@vltpkg/workspaces": "1.3.5",
41
- "@vltpkg/xdg": "1.3.5",
17
+ "@vltpkg/config": "1.3.7",
18
+ "@vltpkg/dep-id": "1.3.7",
19
+ "@vltpkg/dot-prop": "1.3.7",
20
+ "@vltpkg/error-cause": "1.3.7",
21
+ "@vltpkg/git": "1.3.7",
22
+ "@vltpkg/graph": "1.3.7",
23
+ "@vltpkg/graph-run": "1.3.7",
24
+ "@vltpkg/init": "1.3.7",
25
+ "@vltpkg/output": "1.3.7",
26
+ "@vltpkg/package-info": "1.3.7",
27
+ "@vltpkg/package-json": "1.3.7",
28
+ "@vltpkg/pick-manifest": "1.3.7",
29
+ "@vltpkg/promise-spawn": "1.3.7",
30
+ "@vltpkg/query": "1.3.7",
31
+ "@vltpkg/registry-client": "1.3.7",
32
+ "@vltpkg/rollback-remove": "1.3.7",
33
+ "@vltpkg/run": "1.3.7",
34
+ "@vltpkg/security-archive": "1.3.7",
35
+ "@vltpkg/semver": "1.3.7",
36
+ "@vltpkg/spec": "1.3.7",
37
+ "@vltpkg/tar": "1.3.7",
38
+ "@vltpkg/types": "1.3.7",
39
+ "@vltpkg/url-open": "1.3.7",
40
+ "@vltpkg/vlt-json": "1.3.7",
41
+ "@vltpkg/vlx": "1.3.7",
42
+ "@vltpkg/workspaces": "1.3.7",
43
+ "@vltpkg/xdg": "1.3.7",
42
44
  "ansi-to-pre": "^1.0.6",
43
45
  "beautiful-mermaid": "^1.1.3",
44
46
  "chalk": "^5.6.2",