rman 1.0.12 → 1.2.1

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 (102) hide show
  1. package/README.md +90 -70
  2. package/cli.js +226 -14
  3. package/commands/build.command.js +1 -0
  4. package/commands/changed.command.js +2 -2
  5. package/commands/changelog.command.js +13 -15
  6. package/commands/config.command.js +61 -0
  7. package/commands/diff.command.js +9 -4
  8. package/commands/exec.command.js +2 -8
  9. package/commands/github-release.command.js +1 -0
  10. package/commands/info.command.d.ts +9 -0
  11. package/commands/info.command.js +12 -2
  12. package/commands/run.command.js +5 -8
  13. package/commands/test.command.js +1 -0
  14. package/commands/version.command.js +53 -14
  15. package/constants.js +1 -1
  16. package/core/config.d.ts +265 -17
  17. package/core/config.js +651 -76
  18. package/core/custom-command.d.ts +133 -0
  19. package/core/custom-command.js +99 -0
  20. package/core/extends-config.d.ts +27 -0
  21. package/core/extends-config.js +89 -0
  22. package/core/manifest.d.ts +222 -0
  23. package/core/manifest.js +150 -0
  24. package/core/merge-config.d.ts +70 -0
  25. package/core/merge-config.js +193 -0
  26. package/core/package.d.ts +73 -7
  27. package/core/package.js +86 -24
  28. package/core/plugin.d.ts +112 -0
  29. package/core/plugin.js +189 -0
  30. package/core/repository.d.ts +91 -1
  31. package/core/repository.js +277 -132
  32. package/core/resolve-target.d.ts +12 -0
  33. package/core/resolve-target.js +33 -0
  34. package/core/run-step.d.ts +75 -0
  35. package/core/run-step.js +1 -0
  36. package/core/version-scheme.d.ts +134 -0
  37. package/core/version-scheme.js +148 -0
  38. package/core/workspace.d.ts +68 -0
  39. package/core/workspace.js +83 -0
  40. package/index.d.ts +55 -8
  41. package/index.js +42 -7
  42. package/interfaces/rman-config.interface.d.ts +222 -46
  43. package/package.json +16 -7
  44. package/services/change-hash.service.d.ts +88 -0
  45. package/services/change-hash.service.js +112 -0
  46. package/services/changelog.service.d.ts +8 -13
  47. package/services/changelog.service.js +12 -11
  48. package/services/conventional-commits.service.d.ts +73 -0
  49. package/services/conventional-commits.service.js +116 -0
  50. package/services/docker-publish.service.js +1 -1
  51. package/services/exec.service.js +1 -1
  52. package/services/github-release.service.d.ts +2 -2
  53. package/services/github-release.service.js +10 -5
  54. package/services/list.service.js +5 -2
  55. package/services/run.service.d.ts +112 -6
  56. package/services/run.service.js +265 -89
  57. package/services/system-info.d.ts +22 -7
  58. package/services/system-info.js +8 -23
  59. package/services/version-plan.service.d.ts +244 -0
  60. package/services/version-plan.service.js +414 -0
  61. package/services/version.service.d.ts +102 -82
  62. package/services/version.service.js +226 -434
  63. package/services.d.ts +5 -3
  64. package/services.js +5 -3
  65. package/utils/bin-path.d.ts +59 -0
  66. package/utils/bin-path.js +82 -0
  67. package/utils/child-tracker.d.ts +16 -0
  68. package/utils/child-tracker.js +30 -0
  69. package/utils/exec.d.ts +13 -2
  70. package/utils/exec.js +17 -17
  71. package/utils/git.d.ts +9 -3
  72. package/utils/git.js +10 -2
  73. package/utils/package-filter.d.ts +33 -2
  74. package/utils/package-filter.js +47 -7
  75. package/utils/printable-config.d.ts +15 -0
  76. package/utils/printable-config.js +42 -0
  77. package/utils/release-version.js +3 -3
  78. package/utils/run-bin.d.ts +46 -0
  79. package/utils/run-bin.js +63 -0
  80. package/utils/version-stamp.d.ts +14 -6
  81. package/utils/version-stamp.js +25 -13
  82. package/commands/ci.command.js +0 -30
  83. package/commands/clean.command.d.ts +0 -3
  84. package/commands/clean.command.js +0 -36
  85. package/commands/publish.command.d.ts +0 -3
  86. package/commands/publish.command.js +0 -225
  87. package/rmanrc.schema.json +0 -392
  88. package/services/ci.service.d.ts +0 -40
  89. package/services/ci.service.js +0 -204
  90. package/services/clean.service.d.ts +0 -42
  91. package/services/clean.service.js +0 -226
  92. package/services/publish.service.d.ts +0 -79
  93. package/services/publish.service.js +0 -273
  94. package/utils/change-hash.d.ts +0 -68
  95. package/utils/change-hash.js +0 -98
  96. package/utils/conventional-commits.d.ts +0 -52
  97. package/utils/conventional-commits.js +0 -90
  98. package/utils/npm-run-path.d.ts +0 -67
  99. package/utils/npm-run-path.js +0 -63
  100. package/utils/workspace-range.d.ts +0 -17
  101. package/utils/workspace-range.js +0 -28
  102. /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
@@ -1,49 +1,142 @@
1
1
  import os from 'node:os';
2
- import parseNpmScript from '@netlify/parse-npm-script';
2
+ 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
6
  import { exec } from '../utils/exec.js';
7
- import { LOG_LEVELS, resolveRootLogLevel } from '../utils/logger.js';
7
+ import { LOG_LEVELS, Logger, resolveRootLogLevel } from '../utils/logger.js';
8
8
  import { filterPackages } from '../utils/package-filter.js';
9
9
  import { ProgressPanel } from '../utils/progress-panel.js';
10
+ import { runBin } from '../utils/run-bin.js';
10
11
  export var RunService;
11
12
  (function (RunService) {
12
13
  /**
13
- * A package's `.rmanrc` (cascaded) can configure `run.<script>.*` - e.g.
14
- * run:
15
- * build:
16
- * concurrency: 2
17
- * lint:
18
- * topo: false
19
- * bail: false
20
- * and a package can opt itself out of a script entirely:
21
- * run:
22
- * build:
23
- * skip: true
24
- * or supply the command(s) to run when its own package.json doesn't define this script (or its
25
- * pre/post hooks) at all - a single string, or an array to run several in sequence - see
26
- * `getScriptSteps`:
27
- * run:
28
- * build:
29
- * before: [node ./generate.js, node ./validate.js]
30
- * exec: tsc -b
31
- * after: node ./copy-assets.js
32
- * override: true # use these even if the package *does* define its own
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.
33
62
  *
34
- * A bare string is shorthand for `exec`, which is by far the common case - a script that is just
35
- * a command, with nothing to configure about how it runs:
36
- * run:
37
- * test: mocha # same as test: { exec: mocha }
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.
38
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;
39
79
  function getConfig(pkg, script) {
40
80
  const runCfg = pkg.config?.run;
41
81
  const cfg = runCfg && typeof runCfg === 'object' ? runCfg[script] : undefined;
42
- if (typeof cfg === 'string' || Array.isArray(cfg))
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))
43
86
  return { exec: cfg };
44
87
  return cfg && typeof cfg === 'object' ? cfg : {};
45
88
  }
46
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;
47
140
  function resolveEnvPlaceholders(value) {
48
141
  return value.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
49
142
  }
@@ -151,7 +244,7 @@ export var RunService;
151
244
  * abort using *its own* resolved bail setting (see `runSteps` below) - power-tasks' own
152
245
  * `bail` is a single blanket policy for the whole batch, it can't vary per package. */
153
246
  let rootTask;
154
- const runSteps = async (ctx, pkgLabel, steps, cwd, pkgBail, pkgLogLevel) => {
247
+ const runSteps = async (ctx, pkg, pkgLabel, steps, cwd, pkgBail, pkgLogLevel) => {
155
248
  ctx.status = 'running';
156
249
  ctx.startedAt = Date.now();
157
250
  try {
@@ -160,15 +253,14 @@ export var RunService;
160
253
  ctx.currentStep = step.name;
161
254
  ctx.stepIndex = i;
162
255
  if (panel.enabled) {
163
- await exec(step.command, {
164
- cwd,
165
- stdio: 'pipe',
166
- onLine: (line, stdio) => {
167
- ctx.log.push(line);
168
- ctx.lastLine = line;
169
- void stdio;
170
- },
171
- });
256
+ const onLine = (line) => {
257
+ ctx.log.push(line);
258
+ ctx.lastLine = line;
259
+ };
260
+ if (step.run)
261
+ await runFunctionStep(step.run, pkg, cwd, onLine);
262
+ else
263
+ await exec(step.command, { cwd, stdio: 'pipe', onLine });
172
264
  }
173
265
  else {
174
266
  /** Match the classic rman output: raw command output streams straight through
@@ -177,7 +269,12 @@ export var RunService;
177
269
  const stepStart = Date.now();
178
270
  let stepError;
179
271
  try {
180
- await exec(step.command, { cwd, stdio: 'inherit' });
272
+ /** No capture with the panel off: the step owns the terminal, exactly as a shell
273
+ * step's `stdio: 'inherit'` does. */
274
+ if (step.run)
275
+ await runFunctionStep(step.run, pkg, cwd);
276
+ else
277
+ await exec(step.command, { cwd, stdio: 'inherit' });
181
278
  }
182
279
  catch (e) {
183
280
  stepError = e;
@@ -209,19 +306,25 @@ export var RunService;
209
306
  * Only in a monorepo. Without one the root *is* the single package, already in the loop below
210
307
  * with the same hooks and the same directory - a bookend would simply run each of them a
211
308
  * second time. */
212
- const rootIf = !cwdScope && repository.monorepo && parseIfExpr(rootCfg.if);
213
- const rootIfPasses = rootIf ? await evaluateIf(repository, repository.rootPackage, rootIf, ifStatusCache) : true;
214
- const rootSkipped = !!cwdScope || !repository.monorepo || rootCfg.skip === true || !rootIfPasses;
309
+ /** Short-circuited deliberately: a root already out of the run for a structural reason must not
310
+ * have its `if` evaluated, now that evaluating one can mean calling the repository's own code. */
311
+ const rootSkipped = !!cwdScope ||
312
+ !repository.monorepo ||
313
+ rootCfg.skip === true ||
314
+ !(await passesIf(repository, repository.rootPackage, rootCfg.if, repository.dirname, ifStatusCache));
215
315
  const rootSteps = rootSkipped ? [] : getScriptSteps(repository.rootPackage, script);
216
- const rootPre = rootSteps.filter(s => s.name === 'pre' + script);
217
- const rootPost = rootSteps.filter(s => s.name === 'post' + script);
316
+ /** Filtered on the slot, not on `'pre' + script`: the step labels are `before`/`exec`/`after`
317
+ * now - the same words the config uses - rather than npm's `pre<script>` naming, which moved
318
+ * out with the package.json source. */
319
+ const rootPre = rootSteps.filter(s => s.name === 'before');
320
+ const rootPost = rootSteps.filter(s => s.name === 'after');
218
321
  const rootPreName = 'root (pre)';
219
322
  const rootPostName = 'root (post)';
220
323
  if (rootPre.length) {
221
324
  const ctx = panel.addItem(rootPreName, rootPre.length);
222
325
  const pkgBail = resolveBail(options.bail, repository.rootPackage, script, true);
223
326
  const pkgLogLevel = resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault);
224
- children.push(new Task(() => runSteps(ctx, 'root', rootPre, repository.dirname, pkgBail, pkgLogLevel), {
327
+ children.push(new Task(() => runSteps(ctx, repository.rootPackage, 'root', rootPre, repository.dirname, pkgBail, pkgLogLevel), {
225
328
  name: ctx.name,
226
329
  exclusive: true,
227
330
  }));
@@ -231,8 +334,7 @@ export var RunService;
231
334
  const pkgCfg = getConfig(pkg, script);
232
335
  if (pkgCfg.skip === true)
233
336
  continue;
234
- const pkgIf = parseIfExpr(pkgCfg.if);
235
- if (pkgIf && !(await evaluateIf(repository, pkg, pkgIf, ifStatusCache)))
337
+ if (!(await passesIf(repository, pkg, pkgCfg.if, pkg.dirname, ifStatusCache)))
236
338
  continue;
237
339
  const steps = getScriptSteps(pkg, script);
238
340
  if (steps.length)
@@ -246,10 +348,12 @@ export var RunService;
246
348
  const pkgTopo = resolveBool(options.topo, pkg, script, 'topo', topo);
247
349
  const pkgBail = resolveBail(options.bail, pkg, script, true);
248
350
  const pkgLogLevel = resolveLogLevel(options.logLevel, pkg, script, logLevelDefault);
249
- const dependencies = pkgTopo ? pkg.dependencies.filter(d => stepsByPackage.has(d)) : [];
351
+ /** power-tasks identifies a task by its name string, so the graph is handed over as names -
352
+ * the references are what rman reasons with, the names are what the scheduler wants. */
353
+ const dependencies = pkgTopo ? pkg.dependencies.filter(d => stepsByPackage.has(d.name)).map(d => d.name) : [];
250
354
  if (rootPre.length)
251
355
  dependencies.push(rootPreName);
252
- children.push(new Task(() => runSteps(ctx, pkg.name, steps, pkg.dirname, pkgBail, pkgLogLevel), {
356
+ children.push(new Task(() => runSteps(ctx, pkg, pkg.name, steps, pkg.dirname, pkgBail, pkgLogLevel), {
253
357
  name: ctx.name,
254
358
  dependencies,
255
359
  }));
@@ -258,7 +362,7 @@ export var RunService;
258
362
  const ctx = panel.addItem(rootPostName, rootPost.length);
259
363
  const pkgBail = resolveBail(options.bail, repository.rootPackage, script, true);
260
364
  const pkgLogLevel = resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault);
261
- children.push(new Task(() => runSteps(ctx, 'root', rootPost, repository.dirname, pkgBail, pkgLogLevel), {
365
+ children.push(new Task(() => runSteps(ctx, repository.rootPackage, 'root', rootPost, repository.dirname, pkgBail, pkgLogLevel), {
262
366
  name: ctx.name,
263
367
  exclusive: true,
264
368
  /** Must wait for every package task to finish, not just be "exclusive" once it starts. */
@@ -341,7 +445,7 @@ function printLegacyExecutingLine(commandName, pkgLabel, step, level) {
341
445
  if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('verbose'))
342
446
  return;
343
447
  const sep = colors.gray('┆');
344
- console.log(colors.magenta('verbose'), commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), colors.cyanBright.bold('executing'), sep, step.command);
448
+ console.log(colors.magenta('verbose'), commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), colors.cyanBright.bold('executing'), sep, step.label);
345
449
  }
346
450
  function printLegacyStepLine(commandName, pkgLabel, step, durationMs, level, error) {
347
451
  if (!error && LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('info'))
@@ -351,57 +455,129 @@ function printLegacyStepLine(commandName, pkgLabel, step, durationMs, level, err
351
455
  const sep = colors.gray('┆');
352
456
  const levelLabel = error ? colors.red('error') : colors.green('info');
353
457
  const status = error ? colors.red.bold('failed') : colors.green.bold('success');
354
- console.log(levelLabel, commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), status, sep, step.command, colors.yellow(` (${durationMs} ms)`));
458
+ console.log(levelLabel, commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), status, sep, step.label, colors.yellow(` (${durationMs} ms)`));
459
+ }
460
+ function describeValue(value) {
461
+ if (Array.isArray(value))
462
+ return 'a nested array';
463
+ if (value && typeof value === 'object')
464
+ return 'an object';
465
+ return `a ${typeof value} (${JSON.stringify(value)})`;
355
466
  }
356
- /** A `run.<script>.script`/`.preScript`/`.postScript` value: one command, or several to run in sequence. */
357
- function normalizeScriptValue(value) {
358
- if (typeof value === 'string')
359
- return value || undefined;
360
- if (Array.isArray(value)) {
361
- // joined with "&&", same as a multi-command package.json script - each entry becomes its
362
- // own step below, and an earlier failure stops the rest (npm's own semantics).
363
- const parts = value.filter((v) => typeof v === 'string' && !!v);
364
- return parts.length ? parts.join(' && ') : undefined;
467
+ /** One step, with the label the panel and the per-step log show. A function's own name - so
468
+ * `function copyDocs()` and `const copyDocs = () => {}` both read as `copyDocs` - falling back to
469
+ * the slot's own word for one passed inline, which has no name at all. */
470
+ function toStep(slot, value) {
471
+ if (typeof value === 'function')
472
+ return { name: slot, label: value.name || `${slot} (js)`, run: value };
473
+ return { name: slot, label: value, command: value };
474
+ }
475
+ /**
476
+ * Runs a function step.
477
+ *
478
+ * **`console` is redirected while it runs, but only when the panel is on** - and that is the same
479
+ * split a shell step already makes. `exec` hands the panel its output through `stdio: 'pipe'` and
480
+ * `onLine`, so a function writing straight to the real stdout would print *over* the panel it is
481
+ * being rendered inside. With the panel off, `exec` uses `stdio: 'inherit'` and the step owns the
482
+ * terminal; a function gets the same, untouched.
483
+ *
484
+ * Restored in a `finally`, because a step that throws must not leave the rest of the run writing
485
+ * into a log nobody reads.
486
+ */
487
+ async function runFunctionStep(run, pkg, cwd, onLine) {
488
+ const context = RunService.createStepContext(pkg, cwd);
489
+ if (!onLine) {
490
+ await run(context);
491
+ return;
365
492
  }
366
- return undefined;
493
+ const console_ = globalThis.console;
494
+ const original = {};
495
+ for (const method of CAPTURED_CONSOLE) {
496
+ original[method] = console_[method];
497
+ console_[method] = (...args) => {
498
+ /** Split, because one `console.log` may carry several lines and the panel's log is a list of
499
+ * them - a multi-line entry would render as one unreadable row. */
500
+ for (const line of format(args).split('\n'))
501
+ onLine(line);
502
+ };
503
+ }
504
+ try {
505
+ await run(context);
506
+ }
507
+ finally {
508
+ for (const method of CAPTURED_CONSOLE)
509
+ console_[method] = original[method];
510
+ }
511
+ }
512
+ const CAPTURED_CONSOLE = ['log', 'info', 'warn', 'error', 'debug'];
513
+ function format(args) {
514
+ return args.map(arg => (typeof arg === 'string' ? arg : inspect(arg))).join(' ');
367
515
  }
368
516
  /**
369
- * Resolves a package's steps for `script`, including its own pre/post hooks (npm's own
370
- * convention) - `.rmanrc run.<script>.script`/`.preScript`/`.postScript` (cascaded) can each supply
371
- * a command (or an array of them, run in sequence) to use when the package's own package.json has
372
- * none for that slot (`script`/`pre<script>`/`post<script>`), letting a package run a script it
373
- * never defined at all. `run.<script>.override` goes further: with it, the config's command
374
- * replaces the package's own for a slot even when the package *does* define one there.
517
+ * Whether a script runs for `pkg` at all - `run.<script>.if`, in either of its two forms.
518
+ *
519
+ * The function form is checked **first**: `parseIfExpr` answers `undefined` for anything that is
520
+ * not a string, which the caller reads as "no condition given", so a function reaching it would be
521
+ * a condition that silently always passed.
522
+ */
523
+ async function passesIf(repository, pkg, raw, cwd, statusCache) {
524
+ if (typeof raw === 'function') {
525
+ return !!(await raw(RunService.createStepContext(pkg, cwd)));
526
+ }
527
+ const node = RunService.parseIfExpr(raw);
528
+ return node ? RunService.evaluateIf(repository, pkg, node, statusCache) : true;
529
+ }
530
+ /**
531
+ * Resolves a package's steps for `script`, from its `.rmanrc` and from whatever sources plugins
532
+ * contributed (`RunService.addStepSource`).
533
+ *
534
+ * **Resolved slot by slot, not script by script**, and that is load-bearing: a package whose
535
+ * `package.json` defines only `build` still gets `before`/`after` from a `"[*]"` config block. A
536
+ * whole-script precedence would silently drop them.
537
+ *
538
+ * Precedence per slot, first match winning:
539
+ *
540
+ * 1. `run.<script>.override: true` -> the config, always. The escape hatch for "ignore what the
541
+ * 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.
375
548
  */
376
549
  function getScriptSteps(pkg, script) {
377
550
  const cfg = RunService.getConfig(pkg, script);
378
- const json = { ...pkg.json, scripts: { ...(pkg.json.scripts || {}) } };
379
- const applyConfigScript = (key, rawValue) => {
380
- const value = normalizeScriptValue(rawValue);
381
- if (!value)
382
- return;
383
- const hasOwn = typeof json.scripts[key] === 'string' && json.scripts[key];
384
- if (cfg.override === true || !hasOwn)
385
- json.scripts[key] = value;
551
+ const override = cfg.override === true;
552
+ const contributed = firstContributed(pkg, script);
553
+ const fromConfig = {
554
+ before: RunService.normalizeScriptValue(cfg.before, `run.${script}.before`),
555
+ exec: RunService.normalizeScriptValue(cfg.exec, `run.${script}.exec`),
556
+ after: RunService.normalizeScriptValue(cfg.after, `run.${script}.after`),
386
557
  };
387
- applyConfigScript(script, cfg.exec);
388
- applyConfigScript('pre' + script, cfg.before);
389
- applyConfigScript('post' + script, cfg.after);
390
- json.scripts[script] = json.scripts[script] || '#';
391
- const info = parseNpmScript(json, 'npm run ' + script);
392
- if (!info?.raw?.length)
393
- return [];
394
558
  const steps = [];
395
- for (const s of info.steps) {
396
- const parsed = Array.isArray(s.parsed) ? s.parsed : [s.parsed];
397
- for (const command of parsed) {
398
- if (command === '#')
399
- continue;
400
- steps.push({ name: s.name, command });
401
- }
559
+ 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)
564
+ steps.push(toStep(slot, value));
402
565
  }
403
566
  return steps;
404
567
  }
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;
577
+ }
578
+ /** In execution order - `before`, the script itself, then `after`. */
579
+ const SCRIPT_SLOTS = ['before', 'exec', 'after'];
580
+ const stepSources = [];
405
581
  /**
406
582
  * Splits on whitespace, but a `(...)` group collapses to a single token holding its inner text
407
583
  * (parens stripped, inner whitespace kept) - `evaluate` below re-tokenizes any token that still
@@ -8,15 +8,30 @@ export declare namespace SystemInfo {
8
8
  root: string;
9
9
  packageCount: number;
10
10
  }
11
- /** Whatever `envinfo` reports (OS/CPU/Memory/Shell, Node + the resolved package manager, git,
12
- * installed rman/typescript versions), grouped by category - shape is `envinfo`'s own, not ours. */
11
+ /** Whatever `envinfo` reports, grouped by category - shape is `envinfo`'s own, not ours. */
13
12
  type SystemInfo = Record<string, Record<string, unknown>>;
14
13
  /**
15
- * `packageManager` (the repository's resolved `.rmanrc "packageManager"`, default `'npm'`)
16
- * decides which package manager's version actually gets queried/reported - a pnpm-configured
17
- * repo has no real use for npm's own version, since every package-manager-aware command
18
- * (`ci`/`publish`) already shells out to pnpm, not npm, for it.
14
+ * **Deliberately empty of anything language-specific.** A plugin adds what its own ecosystem
15
+ * needs by augmenting this interface - see `rman-node`, which adds `packageManager` and turns it
16
+ * into `envinfo` categories. There is no `packageManager` here, and that is the point: the core
17
+ * has no opinion about npm, so a repository in another language cannot end up reporting
18
+ * "npm: Not Found", which is a wrong answer rather than a missing feature.
19
19
  */
20
- function getSystemInfo(packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun', options?: envinfo.RunConfig): Promise<SystemInfo.SystemInfo>;
20
+ interface Options {
21
+ /**
22
+ * The repository the report is about. The core uses it for nothing - `getRepositoryInfo` is a
23
+ * separate call - and it is here **for augmentations**, which need somewhere to read a setting
24
+ * from: `rman-node` takes `.rmanrc "packageManager"` off it.
25
+ */
26
+ repository?: Repository;
27
+ /** Extra `envinfo` categories, merged **over** the defaults - so an augmentation can replace
28
+ * `Binaries` rather than only append to it. */
29
+ envinfo?: envinfo.RunConfig;
30
+ }
31
+ /** The shape of `getSystemInfo` - named so a plugin can wrap it without restating the signature
32
+ * (see `rman-node`'s system-info augmentation). */
33
+ type GetSystemInfo = (options?: Options) => Promise<SystemInfo.SystemInfo>;
34
+ /** What this machine is, in terms true of any repository: OS, CPU, memory, shell, Node, git. */
35
+ function getSystemInfo(options?: Options): Promise<SystemInfo.SystemInfo>;
21
36
  function getRepositoryInfo(repository: Repository): SystemInfo.RepositoryInfo;
22
37
  }
@@ -1,33 +1,18 @@
1
1
  import envinfo from 'envinfo';
2
2
  export var SystemInfo;
3
3
  (function (SystemInfo) {
4
- /** `.rmanrc "packageManager"` value -> the `Binaries` key `envinfo` recognizes for it (`npm`/
5
- * `pnpm`/`bun` are lowercase, `Yarn` isn't - envinfo's own naming, not ours). */
6
- const PACKAGE_MANAGER_BINARY = {
7
- npm: 'npm',
8
- yarn: 'Yarn',
9
- pnpm: 'pnpm',
10
- bun: 'bun',
11
- };
12
- /**
13
- * `packageManager` (the repository's resolved `.rmanrc "packageManager"`, default `'npm'`)
14
- * decides which package manager's version actually gets queried/reported - a pnpm-configured
15
- * repo has no real use for npm's own version, since every package-manager-aware command
16
- * (`ci`/`publish`) already shells out to pnpm, not npm, for it.
17
- */
18
- async function getSystemInfo(packageManager, options) {
4
+ /** What this machine is, in terms true of any repository: OS, CPU, memory, shell, Node, git. */
5
+ async function getSystemInfo(options) {
19
6
  return JSON.parse(await envinfo.run({
20
7
  System: ['OS', 'CPU', 'Memory', 'Shell'],
21
- Binaries: ['Node', PACKAGE_MANAGER_BINARY[packageManager ?? 'npm']],
8
+ Binaries: ['Node'],
22
9
  Utilities: ['Git'],
23
- npmPackages: ['rman', 'typescript'],
24
- npmGlobalPackages: ['typescript'],
25
- ...options,
10
+ ...options?.envinfo,
26
11
  },
27
- // showNotFound: without it, envinfo *omits* a configured-but-uninstalled package manager
28
- // from Binaries entirely (indistinguishable from never having asked) instead of reporting
29
- // "Not Found" - worth surfacing, since it means .rmanrc "packageManager" points at
30
- // something that isn't actually on this machine.
12
+ // showNotFound: without it, envinfo *omits* a configured-but-uninstalled binary from
13
+ // Binaries entirely (indistinguishable from never having asked) instead of reporting
14
+ // "Not Found" - worth surfacing, since it means something a config points at isn't
15
+ // actually on this machine.
31
16
  { json: true, showNotFound: true }));
32
17
  }
33
18
  SystemInfo.getSystemInfo = getSystemInfo;