rman 2.12.0 → 2.14.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.
@@ -79,6 +79,10 @@ export class NpmPublishTarget {
79
79
  skipReason(pkg) {
80
80
  return PublishService.skipReason(pkg);
81
81
  }
82
+ /** The registry's host where `publishConfig.registry` names one other than npm's own. */
83
+ labelFor(pkg) {
84
+ return PublishService.registryLabel(pkg);
85
+ }
82
86
  getPlan(ctx) {
83
87
  return PublishService.getPlan(ctx.repository, this.planOptions(ctx));
84
88
  }
@@ -46,3 +46,34 @@ export declare function npmViewVersion(name: string, cwd: string, options?: {
46
46
  registry?: string;
47
47
  userconfig?: string;
48
48
  }): Promise<string | undefined>;
49
+ /** One published version, as `rman deps` needs it. */
50
+ export interface NpmRelease {
51
+ version: string;
52
+ /** The deprecation message, when the version carries one. */
53
+ deprecated?: string;
54
+ peerDependencies?: Record<string, string>;
55
+ peerDependenciesMeta?: Record<string, {
56
+ optional?: boolean;
57
+ }>;
58
+ engines?: Record<string, string>;
59
+ }
60
+ /** What `rman deps` asks of the registry about one package name. */
61
+ export interface NpmReleases {
62
+ /** Every version satisfying the range that was asked about, oldest first. */
63
+ releases: NpmRelease[];
64
+ /** The `latest` dist-tag. */
65
+ latest?: string;
66
+ /** When each version was published, when it was asked for. */
67
+ time?: Record<string, string>;
68
+ }
69
+ /**
70
+ * `npm view '<name>@<range>' version deprecated peerDependencies peerDependenciesMeta engines` plus
71
+ * the package's `dist-tags` (and `time`, when `withTime`) - two calls, run at once. `undefined` when
72
+ * the registry gave no answer at all.
73
+ *
74
+ * **Not catch-everything, unlike the two above.** A name the registry does not know is an answer
75
+ * `rman deps` has to report, not a state it can treat as "nothing newer".
76
+ */
77
+ export declare function npmViewReleases(name: string, range: string, cwd: string, options?: {
78
+ withTime?: boolean;
79
+ }): Promise<NpmReleases | undefined>;
@@ -63,9 +63,56 @@ export async function npmViewVersion(name, cwd, options = {}) {
63
63
  return undefined;
64
64
  }
65
65
  }
66
+ /**
67
+ * `npm view '<name>@<range>' version deprecated peerDependencies peerDependenciesMeta engines` plus
68
+ * the package's `dist-tags` (and `time`, when `withTime`) - two calls, run at once. `undefined` when
69
+ * the registry gave no answer at all.
70
+ *
71
+ * **Not catch-everything, unlike the two above.** A name the registry does not know is an answer
72
+ * `rman deps` has to report, not a state it can treat as "nothing newer".
73
+ */
74
+ /* **One call returns every version in the range.** npm answers a range with an array holding one
75
+ * object per matching version - measured at ~0.45s for a range covering dozens - so the cost is
76
+ * per name and not per version. A range matching exactly one version comes back as a bare object
77
+ * instead, which is why the result is normalized.
78
+ *
79
+ * `time` is a separate call because it is the whole package's history: thousands of entries for a
80
+ * package publishing nightlies, so it is fetched only when a minimum age asks for it. */
81
+ export async function npmViewReleases(name, range, cwd, options = {}) {
82
+ const fields = ['version', 'deprecated', 'peerDependencies', 'peerDependenciesMeta', 'engines'];
83
+ const [releases, tags] = await Promise.all([
84
+ npmViewJson(['view', `${name}@${range}`, ...fields, '--json'], cwd),
85
+ npmViewJson(['view', name, 'dist-tags', ...(options.withTime ? ['time'] : []), '--json'], cwd),
86
+ ]);
87
+ if (tags === undefined)
88
+ return undefined;
89
+ const list = Array.isArray(releases) ? releases : releases ? [releases] : [];
90
+ const meta = (options.withTime ? tags : { 'dist-tags': tags });
91
+ return {
92
+ releases: list.filter((r) => typeof r?.version === 'string'),
93
+ latest: meta['dist-tags']?.latest,
94
+ time: meta.time,
95
+ };
96
+ }
66
97
  function toVersionList(versions) {
67
98
  if (Array.isArray(versions))
68
99
  return versions;
69
100
  return versions ? [versions] : [];
70
101
  }
71
102
  const execFileAsync = promisify(execFile);
103
+ /** `npm <argv>` parsed as JSON - `undefined` for a failed call, `null` for an empty answer (a range
104
+ * no version satisfies). */
105
+ async function npmViewJson(argv, cwd) {
106
+ try {
107
+ const { stdout } = await execFileAsync('npm', argv, { cwd, maxBuffer: 64 * 1024 * 1024 });
108
+ const text = stdout.trim();
109
+ return text ? JSON.parse(text) : null;
110
+ }
111
+ catch (e) {
112
+ /** A range nothing satisfies is answered with `E404 No match found` rather than an empty list. */
113
+ if (argv[1]?.includes('@', 1) && /No match found/.test(String(e?.stdout ?? '') + String(e?.stderr ?? ''))) {
114
+ return null;
115
+ }
116
+ return undefined;
117
+ }
118
+ }
@@ -0,0 +1,175 @@
1
+ import type { RmanApplication } from '../core/application.js';
2
+ import { type RmanConfig } from '../interfaces/rman-config.interface.js';
3
+ /** Hoisted for `ArgsOf` - see `version.command.ts` and `RmanConfig.ArgsOf` for why. */
4
+ declare const COMMAND: "deps [names..]";
5
+ declare const config: {
6
+ scope: {
7
+ target: "cli";
8
+ describe: string;
9
+ type: "string";
10
+ };
11
+ ignore: {
12
+ target: "cli";
13
+ describe: string;
14
+ type: "string";
15
+ };
16
+ platform: {
17
+ target: "cli";
18
+ describe: string;
19
+ type: "string";
20
+ };
21
+ upgrade: {
22
+ target: "cli";
23
+ alias: string;
24
+ describe: string;
25
+ type: "boolean";
26
+ };
27
+ /** Its `choices` and its default are the version schemes' own sizes, so they are filled in where
28
+ * the command is built - see `targetOption`. */
29
+ target: {
30
+ target: "both";
31
+ describe: string;
32
+ type: "string";
33
+ };
34
+ reject: {
35
+ target: "both";
36
+ describe: string;
37
+ type: "string";
38
+ array: true;
39
+ };
40
+ minAge: {
41
+ target: "both";
42
+ cliName: string;
43
+ describe: string;
44
+ type: "number";
45
+ };
46
+ types: {
47
+ target: "config";
48
+ describe: string;
49
+ type: "string";
50
+ array: true;
51
+ };
52
+ concurrency: {
53
+ target: "cli";
54
+ describe: string;
55
+ type: "number";
56
+ };
57
+ verify: {
58
+ target: "cli";
59
+ describe: string;
60
+ type: "boolean";
61
+ };
62
+ json: {
63
+ target: "cli";
64
+ alias: string;
65
+ describe: string;
66
+ type: "boolean";
67
+ };
68
+ };
69
+ type Args = RmanConfig.ArgsOf<typeof config, typeof COMMAND>;
70
+ /**
71
+ * `rman deps` - which dependencies have a newer version, and with `--upgrade`, moving them there.
72
+ *
73
+ * A version is offered only when every rule that can be seen allows it: the package's own
74
+ * `deps.target`/`reject`/`minAge`, the peer ranges of the other dependencies it names, the runtime
75
+ * the package supports, and what its sibling packages ask of a shared dependency. Each version held
76
+ * back is printed with the rule that held it.
77
+ */
78
+ declare const depsCommand: (app: RmanApplication) => {
79
+ command: "deps [names..]";
80
+ describe: string;
81
+ config: {
82
+ target: {
83
+ target: "both";
84
+ describe: string;
85
+ type: "string";
86
+ } & {
87
+ choices: string[];
88
+ };
89
+ scope: {
90
+ target: "cli";
91
+ describe: string;
92
+ type: "string";
93
+ };
94
+ ignore: {
95
+ target: "cli";
96
+ describe: string;
97
+ type: "string";
98
+ };
99
+ platform: {
100
+ target: "cli";
101
+ describe: string;
102
+ type: "string";
103
+ };
104
+ upgrade: {
105
+ target: "cli";
106
+ alias: string;
107
+ describe: string;
108
+ type: "boolean";
109
+ };
110
+ reject: {
111
+ target: "both";
112
+ describe: string;
113
+ type: "string";
114
+ array: true;
115
+ };
116
+ minAge: {
117
+ target: "both";
118
+ cliName: string;
119
+ describe: string;
120
+ type: "number";
121
+ };
122
+ types: {
123
+ target: "config";
124
+ describe: string;
125
+ type: "string";
126
+ array: true;
127
+ };
128
+ concurrency: {
129
+ target: "cli";
130
+ describe: string;
131
+ type: "number";
132
+ };
133
+ verify: {
134
+ target: "cli";
135
+ describe: string;
136
+ type: "boolean";
137
+ };
138
+ json: {
139
+ target: "cli";
140
+ alias: string;
141
+ describe: string;
142
+ type: "boolean";
143
+ };
144
+ };
145
+ positionals: {
146
+ names: {
147
+ describe: string;
148
+ type: "string";
149
+ };
150
+ };
151
+ examples: {
152
+ command: string;
153
+ description: string;
154
+ }[];
155
+ handler: (args: Args) => Promise<void>;
156
+ };
157
+ export default depsCommand;
158
+ /** The rest of `deps.*` - what an option cannot describe. */
159
+ export interface DepsExtraKeys {
160
+ /**
161
+ * The largest move for the dependencies a glob matches, ahead of `target` - a package that
162
+ * releases a major every few weeks, or one that has stayed on `0.x` for years, where a `0.x`
163
+ * minor counts as a major. The last glob matching a name wins.
164
+ *
165
+ * @example { "@types/node": "major", "esbuild": "major" }
166
+ */
167
+ targets?: Record<string, string>;
168
+ }
169
+ /** `deps`'s own keys on `RmanConfig`: derived from the `config` block, plus `DepsExtraKeys`. */
170
+ declare module '../interfaces/rman-config.interface.js' {
171
+ namespace RmanConfig {
172
+ interface CommandConfigs extends RmanConfig.CommandContribution<ReturnType<typeof depsCommand>, DepsExtraKeys> {
173
+ }
174
+ }
175
+ }
@@ -0,0 +1,252 @@
1
+ import path from 'node:path';
2
+ import colors from 'ansi-colors';
3
+ import { semverScheme } from '../core/classes/version-scheme.js';
4
+ import { DependencyUpdater } from '../core/interfaces/dependency-updater.js';
5
+ import { registerCommand } from '../interfaces/rman-config.interface.js';
6
+ import { filterPackages, packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
7
+ /** Hoisted for `ArgsOf` - see `version.command.ts` and `RmanConfig.ArgsOf` for why. */
8
+ const COMMAND = 'deps [names..]';
9
+ const { scope, ignore, platform } = packageFilterOptions;
10
+ const config = {
11
+ /* `--deps`/`--dependents` are left out: on a command called `deps`, `rman deps --deps` would read
12
+ * as something other than "and the packages these depend on". */
13
+ scope,
14
+ ignore,
15
+ platform,
16
+ upgrade: {
17
+ target: 'cli',
18
+ alias: 'u',
19
+ describe: 'Write the new ranges to the manifests, then check that they still install',
20
+ type: 'boolean',
21
+ },
22
+ /** Its `choices` and its default are the version schemes' own sizes, so they are filled in where
23
+ * the command is built - see `targetOption`. */
24
+ target: {
25
+ target: 'both',
26
+ describe: 'The largest move a dependency may make',
27
+ type: 'string',
28
+ },
29
+ reject: {
30
+ target: 'both',
31
+ describe: 'Leave the dependencies matching these globs alone (repeatable)',
32
+ type: 'string',
33
+ array: true,
34
+ },
35
+ minAge: {
36
+ target: 'both',
37
+ cliName: 'min-age',
38
+ describe: 'Only move to a version published at least this many days ago',
39
+ type: 'number',
40
+ },
41
+ types: {
42
+ target: 'config',
43
+ describe: "The dependency kinds to look at, in the ecosystem's own words - default: every kind",
44
+ type: 'string',
45
+ array: true,
46
+ },
47
+ concurrency: {
48
+ target: 'cli',
49
+ describe: `How many registry lookups run at once (default: ${defaultConcurrency()})`,
50
+ type: 'number',
51
+ },
52
+ verify: {
53
+ target: 'cli',
54
+ describe: "After --upgrade, ask the ecosystem's own resolver whether the result installs (default: true)",
55
+ type: 'boolean',
56
+ },
57
+ json: {
58
+ target: 'cli',
59
+ alias: 'j',
60
+ describe: 'Print the plan as JSON - one entry per package and dependency',
61
+ type: 'boolean',
62
+ },
63
+ };
64
+ /**
65
+ * `rman deps` - which dependencies have a newer version, and with `--upgrade`, moving them there.
66
+ *
67
+ * A version is offered only when every rule that can be seen allows it: the package's own
68
+ * `deps.target`/`reject`/`minAge`, the peer ranges of the other dependencies it names, the runtime
69
+ * the package supports, and what its sibling packages ask of a shared dependency. Each version held
70
+ * back is printed with the rule that held it.
71
+ */
72
+ /* **The command is the core's and the answer is the technology's** - see `DependencyUpdater`. A
73
+ * package whose platform has no updater is left alone, which in a polyglot repository is the right
74
+ * answer for the half rman cannot speak for. */
75
+ const depsCommand = registerCommand(app => {
76
+ const repository = app.repository;
77
+ return {
78
+ command: COMMAND,
79
+ describe: 'Lists the dependencies that have a newer version - pass -u to upgrade them',
80
+ config: { ...config, target: targetOption(app) },
81
+ positionals: {
82
+ names: { describe: 'Only these dependencies (globs)', type: 'string' },
83
+ },
84
+ examples: [
85
+ { command: '$0 deps', description: '# What could move, and what holds it back' },
86
+ { command: '$0 deps -u', description: '# Write the new ranges, then check that they install' },
87
+ { command: '$0 deps --target major', description: '# Across a major too' },
88
+ { command: '$0 deps "@types/*"', description: '# Only the dependencies matching this glob' },
89
+ ],
90
+ handler: async (args) => {
91
+ const options = {
92
+ names: args.names?.length ? args.names.map(String) : undefined,
93
+ target: args.target,
94
+ reject: args.reject?.map(String),
95
+ minAge: args.minAge,
96
+ concurrency: Math.max(1, args.concurrency ?? defaultConcurrency()),
97
+ };
98
+ const ctx = { app, repository, options };
99
+ /** The root holds a monorepo's shared tooling, so it is asked too. */
100
+ const candidates = repository.monorepo ? [repository.rootPackage, ...repository.packages] : repository.packages;
101
+ const selected = filterPackages([...candidates], readPackageFilterOptions(args));
102
+ if (!selected.length)
103
+ throw logged('No package matched.');
104
+ const byUpdater = new Map();
105
+ for (const pkg of selected) {
106
+ const updater = pkg.platform.dependencyUpdater;
107
+ if (!updater)
108
+ continue;
109
+ byUpdater.set(updater, [...(byUpdater.get(updater) ?? []), pkg]);
110
+ }
111
+ if (!byUpdater.size) {
112
+ throw logged('None of the selected packages belongs to a technology that can update its dependencies.');
113
+ }
114
+ const plans = new Map();
115
+ for (const [updater, packages] of byUpdater)
116
+ plans.set(updater, await updater.getPlan(ctx, packages));
117
+ const all = [...plans.values()].flat();
118
+ if (args.json)
119
+ console.log(JSON.stringify(jsonPlan(all), undefined, 2));
120
+ else
121
+ printPlan(all, repository.dirname);
122
+ const errors = all.filter(e => e.status === 'error');
123
+ const updates = all.filter(e => e.status === 'update');
124
+ if (!args.upgrade) {
125
+ if (updates.length && !args.json)
126
+ console.log(colors.gray('\nRun "rman deps -u" to upgrade.'));
127
+ if (errors.length)
128
+ throw logged(`${errors.length} dependency lookup(s) failed - see above.`);
129
+ return;
130
+ }
131
+ const applied = [];
132
+ for (const [updater, plan] of plans) {
133
+ if (plan.some(e => e.status === 'update'))
134
+ applied.push(await updater.applyPlan(ctx, plan));
135
+ }
136
+ const files = applied.flatMap(a => a.files);
137
+ if (files.length && args.verify !== false) {
138
+ for (const [updater, packages] of byUpdater) {
139
+ const refused = await updater.verify?.(ctx, packages);
140
+ if (!refused)
141
+ continue;
142
+ for (const a of applied)
143
+ a.restore();
144
+ throw logged(`The upgraded manifests do not install, so they were put back:\n${refused}`);
145
+ }
146
+ }
147
+ if (!args.json) {
148
+ for (const file of files)
149
+ console.log(colors.green('updated'), path.relative(repository.dirname, file));
150
+ }
151
+ if (errors.length)
152
+ throw logged(`${errors.length} dependency lookup(s) failed - see above.`);
153
+ },
154
+ };
155
+ });
156
+ export default depsCommand;
157
+ /** Registry lookups are child processes waiting on the network, not CPU work, so the default is a
158
+ * fixed number rather than the core count. */
159
+ function defaultConcurrency() {
160
+ return 16;
161
+ }
162
+ /**
163
+ * `--target`: one of the version schemes' sizes, and by default every size but the largest.
164
+ *
165
+ * Read off the technologies this repository loaded rather than written down, because the sizes are
166
+ * a scheme's - semver's three are the only ones today.
167
+ */
168
+ function targetOption(app) {
169
+ const schemes = [...app.platforms].map(p => p.manifestProvider.versionScheme ?? semverScheme);
170
+ const sizes = [...new Set((schemes.length ? schemes : [semverScheme]).flatMap(s => s.bumpNames))];
171
+ const defaults = [...new Set(schemes.map(s => DependencyUpdater.defaultTarget(s.bumpNames)))];
172
+ return {
173
+ ...config.target,
174
+ describe: `${config.target.describe} (default: ${defaults.join(' / ') || DependencyUpdater.defaultTarget(semverScheme.bumpNames)})`,
175
+ choices: sizes,
176
+ };
177
+ }
178
+ function jsonPlan(entries) {
179
+ return entries.map(e => ({
180
+ package: e.package.name,
181
+ name: e.name,
182
+ types: e.types,
183
+ status: e.status,
184
+ current: e.current,
185
+ target: e.target,
186
+ latest: e.latest,
187
+ available: e.available,
188
+ bump: e.bump,
189
+ reason: e.reason,
190
+ }));
191
+ }
192
+ /**
193
+ * One block per package with something to say: what moves, grouped by size, smallest first - and
194
+ * then **what does not**, each with the reason it stays.
195
+ */
196
+ /* **The second half is the one `npm-check-updates` taught people to read.** Under `--target minor`
197
+ * a major that was left behind has to be on the screen, or a run reporting three patches looks like
198
+ * a repository that is current. A dependency that moved but not as far as it could appears in both:
199
+ * once under its size, once under "not updated" with what held it. */
200
+ function printPlan(entries, root) {
201
+ const updated = entries.filter(e => e.status === 'update');
202
+ const behind = entries.filter(e => e.status === 'held' || e.status === 'skipped' || e.status === 'error' || (e.status === 'update' && e.reason));
203
+ if (!updated.length && !behind.length) {
204
+ console.log(colors.gray('Every dependency is up to date.'));
205
+ return;
206
+ }
207
+ const shown = [...updated, ...behind];
208
+ const nameWidth = Math.max(...shown.map(e => e.name.length));
209
+ const currentWidth = Math.max(...shown.map(e => e.current.length));
210
+ const row = (e, rest) => ` ${e.name.padEnd(nameWidth)} ${e.current.padEnd(currentWidth)} ${rest}`;
211
+ for (const pkg of [...new Set(shown.map(e => e.package))]) {
212
+ const where = path.relative(root, pkg.dirname);
213
+ console.log(colors.cyan(pkg.name) + (where ? colors.gray(` (${where})`) : ''));
214
+ const sizes = pkg.versionScheme.bumpNames;
215
+ for (const size of [...sizes, undefined]) {
216
+ const group = updated.filter(e => e.package === pkg && (size ? e.bump === size : !sizes.includes(e.bump)));
217
+ if (!group.length)
218
+ continue;
219
+ const paint = sizeColor(size, sizes);
220
+ console.log(` ${paint(size ?? 'other')}`);
221
+ for (const e of group)
222
+ console.log(row(e, `→ ${paint(e.target ?? '')}`));
223
+ }
224
+ const left = behind.filter(e => e.package === pkg);
225
+ if (!left.length)
226
+ continue;
227
+ console.log(` ${colors.yellow('not updated')}`);
228
+ const newest = (e) => (e.status === 'error' ? 'error' : (e.available ?? e.latest ?? ''));
229
+ const width = Math.max(...left.map(e => newest(e).length));
230
+ for (const e of left) {
231
+ const paint = e.status === 'error' ? colors.red : (text) => text;
232
+ console.log(row(e, `${paint(newest(e).padEnd(width))} ${colors.gray(e.reason ?? '')}`));
233
+ }
234
+ }
235
+ }
236
+ /** The colours `npm-check-updates` uses - the largest size red, the next cyan, the rest green - read
237
+ * off the scheme's own order so a scheme with other names still gets them. */
238
+ function sizeColor(size, sizes) {
239
+ const rank = size ? sizes.indexOf(size) : -1;
240
+ if (rank === sizes.length - 1)
241
+ return colors.red;
242
+ if (rank === sizes.length - 2)
243
+ return colors.cyan;
244
+ return colors.green;
245
+ }
246
+ /** The `logged` convention: printed here, so `runCli`'s catch does not print it a second time. */
247
+ function logged(message) {
248
+ console.error(colors.red(message));
249
+ const err = new Error(message);
250
+ err.logged = true;
251
+ return err;
252
+ }
@@ -2,6 +2,7 @@ import colors from 'ansi-colors';
2
2
  import EasyTable from 'easy-table';
3
3
  import { registerCommand } from '../interfaces/rman-config.interface.js';
4
4
  import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
5
+ import { isNamedGroupKey, isSoloGroupKey, ROOT_GROUP_KEY } from '../utils/version-group.js';
5
6
  /** `[options...]` is yargs-meaningless - its variadic marker is two dots, and this command declares
6
7
  * no positional for it. Left as it is rather than fixed in passing: dropping it makes
7
8
  * `rman list foo` start failing, which is a behaviour change and belongs in its own commit. */
@@ -82,8 +83,9 @@ const listCommand = registerCommand(app => {
82
83
  * so counting rows that are not the root reported `0 Package(s) found` there (measured). The
83
84
  * condition is the same one the service applies, and it is one line.
84
85
  */
86
+ /** Grouped, except where the order *is* the answer: `--toposort` is dependency order. */
85
87
  else
86
- printTable(items, table && app.repository.monorepo);
88
+ printTable(args.toposort ? items : byGroup(items), table && app.repository.monorepo);
87
89
  },
88
90
  };
89
91
  });
@@ -100,10 +102,54 @@ function statusLabel(status) {
100
102
  return '';
101
103
  }
102
104
  }
103
- /** Where a package ships, one target name each - grey for a target `publish` would skip it for
104
- * (`Item.skippedTargets`), so the column never reads as a promise the release will not keep. */
105
+ /** Where a package ships, one target each - its label where it has one (`Item.targetLabels`, npm's
106
+ * registry host). A target `publish` would skip it for (`Item.skippedTargets`) is left out, and a
107
+ * package left with none reads `-`, so the column never reads as a promise the release will not
108
+ * keep. Blank where no target claims the package at all - a monorepo's root, for one. */
105
109
  function publishLabel(it) {
106
- return it.publishTargets.map(name => (it.skippedTargets[name] ? colors.gray(name) : colors.green(name))).join(', ');
110
+ if (!it.publishTargets.length)
111
+ return '';
112
+ const shipping = it.publishTargets.filter(name => !it.skippedTargets[name]);
113
+ if (!shipping.length)
114
+ return colors.gray('-');
115
+ return shipping.map(name => colors.green(it.targetLabels[name] ?? name)).join(', ');
116
+ }
117
+ /**
118
+ * The rows with each release group's members together - the root first, then the groups in the
119
+ * order their first member appears, then the packages in no group (`group: false`) - the order
120
+ * `version`'s plan prints them in. Stable within a group, so the lexical order holds there.
121
+ */
122
+ /* The cost, stated: a package nested inside another can land away from it, when the two are in
123
+ * different groups - its indentation still says how deep it sits, not under which row. */
124
+ function byGroup(items) {
125
+ const root = items.filter(it => it.groupKey === ROOT_GROUP_KEY);
126
+ const groups = new Map();
127
+ const solo = [];
128
+ for (const it of items) {
129
+ if (it.groupKey === ROOT_GROUP_KEY)
130
+ continue;
131
+ if (isSoloGroupKey(it.groupKey))
132
+ solo.push(it);
133
+ else
134
+ groups.set(it.groupKey, [...(groups.get(it.groupKey) ?? []), it]);
135
+ }
136
+ return [...root, ...[...groups.values()].flat(), ...solo];
137
+ }
138
+ /** Whether the Group column says anything: a group the repository named, or the default group
139
+ * beside packages in none. One line for everything, or no grouping at all, would put the same
140
+ * answer - or nothing - on every row. */
141
+ function showsGroups(items) {
142
+ const keys = items.map(it => it.groupKey).filter(key => key !== ROOT_GROUP_KEY);
143
+ return keys.some(isNamedGroupKey) || (keys.includes('default') && keys.some(isSoloGroupKey));
144
+ }
145
+ /** A named group by its name, the default one as a grey `(default)` - `version`'s plan table reads
146
+ * the same way. Blank for the root and for a package in no group. */
147
+ function groupCell(it) {
148
+ if (isNamedGroupKey(it.groupKey))
149
+ return colors.cyan(it.group);
150
+ if (it.groupKey === 'default')
151
+ return colors.gray('(default)');
152
+ return '';
107
153
  }
108
154
  /**
109
155
  * **The tree, as a table**: the root first, then each package indented by how far below it sits.
@@ -125,10 +171,13 @@ function publishLabel(it) {
125
171
  */
126
172
  function printTable(items, withRoot) {
127
173
  const table = new EasyTable();
174
+ const withGroup = showsGroups(items);
128
175
  for (const it of items) {
129
176
  const indent = ' '.repeat(it.depth);
130
177
  table.cell('Package', indent + (it.isRoot ? colors.whiteBright(it.selector) : colors.yellowBright(it.selector)));
131
178
  table.cell('Version', colors.yellow(it.version));
179
+ if (withGroup)
180
+ table.cell('Group', groupCell(it));
132
181
  table.cell('Platform', it.platform ? colors.cyan(it.platform) : '');
133
182
  table.cell('Private', it.private ? colors.magentaBright('yes') : '');
134
183
  table.cell('Publish', publishLabel(it));
package/commands.d.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  import './commands/build.command.js';
16
16
  import './commands/changelog.command.js';
17
17
  import './commands/config.command.js';
18
+ import './commands/deps.command.js';
18
19
  import './commands/diff.command.js';
19
20
  import './commands/exec.command.js';
20
21
  import './commands/github-release.command.js';
@@ -27,5 +28,6 @@ import './commands/test.command.js';
27
28
  import './commands/version.command.js';
28
29
  /** The config shapes a command declares by hand, where an option cannot describe them - see
29
30
  * `RmanConfig.CommandContribution`'s `Extra`. Exported so a plugin can name one. */
31
+ export type { DepsExtraKeys } from './commands/deps.command.js';
30
32
  export type { PublishExtraKeys, PublishTargetConfigs } from './commands/publish.command.js';
31
33
  export type { VersionExtraKeys, VersionStampEntry } from './commands/version.command.js';
package/commands.js CHANGED
@@ -15,6 +15,7 @@
15
15
  import './commands/build.command.js';
16
16
  import './commands/changelog.command.js';
17
17
  import './commands/config.command.js';
18
+ import './commands/deps.command.js';
18
19
  import './commands/diff.command.js';
19
20
  import './commands/exec.command.js';
20
21
  import './commands/github-release.command.js';
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.12.0';
1
+ export const version = '2.14.0';