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
@@ -3,221 +3,31 @@ 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/classes/service.js';
6
7
  import { exec } from '../utils/exec.js';
7
8
  import { LOG_LEVELS, Logger, resolveRootLogLevel } from '../utils/logger.js';
8
9
  import { filterPackages } from '../utils/package-filter.js';
9
10
  import { ProgressPanel } from '../utils/progress-panel.js';
10
11
  import { runBin } from '../utils/run-bin.js';
11
- export var RunService;
12
- (function (RunService) {
13
- /**
14
- * Registers a source. Called by `loadPlugins` for each plugin's `runSteps`, in `plugins`
15
- * declaration order - never as an import side effect, so what is registered is exactly what the
16
- * repository's `.rmanrc` asked for.
17
- */
18
- function addStepSource(source) {
19
- /** Idempotent per source: a plugin both declares `runSteps` (registered by `loadPlugins`) and
20
- * may call its own `augmentRun()` for programmatic callers, so the same function arrives
21
- * twice. Pushing twice is harmless - the first match wins - but it makes the registry lie
22
- * about what is in it. */
23
- if (stepSources.includes(source))
24
- return;
25
- stepSources.push(source);
26
- }
27
- RunService.addStepSource = addStepSource;
28
- /** For tests, which would otherwise leak a source into every later case in the process. */
29
- function clearStepSources() {
30
- stepSources.length = 0;
31
- }
32
- RunService.clearStepSources = clearStepSources;
33
- /**
34
- * What the *package itself* declares for the lifecycle `script`, from the contributed sources
35
- * alone - no `.rmanrc` involved. `undefined` when it declares nothing.
36
- *
37
- * Exported because `run` is not the only lifecycle rman wraps: `version` runs hooks around the
38
- * version write, and npm spells those `preversion`/`version`/`postversion` in `package.json` -
39
- * which is the same `pre<script>`/`<script>`/`post<script>` shape a step source already answers.
40
- * So `VersionService` asks here for `'version'` instead of reading `manifest.raw.scripts` itself,
41
- * and npm's version lifecycle keeps working with no second seam and no extra line in any plugin.
42
- * A plugin for another ecosystem gets its own lifecycle hooks the moment it contributes steps.
43
- */
44
- function contributedSlots(pkg, script) {
45
- return firstContributed(pkg, script);
46
- }
47
- RunService.contributedSlots = contributedSlots;
48
- /**
49
- * Runs one slot of a lifecycle belonging to some operation other than `run` itself - `version`'s
50
- * hooks around the version write are the only one so far.
51
- *
52
- * **Here rather than in `VersionService`, because the rule it applies is this service's**: the
53
- * package's own declaration for `script` wins over the caller's `fallback`, slot by slot, exactly
54
- * as `getScriptSteps` decides it for `run`. Kept in two places that rule would drift, and one of
55
- * the copies would sit in the file that writes versions - which now runs no command of its own at
56
- * all.
57
- *
58
- * `fallback` is the caller's own configured step(s), **already evaluated**: `version`'s three
59
- * paths are in `DEFERRED_PATHS` precisely because only the caller can bind
60
- * `${{ pkg.targetVersion }}`, so interpolating here would either be too early or need a scope this
61
- * service has no business holding.
62
- *
63
- * **A list, not one joined string.** `VersionService` used to `join(' && ')` an array into a
64
- * single shell line, which a function step cannot be part of - and which quietly changed the
65
- * semantics of the shell case too, since `cd x && y` in one process is not the same as two.
66
- */
67
- async function runLifecycleSlot(pkg, script, slot, fallback) {
68
- const own = contributedSlots(pkg, script)?.[slot] ?? [];
69
- const values = own.length ? own : (fallback ?? []);
70
- for (const value of values) {
71
- if (typeof value === 'function') {
72
- await value(createStepContext(pkg, pkg.dirname));
73
- continue;
74
- }
75
- await exec(value, { cwd: pkg.dirname, stdio: 'inherit' });
76
- }
77
- }
78
- RunService.runLifecycleSlot = runLifecycleSlot;
79
- function getConfig(pkg, script) {
80
- const runCfg = pkg.config?.run;
81
- const cfg = runCfg && typeof runCfg === 'object' ? runCfg[script] : undefined;
82
- /** The bare-value shorthand. A function is `typeof 'function'`, not `'object'`, so without
83
- * naming it here `run: { build: myFn }` fell through to the `{}` below - the long form would
84
- * have worked and the short one silently done nothing, an arbitrary difference. */
85
- if (typeof cfg === 'string' || typeof cfg === 'function' || Array.isArray(cfg))
86
- return { exec: cfg };
87
- return cfg && typeof cfg === 'object' ? cfg : {};
88
- }
89
- RunService.getConfig = getConfig;
90
- /**
91
- * The context a function step or `if` is handed - see [`RunStepContext`](../core/run-step.ts).
92
- *
93
- * `runBin` and `logger` are bound to *this run* rather than left to be imported, which is the
94
- * whole reason they are handed over: an imported `runBin` knows neither the cwd nor the resolved
95
- * log level.
96
- */
97
- /**
98
- * A `run.<script>.before`/`.exec`/`.after` (or `version.<slot>`) value: one step, or several to
99
- * run in sequence. A shell command or a function, and a list may mix them.
100
- *
101
- * **Anything else throws, naming the path.** It used to `return []`, which meant a value rman did
102
- * not recognize was dropped with no trace: writing a function here - the obvious guess, and now
103
- * the supported form - produced `1 succeeded, 0 failed` with the step never run (measured). A
104
- * configuration mistake has to be loud; silence here reads as success.
105
- *
106
- * Exported, and the only implementation: `VersionService` used to carry a second one that behaved
107
- * differently, which is how `version.<slot>` came to join its array with `' && '`.
108
- */
109
- function normalizeScriptValue(value, at) {
110
- const items = Array.isArray(value) ? value : [value];
111
- const steps = [];
112
- for (let i = 0; i < items.length; i++) {
113
- const item = items[i];
114
- /** An empty string and an absent value are both "nothing here", which is how a `"[*]"` block
115
- * declaring a slot some packages don't use has always behaved. */
116
- if (item === undefined || item === null || item === '')
117
- continue;
118
- if (typeof item === 'string' || typeof item === 'function') {
119
- steps.push(item);
120
- continue;
121
- }
122
- const where = Array.isArray(value) ? `${at}[${i}]` : at;
123
- throw new Error(`"${where}" must be a shell command or a function, but it is ${describeValue(item)}.\n` +
124
- ` A list of either (or both) runs them in sequence.`);
125
- }
126
- return steps;
127
- }
128
- RunService.normalizeScriptValue = normalizeScriptValue;
129
- function createStepContext(pkg, cwd) {
130
- const logLevel = resolveRootLogLevel(pkg.repository);
131
- return {
132
- pkg,
133
- repository: pkg.repository,
134
- cwd,
135
- runBin: (bin, argv, opts) => runBin(bin, argv, { cwd, logLevel, ...opts }),
136
- logger: new Logger(logLevel),
137
- };
138
- }
139
- RunService.createStepContext = createStepContext;
140
- function resolveEnvPlaceholders(value) {
141
- return value.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
142
- }
143
- /** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
144
- * and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
145
- function parseIfExpr(raw) {
146
- if (typeof raw !== 'string' || !raw.trim())
147
- return undefined;
148
- const tokens = tokenizeIf(raw);
149
- if (!tokens.length)
150
- return undefined;
151
- let i = 0;
152
- const peek = () => tokens[i];
153
- const next = () => tokens[i++];
154
- function parseUnary() {
155
- if (peek()?.toLowerCase() === 'not') {
156
- next();
157
- return { kind: 'not', node: parseUnary() };
158
- }
159
- const tok = next() ?? '';
160
- /** A token containing whitespace can only be a collapsed `(...)` group - plain
161
- * atom/operator tokens never do, since they're themselves split on whitespace. */
162
- if (/\s/.test(tok)) {
163
- const inner = parseIfExpr(tok);
164
- if (!inner)
165
- throw new Error(`Empty group in "if" expression: "${raw}"`);
166
- return inner;
167
- }
168
- let value;
169
- if (peek() === '=') {
170
- next();
171
- value = resolveEnvPlaceholders(next() ?? '');
172
- }
173
- return { kind: 'atom', name: tok, value };
174
- }
175
- function parseAnd() {
176
- let node = parseUnary();
177
- while (peek()?.toLowerCase() === 'and') {
178
- next();
179
- node = { kind: 'and', left: node, right: parseUnary() };
180
- }
181
- return node;
182
- }
183
- function parseOr() {
184
- let node = parseAnd();
185
- while (peek()?.toLowerCase() === 'or') {
186
- next();
187
- node = { kind: 'or', left: node, right: parseAnd() };
188
- }
189
- return node;
190
- }
191
- return parseOr();
192
- }
193
- RunService.parseIfExpr = parseIfExpr;
194
- /** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
195
- * for the same reference hash across packages/scripts in a single run. */
196
- async function evaluateIf(repository, pkg, node, statusCache) {
197
- if (node.kind === 'and') {
198
- if (!(await evaluateIf(repository, pkg, node.left, statusCache)))
199
- return false;
200
- return evaluateIf(repository, pkg, node.right, statusCache);
201
- }
202
- if (node.kind === 'or') {
203
- if (await evaluateIf(repository, pkg, node.left, statusCache))
204
- return true;
205
- return evaluateIf(repository, pkg, node.right, statusCache);
206
- }
207
- if (node.kind === 'not') {
208
- return !(await evaluateIf(repository, pkg, node.node, statusCache));
209
- }
210
- return evaluateIfAtom(repository, pkg, node.name, node.value, statusCache);
211
- }
212
- RunService.evaluateIf = evaluateIf;
213
- async function runScript(repository, script, options = {}) {
12
+ /**
13
+ * A service class - see `ListService` for the shape and `Service` for the three measured
14
+ * consequences a namespace had.
15
+ *
16
+ * **Only `runScript` became a method**, because only it takes a repository. `getConfig`,
17
+ * `parseIfExpr`, `normalizeScriptValue` and the rest take a `Package` or a raw value and stay
18
+ * functions on the namespace below - the same rule that leaves `ChangeHashService` a namespace
19
+ * entirely. Declared before the namespace, which TypeScript requires for the merge.
20
+ */
21
+ export class RunService extends Service {
22
+ async runScript(script, options = {}) {
23
+ const repository = this.repository;
214
24
  const commandName = options.commandName || 'run';
215
- const rootCfg = getConfig(repository.rootPackage, script);
25
+ const rootCfg = RunService.getConfig(repository.rootPackage, script);
216
26
  const logLevelDefault = resolveRootLogLevel(repository);
217
27
  /** Standing inside a single package's own directory scopes the run to just that package
218
- * (and drops the root bookend below) unless `--root` asks for the whole repository anyway -
28
+ * (and drops the root bookend below) unless `--from-root` asks for the whole repository anyway -
219
29
  * a no-op when already at the root, or outside any known package. */
220
- const cwdScope = options.root ? undefined : repository.currentPackage;
30
+ const cwdScope = options.fromRoot ? undefined : repository.currentPackage;
221
31
  /** Global fallback for topo - individual packages can still override their own linking below,
222
32
  * but the initial sort (topological vs alphabetical) has to be decided for the whole list at once. */
223
33
  const topo = resolveBool(options.topo, repository.rootPackage, script, 'topo', true);
@@ -251,6 +61,12 @@ export var RunService;
251
61
  for (let i = 0; i < steps.length; i++) {
252
62
  const step = steps[i];
253
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)');
254
70
  ctx.stepIndex = i;
255
71
  if (panel.enabled) {
256
72
  const onLine = (line) => {
@@ -260,7 +76,7 @@ export var RunService;
260
76
  if (step.run)
261
77
  await runFunctionStep(step.run, pkg, cwd, onLine);
262
78
  else
263
- await exec(step.command, { cwd, stdio: 'pipe', onLine });
79
+ await exec(step.command, { cwd, stdio: 'pipe', onLine, app: pkg.repository.app });
264
80
  }
265
81
  else {
266
82
  /** Match the classic rman output: raw command output streams straight through
@@ -274,10 +90,23 @@ export var RunService;
274
90
  if (step.run)
275
91
  await runFunctionStep(step.run, pkg, cwd);
276
92
  else
277
- await exec(step.command, { cwd, stdio: 'inherit' });
93
+ await exec(step.command, { cwd, stdio: 'inherit', app: pkg.repository.app });
278
94
  }
279
95
  catch (e) {
280
- stepError = e;
96
+ /**
97
+ * **Normalized to an `Error`, because a *falsy* throw was indistinguishable from no
98
+ * failure at all.** This was `let stepError: any` with `if (stepError) throw
99
+ * stepError` below, so a step doing `throw undefined` - legal JavaScript, and what a
100
+ * rejected promise carrying nothing gives you - left `stepError` falsy: the step
101
+ * line printed **success**, nothing was rethrown, and the run exited 0. Found by the
102
+ * spec written for the message-reporting fix above, which is the only reason it is
103
+ * not still there. A step that fails while reporting success is the one outcome this
104
+ * slot exists to rule out.
105
+ *
106
+ * Only the panel-off path had it: with the panel on there is no local catch, and the
107
+ * outer one runs whatever was thrown.
108
+ */
109
+ stepError = e instanceof Error ? e : new Error(messageOf(e));
281
110
  }
282
111
  printLegacyStepLine(commandName, pkgLabel, step, Date.now() - stepStart, pkgLogLevel, stepError);
283
112
  if (stepError)
@@ -331,7 +160,7 @@ export var RunService;
331
160
  }
332
161
  const stepsByPackage = new Map();
333
162
  for (const pkg of packages) {
334
- const pkgCfg = getConfig(pkg, script);
163
+ const pkgCfg = RunService.getConfig(pkg, script);
335
164
  if (pkgCfg.skip === true)
336
165
  continue;
337
166
  if (!(await passesIf(repository, pkg, pkgCfg.if, pkg.dirname, ifStatusCache)))
@@ -431,7 +260,191 @@ export var RunService;
431
260
  throw err;
432
261
  }
433
262
  }
434
- RunService.runScript = runScript;
263
+ }
264
+ (function (RunService) {
265
+ /**
266
+ * What the *package itself* declares for the lifecycle `script`, from the contributed sources
267
+ * alone - no `.rmanrc` involved. `undefined` when it declares nothing.
268
+ *
269
+ * Exported because `run` is not the only lifecycle rman wraps: `version` runs hooks around the
270
+ * version write, and npm spells those `preversion`/`version`/`postversion` in `package.json` -
271
+ * which is the same `pre<script>`/`<script>`/`post<script>` shape a step source already answers.
272
+ * So `VersionService` asks here for `'version'` instead of reading `manifest.raw.scripts` itself,
273
+ * and npm's version lifecycle keeps working with no second seam and no extra line in any plugin.
274
+ * A plugin for another ecosystem gets its own lifecycle hooks the moment it contributes steps.
275
+ */
276
+ function contributedSlots(pkg, script) {
277
+ return contributedSlotsFor(pkg, script);
278
+ }
279
+ RunService.contributedSlots = contributedSlots;
280
+ /**
281
+ * Runs one slot of a lifecycle belonging to some operation other than `run` itself - `version`'s
282
+ * hooks around the version write are the only one so far.
283
+ *
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.
293
+ *
294
+ * `fallback` is the caller's own configured step(s), **already evaluated**: `version`'s three
295
+ * paths are in `DEFERRED_PATHS` precisely because only the caller can bind
296
+ * `${{ pkg.targetVersion }}`, so interpolating here would either be too early or need a scope this
297
+ * service has no business holding.
298
+ *
299
+ * **A list, not one joined string.** `VersionService` used to `join(' && ')` an array into a
300
+ * single shell line, which a function step cannot be part of - and which quietly changed the
301
+ * semantics of the shell case too, since `cd x && y` in one process is not the same as two.
302
+ */
303
+ async function runLifecycleSlot(pkg, script, slot, fallback) {
304
+ const values = slotValues(slot, contributedSlots(pkg, script)?.[slot] ?? [], fallback ?? [], false);
305
+ for (const value of values) {
306
+ if (typeof value === 'function') {
307
+ await value(createStepContext(pkg, pkg.dirname));
308
+ continue;
309
+ }
310
+ await exec(value, { cwd: pkg.dirname, stdio: 'inherit', app: pkg.repository.app });
311
+ }
312
+ }
313
+ RunService.runLifecycleSlot = runLifecycleSlot;
314
+ function getConfig(pkg, script) {
315
+ const runCfg = pkg.config?.run;
316
+ const cfg = runCfg && typeof runCfg === 'object' ? runCfg[script] : undefined;
317
+ /** The bare-value shorthand. A function is `typeof 'function'`, not `'object'`, so without
318
+ * naming it here `run: { build: myFn }` fell through to the `{}` below - the long form would
319
+ * have worked and the short one silently done nothing, an arbitrary difference. */
320
+ if (typeof cfg === 'string' || typeof cfg === 'function' || Array.isArray(cfg))
321
+ return { exec: cfg };
322
+ return cfg && typeof cfg === 'object' ? cfg : {};
323
+ }
324
+ RunService.getConfig = getConfig;
325
+ /**
326
+ * The context a function step or `if` is handed - see [`RunStepContext`](../core/run-step.ts).
327
+ *
328
+ * `runBin` and `logger` are bound to *this run* rather than left to be imported, which is the
329
+ * whole reason they are handed over: an imported `runBin` knows neither the cwd nor the resolved
330
+ * log level.
331
+ */
332
+ /**
333
+ * A `run.<script>.before`/`.exec`/`.after` (or `version.<slot>`) value: one step, or several to
334
+ * run in sequence. A shell command or a function, and a list may mix them.
335
+ *
336
+ * **Anything else throws, naming the path.** It used to `return []`, which meant a value rman did
337
+ * not recognize was dropped with no trace: writing a function here - the obvious guess, and now
338
+ * the supported form - produced `1 succeeded, 0 failed` with the step never run (measured). A
339
+ * configuration mistake has to be loud; silence here reads as success.
340
+ *
341
+ * Exported, and the only implementation: `VersionService` used to carry a second one that behaved
342
+ * differently, which is how `version.<slot>` came to join its array with `' && '`.
343
+ */
344
+ function normalizeScriptValue(value, at) {
345
+ const items = Array.isArray(value) ? value : [value];
346
+ const steps = [];
347
+ for (let i = 0; i < items.length; i++) {
348
+ const item = items[i];
349
+ /** An empty string and an absent value are both "nothing here", which is how a `"[*]"` block
350
+ * declaring a slot some packages don't use has always behaved. */
351
+ if (item === undefined || item === null || item === '')
352
+ continue;
353
+ if (typeof item === 'string' || typeof item === 'function') {
354
+ steps.push(item);
355
+ continue;
356
+ }
357
+ const where = Array.isArray(value) ? `${at}[${i}]` : at;
358
+ throw new Error(`"${where}" must be a shell command or a function, but it is ${describeValue(item)}.\n` +
359
+ ` A list of either (or both) runs them in sequence.`);
360
+ }
361
+ return steps;
362
+ }
363
+ RunService.normalizeScriptValue = normalizeScriptValue;
364
+ function createStepContext(pkg, cwd) {
365
+ const logLevel = resolveRootLogLevel(pkg.repository);
366
+ return {
367
+ pkg,
368
+ repository: pkg.repository,
369
+ cwd,
370
+ runBin: (bin, argv, opts) => runBin(bin, argv, { cwd, logLevel, app: pkg.repository.app, ...opts }),
371
+ logger: new Logger(logLevel),
372
+ };
373
+ }
374
+ RunService.createStepContext = createStepContext;
375
+ function resolveEnvPlaceholders(value) {
376
+ return value.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
377
+ }
378
+ /** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
379
+ * and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
380
+ function parseIfExpr(raw) {
381
+ if (typeof raw !== 'string' || !raw.trim())
382
+ return undefined;
383
+ const tokens = tokenizeIf(raw);
384
+ if (!tokens.length)
385
+ return undefined;
386
+ let i = 0;
387
+ const peek = () => tokens[i];
388
+ const next = () => tokens[i++];
389
+ function parseUnary() {
390
+ if (peek()?.toLowerCase() === 'not') {
391
+ next();
392
+ return { kind: 'not', node: parseUnary() };
393
+ }
394
+ const tok = next() ?? '';
395
+ /** A token containing whitespace can only be a collapsed `(...)` group - plain
396
+ * atom/operator tokens never do, since they're themselves split on whitespace. */
397
+ if (/\s/.test(tok)) {
398
+ const inner = parseIfExpr(tok);
399
+ if (!inner)
400
+ throw new Error(`Empty group in "if" expression: "${raw}"`);
401
+ return inner;
402
+ }
403
+ let value;
404
+ if (peek() === '=') {
405
+ next();
406
+ value = resolveEnvPlaceholders(next() ?? '');
407
+ }
408
+ return { kind: 'atom', name: tok, value };
409
+ }
410
+ function parseAnd() {
411
+ let node = parseUnary();
412
+ while (peek()?.toLowerCase() === 'and') {
413
+ next();
414
+ node = { kind: 'and', left: node, right: parseUnary() };
415
+ }
416
+ return node;
417
+ }
418
+ function parseOr() {
419
+ let node = parseAnd();
420
+ while (peek()?.toLowerCase() === 'or') {
421
+ next();
422
+ node = { kind: 'or', left: node, right: parseAnd() };
423
+ }
424
+ return node;
425
+ }
426
+ return parseOr();
427
+ }
428
+ RunService.parseIfExpr = parseIfExpr;
429
+ /** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
430
+ * for the same reference hash across packages/scripts in a single run. */
431
+ async function evaluateIf(repository, pkg, node, statusCache) {
432
+ if (node.kind === 'and') {
433
+ if (!(await evaluateIf(repository, pkg, node.left, statusCache)))
434
+ return false;
435
+ return evaluateIf(repository, pkg, node.right, statusCache);
436
+ }
437
+ if (node.kind === 'or') {
438
+ if (await evaluateIf(repository, pkg, node.left, statusCache))
439
+ return true;
440
+ return evaluateIf(repository, pkg, node.right, statusCache);
441
+ }
442
+ if (node.kind === 'not') {
443
+ return !(await evaluateIf(repository, pkg, node.node, statusCache));
444
+ }
445
+ return evaluateIfAtom(repository, pkg, node.name, node.value, statusCache);
446
+ }
447
+ RunService.evaluateIf = evaluateIf;
435
448
  })(RunService || (RunService = {}));
436
449
  /**
437
450
  * Classic one-line-per-step log ("info build pkg ┆ step success ┆ command (123 ms)"),
@@ -483,11 +496,30 @@ function toStep(slot, value) {
483
496
  *
484
497
  * Restored in a `finally`, because a step that throws must not leave the rest of the run writing
485
498
  * into a log nobody reads.
499
+ *
500
+ * **Its failure message is reported here, because nothing else does it.** A shell step's reason
501
+ * reaches the user on its own - the output streams through `onLine` or straight to the terminal,
502
+ * and `exec` names the command and its exit code. A function step has neither: it fails by
503
+ * throwing, and the throw goes to the outer `catch` that marks the package failed and rethrows an
504
+ * error the CLI treats as already-logged. Measured on a real config whose build step threw a
505
+ * carefully worded message about a missing `tsconfig.json`: the run printed
506
+ * `error build pkg-forgot ┆ exec failed ┆ buildWithTsc` and exited 1, and the message appeared
507
+ * nowhere at all - so the one thing that said what to do was the one thing dropped.
508
+ *
509
+ * Written where a shell step's output goes, so it needs no second channel: through `onLine` with
510
+ * the panel on (the step's own log, which is what the panel shows for a failed item), and to
511
+ * stderr with it off, ahead of the `failed` line - the order a shell step already produces.
486
512
  */
487
513
  async function runFunctionStep(run, pkg, cwd, onLine) {
488
514
  const context = RunService.createStepContext(pkg, cwd);
489
515
  if (!onLine) {
490
- await run(context);
516
+ try {
517
+ await run(context);
518
+ }
519
+ catch (e) {
520
+ console.error(colors.red(messageOf(e)));
521
+ throw e;
522
+ }
491
523
  return;
492
524
  }
493
525
  const console_ = globalThis.console;
@@ -504,11 +536,24 @@ async function runFunctionStep(run, pkg, cwd, onLine) {
504
536
  try {
505
537
  await run(context);
506
538
  }
539
+ catch (e) {
540
+ /** Through the patched `console` deliberately - `onLine` is still installed at this point, so
541
+ * the message lands in this step's log rather than over the panel it is drawn inside. */
542
+ for (const line of messageOf(e).split('\n'))
543
+ onLine(line);
544
+ throw e;
545
+ }
507
546
  finally {
508
547
  for (const method of CAPTURED_CONSOLE)
509
548
  console_[method] = original[method];
510
549
  }
511
550
  }
551
+ /** What a thrown value has to say for itself. A step may throw anything, and `String(undefined)`
552
+ * reading as `undefined` in a run log is worse than saying nothing was said. */
553
+ function messageOf(error) {
554
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';
555
+ return message.trim() || `the step threw ${inspect(error)}`;
556
+ }
512
557
  const CAPTURED_CONSOLE = ['log', 'info', 'warn', 'error', 'debug'];
513
558
  function format(args) {
514
559
  return args.map(arg => (typeof arg === 'string' ? arg : inspect(arg))).join(' ');
@@ -539,17 +584,19 @@ async function passesIf(repository, pkg, raw, cwd, statusCache) {
539
584
  *
540
585
  * 1. `run.<script>.override: true` -> the config, always. The escape hatch for "ignore what the
541
586
  * package says it does".
542
- * 2. a **contributed** source - `package.json#scripts` via `rman-node`. It wins over the config
543
- * because it is the *package's own* declaration, while a config value typically arrives
544
- * cascaded from the root or an `extends` base; the more specific statement wins, as everywhere
545
- * else in rman.
546
- * 3. the config's own `before`/`exec`/`after`, letting a package run a script it never declared.
547
- * 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.
548
595
  */
549
596
  function getScriptSteps(pkg, script) {
550
597
  const cfg = RunService.getConfig(pkg, script);
551
598
  const override = cfg.override === true;
552
- const contributed = firstContributed(pkg, script);
599
+ const contributed = contributedSlotsFor(pkg, script);
553
600
  const fromConfig = {
554
601
  before: RunService.normalizeScriptValue(cfg.before, `run.${script}.before`),
555
602
  exec: RunService.normalizeScriptValue(cfg.exec, `run.${script}.exec`),
@@ -557,27 +604,63 @@ function getScriptSteps(pkg, script) {
557
604
  };
558
605
  const steps = [];
559
606
  for (const slot of SCRIPT_SLOTS) {
560
- const own = contributed?.[slot] ?? [];
561
- const configured = fromConfig[slot] ?? [];
562
- const values = override ? (configured.length ? configured : own) : own.length ? own : configured;
563
- for (const value of values)
607
+ for (const value of slotValues(slot, contributed?.[slot] ?? [], fromConfig[slot] ?? [], override)) {
564
608
  steps.push(toStep(slot, value));
609
+ }
565
610
  }
566
611
  return steps;
567
612
  }
568
- /** The first source that says this package declares the script at all. Declaration order, so a
569
- * repository listing two plugins gets a predictable answer rather than a merged one. */
570
- function firstContributed(pkg, script) {
571
- for (const source of stepSources) {
572
- const slots = source(pkg, script);
573
- if (slots && (slots.before?.length || slots.exec?.length || slots.after?.length))
574
- return slots;
575
- }
576
- return undefined;
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
+ }
648
+ /**
649
+ * What this package's own technology says it declares for `script`.
650
+ *
651
+ * **Its own, not every registered one.** This walked all the contributed sources and took the first
652
+ * that answered, which in a polyglot repository meant npm's `package.json#scripts` reader was handed
653
+ * a Cargo package and asked whether it declared `build` - its first line is
654
+ * `pkg.manifest.raw?.scripts`, so it was reading a manifest another technology produced. The package
655
+ * already knows which technology claimed it; asking anyone else was only ever a way of finding that
656
+ * out again.
657
+ */
658
+ function contributedSlotsFor(pkg, script) {
659
+ const slots = pkg.platform.getRunSteps?.(pkg, script);
660
+ return slots && (slots.before?.length || slots.exec?.length || slots.after?.length) ? slots : undefined;
577
661
  }
578
662
  /** In execution order - `before`, the script itself, then `after`. */
579
663
  const SCRIPT_SLOTS = ['before', 'exec', 'after'];
580
- const stepSources = [];
581
664
  /**
582
665
  * Splits on whitespace, but a `(...)` group collapses to a single token holding its inner text
583
666
  * (parens stripped, inner whitespace kept) - `evaluate` below re-tokenizes any token that still
@@ -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';