rman 1.0.12 → 1.2.1

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 (102) hide show
  1. package/README.md +90 -70
  2. package/cli.js +226 -14
  3. package/commands/build.command.js +1 -0
  4. package/commands/changed.command.js +2 -2
  5. package/commands/changelog.command.js +13 -15
  6. package/commands/config.command.js +61 -0
  7. package/commands/diff.command.js +9 -4
  8. package/commands/exec.command.js +2 -8
  9. package/commands/github-release.command.js +1 -0
  10. package/commands/info.command.d.ts +9 -0
  11. package/commands/info.command.js +12 -2
  12. package/commands/run.command.js +5 -8
  13. package/commands/test.command.js +1 -0
  14. package/commands/version.command.js +53 -14
  15. package/constants.js +1 -1
  16. package/core/config.d.ts +265 -17
  17. package/core/config.js +651 -76
  18. package/core/custom-command.d.ts +133 -0
  19. package/core/custom-command.js +99 -0
  20. package/core/extends-config.d.ts +27 -0
  21. package/core/extends-config.js +89 -0
  22. package/core/manifest.d.ts +222 -0
  23. package/core/manifest.js +150 -0
  24. package/core/merge-config.d.ts +70 -0
  25. package/core/merge-config.js +193 -0
  26. package/core/package.d.ts +73 -7
  27. package/core/package.js +86 -24
  28. package/core/plugin.d.ts +112 -0
  29. package/core/plugin.js +189 -0
  30. package/core/repository.d.ts +91 -1
  31. package/core/repository.js +277 -132
  32. package/core/resolve-target.d.ts +12 -0
  33. package/core/resolve-target.js +33 -0
  34. package/core/run-step.d.ts +75 -0
  35. package/core/run-step.js +1 -0
  36. package/core/version-scheme.d.ts +134 -0
  37. package/core/version-scheme.js +148 -0
  38. package/core/workspace.d.ts +68 -0
  39. package/core/workspace.js +83 -0
  40. package/index.d.ts +55 -8
  41. package/index.js +42 -7
  42. package/interfaces/rman-config.interface.d.ts +222 -46
  43. package/package.json +16 -7
  44. package/services/change-hash.service.d.ts +88 -0
  45. package/services/change-hash.service.js +112 -0
  46. package/services/changelog.service.d.ts +8 -13
  47. package/services/changelog.service.js +12 -11
  48. package/services/conventional-commits.service.d.ts +73 -0
  49. package/services/conventional-commits.service.js +116 -0
  50. package/services/docker-publish.service.js +1 -1
  51. package/services/exec.service.js +1 -1
  52. package/services/github-release.service.d.ts +2 -2
  53. package/services/github-release.service.js +10 -5
  54. package/services/list.service.js +5 -2
  55. package/services/run.service.d.ts +112 -6
  56. package/services/run.service.js +265 -89
  57. package/services/system-info.d.ts +22 -7
  58. package/services/system-info.js +8 -23
  59. package/services/version-plan.service.d.ts +244 -0
  60. package/services/version-plan.service.js +414 -0
  61. package/services/version.service.d.ts +102 -82
  62. package/services/version.service.js +226 -434
  63. package/services.d.ts +5 -3
  64. package/services.js +5 -3
  65. package/utils/bin-path.d.ts +59 -0
  66. package/utils/bin-path.js +82 -0
  67. package/utils/child-tracker.d.ts +16 -0
  68. package/utils/child-tracker.js +30 -0
  69. package/utils/exec.d.ts +13 -2
  70. package/utils/exec.js +17 -17
  71. package/utils/git.d.ts +9 -3
  72. package/utils/git.js +10 -2
  73. package/utils/package-filter.d.ts +33 -2
  74. package/utils/package-filter.js +47 -7
  75. package/utils/printable-config.d.ts +15 -0
  76. package/utils/printable-config.js +42 -0
  77. package/utils/release-version.js +3 -3
  78. package/utils/run-bin.d.ts +46 -0
  79. package/utils/run-bin.js +63 -0
  80. package/utils/version-stamp.d.ts +14 -6
  81. package/utils/version-stamp.js +25 -13
  82. package/commands/ci.command.js +0 -30
  83. package/commands/clean.command.d.ts +0 -3
  84. package/commands/clean.command.js +0 -36
  85. package/commands/publish.command.d.ts +0 -3
  86. package/commands/publish.command.js +0 -225
  87. package/rmanrc.schema.json +0 -392
  88. package/services/ci.service.d.ts +0 -40
  89. package/services/ci.service.js +0 -204
  90. package/services/clean.service.d.ts +0 -42
  91. package/services/clean.service.js +0 -226
  92. package/services/publish.service.d.ts +0 -79
  93. package/services/publish.service.js +0 -273
  94. package/utils/change-hash.d.ts +0 -68
  95. package/utils/change-hash.js +0 -98
  96. package/utils/conventional-commits.d.ts +0 -52
  97. package/utils/conventional-commits.js +0 -90
  98. package/utils/npm-run-path.d.ts +0 -67
  99. package/utils/npm-run-path.js +0 -63
  100. package/utils/workspace-range.d.ts +0 -17
  101. package/utils/workspace-range.js +0 -28
  102. /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
package/README.md CHANGED
@@ -11,7 +11,7 @@ packages, computing semantic version bumps from your commit history, publishing,
11
11
  importing external repos with history intact, and more - all driven by a single, cascading
12
12
  `.rmanrc`/`.rmanrc.yml` config.
13
13
 
14
- Every command is also available as a **programmatic API** - see [docs/api.md](docs/api.md) if you
14
+ Every command is also available as a **programmatic API** - see [docs/rman.md](https://github.com/panates/rman/blob/main/docs/rman.md) if you
15
15
  want to call `rman`'s logic directly from a Node.js script instead of shelling out to the CLI.
16
16
 
17
17
  ## Table of contents
@@ -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
69
+ # Publish everything that isn't already on the registry (needs rman-node)
70
70
  rman publish
71
71
  ```
72
72
 
@@ -75,7 +75,7 @@ Run any command with `--help` for its full option list (`rman version --help`, `
75
75
  ## Commands
76
76
 
77
77
  The summaries and examples below cover the common cases. For the full option list, defaults, and
78
- worked examples of every single command, see **[docs/cli.md](docs/cli.md)**.
78
+ worked examples of every single command, see **[docs/cli-rman.md](https://github.com/panates/rman/blob/main/docs/cli-rman.md)**.
79
79
 
80
80
  | Command | Purpose |
81
81
  | --- | --- |
@@ -85,23 +85,34 @@ worked examples of every single command, see **[docs/cli.md](docs/cli.md)**.
85
85
  | [`build`](#rman-build) | Alias for `run build`. |
86
86
  | [`test`](#rman-test) | Alias for `run test`. |
87
87
  | [`exec <command..>`](#rman-exec-command) | Runs an arbitrary shell command in each package. |
88
- | [`ci`](#rman-ci) | Deletes `node_modules`/lockfiles everywhere, then reinstalls from scratch. |
89
- | [`clean`](#rman-clean) | Removes compiled TypeScript output and configured extra files/dirs. |
88
+ | [`config`](#rman-config) | Prints the effective `.rmanrc` config for the current directory's package. |
90
89
  | [`changed`](#rman-changed) | Shows which packages the next `version` run would bump. |
91
90
  | [`diff [package]`](#rman-diff-package) | Shows the git diff since a package's (or the repo's) last release tag. |
92
91
  | [`changelog`](#rman-changelog) | Generates a changelog per package from unreleased commits. |
93
92
  | [`version [bump]`](#rman-version-bump) | Bumps versions of changed packages (and their dependents). |
94
- | [`publish`](#rman-publish) | Publishes every package to its configured target(s) - npm, Docker and/or GitHub Releases. |
93
+ | [`github-release`](#rman-github-release) | Creates the repository's GitHub Release for its release tag. |
95
94
  | [`import <path>`](#rman-import-path) | Imports an external git repository as a new package, with history. |
96
95
 
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:
99
+
100
+ ```yaml
101
+ plugins: ['rman-node']
102
+ ```
103
+
97
104
  Options shared across several commands:
98
105
 
99
- - **Package filtering** (`list`, `run`/`build`/`test`, `exec`, `ci`, `clean`, `version`, `publish`,
100
- `changelog`): `--scope <glob>`, `--ignore <glob>`, `--deps`, `--dependents` - see
101
- [Package filtering](docs/api.md#package-filtering-scopeignoredepsdependents) for the full
102
- semantics.
106
+ - **`skip`** (`.rmanrc`, per package): leave this package alone - honoured by every command that
107
+ *acts* on packages, ignored by `list`, which reports on them.
108
+ - **Package filtering** (`list`, `run`/`build`/`test`, `exec`, `version`, `changelog`, and a
109
+ plugin's own commands): `--scope <glob>`, `--ignore <glob>`, `--deps`, `--dependents` - see
110
+ [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 -
113
+ `run`/`build`/`test`, `exec`, `changelog`, `diff`): run against the whole repository instead.
103
114
  - **Branch guard** (every command that mutates state or runs scripts - `run`/`build`/`test`,
104
- `exec`, `ci`, `clean`, `version`, `publish`): `--allow-branch <glob>`, `--ignore-branch <glob>` -
115
+ `exec`, `version`): `--allow-branch <glob>`, `--ignore-branch <glob>` -
105
116
  refuses to run unless (or if) the current git branch matches, the same idea as GitHub Actions'
106
117
  own `branches`/`branches-ignore` workflow filters.
107
118
 
@@ -123,8 +134,9 @@ rman list --scope '@myorg/*' --ignore '*-internal'
123
134
 
124
135
  ### `rman info`
125
136
 
126
- Prints local environment (OS/CPU/memory, Node + whichever package manager `.rmanrc
127
- "packageManager"` configures, git) and repository information.
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.
128
140
 
129
141
  ```bash
130
142
  rman info
@@ -148,7 +160,7 @@ rman run build --scope pkg-a --deps # pkg-a plus everything it depends on
148
160
 
149
161
  Per-package/script behavior (pre/post hooks, `if` conditions, skip, concurrency, ...) is
150
162
  configurable via `.rmanrc run.<script>.*` - see [Configuration](#configuration) below and the full
151
- writeup in [docs/api.md](docs/api.md#runservice).
163
+ writeup in [docs/rman.md](https://github.com/panates/rman/blob/main/docs/rman.md#runservice).
152
164
 
153
165
  ### `rman build`
154
166
 
@@ -177,25 +189,16 @@ rman exec --scope pkg-a -- ls -la
177
189
  rman exec --topo=false pwd # every package independently, alphabetical order
178
190
  ```
179
191
 
180
- ### `rman ci`
181
-
182
- Deletes `node_modules` and any lockfile in every package (or runs the package's own `"ci"` script
183
- instead, if it defines one), then installs once at the root.
184
-
185
- ```bash
186
- rman ci
187
- rman ci --package-manager pnpm
188
- ```
189
-
190
- ### `rman clean`
192
+ ### `rman config`
191
193
 
192
- Removes compiled TypeScript output (`.js`/`.js.map`/`.d.ts` under `src`/`test`, plus any
193
- `*.tsbuildinfo`) and whatever `.rmanrc clean.include`/`clean.exclude` configures. Never touches
194
- `node_modules` - that's `ci`'s job.
194
+ 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
196
+ applied. What rman actually sees there, which no single file shows.
195
197
 
196
198
  ```bash
197
- rman clean
198
- rman clean --dry-run # preview what would be removed
199
+ rman config # the package you are standing in
200
+ rman config --root # the repository root's own config instead
201
+ rman config --json | jq .run
199
202
  ```
200
203
 
201
204
  ### `rman changed`
@@ -260,36 +263,9 @@ feat: needs to ship right now
260
263
  Release-As: patch
261
264
  ```
262
265
 
263
- See [docs/api.md#versionservice](docs/api.md#versionservice) for the full grouping/propagation
266
+ See [docs/rman.md#versionservice](https://github.com/panates/rman/blob/main/docs/rman.md#versionservice) for the full grouping/propagation
264
267
  algorithm, prerelease semantics, and `"workspace:"` dependency-range handling.
265
268
 
266
- ### `rman publish`
267
-
268
- Publishes every package to its configured registry - `npm` by default, or whatever each package's
269
- own `.rmanrc "publish.target"` says (`"npm"`, `"docker"`, or both). Each target decides for itself
270
- whether the current version is already out there: `npm view` on the npm side, `docker manifest
271
- inspect` on the docker side.
272
-
273
- ```bash
274
- rman publish # show the plan, then ask for confirmation
275
- rman publish --yes # publish immediately, no confirmation
276
- rman publish --dry-run # only show the plan, never publish
277
- rman publish --access public # required for a new scoped package
278
- rman publish --tag next
279
- rman publish --otp 123456
280
- rman publish --registry https://registry.example.com --userconfig ./ci.npmrc
281
- rman publish --package-manager pnpm
282
- rman publish --target docker # only the packages configured for the "docker" target
283
- ```
284
-
285
- A `"workspace:*"`/`"workspace:^"`/`"workspace:~"` dependency range is automatically rewritten to a
286
- real, registry-consumable range immediately before each package's publish, and restored right
287
- after - see [docs/api.md#publishservice](docs/api.md#publishservice).
288
-
289
- A package opts into building/pushing a Docker image via `.rmanrc "publish.target": ["docker"]` plus
290
- a `"publish.docker"` block (`image`, `platforms`, `buildContexts`, `buildArgs`, ...) - see
291
- [docs/cli/publish.md#docker-publishing-publishdocker](docs/cli/publish.md#docker-publishing-publishdocker).
292
-
293
269
  ### `rman github-release`
294
270
 
295
271
  Creates the repository's GitHub Release for the version that just shipped - one per run, named after
@@ -302,7 +278,7 @@ rman github-release --yes
302
278
  It is deliberately neither a `publish.target` nor opt-in: a release isn't a registry a package ships
303
279
  to, it's the repository's own record that a version shipped, and every repository wants that record.
304
280
  It needs no configuration at all - see
305
- [docs/cli/github-release.md](docs/cli/github-release.md).
281
+ [docs/cli/github-release.md](https://github.com/panates/rman/blob/main/docs/cli/github-release.md).
306
282
 
307
283
  ### `rman import <path>`
308
284
 
@@ -318,6 +294,51 @@ rman import ../my-old-standalone-repo --dest libs # under libs/ instead of pac
318
294
  already. After importing, add the new directory to your `workspaces` glob if it isn't already
319
295
  covered, then run `rman ci` to install it.
320
296
 
297
+ ## Shared config (`extends`) and appending (`+key`)
298
+
299
+ House rules live in one package, and a repository names it:
300
+
301
+ ```yaml
302
+ # .rmanrc.yml
303
+ extends: '@panates/rman-monorepo'
304
+
305
+ '[*]':
306
+ run:
307
+ build:
308
+ +before: 'rm -rf ./cache' # adds to the base's step, rather than replacing it
309
+ ```
310
+
311
+ `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).
315
+
316
+ ## Your own commands
317
+
318
+ A module in `.rman/` at the repository root becomes an `rman` command:
319
+
320
+ ```js
321
+ // .rman/deploy.mjs
322
+ import { defineCommand, PublishService } from 'rman';
323
+
324
+ export default defineCommand({
325
+ describe: 'Ships what was just published to the staging cluster',
326
+ builder: y => y.option('stage', { choices: ['dev', 'prod'], demandOption: true }),
327
+ async handler({ repository }, args) {
328
+ const plan = await PublishService.getPlan(repository);
329
+ console.log(plan.filter(e => e.status === 'publish').length, '->', args.stage);
330
+ },
331
+ });
332
+ ```
333
+
334
+ ```bash
335
+ rman deploy --stage prod
336
+ ```
337
+
338
+ It gets its own `--help` entry, its own options, and the `Repository` handed to it. For a shell step
339
+ across every package, reach for `.rmanrc "run.<script>"` instead - see
340
+ [docs/cli/custom-commands.md](https://github.com/panates/rman/blob/main/docs/cli/custom-commands.md) for where the line falls.
341
+
321
342
  ## Configuration
322
343
 
323
344
  `rman` reads config cascaded from the repository root down to each package's own directory (the
@@ -373,25 +394,24 @@ version:
373
394
  { "group": "plugins" }
374
395
  ```
375
396
 
376
- See [docs/api.md#configuration-rmanrc-rmanrcyml](docs/api.md#configuration-rmanrc-rmanrcyml) for the
397
+ See [docs/rman.md#configuration-rmanrc--rmanrcyml](https://github.com/panates/rman/blob/main/docs/rman.md#configuration-rmanrc--rmanrcyml) for the
377
398
  full key reference (every `run.<script>.*` sub-key, `clean.*`, `changelog.*`, selector precedence,
378
399
  and which keys are root-level-only today).
379
400
 
380
- **Editor autocomplete:** `rman` ships a JSON Schema for `.rmanrc`/`.rmanrc.yml` at
381
- `rman/rmanrc.schema.json` - add `"$schema": "./node_modules/rman/rmanrc.schema.json"` to your
382
- `.rmanrc` (or the equivalent `# yaml-language-server: $schema=...` comment in `.rmanrc.yml`) to get
383
- autocomplete and validation in VS Code/WebStorm. For a `.rmanrc.cjs`/`.mjs`/`.js` config, wrap it in
384
- the exported `defineConfig()` helper instead for the same autocomplete via the `RmanConfig` type:
401
+ **Editor autocomplete** comes from the `RmanConfig` type, so it applies to the JS forms of the
402
+ config - wrap a `.rmanrc.cjs`/`.mjs`/`.js` in the exported `defineConfig()` helper (or annotate it
403
+ with `/** @type {import('rman').RmanConfig} */`):
385
404
 
386
405
  ```js
387
406
  // .rmanrc.mjs
388
407
  import { defineConfig } from 'rman';
389
- export default defineConfig({ packageManager: 'pnpm' });
408
+ export default defineConfig({ allowBranch: ['main'] });
390
409
  ```
391
410
 
392
- See
393
- [docs/api.md#editor-support-json-schema](docs/api.md#editor-support-json-schema) for details,
394
- including a WebStorm setup that needs no changes to the config file itself.
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
413
+ [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
+ used to cover them was removed.
395
415
 
396
416
  ## Programmatic API
397
417
 
@@ -408,7 +428,7 @@ await VersionService.applyPlan(repository, plan, { changelog: true, push: true }
408
428
 
409
429
  Full reference, with detailed examples for every service (`VersionService`, `PublishService`,
410
430
  `ChangelogService`, `RunService`, `CiService`, `CleanService`, `ExecService`, `ListService`,
411
- `ImportService`, `SystemInfo`) and the `Repository`/`Package` core classes: **[docs/api.md](docs/api.md)**.
431
+ `ImportService`, `SystemInfo`) and the `Repository`/`Package` core classes: **[docs/rman.md](https://github.com/panates/rman/blob/main/docs/rman.md)**.
412
432
 
413
433
  ## Node compatibility
414
434
 
package/cli.js CHANGED
@@ -1,27 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from 'node:fs';
3
+ import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import colors from 'ansi-colors';
5
- import yargs from 'yargs';
6
+ import * as yaml from 'js-yaml';
7
+ import yargs, {} from 'yargs';
6
8
  import { hideBin } from 'yargs/helpers';
7
9
  import * as buildCommand from './commands/build.command.js';
8
10
  import * as changedCommand from './commands/changed.command.js';
9
11
  import * as changelogCommand from './commands/changelog.command.js';
10
- import * as ciCommand from './commands/ci.command.js';
11
- import * as cleanCommand from './commands/clean.command.js';
12
+ import * as configCommand from './commands/config.command.js';
12
13
  import * as diffCommand from './commands/diff.command.js';
13
14
  import * as execCommand from './commands/exec.command.js';
14
15
  import * as githubReleaseCommand from './commands/github-release.command.js';
15
16
  import * as importCommand from './commands/import.command.js';
16
17
  import * as infoCommand from './commands/info.command.js';
17
18
  import * as listCommand from './commands/list.command.js';
18
- import * as publishCommand from './commands/publish.command.js';
19
19
  import * as runCommand from './commands/run.command.js';
20
20
  import * as testCommand from './commands/test.command.js';
21
21
  import * as versionCommand from './commands/version.command.js';
22
22
  import { version } from './constants.js';
23
+ import { assertNoBuiltinShadowing, loadCustomCommands } from './core/custom-command.js';
23
24
  import { Repository } from './core/repository.js';
24
- import { LOG_LEVELS } from './utils/logger.js';
25
+ import { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
26
+ import { filterPackages, readPackageFilterOptions, readRootOption } from './utils/package-filter.js';
27
+ import { printableConfig } from './utils/printable-config.js';
28
+ import { runBin } from './utils/run-bin.js';
25
29
  export async function runCli(options) {
26
30
  try {
27
31
  const repository = await Repository.create(options?.cwd);
@@ -37,6 +41,12 @@ export async function runCli(options) {
37
41
  describe: 'Default verbosity of the per-step log for run/build/ci (default: info, or .rmanrc "logLevel"; ' +
38
42
  'overridable per-package via .rmanrc run.<script>.logLevel)',
39
43
  choices: LOG_LEVELS,
44
+ })
45
+ .option('config', {
46
+ describe: 'Show what this command would run with - its resolved options, the packages it would act ' +
47
+ 'on, and the .rmanrc keys it reads - and run nothing',
48
+ type: 'boolean',
49
+ default: false,
40
50
  })
41
51
  .showHelpOnFail(false, 'Run with --help for available options')
42
52
  .fail((msg, err) => {
@@ -47,36 +57,218 @@ export async function runCli(options) {
47
57
  ? err.message
48
58
  : '';
49
59
  console.log('\n' + colors.red(text));
50
- throw msg;
60
+ /** A real `Error`, marked `logged` since the text above just went out: yargs hands the
61
+ * reason over as a bare string, and rethrowing that raw made bad argv indistinguishable
62
+ * from success to anything holding the promise - or the shell. */
63
+ const failure = new Error(typeof msg === 'string' && msg ? msg : typeof err?.message === 'string' ? err.message : 'invalid arguments');
64
+ failure.logged = true;
65
+ throw failure;
51
66
  }
52
- else
53
- process.exit(1);
67
+ /**
68
+ * Already printed by whoever threw it (the `logged` convention), so there is nothing to say
69
+ * - but it still has to **throw**, not exit.
70
+ *
71
+ * This branch called `process.exit(1)`, and `runCli` is a library entry point: rman's own
72
+ * bin calls it, so do `rman-node`'s fixtures and every spec. Exiting from in here took the
73
+ * whole process down before any caller could see the rejection - which in mocha meant the
74
+ * first command that failed killed the run and the suite could not report a single result.
75
+ * The exit belongs to the bin entry alone (see `isMain()` at the bottom), which already does
76
+ * it; the shell sees the same code either way.
77
+ */
78
+ const failure = new Error(err?.message || 'command failed');
79
+ failure.logged = true;
80
+ throw failure;
54
81
  });
82
+ /**
83
+ * **`--config` is applied here, once, rather than in every command.** Every command - built-in,
84
+ * a plugin's, a `.rman/*.mjs` one - reaches yargs through `program.command`, so wrapping that
85
+ * one method is what makes the flag genuinely global. Adding an option to each command instead
86
+ * would have meant twelve edits plus a rule for plugin authors to remember, and a flag that
87
+ * quietly does nothing on whichever command forgot it is worse than no flag.
88
+ */
89
+ interceptConfigFlag(repository, program);
55
90
  infoCommand.initCli(repository, program);
56
91
  listCommand.initCli(repository, program);
57
92
  runCommand.initCli(repository, program);
58
93
  buildCommand.initCli(repository, program);
59
- ciCommand.initCli(repository, program);
60
- cleanCommand.initCli(repository, program);
61
94
  changelogCommand.initCli(repository, program);
62
95
  testCommand.initCli(repository, program);
63
96
  versionCommand.initCli(repository, program);
64
- publishCommand.initCli(repository, program);
65
97
  githubReleaseCommand.initCli(repository, program);
66
98
  execCommand.initCli(repository, program);
67
99
  changedCommand.initCli(repository, program);
68
100
  diffCommand.initCli(repository, program);
101
+ configCommand.initCli(repository, program);
69
102
  importCommand.initCli(repository, program);
103
+ /**
104
+ * Commands that are not built in, from two places, registered after the built-ins so the clash
105
+ * check below has the full list to compare against:
106
+ *
107
+ * - **plugins** (`.rmanrc "plugins"`) - a *package* contributing commands, which is how
108
+ * everything Node-specific lives outside rman's core;
109
+ * - **`.rman/*.mjs`** - this one repository's own commands.
110
+ *
111
+ * The repository wins a name clash with a plugin, and silently: it is the more specific
112
+ * statement, the same way its own `.rmanrc` overrides an `extends` base. A plugin taking a
113
+ * *built-in's* name is still refused outright.
114
+ */
115
+ /** Already loaded: `Repository.create` had to, because a plugin's workspace provider is what
116
+ * finds the packages. This is just what it brought back. */
117
+ const pluginCommands = repository.pluginCommands;
118
+ const { commands: localCommands, errors } = await loadCustomCommands(repository.dirname);
119
+ const localNames = new Set(localCommands.map(c => c.name));
120
+ const commands = [...pluginCommands.filter(c => !localNames.has(c.name)), ...localCommands];
121
+ assertNoBuiltinShadowing(commands, BUILT_IN_COMMANDS);
122
+ for (const custom of commands) {
123
+ program.command({
124
+ command: custom.command,
125
+ describe: custom.describe,
126
+ /** Forwarded, not dropped: this loop builds a *new* spec object, so anything the command
127
+ * declared and is not copied here is silently lost - `--config` printed the whole config
128
+ * for every plugin command until this line existed (measured, on `clean` and `ci`). */
129
+ configKeys: custom.configKeys,
130
+ builder: custom.builder ?? (y => y),
131
+ handler: args => {
132
+ /** Resolved per invocation, not once at registration: `--log-level` is only known now. */
133
+ const logLevel = args.logLevel ?? resolveRootLogLevel(repository);
134
+ const context = {
135
+ repository,
136
+ package: repository.currentPackage,
137
+ runBin: (bin, argv, opts) => runBin(bin, argv, { cwd: repository.dirname, logLevel, ...opts }),
138
+ logger: new Logger(logLevel),
139
+ };
140
+ return custom.handler(context, args);
141
+ },
142
+ });
143
+ }
144
+ /** Warned about, not thrown: one unparseable file must not take the other commands with it.
145
+ * Loud enough not to be mistaken for success, and it names the file and the reason - "my
146
+ * command isn't there" is otherwise a long afternoon. */
147
+ for (const { file, reason } of errors) {
148
+ console.error(colors.yellow(`Skipped "${path.relative(process.cwd(), file)}": ${reason}`));
149
+ }
70
150
  program.demandCommand(1).strict().recommendCommands().completion();
71
151
  if (!_argv.length)
72
152
  program.showHelp();
153
+ /** Rejects rather than exiting, for the reason the `fail` handler above does - the bin entry
154
+ * turns the rejection into an exit code. */
73
155
  else
74
- await program.parseAsync().catch(() => process.exit(1));
156
+ await program.parseAsync();
75
157
  }
76
158
  catch (e) {
77
- console.error(colors.red(e.message));
159
+ /** Setup failures - no `package.json` to be found, a `.rman` command shadowing a built-in -
160
+ * used to be printed and then swallowed, so the shell saw success: `rman info` in the wrong
161
+ * directory reported failure on stdout and 0 to whatever called it. Printed once (unless the
162
+ * thrower already did, per the `logged` convention) and rethrown, so the exit code agrees
163
+ * with the message. */
164
+ if (!e?.logged)
165
+ console.error(colors.red(e.message));
166
+ throw e;
167
+ }
168
+ }
169
+ /**
170
+ * Replaces `program.command` with a version that wraps every handler: with `--config` it prints
171
+ * what the command would run with and returns, instead of running it.
172
+ *
173
+ * One interception point rather than an option per command - see the call site. It has to go in
174
+ * **before** any command is registered, since it only affects registrations that pass through it.
175
+ */
176
+ function interceptConfigFlag(repository, program) {
177
+ const register = program.command.bind(program);
178
+ program.command = (spec) => {
179
+ if (!spec || typeof spec !== 'object' || typeof spec.handler !== 'function')
180
+ return register(spec);
181
+ const run = spec.handler;
182
+ return register({
183
+ ...spec,
184
+ handler: (args) => (args.config ? printCommandConfig(repository, spec, args) : run(args)),
185
+ });
186
+ };
187
+ }
188
+ /**
189
+ * What a command would run with: its resolved options, the packages it would act on, and the
190
+ * `.rmanrc` those packages carry.
191
+ *
192
+ * The three parts answer the three ways a run surprises someone. **Options** is the parsed argv -
193
+ * what *this invocation* asked for, plus any default yargs itself declares. Most of rman's own
194
+ * defaults are not yargs defaults (`run`'s `bail`/`topo`/`progress` are resolved per package inside
195
+ * `RunService`, from `.rmanrc run.<script>.*`), so an option missing here means "not stated on the
196
+ * command line", and the `.rmanrc` section below is where its value comes from. **Packages** is the
197
+ * set after `--scope`/`--ignore`/
198
+ * `--deps`/`skip`, computed the way the command itself computes it, because a config that is
199
+ * perfect for a package the command never reaches explains nothing. **Config** is narrowed to the
200
+ * keys the command declares it reads (`configKeys` on the command object, which yargs ignores) and
201
+ * is the whole effective config otherwise - the honest answer when nobody has said which half
202
+ * matters.
203
+ */
204
+ function printCommandConfig(repository, spec, args) {
205
+ const name = String(spec.command).split(/\s+/)[0];
206
+ const comment = (text) => (process.stdout.isTTY ? colors.gray(text) : text);
207
+ console.log(comment(`# ${name} --config: nothing was run.`));
208
+ console.log(`command: ${name}`);
209
+ const options = readOptions(args);
210
+ console.log(Object.keys(options).length
211
+ ? `options:\n${indent(yaml.dump(printableConfig(options), { noRefs: true }).trimEnd())}`
212
+ : `options: {}${comment(' # nothing but defaults')}`);
213
+ const targets = commandTargets(repository, args);
214
+ console.log(`packages: ${targets.length ? `[${targets.map(p => p.name).join(', ')}]` : '[]'}`);
215
+ const keys = typeof spec.configKeys === 'function' ? spec.configKeys(args) : spec.configKeys;
216
+ console.log(comment(`# .rmanrc${keys?.length ? `, the keys ${name} reads: ${keys.join(', ')}` : ', in full'}`));
217
+ /**
218
+ * **The root package is always listed, even when it is not a target.** A repo-wide setting is
219
+ * read off the root - `packageManager`, `allowBranch`, `version.*`, `githubRelease.*` - so
220
+ * showing only the targets answered `rman ci --config` with `pkg-a: {}`, which reads as "nothing
221
+ * is configured" about the one key `ci` actually reads (measured).
222
+ */
223
+ const shown = [...targets];
224
+ if (!shown.some(p => p === repository.rootPackage))
225
+ shown.push(repository.rootPackage);
226
+ const config = {};
227
+ for (const pkg of shown)
228
+ config[pkg.name] = keys?.length ? pick(pkg.config, keys) : pkg.config;
229
+ console.log(indent(yaml.dump(printableConfig(config), { noRefs: true, lineWidth: 100 }).trimEnd()));
230
+ if (!targets.includes(repository.rootPackage)) {
231
+ console.log(comment(`# "${repository.rootPackage.name}" is the root - listed because repo-wide keys are read there.`));
78
232
  }
79
233
  }
234
+ /** The parsed argv, minus what yargs adds and minus the camelCase twin of every dashed option -
235
+ * both spellings are the same option, and printing both reads as two settings. */
236
+ function readOptions(args) {
237
+ const dashed = new Set(Object.keys(args).filter(k => k.includes('-')));
238
+ const camel = new Set([...dashed].map(k => k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())));
239
+ const result = {};
240
+ for (const [key, value] of Object.entries(args)) {
241
+ if (key === '_' || key === '$0' || key === 'config' || camel.has(key))
242
+ continue;
243
+ result[key] = value;
244
+ }
245
+ return result;
246
+ }
247
+ /** The packages the command would act on - `filterPackages` with the command's own options, so
248
+ * this is the same set the command will compute, `skip` included. Narrowed to the current package
249
+ * for a command that scopes by directory, unless `--root` says otherwise. */
250
+ function commandTargets(repository, args) {
251
+ const current = repository.currentPackage;
252
+ if (current && !readRootOption(args))
253
+ return [current];
254
+ return filterPackages(repository.getPackages(), readPackageFilterOptions(args));
255
+ }
256
+ /** The named config paths only - `"run.build"` keeps just that subtree, under that path. */
257
+ function pick(config, keys) {
258
+ const result = {};
259
+ for (const key of keys) {
260
+ const value = key.split('.').reduce((node, part) => (node == null ? undefined : node[part]), config);
261
+ if (value !== undefined)
262
+ result[key] = value;
263
+ }
264
+ return result;
265
+ }
266
+ function indent(text) {
267
+ return text
268
+ .split('\n')
269
+ .map(line => (line ? ` ${line}` : line))
270
+ .join('\n');
271
+ }
80
272
  function isMain() {
81
273
  try {
82
274
  return !!process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
@@ -86,4 +278,24 @@ function isMain() {
86
278
  }
87
279
  }
88
280
  if (isMain())
89
- runCli().catch(() => 0);
281
+ runCli().catch(() => process.exit(1));
282
+ /** Every name a built-in command answers to - what a `.rman/*.mjs` command may not take. Kept here
283
+ * rather than read back out of yargs (which exposes no such list) and pinned by a test against the
284
+ * `command:` strings in `src/commands/*.command.ts`, so adding a command can't quietly leave a
285
+ * repository's own able to shadow it. */
286
+ const BUILT_IN_COMMANDS = [
287
+ 'build',
288
+ 'changed',
289
+ 'changelog',
290
+ 'completion',
291
+ 'config',
292
+ 'diff',
293
+ 'exec',
294
+ 'github-release',
295
+ 'import',
296
+ 'info',
297
+ 'list',
298
+ 'run',
299
+ 'test',
300
+ 'version',
301
+ ];
@@ -4,6 +4,7 @@ import { applyRunOptions, readRunOptions } from './run.command.js';
4
4
  export function initCli(repository, program) {
5
5
  program.command({
6
6
  command: 'build',
7
+ configKeys: ['run.build'],
7
8
  describe: 'Alias for "run build"',
8
9
  builder: cmd => applyRunOptions(cmd).example('$0 build', '# Builds packages'),
9
10
  handler: async (args) => {
@@ -1,5 +1,5 @@
1
1
  import colors from 'ansi-colors';
2
- import { VersionService } from '../services/version.service.js';
2
+ import { VersionPlanService } from '../services/version-plan.service.js';
3
3
  import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
4
4
  export function initCli(repository, program) {
5
5
  program.command({
@@ -11,7 +11,7 @@ export function initCli(repository, program) {
11
11
  type: 'boolean',
12
12
  }),
13
13
  handler: async (args) => {
14
- const plan = await VersionService.getPlan(repository, readPackageFilterOptions(args));
14
+ const plan = await VersionPlanService.getPlanner().getPlan(repository, readPackageFilterOptions(args));
15
15
  const changed = plan.filter(e => e.status === 'bump');
16
16
  if (args.json) {
17
17
  console.log(JSON.stringify(changed.map(e => ({ name: e.package.name, group: e.group, from: e.from, to: e.to, reason: e.reason })), undefined, 2));
@@ -1,19 +1,22 @@
1
1
  import colors from 'ansi-colors';
2
+ import { ChangeHashService } from '../services/change-hash.service.js';
2
3
  import { ChangelogService } from '../services/changelog.service.js';
3
4
  import { Logger, resolveRootLogLevel } from '../utils/logger.js';
4
- import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
5
+ import { applyPackageFilterOptions, applyRootOption, readPackageFilterOptions } from '../utils/package-filter.js';
5
6
  export function initCli(repository, program) {
6
7
  program.command({
7
8
  command: 'changelog',
9
+ configKeys: ['changelog', 'publish.skip'],
8
10
  describe: 'Generates a changelog per package from unreleased commits',
9
- builder: cmd => applyPackageFilterOptions(cmd)
11
+ builder: cmd => applyRootOption(applyPackageFilterOptions(cmd), 'Generate')
10
12
  .example('$0 changelog', "# Auto-detects each package's own last release tag (or npm version)")
11
13
  .example('$0 changelog --from <hash> --write', '# Since a specific commit, written to file')
12
14
  .option('from', {
13
15
  describe: 'Generate the changelog since this commit/hash, applied the same way to every package. ' +
14
- 'Default (also "npm" explicitly): auto-detect per package from its own most recent release ' +
15
- 'tag - same as "version"/"changed" - falling back to its published npm version (no tag yet), ' +
16
- "then to its whole history for a package that's never been released at all",
16
+ 'Default (also "auto" explicitly): auto-detect per package from its own most recent release ' +
17
+ 'tag - same as "version"/"changed" - falling back to the version its own ecosystem\'s ' +
18
+ "registry reports (no tag yet), then to its whole history for a package that's never been " +
19
+ 'released at all',
17
20
  type: 'string',
18
21
  })
19
22
  .option('write', {
@@ -24,12 +27,6 @@ export function initCli(repository, program) {
24
27
  describe: "With --write, the file to prepend into, relative to each package's own directory " +
25
28
  '(default: "CHANGELOG.md", or .rmanrc "changelog.filePath")',
26
29
  type: 'string',
27
- })
28
- .option('root', {
29
- alias: 'r',
30
- describe: 'Generate for the whole repository even when the current directory is inside a single ' +
31
- 'package (which otherwise scopes it to just that package). No effect elsewhere.',
32
- type: 'boolean',
33
30
  })
34
31
  .option('include-skipped', {
35
32
  describe: 'Also generate for a package with .rmanrc "publish.skip" - excluded by default',
@@ -46,10 +43,11 @@ export function initCli(repository, program) {
46
43
  const from = args.from;
47
44
  const write = args.write;
48
45
  const logger = new Logger(args.logLevel ?? resolveRootLogLevel(repository));
49
- if (!from || from === 'npm') {
50
- // Auto-detection is mostly local git work, but the npm fallback it can reach for (only
51
- // when a package has no tag at all) is a network round trip per package - without this,
52
- // the command looks hung for that stretch instead of just busy.
46
+ if (!from || from === ChangeHashService.AUTO) {
47
+ // Auto-detection is mostly local git work, but the registry fallback it can reach for
48
+ // (only when a package has no tag at all, and only if the package's own ecosystem provides
49
+ // one) is a network round trip per package - without this, the command looks hung for that
50
+ // stretch instead of just busy.
53
51
  logger.info(colors.gray("Detecting each package's last release..."));
54
52
  }
55
53
  const options = {