rman 1.3.0 → 2.0.0-beta.3

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 (187) hide show
  1. package/README.md +65 -31
  2. package/builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts +83 -0
  3. package/builtins/platforms/node/augmentation/system-info.augmentation.d.ts +26 -0
  4. package/builtins/platforms/node/augmentation/system-info.augmentation.js +79 -0
  5. package/builtins/platforms/node/commands/ci.command.d.ts +133 -0
  6. package/builtins/platforms/node/commands/ci.command.js +60 -0
  7. package/builtins/platforms/node/commands/clean.command.d.ts +185 -0
  8. package/builtins/platforms/node/commands/clean.command.js +74 -0
  9. package/builtins/platforms/node/index.d.ts +8 -0
  10. package/builtins/platforms/node/index.js +8 -0
  11. package/builtins/platforms/node/node-config.interface.d.ts +76 -0
  12. package/builtins/platforms/node/node-config.interface.js +7 -0
  13. package/builtins/platforms/node/node-manifest.provider.d.ts +68 -0
  14. package/builtins/platforms/node/node-manifest.provider.js +125 -0
  15. package/builtins/platforms/node/node.platform.d.ts +53 -0
  16. package/builtins/platforms/node/node.platform.js +134 -0
  17. package/builtins/platforms/node/services/ci.service.d.ts +47 -0
  18. package/builtins/platforms/node/services/ci.service.js +213 -0
  19. package/builtins/platforms/node/services/clean.service.d.ts +53 -0
  20. package/builtins/platforms/node/services/clean.service.js +237 -0
  21. package/builtins/platforms/node/services/publish.service.d.ts +114 -0
  22. package/builtins/platforms/node/services/publish.service.js +371 -0
  23. package/builtins/platforms/node/services/version-plan.service.d.ts +44 -0
  24. package/builtins/platforms/node/services/version-plan.service.js +58 -0
  25. package/builtins/platforms/node/utils/workspace-range.d.ts +26 -0
  26. package/builtins/platforms/node/utils/workspace-range.js +28 -0
  27. package/builtins/presets/index.d.ts +15 -0
  28. package/builtins/presets/index.js +57 -0
  29. package/builtins/presets/node.d.ts +16 -0
  30. package/builtins/presets/node.js +57 -0
  31. package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +52 -31
  32. package/{services → builtins/publish-targets/docker}/docker-publish.service.js +49 -26
  33. package/builtins/publish-targets/docker/docker.target.d.ts +54 -0
  34. package/builtins/publish-targets/docker/docker.target.js +40 -0
  35. package/builtins/publish-targets/docker/index.d.ts +2 -0
  36. package/builtins/publish-targets/docker/index.js +2 -0
  37. package/builtins/publish-targets/npm/index.d.ts +2 -0
  38. package/builtins/publish-targets/npm/index.js +2 -0
  39. package/builtins/publish-targets/npm/npm-publish-target.d.ts +73 -0
  40. package/builtins/publish-targets/npm/npm-publish-target.js +95 -0
  41. package/builtins/publish-targets/npm/npm-view.d.ts +48 -0
  42. package/builtins/publish-targets/npm/npm-view.js +71 -0
  43. package/cli.d.ts +8 -0
  44. package/cli.js +228 -93
  45. package/commands/build.command.d.ts +177 -3
  46. package/commands/build.command.js +20 -10
  47. package/commands/changelog.command.d.ts +296 -3
  48. package/commands/changelog.command.js +127 -43
  49. package/commands/config.command.d.ts +44 -3
  50. package/commands/config.command.js +53 -24
  51. package/commands/diff.command.d.ts +37 -3
  52. package/commands/diff.command.js +25 -16
  53. package/commands/exec.command.d.ts +193 -3
  54. package/commands/exec.command.js +60 -58
  55. package/commands/github-release.command.d.ts +154 -3
  56. package/commands/github-release.command.js +67 -37
  57. package/commands/import.command.d.ts +36 -3
  58. package/commands/import.command.js +28 -20
  59. package/commands/info.command.d.ts +35 -7
  60. package/commands/info.command.js +36 -30
  61. package/commands/lint.command.d.ts +176 -0
  62. package/commands/lint.command.js +34 -0
  63. package/commands/list.command.d.ts +163 -3
  64. package/commands/list.command.js +109 -71
  65. package/commands/publish.command.d.ts +231 -0
  66. package/commands/publish.command.js +304 -0
  67. package/commands/run.command.d.ts +186 -6
  68. package/commands/run.command.js +26 -72
  69. package/commands/test.command.d.ts +173 -3
  70. package/commands/test.command.js +16 -10
  71. package/commands/version.command.d.ts +330 -3
  72. package/commands/version.command.js +292 -78
  73. package/commands.d.ts +32 -0
  74. package/commands.js +28 -0
  75. package/constants.js +1 -1
  76. package/core/application.d.ts +116 -0
  77. package/core/application.js +160 -0
  78. package/core/classes/package.d.ts +219 -0
  79. package/core/classes/package.js +214 -0
  80. package/core/classes/registry.d.ts +30 -0
  81. package/core/classes/registry.js +47 -0
  82. package/core/{repository.d.ts → classes/repository.d.ts} +69 -20
  83. package/core/{repository.js → classes/repository.js} +195 -75
  84. package/core/classes/service.d.ts +49 -0
  85. package/core/classes/service.js +40 -0
  86. package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +23 -1
  87. package/core/{version-scheme.js → classes/version-scheme.js} +29 -1
  88. package/core/classes/workspace.d.ts +275 -0
  89. package/core/classes/workspace.js +537 -0
  90. package/core/command-builder.d.ts +14 -0
  91. package/core/command-builder.js +94 -0
  92. package/core/config/config-file-scope.d.ts +107 -0
  93. package/core/config/config-file-scope.js +226 -0
  94. package/core/config/config-interpolator.d.ts +164 -0
  95. package/core/config/config-interpolator.js +577 -0
  96. package/core/config/config-paths.d.ts +65 -0
  97. package/core/config/config-paths.js +78 -0
  98. package/core/config/config-reader.d.ts +192 -0
  99. package/core/config/config-reader.js +727 -0
  100. package/core/{merge-config.d.ts → config/merge-config.d.ts} +33 -34
  101. package/core/config/merge-config.js +260 -0
  102. package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +1 -1
  103. package/core/{resolve-target.js → config/resolve-config-target.js} +1 -1
  104. package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +46 -10
  105. package/core/{custom-command.js → interfaces/custom-command.js} +44 -17
  106. package/core/{manifest.d.ts → interfaces/manifest.d.ts} +49 -27
  107. package/core/interfaces/manifest.js +132 -0
  108. package/core/interfaces/plugin.d.ts +174 -0
  109. package/core/interfaces/plugin.js +124 -0
  110. package/core/interfaces/publish-target.d.ts +136 -0
  111. package/core/interfaces/publish-target.js +43 -0
  112. package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
  113. package/core/interfaces/run-step.js +1 -0
  114. package/core/plugin-loader.d.ts +25 -0
  115. package/core/plugin-loader.js +53 -0
  116. package/index.d.ts +122 -24
  117. package/index.js +66 -16
  118. package/interfaces/config-scope.interface.d.ts +223 -0
  119. package/interfaces/config-scope.interface.js +1 -0
  120. package/interfaces/rman-config.interface.d.ts +821 -203
  121. package/interfaces/rman-config.interface.js +78 -1
  122. package/package.json +3 -1
  123. package/services/change-hash.service.d.ts +42 -8
  124. package/services/change-hash.service.js +92 -13
  125. package/services/changelog.service.d.ts +91 -52
  126. package/services/changelog.service.js +460 -74
  127. package/services/config-service.d.ts +8 -0
  128. package/services/config-service.js +3 -0
  129. package/services/exec.service.d.ts +23 -12
  130. package/services/exec.service.js +14 -9
  131. package/services/github-release.service.d.ts +45 -34
  132. package/services/github-release.service.js +15 -12
  133. package/services/import.service.d.ts +25 -14
  134. package/services/import.service.js +9 -5
  135. package/services/list.service.d.ts +67 -13
  136. package/services/list.service.js +62 -15
  137. package/services/run.service.d.ts +35 -22
  138. package/services/run.service.js +316 -233
  139. package/services/system-info.d.ts +1 -1
  140. package/services/version-plan.service.d.ts +35 -8
  141. package/services/version-plan.service.js +96 -36
  142. package/services/version.service.d.ts +34 -14
  143. package/services/version.service.js +35 -16
  144. package/services.d.ts +1 -1
  145. package/services.js +1 -1
  146. package/utils/bin-path.d.ts +6 -7
  147. package/utils/bin-path.js +7 -18
  148. package/utils/branch-guard.d.ts +30 -1
  149. package/utils/branch-guard.js +31 -0
  150. package/utils/color-yaml.d.ts +8 -0
  151. package/utils/color-yaml.js +86 -0
  152. package/utils/exec.d.ts +10 -0
  153. package/utils/exec.js +1 -1
  154. package/utils/git.d.ts +26 -0
  155. package/utils/git.js +65 -0
  156. package/utils/logger.d.ts +2 -2
  157. package/utils/logger.js +1 -1
  158. package/utils/package-filter.d.ts +135 -8
  159. package/utils/package-filter.js +204 -16
  160. package/utils/printable-config.d.ts +9 -1
  161. package/utils/printable-config.js +46 -1
  162. package/utils/progress-panel.d.ts +9 -0
  163. package/utils/progress-panel.js +23 -1
  164. package/utils/release-version.d.ts +1 -1
  165. package/utils/run-bin.d.ts +10 -0
  166. package/utils/run-bin.js +1 -1
  167. package/utils/run-options.d.ts +97 -0
  168. package/utils/run-options.js +81 -0
  169. package/utils/version-group.d.ts +31 -0
  170. package/utils/version-group.js +43 -0
  171. package/utils/version-stamp.d.ts +1 -1
  172. package/utils/version-stamp.js +1 -1
  173. package/commands/changed.command.d.ts +0 -3
  174. package/commands/changed.command.js +0 -29
  175. package/core/config.d.ts +0 -409
  176. package/core/config.js +0 -939
  177. package/core/extends-config.d.ts +0 -27
  178. package/core/extends-config.js +0 -89
  179. package/core/manifest.js +0 -150
  180. package/core/merge-config.js +0 -219
  181. package/core/package.d.ts +0 -83
  182. package/core/package.js +0 -106
  183. package/core/plugin.d.ts +0 -112
  184. package/core/plugin.js +0 -189
  185. package/core/workspace.d.ts +0 -68
  186. package/core/workspace.js +0 -83
  187. /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
@@ -0,0 +1,537 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DEFAULT_PRESETS } from '../../builtins/presets/index.js';
4
+ import { ConfigReader } from '../config/config-reader.js';
5
+ import { mergeConfig, ORIGINS } from '../config/merge-config.js';
6
+ import { basePlatform } from '../interfaces/plugin.js';
7
+ import { Package } from './package.js';
8
+ /**
9
+ * A repository's layout, resolved: which directories hold packages, which technology claims each of
10
+ * them, what each one answers to, and the **raw** config each one ends up with.
11
+ *
12
+ * Built with {@link Workspace.create}, which runs the whole resolution; the constructor is
13
+ * protected, so there is no such thing as a half-built workspace.
14
+ *
15
+ * The configs it produces are **raw** - every `${{ }}` and every value function is still
16
+ * unevaluated. Resolving one needs `pkg`, `repository` and `git`, which belong to the repository
17
+ * rather than to its layout; `ConfigInterpolator` is the step after this.
18
+ */
19
+ /* The three steps live together because they depend on each other in one direction only: the root's
20
+ * config names the plugins, the plugins decide which directories are packages, and a package's
21
+ * config is the directory chain above it layered together. None of it can be done out of order, and
22
+ * each step is useless on its own.
23
+ *
24
+ * Every step is an overridable `protected` method, so a technology or a spec can replace one and
25
+ * keep the rest. */
26
+ export class Workspace {
27
+ options;
28
+ /** The repository root - where the walk starts and what every directory chain is relative to. */
29
+ rootDir;
30
+ /** Every plugin in play, in load order: the ones passed in first, then each level's own. */
31
+ plugins = [];
32
+ /** Every technology in play, in contribution order - which is what decides who claims a
33
+ * directory, since the first that recognizes it wins. */
34
+ platforms = [];
35
+ /** The root directory's own package. Not one of `packages`, which holds the members. */
36
+ rootPackage;
37
+ /** Every package below the root, depth-first. */
38
+ packages = [];
39
+ /** Each directory's own config, read once - see `_readLevel`. */
40
+ _levels = new Map();
41
+ /** The packages whose selector came from a `"name"` declaration rather than from their platform,
42
+ * for the clash message alone. */
43
+ _declaredSelectors = new Set();
44
+ /** Built on first use, unless `options.reader` supplied one. */
45
+ _configReader;
46
+ /**
47
+ * Builds a workspace and resolves it: discovery, selectors, and every package's raw config.
48
+ *
49
+ * A subclass inherits this and gets an instance of itself. Its declared return type stays
50
+ * `Workspace`, so a subclass wanting its own declares a one-line `create` that calls this.
51
+ *
52
+ * @param rootDir the repository root
53
+ */
54
+ /* **`new this()`, not `new Workspace()`.** In a static method `this` is the class the call was
55
+ * made on, so a subclass inheriting this factory builds itself; hard-coding the name would let
56
+ * someone override `_init` and never see it run. The spec for it asserts `instanceof` on the
57
+ * subclass.
58
+ *
59
+ * **The return type is a TypeScript limit rather than a choice.** The polymorphic form
60
+ * (`this: new (...) => T`) requires a *publicly* constructible class, and the constructor here is
61
+ * protected on purpose. The two cannot both be had, and the guard is worth more than the inferred
62
+ * type. */
63
+ static async create(rootDir, options) {
64
+ const workspace = new this(rootDir, options);
65
+ await workspace._init();
66
+ return workspace;
67
+ }
68
+ /** Use {@link Workspace.create}. */
69
+ /* Protected because an un-initialized workspace is a half-built object - no packages, no configs,
70
+ * and nothing saying so. */
71
+ constructor(rootDir, options) {
72
+ this.options = options;
73
+ this.rootDir = path.resolve(rootDir);
74
+ this.plugins = [...(options.plugins ?? [])];
75
+ this.platforms = [...(options.platforms ?? [])];
76
+ }
77
+ /** The package at a directory, root included, or `undefined` for one that holds none. */
78
+ packageAt(dirname) {
79
+ const resolved = path.resolve(dirname);
80
+ if (this.rootPackage && this.rootPackage.dirname === resolved)
81
+ return this.rootPackage;
82
+ return this.packages.find(pkg => pkg.dirname === resolved);
83
+ }
84
+ /** The whole resolution, in the one order the steps admit. */
85
+ /* Each line is a `protected` method rather than inline code precisely so this reads as the order
86
+ * and nothing else - and so a subclass replacing one step does not have to restate the rest. */
87
+ async _init() {
88
+ await this._discover();
89
+ await this._resolveSelectors();
90
+ this._assertUniqueSelectors();
91
+ await this._resolveConfigs();
92
+ }
93
+ /**
94
+ * Descends from the root, asking each directory's own technology where its children are, and
95
+ * fills `rootPackage` and `packages`.
96
+ *
97
+ * One step, repeated: read the directory's config (which loads whatever plugins it names and
98
+ * decides its platform), then ask *that* platform's `getWorkspace` for the directories below it
99
+ * holding packages. A directory is visited once, and `options.deep` bounds the descent.
100
+ */
101
+ /* **First visit wins.** A provider may legitimately name a directory another one already claimed
102
+ * (two overlapping globs, a symlinked package), and one naming an ancestor would otherwise
103
+ * recurse forever.
104
+ *
105
+ * `deep` bounds the descent for the same reason `findRoot` bounds its climb: a provider computing
106
+ * paths rather than reading them can produce a chain that never ends, and a guessed depth beats a
107
+ * hang with nothing printed. */
108
+ async _discover() {
109
+ const visited = new Set();
110
+ const descend = async (dir, parent, remaining) => {
111
+ const dirname = path.resolve(dir);
112
+ visited.add(dirname);
113
+ const level = await this._readLevel(dirname);
114
+ const platform = this._platformFor(dirname, level.config);
115
+ /** The platform is handed over rather than searched for: the walk that found this directory
116
+ * is what established it, and `Package` re-guessing would be a second answer that can
117
+ * disagree with the first. */
118
+ const pkg = this._createPackage(dirname, platform);
119
+ if (dirname === this.rootDir)
120
+ this.rootPackage = pkg;
121
+ else
122
+ this.packages.push(pkg);
123
+ if (parent) {
124
+ /** `children` is enumerable and `parent` is not: one edge, and only one direction can be
125
+ * the one a walk follows - a `Package` that can be serialized needs the back-reference
126
+ * hidden. */
127
+ parent.children.push(pkg);
128
+ Object.defineProperty(pkg, 'parent', { value: parent, enumerable: false, configurable: true });
129
+ }
130
+ if (remaining <= 0)
131
+ return;
132
+ /** **Its own platform is asked, and nobody else.** A platform that did not claim the
133
+ * directory has no standing to say what is under it. A directory no technology claimed has
134
+ * no `getWorkspace`, hence no children - which is a single-package repository arrived at
135
+ * rather than guessed. */
136
+ for (const child of platform.getWorkspace?.(dirname) ?? []) {
137
+ const childDir = path.resolve(child);
138
+ if (visited.has(childDir))
139
+ continue;
140
+ await descend(childDir, pkg, remaining - 1);
141
+ }
142
+ };
143
+ await descend(this.rootDir, undefined, this.options.deep ?? 10);
144
+ }
145
+ /**
146
+ * Gives every package the selector it answers to - its `.rmanrc "name"` when it declares one, and
147
+ * what its platform says otherwise.
148
+ *
149
+ * `"[glob]"` and `--scope` match this, never the package's name: a name is an *ecosystem's*
150
+ * promise rather than rman's.
151
+ */
152
+ /* **Resolved from a cascade run with no selector at all**, and that is the step whose absence is
153
+ * silent: a `"[pkg-a]"` block is applied on the strength of the selector, and the selector comes
154
+ * from `name`, which is inside the very config being resolved. Reading `name` while applying
155
+ * selector blocks would need the answer in order to find it. So the chain is walked twice -
156
+ * unmarked only, then for real - over one directory cache, which is what keeps the second pass
157
+ * free.
158
+ *
159
+ * `name` is refused inside a glob block for exactly this reason (`_assertSelectorBlocks`), so
160
+ * nothing is lost by the first pass ignoring them. */
161
+ async _resolveSelectors() {
162
+ for (const pkg of [this.rootPackage, ...this.packages]) {
163
+ const config = await this._cascade(pkg, undefined);
164
+ const declared = config.name;
165
+ if (declared !== undefined && (typeof declared !== 'string' || !declared.trim())) {
166
+ throw new Error(`"name" takes the selector this package answers to - "${pkg.dirname}" gave ${typeof declared}.`);
167
+ }
168
+ if (declared !== undefined)
169
+ this._declaredSelectors.add(pkg);
170
+ /** `platformSelector()` is the package's own - the manifest's name, or whatever its platform
171
+ * says addresses it. `Package` already computed it in its constructor; this only replaces it
172
+ * when the config declared one. */
173
+ pkg.selector = declared?.trim() || pkg.platformSelector();
174
+ }
175
+ }
176
+ /**
177
+ * Refuses two packages answering to one selector. The root is exempt - a glob never matches it and
178
+ * `"[/]"` needs no name, so it shares an address with nobody.
179
+ */
180
+ /* Two packages answering to one selector make `"[that]"` and `--scope that` ambiguous *silently* -
181
+ * the config reaches both and a lookup returns the first.
182
+ *
183
+ * `name` cascades like every unmarked key, so one declaration above two packages is the usual way
184
+ * in - and the message says so when that is what happened, since the fix is different. */
185
+ _assertUniqueSelectors() {
186
+ const bySelector = new Map();
187
+ for (const pkg of this.packages) {
188
+ const clash = bySelector.get(pkg.selector);
189
+ if (clash) {
190
+ const cascaded = this._declaredSelectors.has(pkg) && this._declaredSelectors.has(clash);
191
+ throw new Error(`Two packages answer to the selector "${pkg.selector}":\n ${clash.dirname}\n ${pkg.dirname}\n` +
192
+ ` A selector has to be unique - "[${pkg.selector}]" and \`--scope ${pkg.selector}\` ` +
193
+ `cannot mean two packages.` +
194
+ (cascaded
195
+ ? `\n Both got it from one cascading "name" declaration above them; declare it in ` +
196
+ `each package's own ".rmanrc" instead.`
197
+ : ''));
198
+ }
199
+ bySelector.set(pkg.selector, pkg);
200
+ }
201
+ }
202
+ /** Cascades every package's config into `Package.rawConfig`, now that the selectors are known.
203
+ * Still raw - `Repository` is what interpolates it into `Package.config`. */
204
+ async _resolveConfigs() {
205
+ for (const pkg of [this.rootPackage, ...this.packages]) {
206
+ pkg.rawConfig = await this._cascade(pkg, pkg.selector);
207
+ }
208
+ }
209
+ /**
210
+ * Every directory from the root down to `pkg`, layered into one config.
211
+ *
212
+ * Two layers per level: the level's unmarked keys first, then the `"[selector]"` blocks that
213
+ * speak for this package, in the order they were written - later wins, as `overrides` does in
214
+ * eslint and prettier.
215
+ *
216
+ * @param selector what the blocks are matched against; `undefined` skips them, except `"[/]"` for
217
+ * the root, which is structural and needs no selector.
218
+ */
219
+ /* **Unmarked first because it is the widest thing the level says** - it configures that directory
220
+ * *and every package under it*, so it is the level's floor. A selector block written *above* the
221
+ * plain keys still beats them: unmarked is not a fourth selector, it is the layer that also feeds
222
+ * the directories below, and making that depend on key order in the file would be absurd. */
223
+ async _cascade(pkg, selector) {
224
+ const result = {};
225
+ const audience = {
226
+ isRoot: pkg.dirname === this.rootDir,
227
+ selector,
228
+ platform: pkg.platform.name,
229
+ };
230
+ for (const dir of this._dirChain(pkg.dirname)) {
231
+ const level = await this._readLevel(dir);
232
+ mergeConfig(result, this._stripSelectors(level.config));
233
+ for (const block of this._matchingSelectors(level.config, audience)) {
234
+ mergeConfig(result, block);
235
+ }
236
+ }
237
+ return result;
238
+ }
239
+ /** One directory's own config, read once and cached. Plugins the level names are loaded here and
240
+ * accumulated into `plugins`. */
241
+ /* The cache is what makes the two cascade passes affordable - twenty packages sharing a root
242
+ * means that root is read once rather than forty times. It is also what keeps a level's plugins
243
+ * from being loaded a second time. */
244
+ async _readLevel(dirname) {
245
+ const resolved = path.resolve(dirname);
246
+ const hit = this._levels.get(resolved);
247
+ if (hit)
248
+ return hit;
249
+ const result = await this._reader().resolve(resolved, {
250
+ plugins: this.plugins,
251
+ platforms: this.platforms,
252
+ /** **The root alone**, because that is the level they belong to: everything below it inherits
253
+ * whatever the root settled on, and laying them down again there would put a preset's
254
+ * technology ahead of one an intermediate directory declared. */
255
+ presets: resolved === this.rootDir ? (this.options.presets ?? DEFAULT_PRESETS) : undefined,
256
+ });
257
+ /** Whatever this level added, in the order it was loaded - `plugins` appends, never replaces,
258
+ * so a level naming one never means "and drop the ones above me". The technologies a plugin
259
+ * carries are already in `result.platforms`; the reader puts them there, so this walk and the
260
+ * reader cannot end up with two orders. */
261
+ for (const plugin of result.plugins) {
262
+ if (!this.plugins.some(p => p.name === plugin.name))
263
+ this.plugins.push(plugin);
264
+ }
265
+ for (const platform of result.platforms)
266
+ this._addPlatform(platform);
267
+ this._levels.set(resolved, result);
268
+ return result;
269
+ }
270
+ /** Appends a technology unless one already answers to its name. */
271
+ _addPlatform(platform) {
272
+ if (!this.platforms.some(p => p.name === platform.name))
273
+ this.platforms.push(platform);
274
+ }
275
+ /** The reader levels are read with - `options.reader`, or a default one. */
276
+ _reader() {
277
+ return (this._configReader ??= this.options.reader ?? new ConfigReader());
278
+ }
279
+ /** Root, then every directory down to `targetDir`, inclusive. A target outside the root is the
280
+ * root alone. */
281
+ _dirChain(targetDir) {
282
+ const rel = path.relative(this.rootDir, targetDir);
283
+ if (!rel || rel === '.' || rel.startsWith('..'))
284
+ return [this.rootDir];
285
+ const dirs = [this.rootDir];
286
+ let dir = this.rootDir;
287
+ for (const segment of rel.split(path.sep)) {
288
+ dir = path.join(dir, segment);
289
+ dirs.push(dir);
290
+ }
291
+ return dirs;
292
+ }
293
+ /** A level's own settings - everything that is not a `"[...]"` block. */
294
+ _stripSelectors(config) {
295
+ const result = {};
296
+ for (const [key, value] of Object.entries(config))
297
+ if (!ConfigReader.isSelectorKey(key))
298
+ result[key] = value;
299
+ return result;
300
+ }
301
+ /**
302
+ * The blocks in `config` that speak for this package, in declaration order.
303
+ *
304
+ * `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
305
+ * root.
306
+ */
307
+ /* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
308
+ * a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
309
+ * setting to a root with no build directory to apply it to. A root block needs no selector, which
310
+ * is what makes `/` structural. */
311
+ _matchingSelectors(config, audience) {
312
+ const matches = [];
313
+ for (const [key, value] of Object.entries(config)) {
314
+ if (!ConfigReader.isSelectorKey(key) || !value || typeof value !== 'object')
315
+ continue;
316
+ const { scope, test } = ConfigReader.parseSelector(key);
317
+ if (!this._speaksFor(scope, test, audience))
318
+ continue;
319
+ matches.push(value);
320
+ }
321
+ return matches;
322
+ }
323
+ /**
324
+ * Whether one selector block speaks for this package.
325
+ *
326
+ * `"[/]"` asks only whether this *is* the root. Everything else matches packages **below** the
327
+ * root and never the root itself, which is addressed structurally or not at all.
328
+ */
329
+ /* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
330
+ * a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
331
+ * setting to a root with no build directory to apply it to. A platform block is held to the same
332
+ * line for consistency - the root has a platform, but it also has `"[/]"`.
333
+ *
334
+ * A package no technology claimed carries `basePlatform`, whose name is `''`, so it matches no
335
+ * platform block at all - `parseSelector`'s test refuses an empty name rather than letting an
336
+ * empty selector quietly match it. */
337
+ _speaksFor(scope, test, audience) {
338
+ if (scope === 'root')
339
+ return audience.isRoot;
340
+ if (audience.isRoot)
341
+ return false;
342
+ if (scope === 'platform')
343
+ return test(audience.platform);
344
+ return audience.selector !== undefined && test(audience.selector);
345
+ }
346
+ /**
347
+ * The platform that claims a directory - the one its config names, or the first loaded platform
348
+ * whose manifest provider recognizes it. `basePlatform` when none does, so a caller needs no
349
+ * guard; a *declared* platform no plugin provides is an error.
350
+ */
351
+ /* `ConfigReader` already decided the name, from a declaration or by asking the loaded plugins, so
352
+ * this only has to find the platform that answers to it. */
353
+ _platformFor(dirname, config) {
354
+ const declared = this._declaredPlatformName(config);
355
+ if (declared === undefined) {
356
+ return this.platforms.find(p => p.manifestProvider.read(dirname)) ?? basePlatform;
357
+ }
358
+ const named = this.platforms.find(p => p.name === declared);
359
+ if (!named) {
360
+ const have = this.platforms.map(p => p.name).filter(Boolean);
361
+ throw new Error(`"platform" names "${declared}" (${this._originOf(config, 'platform')}), which is not a ` +
362
+ `platform this repository has. ` +
363
+ `${have.length ? `Registered: ${have.join(', ')}. ` : 'None is registered. '}` +
364
+ `Contribute it with "platforms", or inherit it with "extends".`);
365
+ }
366
+ /** **A declaration is held to the directory it names.** The failure it replaces is invisible:
367
+ * the manifest reads as nothing, so the package is named after its directory at `0.0.0` and
368
+ * the repository looks fine. The message names the file the platform looked for, because that
369
+ * is the thing to go and check. */
370
+ if (!named.manifestProvider.read(dirname)) {
371
+ throw new Error(`"${dirname}" declares \`platform: '${declared}'\` (${this._originOf(config, 'platform')}), ` +
372
+ `and "${declared}" does not recognize it - it looks for "${named.manifestProvider.fileName}".`);
373
+ }
374
+ return named;
375
+ }
376
+ /** A platform named unmarked or under `"[/]"` - both are the directory saying what it is. */
377
+ /* **An expression is refused rather than read as a literal.** `platform` is consulted before any
378
+ * package exists - it is what decides what a package *is* - so there is no `pkg` for an
379
+ * expression to be about, and interpolation runs long afterwards. Passed through, a `${{ }}` here
380
+ * reached the lookup as its raw text and failed as an unknown platform name, which sends the
381
+ * reader to check their `platforms` instead of the line they wrote.
382
+ *
383
+ * A value that is not a usable string is refused too, rather than read as "declared nothing": the
384
+ * two are opposite answers, and the silent one leaves a directory claimed by whichever technology
385
+ * happened to recognize it. */
386
+ _declaredPlatformName(config) {
387
+ const root = config['[/]'];
388
+ const declared = config.platform ?? (root && typeof root === 'object' ? root.platform : undefined);
389
+ if (declared === undefined)
390
+ return undefined;
391
+ const where = this._originOf(config, 'platform');
392
+ if (typeof declared !== 'string' || !declared.trim()) {
393
+ throw new Error(`"platform" takes a platform's name - ${where} gave ${typeof declared}.`);
394
+ }
395
+ if (declared.includes('${{')) {
396
+ throw new Error(`"platform" cannot be an expression (${where}) - it is read while the packages are still ` +
397
+ `being found, so there is no package for one to be about. Write the name, and use a ` +
398
+ `package's own ".rmanrc" where the answer differs.`);
399
+ }
400
+ return declared.trim();
401
+ }
402
+ /** Which file a key came from, for an error that can be acted on. */
403
+ /* A config is merged from a directory's own forms, an `extends` base, every `"[selector]"` block
404
+ * and one layer per directory before anything reads it, so the key alone leaves the reader
405
+ * searching all of them. `mergeConfig` records one file per key under `ORIGINS`. */
406
+ _originOf(config, key) {
407
+ const origins = config[ORIGINS];
408
+ return origins?.[key] ? `in "${origins[key]}"` : `the "${key}" key`;
409
+ }
410
+ /** Where a `Package` is made, and the only place - override it to build a subclass of `Package`
411
+ * and keep the rest of the walk. */
412
+ _createPackage(dirname, platform) {
413
+ return new Package(dirname, this.options.app, platform);
414
+ }
415
+ }
416
+ /**
417
+ * How a repository is laid out, and who decides.
418
+ *
419
+ * A namespace for the same reason `Manifest` is one: it is what a plugin can *augment*
420
+ * (`declare module 'rman' { namespace Workspace { ... } }`), so whatever this seam grows later
421
+ * arrives as a member here rather than as another top-level export. It merges with the class above,
422
+ * so `Workspace.walk` and `Workspace.create` are reached the same way.
423
+ */
424
+ (function (Workspace) {
425
+ /**
426
+ * **The walk**: descend from `rootDir`, asking each directory's own technology where its children
427
+ * are, and repeating for each answer.
428
+ *
429
+ * One step, applied recursively:
430
+ *
431
+ * 1. take the directory's **declared** platform if it has one, and otherwise the first registered
432
+ * platform whose manifest provider recognizes it (`app.platformFor`, `basePlatform` if none);
433
+ * 2. ask **that** platform's `getWorkspace` for the directories below it holding packages;
434
+ * 3. do the same for each of them.
435
+ *
436
+ * **A declaration wins, and is then held to it.** A platform named for a directory it does not
437
+ * recognize is a statement that is simply untrue, and the failure it would otherwise become is
438
+ * invisible: the manifest reads as nothing, so the package is named after its directory at
439
+ * `0.0.0` and the repository looks like it works. The error names the directory, the platform and
440
+ * the file that platform looked for.
441
+ *
442
+ * The root node always exists - a repository is a package whatever its technology - so this never
443
+ * returns `undefined`. A repository nobody recognizes is a root with no children, which is the
444
+ * single-package answer arrived at rather than guessed.
445
+ *
446
+ * **A directory is visited once.** A provider may legitimately name a directory another one
447
+ * already claimed (two globs overlapping, a symlinked package), and a provider naming an ancestor
448
+ * would otherwise recurse forever. First visit wins, so a package sits where it was first found.
449
+ *
450
+ * `deep` bounds the descent for the same reason `findRoot` bounds its climb: a provider computing
451
+ * paths rather than reading them can produce a chain that never ends, and a guessed depth is
452
+ * better than a hang with nothing printed.
453
+ */
454
+ async function walk(app, rootDir, options) {
455
+ const visited = new Set();
456
+ const descend = async (dir, remaining) => {
457
+ const resolved = path.resolve(dir);
458
+ visited.add(resolved);
459
+ const declared = await options?.declared?.(resolved);
460
+ if (declared && !declared.manifestProvider.read(resolved)) {
461
+ throw new Error(`"${resolved}" declares \`platform: '${declared.name}'\`, and that platform does not ` +
462
+ `recognize it - it looks for "${declared.manifestProvider.fileName}". Either the ` +
463
+ `directory is not a ${declared.name} package, or the declaration belongs one level down.`);
464
+ }
465
+ const platform = declared ?? app.platformFor(resolved);
466
+ const node = { dirname: resolved, platform, children: [] };
467
+ if (remaining <= 0)
468
+ return node;
469
+ /**
470
+ * **Its own platform is asked, and nobody else.** A platform that did not claim the directory
471
+ * has no standing to say what is under it - that was the old first-wins rule, one level up.
472
+ * `basePlatform` has no `getWorkspace`, so a directory no technology claimed has no children,
473
+ * which is the documented behaviour of a repository naming no plugin.
474
+ */
475
+ for (const child of platform.getWorkspace?.(resolved) ?? []) {
476
+ const childDir = path.resolve(child);
477
+ if (visited.has(childDir))
478
+ continue;
479
+ node.children.push(await descend(childDir, remaining - 1));
480
+ }
481
+ return node;
482
+ };
483
+ return descend(rootDir, options?.deep ?? 10);
484
+ }
485
+ Workspace.walk = walk;
486
+ /** Every node below `node`, depth-first, excluding `node` itself - the flat list `Repository`
487
+ * reports as its packages, since the root is not one of its own members. */
488
+ function flatten(node) {
489
+ return node.children.flatMap(child => [child, ...flatten(child)]);
490
+ }
491
+ Workspace.flatten = flatten;
492
+ /**
493
+ * Where the repository starts, decided **without knowing anything about any ecosystem** - it has
494
+ * to be, because the plugins that do know are named in the config file this walk is looking for.
495
+ *
496
+ * Walking up from `from`, stopping after a directory holding `.git`, the root is:
497
+ *
498
+ * 1. the **outermost** directory in that chain holding an `.rmanrc*` - outermost, because a
499
+ * *package* may have its own `.rmanrc` (that is a supported thing), and from inside such a
500
+ * package the nearest one is the package's, not the repository's;
501
+ * 2. otherwise the `.git` directory itself, the ordinary meaning of "repository root";
502
+ * 3. otherwise `from`, which is all that is left to go on.
503
+ *
504
+ * **What "outermost" costs**, since the two cases genuinely conflict and only one can win: a
505
+ * self-contained project nested inside a larger git repository *and sharing its `.git`* resolves
506
+ * to the outer root, not to itself (measured). That is the same repository by any definition git
507
+ * recognizes, so it is the defensible answer - and a nested project with a `.git` of its own is
508
+ * found correctly, because the walk stops there before the outer `.rmanrc` is ever seen (also
509
+ * measured). A per-package `.rmanrc` is the common case and it is the one served.
510
+ */
511
+ function findRoot(from, deep = 10) {
512
+ const chain = [];
513
+ let dir = path.resolve(from);
514
+ let remaining = deep;
515
+ while (remaining-- >= 0 && fs.existsSync(dir)) {
516
+ chain.push(dir);
517
+ if (fs.existsSync(path.join(dir, '.git')))
518
+ break;
519
+ const parent = path.resolve(dir, '..');
520
+ if (parent === dir)
521
+ break;
522
+ dir = parent;
523
+ }
524
+ const withConfig = chain.filter(hasRmanConfig);
525
+ if (withConfig.length)
526
+ return withConfig[withConfig.length - 1];
527
+ const gitRoot = chain.find(d => fs.existsSync(path.join(d, '.git')));
528
+ return gitRoot ?? path.resolve(from);
529
+ }
530
+ Workspace.findRoot = findRoot;
531
+ /** Every file form a `.rmanrc` comes in - `package.json#rman` is deliberately *not* one of them
532
+ * here: it would make the root question npm-shaped again. */
533
+ const CONFIG_FILES = ['.rmanrc', '.rmanrc.yml', '.rmanrc.cjs', '.rmanrc.mjs', '.rmanrc.js'];
534
+ function hasRmanConfig(dir) {
535
+ return CONFIG_FILES.some(name => fs.existsSync(path.join(dir, name)));
536
+ }
537
+ })(Workspace || (Workspace = {}));
@@ -0,0 +1,14 @@
1
+ import type { CommandModule } from 'yargs';
2
+ import type { RmanConfig } from '../interfaces/rman-config.interface.js';
3
+ /**
4
+ * Turns a command's **declaration** into the yargs registration it describes.
5
+ *
6
+ * This is the one place that knows how the two relate, which is the point of declaring commands
7
+ * instead of building them: a command says what it has, and only this function says what yargs is
8
+ * told. Everything a `builder` used to do by hand - options, positionals, examples, parser
9
+ * switches - arrives as data now, so a typo in any of it is a compile error at the command rather
10
+ * than a flag that silently never existed.
11
+ */
12
+ export declare function toYargsCommand(meta: RmanConfig.CommandMetadata): CommandModule;
13
+ /** A command's own name - the first word of its `command` string, before any positional. */
14
+ export declare function commandName(command: string): string;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Turns a command's **declaration** into the yargs registration it describes.
3
+ *
4
+ * This is the one place that knows how the two relate, which is the point of declaring commands
5
+ * instead of building them: a command says what it has, and only this function says what yargs is
6
+ * told. Everything a `builder` used to do by hand - options, positionals, examples, parser
7
+ * switches - arrives as data now, so a typo in any of it is a compile error at the command rather
8
+ * than a flag that silently never existed.
9
+ */
10
+ export function toYargsCommand(meta) {
11
+ return {
12
+ command: meta.command,
13
+ aliases: meta.aliases,
14
+ describe: meta.describe,
15
+ configKeys: configKeysOf(meta),
16
+ builder: (cmd) => {
17
+ if (meta.parserConfiguration)
18
+ cmd.parserConfiguration(meta.parserConfiguration);
19
+ for (const [name, spec] of Object.entries(meta.positionals ?? {}))
20
+ cmd.positional(name, spec);
21
+ for (const [key, option] of Object.entries(meta.config ?? {})) {
22
+ /**
23
+ * **`target: 'config'` means "a `.rmanrc` key, and not a flag"** - `githubRelease.draft` is
24
+ * the first of those. Skipping it here is what makes the field mean something rather than
25
+ * being documentation.
26
+ */
27
+ if (option.target === 'config')
28
+ continue;
29
+ /** `target` and `cliName` are rman's, not yargs' - dropped rather than passed through. */
30
+ const { cliName, ...rest } = option;
31
+ delete rest.target;
32
+ /**
33
+ * Registered under `cliName` when the flag and the config key are spelled differently
34
+ * (`--ignore-dirty` for `ignoreDirty`). yargs expands the hyphenated form back to camelCase
35
+ * in argv, so the handler reads the key it declared either way.
36
+ */
37
+ cmd.option(cliName ?? key, rest);
38
+ }
39
+ for (const example of meta.examples ?? [])
40
+ cmd.example(example.command, example.description ?? '');
41
+ return cmd;
42
+ },
43
+ /**
44
+ * Cast because the two descriptions of argv disagree on purpose: yargs types it as an index
45
+ * signature of `unknown`, and a command annotates it with the options it actually declared. The
46
+ * narrower one is the useful one, and this is the single place the widening is admitted -
47
+ * previously it was an `as` per option read, in every handler.
48
+ */
49
+ handler: withCommandPlatforms(meta),
50
+ };
51
+ }
52
+ /**
53
+ * Carries `CommandMetadata.platforms` into argv, where `readPackageFilterOptions` picks it up.
54
+ *
55
+ * Through argv rather than a parameter because `filterPackages` is called by ten *services*, none
56
+ * of which knows which command is running - `options` is all they see. One wrap here reaches every
57
+ * one of them and changes none.
58
+ */
59
+ function withCommandPlatforms(meta) {
60
+ const handler = meta.handler;
61
+ if (!meta.platforms?.length)
62
+ return handler;
63
+ return (args) => {
64
+ args.commandPlatforms = meta.platforms;
65
+ return handler(args);
66
+ };
67
+ }
68
+ /** A command's own name - the first word of its `command` string, before any positional. */
69
+ export function commandName(command) {
70
+ return command.split(/\s+/)[0];
71
+ }
72
+ /**
73
+ * Which `.rmanrc` keys `--config` shows for a command: the ones it declared reading, plus **its own
74
+ * key when it has one**.
75
+ *
76
+ * `configKeys` is the read-only list on purpose - a command should not have to repeat the key it
77
+ * already owns, and `version` listing `'version'` beside `'changelog'` said nothing about which of
78
+ * the two it defines. Measured before this existed: `rman version --config` reported
79
+ * `changelog, group, allowBranch, ignoreBranch` and left out `version` itself.
80
+ *
81
+ * **Own key only when the command actually declares a config option.** `build` and `test` own
82
+ * nothing - their settings live in `run.build`/`run.test`, which they merely read - so prepending
83
+ * their names would print an empty `build: {}` that reads as "nothing is configured" about a key
84
+ * that does not exist. A `configKeys` given as a *function* is left alone entirely: `run` computes
85
+ * `run.<script>` from argv, which is narrower than its own key and deliberately so.
86
+ */
87
+ function configKeysOf(meta) {
88
+ if (typeof meta.configKeys === 'function')
89
+ return meta.configKeys;
90
+ const ownsConfig = Object.values(meta.config ?? {}).some(o => o.target !== 'cli');
91
+ const own = ownsConfig ? [meta.configKey ?? commandName(meta.command)] : [];
92
+ const keys = [...own, ...(meta.configKeys ?? [])];
93
+ return keys.length ? keys : undefined;
94
+ }