rman 2.0.0-beta.2 → 2.0.0-beta.4

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 (162) hide show
  1. package/README.md +2 -12
  2. package/{plugins/node/augmentation/rman.augmentation.d.ts → builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts} +4 -5
  3. package/{plugins → builtins/platforms}/node/augmentation/system-info.augmentation.js +1 -1
  4. package/{plugins → builtins/platforms}/node/commands/ci.command.d.ts +5 -3
  5. package/{plugins → builtins/platforms}/node/commands/ci.command.js +6 -5
  6. package/{plugins → builtins/platforms}/node/commands/clean.command.d.ts +6 -4
  7. package/{plugins → builtins/platforms}/node/commands/clean.command.js +6 -5
  8. package/builtins/platforms/node/index.d.ts +8 -0
  9. package/builtins/platforms/node/index.js +8 -0
  10. package/{plugins → builtins/platforms}/node/node-config.interface.d.ts +1 -2
  11. package/{plugins → builtins/platforms}/node/node-manifest.provider.d.ts +3 -3
  12. package/{plugins → builtins/platforms}/node/node-manifest.provider.js +3 -3
  13. package/{plugins → builtins/platforms}/node/node.platform.d.ts +3 -3
  14. package/{plugins → builtins/platforms}/node/services/ci.service.d.ts +3 -3
  15. package/{plugins → builtins/platforms}/node/services/ci.service.js +4 -4
  16. package/{plugins → builtins/platforms}/node/services/clean.service.d.ts +3 -3
  17. package/{plugins → builtins/platforms}/node/services/clean.service.js +3 -3
  18. package/{plugins → builtins/platforms}/node/services/publish.service.d.ts +4 -4
  19. package/{plugins → builtins/platforms}/node/services/publish.service.js +13 -7
  20. package/{plugins → builtins/platforms}/node/services/version-plan.service.d.ts +3 -3
  21. package/{plugins → builtins/platforms}/node/services/version-plan.service.js +2 -2
  22. package/builtins/presets/index.d.ts +15 -0
  23. package/builtins/presets/index.js +57 -0
  24. package/builtins/presets/node.d.ts +16 -0
  25. package/builtins/presets/node.js +57 -0
  26. package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +6 -6
  27. package/{services → builtins/publish-targets/docker}/docker-publish.service.js +9 -9
  28. package/{targets → builtins/publish-targets/docker}/docker.target.d.ts +6 -5
  29. package/{targets → builtins/publish-targets/docker}/docker.target.js +1 -1
  30. package/builtins/publish-targets/docker/index.d.ts +2 -0
  31. package/builtins/publish-targets/docker/index.js +2 -0
  32. package/builtins/publish-targets/npm/index.d.ts +2 -0
  33. package/builtins/publish-targets/npm/index.js +2 -0
  34. package/{plugins/node → builtins/publish-targets/npm}/npm-publish-target.d.ts +4 -4
  35. package/{plugins/node → builtins/publish-targets/npm}/npm-publish-target.js +4 -5
  36. package/cli.d.ts +3 -0
  37. package/cli.js +16 -7
  38. package/commands/changelog.command.d.ts +106 -2
  39. package/commands/changelog.command.js +40 -0
  40. package/commands/config.command.js +23 -5
  41. package/commands/list.command.js +1 -1
  42. package/commands/publish.command.d.ts +1 -1
  43. package/commands/publish.command.js +1 -1
  44. package/commands/run.command.js +1 -1
  45. package/commands/version.command.d.ts +16 -3
  46. package/commands/version.command.js +143 -9
  47. package/commands.d.ts +0 -1
  48. package/commands.js +0 -1
  49. package/constants.js +1 -1
  50. package/core/application.d.ts +10 -10
  51. package/core/application.js +28 -11
  52. package/core/{package.d.ts → classes/package.d.ts} +12 -4
  53. package/core/{package.js → classes/package.js} +17 -1
  54. package/core/{repository.d.ts → classes/repository.d.ts} +22 -36
  55. package/core/{repository.js → classes/repository.js} +112 -241
  56. package/core/{service.d.ts → classes/service.d.ts} +2 -2
  57. package/core/classes/workspace.d.ts +277 -0
  58. package/core/classes/workspace.js +585 -0
  59. package/core/command-builder.js +17 -1
  60. package/core/config/config-file-scope.d.ts +107 -0
  61. package/core/config/config-file-scope.js +226 -0
  62. package/core/config/config-interpolator.d.ts +164 -0
  63. package/core/config/config-interpolator.js +577 -0
  64. package/core/config/config-paths.d.ts +65 -0
  65. package/core/config/config-paths.js +78 -0
  66. package/core/config/config-reader.d.ts +215 -0
  67. package/core/config/config-reader.js +851 -0
  68. package/core/{merge-config.js → config/merge-config.js} +4 -8
  69. package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +5 -5
  70. package/core/{manifest.d.ts → interfaces/manifest.d.ts} +3 -3
  71. package/core/{manifest.js → interfaces/manifest.js} +2 -2
  72. package/core/{plugin.d.ts → interfaces/plugin.d.ts} +44 -27
  73. package/core/{plugin.js → interfaces/plugin.js} +48 -10
  74. package/core/{publish-target.d.ts → interfaces/publish-target.d.ts} +24 -12
  75. package/core/{publish-target.js → interfaces/publish-target.js} +16 -3
  76. package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
  77. package/core/plugin-loader.d.ts +11 -51
  78. package/core/plugin-loader.js +21 -202
  79. package/index.d.ts +29 -28
  80. package/index.js +20 -46
  81. package/interfaces/config-scope.interface.d.ts +223 -0
  82. package/interfaces/config-scope.interface.js +1 -0
  83. package/interfaces/rman-config.interface.d.ts +100 -5
  84. package/interfaces/rman-config.interface.js +17 -0
  85. package/package.json +3 -2
  86. package/services/change-hash.service.d.ts +40 -6
  87. package/services/change-hash.service.js +90 -11
  88. package/services/changelog.service.d.ts +31 -3
  89. package/services/changelog.service.js +448 -65
  90. package/services/config-service.d.ts +8 -0
  91. package/services/config-service.js +3 -0
  92. package/services/exec.service.d.ts +2 -2
  93. package/services/exec.service.js +1 -1
  94. package/services/github-release.service.d.ts +3 -3
  95. package/services/github-release.service.js +3 -3
  96. package/services/import.service.d.ts +2 -2
  97. package/services/import.service.js +1 -1
  98. package/services/list.service.d.ts +9 -7
  99. package/services/list.service.js +2 -2
  100. package/services/run.service.d.ts +15 -11
  101. package/services/run.service.js +62 -18
  102. package/services/system-info.d.ts +1 -1
  103. package/services/version-plan.service.d.ts +8 -4
  104. package/services/version-plan.service.js +54 -21
  105. package/services/version.service.d.ts +5 -5
  106. package/services/version.service.js +7 -4
  107. package/services.d.ts +1 -1
  108. package/services.js +1 -1
  109. package/utils/branch-guard.d.ts +1 -1
  110. package/utils/color-yaml.d.ts +8 -0
  111. package/utils/color-yaml.js +86 -0
  112. package/utils/git.d.ts +26 -0
  113. package/utils/git.js +65 -0
  114. package/utils/logger.d.ts +1 -1
  115. package/utils/package-filter.d.ts +8 -1
  116. package/utils/package-filter.js +7 -0
  117. package/utils/printable-config.d.ts +8 -0
  118. package/utils/printable-config.js +45 -0
  119. package/utils/progress-panel.d.ts +9 -0
  120. package/utils/progress-panel.js +23 -1
  121. package/utils/release-version.d.ts +1 -1
  122. package/utils/run-options.js +1 -1
  123. package/utils/version-group.d.ts +31 -0
  124. package/utils/version-group.js +43 -0
  125. package/commands/changed.command.d.ts +0 -80
  126. package/commands/changed.command.js +0 -36
  127. package/core/config.d.ts +0 -530
  128. package/core/config.js +0 -1123
  129. package/core/core-services.d.ts +0 -14
  130. package/core/core-services.js +0 -30
  131. package/core/core-targets.d.ts +0 -14
  132. package/core/core-targets.js +0 -16
  133. package/core/extends-config.d.ts +0 -35
  134. package/core/extends-config.js +0 -129
  135. package/core/load-config-module.d.ts +0 -28
  136. package/core/load-config-module.js +0 -42
  137. package/core/workspace.d.ts +0 -119
  138. package/core/workspace.js +0 -124
  139. package/plugins/builtins.d.ts +0 -44
  140. package/plugins/builtins.js +0 -33
  141. package/plugins/detect.d.ts +0 -78
  142. package/plugins/detect.js +0 -70
  143. package/plugins/node/index.d.ts +0 -29
  144. package/plugins/node/index.js +0 -40
  145. /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
  146. /package/{plugins → builtins/platforms}/node/augmentation/system-info.augmentation.d.ts +0 -0
  147. /package/{plugins → builtins/platforms}/node/node-config.interface.js +0 -0
  148. /package/{plugins → builtins/platforms}/node/node.platform.js +0 -0
  149. /package/{plugins → builtins/platforms}/node/utils/workspace-range.d.ts +0 -0
  150. /package/{plugins → builtins/platforms}/node/utils/workspace-range.js +0 -0
  151. /package/{plugins/node/utils → builtins/publish-targets/npm}/npm-view.d.ts +0 -0
  152. /package/{plugins/node/utils → builtins/publish-targets/npm}/npm-view.js +0 -0
  153. /package/core/{registry.d.ts → classes/registry.d.ts} +0 -0
  154. /package/core/{registry.js → classes/registry.js} +0 -0
  155. /package/core/{service.js → classes/service.js} +0 -0
  156. /package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +0 -0
  157. /package/core/{version-scheme.js → classes/version-scheme.js} +0 -0
  158. /package/core/{merge-config.d.ts → config/merge-config.d.ts} +0 -0
  159. /package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +0 -0
  160. /package/core/{resolve-target.js → config/resolve-config-target.js} +0 -0
  161. /package/core/{custom-command.js → interfaces/custom-command.js} +0 -0
  162. /package/{plugins/node/augmentation/rman.augmentation.js → core/interfaces/run-step.js} +0 -0
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import fastGlob from 'fast-glob';
4
- import { Package } from '../core/package.js';
5
- import { Repository } from '../core/repository.js';
6
- import { Service } from '../core/service.js';
4
+ import { Package } from '../core/classes/package.js';
5
+ import { Repository } from '../core/classes/repository.js';
6
+ import { Service } from '../core/classes/service.js';
7
7
  import { GitHelper } from '../utils/git.js';
8
8
  import { expandReleaseTag, isCalendarVersion, releaseTagPattern } from '../utils/release-version.js';
9
9
  import { ChangeHashService } from './change-hash.service.js';
@@ -1,4 +1,4 @@
1
- import { Service } from '../core/service.js';
1
+ import { Service } from '../core/classes/service.js';
2
2
  /**
3
3
  * A service class - see `ListService` for the shape and `Service` for the three measured
4
4
  * consequences a namespace had. `repository` left the signature because the application carries it.
@@ -34,7 +34,7 @@ export declare namespace ImportService {
34
34
  commitCount: number;
35
35
  }
36
36
  }
37
- declare module '../core/service.js' {
37
+ declare module '../core/classes/service.js' {
38
38
  interface ServiceMap {
39
39
  import: ImportService;
40
40
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { Service } from '../core/service.js';
4
+ import { Service } from '../core/classes/service.js';
5
5
  import { GitHelper } from '../utils/git.js';
6
6
  /**
7
7
  * A service class - see `ListService` for the shape and `Service` for the three measured
@@ -1,7 +1,7 @@
1
- import { type PublishTargetName } from '../core/publish-target.js';
2
- import type { Repository } from '../core/repository.js';
3
- import { Service } from '../core/service.js';
4
- import type { DockerPublishOptions } from '../targets/docker.target.js';
1
+ import type { DockerPublishOptions } from '../builtins/publish-targets/docker/docker.target.js';
2
+ import type { Repository } from '../core/classes/repository.js';
3
+ import { Service } from '../core/classes/service.js';
4
+ import { type PublishTargetName } from '../core/interfaces/publish-target.js';
5
5
  import { type PackageFilterOptions } from '../utils/package-filter.js';
6
6
  /**
7
7
  * **A class, and the shape every service follows now.**
@@ -39,8 +39,10 @@ export declare namespace ListService {
39
39
  * what it saw before.
40
40
  */
41
41
  includeRoot?: boolean;
42
- /** Only include packages that have changed since the last publish (dirty or committed but
43
- * not yet published) - or, with `changedSince`, since that specific commit/hash. */
42
+ /** Only include packages the developer has touched but not pushed (dirty, or committed and not
43
+ * yet on the upstream branch) - or, with `changedSince`, since that specific commit/hash.
44
+ * **Question C, never a release question**: after a push `git cherry` is empty and everything
45
+ * reads `clean`, which does not mean there is nothing left to release. */
44
46
  changed?: boolean;
45
47
  changedSince?: string;
46
48
  }
@@ -79,7 +81,7 @@ export declare namespace ListService {
79
81
  docker?: DockerPublishOptions;
80
82
  }
81
83
  }
82
- declare module '../core/service.js' {
84
+ declare module '../core/classes/service.js' {
83
85
  interface ServiceMap {
84
86
  list: ListService;
85
87
  }
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
- import { targetsOf } from '../core/publish-target.js';
3
- import { Service } from '../core/service.js';
2
+ import { Service } from '../core/classes/service.js';
3
+ import { targetsOf } from '../core/interfaces/publish-target.js';
4
4
  import { filterPackages } from '../utils/package-filter.js';
5
5
  /**
6
6
  * **A class, and the shape every service follows now.**
@@ -1,7 +1,7 @@
1
- import type { Package } from '../core/package.js';
2
- import type { Repository } from '../core/repository.js';
3
- import type { RunStepContext, RunStepFn, RunStepValue } from '../core/run-step.js';
4
- import { Service } from '../core/service.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 { RunStepContext, RunStepFn, RunStepValue } from '../core/interfaces/run-step.js';
5
5
  import { type LogLevel } from '../utils/logger.js';
6
6
  import { type PackageFilterOptions } from '../utils/package-filter.js';
7
7
  /**
@@ -127,11 +127,15 @@ export declare namespace RunService {
127
127
  * Runs one slot of a lifecycle belonging to some operation other than `run` itself - `version`'s
128
128
  * hooks around the version write are the only one so far.
129
129
  *
130
- * **Here rather than in `VersionService`, because the rule it applies is this service's**: the
131
- * package's own declaration for `script` wins over the caller's `fallback`, slot by slot, exactly
132
- * as `getScriptSteps` decides it for `run`. Kept in two places that rule would drift, and one of
133
- * the copies would sit in the file that writes versions - which now runs no command of its own at
134
- * all.
130
+ * **Here rather than in `VersionService`, because the rule it applies is this service's**: how a
131
+ * package's own declaration for `script` and the caller's `fallback` combine, slot by slot -
132
+ * `slotValues`, the same function `getScriptSteps` decides it with. Kept in two places that rule
133
+ * would drift, and one of the copies would sit in the file that writes versions, which now runs
134
+ * no command of its own at all.
135
+ *
136
+ * So npm's `preversion` does **not** replace a repository's `.rmanrc version.before` any more;
137
+ * both run, the config's first. Only `version.exec` is a single answer, and there the package's
138
+ * own still wins.
135
139
  *
136
140
  * `fallback` is the caller's own configured step(s), **already evaluated**: `version`'s three
137
141
  * paths are in `DEFERRED_PATHS` precisely because only the caller can bind
@@ -172,7 +176,7 @@ export declare namespace RunService {
172
176
  *
173
177
  * run:
174
178
  * build:
175
- * if: changed # changed since the last publish
179
+ * if: changed # touched but not pushed
176
180
  * test:
177
181
  * if: changed = a1b2c3d # changed since a specific commit
178
182
  * if: changed = {CHANGE_HASH} # {NAME} -> process.env.NAME first
@@ -212,7 +216,7 @@ export declare function resolveBool(cliValue: boolean | undefined, pkg: Package,
212
216
  export declare function resolveBail(cliValue: boolean | undefined, pkg: Package, script: string, fallback: boolean): boolean;
213
217
  export declare function resolveNumber(cliValue: number | undefined, pkg: Package, script: string, key: string, fallback: number): number;
214
218
  export declare function resolveLogLevel(cliValue: LogLevel | undefined, pkg: Package, script: string, fallback: LogLevel): LogLevel;
215
- declare module '../core/service.js' {
219
+ declare module '../core/classes/service.js' {
216
220
  interface ServiceMap {
217
221
  run: RunService;
218
222
  }
@@ -3,7 +3,7 @@ import { inspect } from 'node:util';
3
3
  import colors from 'ansi-colors';
4
4
  import { tokenize } from 'fast-tokenizer';
5
5
  import { Task } from 'power-tasks';
6
- import { Service } from '../core/service.js';
6
+ import { Service } from '../core/classes/service.js';
7
7
  import { exec } from '../utils/exec.js';
8
8
  import { LOG_LEVELS, Logger, resolveRootLogLevel } from '../utils/logger.js';
9
9
  import { filterPackages } from '../utils/package-filter.js';
@@ -61,6 +61,12 @@ export class RunService extends Service {
61
61
  for (let i = 0; i < steps.length; i++) {
62
62
  const step = steps[i];
63
63
  ctx.currentStep = step.name;
64
+ /** The command for a shell step, the function's own name for a JS one - and a bare
65
+ * marker for an anonymous function, which is still worth saying: the slot label alone
66
+ * does not distinguish a JS step from a shell one. `step.label` is not used directly
67
+ * because for a function it already folds the slot in (`before (js)`), which the row
68
+ * prints two characters to its left. */
69
+ ctx.currentCommand = step.command ?? (step.run?.name ? `${step.run.name}()` : '(js function)');
64
70
  ctx.stepIndex = i;
65
71
  if (panel.enabled) {
66
72
  const onLine = (line) => {
@@ -275,11 +281,15 @@ export class RunService extends Service {
275
281
  * Runs one slot of a lifecycle belonging to some operation other than `run` itself - `version`'s
276
282
  * hooks around the version write are the only one so far.
277
283
  *
278
- * **Here rather than in `VersionService`, because the rule it applies is this service's**: the
279
- * package's own declaration for `script` wins over the caller's `fallback`, slot by slot, exactly
280
- * as `getScriptSteps` decides it for `run`. Kept in two places that rule would drift, and one of
281
- * the copies would sit in the file that writes versions - which now runs no command of its own at
282
- * all.
284
+ * **Here rather than in `VersionService`, because the rule it applies is this service's**: how a
285
+ * package's own declaration for `script` and the caller's `fallback` combine, slot by slot -
286
+ * `slotValues`, the same function `getScriptSteps` decides it with. Kept in two places that rule
287
+ * would drift, and one of the copies would sit in the file that writes versions, which now runs
288
+ * no command of its own at all.
289
+ *
290
+ * So npm's `preversion` does **not** replace a repository's `.rmanrc version.before` any more;
291
+ * both run, the config's first. Only `version.exec` is a single answer, and there the package's
292
+ * own still wins.
283
293
  *
284
294
  * `fallback` is the caller's own configured step(s), **already evaluated**: `version`'s three
285
295
  * paths are in `DEFERRED_PATHS` precisely because only the caller can bind
@@ -291,8 +301,7 @@ export class RunService extends Service {
291
301
  * semantics of the shell case too, since `cd x && y` in one process is not the same as two.
292
302
  */
293
303
  async function runLifecycleSlot(pkg, script, slot, fallback) {
294
- const own = contributedSlots(pkg, script)?.[slot] ?? [];
295
- const values = own.length ? own : (fallback ?? []);
304
+ const values = slotValues(slot, contributedSlots(pkg, script)?.[slot] ?? [], fallback ?? [], false);
296
305
  for (const value of values) {
297
306
  if (typeof value === 'function') {
298
307
  await value(createStepContext(pkg, pkg.dirname));
@@ -575,12 +584,14 @@ async function passesIf(repository, pkg, raw, cwd, statusCache) {
575
584
  *
576
585
  * 1. `run.<script>.override: true` -> the config, always. The escape hatch for "ignore what the
577
586
  * package says it does".
578
- * 2. a **contributed** source - `package.json#scripts` via `rman-node`. It wins over the config
579
- * because it is the *package's own* declaration, while a config value typically arrives
580
- * cascaded from the root or an `extends` base; the more specific statement wins, as everywhere
581
- * else in rman.
582
- * 3. the config's own `before`/`exec`/`after`, letting a package run a script it never declared.
583
- * 4. nothing - and an empty result is what tells `runScript` this package defines no such script.
587
+ * 2. for `exec`, a **contributed** source - `package.json#scripts` via `rman-node` - wins over the
588
+ * config, because the two are one answer to one question and the more specific statement wins.
589
+ * 3. for `before`/`after`, the two **compose**: they are hooks at a point rather than answers, so
590
+ * the config's bracket the package's own (`config.before -> prebuild -> build -> postbuild ->
591
+ * config.after`). See `slotValues` for the measurement behind that.
592
+ * 4. the config's own `before`/`exec`/`after` alone, letting a package run a script it never
593
+ * declared.
594
+ * 5. nothing - and an empty result is what tells `runScript` this package defines no such script.
584
595
  */
585
596
  function getScriptSteps(pkg, script) {
586
597
  const cfg = RunService.getConfig(pkg, script);
@@ -593,14 +604,47 @@ function getScriptSteps(pkg, script) {
593
604
  };
594
605
  const steps = [];
595
606
  for (const slot of SCRIPT_SLOTS) {
596
- const own = contributed?.[slot] ?? [];
597
- const configured = fromConfig[slot] ?? [];
598
- const values = override ? (configured.length ? configured : own) : own.length ? own : configured;
599
- for (const value of values)
607
+ for (const value of slotValues(slot, contributed?.[slot] ?? [], fromConfig[slot] ?? [], override)) {
600
608
  steps.push(toStep(slot, value));
609
+ }
601
610
  }
602
611
  return steps;
603
612
  }
613
+ /**
614
+ * The steps for one slot, from the package's own declaration and the config's, in the order they
615
+ * run.
616
+ *
617
+ * **`exec` replaces; `before` and `after` compose.** They are not the same kind of key, and
618
+ * treating them alike is what this function exists to stop:
619
+ *
620
+ * - `exec` is *one answer to one question* - what this script does. A package declaring
621
+ * `"build": "tsc -b"` and a config declaring `exec: 'tsc -b'` are the same build stated twice,
622
+ * so the more specific statement wins and the other is dropped.
623
+ * - `before`/`after` are *hooks at a point*, and two of them at one point both belong. npm's
624
+ * `prebuild` means "immediately before my own build"; `run.build.before` means "before this
625
+ * script, for every package". Neither answers the other.
626
+ *
627
+ * **Measured, on a two-line repository**: a root declaring
628
+ * `"[*]": { run: { build: { before: 'echo CONFIG-BEFORE', after: 'echo CONFIG-AFTER' } } }` and a
629
+ * package declaring `prebuild`/`build`/`postbuild` ran the package's three and **neither of the
630
+ * root's** - while the identical repository with `prebuild` deleted ran both. So adding an
631
+ * unrelated codegen hook to one package silently cancelled a repo-wide `rman clean`, with a stale
632
+ * build directory as the only symptom.
633
+ *
634
+ * **The config brackets the package's own**, which is what the wider statement means and the shape
635
+ * a bookend already has here: `config.before -> prebuild -> build -> postbuild -> config.after`.
636
+ *
637
+ * `override: true` is unchanged - it is the escape hatch for "ignore what the package says it
638
+ * does", so the config replaces rather than composes, per slot, falling back to the package's own
639
+ * where the config says nothing about that slot.
640
+ */
641
+ function slotValues(slot, own, configured, override) {
642
+ if (override)
643
+ return configured.length ? configured : own;
644
+ if (slot === 'exec')
645
+ return own.length ? own : configured;
646
+ return slot === 'before' ? [...configured, ...own] : [...own, ...configured];
647
+ }
604
648
  /**
605
649
  * What this package's own technology says it declares for `script`.
606
650
  *
@@ -1,5 +1,5 @@
1
1
  import envinfo from 'envinfo';
2
- import type { Repository } from '../core/repository.js';
2
+ import type { Repository } from '../core/classes/repository.js';
3
3
  export declare namespace SystemInfo {
4
4
  interface RepositoryInfo {
5
5
  type: 'monorepo' | 'package';
@@ -1,7 +1,7 @@
1
1
  import { RmanApplication } from '../core/application.js';
2
- import type { Package } from '../core/package.js';
3
- import type { Repository } from '../core/repository.js';
4
- import { VersionScheme } from '../core/version-scheme.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';
5
5
  import { type CommitInfo, GitHelper } from '../utils/git.js';
6
6
  import { type PackageFilterOptions } from '../utils/package-filter.js';
7
7
  /**
@@ -241,7 +241,11 @@ export declare namespace VersionPlanService {
241
241
  group: string;
242
242
  status: 'bump' | 'skip' | 'error' | 'no-change';
243
243
  from: string;
244
- /** 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. */
245
249
  to?: string;
246
250
  /** Human-readable explanation - e.g. why a package was skipped, or why it's being bumped
247
251
  * despite having no commits of its own (a dependency of it changed elsewhere). */
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { RmanApplication } from '../core/application.js';
3
- import { assertOneScheme, semverScheme, VersionScheme } from '../core/version-scheme.js';
3
+ import { assertOneScheme, semverScheme, VersionScheme } from '../core/classes/version-scheme.js';
4
4
  import { GitHelper } from '../utils/git.js';
5
5
  import { filterPackages } from '../utils/package-filter.js';
6
6
  import { findLastReleaseVersion, formatCalendarVersion, usesCalendarVersion } from '../utils/release-version.js';
7
+ import { groupKeyOf } from '../utils/version-group.js';
7
8
  import { ConventionalCommitsService } from './conventional-commits.service.js';
8
9
  /**
9
10
  * Question A, computed and nothing else: **which packages have changed since their last release,
@@ -96,21 +97,39 @@ export class VersionPlanService {
96
97
  const packages = filterPackages(repository.getPackages(), options);
97
98
  const dirtyFiles = await git.listDirtyFiles({ absolute: true });
98
99
  const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
100
+ const dirty = packages.filter(isDirty);
99
101
  const entries = new Map();
100
- const eligible = [];
101
- for (const pkg of packages) {
102
- if (isDirty(pkg)) {
103
- entries.set(pkg.name, {
104
- package: pkg,
105
- groupKey: this.resolveGroupKey(pkg),
106
- group: this.groupLabel(this.resolveGroupKey(pkg)),
107
- status: options.ignoreDirty ? 'skip' : 'error',
108
- from: pkg.version,
109
- reason: 'uncommitted local changes',
110
- });
111
- continue;
112
- }
113
- 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
+ });
114
133
  }
115
134
  const commitMessage = repository.rootPackage.config?.version?.commitMessage;
116
135
  const changeByPackage = new Map();
@@ -151,6 +170,25 @@ export class VersionPlanService {
151
170
  now: options.now ?? (() => new Date()),
152
171
  }));
153
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
+ }
154
192
  return result;
155
193
  }
156
194
  /**
@@ -230,12 +268,7 @@ export class VersionPlanService {
230
268
  * group; a non-empty string joins exactly the other packages sharing that string, regardless of
231
269
  * the repo's own default; `false` makes it a solo group of one. */
232
270
  resolveGroupKey(pkg) {
233
- const g = pkg.config?.group;
234
- if (g === false)
235
- return `solo:${pkg.name}`;
236
- if (typeof g === 'string' && g)
237
- return `named:${g}`;
238
- return 'default';
271
+ return groupKeyOf(pkg);
239
272
  }
240
273
  /** The human-readable name behind a `groupKey` - what a plan's `group` column shows. */
241
274
  groupLabel(key) {
@@ -1,7 +1,7 @@
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';
4
- import { Service } from '../core/service.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';
5
5
  import { VersionPlanService } from './version-plan.service.js';
6
6
  /**
7
7
  * Applying a version plan: the writes. Every manifest edit, dependency-range refresh, stamp,
@@ -139,7 +139,7 @@ export declare namespace VersionService {
139
139
  */
140
140
  function stampSourceFiles(pkg: Package, version: string): string[];
141
141
  }
142
- declare module '../core/service.js' {
142
+ declare module '../core/classes/service.js' {
143
143
  interface ServiceMap {
144
144
  version: VersionService;
145
145
  }
@@ -1,8 +1,8 @@
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';
5
- import { Service } from '../core/service.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';
6
6
  import { GitHelper } from '../utils/git.js';
7
7
  import { expandReleaseTag, isCalendarVersion } from '../utils/release-version.js';
8
8
  import { stampVersionLabel } from '../utils/version-stamp.js';
@@ -65,6 +65,9 @@ export class VersionService extends Service {
65
65
  */
66
66
  for (const entry of bumped)
67
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();
68
71
  for (const entry of bumped) {
69
72
  const pkg = entry.package;
70
73
  /** The scope these hooks are evaluated against - the only place `${{ pkg.targetVersion }}`
@@ -80,7 +83,7 @@ export class VersionService extends Service {
80
83
  const hook = (slot) => RunService.runLifecycleSlot(pkg, VERSION_LIFECYCLE, slot, RunService.normalizeScriptValue(
81
84
  /** `at`: the path is what tells a step function from a value one, and this is a fragment -
82
85
  * without it a function here was called while the hook was being prepared. */
83
- 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}`));
84
87
  await hook('before');
85
88
  /** Through the manifest, not through a `package.json` field: where a version is written is
86
89
  * the provider's business (see `Plugin`'s manifest members), and this is the one place rman changes
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,5 +1,5 @@
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 {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A YAML document with syntax colour, for the two commands that print one to be **looked at** -
3
+ * `rman config` and `--config`.
4
+ *
5
+ * **Only call this when `process.stdout.isTTY`.** Colouring a document that is being redirected
6
+ * makes it unloadable, which is the whole reason the callers gate on a terminal.
7
+ */
8
+ export declare function colorYaml(text: string): string;
@@ -0,0 +1,86 @@
1
+ import colors from 'ansi-colors';
2
+ /**
3
+ * A YAML document with syntax colour, for the two commands that print one to be **looked at** -
4
+ * `rman config` and `--config`.
5
+ *
6
+ * **Only call this when `process.stdout.isTTY`.** Colouring a document that is being redirected
7
+ * makes it unloadable, which is the whole reason the callers gate on a terminal.
8
+ */
9
+ /* **Line-based and deliberately shallow.** This is a highlighter, not a parser: it colours what a
10
+ * reader's eye uses to navigate - the key, the list marker, a literal - and leaves anything it is
11
+ * not sure about alone. A real parse would have to round-trip js-yaml's own output to gain nothing
12
+ * a reader would notice, and every extra rule is a new way to mangle a value.
13
+ *
14
+ * **What it colours and why that set**:
15
+ *
16
+ * - the **key**, in cyan, because that is what you scan a config for;
17
+ * - **`${{ ... }}`**, in magenta, because rman's configs are full of them and because a `version`
18
+ * hook is printed *raw* (`DEFERRED_PATHS`) - the one place an unresolved expression is correct
19
+ * rather than a bug, and the colour is what stops it reading as a mistake;
20
+ * - **numbers, booleans and `null`**, in yellow, since `skip: false` and `skip: "false"` mean
21
+ * different things and nothing else in the output tells them apart;
22
+ * - **`[Function: name]`**, in grey, because it is this printer's placeholder rather than anything
23
+ * the config says - the one value in the output that is not a value;
24
+ * - the **`-`** of a list item and a `#` comment, in grey, as structure rather than content.
25
+ *
26
+ * A plain string value is left uncoloured on purpose. It is the *content*, usually the longest
27
+ * thing on the line, and colouring it competes with the key for attention - which is the failure
28
+ * mode of a highlighter that paints everything.
29
+ *
30
+ * **The two places a shallow pass has to be careful, and neither is hypothetical:**
31
+ *
32
+ * - **A `:` inside a value.** js-yaml quotes any scalar holding one, so the line is
33
+ * `key: 'a: b'` - and the key pattern stops at the *first* colon, which is the right one. Nothing
34
+ * to do, but it is why the pattern is written `[^:]*` rather than anything greedier.
35
+ * - **A folded block scalar.** `lineWidth: 100` makes js-yaml emit `describe: >-` and continue on
36
+ * deeper-indented lines, and a continuation line is prose: one holding `(default: ...)` would
37
+ * otherwise have half a sentence coloured as a key. So the block's indent is tracked and its
38
+ * content is left alone but for expressions. A `#` there is prose too, not a comment. */
39
+ export function colorYaml(text) {
40
+ /** The indent of the key that opened a block scalar, while one is open. Anything indented past it
41
+ * is that block's content; the first line that is not ends it. */
42
+ let blockIndent;
43
+ return text
44
+ .split('\n')
45
+ .map(line => {
46
+ const indent = line.length - line.trimStart().length;
47
+ if (blockIndent !== undefined) {
48
+ if (line.trim() === '' || indent > blockIndent)
49
+ return colorExpressions(line);
50
+ blockIndent = undefined;
51
+ }
52
+ if (BLOCK_SCALAR.test(line))
53
+ blockIndent = indent;
54
+ return colorLine(line);
55
+ })
56
+ .join('\n');
57
+ }
58
+ /** `key: >-`, `key: |`, `key: >2-`, and the bare `- >-` of a list item. */
59
+ const BLOCK_SCALAR = /:\s*[|>][0-9]*[-+]?\s*$/;
60
+ const EXPRESSION = /\$\{\{[\s\S]*?\}\}/g;
61
+ const LITERAL = /^(true|false|null|~|-?\d+(?:\.\d+)?)$/;
62
+ const FUNCTION = /^"?\[Function(?::[^\]]*)?\]"?$/;
63
+ function colorLine(line) {
64
+ const comment = /^(\s*)#(.*)$/.exec(line);
65
+ if (comment)
66
+ return comment[1] + colors.gray(`#${comment[2]}`);
67
+ /** `indent`, an optional `- ` for a list item, then either `key:` plus a value or a bare item. */
68
+ const parsed = /^(\s*)(- )?(?:([^\s:][^:]*):(\s|$))?([\s\S]*)$/.exec(line);
69
+ if (!parsed)
70
+ return line;
71
+ const [, indent = '', dash, key, gap = '', rest = ''] = parsed;
72
+ return indent + (dash ? colors.gray(dash) : '') + (key ? `${colors.cyan(key)}:${gap}` : '') + colorValue(rest);
73
+ }
74
+ function colorValue(value) {
75
+ const trimmed = value.trim();
76
+ if (!trimmed)
77
+ return value;
78
+ if (LITERAL.test(trimmed))
79
+ return value.replace(trimmed, colors.yellow(trimmed));
80
+ if (FUNCTION.test(trimmed))
81
+ return value.replace(trimmed, colors.gray(trimmed));
82
+ return colorExpressions(value);
83
+ }
84
+ function colorExpressions(value) {
85
+ return value.replace(EXPRESSION, match => colors.magenta(match));
86
+ }
package/utils/git.d.ts CHANGED
@@ -46,6 +46,19 @@ export declare class GitHelper {
46
46
  /** Tag names matching the glob `pattern` (e.g. `"v*"`, `"pkg-a@*"`), newest version first. */
47
47
  listTags(pattern: string): Promise<string[]>;
48
48
  /** Whether `tag` exists as an exact git tag name (not a glob - a literal ref check). */
49
+ /**
50
+ * Every tag matching `pattern`, paired with the **commit** it points at - newest version first,
51
+ * the same order `listTags` returns.
52
+ *
53
+ * `%(*objectname)` is the dereferenced target and is empty for a lightweight tag, so the commit
54
+ * is that or `%(objectname)`. rman's own tags are lightweight; a repository's existing ones may
55
+ * not be, and an annotated tag's `objectname` is the *tag object*, which matches no commit and
56
+ * would make the tag silently invisible to anything comparing shas.
57
+ */
58
+ listTagCommits(pattern: string): Promise<{
59
+ tag: string;
60
+ sha: string;
61
+ }[]>;
49
62
  tagExists(tag: string): Promise<boolean>;
50
63
  /** The most recent commit that changed `file` (tracked history only) - `undefined` if it has
51
64
  * never been committed. Answers "since when has this file last been updated", independent of
@@ -61,6 +74,19 @@ export declare class GitHelper {
61
74
  /** The repository's very first commit (the oldest root commit, for a history with several) -
62
75
  * `undefined` for a repository with no commits at all. The "since the beginning" boundary for
63
76
  * a package being released for the first time, with no earlier tag to measure from. */
77
+ /** HEAD's own sha, or `undefined` in a repository with no commits yet (or none at all) - the
78
+ * same "answer nothing rather than throw" shape `rootCommit` and `describeTag` take, because
79
+ * every caller here already has a "nothing to go on" branch. */
80
+ /** The date a ref points at, as `YYYY-MM-DD`. The **committer** date, not the author's: a
81
+ * rebased or cherry-picked release commit was authored earlier than it shipped, and what a
82
+ * changelog heading states is when the release happened. `undefined` for a ref that resolves to
83
+ * nothing. */
84
+ commitDate(ref: string): Promise<string | undefined>;
85
+ /** The commit `ref` names - a sha, a tag, a branch - or `undefined` when git does not recognize
86
+ * it. `^{commit}` so an annotated tag answers with the commit rather than the tag object, and
87
+ * so a tree or blob answers with nothing rather than a sha that matches no commit. */
88
+ resolveCommit(ref: string): Promise<string | undefined>;
89
+ headSha(): Promise<string | undefined>;
64
90
  rootCommit(): Promise<string | undefined>;
65
91
  /**
66
92
  * Stages and commits exactly `files` (relative to `cwd`, or absolute) with `message` - never a