rman 1.2.5 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +63 -19
  2. package/cli.d.ts +5 -0
  3. package/cli.js +214 -88
  4. package/commands/build.command.d.ts +177 -3
  5. package/commands/build.command.js +20 -10
  6. package/commands/changed.command.d.ts +80 -3
  7. package/commands/changed.command.js +19 -12
  8. package/commands/changelog.command.d.ts +192 -3
  9. package/commands/changelog.command.js +87 -43
  10. package/commands/config.command.d.ts +44 -3
  11. package/commands/config.command.js +30 -19
  12. package/commands/diff.command.d.ts +37 -3
  13. package/commands/diff.command.js +25 -16
  14. package/commands/exec.command.d.ts +193 -3
  15. package/commands/exec.command.js +60 -58
  16. package/commands/github-release.command.d.ts +154 -3
  17. package/commands/github-release.command.js +67 -37
  18. package/commands/import.command.d.ts +36 -3
  19. package/commands/import.command.js +28 -20
  20. package/commands/info.command.d.ts +35 -7
  21. package/commands/info.command.js +36 -30
  22. package/commands/list.command.d.ts +163 -3
  23. package/commands/list.command.js +109 -71
  24. package/commands/publish.command.d.ts +231 -0
  25. package/commands/publish.command.js +304 -0
  26. package/commands/run.command.d.ts +186 -6
  27. package/commands/run.command.js +26 -72
  28. package/commands/test.command.d.ts +173 -3
  29. package/commands/test.command.js +16 -10
  30. package/commands/version.command.d.ts +317 -3
  31. package/commands/version.command.js +149 -69
  32. package/commands.d.ts +32 -0
  33. package/commands.js +28 -0
  34. package/constants.js +1 -1
  35. package/core/application.d.ts +116 -0
  36. package/core/application.js +143 -0
  37. package/core/command-builder.d.ts +14 -0
  38. package/core/command-builder.js +78 -0
  39. package/core/config.d.ts +180 -50
  40. package/core/config.js +332 -153
  41. package/core/core-services.d.ts +14 -0
  42. package/core/core-services.js +30 -0
  43. package/core/core-targets.d.ts +14 -0
  44. package/core/core-targets.js +16 -0
  45. package/core/custom-command.d.ts +42 -6
  46. package/core/custom-command.js +44 -17
  47. package/core/extends-config.d.ts +13 -5
  48. package/core/extends-config.js +52 -12
  49. package/core/load-config-module.d.ts +28 -0
  50. package/core/load-config-module.js +42 -0
  51. package/core/manifest.d.ts +47 -25
  52. package/core/manifest.js +51 -69
  53. package/core/merge-config.d.ts +33 -34
  54. package/core/merge-config.js +138 -93
  55. package/core/package.d.ts +145 -17
  56. package/core/package.js +128 -36
  57. package/core/plugin-loader.d.ts +65 -0
  58. package/core/plugin-loader.js +234 -0
  59. package/core/plugin.d.ts +135 -90
  60. package/core/plugin.js +70 -173
  61. package/core/publish-target.d.ts +124 -0
  62. package/core/publish-target.js +30 -0
  63. package/core/registry.d.ts +30 -0
  64. package/core/registry.js +47 -0
  65. package/core/repository.d.ts +72 -9
  66. package/core/repository.js +293 -43
  67. package/core/resolve-target.d.ts +1 -1
  68. package/core/resolve-target.js +1 -1
  69. package/core/service.d.ts +49 -0
  70. package/core/service.js +40 -0
  71. package/core/version-scheme.d.ts +23 -1
  72. package/core/version-scheme.js +29 -1
  73. package/core/workspace.d.ts +84 -33
  74. package/core/workspace.js +63 -22
  75. package/index.d.ts +111 -14
  76. package/index.js +85 -9
  77. package/interfaces/rman-config.interface.d.ts +739 -212
  78. package/interfaces/rman-config.interface.js +61 -1
  79. package/package.json +2 -1
  80. package/plugins/builtins.d.ts +44 -0
  81. package/plugins/builtins.js +33 -0
  82. package/plugins/detect.d.ts +78 -0
  83. package/plugins/detect.js +70 -0
  84. package/plugins/node/augmentation/rman.augmentation.d.ts +84 -0
  85. package/plugins/node/augmentation/rman.augmentation.js +1 -0
  86. package/plugins/node/augmentation/system-info.augmentation.d.ts +26 -0
  87. package/plugins/node/augmentation/system-info.augmentation.js +79 -0
  88. package/plugins/node/commands/ci.command.d.ts +131 -0
  89. package/plugins/node/commands/ci.command.js +59 -0
  90. package/plugins/node/commands/clean.command.d.ts +183 -0
  91. package/plugins/node/commands/clean.command.js +73 -0
  92. package/plugins/node/index.d.ts +29 -0
  93. package/plugins/node/index.js +40 -0
  94. package/plugins/node/node-config.interface.d.ts +77 -0
  95. package/plugins/node/node-config.interface.js +7 -0
  96. package/plugins/node/node-manifest.provider.d.ts +68 -0
  97. package/plugins/node/node-manifest.provider.js +125 -0
  98. package/plugins/node/node.platform.d.ts +53 -0
  99. package/plugins/node/node.platform.js +134 -0
  100. package/plugins/node/npm-publish-target.d.ts +73 -0
  101. package/plugins/node/npm-publish-target.js +96 -0
  102. package/plugins/node/services/ci.service.d.ts +47 -0
  103. package/plugins/node/services/ci.service.js +213 -0
  104. package/plugins/node/services/clean.service.d.ts +53 -0
  105. package/plugins/node/services/clean.service.js +237 -0
  106. package/plugins/node/services/publish.service.d.ts +114 -0
  107. package/plugins/node/services/publish.service.js +371 -0
  108. package/plugins/node/services/version-plan.service.d.ts +44 -0
  109. package/plugins/node/services/version-plan.service.js +58 -0
  110. package/plugins/node/utils/npm-view.d.ts +48 -0
  111. package/plugins/node/utils/npm-view.js +71 -0
  112. package/plugins/node/utils/workspace-range.d.ts +26 -0
  113. package/plugins/node/utils/workspace-range.js +28 -0
  114. package/services/change-hash.service.d.ts +2 -2
  115. package/services/change-hash.service.js +2 -2
  116. package/services/changelog.service.d.ts +62 -51
  117. package/services/changelog.service.js +14 -11
  118. package/services/docker-publish.service.d.ts +50 -29
  119. package/services/docker-publish.service.js +43 -20
  120. package/services/exec.service.d.ts +23 -12
  121. package/services/exec.service.js +14 -9
  122. package/services/github-release.service.d.ts +44 -33
  123. package/services/github-release.service.js +13 -10
  124. package/services/import.service.d.ts +25 -14
  125. package/services/import.service.js +9 -5
  126. package/services/list.service.d.ts +62 -10
  127. package/services/list.service.js +62 -15
  128. package/services/run.service.d.ts +22 -13
  129. package/services/run.service.js +262 -223
  130. package/services/version-plan.service.d.ts +27 -4
  131. package/services/version-plan.service.js +42 -15
  132. package/services/version.service.d.ts +31 -11
  133. package/services/version.service.js +29 -13
  134. package/targets/docker.target.d.ts +53 -0
  135. package/targets/docker.target.js +40 -0
  136. package/utils/bin-path.d.ts +6 -7
  137. package/utils/bin-path.js +7 -18
  138. package/utils/branch-guard.d.ts +29 -0
  139. package/utils/branch-guard.js +31 -0
  140. package/utils/exec.d.ts +10 -0
  141. package/utils/exec.js +1 -1
  142. package/utils/logger.d.ts +1 -1
  143. package/utils/logger.js +1 -1
  144. package/utils/package-filter.d.ts +127 -7
  145. package/utils/package-filter.js +197 -16
  146. package/utils/printable-config.d.ts +1 -1
  147. package/utils/printable-config.js +1 -1
  148. package/utils/run-bin.d.ts +10 -0
  149. package/utils/run-bin.js +1 -1
  150. package/utils/run-options.d.ts +97 -0
  151. package/utils/run-options.js +81 -0
  152. package/utils/version-stamp.d.ts +1 -1
  153. package/utils/version-stamp.js +1 -1
@@ -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/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);
@@ -260,7 +70,7 @@ export var RunService;
260
70
  if (step.run)
261
71
  await runFunctionStep(step.run, pkg, cwd, onLine);
262
72
  else
263
- await exec(step.command, { cwd, stdio: 'pipe', onLine });
73
+ await exec(step.command, { cwd, stdio: 'pipe', onLine, app: pkg.repository.app });
264
74
  }
265
75
  else {
266
76
  /** Match the classic rman output: raw command output streams straight through
@@ -274,10 +84,23 @@ export var RunService;
274
84
  if (step.run)
275
85
  await runFunctionStep(step.run, pkg, cwd);
276
86
  else
277
- await exec(step.command, { cwd, stdio: 'inherit' });
87
+ await exec(step.command, { cwd, stdio: 'inherit', app: pkg.repository.app });
278
88
  }
279
89
  catch (e) {
280
- stepError = e;
90
+ /**
91
+ * **Normalized to an `Error`, because a *falsy* throw was indistinguishable from no
92
+ * failure at all.** This was `let stepError: any` with `if (stepError) throw
93
+ * stepError` below, so a step doing `throw undefined` - legal JavaScript, and what a
94
+ * rejected promise carrying nothing gives you - left `stepError` falsy: the step
95
+ * line printed **success**, nothing was rethrown, and the run exited 0. Found by the
96
+ * spec written for the message-reporting fix above, which is the only reason it is
97
+ * not still there. A step that fails while reporting success is the one outcome this
98
+ * slot exists to rule out.
99
+ *
100
+ * Only the panel-off path had it: with the panel on there is no local catch, and the
101
+ * outer one runs whatever was thrown.
102
+ */
103
+ stepError = e instanceof Error ? e : new Error(messageOf(e));
281
104
  }
282
105
  printLegacyStepLine(commandName, pkgLabel, step, Date.now() - stepStart, pkgLogLevel, stepError);
283
106
  if (stepError)
@@ -331,7 +154,7 @@ export var RunService;
331
154
  }
332
155
  const stepsByPackage = new Map();
333
156
  for (const pkg of packages) {
334
- const pkgCfg = getConfig(pkg, script);
157
+ const pkgCfg = RunService.getConfig(pkg, script);
335
158
  if (pkgCfg.skip === true)
336
159
  continue;
337
160
  if (!(await passesIf(repository, pkg, pkgCfg.if, pkg.dirname, ifStatusCache)))
@@ -431,7 +254,188 @@ export var RunService;
431
254
  throw err;
432
255
  }
433
256
  }
434
- RunService.runScript = runScript;
257
+ }
258
+ (function (RunService) {
259
+ /**
260
+ * What the *package itself* declares for the lifecycle `script`, from the contributed sources
261
+ * alone - no `.rmanrc` involved. `undefined` when it declares nothing.
262
+ *
263
+ * Exported because `run` is not the only lifecycle rman wraps: `version` runs hooks around the
264
+ * version write, and npm spells those `preversion`/`version`/`postversion` in `package.json` -
265
+ * which is the same `pre<script>`/`<script>`/`post<script>` shape a step source already answers.
266
+ * So `VersionService` asks here for `'version'` instead of reading `manifest.raw.scripts` itself,
267
+ * and npm's version lifecycle keeps working with no second seam and no extra line in any plugin.
268
+ * A plugin for another ecosystem gets its own lifecycle hooks the moment it contributes steps.
269
+ */
270
+ function contributedSlots(pkg, script) {
271
+ return contributedSlotsFor(pkg, script);
272
+ }
273
+ RunService.contributedSlots = contributedSlots;
274
+ /**
275
+ * Runs one slot of a lifecycle belonging to some operation other than `run` itself - `version`'s
276
+ * hooks around the version write are the only one so far.
277
+ *
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.
283
+ *
284
+ * `fallback` is the caller's own configured step(s), **already evaluated**: `version`'s three
285
+ * paths are in `DEFERRED_PATHS` precisely because only the caller can bind
286
+ * `${{ pkg.targetVersion }}`, so interpolating here would either be too early or need a scope this
287
+ * service has no business holding.
288
+ *
289
+ * **A list, not one joined string.** `VersionService` used to `join(' && ')` an array into a
290
+ * single shell line, which a function step cannot be part of - and which quietly changed the
291
+ * semantics of the shell case too, since `cd x && y` in one process is not the same as two.
292
+ */
293
+ async function runLifecycleSlot(pkg, script, slot, fallback) {
294
+ const own = contributedSlots(pkg, script)?.[slot] ?? [];
295
+ const values = own.length ? own : (fallback ?? []);
296
+ for (const value of values) {
297
+ if (typeof value === 'function') {
298
+ await value(createStepContext(pkg, pkg.dirname));
299
+ continue;
300
+ }
301
+ await exec(value, { cwd: pkg.dirname, stdio: 'inherit', app: pkg.repository.app });
302
+ }
303
+ }
304
+ RunService.runLifecycleSlot = runLifecycleSlot;
305
+ function getConfig(pkg, script) {
306
+ const runCfg = pkg.config?.run;
307
+ const cfg = runCfg && typeof runCfg === 'object' ? runCfg[script] : undefined;
308
+ /** The bare-value shorthand. A function is `typeof 'function'`, not `'object'`, so without
309
+ * naming it here `run: { build: myFn }` fell through to the `{}` below - the long form would
310
+ * have worked and the short one silently done nothing, an arbitrary difference. */
311
+ if (typeof cfg === 'string' || typeof cfg === 'function' || Array.isArray(cfg))
312
+ return { exec: cfg };
313
+ return cfg && typeof cfg === 'object' ? cfg : {};
314
+ }
315
+ RunService.getConfig = getConfig;
316
+ /**
317
+ * The context a function step or `if` is handed - see [`RunStepContext`](../core/run-step.ts).
318
+ *
319
+ * `runBin` and `logger` are bound to *this run* rather than left to be imported, which is the
320
+ * whole reason they are handed over: an imported `runBin` knows neither the cwd nor the resolved
321
+ * log level.
322
+ */
323
+ /**
324
+ * A `run.<script>.before`/`.exec`/`.after` (or `version.<slot>`) value: one step, or several to
325
+ * run in sequence. A shell command or a function, and a list may mix them.
326
+ *
327
+ * **Anything else throws, naming the path.** It used to `return []`, which meant a value rman did
328
+ * not recognize was dropped with no trace: writing a function here - the obvious guess, and now
329
+ * the supported form - produced `1 succeeded, 0 failed` with the step never run (measured). A
330
+ * configuration mistake has to be loud; silence here reads as success.
331
+ *
332
+ * Exported, and the only implementation: `VersionService` used to carry a second one that behaved
333
+ * differently, which is how `version.<slot>` came to join its array with `' && '`.
334
+ */
335
+ function normalizeScriptValue(value, at) {
336
+ const items = Array.isArray(value) ? value : [value];
337
+ const steps = [];
338
+ for (let i = 0; i < items.length; i++) {
339
+ const item = items[i];
340
+ /** An empty string and an absent value are both "nothing here", which is how a `"[*]"` block
341
+ * declaring a slot some packages don't use has always behaved. */
342
+ if (item === undefined || item === null || item === '')
343
+ continue;
344
+ if (typeof item === 'string' || typeof item === 'function') {
345
+ steps.push(item);
346
+ continue;
347
+ }
348
+ const where = Array.isArray(value) ? `${at}[${i}]` : at;
349
+ throw new Error(`"${where}" must be a shell command or a function, but it is ${describeValue(item)}.\n` +
350
+ ` A list of either (or both) runs them in sequence.`);
351
+ }
352
+ return steps;
353
+ }
354
+ RunService.normalizeScriptValue = normalizeScriptValue;
355
+ function createStepContext(pkg, cwd) {
356
+ const logLevel = resolveRootLogLevel(pkg.repository);
357
+ return {
358
+ pkg,
359
+ repository: pkg.repository,
360
+ cwd,
361
+ runBin: (bin, argv, opts) => runBin(bin, argv, { cwd, logLevel, app: pkg.repository.app, ...opts }),
362
+ logger: new Logger(logLevel),
363
+ };
364
+ }
365
+ RunService.createStepContext = createStepContext;
366
+ function resolveEnvPlaceholders(value) {
367
+ return value.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
368
+ }
369
+ /** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
370
+ * and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
371
+ function parseIfExpr(raw) {
372
+ if (typeof raw !== 'string' || !raw.trim())
373
+ return undefined;
374
+ const tokens = tokenizeIf(raw);
375
+ if (!tokens.length)
376
+ return undefined;
377
+ let i = 0;
378
+ const peek = () => tokens[i];
379
+ const next = () => tokens[i++];
380
+ function parseUnary() {
381
+ if (peek()?.toLowerCase() === 'not') {
382
+ next();
383
+ return { kind: 'not', node: parseUnary() };
384
+ }
385
+ const tok = next() ?? '';
386
+ /** A token containing whitespace can only be a collapsed `(...)` group - plain
387
+ * atom/operator tokens never do, since they're themselves split on whitespace. */
388
+ if (/\s/.test(tok)) {
389
+ const inner = parseIfExpr(tok);
390
+ if (!inner)
391
+ throw new Error(`Empty group in "if" expression: "${raw}"`);
392
+ return inner;
393
+ }
394
+ let value;
395
+ if (peek() === '=') {
396
+ next();
397
+ value = resolveEnvPlaceholders(next() ?? '');
398
+ }
399
+ return { kind: 'atom', name: tok, value };
400
+ }
401
+ function parseAnd() {
402
+ let node = parseUnary();
403
+ while (peek()?.toLowerCase() === 'and') {
404
+ next();
405
+ node = { kind: 'and', left: node, right: parseUnary() };
406
+ }
407
+ return node;
408
+ }
409
+ function parseOr() {
410
+ let node = parseAnd();
411
+ while (peek()?.toLowerCase() === 'or') {
412
+ next();
413
+ node = { kind: 'or', left: node, right: parseAnd() };
414
+ }
415
+ return node;
416
+ }
417
+ return parseOr();
418
+ }
419
+ RunService.parseIfExpr = parseIfExpr;
420
+ /** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
421
+ * for the same reference hash across packages/scripts in a single run. */
422
+ async function evaluateIf(repository, pkg, node, statusCache) {
423
+ if (node.kind === 'and') {
424
+ if (!(await evaluateIf(repository, pkg, node.left, statusCache)))
425
+ return false;
426
+ return evaluateIf(repository, pkg, node.right, statusCache);
427
+ }
428
+ if (node.kind === 'or') {
429
+ if (await evaluateIf(repository, pkg, node.left, statusCache))
430
+ return true;
431
+ return evaluateIf(repository, pkg, node.right, statusCache);
432
+ }
433
+ if (node.kind === 'not') {
434
+ return !(await evaluateIf(repository, pkg, node.node, statusCache));
435
+ }
436
+ return evaluateIfAtom(repository, pkg, node.name, node.value, statusCache);
437
+ }
438
+ RunService.evaluateIf = evaluateIf;
435
439
  })(RunService || (RunService = {}));
436
440
  /**
437
441
  * Classic one-line-per-step log ("info build pkg ┆ step success ┆ command (123 ms)"),
@@ -483,11 +487,30 @@ function toStep(slot, value) {
483
487
  *
484
488
  * Restored in a `finally`, because a step that throws must not leave the rest of the run writing
485
489
  * into a log nobody reads.
490
+ *
491
+ * **Its failure message is reported here, because nothing else does it.** A shell step's reason
492
+ * reaches the user on its own - the output streams through `onLine` or straight to the terminal,
493
+ * and `exec` names the command and its exit code. A function step has neither: it fails by
494
+ * throwing, and the throw goes to the outer `catch` that marks the package failed and rethrows an
495
+ * error the CLI treats as already-logged. Measured on a real config whose build step threw a
496
+ * carefully worded message about a missing `tsconfig.json`: the run printed
497
+ * `error build pkg-forgot ┆ exec failed ┆ buildWithTsc` and exited 1, and the message appeared
498
+ * nowhere at all - so the one thing that said what to do was the one thing dropped.
499
+ *
500
+ * Written where a shell step's output goes, so it needs no second channel: through `onLine` with
501
+ * the panel on (the step's own log, which is what the panel shows for a failed item), and to
502
+ * stderr with it off, ahead of the `failed` line - the order a shell step already produces.
486
503
  */
487
504
  async function runFunctionStep(run, pkg, cwd, onLine) {
488
505
  const context = RunService.createStepContext(pkg, cwd);
489
506
  if (!onLine) {
490
- await run(context);
507
+ try {
508
+ await run(context);
509
+ }
510
+ catch (e) {
511
+ console.error(colors.red(messageOf(e)));
512
+ throw e;
513
+ }
491
514
  return;
492
515
  }
493
516
  const console_ = globalThis.console;
@@ -504,11 +527,24 @@ async function runFunctionStep(run, pkg, cwd, onLine) {
504
527
  try {
505
528
  await run(context);
506
529
  }
530
+ catch (e) {
531
+ /** Through the patched `console` deliberately - `onLine` is still installed at this point, so
532
+ * the message lands in this step's log rather than over the panel it is drawn inside. */
533
+ for (const line of messageOf(e).split('\n'))
534
+ onLine(line);
535
+ throw e;
536
+ }
507
537
  finally {
508
538
  for (const method of CAPTURED_CONSOLE)
509
539
  console_[method] = original[method];
510
540
  }
511
541
  }
542
+ /** What a thrown value has to say for itself. A step may throw anything, and `String(undefined)`
543
+ * reading as `undefined` in a run log is worse than saying nothing was said. */
544
+ function messageOf(error) {
545
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';
546
+ return message.trim() || `the step threw ${inspect(error)}`;
547
+ }
512
548
  const CAPTURED_CONSOLE = ['log', 'info', 'warn', 'error', 'debug'];
513
549
  function format(args) {
514
550
  return args.map(arg => (typeof arg === 'string' ? arg : inspect(arg))).join(' ');
@@ -549,7 +585,7 @@ async function passesIf(repository, pkg, raw, cwd, statusCache) {
549
585
  function getScriptSteps(pkg, script) {
550
586
  const cfg = RunService.getConfig(pkg, script);
551
587
  const override = cfg.override === true;
552
- const contributed = firstContributed(pkg, script);
588
+ const contributed = contributedSlotsFor(pkg, script);
553
589
  const fromConfig = {
554
590
  before: RunService.normalizeScriptValue(cfg.before, `run.${script}.before`),
555
591
  exec: RunService.normalizeScriptValue(cfg.exec, `run.${script}.exec`),
@@ -565,19 +601,22 @@ function getScriptSteps(pkg, script) {
565
601
  }
566
602
  return steps;
567
603
  }
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;
604
+ /**
605
+ * What this package's own technology says it declares for `script`.
606
+ *
607
+ * **Its own, not every registered one.** This walked all the contributed sources and took the first
608
+ * that answered, which in a polyglot repository meant npm's `package.json#scripts` reader was handed
609
+ * a Cargo package and asked whether it declared `build` - its first line is
610
+ * `pkg.manifest.raw?.scripts`, so it was reading a manifest another technology produced. The package
611
+ * already knows which technology claimed it; asking anyone else was only ever a way of finding that
612
+ * out again.
613
+ */
614
+ function contributedSlotsFor(pkg, script) {
615
+ const slots = pkg.platform.getRunSteps?.(pkg, script);
616
+ return slots && (slots.before?.length || slots.exec?.length || slots.after?.length) ? slots : undefined;
577
617
  }
578
618
  /** In execution order - `before`, the script itself, then `after`. */
579
619
  const SCRIPT_SLOTS = ['before', 'exec', 'after'];
580
- const stepSources = [];
581
620
  /**
582
621
  * Splits on whitespace, but a `(...)` group collapses to a single token holding its inner text
583
622
  * (parens stripped, inner whitespace kept) - `evaluate` below re-tokenizes any token that still
@@ -1,3 +1,4 @@
1
+ import { RmanApplication } from '../core/application.js';
1
2
  import type { Package } from '../core/package.js';
2
3
  import type { Repository } from '../core/repository.js';
3
4
  import { VersionScheme } from '../core/version-scheme.js';
@@ -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
  *
@@ -230,9 +256,6 @@ export declare namespace VersionPlanService {
230
256
  * repository layering its own policy plugin after `rman-node` could never take effect - which is
231
257
  * the only reason to name two in the first place.
232
258
  */
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
259
  /**
237
260
  * The registered planner. **Throws** when there is none, rather than falling back to some
238
261
  * built-in default: `Manifest` and `Workspace` can degrade honestly (a package named after its
@@ -240,5 +263,5 @@ export declare namespace VersionPlanService {
240
263
  * merely a diminished one - a wrong cascade or a wrong boundary reports a release that is
241
264
  * plausible and untrue.
242
265
  */
243
- function getPlanner(): VersionPlanService;
266
+ function getPlanner(app: RmanApplication): VersionPlanService;
244
267
  }