rman 0.37.4 → 1.0.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 (120) hide show
  1. package/README.md +364 -21
  2. package/cli.d.ts +1 -0
  3. package/cli.js +52 -28
  4. package/commands/build.command.d.ts +3 -0
  5. package/commands/build.command.js +14 -0
  6. package/commands/changed.command.d.ts +3 -0
  7. package/commands/changed.command.js +29 -0
  8. package/commands/changelog.command.d.ts +3 -0
  9. package/commands/changelog.command.js +63 -0
  10. package/commands/ci.command.d.ts +3 -0
  11. package/commands/ci.command.js +30 -0
  12. package/commands/clean.command.d.ts +3 -0
  13. package/commands/clean.command.js +36 -0
  14. package/commands/diff.command.d.ts +3 -0
  15. package/commands/diff.command.js +51 -0
  16. package/commands/exec.command.d.ts +3 -0
  17. package/commands/exec.command.js +83 -0
  18. package/commands/import.command.d.ts +3 -0
  19. package/commands/import.command.js +27 -0
  20. package/commands/info.command.d.ts +3 -0
  21. package/commands/info.command.js +61 -0
  22. package/commands/list.command.d.ts +3 -0
  23. package/commands/list.command.js +113 -0
  24. package/commands/publish.command.d.ts +3 -0
  25. package/commands/publish.command.js +147 -0
  26. package/commands/run.command.d.ts +6 -0
  27. package/commands/run.command.js +80 -0
  28. package/commands/test.command.d.ts +3 -0
  29. package/commands/test.command.js +14 -0
  30. package/commands/version.command.d.ts +3 -0
  31. package/commands/version.command.js +125 -0
  32. package/constants.js +1 -1
  33. package/core/config.d.ts +13 -0
  34. package/core/config.js +61 -0
  35. package/core/package.d.ts +2 -0
  36. package/core/package.js +2 -0
  37. package/core/repository.d.ts +36 -3
  38. package/core/repository.js +90 -45
  39. package/index.d.ts +17 -1
  40. package/index.js +15 -1
  41. package/package.json +11 -8
  42. package/services/changelog.service.d.ts +99 -0
  43. package/services/changelog.service.js +272 -0
  44. package/services/ci.service.d.ts +40 -0
  45. package/services/ci.service.js +204 -0
  46. package/services/clean.service.d.ts +42 -0
  47. package/services/clean.service.js +226 -0
  48. package/services/exec.service.d.ts +35 -0
  49. package/services/exec.service.js +114 -0
  50. package/services/import.service.d.ts +30 -0
  51. package/services/import.service.js +84 -0
  52. package/services/list.service.d.ts +27 -0
  53. package/services/list.service.js +25 -0
  54. package/services/publish.service.d.ts +79 -0
  55. package/services/publish.service.js +199 -0
  56. package/services/run.service.d.ts +99 -0
  57. package/services/run.service.js +432 -0
  58. package/services/system-info.d.ts +16 -0
  59. package/services/system-info.js +25 -0
  60. package/services/version.service.d.ts +95 -0
  61. package/services/version.service.js +450 -0
  62. package/services.d.ts +10 -0
  63. package/services.js +10 -0
  64. package/utils/branch-guard.d.ts +25 -0
  65. package/utils/branch-guard.js +61 -0
  66. package/utils/change-hash.d.ts +55 -0
  67. package/utils/change-hash.js +80 -0
  68. package/utils/conventional-commits.d.ts +39 -0
  69. package/utils/conventional-commits.js +43 -0
  70. package/utils/exec.d.ts +5 -6
  71. package/utils/exec.js +46 -60
  72. package/utils/git.d.ts +86 -0
  73. package/utils/git.js +276 -0
  74. package/utils/live-region.d.ts +16 -0
  75. package/utils/live-region.js +59 -0
  76. package/utils/logger.d.ts +24 -0
  77. package/utils/logger.js +39 -0
  78. package/utils/npm-run-path.d.ts +1 -1
  79. package/utils/npm-run-path.js +10 -10
  80. package/utils/package-filter.d.ts +32 -0
  81. package/utils/package-filter.js +73 -0
  82. package/utils/progress-panel.d.ts +59 -0
  83. package/utils/progress-panel.js +149 -0
  84. package/utils/workspace-range.d.ts +17 -0
  85. package/utils/workspace-range.js +28 -0
  86. package/bin/rman.mjs +0 -4
  87. package/commands/build-command.d.ts +0 -11
  88. package/commands/build-command.js +0 -24
  89. package/commands/changed-command.d.ts +0 -16
  90. package/commands/changed-command.js +0 -32
  91. package/commands/ci-command.d.ts +0 -24
  92. package/commands/ci-command.js +0 -68
  93. package/commands/execute-command.d.ts +0 -19
  94. package/commands/execute-command.js +0 -72
  95. package/commands/info-command.d.ts +0 -10
  96. package/commands/info-command.js +0 -55
  97. package/commands/list-command.d.ts +0 -38
  98. package/commands/list-command.js +0 -134
  99. package/commands/multi-task-command.d.ts +0 -22
  100. package/commands/multi-task-command.js +0 -65
  101. package/commands/publish-command.d.ts +0 -24
  102. package/commands/publish-command.js +0 -131
  103. package/commands/run-command.d.ts +0 -28
  104. package/commands/run-command.js +0 -131
  105. package/commands/version-command.d.ts +0 -25
  106. package/commands/version-command.js +0 -195
  107. package/core/command.d.ts +0 -35
  108. package/core/command.js +0 -116
  109. package/core/constants.d.ts +0 -1
  110. package/core/constants.js +0 -2
  111. package/core/logger.d.ts +0 -12
  112. package/core/logger.js +0 -2
  113. package/utils/file-utils.d.ts +0 -4
  114. package/utils/file-utils.js +0 -28
  115. package/utils/git-utils.d.ts +0 -25
  116. package/utils/git-utils.js +0 -66
  117. package/utils/npm-utils.d.ts +0 -11
  118. package/utils/npm-utils.js +0 -33
  119. package/utils/package-not-found-error.d.ts +0 -2
  120. package/utils/package-not-found-error.js +0 -2
@@ -0,0 +1,432 @@
1
+ import os from 'node:os';
2
+ import parseNpmScript from '@netlify/parse-npm-script';
3
+ import colors from 'ansi-colors';
4
+ import { tokenize } from 'fast-tokenizer';
5
+ import { Task } from 'power-tasks';
6
+ import { exec } from '../utils/exec.js';
7
+ import { LOG_LEVELS, resolveRootLogLevel } from '../utils/logger.js';
8
+ import { filterPackages } from '../utils/package-filter.js';
9
+ import { ProgressPanel } from '../utils/progress-panel.js';
10
+ export var RunService;
11
+ (function (RunService) {
12
+ /**
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
+ * script: tsc -b
30
+ * preScript: [node ./generate.js, node ./validate.js]
31
+ * postScript: node ./copy-assets.js
32
+ * override: true # use these even if the package *does* define its own
33
+ */
34
+ function getConfig(pkg, script) {
35
+ const runCfg = pkg.config?.run;
36
+ const cfg = runCfg && typeof runCfg === 'object' ? runCfg[script] : undefined;
37
+ return cfg && typeof cfg === 'object' ? cfg : {};
38
+ }
39
+ RunService.getConfig = getConfig;
40
+ function resolveEnvPlaceholders(value) {
41
+ return value.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
42
+ }
43
+ /** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
44
+ * and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
45
+ function parseIfExpr(raw) {
46
+ if (typeof raw !== 'string' || !raw.trim())
47
+ return undefined;
48
+ const tokens = tokenizeIf(raw);
49
+ if (!tokens.length)
50
+ return undefined;
51
+ let i = 0;
52
+ const peek = () => tokens[i];
53
+ const next = () => tokens[i++];
54
+ function parseUnary() {
55
+ if (peek()?.toLowerCase() === 'not') {
56
+ next();
57
+ return { kind: 'not', node: parseUnary() };
58
+ }
59
+ const tok = next() ?? '';
60
+ /** A token containing whitespace can only be a collapsed `(...)` group - plain
61
+ * atom/operator tokens never do, since they're themselves split on whitespace. */
62
+ if (/\s/.test(tok)) {
63
+ const inner = parseIfExpr(tok);
64
+ if (!inner)
65
+ throw new Error(`Empty group in "if" expression: "${raw}"`);
66
+ return inner;
67
+ }
68
+ let value;
69
+ if (peek() === '=') {
70
+ next();
71
+ value = resolveEnvPlaceholders(next() ?? '');
72
+ }
73
+ return { kind: 'atom', name: tok, value };
74
+ }
75
+ function parseAnd() {
76
+ let node = parseUnary();
77
+ while (peek()?.toLowerCase() === 'and') {
78
+ next();
79
+ node = { kind: 'and', left: node, right: parseUnary() };
80
+ }
81
+ return node;
82
+ }
83
+ function parseOr() {
84
+ let node = parseAnd();
85
+ while (peek()?.toLowerCase() === 'or') {
86
+ next();
87
+ node = { kind: 'or', left: node, right: parseAnd() };
88
+ }
89
+ return node;
90
+ }
91
+ return parseOr();
92
+ }
93
+ RunService.parseIfExpr = parseIfExpr;
94
+ /** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
95
+ * for the same reference hash across packages/scripts in a single run. */
96
+ async function evaluateIf(repository, pkg, node, statusCache) {
97
+ if (node.kind === 'and') {
98
+ if (!(await evaluateIf(repository, pkg, node.left, statusCache)))
99
+ return false;
100
+ return evaluateIf(repository, pkg, node.right, statusCache);
101
+ }
102
+ if (node.kind === 'or') {
103
+ if (await evaluateIf(repository, pkg, node.left, statusCache))
104
+ return true;
105
+ return evaluateIf(repository, pkg, node.right, statusCache);
106
+ }
107
+ if (node.kind === 'not') {
108
+ return !(await evaluateIf(repository, pkg, node.node, statusCache));
109
+ }
110
+ return evaluateIfAtom(repository, pkg, node.name, node.value, statusCache);
111
+ }
112
+ RunService.evaluateIf = evaluateIf;
113
+ async function runScript(repository, script, options = {}) {
114
+ const commandName = options.commandName || 'run';
115
+ const rootCfg = getConfig(repository.rootPackage, script);
116
+ const logLevelDefault = resolveRootLogLevel(repository);
117
+ /** Standing inside a single package's own directory scopes the run to just that package
118
+ * (and drops the root bookend below) unless `--root` asks for the whole repository anyway -
119
+ * a no-op when already at the root, or outside any known package. */
120
+ const cwdScope = options.root ? undefined : repository.currentPackage;
121
+ /** Global fallback for topo - individual packages can still override their own linking below,
122
+ * but the initial sort (topological vs alphabetical) has to be decided for the whole list at once. */
123
+ const topo = resolveBool(options.topo, repository.rootPackage, script, 'topo', true);
124
+ let packages = repository.getPackages({ toposort: topo, scope: cwdScope?.name });
125
+ if (!topo)
126
+ packages = [...packages].sort((a, b) => a.name.localeCompare(b.name));
127
+ packages = filterPackages(packages, options);
128
+ const changed = resolveBool(options.changed, repository.rootPackage, script, 'changed', false);
129
+ const changedSince = options.changedSince ?? (typeof rootCfg.changedSince === 'string' ? rootCfg.changedSince : undefined);
130
+ if (changed || changedSince) {
131
+ const status = await repository.listStatus({ hash: changedSince });
132
+ packages = packages.filter(p => status[p.name] !== 'clean');
133
+ }
134
+ const concurrency = options.parallel === false
135
+ ? 1
136
+ : typeof options.parallel === 'number'
137
+ ? options.parallel
138
+ : options.parallel === true
139
+ ? os.cpus().length
140
+ : resolveNumber(undefined, repository.rootPackage, script, 'concurrency', os.cpus().length);
141
+ const progress = resolveBool(options.progress, repository.rootPackage, script, 'progress', true);
142
+ const panel = new ProgressPanel(`RUN ${script}`, !!process.stdout.isTTY && progress);
143
+ /** Set once the aggregate Task exists, so a package's own failure can trigger a manual
144
+ * abort using *its own* resolved bail setting (see `runSteps` below) - power-tasks' own
145
+ * `bail` is a single blanket policy for the whole batch, it can't vary per package. */
146
+ let rootTask;
147
+ const runSteps = async (ctx, pkgLabel, steps, cwd, pkgBail, pkgLogLevel) => {
148
+ ctx.status = 'running';
149
+ ctx.startedAt = Date.now();
150
+ try {
151
+ for (let i = 0; i < steps.length; i++) {
152
+ const step = steps[i];
153
+ ctx.currentStep = step.name;
154
+ ctx.stepIndex = i;
155
+ if (panel.enabled) {
156
+ await exec(step.command, {
157
+ cwd,
158
+ stdio: 'pipe',
159
+ onLine: (line, stdio) => {
160
+ ctx.log.push(line);
161
+ ctx.lastLine = line;
162
+ void stdio;
163
+ },
164
+ });
165
+ }
166
+ else {
167
+ /** Match the classic rman output: raw command output streams straight through
168
+ * (unbuffered, unprefixed), followed by our own one-line-per-step summary. */
169
+ printLegacyExecutingLine(commandName, pkgLabel, step, pkgLogLevel);
170
+ const stepStart = Date.now();
171
+ let stepError;
172
+ try {
173
+ await exec(step.command, { cwd, stdio: 'inherit' });
174
+ }
175
+ catch (e) {
176
+ stepError = e;
177
+ }
178
+ printLegacyStepLine(commandName, pkgLabel, step, Date.now() - stepStart, pkgLogLevel, stepError);
179
+ if (stepError)
180
+ throw stepError;
181
+ }
182
+ }
183
+ ctx.status = 'success';
184
+ }
185
+ catch (e) {
186
+ ctx.status = 'failed';
187
+ if (pkgBail)
188
+ rootTask?.abort();
189
+ throw e;
190
+ }
191
+ finally {
192
+ ctx.finishedAt = Date.now();
193
+ }
194
+ };
195
+ const children = [];
196
+ /** Shared across all `if: changed[ = hash]` evaluations so the same reference is only `git`-queried once. */
197
+ const ifStatusCache = new Map();
198
+ /** Repo-wide bookend: root's own pre/post hooks run once each, exclusively, around every package
199
+ * (unless root itself opts out via `run.<script>.skip`, fails its own `run.<script>.if`, or the
200
+ * run is scoped to a single package by `cwdScope` - a repo-wide bookend has no place there). */
201
+ const rootIf = !cwdScope && parseIfExpr(rootCfg.if);
202
+ const rootIfPasses = rootIf ? await evaluateIf(repository, repository.rootPackage, rootIf, ifStatusCache) : true;
203
+ const rootSkipped = !!cwdScope || rootCfg.skip === true || !rootIfPasses;
204
+ const rootSteps = rootSkipped ? [] : getScriptSteps(repository.rootPackage, script);
205
+ const rootPre = rootSteps.filter(s => s.name === 'pre' + script);
206
+ const rootPost = rootSteps.filter(s => s.name === 'post' + script);
207
+ const rootPreName = 'root (pre)';
208
+ const rootPostName = 'root (post)';
209
+ if (rootPre.length) {
210
+ const ctx = panel.addItem(rootPreName, rootPre.length);
211
+ const pkgBail = resolveBail(options.bail, repository.rootPackage, script, true);
212
+ const pkgLogLevel = resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault);
213
+ children.push(new Task(() => runSteps(ctx, 'root', rootPre, repository.dirname, pkgBail, pkgLogLevel), {
214
+ name: ctx.name,
215
+ exclusive: true,
216
+ }));
217
+ }
218
+ const stepsByPackage = new Map();
219
+ for (const pkg of packages) {
220
+ const pkgCfg = getConfig(pkg, script);
221
+ if (pkgCfg.skip === true)
222
+ continue;
223
+ const pkgIf = parseIfExpr(pkgCfg.if);
224
+ if (pkgIf && !(await evaluateIf(repository, pkg, pkgIf, ifStatusCache)))
225
+ continue;
226
+ const steps = getScriptSteps(pkg, script);
227
+ if (steps.length)
228
+ stepsByPackage.set(pkg.name, steps);
229
+ }
230
+ for (const pkg of packages) {
231
+ const steps = stepsByPackage.get(pkg.name);
232
+ if (!steps)
233
+ continue;
234
+ const ctx = panel.addItem(pkg.name, steps.length);
235
+ const pkgTopo = resolveBool(options.topo, pkg, script, 'topo', topo);
236
+ const pkgBail = resolveBail(options.bail, pkg, script, true);
237
+ const pkgLogLevel = resolveLogLevel(options.logLevel, pkg, script, logLevelDefault);
238
+ const dependencies = pkgTopo ? pkg.dependencies.filter(d => stepsByPackage.has(d)) : [];
239
+ if (rootPre.length)
240
+ dependencies.push(rootPreName);
241
+ children.push(new Task(() => runSteps(ctx, pkg.name, steps, pkg.dirname, pkgBail, pkgLogLevel), {
242
+ name: ctx.name,
243
+ dependencies,
244
+ }));
245
+ }
246
+ if (rootPost.length) {
247
+ const ctx = panel.addItem(rootPostName, rootPost.length);
248
+ const pkgBail = resolveBail(options.bail, repository.rootPackage, script, true);
249
+ const pkgLogLevel = resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault);
250
+ children.push(new Task(() => runSteps(ctx, 'root', rootPost, repository.dirname, pkgBail, pkgLogLevel), {
251
+ name: ctx.name,
252
+ exclusive: true,
253
+ /** Must wait for every package task to finish, not just be "exclusive" once it starts. */
254
+ dependencies: [...stepsByPackage.keys()],
255
+ }));
256
+ }
257
+ if (!children.length) {
258
+ console.log(colors.gray(`No package defines a "${script}" script.`));
259
+ return;
260
+ }
261
+ panel.start();
262
+ let failed = false;
263
+ try {
264
+ /** bail:false here - each package's own resolved bail setting decides whether to call
265
+ * `rootTask.abort()` itself (see `runSteps`), since power-tasks' own `bail` can't vary per package. */
266
+ rootTask = new Task(children, { concurrency, bail: false });
267
+ await rootTask.toPromise();
268
+ }
269
+ catch {
270
+ failed = true;
271
+ }
272
+ finally {
273
+ panel.stop();
274
+ }
275
+ panel.printSummary();
276
+ if (failed) {
277
+ const err = new Error(`"${script}" failed`);
278
+ err.logged = true;
279
+ throw err;
280
+ }
281
+ }
282
+ RunService.runScript = runScript;
283
+ })(RunService || (RunService = {}));
284
+ /**
285
+ * Classic one-line-per-step log ("info build pkg ┆ step success ┆ command (123 ms)"),
286
+ * matching rman's original npmlog-based output. Used when the live panel is off.
287
+ *
288
+ * Respects `logLevel`: 'verbose' also prints an "executing" line before the step runs
289
+ * (rman's old behavior, hidden by default); 'error' suppresses success lines; 'silent'
290
+ * suppresses everything but a real failure.
291
+ */
292
+ function printLegacyExecutingLine(commandName, pkgLabel, step, level) {
293
+ if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('verbose'))
294
+ return;
295
+ const sep = colors.gray('┆');
296
+ console.log(colors.magenta('verbose'), commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), colors.cyanBright.bold('executing'), sep, step.command);
297
+ }
298
+ function printLegacyStepLine(commandName, pkgLabel, step, durationMs, level, error) {
299
+ if (!error && LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('info'))
300
+ return;
301
+ if (error && level === 'silent')
302
+ return;
303
+ const sep = colors.gray('┆');
304
+ const levelLabel = error ? colors.red('error') : colors.green('info');
305
+ const status = error ? colors.red.bold('failed') : colors.green.bold('success');
306
+ console.log(levelLabel, commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), status, sep, step.command, colors.yellow(` (${durationMs} ms)`));
307
+ }
308
+ /** A `run.<script>.script`/`.preScript`/`.postScript` value: one command, or several to run in sequence. */
309
+ function normalizeScriptValue(value) {
310
+ if (typeof value === 'string')
311
+ return value || undefined;
312
+ if (Array.isArray(value)) {
313
+ // joined with "&&", same as a multi-command package.json script - each entry becomes its
314
+ // own step below, and an earlier failure stops the rest (npm's own semantics).
315
+ const parts = value.filter((v) => typeof v === 'string' && !!v);
316
+ return parts.length ? parts.join(' && ') : undefined;
317
+ }
318
+ return undefined;
319
+ }
320
+ /**
321
+ * Resolves a package's steps for `script`, including its own pre/post hooks (npm's own
322
+ * convention) - `.rmanrc run.<script>.script`/`.preScript`/`.postScript` (cascaded) can each supply
323
+ * a command (or an array of them, run in sequence) to use when the package's own package.json has
324
+ * none for that slot (`script`/`pre<script>`/`post<script>`), letting a package run a script it
325
+ * never defined at all. `run.<script>.override` goes further: with it, the config's command
326
+ * replaces the package's own for a slot even when the package *does* define one there.
327
+ */
328
+ function getScriptSteps(pkg, script) {
329
+ const cfg = RunService.getConfig(pkg, script);
330
+ const json = { ...pkg.json, scripts: { ...(pkg.json.scripts || {}) } };
331
+ const applyConfigScript = (key, rawValue) => {
332
+ const value = normalizeScriptValue(rawValue);
333
+ if (!value)
334
+ return;
335
+ const hasOwn = typeof json.scripts[key] === 'string' && json.scripts[key];
336
+ if (cfg.override === true || !hasOwn)
337
+ json.scripts[key] = value;
338
+ };
339
+ applyConfigScript(script, cfg.script);
340
+ applyConfigScript('pre' + script, cfg.preScript);
341
+ applyConfigScript('post' + script, cfg.postScript);
342
+ json.scripts[script] = json.scripts[script] || '#';
343
+ const info = parseNpmScript(json, 'npm run ' + script);
344
+ if (!info?.raw?.length)
345
+ return [];
346
+ const steps = [];
347
+ for (const s of info.steps) {
348
+ const parsed = Array.isArray(s.parsed) ? s.parsed : [s.parsed];
349
+ for (const command of parsed) {
350
+ if (command === '#')
351
+ continue;
352
+ steps.push({ name: s.name, command });
353
+ }
354
+ }
355
+ return steps;
356
+ }
357
+ /**
358
+ * Splits on whitespace, but a `(...)` group collapses to a single token holding its inner text
359
+ * (parens stripped, inner whitespace kept) - `evaluate` below re-tokenizes any token that still
360
+ * contains whitespace, which is what makes nested groups "just work" without a separate grammar
361
+ * for parens: `((a or b) and c)` peels off one bracket layer per recursive `tokenizeIf` call.
362
+ */
363
+ function tokenizeIf(raw) {
364
+ return tokenize(raw, { delimiters: /\s+/, brackets: true, keepBrackets: false }).all();
365
+ }
366
+ const warnedUnknownIf = new Set();
367
+ const warnedEmptyIfValue = new Set();
368
+ async function evaluateIfAtom(repository, pkg, name, value, statusCache) {
369
+ switch (name) {
370
+ case 'changed':
371
+ case 'dirty':
372
+ case 'committed': {
373
+ if (value !== undefined && !value) {
374
+ const warnKey = `${pkg.name}:${name}`;
375
+ if (!warnedEmptyIfValue.has(warnKey)) {
376
+ warnedEmptyIfValue.add(warnKey);
377
+ console.log(colors.yellow(`[${pkg.name}] "if: ${name} = ..." resolved to an empty value - treating as not matched.`));
378
+ }
379
+ return false;
380
+ }
381
+ const cacheKey = value || '';
382
+ let statusMap = statusCache.get(cacheKey);
383
+ if (!statusMap) {
384
+ statusMap = await repository.listStatus(value ? { hash: value } : undefined);
385
+ statusCache.set(cacheKey, statusMap);
386
+ }
387
+ const status = statusMap[pkg.name];
388
+ /** With a hash, status only ever resolves to 'changed'/'clean' (see Repository.listStatus) -
389
+ * the dirty/committed distinction only exists relative to upstream, so those two conditions
390
+ * naturally never match when a hash is given. */
391
+ if (name === 'changed')
392
+ return status !== 'clean';
393
+ return status === name;
394
+ }
395
+ default:
396
+ if (!warnedUnknownIf.has(name)) {
397
+ warnedUnknownIf.add(name);
398
+ console.log(colors.yellow(`Unknown "if" condition "${name}" - ignoring, package will still run.`));
399
+ }
400
+ return true;
401
+ }
402
+ }
403
+ /** Resolution order: explicit CLI flag > the package's resolved `.rmanrc` > `fallback`. */
404
+ export function resolveBool(cliValue, pkg, script, key, fallback) {
405
+ if (cliValue !== undefined)
406
+ return cliValue;
407
+ const v = RunService.getConfig(pkg, script)[key];
408
+ return typeof v === 'boolean' ? v : fallback;
409
+ }
410
+ /**
411
+ * Bail is the one setting where a package's own `.rmanrc` outranks even an explicit CLI flag:
412
+ * "this package's failure must always stop the batch" is a more specific, intentional statement
413
+ * than a broad `--no-bail` meant for the run as a whole, and shouldn't be silently overridable by it.
414
+ */
415
+ export function resolveBail(cliValue, pkg, script, fallback) {
416
+ const v = RunService.getConfig(pkg, script).bail;
417
+ if (typeof v === 'boolean')
418
+ return v;
419
+ return cliValue !== undefined ? cliValue : fallback;
420
+ }
421
+ export function resolveNumber(cliValue, pkg, script, key, fallback) {
422
+ if (cliValue !== undefined)
423
+ return cliValue;
424
+ const v = RunService.getConfig(pkg, script)[key];
425
+ return typeof v === 'number' ? v : fallback;
426
+ }
427
+ export function resolveLogLevel(cliValue, pkg, script, fallback) {
428
+ if (cliValue !== undefined)
429
+ return cliValue;
430
+ const v = RunService.getConfig(pkg, script).logLevel;
431
+ return typeof v === 'string' && LOG_LEVELS.includes(v) ? v : fallback;
432
+ }
@@ -0,0 +1,16 @@
1
+ import envinfo from 'envinfo';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare namespace SystemInfo {
4
+ interface RepositoryInfo {
5
+ type: 'monorepo' | 'package';
6
+ name: string;
7
+ version: string;
8
+ root: string;
9
+ packageCount: number;
10
+ }
11
+ /** Whatever `envinfo` reports (OS/CPU/Memory/Shell, Node/Yarn/npm, git, installed
12
+ * rman/typescript versions), grouped by category - shape is `envinfo`'s own, not ours. */
13
+ type SystemInfo = Record<string, Record<string, unknown>>;
14
+ function getSystemInfo(options?: envinfo.RunConfig): Promise<SystemInfo.SystemInfo>;
15
+ function getRepositoryInfo(repository: Repository): SystemInfo.RepositoryInfo;
16
+ }
@@ -0,0 +1,25 @@
1
+ import envinfo from 'envinfo';
2
+ export var SystemInfo;
3
+ (function (SystemInfo) {
4
+ async function getSystemInfo(options) {
5
+ return JSON.parse(await envinfo.run({
6
+ System: ['OS', 'CPU', 'Memory', 'Shell'],
7
+ Binaries: ['Node', 'Yarn', 'npm'],
8
+ Utilities: ['Git'],
9
+ npmPackages: ['rman', 'typescript'],
10
+ npmGlobalPackages: ['typescript'],
11
+ ...options,
12
+ }, { json: true }));
13
+ }
14
+ SystemInfo.getSystemInfo = getSystemInfo;
15
+ function getRepositoryInfo(repository) {
16
+ return {
17
+ type: repository.monorepo ? 'monorepo' : 'package',
18
+ name: repository.rootPackage.name,
19
+ version: repository.rootPackage.version,
20
+ root: repository.dirname,
21
+ packageCount: repository.getPackages().length,
22
+ };
23
+ }
24
+ SystemInfo.getRepositoryInfo = getRepositoryInfo;
25
+ })(SystemInfo || (SystemInfo = {}));
@@ -0,0 +1,95 @@
1
+ import type { Package } from '../core/package.js';
2
+ import type { Repository } from '../core/repository.js';
3
+ import { type PackageFilterOptions } from '../utils/package-filter.js';
4
+ export declare namespace VersionService {
5
+ type BumpKeyword = 'patch' | 'minor' | 'major';
6
+ function isBumpKeyword(value: unknown): value is BumpKeyword;
7
+ interface Options extends PackageFilterOptions {
8
+ /** A release-type keyword (applied as the severity for every group that has real changes) or
9
+ * a concrete semver version (applied as the literal new version wherever something changed) -
10
+ * either way, this replaces auto-detection entirely. Omit to auto-detect the severity per
11
+ * group from conventional-commit subjects since each package's/group's last release tag. */
12
+ bump?: string;
13
+ /** A package with uncommitted local changes is excluded from bumping (status `'skip'`)
14
+ * instead of aborting the whole plan (status `'error'`). Default false. */
15
+ ignoreDirty?: boolean;
16
+ /** Makes every computed bump a prerelease (`1.2.3` -> `1.3.0-beta.0` for a `minor`, say)
17
+ * tagged with this identifier, instead of a normal release - same idea as `npm version
18
+ * <type> --preid <name>`. A group already sitting on a matching prerelease (same identifier)
19
+ * just has its prerelease counter incremented instead of jumping to a new base version - see
20
+ * `incVersion`. Has no effect when `bump` is an explicit semver version rather than a keyword
21
+ * (there's no severity left to "pre-fix" at that point). */
22
+ preid?: string;
23
+ }
24
+ interface ApplyOptions {
25
+ /** Push the resulting commit(s) and tag(s) to the remote once applied. Default false - same
26
+ * as a plain `npm version`, which never pushes on its own either. */
27
+ push?: boolean;
28
+ /** Overrides `.rmanrc version.commitMessage` (and the built-in default) for every group's
29
+ * commit this run produces - `{version}` is still substituted the same way. */
30
+ message?: string;
31
+ /** Also writes each bumped package's `CHANGELOG.md` (via `ChangelogService.generateToFile`,
32
+ * scoped to just the packages this run actually bumped) and folds those file changes into the
33
+ * same per-group commit, instead of requiring a separate `rman changelog --write` run. */
34
+ changelog?: boolean;
35
+ }
36
+ /** One package's outcome in a version plan - see `getPlan`. */
37
+ interface Entry {
38
+ package: Package;
39
+ /** Internal group identity packages are batched by (not for display) - see `resolveGroupKey`. */
40
+ groupKey: string;
41
+ /** Human-readable group name: `'default'` for the implicit repo-wide group, the configured
42
+ * name for a named `group`, or the package's own name when it isn't grouped with anyone. */
43
+ group: string;
44
+ status: 'bump' | 'skip' | 'error' | 'no-change';
45
+ from: string;
46
+ /** Only set when `status === 'bump'`. */
47
+ to?: string;
48
+ /** Human-readable explanation - e.g. why a package was skipped, or why it's being bumped
49
+ * despite having no commits of its own (a dependency of it changed elsewhere). */
50
+ reason?: string;
51
+ }
52
+ /**
53
+ * Computes what a version bump *would* do, across every package `.rmanrc group` puts together -
54
+ * never writes anything (no package.json edits, no git commits/tags) and safe to call any time,
55
+ * including as the "preview" a bare `rman version` (no bump given) stops at.
56
+ *
57
+ * Packages are partitioned into groups by their resolved `group` value (cascaded): `true`
58
+ * (the default) puts every such package into one implicit repo-wide group; a string joins
59
+ * exactly the other packages sharing that same string, regardless of the repo's default; `false`
60
+ * makes a package its own solo group. Each group's "current version" is always the highest
61
+ * version currently found among its own members (never persisted anywhere) - see
62
+ * `resolveGroupKey`.
63
+ *
64
+ * Within a group, a member with real commits since its own last release tag (or an explicit
65
+ * `bump`) sets the group's severity to the highest found among changed members; the new version
66
+ * is that current version bumped by that severity. Which members actually receive it depends on
67
+ * the severity: **patch** only the changed member(s) (a caret dependency range already tolerates
68
+ * a patch bump, no republish needed downstream); **minor** also every transitive in-group
69
+ * dependent; **major** the entire group, changed or not - see `computeGroupPlan`.
70
+ *
71
+ * Across groups: a package depending on another group's bumped package always gets exactly a
72
+ * **patch** bump of its own (never inheriting the source's severity) - the dependency reference
73
+ * itself is the only thing that changed for it. This never re-triggers *its own* group's
74
+ * minor/major cascade (a patch never cascades), but can itself ripple into a third group, and so
75
+ * on, until nothing new is affected - see `rippleCrossGroup`.
76
+ *
77
+ * A monorepo's root package is never a real member of any group (it's never published on its
78
+ * own) - it gets one trailing informational entry instead, always `'bump'`ed to whatever single
79
+ * version every group ended up sharing, or the overall highest version when groups diverged.
80
+ */
81
+ function getPlan(repository: Repository, options?: Options): Promise<Entry[]>;
82
+ /**
83
+ * Same as `getPlan`, and additionally writes every `'bump'` entry's new version into its own
84
+ * `package.json` (and refreshes any other bumped package's dependency range on it), runs that
85
+ * package's `version.preScript`/`.script`/`.postScript` (or its own real `preversion`/`version`/
86
+ * `postversion` npm scripts) around the write, then commits and tags **once per group** - so
87
+ * independently-versioned groups each get their own clean commit/tag rather than one entangled
88
+ * commit spanning unrelated version lines. Pushes only when `options.push` is set - same as a
89
+ * plain `npm version`, this never reaches the network on its own otherwise.
90
+ */
91
+ function applyPlan(repository: Repository, plan: Entry[], options?: ApplyOptions): Promise<Entry[]>;
92
+ }
93
+ /** Every `package.json` field holding dependency ranges - shared with `PublishService`'s own
94
+ * `"workspace:"` rewrite-for-publish step, since it needs to scan the same fields. */
95
+ export declare const DEPENDENCY_KEYS: readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];