rman 1.3.0 → 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 +140 -19
  40. package/core/config.js +258 -74
  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 +137 -92
  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 -44
  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 +725 -202
  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
@@ -1,98 +1,272 @@
1
- import type { RmanPlugin } from '../core/plugin.js';
1
+ import type * as yargs from 'yargs';
2
+ import type { RmanApplication } from '../core/application.js';
3
+ import type { ConfigValue } from '../core/config.js';
4
+ import type { CustomCommand } from '../core/custom-command.js';
5
+ import type { Plugin } from '../core/plugin.js';
6
+ import type { PublishTarget } from '../core/publish-target.js';
2
7
  import type { RunConditionFn, RunStepValue } from '../core/run-step.js';
3
- /**
4
- * Adds a `+key` alongside every key of `T`, which **appends** to whatever that key already resolved
5
- * to instead of replacing it - see `mergeConfig`.
6
- *
7
- * Generated by key remapping rather than written out, so a key added to the interface gets its
8
- * append form automatically and the two can never drift apart.
9
- */
10
- export type WithAppend<T> = {
11
- [K in keyof T as `+${K & string}`]?: T[K];
12
- };
13
- /**
14
- * The one key every nested config node may carry: `vars` scoping that node's subtree - a fresh copy
15
- * per level, merged per key over the level above. See `withScopedVars` in `core/config.ts` for what
16
- * it does at resolution time, and docs/rman.md#scoped-vars for how it reads.
17
- *
18
- * **Every nested options interface extends this**, and a new one has to remember to - which is the
19
- * cost of the runtime rule being general (any object node scopes) while a type can only say it one
20
- * interface at a time. TypeScript has no way to state "and every object below this may also carry
21
- * `vars`" without a recursive remap that would wreck the error messages.
22
- *
23
- * Extended by the `XOptions` interface rather than declared on `XOptionsKeys`, so `WithAppend` does
24
- * not generate a `+vars`: appending to `vars` means nothing, since objects merge either way.
25
- */
26
- export interface ScopedVars {
27
- /** Values for `${{ vars.* }}` to read, for this node and everything under it. */
28
- vars?: Record<string, unknown>;
29
- }
30
8
  /**
31
9
  * The shape of `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` (and `package.json`'s own
32
10
  * `"rman"` key) - see docs/rman.md#configuration-rmanrc-rmanrcyml for the full reference. Every
33
11
  * field is optional and cascades from the repository root down to each package's own directory.
34
- * Purely a typing aid (used by `defineConfig` below, and importable on its own for a `.rmanrc.ts`/
35
- * `.mts` authored config, or a plain `: RmanConfig` annotation) - never read by rman itself, which
36
- * only ever sees the plain JS object a JS config file exports.
12
+ * Purely a typing aid - never read by rman itself, which only ever sees the plain JS object a
13
+ * config file exports.
14
+ *
15
+ * **Two halves, and the split is who declares a key.** `RmanConfigKeys` holds the ones no command
16
+ * owns; `CommandConfigs` is what every command contributes its own into, from beside the command
17
+ * (see `CommandContribution`). This used to be two files and two types both called `RmanConfig` -
18
+ * one for the config shape, one for the command declarations - which is why the package could not
19
+ * export the second at all.
20
+ *
21
+ * **This is the type an *author* writes**, and `ResolvedConfig` - derived from it - is what a
22
+ * *reader* gets. One type could not be both: a value may be written as a **function** here, because
23
+ * rman calls it while the config resolves, and by the time anything reads `pkg.config` it has
24
+ * already been called. Answering only the reader's question is what this type used to do, and it
25
+ * made `docs/rman.md`'s own `changelog: { filePath: ({ vars }) => ... }` a compile error.
26
+ *
27
+ * **The direction matters, and it was measured the wrong way round first.** Widening this type
28
+ * while `Package.config` still used it moved the cast to every **read** - six sites broke
29
+ * (`version.command.ts`, `publish-target.ts`, `changelog.service.ts`, three in
30
+ * `github-release.service.ts`). The widening was right; leaving the reader on the same name was not.
31
+ * So the author's view is the declared one and the reader's is computed: see `Resolved`, which also
32
+ * records the two guards that make a mechanical transform safe here.
33
+ *
34
+ * **Deriving the other direction is what cannot work, and the reason is this name.** `RmanConfig` is
35
+ * also a **namespace** (`RmanConfig.CommandConfigs`, `CommandContribution`, …) that `rman-node`
36
+ * augments; a `type` alias cannot merge with a namespace, and the resulting circular reference
37
+ * resolves to `{}` **silently** - measured, every key then read as "does not exist", `vars` and
38
+ * `run` included. A derived *reader* view needs no such merge, which is why it is the half that is
39
+ * derived.
40
+ *
41
+ * Which keys may be written as a function is not a judgement made key by key: every option a
42
+ * command declares is one, because an option's value is by construction a value rather than a step
43
+ * (`CommandConfigFromMetadata`). Only the hand-written `Extra` keys are decided individually, since
44
+ * that is where a step key lives.
37
45
  */
38
- export interface RmanConfig extends RmanConfigKeys, WithAppend<RmanConfigKeys> {
46
+ export interface RmanConfig extends RmanConfigKeys, RmanConfig.CommandConfigs {
39
47
  /**
40
48
  * Configs to inherit from, merged **underneath** this one - a shared package
41
49
  * (`"@panates/rman-monorepo"`), a relative path, or an array applied in declaration order.
42
50
  *
43
- * A bare name resolves through *this* file's own `node_modules`, so a subpath works too
44
- * (`"@panates/rman-monorepo/strict"`). The target may be YAML, JSON, or a module exporting a
45
- * config via `defineConfig`, and may itself `extends` another.
51
+ * A bare name resolves through *this* file's own `node_modules`, so a subpath works too. The
52
+ * target may be YAML, JSON, or a module exporting a config via `defineConfig`, and may itself
53
+ * `extends` another.
46
54
  *
47
55
  * Top level only: a `"[selector]"` block naming one is an error rather than a no-op, since
48
56
  * inheritance is a statement about this config and not about the packages a selector names.
57
+ *
58
+ * Resolved away long before a reader sees one, and kept on this type because `rman config` prints
59
+ * the object it was resolved from.
49
60
  */
50
61
  extends?: string | string[];
51
62
  }
52
- /** Every setting a config may carry, without the `+key` append forms or `extends` - the shape
53
- * `RmanConfig` is built from, kept separate only so `WithAppend` has something to map over. */
63
+ /**
64
+ * One entry of `.rmanrc "commands"` - a command, or a **glob** naming `.js` modules that
65
+ * `export default` one.
66
+ *
67
+ * Both authoring forms count as "a command": the declarative `app => ({ ... })` factory, which is
68
+ * the one to write, and the older `defineCommand({ ... })` object a `.rman/*.mjs` uses.
69
+ */
70
+ export type CommandEntry = string | CustomCommand | RmanConfig.CommandRegisterFunction;
71
+ /** One entry of `.rmanrc "publishTargets"` - a target, or a **glob** naming `.js` modules that
72
+ * `export default` one. */
73
+ export type PublishTargetEntry = string | PublishTarget;
74
+ /**
75
+ * **The `.rmanrc` keys no command owns**, without `extends` - kept
76
+ * separate from `RmanConfig` only so the two halves stay legible. A key a *command* owns is
77
+ * declared beside that command and arrives through `RmanConfig.CommandConfigs` instead.
78
+ *
79
+ * **A plugin adds its own keys here, by declaration merging** - `rman-node` contributes `clean`
80
+ * from its own `interfaces/rman-config.interface.ts`, so `pkg.config.clean` stays typed wherever it
81
+ * is read without the core having to know npm has a `node_modules` or that TypeScript has build
82
+ * output. (A *target's* block is different again: `publish.npm.*` goes through the
83
+ * `PublishTargetConfigs` slot, beside the core's own `publish.docker.*`.)
84
+ *
85
+ * A config author annotates with the plugin's own name for the union - `RmanNodeConfig` - which is
86
+ * what makes the import that carries the augmentation explicit rather than incidental.
87
+ */
54
88
  export interface RmanConfigKeys {
55
89
  /**
56
- * **A plugin adds its own keys here, by declaration merging** - `rman-node` contributes
57
- * `clean` and `publish.directory` from its own
58
- * `interfaces/rman-config.interface.ts`, so `pkg.config.clean` stays typed wherever it is read
59
- * without the core having to know npm has a `node_modules` or that TypeScript has build output.
60
- * `WithAppend` is a mapped type evaluated at use, so an augmented key gets its `+key` form too.
61
- *
62
- * A config author annotates with the plugin's own name for the union - `RmanNodeConfig` - which
63
- * is what makes the import that carries the augmentation explicit rather than incidental.
90
+ * The technologies this repository holds, in declaration order - how its packages are
91
+ * recognized, where they are, how their versions are planned. One per ecosystem: `rman-node`
92
+ * contributes the `node` one, a Cargo plugin would contribute `cargo`, and a polyglot repository
93
+ * names both.
94
+ *
95
+ * Each entry is **a plugin, or a glob naming `.js` modules that `export default` one** - the same
96
+ * two forms `commands` and `publishTargets` take:
97
+ *
98
+ * ```js
99
+ * // .rmanrc.mjs - the instance, which a JS config can hand over and a YAML one cannot
100
+ * import { defineConfig, definePlugin } from 'rman';
101
+ * export default defineConfig({ plugins: [definePlugin({ name: 'cargo', manifestProvider })] });
102
+ * ```
103
+ *
104
+ * A *published* plugin is not usually named here at all: its package exports a config carrying
105
+ * it, so the repository writes `extends` and the plugin arrives with everything else the package
106
+ * declares. `rman-node` deliberately exports no `NodePlugin` for this reason - there is nothing a
107
+ * consumer needs to do with it by hand.
108
+ *
109
+ * ```yaml
110
+ * # .rmanrc.yml - a glob, anchored to this file's own directory
111
+ * plugins: './plugins/*.js'
112
+ * ```
113
+ *
114
+ * **A package name is not one of the forms, and `plugins: ['rman-node']` is never valid.** That
115
+ * package's entry point exports an rman *config* - `{ plugins, commands, publishTargets }` - and
116
+ * a config's way into a repository is `extends`, the key that means "merge this underneath
117
+ * mine". The two are different statements: `extends: 'rman-node'` inherits everything the
118
+ * package declares, while `plugins` names the technologies themselves. Naming the package here
119
+ * is refused with a message saying so, rather than half-read.
120
+ *
121
+ * A plugin that cannot be loaded is an error, not a skip - silently losing a manifest reader
122
+ * means every package reads as nameless at `0.0.0`, which looks like a working repository.
123
+ *
124
+ * **Root level only, and unlike `commands` this is forced rather than chosen**: `loadPlugins`
125
+ * runs inside `Repository.create` *before* the packages are known, because a plugin's
126
+ * `getWorkspace` is what finds them - so there are no package configs to read. An entry in a
127
+ * package's own `.rmanrc` is never seen.
64
128
  */
129
+ plugins?: string | Plugin | (string | Plugin)[];
65
130
  /**
66
- * Plugins to load, in declaration order - a *package* contributing commands, where `.rman/*.mjs`
67
- * contributes one repository's own.
131
+ * **Which technology a package belongs to**, by `Platform.name`.
132
+ *
133
+ * ```yaml
134
+ * plugins: ['node']
135
+ * "[/]":
136
+ * platform: node
137
+ * ```
138
+ *
139
+ * **`plugins` loads; this selects.** They were one key for a while and that conflated two
140
+ * statements: `plugins` says which technologies this repository has available, which is a fact
141
+ * about the *repository* and is read once at its root. Which technology a given directory's
142
+ * package belongs to is a fact about the *package*, and a repository may hold more than one - so
143
+ * it is an ordinary cascading key, declared wherever the answer changes.
68
144
  *
69
- * Each entry is **either a package name (or path) to import, or a plugin object itself**:
145
+ * - **A declaration wins over the guess.** Without it, a directory belongs to the first
146
+ * registered platform whose manifest provider recognizes it (`app.platformFor`) - which is a
147
+ * reasonable default and is registration order deciding a question about someone's code. Naming
148
+ * it takes the answer away from load order.
149
+ * - **The named platform validates it, and a mismatch is an error.** If it does not recognize the
150
+ * directory the declaration is simply untrue, and the alternative is a package reading as
151
+ * nameless at `0.0.0` - which looks like a working repository. The message names the file the
152
+ * platform looked for.
153
+ * - **It cascades like any unmarked key**, so one line at the root covers a single-technology
154
+ * repository and a package's own `.rmanrc` overrides it for that subtree. `"[/]"` keeps it on
155
+ * the root package alone.
156
+ * - **At the root it also means the repository said something**, so detection does not run - the
157
+ * same rule `plugins: []` follows. A repository that states its technology is not a repository
158
+ * that stated nothing.
159
+ *
160
+ * **At the root, naming a built-in is the same statement as `plugins: ['node']`** - it is put at
161
+ * the *front* of `plugins`, so the whole built-in arrives: the technology, its commands and its
162
+ * publish targets. Saying which technology this repository is *is* saying it has it, and making
163
+ * an author write both was a distinction only rman could see. At the front rather than appended,
164
+ * because `platformFor` takes the first registered platform that recognizes a directory - the one
165
+ * the repository says it *is* should win over whatever a shared config brought along. Read from
166
+ * the unmarked key or from `"[/]"`, since both are the root speaking.
167
+ *
168
+ * **Below the root it loads the technology alone**, and that is structural rather than chosen:
169
+ * `plugins` is read once, at the root, before any package exists, so a nested declaration cannot
170
+ * contribute commands even in principle. It is also what you want there - a Node package inside a
171
+ * Cargo repository wants npm's manifest read, not a repo-wide `rman clean` sweeping the tree.
172
+ *
173
+ * Only a built-in is promoted. A platform rman does not ship is loaded by the `plugins` entry
174
+ * that brings it, and naming one with no such entry is an error listing what the repository has -
175
+ * putting the bare name into `plugins` would hand the loader a glob matching no file, so the
176
+ * failure would read as the plugin being missing while it is registered perfectly well.
177
+ *
178
+ * **A plain string, never a `${{ }}` expression.** This is read while the packages are still
179
+ * being found, so there is no `pkg` for an expression to be about - it is what decides what a
180
+ * package *is*. An expression here is refused rather than passed through as a literal, which is
181
+ * what silently happened until it was.
182
+ */
183
+ platform?: string;
184
+ /**
185
+ * **The selector this package answers to** - what a `"[glob]"` block and `--scope`/`--ignore`
186
+ * match against, overriding whatever its platform said.
70
187
  *
71
188
  * ```yaml
72
- * # .rmanrc.yml - imported by name, resolved through the repository's own node_modules
73
- * plugins: ['rman-node']
189
+ * # packages/web/.rmanrc
190
+ * name: web
74
191
  * ```
75
192
  *
76
- * ```js
77
- * // .rmanrc.mjs - or handed over directly, which a JS config can do and a YAML one cannot
78
- * import { defineConfig, definePlugin } from 'rman';
79
- * export default defineConfig({ plugins: [definePlugin({ name: 'mine', commands: [...] })] });
193
+ * **The escape hatch for an ecosystem that cannot name a package, and only that.** npm can:
194
+ * `package.json#name` exists and identifies the package, so a Node repository never writes this.
195
+ * An ecosystem where a package need not be named, or where the name is an import path nobody
196
+ * would type after `--scope`, leaves `ManifestProvider.selector` unanswered and the repository
197
+ * fills it in here.
198
+ *
199
+ * **It does not rename the package.** `pkg.name` is still what the manifest says, so tags,
200
+ * changelogs and the registry are untouched - this is an address, and `Package.selector` is where
201
+ * it lands.
202
+ *
203
+ * **Unique within the repository, and checked.** Two packages answering to one selector make
204
+ * `"[that]"` and `--scope that` ambiguous, and the ambiguity is silent: the config reaches both
205
+ * and `getPackage` returns whichever came first. The error names both directories.
206
+ *
207
+ * **Cascades like any unmarked key, which for this one is almost always a mistake** - one
208
+ * declaration above two packages gives them the same address, so the uniqueness check fires and
209
+ * says so. Declare it in the package's own `.rmanrc`.
210
+ *
211
+ * **Not inside a `"[glob]"` block**, which is refused rather than ignored: the glob matches the
212
+ * selector, so a block that sets it would need its own answer to find itself. Read from the
213
+ * unmarked cascade for the same reason `platform` is - before the package exists.
214
+ */
215
+ name?: string;
216
+ /**
217
+ * Where this repository keeps command modules of its own - a glob, or a list of them.
218
+ *
219
+ * ```yaml
220
+ * commands: ['tools/commands/*.mjs']
80
221
  * ```
81
222
  *
82
- * The object form is what lets a **plugin package export a config** rather than a single plugin:
83
- * `rman-node`'s entry point is `export default defineConfig({ plugins: [ ... ] })`, so it is an
84
- * `.rmanrc` like any other and is free to grow a second plugin without changing its shape. When an
85
- * imported module exports a config this way, **only its `plugins` are read** - a config's other
86
- * keys reach a repository through `extends`, which is the key that means "merge this underneath
87
- * mine".
223
+ * **Where a command comes from, whoever ships it.** A repository writing one of its own, and a
224
+ * package contributing three, reach yargs by this one key - so there is one source of
225
+ * non-built-in commands and one precedence slot. `.rman/*.{js,mjs,cjs}` is simply this key's
226
+ * default value rather than a second mechanism beside it, and a package needs no plugin to
227
+ * contribute a command: a plugin is a *technology*, and a command is not one.
228
+ *
229
+ * **A relative glob is anchored to the file that declared it**, not to the repository root (see
230
+ * `anchorContributions`), so a shared config can ship commands with `commands: './commands/*.js'`
231
+ * and have it mean its own directory.
232
+ *
233
+ * **Always appends** (`ALWAYS_APPEND`), like the other two contribution keys and for the same
234
+ * reason: naming a directory of your own never means "and stop loading the ones my shared config
235
+ * ships". It follows that a closer layer cannot *un*-say one.
236
+ *
237
+ * **Trap: a glob replaces the `.rman/` default and an instance does not.** The default applies
238
+ * only when *no* glob was declared anywhere - the key appends onto other layers, not onto a
239
+ * built-in fallback - so a shared config shipping its commands by glob silently takes the
240
+ * `.rman/` directory away from every repository inheriting it. Measured both ways on the same
241
+ * pair of repositories: with the shared config declaring instances, the consumer's own
242
+ * `.rman/hello.mjs` is in `rman --help`; with it declaring `'./commands/*.js'`, that command is
243
+ * simply gone. **A config meant to be inherited should therefore list its commands
244
+ * individually.**
245
+ *
246
+ * Declared at any level, unlike `plugins` and `publishTargets`, because by the time this is read
247
+ * the packages exist - so a package's own `.rmanrc` may contribute one. The commands themselves
248
+ * are still repository-wide, because there is one command list; a package declaring one is
249
+ * contributing it to the repository.
250
+ *
251
+ * A module exports either form: the declarative `app => ({ ... })` factory, which is the one to
252
+ * write, or the `defineCommand({ ... })` object. **`.ts` is not loadable** - rman imports these in
253
+ * its own process, with no loader registered - so a TypeScript repository compiles them first or
254
+ * writes them as `.mjs`.
255
+ */
256
+ commands?: CommandEntry | CommandEntry[];
257
+ /**
258
+ * Publish targets this repository has - where a package's artifact ships. An instance, or a glob
259
+ * naming `.js` modules that `export default` one.
88
260
  *
89
- * This is how everything that only means something in a Node repository lives outside rman's
90
- * core. A plugin that cannot be loaded is an error, not a skip: silently losing `rman publish` is
91
- * worse than not starting.
261
+ * The same two forms as `plugins` and `commands`, and for the same reason: a target is a
262
+ * contribution, so a config declares it rather than a plugin registering it by hand. rman's own
263
+ * `docker` target is built in; `rman-node` contributes `npm` from its own config.
92
264
  *
93
- * Root level only - which commands exist is a property of the repository, not of a package.
265
+ * Always appends. **Root level only, like `plugins` and for the same mechanical reason** - both
266
+ * are read by `loadPlugins` inside `Repository.create`, before there is a package whose config
267
+ * could be consulted. `commands` is the one of the three that is read at any level.
94
268
  */
95
- plugins?: string | RmanPlugin | (string | RmanPlugin)[];
269
+ publishTargets?: PublishTargetEntry | PublishTargetEntry[];
96
270
  /**
97
271
  * Values for `${{ vars.* }}` to read - a name for something the config would otherwise repeat:
98
272
  *
@@ -132,10 +306,6 @@ export interface RmanConfigKeys {
132
306
  */
133
307
  skip?: boolean;
134
308
  group?: boolean | string;
135
- version?: RmanConfig.VersionOptions;
136
- changelog?: RmanConfig.ChangelogOptions;
137
- publish?: RmanConfig.PublishOptions;
138
- githubRelease?: RmanConfig.GithubReleaseOptions;
139
309
  /** Keyed by npm script name (e.g. `"build"`, `"lint"`, `"test"`). A bare string (or array of
140
310
  * them) is shorthand for `{ exec: ... }` - `test: "mocha"` and `test: { exec: "mocha" }` mean
141
311
  * exactly the same thing, and a bare function is the same shorthand for a function step. */
@@ -155,11 +325,11 @@ export interface RmanConfigKeys {
155
325
  * **A list, and only a list.** It used to accept a `Record<string, string>` too, documented as "an
156
326
  * explicit name -> range map" - and the ranges went nowhere: the one reader took `Object.keys` and
157
327
  * dropped the values. Nor could they ever mean anything here, since the cascade works from groups
158
- * and severities, and `ManifestProvider.updateDependencyVersions` rewrites ranges in the
328
+ * and severities, and `Plugin.updateDependencyVersions` rewrites ranges in the
159
329
  * *manifest* - a range declared only in `.rmanrc` has no file to be written to. What this key
160
330
  * states is an **edge**, and an edge needs two ends and nothing else.
161
331
  *
162
- * **Core, and it has to be**: it is layered on top of whatever `ManifestProvider.dependencies`
332
+ * **Core, and it has to be**: it is layered on top of whatever `Plugin.readDependencies`
163
333
  * read, and it is the *only* way a repository with no provider at all has a graph - a repo whose
164
334
  * manifests rman cannot read can still state its edges by hand. Moving it to an ecosystem plugin
165
335
  * would take that away from exactly the repositories that need it.
@@ -198,73 +368,355 @@ export interface RmanConfigKeys {
198
368
  */
199
369
  [selector: `[${string}]`]: RmanConfig;
200
370
  }
371
+ export declare const commandRegistry: RmanConfig.CommandRegisterFunction[];
372
+ /**
373
+ * The one key every nested config node may carry: `vars` scoping that node's subtree - a fresh copy
374
+ * per level, merged per key over the level above. See `withScopedVars` in `core/config.ts` for what
375
+ * it does at resolution time, and docs/rman.md#scoped-vars for how it reads.
376
+ *
377
+ * **Every nested options interface has to carry this**, and a hand-written one had to remember to -
378
+ * which is the cost of the runtime rule being general (any object node scopes) while a type can only
379
+ * say it one interface at a time. TypeScript has no way to state "and every object below this may
380
+ * also carry `vars`" without a recursive remap that would wreck the error messages. A command's
381
+ * contributed block gets it from `RmanConfig.ConfigBlock` and cannot forget.
382
+ */
383
+ export interface ScopedVars {
384
+ /** Values for `${{ vars.* }}` to read, for this node and everything under it. */
385
+ vars?: Record<string, unknown>;
386
+ }
201
387
  export declare namespace RmanConfig {
202
388
  /**
203
- * One `version.stamp` entry: a path, or a path plus the identifier that holds the version.
389
+ * **The slot every command merges its own keys into.** Empty here on purpose: the core declares
390
+ * no command's config, each command declares its own beside the command itself, and a plugin's
391
+ * commands do the same from their own package.
392
+ *
393
+ * A command augments it with its derived contribution rather than a hand-written interface, so
394
+ * the option list and the config type cannot drift apart:
204
395
  *
205
- * The object form exists because the identifier was unnameable - only the exact lowercase word
206
- * `version` was ever matched, so a Go `const Version`, a Python `__version__` and a plain
207
- * `appVersion` were all silently skipped. What the identifier *means* is the ecosystem's
208
- * (`ManifestProvider.stampVersion`); which file holds one is the repository's, which is why it is
209
- * here.
396
+ * ```ts
397
+ * const versionCommand = registerCommand(...);
398
+ * export default versionCommand;
399
+ *
400
+ * declare module '../interfaces/rman-config.interface.js' {
401
+ * namespace RmanConfig {
402
+ * interface CommandConfigs extends CommandContribution<ReturnType<typeof versionCommand>> {}
403
+ * }
404
+ * }
405
+ * ```
406
+ *
407
+ * **Two commands may not contribute under the same top-level key.** Interface merging is not a
408
+ * deep merge: `{run: {build: ...}}` and `{run: {test: ...}}` is
409
+ * `Interface 'CommandConfigs' cannot simultaneously extend types ...` - measured. That is the
410
+ * mechanical reason `build` and `test` own nothing and only *read* `run.build`/`run.test`; `run`
411
+ * is the single owner of that subtree.
210
412
  */
211
- type VersionStampEntry = string | {
212
- file: string;
213
- constant?: string;
214
- };
215
- interface VersionOptions extends VersionOptionsKeys, WithAppend<VersionOptionsKeys>, ScopedVars {
413
+ interface CommandConfigs {
216
414
  }
217
- interface VersionOptionsKeys {
218
- commitMessage?: string;
219
- /** Default for `version --changelog` when the CLI flag isn't given - a standing "always fold
220
- * the changelog into the version-bump commit" policy, rather than something that behaves
221
- * differently on the one run someone forgets to pass `--changelog`. An explicit `--changelog`/
222
- * `--no-changelog` on the command line still wins either way. Root-level only. Default `false`. */
223
- changelog?: boolean;
224
- /** Tag naming the repository's own release, as opposed to the per-package/group tags
225
- * `changelog.tagPattern` names - only created when the root is on a calendar version (a repo
226
- * with more than one version line). Root-level only. Default `"release-*"`. Must **not** match
227
- * any package's own `changelog.tagPattern`, or that package's changelog boundary will resolve
228
- * to the repository release instead of its own last release. */
229
- releaseTagPattern?: string;
230
- /** Keep this package's Dockerfile `org.opencontainers.image.version` label in step with the
231
- * version being written. Per-package cascaded. Default `true` - the label's value is, by
232
- * specification, the version of the packaged software, so there is only ever one correct
233
- * value for it, and `version` is what knows it. Only ever *rewrites* a label the Dockerfile
234
- * already declares (never inserts one), and reads the same path `publish --target docker`
235
- * builds from (`publish.docker.dockerfile`), so a package without one is a no-op. */
236
- stampDockerfile?: boolean;
237
- /** Files whose hard-coded version is rewritten to the version being written, in the same
238
- * commit as the bump - paths relative to the package's own directory (e.g.
239
- * `["src/constants.ts"]`). Per-package cascaded; a listed file a package doesn't have is a
240
- * silent no-op, so one `"[*]"` declaration covers a repo where only some packages carry one.
241
- *
242
- * Stamping the source, not the build output: a build-time rewrite leaves the checked-in file
243
- * claiming a placeholder, so anything running from source reports that placeholder, git never
244
- * records the released version, and the rewrite has to be redone on every build. */
245
- stamp?: VersionStampEntry | VersionStampEntry[];
246
- /** Command(s) run at the version write itself, when the package does not declare a hook for
247
- * that slot of its own (`version` in a Node repository's `package.json#scripts`, whatever a
248
- * plugin's step source answers elsewhere - the package's own declaration wins, as in `run`).
249
- * An array runs them in sequence. `${{ pkg.targetVersion }}` is bound here and in the two
250
- * below, and nowhere else.
251
- *
252
- * A `RunStepFn` runs in place of a shell command - but note that `${{ pkg.targetVersion }}` is
253
- * a *string* substitution, so a function reads the written version off `pkg` instead. */
254
- exec?: RunStepValue | RunStepValue[];
255
- /** Same, before the write (`preversion` in a Node repository). */
256
- before?: RunStepValue | RunStepValue[];
257
- /** Same, after it (`postversion` in a Node repository). */
258
- after?: RunStepValue | RunStepValue[];
259
- }
260
- interface ChangelogOptions extends ChangelogOptionsKeys, WithAppend<ChangelogOptionsKeys>, ScopedVars {
415
+ interface CommandConfig {
416
+ skip?: boolean;
417
+ logLevel?: 'silent' | 'error' | 'info' | 'verbose';
418
+ vars?: RmanConfig.VarsMap;
419
+ [index: string]: any;
261
420
  }
262
- interface ChangelogOptionsKeys {
263
- ignoreTypes?: string[];
264
- template?: string;
265
- filePath?: string;
266
- tagPattern?: string;
421
+ interface VarsMap {
422
+ /** Values for `${{ vars.* }}` to read, for this node and everything under it. */
423
+ vars?: Record<string, unknown>;
267
424
  }
425
+ /**
426
+ * Called during init, with the application - not at import, and not with just the repository.
427
+ *
428
+ * The application is what a command reaches services through (`app.getService('changelog')`), and
429
+ * `app.repository` is still there for the metadata that reads it: `version`'s help names its own
430
+ * scheme's bump words, which cannot be known before a repository exists.
431
+ */
432
+ type CommandRegisterFunction = (app: RmanApplication) => CommandMetadata;
433
+ /** One declared option: everything yargs takes, plus where it may be set from. */
434
+ type CommandOption = yargs.Options & {
435
+ array?: boolean;
436
+ conflicts?: string | string[];
437
+ target: 'cli' | 'config' | 'both';
438
+ /** The flag's spelling on the command line when it differs from the config key - `ignoreDirty`
439
+ * is `--ignore-dirty`. The key stays the camelCase one, since that is what a `.rmanrc` writes. */
440
+ cliName?: string;
441
+ };
442
+ /**
443
+ * One declared positional, as `CommandMetadata.positionals` holds them - yargs' own type, named
444
+ * here so a command's author can reach it.
445
+ *
446
+ * **An alias with no additions, and it earns its place anyway.** `positionals` was the one part
447
+ * of a declaration that could only be typed by importing yargs, and a plugin does not depend on
448
+ * yargs - rman does. Nothing enforced that, because a returned object literal is checked
449
+ * structurally: measured on a JavaScript plugin, `positionals: { paths: { type: 'string' } }`
450
+ * widens `'string'` to `string` and fails `PositionalOptionsType`, with the error arriving on
451
+ * the whole `commands` key several levels of "is not assignable" away from the word that caused
452
+ * it. A `satisfies Record<string, PositionalOption>` beside the options' own puts it back on the
453
+ * line. Same reasoning as the flat `CommandOption` in `index.ts`.
454
+ */
455
+ type PositionalOption = yargs.PositionalOptions;
456
+ type CommandMetadata = {
457
+ command: string;
458
+ /** Other names the command answers to - `list` is also `ls`. */
459
+ aliases?: string[];
460
+ /**
461
+ * yargs parser switches this command needs. `exec` is the only one so far and needs two:
462
+ * `populate--` to keep everything after `--` out of its own options, and
463
+ * `unknown-options-as-args` so the flags of the command *being run* pass through untouched.
464
+ */
465
+ parserConfiguration?: Partial<yargs.ParserConfigurationOptions>;
466
+ describe?: string;
467
+ examples?: {
468
+ command: string;
469
+ description?: string;
470
+ }[];
471
+ /**
472
+ * The options this command **owns** - the ones it contributes to `RmanConfig`, under
473
+ * `configKey`. An option a command merely *reads* belongs to whoever declares it; name it in
474
+ * `configKeys` instead. One key, one owner: two commands declaring the same key differently
475
+ * would collide silently in the augmentation's intersection.
476
+ */
477
+ config?: Record<string, CommandOption>;
478
+ /**
479
+ * Where `config` lands in a `.rmanrc`. Defaults to the first word of `command`, and a dotted
480
+ * path nests (`'run.build'` -> `{ run: { build: ... } }`).
481
+ *
482
+ * **Needed exactly once across the nine commands today**, which is why it is optional rather
483
+ * than required: `github-release` writes `githubRelease`, since a config key is camelCase and a
484
+ * command name is hyphenated. Deriving that with a kebab-to-camel rule was the alternative and
485
+ * was refused - it is one more rule to remember, and when it goes wrong the key lands somewhere
486
+ * plausible and nobody notices.
487
+ */
488
+ configKey?: string;
489
+ /**
490
+ * Config subtrees this command reads but does not own - what `--config` should show beside its
491
+ * own. A function of argv where the answer depends on it (`run <script>` reads `run.<script>`).
492
+ *
493
+ * `version` is the clearest case: it owns `version.*`, but `group` decides which packages move
494
+ * together and `changelog.*` is consulted when `--changelog` folds one into the bump commit.
495
+ * Neither is `version`'s to declare.
496
+ */
497
+ configKeys?: string[] | ((argv: yargs.Arguments) => string[]);
498
+ /**
499
+ * The positionals `command` declares, described - `'version [bump]'` names `bump`, this says
500
+ * what it is. Keyed by name, and **the names are checked against the command string**: a typo
501
+ * or a positional the command never declared fails on its own key and names the string it was
502
+ * checked against, which the `.positional('bump', ...)` builder call could never do.
503
+ *
504
+ * Only the description belongs here. Whether one is required (`<x>`) or variadic (`[x..]`) is
505
+ * part of the command string, which is the only place it can be, since that is what yargs
506
+ * parses.
507
+ */
508
+ positionals?: Record<string, PositionalOption>;
509
+ /**
510
+ * **The parameter is the command's own business, so this states no type for it.**
511
+ * A handler annotates itself with `ArgsOf<typeof config, typeof COMMAND>`, which this interface
512
+ * cannot compute for it - `M` is inferred from the literal the handler sits in, so naming it
513
+ * here is the circularity `ArgsOf` documents.
514
+ *
515
+ * It was `(argv: yargs.Arguments) => void`, and that was **not** a neutral placeholder: a
516
+ * parameter is checked contravariantly, so every command's own `Args` had to be a *supertype* of
517
+ * yargs' `{ [argName: string]: unknown }`. An index signature does not make a property present,
518
+ * so any required key put the declaration out of assignment - which is what forced `ArgsOf` to
519
+ * mark **everything** optional, required positionals included. Measured when it stopped:
520
+ * `Property 'script' is missing in type '{ [argName: string]: unknown; ... }' but required in
521
+ * type '{ script: string; }'`, on `run` and `import` alone.
522
+ *
523
+ * `never` is the spelling for "any parameter type is acceptable" - it constrains the *shape*
524
+ * (one argument, returning nothing a caller reads) and leaves the type to the annotation. It is
525
+ * not an `any` that could leak: nothing ever reads argv through this field, since
526
+ * `toYargsCommand` casts the handler to yargs' own and that cast is the single place the two
527
+ * descriptions of argv are admitted to disagree.
528
+ */
529
+ handler: (argv: never) => void;
530
+ };
531
+ /**
532
+ * The names a command string declares as positionals: `'run <script> [args..]'` is
533
+ * `'script' | 'args'`.
534
+ *
535
+ * Handles all four spellings in use - `<required>`, `[optional]`, `[variadic..]`, and the
536
+ * `[three...]` that `list` writes (yargs' own marker is two dots, so that one is a latent bug
537
+ * worth fixing separately; the parser tolerates it rather than silently producing a name with a
538
+ * dot in it).
539
+ */
540
+ type PositionalsOf<C extends string> = C extends `${string}<${infer N}>${infer Rest}` ? StripDots<N> | PositionalsOf<Rest> : C extends `${string}[${infer N}]${infer Rest}` ? StripDots<N> | PositionalsOf<Rest> : never;
541
+ /**
542
+ * `argv` as a command's own declarations describe it - every option by its config key, every
543
+ * positional the command string names, plus yargs' own two.
544
+ *
545
+ * **It is written as an explicit annotation on the handler, not inferred**, and that is a
546
+ * measured limitation rather than a preference. Inferring it from the sibling `config` works on
547
+ * its own, but not beside the `M & ValidMeta<M>` check: `M` is inferred from the whole literal,
548
+ * `handler` is part of that literal, so `ArgsOf<M>` is circular and resolves to
549
+ * `ArgsOf<CommandMetadata>` - every option "does not exist". Splitting the inference sites gets
550
+ * the names back and loses the values instead, because `M` and `C` then infer from the same
551
+ * `config` property and the literals widen, so every option arrives as `unknown`.
552
+ *
553
+ * So the declarations are hoisted out of the literal and the handler is annotated:
554
+ *
555
+ * ```ts
556
+ * const COMMAND = 'version [bump]' as const;
557
+ * const config = { ... } satisfies Record<string, RmanConfig.CommandOption>;
558
+ *
559
+ * handler: (args: RmanConfig.ArgsOf<typeof config, typeof COMMAND>) => { ... }
560
+ * ```
561
+ *
562
+ * Two lines, and in exchange the typo checking stays and a renamed option can no longer survive
563
+ * silently in the handler behind an `as`.
564
+ *
565
+ * **A `<required>` positional is required here too, and that is the one thing argv genuinely
566
+ * guarantees.** yargs refuses the call before the handler runs - measured on both commands that
567
+ * declare one: `rman run` and `rman import` each exit 1 with `Not enough non-option arguments:
568
+ * got 0, need at least 1`. Typed optional, the two handlers had to say so themselves
569
+ * (`args.script as string`, `args.path!`), and an `as` in a handler is exactly what declaring
570
+ * commands exists to remove - it is also how a renamed positional would survive silently. The
571
+ * rule is purely syntactic (`<x>` vs `[x]`), so it needs nothing but the command string, which
572
+ * is already the only place that says whether a positional is variadic.
573
+ *
574
+ * **What had made everything optional was not a judgement about argv - it was `handler`'s own
575
+ * parameter type**, which is where that story is written down. Worth knowing before adding a
576
+ * required key here: the constraint was three types away from the one that looked responsible.
577
+ * The claim and the parsing are pinned together in `cli.spec.ts` ("cli: a required positional"),
578
+ * since a type asserting a key is present is only honest while yargs refuses the call without it.
579
+ *
580
+ * **Every option stays optional, `default:` included, and that asymmetry is deliberate.**
581
+ * Narrowing on `default` needs *two* conditions, not one: the default must be present **and**
582
+ * `target !== 'config'`, because `toYargsCommand` never registers a `target: 'config'` option, so
583
+ * yargs never applies its default and the key is simply absent from argv. Written with the one
584
+ * obvious condition the type would claim a key is present where the runtime has `undefined` -
585
+ * failing in the direction that crashes. The gain does not pay for that risk yet: rman declares
586
+ * exactly **one** real option default (`config --json`), read as `if (args.json)`, so `undefined`
587
+ * and `false` already behave alike. The day a contributed command wants it, both conditions are
588
+ * written down here. `demandOption` would take the same pair of conditions and no declared
589
+ * command uses it - the only mention in the tree is a `CustomCommand`'s hand-written `builder`,
590
+ * which does not go through `ArgsOf` at all.
591
+ */
592
+ type ArgsOf<C, Cmd extends string> = {
593
+ [K in keyof C]?: OptionValue<C[K]>;
594
+ } & {
595
+ [K in RequiredPositionalsOf<Cmd>]: PositionalValue<Cmd, K>;
596
+ } & {
597
+ [K in Exclude<PositionalsOf<Cmd>, RequiredPositionalsOf<Cmd>>]?: PositionalValue<Cmd, K>;
598
+ } & GlobalArgs;
599
+ /**
600
+ * What every command's argv carries whatever it declares - yargs' own two, and rman's global
601
+ * options, which are registered on the program rather than per command.
602
+ *
603
+ * **Measured the moment `exec` was converted**: its handler read `args.logLevel`, which compiled
604
+ * only because `yargs.Arguments` is an index signature. With a real type it is
605
+ * `Property 'logLevel' does not exist` - correct, and exactly the kind of thing that was invisible
606
+ * before. Global options have to be stated once, here, or every handler that reads one is wrong.
607
+ */
608
+ type GlobalArgs = {
609
+ _: (string | number)[];
610
+ $0: string;
611
+ /** `--log-level`, declared on the program in `cli.ts`. */
612
+ logLevel?: 'silent' | 'error' | 'info' | 'verbose';
613
+ /** `--config`: print what this command would run with, and run nothing. */
614
+ config?: boolean;
615
+ /** Everything after `--`, present when the command sets `populate--` - `exec` reads it to tell
616
+ * its own flags from the ones belonging to the command it runs. */
617
+ '--'?: (string | number)[];
618
+ };
619
+ /**
620
+ * The config object a command's `config` declaration describes - `{ type: 'string' }` becomes
621
+ * `string`, `{ type: 'string', array: true }` becomes `string[]`.
622
+ *
623
+ * **The literal survives inference, which is the whole reason this works.** `{ type: 'string' }`
624
+ * would normally widen to `string`, but `yargs.Options['type']` is a union of string literals and
625
+ * the generic's constraint types the object literal contextually - so `ReturnType<T>` still holds
626
+ * `'string'` by the time this maps over it. Measured, with negative controls: reading a mapped
627
+ * value as the wrong type errors, and so does naming an option that was not declared.
628
+ *
629
+ * `choices` narrows further, and is the one place the caller has to help: an array literal widens
630
+ * to `string[]` on its own, so `choices: ['a', 'b'] as const` is what yields `'a' | 'b'`. Without
631
+ * the assertion it falls back to the declared `type`.
632
+ *
633
+ * **Every one of them is a `ConfigValue`, and that needs no per-key judgement.** An option
634
+ * declares a `type` yargs can parse, so what it contributes to `.rmanrc` is by construction a
635
+ * *value* - never a step, which is why steps arrive through `Extra` instead and have to be decided
636
+ * one at a time. So the whole derived half widens here, in one place, and a config author may
637
+ * write `changelog: { filePath: ({ vars }) => ... }` for any of them. `Resolved` takes it back off
638
+ * again for the reader.
639
+ */
640
+ type CommandConfigFromMetadata<T extends CommandMetadata> = T['config'] extends infer C ? C extends Record<string, CommandOption> ? {
641
+ [K in keyof ConfigOnly<C>]?: ConfigValue<OptionValue<ConfigOnly<C>[K]>>;
642
+ } : {} : {};
643
+ /**
644
+ * What the command adds to `RmanConfig`: its config options, under the key they live at -
645
+ * `{ version: { changelog: boolean } }`, or `{ run: { build: { exec: string } } }` for a dotted
646
+ * `configKey`.
647
+ *
648
+ * **`Extra` is for what an option cannot describe**, and only that. A `CommandOption` says
649
+ * `type: 'string'` or `type: 'boolean'`; it has no way to say `{ file: string; constant?: string }`
650
+ * or "a shell command or a function". So `version.stamp` and `version.before`/`.exec`/`.after`
651
+ * arrive through `Extra`, hand-written beside the command, while `version.commitMessage` and
652
+ * `version.releaseTagPattern` are ordinary `target: 'config'` options and are derived like the
653
+ * rest. Reach for `Extra` when the shape genuinely resists, never to avoid declaring an option.
654
+ *
655
+ * **One key, one declaration - which is what makes `Extra` necessary rather than convenient.**
656
+ * Interface merging is not a deep merge, so a key that arrives from two places is
657
+ * `Interface 'RmanConfig' cannot simultaneously extend types ... Named property 'publish' of
658
+ * types ... are not identical` (measured). A command therefore contributes its key *whole*: the
659
+ * derived half intersected with the hand-written one, never one half here and the other in some
660
+ * central interface.
661
+ *
662
+ * **`run` is the one command this cannot describe**, and it is worth knowing before relying on
663
+ * this: `run.<script>.*` is keyed by script name, so there is no flat option map to derive from
664
+ * and `Extra` would be the entire block. Its contribution stays hand-written in `RmanConfig`.
665
+ *
666
+ * **A widened key is refused rather than used**, because the damage otherwise is not local:
667
+ * `Nest<string, V>` is an index signature, so a `configKey` that lost its literal (a missing
668
+ * `as const`) turns `CommandConfigs` into "every string key has this command's shape" and every
669
+ * other key in `RmanConfig` then fails to satisfy it. Measured, on `github-release`: four errors
670
+ * pointing at `extends`, `logLevel`, `vars` and `changelog`, none of them anywhere near the
671
+ * mistake. The named property fails at the augmentation instead, and says what to add.
672
+ */
673
+ type CommandContribution<T extends CommandMetadata, Extra = {}> = string extends ConfigKeyOf<T> ? {
674
+ __configKeyMustBeALiteral: 'add `as const` to `command` (or to `configKey`)';
675
+ } : Nest<ConfigKeyOf<T>, ConfigBlock<CommandConfigFromMetadata<T> & Extra>>;
676
+ /**
677
+ * One config block as a `.rmanrc` may actually write it: the keys themselves, their `+key` append
678
+ * forms, and the `vars` that scopes the subtree.
679
+ *
680
+ * This is what a hand-written options interface said by extending
681
+ * `XOptionsKeys, ScopedVars` - clauses every new one had to remember. Derived, it cannot be
682
+ * forgotten. `ScopedVars` is folded in **before**
683
+ * `ScopedVars` is added, so no `+vars` is generated: appending to `vars` means nothing, since
684
+ * objects merge either way.
685
+ */
686
+ type ConfigBlock<K> = K & ScopedVars;
687
+ /** The declared `configKey`, or the first word of `command`. */
688
+ type ConfigKeyOf<T> = T extends {
689
+ configKey: infer K extends string;
690
+ } ? K : T extends {
691
+ command: infer C extends string;
692
+ } ? C extends `${infer N} ${string}` ? N : C : never;
693
+ /** One option's value type: the scalar its `type` denotes, in a list when it is one. */
694
+ type OptionValue<O> = O extends {
695
+ array: true;
696
+ } ? OptionScalar<O>[] : O extends {
697
+ type: 'array';
698
+ } ? OptionScalar<O>[] : OptionScalar<O>;
699
+ /**
700
+ * The scalar behind a yargs `type`, in decreasing order of how much each says.
701
+ *
702
+ * `coerce` first: it is a function that *produces* the value, so its return type is the answer and
703
+ * whatever `type` claims is a description of the input. `run --parallel` is the case - it takes a
704
+ * flag, a number or `false` and coerces them into `boolean | number | undefined`, which no `type`
705
+ * can state. Then `choices`, which narrows a `type` to the values it allows; then the `type`
706
+ * itself. An option declaring none of the three is `unknown` rather than `any` - a value nothing
707
+ * described should not silently type-check against everything.
708
+ */
709
+ type OptionScalar<O> = O extends {
710
+ coerce: (...args: never[]) => infer R;
711
+ } ? R : O extends {
712
+ choices: readonly (infer C)[];
713
+ } ? C : O extends {
714
+ type: 'string';
715
+ } ? string : O extends {
716
+ type: 'number' | 'count';
717
+ } ? number : O extends {
718
+ type: 'boolean';
719
+ } ? boolean : unknown;
268
720
  /**
269
721
  * The `run` block: scripts by name.
270
722
  *
@@ -281,14 +733,29 @@ export declare namespace RmanConfig {
281
733
  * remap still produces an index signature that claims `vars`.
282
734
  */
283
735
  type RunConfig = Record<string, RunStepValue | RunStepValue[] | RunScriptOptions>;
284
- interface RunScriptOptions extends RunScriptOptionsKeys, WithAppend<RunScriptOptionsKeys>, ScopedVars {
736
+ interface RunScriptOptions extends RunScriptOptionsKeys, ScopedVars {
285
737
  }
738
+ /**
739
+ * **Which level a key is read at is not uniform, and it follows what the key decides.**
740
+ * `RunService` reads `concurrency`, `progress`, `changed` and `changedSince` off the **root
741
+ * package only** - one scheduler, one answer for the whole batch - so those belong under
742
+ * `"[/]"`, and a `"[*]"` block declaring them is silently ignored (measured: `concurrency: 1`
743
+ * under `"[*]"` still ran two packages at once). `logLevel`, `skip`, `if`, `override` and the
744
+ * step slots are per package. `topo` and `bail` are read **both** ways and mean different things
745
+ * at each: the root's `topo` picks the sort (topological vs alphabetical), a package's own
746
+ * decides whether *it* waits for its dependencies; the root's `bail` is the default, a package's
747
+ * own outranks even an explicit CLI flag.
748
+ */
286
749
  interface RunScriptOptionsKeys {
287
750
  concurrency?: number;
288
751
  topo?: boolean;
289
752
  bail?: boolean;
290
753
  progress?: boolean;
291
754
  logLevel?: 'silent' | 'error' | 'info' | 'verbose';
755
+ /** Only run in packages that have changed since their last release - the config twin of
756
+ * `--changed`, read off the root. It was read at runtime long before it was declared here, so
757
+ * a typed config could not say the thing that already worked. */
758
+ changed?: boolean;
292
759
  changedSince?: string;
293
760
  skip?: boolean;
294
761
  /** Whether this script runs for a package at all - the small `changed and not private` grammar,
@@ -305,74 +772,130 @@ export declare namespace RmanConfig {
305
772
  after?: RunStepValue | RunStepValue[];
306
773
  override?: boolean;
307
774
  }
308
- interface PublishOptions extends PublishOptionsKeys, WithAppend<PublishOptionsKeys>, ScopedVars {
309
- }
310
- interface PublishOptionsKeys {
311
- /** Which **registry** `publish` ships this package to - default `['npm']` (every existing repo
312
- * keeps working unchanged). A package that only ever wants Docker images (typically also
313
- * `"private": true`, since it's not meant for npm at all) sets `['docker']`; both works too.
314
- * Each target answers "is this version already out there?" against its own registry, so a
315
- * package is never left without one: npm via `npm view`, docker via `docker manifest inspect`.
316
- *
317
- * Note this is strictly about *package distribution*. The repository's GitHub Release is not
318
- * a target here - it isn't a place a package ships to, it's the repository's own record that
319
- * a release happened, and it is never opted into: see `githubRelease` and the
320
- * `github-release` command. */
321
- target?: PublishTarget | PublishTarget[];
322
- docker?: DockerPublishOptions;
323
- /** Excludes this package from `publish` entirely (every target), regardless of
324
- * `target`/`"private"` - a single, explicit "never published" statement, e.g. for a package
325
- * released through some separate, unrelated process. `changelog` also skips it by default
326
- * (see its own `--include-skipped`) - there's little point changelogging something that's
327
- * never actually released. Independent of `version`, which never consults this at all - a
328
- * package can still be meaningfully versioned without ever being published. */
329
- skip?: boolean;
330
- }
331
- type PublishTarget = 'npm' | 'docker';
332
- /** Required once `"docker"` is one of this package's `publish.target`s - `publish --target
333
- * docker` errors clearly on a package that opts in here but leaves this out. */
334
- interface DockerPublishOptions extends DockerPublishOptionsKeys, WithAppend<DockerPublishOptionsKeys>, ScopedVars {
335
- }
336
- interface DockerPublishOptionsKeys {
337
- /** DockerHub image name/repository - bare (e.g. `"my-app"`) to be prefixed with
338
- * `--docker-namespace`/`DOCKERHUB_NAMESPACE`, or already-namespaced (contains a `/`) to use
339
- * verbatim. */
340
- image: string;
341
- /** Relative to the package's own directory. Default `"Dockerfile"`. */
342
- dockerfile?: string;
343
- /** Default `["linux/amd64"]`. */
344
- platforms?: string[];
345
- /** Build `cwd` override, relative to the repository root - only needed when the Dockerfile's
346
- * own `COPY`/`ADD` paths expect something other than the package's own directory (rare). */
347
- cwd?: string;
348
- /** Named `docker buildx build --build-context <name>=<path>` entries, keyed by name - each
349
- * path is relative to the package's own directory (or absolute). */
350
- buildContexts?: Record<string, string>;
351
- /** `docker buildx build --build-arg <name>=<value>` entries - a value of exactly `"$NAME"`
352
- * expands to `process.env.NAME` at build time (e.g. to pass a CI secret through). */
353
- buildArgs?: Record<string, string>;
354
- /** A file (relative to the package's own directory) whose contents become the DockerHub repo's
355
- * full description, if present. Default `"DOCKER_README.md"`. */
356
- readme?: string;
357
- }
358
- /** Entirely optional - `github-release` needs no configuration at all, since every required fact
359
- * (which tag, which repository, what the notes say) already has a sensible source. Nothing here
360
- * decides *whether* a release is cut: a release records that the repository shipped, so it is
361
- * always cut, and these are only details about how. */
362
- interface GithubReleaseOptions extends GithubReleaseOptionsKeys, WithAppend<GithubReleaseOptionsKeys>, ScopedVars {
363
- }
364
- interface GithubReleaseOptionsKeys {
365
- /** Files to attach to the release, as glob patterns relative to the package's own directory
366
- * (e.g. `["dist/*.tar.gz"]`). Read from **every** package, since one release covers the whole
367
- * source tree. A release with no assets at all is still perfectly valid - it records that the
368
- * version shipped, which is all a deploy-elsewhere package needs. */
369
- assets?: string[];
370
- /** `owner/repo`. Default: parsed from the `origin` remote's URL. Root-level only. */
371
- repository?: string;
372
- /** Create the release as an unpublished draft. Default `false`. Root-level only. */
373
- draft?: boolean;
374
- /** Default: whether the version being released is itself a prerelease by the root's own version
375
- * scheme (`1.3.0-beta.0` under semver) - see `VersionScheme.isPrerelease`. Root-level only. */
376
- prerelease?: boolean;
377
- }
378
775
  }
776
+ /**
777
+ * Registers a command and hands the register function straight back.
778
+ *
779
+ * **It returns the function, not the command's config, and that is the point.** The function has
780
+ * not run yet - it runs during init, once there is a `Repository` - so at registration time no
781
+ * config object exists to return. Declaring one anyway is what made the old body
782
+ * (`return def['config']`) uncompilable: `def` is a function, and `config` belongs to what it
783
+ * *returns*.
784
+ *
785
+ * Nothing is lost by being honest, because the metadata type rides along on the return type:
786
+ * `ReturnType<typeof someCommand>` is the `CommandMetadata`, and `CommandConfigFromMetadata` maps
787
+ * it to the config object the command contributes. A phantom property carrying the config type was
788
+ * tried and is unnecessary for exactly this reason - and it would not have survived being stored in
789
+ * `commandRegistry`, whose element type erases it.
790
+ *
791
+ * **`M` is inferred from the metadata rather than from the function, so that the same position can
792
+ * check it.** With the plainer `<T extends CommandRegisterFunction>(def: T)`, `T` is inferred from
793
+ * the literal itself, so the constraint is a subtype check - and a subtype check does no
794
+ * excess-property checking. Measured: `anotherBogusTopLevelKey: true` and a `totallyBogusKey`
795
+ * inside an option both compiled silently, which is how `cliName` and `examples` went unnoticed in
796
+ * `version.command.ts`. `M & ValidMeta<M>` keeps the bare `M` as the inference site and puts the
797
+ * check beside it, so a misspelled key now errors on its own line and names itself.
798
+ *
799
+ * **A command's `command` string needs `as const`** for the config key to be derivable from it
800
+ * (`'version [bump]' as const` -> `'version'`). `type: 'string'` survives inference on its own,
801
+ * since `yargs.Options['type']` is a union of literals and a union suppresses widening; `command`
802
+ * is a plain `string`, so it widens without the assertion. Spelling it `` `${string}` `` does not
803
+ * help - measured; that trick only applies where the constraint sits on the generic parameter.
804
+ */
805
+ export declare function registerCommand<M extends RmanConfig.CommandMetadata>(def: (app: RmanApplication) => M & ValidMeta<M>): (app: RmanApplication) => M;
806
+ /**
807
+ * The same declaration, **without** the registration - for a command that must exist only when
808
+ * something asks for it.
809
+ *
810
+ * That is exactly a contributed command's situation: `commandRegistry` is a module-level array
811
+ * walked by every `runCli`, so a package pushing onto it would give its commands to repositories
812
+ * that never named it - the module is imported as soon as anything imports the package. A package
813
+ * puts the function in its config's `commands` instead, and `cli.ts` calls it once the repository
814
+ * exists.
815
+ *
816
+ * **Why a factory rather than the metadata itself**: the config is resolved *inside*
817
+ * `Repository.create`, before the packages are known (plugins are what find them), so
818
+ * `app.repository` throws at that point. The function is stored unrun and called later, in
819
+ * `cli.ts`, where the built-ins' own factories run.
820
+ *
821
+ * Everything `registerCommand` documents about inference - `M & ValidMeta<M>`, the `as const` on
822
+ * `command`, the metadata riding on the return type - applies here unchanged; the two differ in one
823
+ * line.
824
+ */
825
+ export declare function declareCommand<M extends RmanConfig.CommandMetadata>(def: (app: RmanApplication) => M & ValidMeta<M>): (app: RmanApplication) => M;
826
+ /**
827
+ * Only the options a `.rmanrc` may actually set. `target: 'cli'` says the flag exists on the
828
+ * command line and nowhere else - `--interactive`, `--yes`, `--show` - and putting those in the
829
+ * config type would invite writing them where nothing reads them.
830
+ */
831
+ type ConfigOnly<C> = {
832
+ [K in keyof C as C[K] extends {
833
+ target: 'config' | 'both';
834
+ } ? K : never]: C[K];
835
+ };
836
+ /**
837
+ * A dotted path into nested objects: `'run.build'` + shape -> `{ run?: { build?: shape } }`.
838
+ *
839
+ * **Optional at every level**, and that is not a detail: a required key would make the whole
840
+ * contribution mandatory, so `defineConfig({})` stopped compiling with
841
+ * `Property 'version' is missing in type '{}'` (measured). A `.rmanrc` may say nothing about a
842
+ * command, and usually does.
843
+ */
844
+ type Nest<P extends string, V> = P extends `${infer H}.${infer R}` ? {
845
+ [K in H]?: Nest<R, V>;
846
+ } : {
847
+ [K in P]?: V;
848
+ };
849
+ /** Keys `T` declares that `Shape` does not know about. */
850
+ type Excess<T, Shape> = Exclude<keyof T, keyof Shape>;
851
+ /**
852
+ * `T` itself when it declares nothing extra; otherwise a shape it cannot satisfy, so the assignment
853
+ * fails on the offending key and says which one it is.
854
+ *
855
+ * The failure payload is an **object** rather than a tuple or a string literal. Intersecting a
856
+ * property's real type with an object leaves a real object type, so the error reads `Property
857
+ * '__unknownKey' is missing`; with a string or a tuple the intersection collapses to `never` and the
858
+ * message stops naming anything.
859
+ */
860
+ type Exact<T, Shape> = [Excess<T, Shape>] extends [never] ? T : {
861
+ [K in Excess<T, Shape>]: {
862
+ __unknownKey: K;
863
+ };
864
+ };
865
+ /** Every option checked against `CommandOption`, so a typo inside one is caught too. */
866
+ type ValidConfig<C> = {
867
+ [K in keyof C]: Exact<C[K], RmanConfig.CommandOption>;
868
+ };
869
+ type ValidMeta<M> = Exact<M, RmanConfig.CommandMetadata> & {
870
+ config?: M extends {
871
+ config: infer C;
872
+ } ? ValidConfig<C> : unknown;
873
+ positionals?: ValidPositionals<M>;
874
+ };
875
+ /** A positional the `command` string never declared fails on its own key, naming the string it was
876
+ * checked against. */
877
+ type ValidPositionals<M> = M extends {
878
+ command: infer C extends string;
879
+ positionals: infer P;
880
+ } ? {
881
+ [K in keyof P]: K extends RmanConfig.PositionalsOf<C> ? P[K] : {
882
+ __notDeclaredIn: C;
883
+ };
884
+ } : unknown;
885
+ /** A trailing `..`/`...` is yargs' variadic marker, not part of the positional's name. */
886
+ type StripDots<S extends string> = S extends `${infer N}...` ? N : S extends `${infer N}..` ? N : S;
887
+ /**
888
+ * The names a command string declares as **required** positionals - `'run <script> [args..]'` is
889
+ * `'script'` alone. `ArgsOf`'s only caller, which is why it is private: whether a positional is
890
+ * required is `ArgsOf`'s question, while `PositionalsOf` answers the different one `ValidPositionals`
891
+ * asks - which names the string declares at all.
892
+ *
893
+ * Unlike `PositionalsOf` it recurses through **one** branch, so a `[bracketed]` name is never
894
+ * emitted; the recursion still steps past one, since `${string}` matches whatever precedes the next
895
+ * `<`.
896
+ */
897
+ type RequiredPositionalsOf<C extends string> = C extends `${string}<${infer N}>${infer Rest}` ? StripDots<N> | RequiredPositionalsOf<Rest> : never;
898
+ /** A variadic positional (`[command..]`) collects a list; every other one is a single value. The
899
+ * command string is the only place that says which, since it is what yargs parses. */
900
+ type PositionalValue<Cmd extends string, N extends string> = Cmd extends `${string}${'<' | '['}${N}..${string}` ? string[] : string;
901
+ export {};