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
@@ -1,6 +1,7 @@
1
- import type { Package } from '../core/package.js';
2
- import type { Repository } from '../core/repository.js';
3
- import { VersionScheme } from '../core/version-scheme.js';
1
+ import { RmanApplication } from '../core/application.js';
2
+ import type { Package } from '../core/classes/package.js';
3
+ import type { Repository } from '../core/classes/repository.js';
4
+ import { VersionScheme } from '../core/classes/version-scheme.js';
4
5
  import { type CommitInfo, GitHelper } from '../utils/git.js';
5
6
  import { type PackageFilterOptions } from '../utils/package-filter.js';
6
7
  /**
@@ -97,6 +98,31 @@ export declare abstract class VersionPlanService {
97
98
  * wrong one here is invisible - it produces a plan that simply releases too little.
98
99
  */
99
100
  protected abstract cascade(bump: string): VersionPlanService.Cascade;
101
+ /**
102
+ * **Which planner answers for one package: its own technology's.**
103
+ *
104
+ * The two abstract members above are the ecosystem's, and a repository can hold more than one.
105
+ * `app.versionPlanner` is a single slot - last registration wins - so in a polyglot repository
106
+ * both of them used to be answered by whichever plugin happened to register last: a Cargo
107
+ * package's boundary fell back to `npm view`, and its cascade assumed npm's caret ranges. That is
108
+ * the same shape of bug the hardcoded `['npm']` publish default was, and the same fix - ask the
109
+ * technology that read the manifest.
110
+ *
111
+ * Falls back to `this`, which is what a single-technology repository always gets and what a
112
+ * `Plugin` contributing no planner of its own means.
113
+ */
114
+ protected plannerFor(pkg: Package): VersionPlanService;
115
+ /**
116
+ * `cascade` for a whole group, which is the unit it is asked about - one answer applies to every
117
+ * member, because the group releases as one version line.
118
+ *
119
+ * **Members disagreeing take the widest answer**, and the direction is deliberate: a cascade that
120
+ * is too narrow produces a plan that silently *releases too little* (the failure `cascade`'s own
121
+ * doc calls invisible), while one that is too wide releases a package that did not strictly need
122
+ * it - visible, and harmless. A mixed-technology group is unusual; a mixed group quietly skipping
123
+ * a dependent that pins exact versions is a broken install.
124
+ */
125
+ protected cascadeFor(members: Package[], bump: string): VersionPlanService.Cascade;
100
126
  /**
101
127
  * The largest bump `commits` ask for, in `scheme`'s own names.
102
128
  *
@@ -215,7 +241,11 @@ export declare namespace VersionPlanService {
215
241
  group: string;
216
242
  status: 'bump' | 'skip' | 'error' | 'no-change';
217
243
  from: string;
218
- /** Only set when `status === 'bump'`. */
244
+ /** The version this package would receive. Set for `'bump'`, and for an `'error'` entry that
245
+ * earned one - a package with uncommitted changes aborts the run, and `--show` exists to say
246
+ * what it would get. **Never** set for `'skip'`: `--ignore-dirty` proceeds to write, and a
247
+ * version beside an excluded package would name one it is not getting. So `status`, not `to`,
248
+ * is what decides whether something is applied. */
219
249
  to?: string;
220
250
  /** Human-readable explanation - e.g. why a package was skipped, or why it's being bumped
221
251
  * despite having no commits of its own (a dependency of it changed elsewhere). */
@@ -230,9 +260,6 @@ export declare namespace VersionPlanService {
230
260
  * repository layering its own policy plugin after `rman-node` could never take effect - which is
231
261
  * the only reason to name two in the first place.
232
262
  */
233
- function setPlanner(planner: VersionPlanService): void;
234
- /** For tests, which would otherwise leak a planner into every later case in the process. */
235
- function clearPlanner(): void;
236
263
  /**
237
264
  * The registered planner. **Throws** when there is none, rather than falling back to some
238
265
  * built-in default: `Manifest` and `Workspace` can degrade honestly (a package named after its
@@ -240,5 +267,5 @@ export declare namespace VersionPlanService {
240
267
  * merely a diminished one - a wrong cascade or a wrong boundary reports a release that is
241
268
  * plausible and untrue.
242
269
  */
243
- function getPlanner(): VersionPlanService;
270
+ function getPlanner(app: RmanApplication): VersionPlanService;
244
271
  }
@@ -1,8 +1,10 @@
1
1
  import path from 'node:path';
2
- import { assertOneScheme, semverScheme, VersionScheme } from '../core/version-scheme.js';
2
+ import { RmanApplication } from '../core/application.js';
3
+ import { assertOneScheme, semverScheme, VersionScheme } from '../core/classes/version-scheme.js';
3
4
  import { GitHelper } from '../utils/git.js';
4
5
  import { filterPackages } from '../utils/package-filter.js';
5
6
  import { findLastReleaseVersion, formatCalendarVersion, usesCalendarVersion } from '../utils/release-version.js';
7
+ import { groupKeyOf } from '../utils/version-group.js';
6
8
  import { ConventionalCommitsService } from './conventional-commits.service.js';
7
9
  /**
8
10
  * Question A, computed and nothing else: **which packages have changed since their last release,
@@ -95,21 +97,39 @@ export class VersionPlanService {
95
97
  const packages = filterPackages(repository.getPackages(), options);
96
98
  const dirtyFiles = await git.listDirtyFiles({ absolute: true });
97
99
  const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
100
+ const dirty = packages.filter(isDirty);
98
101
  const entries = new Map();
99
- const eligible = [];
100
- for (const pkg of packages) {
101
- if (isDirty(pkg)) {
102
- entries.set(pkg.name, {
103
- package: pkg,
104
- groupKey: this.resolveGroupKey(pkg),
105
- group: this.groupLabel(this.resolveGroupKey(pkg)),
106
- status: options.ignoreDirty ? 'skip' : 'error',
107
- from: pkg.version,
108
- reason: 'uncommitted local changes',
109
- });
110
- continue;
111
- }
112
- eligible.push(pkg);
102
+ /**
103
+ * **A dirty package is removed from the plan only when the run is going to proceed without it.**
104
+ * The two answers are not symmetric, and treating them as one is what left the `To` column
105
+ * blank on the command that exists to show it.
106
+ *
107
+ * - `--ignore-dirty` **writes**. The package is deliberately excluded, so it must not reach a
108
+ * group either: a number its clean siblings receive cannot be derived from commits nobody is
109
+ * releasing, and a `to` printed beside `skip` would name a version that package is not
110
+ * getting. Diverted here, before `eligible`.
111
+ * - Aborting writes **nothing** - `getPlan` is pure and the command throws after printing - so
112
+ * there is no number to keep safe, and the one thing the reader wants is what each package
113
+ * would have got once it is committed. The plan is computed in full and the dirty entries are
114
+ * marked at the very end, below.
115
+ *
116
+ * Excluding them from the abort case cost more than the column. `buildRootEntry` reads
117
+ * `e.to ?? e.from`, and its `groupCount` comes from the groups that were actually built - so a
118
+ * dirty tree quietly moved the repository's own release identity: measured on a four-package
119
+ * repo with `group: false`, three dirty packages left one group standing, which is
120
+ * `usesCalendarVersion`'s "one line" case, and the root reported `0.0.5 -> 2.1.6` on the
121
+ * *semver* path instead of the calendar version it takes with four.
122
+ */
123
+ const eligible = options.ignoreDirty ? packages.filter(pkg => !dirty.includes(pkg)) : packages;
124
+ for (const pkg of options.ignoreDirty ? dirty : []) {
125
+ entries.set(pkg.name, {
126
+ package: pkg,
127
+ groupKey: this.resolveGroupKey(pkg),
128
+ group: this.groupLabel(this.resolveGroupKey(pkg)),
129
+ status: 'skip',
130
+ from: pkg.version,
131
+ reason: 'uncommitted local changes',
132
+ });
113
133
  }
114
134
  const commitMessage = repository.rootPackage.config?.version?.commitMessage;
115
135
  const changeByPackage = new Map();
@@ -118,7 +138,7 @@ export class VersionPlanService {
118
138
  changeByPackage.set(pkg.name, { bump: undefined, reason: `explicit version ${explicitVersion}` });
119
139
  return;
120
140
  }
121
- const since = await this.detectBoundary(git, pkg, options);
141
+ const since = await this.plannerFor(pkg).detectBoundary(git, pkg, options);
122
142
  const commits = since ? await git.listCommits({ hash: since }) : await git.listAllCommits();
123
143
  const belongsToPkg = (c) => c.files.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
124
144
  const real = commits.filter(c => belongsToPkg(c) && !ConventionalCommitsService.isReleaseCommit(c.subject, commitMessage));
@@ -150,8 +170,60 @@ export class VersionPlanService {
150
170
  now: options.now ?? (() => new Date()),
151
171
  }));
152
172
  }
173
+ /**
174
+ * The abort case, marked **after** the root entry so the whole table reads as one coherent
175
+ * "if this were committed" preview rather than a mixture of two.
176
+ *
177
+ * `to` survives the overwrite; `status` is what every writer filters on (`VersionService`,
178
+ * `changed`), so an `'error'` entry carrying a version cannot be applied by any of them - and
179
+ * `version` throws on the first error before it would reach `applyPlan` anyway. The reason
180
+ * keeps the boundary it was computed from, because the status column already says `error` and
181
+ * a reader losing `changed since <tag>` loses the only thing that explains the number.
182
+ */
183
+ if (!options.ignoreDirty) {
184
+ for (const pkg of dirty) {
185
+ const entry = entries.get(pkg.name);
186
+ if (!entry)
187
+ continue;
188
+ entry.status = 'error';
189
+ entry.reason = entry.reason ? `uncommitted local changes (${entry.reason})` : 'uncommitted local changes';
190
+ }
191
+ }
153
192
  return result;
154
193
  }
194
+ /**
195
+ * **Which planner answers for one package: its own technology's.**
196
+ *
197
+ * The two abstract members above are the ecosystem's, and a repository can hold more than one.
198
+ * `app.versionPlanner` is a single slot - last registration wins - so in a polyglot repository
199
+ * both of them used to be answered by whichever plugin happened to register last: a Cargo
200
+ * package's boundary fell back to `npm view`, and its cascade assumed npm's caret ranges. That is
201
+ * the same shape of bug the hardcoded `['npm']` publish default was, and the same fix - ask the
202
+ * technology that read the manifest.
203
+ *
204
+ * Falls back to `this`, which is what a single-technology repository always gets and what a
205
+ * `Plugin` contributing no planner of its own means.
206
+ */
207
+ plannerFor(pkg) {
208
+ return pkg.platform.versionPlanner ?? this;
209
+ }
210
+ /**
211
+ * `cascade` for a whole group, which is the unit it is asked about - one answer applies to every
212
+ * member, because the group releases as one version line.
213
+ *
214
+ * **Members disagreeing take the widest answer**, and the direction is deliberate: a cascade that
215
+ * is too narrow produces a plan that silently *releases too little* (the failure `cascade`'s own
216
+ * doc calls invisible), while one that is too wide releases a package that did not strictly need
217
+ * it - visible, and harmless. A mixed-technology group is unusual; a mixed group quietly skipping
218
+ * a dependent that pins exact versions is a broken install.
219
+ */
220
+ cascadeFor(members, bump) {
221
+ const answers = new Set(members.map(m => this.plannerFor(m).cascade(bump)));
222
+ for (const widest of CASCADE_WIDEST_FIRST)
223
+ if (answers.has(widest))
224
+ return widest;
225
+ return 'changed';
226
+ }
155
227
  /**
156
228
  * The largest bump `commits` ask for, in `scheme`'s own names.
157
229
  *
@@ -196,12 +268,7 @@ export class VersionPlanService {
196
268
  * group; a non-empty string joins exactly the other packages sharing that string, regardless of
197
269
  * the repo's own default; `false` makes it a solo group of one. */
198
270
  resolveGroupKey(pkg) {
199
- const g = pkg.config?.group;
200
- if (g === false)
201
- return `solo:${pkg.name}`;
202
- if (typeof g === 'string' && g)
203
- return `named:${g}`;
204
- return 'default';
271
+ return groupKeyOf(pkg);
205
272
  }
206
273
  /** The human-readable name behind a `groupKey` - what a plan's `group` column shows. */
207
274
  groupLabel(key) {
@@ -246,7 +313,7 @@ export class VersionPlanService {
246
313
  to = scheme.next(current, bump, { preid });
247
314
  }
248
315
  const bumping = new Set(changed);
249
- const cascade = bump ? this.cascade(bump) : 'changed';
316
+ const cascade = bump ? this.cascadeFor(members, bump) : 'changed';
250
317
  if (cascade === 'group') {
251
318
  for (const m of members)
252
319
  bumping.add(m);
@@ -377,15 +444,6 @@ export class VersionPlanService {
377
444
  * repository layering its own policy plugin after `rman-node` could never take effect - which is
378
445
  * the only reason to name two in the first place.
379
446
  */
380
- function setPlanner(planner) {
381
- current = planner;
382
- }
383
- VersionPlanService.setPlanner = setPlanner;
384
- /** For tests, which would otherwise leak a planner into every later case in the process. */
385
- function clearPlanner() {
386
- current = undefined;
387
- }
388
- VersionPlanService.clearPlanner = clearPlanner;
389
447
  /**
390
448
  * The registered planner. **Throws** when there is none, rather than falling back to some
391
449
  * built-in default: `Manifest` and `Workspace` can degrade honestly (a package named after its
@@ -393,16 +451,18 @@ export class VersionPlanService {
393
451
  * merely a diminished one - a wrong cascade or a wrong boundary reports a release that is
394
452
  * plausible and untrue.
395
453
  */
396
- function getPlanner() {
397
- if (!current) {
454
+ function getPlanner(app) {
455
+ const planner = app.versionPlanner;
456
+ if (!planner) {
398
457
  throw new Error('No version planner is registered, so no version plan can be computed. Name a plugin that ' +
399
458
  'contributes one in .rmanrc "plugins" - "rman-node" for a Node repository.');
400
459
  }
401
- return current;
460
+ return planner;
402
461
  }
403
462
  VersionPlanService.getPlanner = getPlanner;
404
- let current;
405
463
  })(VersionPlanService || (VersionPlanService = {}));
464
+ /** Widest reach first - what `cascadeFor` walks when a group's members answer differently. */
465
+ const CASCADE_WIDEST_FIRST = ['group', 'dependents', 'changed'];
406
466
  /** What one commit says happened, with no reference to any version format - `VersionScheme.bumpFor`
407
467
  * turns it into a number's movement. Everything unrecognized is a `'fix'`: something changed, so
408
468
  * the smallest release is still warranted. */
@@ -1,17 +1,41 @@
1
- import type { Package } from '../core/package.js';
2
- import type { Repository } from '../core/repository.js';
3
- import type { RunStepValue } from '../core/run-step.js';
1
+ import type { Package } from '../core/classes/package.js';
2
+ import type { Repository } from '../core/classes/repository.js';
3
+ import { Service } from '../core/classes/service.js';
4
+ import type { RunStepValue } from '../core/interfaces/run-step.js';
4
5
  import { VersionPlanService } from './version-plan.service.js';
5
6
  /**
6
7
  * Applying a version plan: the writes. Every manifest edit, dependency-range refresh, stamp,
7
8
  * commit and tag lives here; **what** to write is `VersionPlanService`'s answer.
8
9
  *
9
10
  * Nothing here knows what a `package.json` is, names an npm script, or runs a command: the version
10
- * goes through `Manifest`/`ManifestProvider`, refreshing a sibling's dependency range goes through
11
+ * goes through `Manifest`/`Plugin`'s manifest members, refreshing a sibling's dependency range goes through
11
12
  * the same provider, and the lifecycle hooks around the write go through
12
13
  * `RunService.runLifecycleSlot` - this module only supplies the `.rmanrc version.<slot>` fallback,
13
14
  * which is its own config. What is left is git, that config, and the version stamps.
14
15
  */
16
+ /**
17
+ * A service class - see `ListService` for the shape and `Service` for the three measured
18
+ * consequences a namespace had.
19
+ *
20
+ * **Only `applyPlan` became a method**, because only it takes a repository. `buildCommitMessage`,
21
+ * `stampDockerfile`, `stampSourceFiles` and `normalizeScriptValue` take a `Package` or nothing and
22
+ * stay functions on the namespace below - the same rule that leaves `ChangeHashService` a namespace
23
+ * entirely.
24
+ *
25
+ * **Declared before the namespace**, which TypeScript requires: the other order is
26
+ * `A namespace declaration cannot be located prior to a class with which it is merged`.
27
+ */
28
+ export declare class VersionService extends Service {
29
+ /**
30
+ * Writes every `'bump'` entry's new version into its own manifest (and refreshes any other bumped
31
+ * package's dependency range on it), runs that package's own version-lifecycle hooks or its
32
+ * `.rmanrc version.before`/`.exec`/`.after` around the write - see the `hook` closure below -
33
+ * then commits and tags **once per group** - so independently-versioned groups each get their own clean commit/tag rather than one entangled
34
+ * commit spanning unrelated version lines. Pushes only when `options.push` is set - same as a
35
+ * plain `npm version`, this never reaches the network on its own otherwise.
36
+ */
37
+ applyPlan(plan: VersionPlanService.Entry[], options?: VersionService.ApplyOptions): Promise<VersionService.ApplyResult>;
38
+ }
15
39
  export declare namespace VersionService {
16
40
  interface ApplyOptions {
17
41
  /** Push the resulting commit(s) and tag(s) to the remote once applied. Default false - same
@@ -61,15 +85,6 @@ export declare namespace VersionService {
61
85
  /** True for the repository's own release tag, which belongs to no single package. */
62
86
  release?: boolean;
63
87
  }
64
- /**
65
- * Writes every `'bump'` entry's new version into its own manifest (and refreshes any other bumped
66
- * package's dependency range on it), runs that package's own version-lifecycle hooks or its
67
- * `.rmanrc version.before`/`.exec`/`.after` around the write - see the `hook` closure below -
68
- * then commits and tags **once per group** - so independently-versioned groups each get their own clean commit/tag rather than one entangled
69
- * commit spanning unrelated version lines. Pushes only when `options.push` is set - same as a
70
- * plain `npm version`, this never reaches the network on its own otherwise.
71
- */
72
- function applyPlan(repository: Repository, plan: VersionPlanService.Entry[], options?: ApplyOptions): Promise<ApplyResult>;
73
88
  /** `.rmanrc version.commitMessage` (root-level; `{version}` is replaced when every bumped package
74
89
  * in this commit shares one version) - defaults to `"chore(release): v{version}"`, or a plain
75
90
  * listing of `name@version` pairs when this particular commit spans different versions (a
@@ -119,8 +134,13 @@ export declare namespace VersionService {
119
134
  * pattern missed it) released a tagged commit with a stale constant and said nothing. The error
120
135
  * names the file and, when the package's ecosystem declared none, says so.
121
136
  *
122
- * *How* a version is declared is the provider's (`ManifestProvider.stampVersion`); *which* files
137
+ * *How* a version is declared is the provider's (`Plugin.stampVersion`); *which* files
123
138
  * hold one is the repository's, which is why the list is config and the rewrite is a seam.
124
139
  */
125
140
  function stampSourceFiles(pkg: Package, version: string): string[];
126
141
  }
142
+ declare module '../core/classes/service.js' {
143
+ interface ServiceMap {
144
+ version: VersionService;
145
+ }
146
+ }
@@ -1,12 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { interpolateConfig } from '../core/config.js';
4
- import { Manifest } from '../core/manifest.js';
3
+ import { Service } from '../core/classes/service.js';
4
+ import { ConfigInterpolator } from '../core/config/config-interpolator.js';
5
+ import { Manifest } from '../core/interfaces/manifest.js';
5
6
  import { GitHelper } from '../utils/git.js';
6
7
  import { expandReleaseTag, isCalendarVersion } from '../utils/release-version.js';
7
8
  import { stampVersionLabel } from '../utils/version-stamp.js';
8
9
  import { ChangeHashService } from './change-hash.service.js';
9
- import { ChangelogService } from './changelog.service.js';
10
10
  import { RunService } from './run.service.js';
11
11
  import { VersionPlanService } from './version-plan.service.js';
12
12
  /**
@@ -14,13 +14,24 @@ import { VersionPlanService } from './version-plan.service.js';
14
14
  * commit and tag lives here; **what** to write is `VersionPlanService`'s answer.
15
15
  *
16
16
  * Nothing here knows what a `package.json` is, names an npm script, or runs a command: the version
17
- * goes through `Manifest`/`ManifestProvider`, refreshing a sibling's dependency range goes through
17
+ * goes through `Manifest`/`Plugin`'s manifest members, refreshing a sibling's dependency range goes through
18
18
  * the same provider, and the lifecycle hooks around the write go through
19
19
  * `RunService.runLifecycleSlot` - this module only supplies the `.rmanrc version.<slot>` fallback,
20
20
  * which is its own config. What is left is git, that config, and the version stamps.
21
21
  */
22
- export var VersionService;
23
- (function (VersionService) {
22
+ /**
23
+ * A service class - see `ListService` for the shape and `Service` for the three measured
24
+ * consequences a namespace had.
25
+ *
26
+ * **Only `applyPlan` became a method**, because only it takes a repository. `buildCommitMessage`,
27
+ * `stampDockerfile`, `stampSourceFiles` and `normalizeScriptValue` take a `Package` or nothing and
28
+ * stay functions on the namespace below - the same rule that leaves `ChangeHashService` a namespace
29
+ * entirely.
30
+ *
31
+ * **Declared before the namespace**, which TypeScript requires: the other order is
32
+ * `A namespace declaration cannot be located prior to a class with which it is merged`.
33
+ */
34
+ export class VersionService extends Service {
24
35
  /**
25
36
  * Writes every `'bump'` entry's new version into its own manifest (and refreshes any other bumped
26
37
  * package's dependency range on it), runs that package's own version-lifecycle hooks or its
@@ -29,7 +40,8 @@ export var VersionService;
29
40
  * commit spanning unrelated version lines. Pushes only when `options.push` is set - same as a
30
41
  * plain `npm version`, this never reaches the network on its own otherwise.
31
42
  */
32
- async function applyPlan(repository, plan, options = {}) {
43
+ async applyPlan(plan, options = {}) {
44
+ const repository = this.repository;
33
45
  const git = new GitHelper({ cwd: repository.dirname });
34
46
  // The root's own entry is only ever a real package to write/commit like any other when this
35
47
  // *isn't* a monorepo (see `getPlan`) - in a monorepo it's the separate, purely informational
@@ -53,6 +65,9 @@ export var VersionService;
53
65
  */
54
66
  for (const entry of bumped)
55
67
  assertStampable(entry.package, entry.to);
68
+ /** One for the whole run: it carries no per-package state, and `defer(...)`'s bookkeeping is
69
+ * per call. */
70
+ const interpolator = new ConfigInterpolator();
56
71
  for (const entry of bumped) {
57
72
  const pkg = entry.package;
58
73
  /** The scope these hooks are evaluated against - the only place `${{ pkg.targetVersion }}`
@@ -68,20 +83,23 @@ export var VersionService;
68
83
  const hook = (slot) => RunService.runLifecycleSlot(pkg, VERSION_LIFECYCLE, slot, RunService.normalizeScriptValue(
69
84
  /** `at`: the path is what tells a step function from a value one, and this is a fragment -
70
85
  * without it a function here was called while the hook was being prepared. */
71
- interpolateConfig(pkg.config?.version?.[slot], scope, { at: ['version', slot] }), `version.${slot}`));
86
+ interpolator.interpolate({ config: pkg.config?.version?.[slot], scope, at: ['version', slot] }), `version.${slot}`));
72
87
  await hook('before');
73
88
  /** Through the manifest, not through a `package.json` field: where a version is written is
74
- * the provider's business (see `ManifestProvider`), and this is the one place rman changes
89
+ * the provider's business (see `Plugin`'s manifest members), and this is the one place rman changes
75
90
  * it. */
76
91
  pkg.manifest.version = entry.to;
77
92
  /** Which fields hold a sibling reference, and what a reference even looks like, is the
78
93
  * ecosystem's - npm's four fields and its `"workspace:"` protocol used to be spelled out
79
- * here. See `ManifestProvider.updateDependencyVersions`. */
94
+ * here. See `Plugin.updateDependencyVersions`. */
80
95
  Manifest.updateDependencyVersions(pkg, bumpedVersions);
81
96
  await hook('exec');
82
97
  pkg.writeManifest();
83
98
  // Before the `after` hook, so a script reacting to the bump sees the whole new state.
84
- const stamped = [stampDockerfile(pkg, entry.to), ...stampSourceFiles(pkg, entry.to)].filter((f) => !!f);
99
+ const stamped = [
100
+ VersionService.stampDockerfile(pkg, entry.to),
101
+ ...VersionService.stampSourceFiles(pkg, entry.to),
102
+ ].filter((f) => !!f);
85
103
  if (stamped.length) {
86
104
  stampedByPackage.set(pkg.name, stamped.map(f => path.relative(repository.dirname, f)));
87
105
  }
@@ -105,9 +123,9 @@ export var VersionService;
105
123
  for (const entry of bumped) {
106
124
  const fromTag = ChangeHashService.expandTag(entry.package, entry.from);
107
125
  const from = (await git.tagExists(fromTag)) ? fromTag : undefined;
108
- const changelogEntries = await ChangelogService.generateToFile(repository, {
126
+ const changelogEntries = await this.app.getService('changelog').generateToFile({
109
127
  scope: entry.package.name,
110
- root: true,
128
+ fromRoot: true,
111
129
  from,
112
130
  // The tag for this release doesn't exist yet (it's created below), so changelog's own
113
131
  // tag-derived version would resolve to the *previous* release and label the entry with it.
@@ -152,7 +170,7 @@ export var VersionService;
152
170
  files.push(changelogFile);
153
171
  files.push(...(stampedByPackage.get(e.package.name) ?? []));
154
172
  }
155
- const message = buildCommitMessage(repository, groupEntries, options.message);
173
+ const message = VersionService.buildCommitMessage(repository, groupEntries, options.message);
156
174
  const sha = await git.commit(files, message);
157
175
  commits.push({ sha, message, packages: groupEntries.map(e => e.package.name) });
158
176
  const tags = new Set(groupEntries.map(e => ChangeHashService.expandTag(e.package, e.to)));
@@ -182,7 +200,8 @@ export var VersionService;
182
200
  await git.push();
183
201
  return { entries: plan, updated: bumped, commits, tags: tagged, pushed };
184
202
  }
185
- VersionService.applyPlan = applyPlan;
203
+ }
204
+ (function (VersionService) {
186
205
  /** `.rmanrc version.commitMessage` (root-level; `{version}` is replaced when every bumped package
187
206
  * in this commit shares one version) - defaults to `"chore(release): v{version}"`, or a plain
188
207
  * listing of `name@version` pairs when this particular commit spans different versions (a
@@ -260,7 +279,7 @@ export var VersionService;
260
279
  * pattern missed it) released a tagged commit with a stale constant and said nothing. The error
261
280
  * names the file and, when the package's ecosystem declared none, says so.
262
281
  *
263
- * *How* a version is declared is the provider's (`ManifestProvider.stampVersion`); *which* files
282
+ * *How* a version is declared is the provider's (`Plugin.stampVersion`); *which* files
264
283
  * hold one is the repository's, which is why the list is config and the rewrite is a seam.
265
284
  */
266
285
  function stampSourceFiles(pkg, version) {
package/services.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ export { DockerPublishService } from './builtins/publish-targets/docker/docker-publish.service.js';
1
2
  export { ChangelogService } from './services/changelog.service.js';
2
- export { DockerPublishService } from './services/docker-publish.service.js';
3
3
  export { ExecService } from './services/exec.service.js';
4
4
  export { GithubReleaseService } from './services/github-release.service.js';
5
5
  export { ImportService } from './services/import.service.js';
package/services.js CHANGED
@@ -1,5 +1,5 @@
1
+ export { DockerPublishService } from './builtins/publish-targets/docker/docker-publish.service.js';
1
2
  export { ChangelogService } from './services/changelog.service.js';
2
- export { DockerPublishService } from './services/docker-publish.service.js';
3
3
  export { ExecService } from './services/exec.service.js';
4
4
  export { GithubReleaseService } from './services/github-release.service.js';
5
5
  export { ImportService } from './services/import.service.js';
@@ -1,3 +1,4 @@
1
+ import { RmanApplication } from '../core/application.js';
1
2
  /**
2
3
  * Where a repository's **locally installed executables** live, so a command an author wrote
3
4
  * (`eslint .`, `cargo build`) is found the way it would be in their own shell.
@@ -6,7 +7,7 @@
6
7
  *
7
8
  * - **Which directories** is the *ecosystem's* answer, and the core has none - `node_modules/.bin`
8
9
  * walked up the directory chain is npm's layout and nothing else's (a Python venv says
9
- * `.venv/bin`, a Ruby project `bin`). `rman-node` contributes it; see `RmanPlugin.binPaths`.
10
+ * `.venv/bin`, a Ruby project `bin`). `rman-node` contributes it; see `Plugin.getBinPaths`.
10
11
  * - **How a PATH is spelled** is the *operating system's*, and that stays here: the variable is
11
12
  * `PATH` everywhere except Windows, where its case is whatever the environment happens to use.
12
13
  * That has nothing to do with any ecosystem, and every provider would otherwise get it wrong
@@ -22,20 +23,18 @@ export declare namespace BinPath {
22
23
  * Return them most-specific-first; the core concatenates providers without reordering. */
23
24
  type Provider = (cwd: string) => string[];
24
25
  interface EnvOptions {
26
+ /** The application whose technologies contribute directories. Omitted where a caller genuinely
27
+ * has none - `exec` outside a repository - which leaves the inherited PATH untouched. */
28
+ readonly app?: RmanApplication;
25
29
  /** The directory the command will run in. Default `process.cwd()`. */
26
30
  readonly cwd?: string;
27
31
  /** The environment to derive from, like `process.env`. Default `process.env`. */
28
32
  readonly env?: ProcessEnv;
29
33
  }
30
- /** Registers a provider. Called by `loadPlugins` for each plugin's `binPaths`, in `plugins`
31
- * declaration order - so what is on PATH is a function of the repository's own config. */
32
- function addProvider(provider: Provider): void;
33
- /** For tests, which would otherwise leak a provider into every later case in the process. */
34
- function clearProviders(): void;
35
34
  /** Every provider's directories for `cwd`, concatenated in declaration order. Empty for a
36
35
  * repository that names no plugin - the inherited PATH then stands on its own, which is the
37
36
  * honest answer rather than a guess at some ecosystem's layout. */
38
- function resolve(cwd: string): string[];
37
+ function resolve(app: RmanApplication, cwd: string): string[];
39
38
  /**
40
39
  * `env` with the contributed directories prepended to its PATH - what `exec` and `runBin` hand to
41
40
  * a child process.
package/utils/bin-path.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import process from 'node:process';
3
+ import { RmanApplication } from '../core/application.js';
3
4
  /**
4
5
  * Where a repository's **locally installed executables** live, so a command an author wrote
5
6
  * (`eslint .`, `cargo build`) is found the way it would be in their own shell.
@@ -8,7 +9,7 @@ import process from 'node:process';
8
9
  *
9
10
  * - **Which directories** is the *ecosystem's* answer, and the core has none - `node_modules/.bin`
10
11
  * walked up the directory chain is npm's layout and nothing else's (a Python venv says
11
- * `.venv/bin`, a Ruby project `bin`). `rman-node` contributes it; see `RmanPlugin.binPaths`.
12
+ * `.venv/bin`, a Ruby project `bin`). `rman-node` contributes it; see `Plugin.getBinPaths`.
12
13
  * - **How a PATH is spelled** is the *operating system's*, and that stays here: the variable is
13
14
  * `PATH` everywhere except Windows, where its case is whatever the environment happens to use.
14
15
  * That has nothing to do with any ecosystem, and every provider would otherwise get it wrong
@@ -20,25 +21,12 @@ import process from 'node:process';
20
21
  */
21
22
  export var BinPath;
22
23
  (function (BinPath) {
23
- /** Registers a provider. Called by `loadPlugins` for each plugin's `binPaths`, in `plugins`
24
- * declaration order - so what is on PATH is a function of the repository's own config. */
25
- function addProvider(provider) {
26
- if (providers.includes(provider))
27
- return;
28
- providers.push(provider);
29
- }
30
- BinPath.addProvider = addProvider;
31
- /** For tests, which would otherwise leak a provider into every later case in the process. */
32
- function clearProviders() {
33
- providers.length = 0;
34
- }
35
- BinPath.clearProviders = clearProviders;
36
24
  /** Every provider's directories for `cwd`, concatenated in declaration order. Empty for a
37
25
  * repository that names no plugin - the inherited PATH then stands on its own, which is the
38
26
  * honest answer rather than a guess at some ecosystem's layout. */
39
- function resolve(cwd) {
27
+ function resolve(app, cwd) {
40
28
  const dir = path.resolve(cwd);
41
- return providers.flatMap(provider => provider(dir));
29
+ return [...app.platforms].flatMap(stack => stack.getBinPaths?.(dir) ?? []);
42
30
  }
43
31
  BinPath.resolve = resolve;
44
32
  /**
@@ -52,7 +40,9 @@ export var BinPath;
52
40
  const cwd = options.cwd || process.cwd();
53
41
  const result = { ...(options.env || process.env) };
54
42
  const key = pathKey({ env: result });
55
- const entries = resolve(cwd);
43
+ /** No application means no technologies, so nothing is prepended and the inherited PATH stands
44
+ * on its own - the same answer a repository naming no plugin has always got. */
45
+ const entries = options.app ? resolve(options.app, cwd) : [];
56
46
  if (!entries.length)
57
47
  return result;
58
48
  const inherited = result[key];
@@ -78,5 +68,4 @@ export var BinPath;
78
68
  .find(key => key.toUpperCase() === 'PATH') || 'Path');
79
69
  }
80
70
  BinPath.pathKey = pathKey;
81
- const providers = [];
82
71
  })(BinPath || (BinPath = {}));
@@ -1,11 +1,40 @@
1
1
  import type { Argv } from 'yargs';
2
- import type { Repository } from '../core/repository.js';
2
+ import type { Repository } from '../core/classes/repository.js';
3
3
  /** Same idea as GitHub Actions' own `branches`/`branches-ignore` workflow filters - restricts a
4
4
  * command to running only from certain branches (or refuses certain ones), instead of any. */
5
5
  export interface BranchGuardOptions {
6
6
  allowBranch?: string | string[];
7
7
  ignoreBranch?: string | string[];
8
8
  }
9
+ /**
10
+ * `--allow-branch`/`--ignore-branch` as a declaration, to spread into a command's `config` block.
11
+ * See `packageFilterOptions` for why this is `satisfies` rather than an annotation.
12
+ *
13
+ * **The key is the camelCase one and `cliName` carries the flag's spelling.** `allowBranch` is what
14
+ * a `.rmanrc` writes and what `assertAllowedBranch` reads off the config; `--allow-branch` is what
15
+ * the shell sees. The `Argv` version relied on yargs converting one into the other silently, which
16
+ * is why `applyBranchGuardOptions` declares `'allow-branch'` and `readBranchGuardOptions` reads
17
+ * `args.allowBranch`.
18
+ *
19
+ * **`target: 'cli'`, even though `allowBranch` *is* a config key.** It is a repo-wide core key read
20
+ * off the root, not any command's - so it belongs on `RmanConfig` directly, and a command that
21
+ * reads it names it in `configKeys`. Declaring it `'both'` here would contribute `version.allowBranch`,
22
+ * which nothing reads.
23
+ */
24
+ export declare const branchGuardOptions: {
25
+ allowBranch: {
26
+ target: "cli";
27
+ cliName: string;
28
+ describe: string;
29
+ type: "string";
30
+ };
31
+ ignoreBranch: {
32
+ target: "cli";
33
+ cliName: string;
34
+ describe: string;
35
+ type: "string";
36
+ };
37
+ };
9
38
  /** `--allow-branch`/`--ignore-branch`, the same shape and describe text in every command that
10
39
  * supports them - mirrors `package-filter.ts`'s own `applyPackageFilterOptions`. */
11
40
  export declare function applyBranchGuardOptions<T>(cmd: Argv<T>): Argv<T>;