rman 1.2.5 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +63 -19
  2. package/cli.d.ts +5 -0
  3. package/cli.js +214 -88
  4. package/commands/build.command.d.ts +177 -3
  5. package/commands/build.command.js +20 -10
  6. package/commands/changed.command.d.ts +80 -3
  7. package/commands/changed.command.js +19 -12
  8. package/commands/changelog.command.d.ts +192 -3
  9. package/commands/changelog.command.js +87 -43
  10. package/commands/config.command.d.ts +44 -3
  11. package/commands/config.command.js +30 -19
  12. package/commands/diff.command.d.ts +37 -3
  13. package/commands/diff.command.js +25 -16
  14. package/commands/exec.command.d.ts +193 -3
  15. package/commands/exec.command.js +60 -58
  16. package/commands/github-release.command.d.ts +154 -3
  17. package/commands/github-release.command.js +67 -37
  18. package/commands/import.command.d.ts +36 -3
  19. package/commands/import.command.js +28 -20
  20. package/commands/info.command.d.ts +35 -7
  21. package/commands/info.command.js +36 -30
  22. package/commands/list.command.d.ts +163 -3
  23. package/commands/list.command.js +109 -71
  24. package/commands/publish.command.d.ts +231 -0
  25. package/commands/publish.command.js +304 -0
  26. package/commands/run.command.d.ts +186 -6
  27. package/commands/run.command.js +26 -72
  28. package/commands/test.command.d.ts +173 -3
  29. package/commands/test.command.js +16 -10
  30. package/commands/version.command.d.ts +317 -3
  31. package/commands/version.command.js +149 -69
  32. package/commands.d.ts +32 -0
  33. package/commands.js +28 -0
  34. package/constants.js +1 -1
  35. package/core/application.d.ts +116 -0
  36. package/core/application.js +143 -0
  37. package/core/command-builder.d.ts +14 -0
  38. package/core/command-builder.js +78 -0
  39. package/core/config.d.ts +180 -50
  40. package/core/config.js +332 -153
  41. package/core/core-services.d.ts +14 -0
  42. package/core/core-services.js +30 -0
  43. package/core/core-targets.d.ts +14 -0
  44. package/core/core-targets.js +16 -0
  45. package/core/custom-command.d.ts +42 -6
  46. package/core/custom-command.js +44 -17
  47. package/core/extends-config.d.ts +13 -5
  48. package/core/extends-config.js +52 -12
  49. package/core/load-config-module.d.ts +28 -0
  50. package/core/load-config-module.js +42 -0
  51. package/core/manifest.d.ts +47 -25
  52. package/core/manifest.js +51 -69
  53. package/core/merge-config.d.ts +33 -34
  54. package/core/merge-config.js +138 -93
  55. package/core/package.d.ts +145 -17
  56. package/core/package.js +128 -36
  57. package/core/plugin-loader.d.ts +65 -0
  58. package/core/plugin-loader.js +234 -0
  59. package/core/plugin.d.ts +135 -90
  60. package/core/plugin.js +70 -173
  61. package/core/publish-target.d.ts +124 -0
  62. package/core/publish-target.js +30 -0
  63. package/core/registry.d.ts +30 -0
  64. package/core/registry.js +47 -0
  65. package/core/repository.d.ts +72 -9
  66. package/core/repository.js +293 -43
  67. package/core/resolve-target.d.ts +1 -1
  68. package/core/resolve-target.js +1 -1
  69. package/core/service.d.ts +49 -0
  70. package/core/service.js +40 -0
  71. package/core/version-scheme.d.ts +23 -1
  72. package/core/version-scheme.js +29 -1
  73. package/core/workspace.d.ts +84 -33
  74. package/core/workspace.js +63 -22
  75. package/index.d.ts +111 -14
  76. package/index.js +85 -9
  77. package/interfaces/rman-config.interface.d.ts +739 -212
  78. package/interfaces/rman-config.interface.js +61 -1
  79. package/package.json +2 -1
  80. package/plugins/builtins.d.ts +44 -0
  81. package/plugins/builtins.js +33 -0
  82. package/plugins/detect.d.ts +78 -0
  83. package/plugins/detect.js +70 -0
  84. package/plugins/node/augmentation/rman.augmentation.d.ts +84 -0
  85. package/plugins/node/augmentation/rman.augmentation.js +1 -0
  86. package/plugins/node/augmentation/system-info.augmentation.d.ts +26 -0
  87. package/plugins/node/augmentation/system-info.augmentation.js +79 -0
  88. package/plugins/node/commands/ci.command.d.ts +131 -0
  89. package/plugins/node/commands/ci.command.js +59 -0
  90. package/plugins/node/commands/clean.command.d.ts +183 -0
  91. package/plugins/node/commands/clean.command.js +73 -0
  92. package/plugins/node/index.d.ts +29 -0
  93. package/plugins/node/index.js +40 -0
  94. package/plugins/node/node-config.interface.d.ts +77 -0
  95. package/plugins/node/node-config.interface.js +7 -0
  96. package/plugins/node/node-manifest.provider.d.ts +68 -0
  97. package/plugins/node/node-manifest.provider.js +125 -0
  98. package/plugins/node/node.platform.d.ts +53 -0
  99. package/plugins/node/node.platform.js +134 -0
  100. package/plugins/node/npm-publish-target.d.ts +73 -0
  101. package/plugins/node/npm-publish-target.js +96 -0
  102. package/plugins/node/services/ci.service.d.ts +47 -0
  103. package/plugins/node/services/ci.service.js +213 -0
  104. package/plugins/node/services/clean.service.d.ts +53 -0
  105. package/plugins/node/services/clean.service.js +237 -0
  106. package/plugins/node/services/publish.service.d.ts +114 -0
  107. package/plugins/node/services/publish.service.js +371 -0
  108. package/plugins/node/services/version-plan.service.d.ts +44 -0
  109. package/plugins/node/services/version-plan.service.js +58 -0
  110. package/plugins/node/utils/npm-view.d.ts +48 -0
  111. package/plugins/node/utils/npm-view.js +71 -0
  112. package/plugins/node/utils/workspace-range.d.ts +26 -0
  113. package/plugins/node/utils/workspace-range.js +28 -0
  114. package/services/change-hash.service.d.ts +2 -2
  115. package/services/change-hash.service.js +2 -2
  116. package/services/changelog.service.d.ts +62 -51
  117. package/services/changelog.service.js +14 -11
  118. package/services/docker-publish.service.d.ts +50 -29
  119. package/services/docker-publish.service.js +43 -20
  120. package/services/exec.service.d.ts +23 -12
  121. package/services/exec.service.js +14 -9
  122. package/services/github-release.service.d.ts +44 -33
  123. package/services/github-release.service.js +13 -10
  124. package/services/import.service.d.ts +25 -14
  125. package/services/import.service.js +9 -5
  126. package/services/list.service.d.ts +62 -10
  127. package/services/list.service.js +62 -15
  128. package/services/run.service.d.ts +22 -13
  129. package/services/run.service.js +262 -223
  130. package/services/version-plan.service.d.ts +27 -4
  131. package/services/version-plan.service.js +42 -15
  132. package/services/version.service.d.ts +31 -11
  133. package/services/version.service.js +29 -13
  134. package/targets/docker.target.d.ts +53 -0
  135. package/targets/docker.target.js +40 -0
  136. package/utils/bin-path.d.ts +6 -7
  137. package/utils/bin-path.js +7 -18
  138. package/utils/branch-guard.d.ts +29 -0
  139. package/utils/branch-guard.js +31 -0
  140. package/utils/exec.d.ts +10 -0
  141. package/utils/exec.js +1 -1
  142. package/utils/logger.d.ts +1 -1
  143. package/utils/logger.js +1 -1
  144. package/utils/package-filter.d.ts +127 -7
  145. package/utils/package-filter.js +197 -16
  146. package/utils/printable-config.d.ts +1 -1
  147. package/utils/printable-config.js +1 -1
  148. package/utils/run-bin.d.ts +10 -0
  149. package/utils/run-bin.js +1 -1
  150. package/utils/run-options.d.ts +97 -0
  151. package/utils/run-options.js +81 -0
  152. package/utils/version-stamp.d.ts +1 -1
  153. package/utils/version-stamp.js +1 -1
package/README.md CHANGED
@@ -66,7 +66,7 @@ rman version
66
66
  # Apply it: bump versions, write CHANGELOG.md, commit, tag
67
67
  rman version --changelog
68
68
 
69
- # Publish everything that isn't already on the registry (needs rman-node)
69
+ # Publish everything that isn't already on the registry
70
70
  rman publish
71
71
  ```
72
72
 
@@ -90,17 +90,29 @@ worked examples of every single command, see **[docs/cli-rman.md](https://github
90
90
  | [`diff [package]`](#rman-diff-package) | Shows the git diff since a package's (or the repo's) last release tag. |
91
91
  | [`changelog`](#rman-changelog) | Generates a changelog per package from unreleased commits. |
92
92
  | [`version [bump]`](#rman-version-bump) | Bumps versions of changed packages (and their dependents). |
93
+ | [`publish`](#rman-publish) | Publishes every package whose version isn't on its registry yet. |
93
94
  | [`github-release`](#rman-github-release) | Creates the repository's GitHub Release for its release tag. |
94
95
  | [`import <path>`](#rman-import-path) | Imports an external git repository as a new package, with history. |
95
96
 
96
- **`publish`, `ci` and `clean` come from [`rman-node`](https://www.npmjs.com/package/rman-node)**,
97
- not from this package - each is about npm or TypeScript rather than about repositories. Name the
98
- plugin in `.rmanrc` to get them:
97
+ **`ci` and `clean` come from the `node` built-in**, which ships inside this package - each is about
98
+ npm or TypeScript rather than about repositories, so nothing it contributes exists until a
99
+ repository asks for it:
99
100
 
100
101
  ```yaml
101
- plugins: ['rman-node']
102
+ plugins: ['node'] # by name
103
+ platform: node # the same statement at a repository root, plus which technology its packages are
102
104
  ```
103
105
 
106
+ Or say nothing: a repository that declares no technology gets the one its own files imply, announced
107
+ on stderr rather than guessed silently. It used to take a second package (`rman-node`) and a
108
+ `.rmanrc` before anything worked at all.
109
+
110
+ **`publish` is here, but *where* a package ships is a plugin's to say.** A **publish target** is
111
+ one answer to "is this version on the registry, and how do I push it" - rman ships `docker`
112
+ (any language's project can push an image), and the `node` built-in contributes `npm` with the flags
113
+ that only mean something there (`--access`, `--tag`, `--otp`, `--registry`, ...). So
114
+ `rman publish --help` lists what this repository's targets actually understand.
115
+
104
116
  Options shared across several commands:
105
117
 
106
118
  - **`skip`** (`.rmanrc`, per package): leave this package alone - honoured by every command that
@@ -108,8 +120,10 @@ Options shared across several commands:
108
120
  - **Package filtering** (`list`, `run`/`build`/`test`, `exec`, `version`, `changelog`, and a
109
121
  plugin's own commands): `--scope <glob>`, `--ignore <glob>`, `--deps`, `--dependents` - see
110
122
  [Package filtering](https://github.com/panates/rman/blob/main/docs/rman.md#package-filtering-scopeignoredepsdependents)
111
- for the full semantics.
112
- - **`--root`/`-r`** (every command that narrows to the package you are standing in -
123
+ for the full semantics. **`--scope /` is the repository's own root package** - the same `/`
124
+ `.rmanrc`'s `"[/]"` block uses, and not a glob, so `--scope '*'` means the members and a glob
125
+ never picks up the root by name.
126
+ - **`--from-root`/`-r`** (every command that narrows to the package you are standing in -
113
127
  `run`/`build`/`test`, `exec`, `changelog`, `diff`): run against the whole repository instead.
114
128
  - **Branch guard** (every command that mutates state or runs scripts - `run`/`build`/`test`,
115
129
  `exec`, `version`): `--allow-branch <glob>`, `--ignore-branch <glob>` -
@@ -134,9 +148,10 @@ rman list --scope '@myorg/*' --ignore '*-internal'
134
148
 
135
149
  ### `rman info`
136
150
 
137
- Prints local environment (OS/CPU/memory, Node, git) and repository information. A plugin adds its
138
- own ecosystem's part - `rman-node` reports whichever package manager `.rmanrc "packageManager"`
139
- names, plus the installed `rman` packages.
151
+ Prints local environment (OS/CPU/memory, Node, git) and repository information. A platform adds its
152
+ own ecosystem's part - the `node` built-in reports whichever package manager
153
+ `.rmanrc "packageManager"` names, plus the installed `rman` packages, and only in a repository that
154
+ asked for it.
140
155
 
141
156
  ```bash
142
157
  rman info
@@ -192,12 +207,12 @@ rman exec --topo=false pwd # every package independently, alphabetical o
192
207
  ### `rman config`
193
208
 
194
209
  Prints the **effective** config for the package of the current directory - after the directory
195
- cascade, `"[selector]"` blocks, `extends`, `+key` appends and `${{ ... }}` expressions have all been
210
+ cascade, `"[selector]"` blocks, `extends` and `${{ ... }}` expressions have all been
196
211
  applied. What rman actually sees there, which no single file shows.
197
212
 
198
213
  ```bash
199
214
  rman config # the package you are standing in
200
- rman config --root # the repository root's own config instead
215
+ rman config --from-root # the repository root's own config instead
201
216
  rman config --json | jq .run
202
217
  ```
203
218
 
@@ -266,6 +281,29 @@ Release-As: patch
266
281
  See [docs/rman.md#versionservice](https://github.com/panates/rman/blob/main/docs/rman.md#versionservice) for the full grouping/propagation
267
282
  algorithm, prerelease semantics, and `"workspace:"` dependency-range handling.
268
283
 
284
+ ### `rman publish`
285
+
286
+ Publishes every package whose current version isn't on its registry yet. Shows the plan first, then
287
+ asks for confirmation (unless `--yes` or `--dry-run`), then publishes in topological order,
288
+ dependencies before dependents.
289
+
290
+ ```bash
291
+ rman publish # show the plan, then ask for confirmation
292
+ rman publish --yes # publish immediately, no confirmation
293
+ rman publish --dry-run --json # "is there anything to release?", for a CI gate
294
+ rman publish --target docker # only the packages configured for that target
295
+ ```
296
+
297
+ **Where a package ships is a publish target, and a target is a contribution.** rman ships `docker`;
298
+ the `node` built-in contributes `npm`. A package says where it goes with `.rmanrc "publish.target"`, or says
299
+ nothing and goes wherever the installed targets claim it - so a Cargo package is never assumed to be
300
+ an npm one. Each target adds its own flags, so `rman publish --help` is worth reading in your own
301
+ repository. See
302
+ [docs/cli/publish.md](https://github.com/panates/rman/blob/main/docs/cli/publish.md).
303
+
304
+ It never looks at whether `version` ran: it inspects what is on disk and on each registry, so it
305
+ behaves the same right after a bump or days later, and re-running is safe.
306
+
269
307
  ### `rman github-release`
270
308
 
271
309
  Creates the repository's GitHub Release for the version that just shipped - one per run, named after
@@ -294,7 +332,7 @@ rman import ../my-old-standalone-repo --dest libs # under libs/ instead of pac
294
332
  already. After importing, add the new directory to your `workspaces` glob if it isn't already
295
333
  covered, then run `rman ci` to install it.
296
334
 
297
- ## Shared config (`extends`) and appending (`+key`)
335
+ ## Shared config (`extends`) and adding to it (`value`)
298
336
 
299
337
  House rules live in one package, and a repository names it:
300
338
 
@@ -305,13 +343,18 @@ extends: '@panates/rman-monorepo'
305
343
  '[*]':
306
344
  run:
307
345
  build:
308
- +before: 'rm -rf ./cache' # adds to the base's step, rather than replacing it
346
+ # adds to the base's step, rather than replacing it
347
+ before: "${{ [...value, 'rm -rf ./cache'] }}"
309
348
  ```
310
349
 
311
350
  `extends` merges underneath the file naming it (a package, a path, or a list), and may itself be
312
- chained. `+key` appends to whatever the key already resolved to - from the base, a parent directory,
313
- or a selector - which is what lets a repository add one step without restating a list it doesn't
314
- own. See [docs/rman.md](https://github.com/panates/rman/blob/main/docs/rman.md#inheriting-a-shared-config-extends).
351
+ chained. `value` is what the key already resolved to - from the base, a parent directory, or a
352
+ selector - which is what lets a repository add one step without restating a list it doesn't own. It
353
+ is the list form of whatever is underneath, so the spread needs no guard even when nothing is.
354
+ See [docs/rman.md](https://github.com/panates/rman/blob/main/docs/rman.md#inheriting-a-shared-config-extends).
355
+
356
+ There was a `+key` prefix for this and it is gone; one still in a config is refused, naming what to
357
+ write instead.
315
358
 
316
359
  ## Your own commands
317
360
 
@@ -408,8 +451,9 @@ import { defineConfig } from 'rman';
408
451
  export default defineConfig({ allowBranch: ['main'] });
409
452
  ```
410
453
 
411
- With a plugin, import `defineConfig` from the plugin instead (`rman-node`'s carries its own keys
412
- into the type). The `.rmanrc`/`.rmanrc.yml` forms get no checking - see
454
+ A plugin's keys arrive by declaration merging, so annotating with `RmanConfig` types them too;
455
+ `RmanNodeConfig` is the alias that says out loud which set a config is using. The
456
+ `.rmanrc`/`.rmanrc.yml` forms get no checking - see
413
457
  [docs/rman.md#editor-support-types](https://github.com/panates/rman/blob/main/docs/rman.md#editor-support-types) for why the JSON Schema that
414
458
  used to cover them was removed.
415
459
 
package/cli.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ /** Every built-in command, registered by importing them - see `commands.ts` for why the list lives
3
+ * there rather than here. */
4
+ import './commands.js';
5
+ import { RmanApplication } from './core/application.js';
2
6
  export declare function runCli(options?: {
3
7
  argv?: string[];
4
8
  cwd?: string;
9
+ app?: RmanApplication;
5
10
  }): Promise<void>;
package/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ /** Every built-in command, registered by importing them - see `commands.ts` for why the list lives
3
+ * there rather than here. */
4
+ import './commands.js';
2
5
  import { realpathSync } from 'node:fs';
3
6
  import path from 'node:path';
4
7
  import { fileURLToPath } from 'node:url';
@@ -6,24 +9,15 @@ import colors from 'ansi-colors';
6
9
  import * as yaml from 'js-yaml';
7
10
  import yargs, {} from 'yargs';
8
11
  import { hideBin } from 'yargs/helpers';
9
- import * as buildCommand from './commands/build.command.js';
10
- import * as changedCommand from './commands/changed.command.js';
11
- import * as changelogCommand from './commands/changelog.command.js';
12
- import * as configCommand from './commands/config.command.js';
13
- import * as diffCommand from './commands/diff.command.js';
14
- import * as execCommand from './commands/exec.command.js';
15
- import * as githubReleaseCommand from './commands/github-release.command.js';
16
- import * as importCommand from './commands/import.command.js';
17
- import * as infoCommand from './commands/info.command.js';
18
- import * as listCommand from './commands/list.command.js';
19
- import * as runCommand from './commands/run.command.js';
20
- import * as testCommand from './commands/test.command.js';
21
- import * as versionCommand from './commands/version.command.js';
22
12
  import { version } from './constants.js';
23
- import { assertNoBuiltinShadowing, loadCustomCommands } from './core/custom-command.js';
13
+ import { RmanApplication } from './core/application.js';
14
+ import { commandName, toYargsCommand } from './core/command-builder.js';
15
+ import { assertNoBuiltinShadowing, defaultCommandGlobs, loadCustomCommands, } from './core/custom-command.js';
16
+ import { checkCustomCommand } from './core/plugin-loader.js';
24
17
  import { Repository } from './core/repository.js';
18
+ import { commandRegistry } from './interfaces/rman-config.interface.js';
25
19
  import { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
26
- import { filterPackages, readPackageFilterOptions, readRootOption } from './utils/package-filter.js';
20
+ import { filterPackages, readFromRootOption, readPackageFilterOptions } from './utils/package-filter.js';
27
21
  import { printableConfig } from './utils/printable-config.js';
28
22
  import { runBin } from './utils/run-bin.js';
29
23
  export async function runCli(options) {
@@ -41,7 +35,10 @@ export async function runCli(options) {
41
35
  return;
42
36
  }
43
37
  try {
44
- const repository = await Repository.create(options?.cwd);
38
+ /** One application per run, made here so `--log-level` reaches its logger, and handed to
39
+ * `Repository.create` rather than found through a global. */
40
+ const app = options?.app ?? new RmanApplication();
41
+ const repository = await Repository.create(options?.cwd, { app });
45
42
  const program = yargs(_argv)
46
43
  .scriptName('rman')
47
44
  .version(version)
@@ -81,7 +78,7 @@ export async function runCli(options) {
81
78
  * - but it still has to **throw**, not exit.
82
79
  *
83
80
  * This branch called `process.exit(1)`, and `runCli` is a library entry point: rman's own
84
- * bin calls it, so do `rman-node`'s fixtures and every spec. Exiting from in here took the
81
+ * bin calls it, so do the fixtures and every spec. Exiting from in here took the
85
82
  * whole process down before any caller could see the rejection - which in mocha meant the
86
83
  * first command that failed killed the run and the suite could not report a single result.
87
84
  * The exit belongs to the bin entry alone (see `isMain()` at the bottom), which already does
@@ -99,60 +96,94 @@ export async function runCli(options) {
99
96
  * quietly does nothing on whichever command forgot it is worse than no flag.
100
97
  */
101
98
  interceptConfigFlag(repository, program);
102
- infoCommand.initCli(repository, program);
103
- listCommand.initCli(repository, program);
104
- runCommand.initCli(repository, program);
105
- buildCommand.initCli(repository, program);
106
- changelogCommand.initCli(repository, program);
107
- testCommand.initCli(repository, program);
108
- versionCommand.initCli(repository, program);
109
- githubReleaseCommand.initCli(repository, program);
110
- execCommand.initCli(repository, program);
111
- changedCommand.initCli(repository, program);
112
- diffCommand.initCli(repository, program);
113
- configCommand.initCli(repository, program);
114
- importCommand.initCli(repository, program);
115
99
  /**
116
- * Commands that are not built in, from two places, registered after the built-ins so the clash
117
- * check below has the full list to compare against:
100
+ * **Built-ins come from `commandRegistry`, in the order their modules were imported.** Each
101
+ * entry is a register function that has not run yet - it runs here, with the repository, and
102
+ * returns the command's declaration; `toYargsCommand` is the only thing that knows how a
103
+ * declaration becomes a yargs registration.
118
104
  *
119
- * - **plugins** (`.rmanrc "plugins"`) - a *package* contributing commands, which is how
120
- * everything Node-specific lives outside rman's core;
121
- * - **`.rman/*.mjs`** - this one repository's own commands.
105
+ * The thirteen hand-written `initCli(repository, program)` calls this replaces were the second
106
+ * place a command had to be listed, and the list the shadow check guards with was a third.
107
+ */
108
+ const builtIns = commandRegistry.map(register => register(app));
109
+ for (const meta of builtIns)
110
+ program.command(toYargsCommand(meta));
111
+ /**
112
+ * Commands that are not built in, all from **one** key: `.rmanrc "commands"`, whose default
113
+ * value is `.rman/*.{js,mjs,cjs}`.
114
+ *
115
+ * An entry is a command or a glob naming modules that export one, so a package contributing
116
+ * commands (a built-in's, a plugin's) and a repository writing its own reach yargs by the same path - there
117
+ * is one source of non-built-in commands and one precedence slot. A plugin used to hand them
118
+ * over separately through `addCommand`, which is the step this replaces.
119
+ *
120
+ * Registered after the built-ins so the shadow check below has the full list to compare
121
+ * against. A command taking a *built-in's* name is refused outright.
122
+ */
123
+ const { globs, direct } = commandEntries(repository);
124
+ const { commands: loaded, errors } = await loadCustomCommands(globs);
125
+ /**
126
+ * The two authoring forms, resolved the same way - a declarative one's factory runs here,
127
+ * where `app.repository` exists, rather than when the config was read.
128
+ *
129
+ * **The file name is the fallback for `command`**, which is the convention a command loaded
130
+ * from a *file* has and one written straight into the config has not: `checkCustomCommand`
131
+ * refuses nameless metadata, and an inline command has nothing to fall back on, so it must say
132
+ * its own name. Spread *under* the factory's result, so metadata that does declare one -
133
+ * `deploy <stage>`, with its positionals - wins.
134
+ *
135
+ * The name comes from what the factory *returned*, not from the file: those differ exactly
136
+ * when the metadata declared one, and using the file name would leave the clash check
137
+ * comparing something yargs never registered.
138
+ */
139
+ const localModules = [...direct, ...loaded].map(c => {
140
+ if (!c.register)
141
+ return { name: c.name, file: c.file, module: toCustomModule(c.custom, repository, app) };
142
+ const declared = c.register(app);
143
+ const meta = checkCustomCommand({ ...declared, command: declared.command?.trim() || c.name }, c.file);
144
+ return { name: commandName(meta.command), file: c.file, module: toYargsCommand(meta) };
145
+ });
146
+ /**
147
+ * **One command per name, keeping the last** - which is the precedence that already applied,
148
+ * made visible instead of left to yargs.
122
149
  *
123
- * The repository wins a name clash with a plugin, and silently: it is the more specific
124
- * statement, the same way its own `.rmanrc` overrides an `extends` base. A plugin taking a
125
- * *built-in's* name is still refused outright.
150
+ * A repository's own `.rman/check.mjs` overriding a command its config contributed is the
151
+ * intended escape hatch, and it is deliberately not an error: the same precedence a package's
152
+ * own `.rmanrc` has over an `extends` base. But both were being *registered*, and what that
153
+ * cost was the help output - measured, `rman --help` listed `deploy` twice, once with each
154
+ * description, and nothing said which of the two would run.
155
+ *
156
+ * Only the listing was wrong; the override itself was already clean (`rman deploy --help`
157
+ * showed the winner's options alone, and the loser's flag was rejected), which is why this is
158
+ * a registration fix and not a change to how a clash resolves. Registered later wins because
159
+ * `loaded` follows `direct` - a `.rman/` file after a contributed command - and yargs took the
160
+ * last, so keeping the last keeps today's behaviour exactly.
161
+ *
162
+ * The survivor takes the *loser's* position in the list, since that is where the name was
163
+ * first seen. Help ordering only.
164
+ */
165
+ const byName = new Map();
166
+ for (const command of localModules)
167
+ byName.set(command.name, command);
168
+ const commands = [...byName.values()];
169
+ /**
170
+ * Said out loud, at `verbose`, because deduplicating silently is what would make this the
171
+ * trap it is warned about elsewhere: before, two rows at least hinted that something was
172
+ * doubled - after, the overridden command is simply absent, and "my plugin's command does
173
+ * nothing" has no thread to pull. Not a warning: an override is a correct thing to do, and a
174
+ * repository that does it on purpose should not be nagged on every invocation.
126
175
  */
127
- /** Already loaded: `Repository.create` had to, because a plugin's workspace provider is what
128
- * finds the packages. This is just what it brought back. */
129
- const pluginCommands = repository.pluginCommands;
130
- const { commands: localCommands, errors } = await loadCustomCommands(repository.dirname);
131
- const localNames = new Set(localCommands.map(c => c.name));
132
- const commands = [...pluginCommands.filter(c => !localNames.has(c.name)), ...localCommands];
133
- assertNoBuiltinShadowing(commands, BUILT_IN_COMMANDS);
134
- for (const custom of commands) {
135
- program.command({
136
- command: custom.command,
137
- describe: custom.describe,
138
- /** Forwarded, not dropped: this loop builds a *new* spec object, so anything the command
139
- * declared and is not copied here is silently lost - `--config` printed the whole config
140
- * for every plugin command until this line existed (measured, on `clean` and `ci`). */
141
- configKeys: custom.configKeys,
142
- builder: custom.builder ?? (y => y),
143
- handler: args => {
144
- /** Resolved per invocation, not once at registration: `--log-level` is only known now. */
145
- const logLevel = args.logLevel ?? resolveRootLogLevel(repository);
146
- const context = {
147
- repository,
148
- package: repository.currentPackage,
149
- runBin: (bin, argv, opts) => runBin(bin, argv, { cwd: repository.dirname, logLevel, ...opts }),
150
- logger: new Logger(logLevel),
151
- };
152
- return custom.handler(context, args);
153
- },
154
- });
176
+ if (commands.length < localModules.length) {
177
+ const logger = new Logger(argvLogLevel(_argv) ?? resolveRootLogLevel(repository));
178
+ const survivors = new Set(commands);
179
+ for (const lost of localModules.filter(c => !survivors.has(c))) {
180
+ const winner = byName.get(lost.name);
181
+ logger.verbose(`"${lost.name}" from ${lost.file} is overridden by ${winner.file}.`);
182
+ }
155
183
  }
184
+ assertNoBuiltinShadowing(commands, builtInNames(builtIns));
185
+ for (const { module } of commands)
186
+ program.command(module);
156
187
  /** Warned about, not thrown: one unparseable file must not take the other commands with it.
157
188
  * Loud enough not to be mistaken for success, and it names the file and the reason - "my
158
189
  * command isn't there" is otherwise a long afternoon. */
@@ -199,6 +230,56 @@ export async function runCli(options) {
199
230
  throw e;
200
231
  }
201
232
  }
233
+ /**
234
+ * Everything `.rmanrc "commands"` declares across the repository, split into the globs to load and
235
+ * the commands written straight into the config.
236
+ *
237
+ * **Collected from the root and from every package**, because `commands` is not a root-level key -
238
+ * a package's own `.rmanrc` may contribute one, and a glob was anchored to that file when it was
239
+ * read. The commands themselves are repository-wide; there is one command list, so a package
240
+ * declaring one is contributing it to the repository. (`plugins` and `publishTargets` cannot work
241
+ * this way: they are read before the packages exist, because a plugin is what finds them.)
242
+ *
243
+ * The cascade means a root-declared entry also appears in each package's resolved config, so the
244
+ * same one arrives many times over. Globs are de-duplicated here and by resolved *file* again in
245
+ * the loader - the second pass is the one that matters, since two different globs can name one
246
+ * file. Inline commands are de-duplicated by identity, which is what the cascade produces.
247
+ *
248
+ * The `.rman/` default is used only when nothing was declared anywhere. Declaring `commands` and
249
+ * still wanting `.rman/` scanned means naming it: the key appends to other layers, not to a
250
+ * built-in fallback, and a default that could never be turned off is not a default.
251
+ */
252
+ function commandEntries(repository) {
253
+ const globs = new Set();
254
+ const seen = new Set();
255
+ const direct = [];
256
+ for (const config of [repository.rootPackage.config, ...repository.getPackages().map(p => p.config)]) {
257
+ const value = config?.commands;
258
+ for (const entry of Array.isArray(value) ? value : value ? [value] : []) {
259
+ if (typeof entry === 'string') {
260
+ if (entry.trim())
261
+ globs.add(entry);
262
+ continue;
263
+ }
264
+ if (!entry || seen.has(entry))
265
+ continue;
266
+ seen.add(entry);
267
+ /** No file to fall back on, so the metadata has to name itself - `checkCustomCommand` says
268
+ * so when it does not. `"commands"` stands in for the file in that message. */
269
+ if (typeof entry === 'function') {
270
+ direct.push({ name: '', file: '"commands"', register: entry });
271
+ }
272
+ else {
273
+ /** `checkCustomCommand` is what refuses a nameless one, with the message that names the
274
+ * omission - reached here rather than at registration so an inline command is checked
275
+ * the same way a plugin's used to be. */
276
+ const custom = checkCustomCommand(entry, '"commands"');
277
+ direct.push({ name: commandName(custom.command), file: '"commands"', custom });
278
+ }
279
+ }
280
+ }
281
+ return { globs: globs.size ? [...globs] : defaultCommandGlobs(repository.dirname), direct };
282
+ }
202
283
  /**
203
284
  * Replaces `program.command` with a version that wraps every handler: with `--config` it prints
204
285
  * what the command would run with and returns, instead of running it.
@@ -279,10 +360,10 @@ function readOptions(args) {
279
360
  }
280
361
  /** The packages the command would act on - `filterPackages` with the command's own options, so
281
362
  * this is the same set the command will compute, `skip` included. Narrowed to the current package
282
- * for a command that scopes by directory, unless `--root` says otherwise. */
363
+ * for a command that scopes by directory, unless `--from-root` says otherwise. */
283
364
  function commandTargets(repository, args) {
284
365
  const current = repository.currentPackage;
285
- if (current && !readRootOption(args))
366
+ if (current && !readFromRootOption(args))
286
367
  return [current];
287
368
  return filterPackages(repository.getPackages(), readPackageFilterOptions(args));
288
369
  }
@@ -312,23 +393,68 @@ function isMain() {
312
393
  }
313
394
  if (isMain())
314
395
  runCli().catch(() => process.exit(1));
315
- /** Every name a built-in command answers to - what a `.rman/*.mjs` command may not take. Kept here
316
- * rather than read back out of yargs (which exposes no such list) and pinned by a test against the
317
- * `command:` strings in `src/commands/*.command.ts`, so adding a command can't quietly leave a
318
- * repository's own able to shadow it. */
319
- const BUILT_IN_COMMANDS = [
320
- 'build',
321
- 'changed',
322
- 'changelog',
323
- 'completion',
324
- 'config',
325
- 'diff',
326
- 'exec',
327
- 'github-release',
328
- 'import',
329
- 'info',
330
- 'list',
331
- 'run',
332
- 'test',
333
- 'version',
334
- ];
396
+ /**
397
+ * Every name a repository's own command may not take - **derived from what was actually
398
+ * registered**, not listed.
399
+ *
400
+ * It used to be a hand-maintained array, because yargs exposes no such list, and a spec had to pin
401
+ * it against the command sources so that adding a command could not quietly leave a repository's
402
+ * own able to shadow it. With the built-ins coming out of `commandRegistry` the list and the
403
+ * registrations cannot disagree: they are the same walk.
404
+ *
405
+ * Aliases count - `ls` is `list`, and shadowing it would be the same mistake. `completion` is
406
+ * yargs' own command rather than one of ours, so it is the one name still written here.
407
+ */
408
+ function builtInNames(metas) {
409
+ const names = metas.flatMap(meta => [commandName(meta.command), ...(meta.aliases ?? [])]);
410
+ return [...names, 'completion'];
411
+ }
412
+ /**
413
+ * A `CustomCommand` - a `.rman/*.mjs` command, or a plugin's written the older way - as the yargs
414
+ * registration it describes. `toYargsCommand` is the same function for a *declarative* command;
415
+ * this is the other authoring form, and both end at one `program.command`.
416
+ *
417
+ * **Every field has to be copied deliberately**, because this builds a new object: anything the
418
+ * command declared and this forgets is silently lost. `--config` printed the whole config for every
419
+ * plugin command until `configKeys` was on this list (measured, on `clean` and `ci`).
420
+ */
421
+ function toCustomModule(custom, repository, app) {
422
+ return {
423
+ command: custom.command,
424
+ describe: custom.describe,
425
+ configKeys: custom.configKeys,
426
+ builder: custom.builder ?? ((y) => y),
427
+ handler: (args) => {
428
+ /** Resolved per invocation, not once at registration: `--log-level` is only known now. */
429
+ const logLevel = args.logLevel ?? resolveRootLogLevel(repository);
430
+ const context = {
431
+ repository,
432
+ package: repository.currentPackage,
433
+ runBin: (bin, argv, opts) => runBin(bin, argv, { cwd: repository.dirname, logLevel, app, ...opts }),
434
+ logger: new Logger(logLevel),
435
+ };
436
+ return custom.handler(context, args);
437
+ },
438
+ };
439
+ }
440
+ /**
441
+ * `--log-level` read straight off argv, for a message printed **before** yargs parses anything.
442
+ *
443
+ * Commands are registered before `parseAsync` runs - registering them is what makes parsing
444
+ * possible - so a diagnostic emitted at registration time cannot come from `args.logLevel`.
445
+ * Measured: the override note in `runCli` was silent for `--log-level verbose` and appeared only
446
+ * when `.rmanrc` said so, which is the one spelling a reader would not reach for first.
447
+ *
448
+ * A peek, not a parser. Both spellings, first match wins, and a value that is not a level is left
449
+ * alone for yargs to reject in its own words - this must not become a second place where an
450
+ * invalid `--log-level` is diagnosed.
451
+ */
452
+ function argvLogLevel(argv) {
453
+ for (let i = 0; i < argv.length; i++) {
454
+ const arg = argv[i];
455
+ const value = arg === '--log-level' ? argv[i + 1] : arg.startsWith('--log-level=') ? arg.slice(12) : undefined;
456
+ if (value && LOG_LEVELS.includes(value))
457
+ return value;
458
+ }
459
+ return undefined;
460
+ }