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
package/services.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  export { ChangelogService } from './services/changelog.service.js';
2
- export { CiService } from './services/ci.service.js';
3
- export { CleanService } from './services/clean.service.js';
4
2
  export { DockerPublishService } from './services/docker-publish.service.js';
5
3
  export { ExecService } from './services/exec.service.js';
6
4
  export { GithubReleaseService } from './services/github-release.service.js';
7
5
  export { ImportService } from './services/import.service.js';
8
6
  export { ListService } from './services/list.service.js';
9
- export { PublishService } from './services/publish.service.js';
10
7
  export { RunService } from './services/run.service.js';
11
8
  export { SystemInfo } from './services/system-info.js';
12
9
  export { VersionService } from './services/version.service.js';
10
+ export { VersionPlanService } from './services/version-plan.service.js';
11
+ /** Release boundaries and tag names, both directions - `ChangeHashService.detect` is what a
12
+ * plugin's planner answers `detectBoundary` with, and the only place a tag name is built. */
13
+ export { ChangeHashService } from './services/change-hash.service.js';
14
+ export { ConventionalCommitsService } from './services/conventional-commits.service.js';
package/services.js CHANGED
@@ -1,12 +1,14 @@
1
1
  export { ChangelogService } from './services/changelog.service.js';
2
- export { CiService } from './services/ci.service.js';
3
- export { CleanService } from './services/clean.service.js';
4
2
  export { DockerPublishService } from './services/docker-publish.service.js';
5
3
  export { ExecService } from './services/exec.service.js';
6
4
  export { GithubReleaseService } from './services/github-release.service.js';
7
5
  export { ImportService } from './services/import.service.js';
8
6
  export { ListService } from './services/list.service.js';
9
- export { PublishService } from './services/publish.service.js';
10
7
  export { RunService } from './services/run.service.js';
11
8
  export { SystemInfo } from './services/system-info.js';
12
9
  export { VersionService } from './services/version.service.js';
10
+ export { VersionPlanService } from './services/version-plan.service.js';
11
+ /** Release boundaries and tag names, both directions - `ChangeHashService.detect` is what a
12
+ * plugin's planner answers `detectBoundary` with, and the only place a tag name is built. */
13
+ export { ChangeHashService } from './services/change-hash.service.js';
14
+ export { ConventionalCommitsService } from './services/conventional-commits.service.js';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Where a repository's **locally installed executables** live, so a command an author wrote
3
+ * (`eslint .`, `cargo build`) is found the way it would be in their own shell.
4
+ *
5
+ * Two halves, split by who actually owns them:
6
+ *
7
+ * - **Which directories** is the *ecosystem's* answer, and the core has none - `node_modules/.bin`
8
+ * walked up the directory chain is npm's layout and nothing else's (a Python venv says
9
+ * `.venv/bin`, a Ruby project `bin`). `rman-node` contributes it; see `RmanPlugin.binPaths`.
10
+ * - **How a PATH is spelled** is the *operating system's*, and that stays here: the variable is
11
+ * `PATH` everywhere except Windows, where its case is whatever the environment happens to use.
12
+ * That has nothing to do with any ecosystem, and every provider would otherwise get it wrong
13
+ * separately.
14
+ *
15
+ * **Every provider contributes, in `plugins` declaration order** - unlike `Manifest`/`Workspace`,
16
+ * which take the first that recognizes a repository. A PATH is a list, and a repository holding two
17
+ * ecosystems wants both sets of binaries reachable; "first wins" would silently hide one.
18
+ */
19
+ export declare namespace BinPath {
20
+ type ProcessEnv = Record<string, string | undefined>;
21
+ /** Absolute directories to put **ahead of** the inherited PATH, for a command run in `cwd`.
22
+ * Return them most-specific-first; the core concatenates providers without reordering. */
23
+ type Provider = (cwd: string) => string[];
24
+ interface EnvOptions {
25
+ /** The directory the command will run in. Default `process.cwd()`. */
26
+ readonly cwd?: string;
27
+ /** The environment to derive from, like `process.env`. Default `process.env`. */
28
+ readonly env?: ProcessEnv;
29
+ }
30
+ /** Registers a provider. Called by `loadPlugins` for each plugin's `binPaths`, in `plugins`
31
+ * declaration order - so what is on PATH is a function of the repository's own config. */
32
+ function addProvider(provider: Provider): void;
33
+ /** For tests, which would otherwise leak a provider into every later case in the process. */
34
+ function clearProviders(): void;
35
+ /** Every provider's directories for `cwd`, concatenated in declaration order. Empty for a
36
+ * repository that names no plugin - the inherited PATH then stands on its own, which is the
37
+ * honest answer rather than a guess at some ecosystem's layout. */
38
+ function resolve(cwd: string): string[];
39
+ /**
40
+ * `env` with the contributed directories prepended to its PATH - what `exec` and `runBin` hand to
41
+ * a child process.
42
+ *
43
+ * Prepended, not appended: a repository's own pinned `eslint` has to win over one that happens to
44
+ * be installed globally, which is the whole point of a local install.
45
+ */
46
+ function env(options?: EnvOptions): ProcessEnv;
47
+ /**
48
+ * The name of the PATH variable in `env` - `PATH` everywhere but Windows, where the environment
49
+ * is case-insensitive and the key can genuinely arrive as `Path`.
50
+ *
51
+ * The OS's business, so it lives here rather than in any provider: read the *existing* key rather
52
+ * than writing a second one, or a child process inherits two PATHs and the one it reads is up to
53
+ * the platform.
54
+ */
55
+ function pathKey(options?: {
56
+ env?: ProcessEnv;
57
+ platform?: string;
58
+ }): string;
59
+ }
@@ -0,0 +1,82 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ /**
4
+ * Where a repository's **locally installed executables** live, so a command an author wrote
5
+ * (`eslint .`, `cargo build`) is found the way it would be in their own shell.
6
+ *
7
+ * Two halves, split by who actually owns them:
8
+ *
9
+ * - **Which directories** is the *ecosystem's* answer, and the core has none - `node_modules/.bin`
10
+ * walked up the directory chain is npm's layout and nothing else's (a Python venv says
11
+ * `.venv/bin`, a Ruby project `bin`). `rman-node` contributes it; see `RmanPlugin.binPaths`.
12
+ * - **How a PATH is spelled** is the *operating system's*, and that stays here: the variable is
13
+ * `PATH` everywhere except Windows, where its case is whatever the environment happens to use.
14
+ * That has nothing to do with any ecosystem, and every provider would otherwise get it wrong
15
+ * separately.
16
+ *
17
+ * **Every provider contributes, in `plugins` declaration order** - unlike `Manifest`/`Workspace`,
18
+ * which take the first that recognizes a repository. A PATH is a list, and a repository holding two
19
+ * ecosystems wants both sets of binaries reachable; "first wins" would silently hide one.
20
+ */
21
+ export var BinPath;
22
+ (function (BinPath) {
23
+ /** Registers a provider. Called by `loadPlugins` for each plugin's `binPaths`, in `plugins`
24
+ * declaration order - so what is on PATH is a function of the repository's own config. */
25
+ function addProvider(provider) {
26
+ if (providers.includes(provider))
27
+ return;
28
+ providers.push(provider);
29
+ }
30
+ BinPath.addProvider = addProvider;
31
+ /** For tests, which would otherwise leak a provider into every later case in the process. */
32
+ function clearProviders() {
33
+ providers.length = 0;
34
+ }
35
+ BinPath.clearProviders = clearProviders;
36
+ /** Every provider's directories for `cwd`, concatenated in declaration order. Empty for a
37
+ * repository that names no plugin - the inherited PATH then stands on its own, which is the
38
+ * honest answer rather than a guess at some ecosystem's layout. */
39
+ function resolve(cwd) {
40
+ const dir = path.resolve(cwd);
41
+ return providers.flatMap(provider => provider(dir));
42
+ }
43
+ BinPath.resolve = resolve;
44
+ /**
45
+ * `env` with the contributed directories prepended to its PATH - what `exec` and `runBin` hand to
46
+ * a child process.
47
+ *
48
+ * Prepended, not appended: a repository's own pinned `eslint` has to win over one that happens to
49
+ * be installed globally, which is the whole point of a local install.
50
+ */
51
+ function env(options = {}) {
52
+ const cwd = options.cwd || process.cwd();
53
+ const result = { ...(options.env || process.env) };
54
+ const key = pathKey({ env: result });
55
+ const entries = resolve(cwd);
56
+ if (!entries.length)
57
+ return result;
58
+ const inherited = result[key];
59
+ result[key] = [...entries, ...(inherited ? [inherited] : [])].join(path.delimiter);
60
+ return result;
61
+ }
62
+ BinPath.env = env;
63
+ /**
64
+ * The name of the PATH variable in `env` - `PATH` everywhere but Windows, where the environment
65
+ * is case-insensitive and the key can genuinely arrive as `Path`.
66
+ *
67
+ * The OS's business, so it lives here rather than in any provider: read the *existing* key rather
68
+ * than writing a second one, or a child process inherits two PATHs and the one it reads is up to
69
+ * the platform.
70
+ */
71
+ function pathKey(options) {
72
+ const source = options?.env || process.env;
73
+ const platform = options?.platform || process.platform;
74
+ if (platform !== 'win32')
75
+ return 'PATH';
76
+ return (Object.keys(source)
77
+ .reverse()
78
+ .find(key => key.toUpperCase() === 'PATH') || 'Path');
79
+ }
80
+ BinPath.pathKey = pathKey;
81
+ const providers = [];
82
+ })(BinPath || (BinPath = {}));
@@ -0,0 +1,16 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ /**
3
+ * Registers `child` to be killed if rman exits while it is still running - Ctrl-C, a signal, a throw
4
+ * on some other path.
5
+ *
6
+ * **Here rather than inside `exec`, because `runBin` needs the same thing and did not have it.** A
7
+ * child spawned by `runBin` (which is what a plugin's or a `.rman/*.mjs` command's `runBin` reaches)
8
+ * outlived an interrupted rman run: the registry and the exit hook lived in `exec.ts` and nothing
9
+ * else could see them. Anything in rman that spawns a process goes through one of those two, so
10
+ * tracking belongs where both can reach it.
11
+ *
12
+ * Untracks itself on `close`/`error`, so a long session does not accumulate dead entries. Adding
13
+ * listeners here is safe alongside the caller's own - an `error` event needs *a* listener, not
14
+ * exactly one, and both `exec` and `runBin` attach theirs anyway.
15
+ */
16
+ export declare function trackChild(child: ChildProcess): void;
@@ -0,0 +1,30 @@
1
+ import { onExit } from 'signal-exit';
2
+ /**
3
+ * Registers `child` to be killed if rman exits while it is still running - Ctrl-C, a signal, a throw
4
+ * on some other path.
5
+ *
6
+ * **Here rather than inside `exec`, because `runBin` needs the same thing and did not have it.** A
7
+ * child spawned by `runBin` (which is what a plugin's or a `.rman/*.mjs` command's `runBin` reaches)
8
+ * outlived an interrupted rman run: the registry and the exit hook lived in `exec.ts` and nothing
9
+ * else could see them. Anything in rman that spawns a process goes through one of those two, so
10
+ * tracking belongs where both can reach it.
11
+ *
12
+ * Untracks itself on `close`/`error`, so a long session does not accumulate dead entries. Adding
13
+ * listeners here is safe alongside the caller's own - an `error` event needs *a* listener, not
14
+ * exactly one, and both `exec` and `runBin` attach theirs anyway.
15
+ */
16
+ export function trackChild(child) {
17
+ const pid = child.pid;
18
+ /** No pid means the spawn failed before the OS gave it one - there is nothing to kill, and the
19
+ * caller's own `error` handler is about to report it. */
20
+ if (!pid)
21
+ return;
22
+ running.set(pid, child);
23
+ const forget = () => running.delete(pid);
24
+ child.on('close', forget);
25
+ child.on('error', forget);
26
+ }
27
+ const running = new Map();
28
+ onExit(() => {
29
+ running.forEach(child => child.kill());
30
+ });
package/utils/exec.d.ts CHANGED
@@ -3,10 +3,11 @@ export interface ExecOptions {
3
3
  * 'pipe' (default) captures output so the caller can drive a live view via onLine. */
4
4
  stdio?: 'inherit' | 'pipe';
5
5
  cwd?: string;
6
- argv?: string[];
7
6
  env?: Record<string, string | undefined>;
8
- shell?: boolean;
9
7
  onLine?: (line: string, stdio: 'stderr' | 'stdout') => void;
8
+ /** Reject on a non-zero exit (default `true`). Set false to read `code` instead - for a command
9
+ * whose failure is an expected answer rather than a problem (`docker buildx create --use` on a
10
+ * builder that already exists). */
10
11
  throwOnError?: boolean;
11
12
  }
12
13
  export interface ExecResult {
@@ -14,4 +15,14 @@ export interface ExecResult {
14
15
  error?: Error;
15
16
  stdout?: string;
16
17
  }
18
+ /**
19
+ * Runs `command` **through a shell, as one string** - the right tool for a command line a config
20
+ * author wrote, shell operators and all (`run.<script>`, `version` hooks, `rman exec`).
21
+ *
22
+ * **It takes no argv and the shell is not optional**, deliberately: an `argv`/`shell: false` pair
23
+ * would make this able to do `runBin`'s job badly, and for years nothing passed either. Arguments
24
+ * assembled in code go to `runBin`, which spawns with no shell - so an interpolated value holding a
25
+ * space stays one argument and one holding `;` stays data. Keeping the boundary in the *signature*
26
+ * is what stops a caller from having to know that rule.
27
+ */
17
28
  export declare function exec(command: string, options?: ExecOptions): Promise<ExecResult>;
package/utils/exec.js CHANGED
@@ -1,14 +1,23 @@
1
- import { ChildProcess, spawn } from 'child_process';
2
- import { onExit } from 'signal-exit';
3
- import { npmRunPathEnv } from './npm-run-path.js';
1
+ import { spawn } from 'node:child_process';
2
+ import { BinPath } from './bin-path.js';
3
+ import { trackChild } from './child-tracker.js';
4
+ /**
5
+ * Runs `command` **through a shell, as one string** - the right tool for a command line a config
6
+ * author wrote, shell operators and all (`run.<script>`, `version` hooks, `rman exec`).
7
+ *
8
+ * **It takes no argv and the shell is not optional**, deliberately: an `argv`/`shell: false` pair
9
+ * would make this able to do `runBin`'s job badly, and for years nothing passed either. Arguments
10
+ * assembled in code go to `runBin`, which spawns with no shell - so an interpolated value holding a
11
+ * space stays one argument and one holding `;` stays data. Keeping the boundary in the *signature*
12
+ * is what stops a caller from having to know that rule.
13
+ */
4
14
  export async function exec(command, options) {
5
15
  const opts = {
6
- shell: true,
7
16
  throwOnError: true,
8
17
  ...options,
9
18
  };
10
19
  opts.env = {
11
- ...npmRunPathEnv({ cwd: opts.cwd }),
20
+ ...BinPath.env({ cwd: opts.cwd }),
12
21
  ...opts.env,
13
22
  };
14
23
  opts.cwd = opts.cwd || process.cwd();
@@ -16,7 +25,7 @@ export async function exec(command, options) {
16
25
  stdio: opts.stdio === 'inherit' ? 'inherit' : 'pipe',
17
26
  env: opts.env,
18
27
  cwd: opts.cwd,
19
- shell: opts.shell,
28
+ shell: true,
20
29
  windowsHide: true,
21
30
  };
22
31
  const result = { code: undefined, stdout: '' };
@@ -43,16 +52,13 @@ export async function exec(command, options) {
43
52
  else
44
53
  stderrBuffer = buf;
45
54
  };
46
- const child = spawn(command, opts.argv || [], spawnOptions);
47
- if (child.pid)
48
- runningChildren.set(child.pid, child);
55
+ const child = spawn(command, [], spawnOptions);
56
+ trackChild(child);
49
57
  child.stdout?.on('data', data => processLines(String(data), 'stdout'));
50
58
  child.stderr?.on('data', data => processLines(String(data), 'stderr'));
51
59
  return new Promise((resolve, reject) => {
52
60
  let resolved = false;
53
61
  child.on('error', (err) => {
54
- if (child.pid)
55
- runningChildren.delete(child.pid);
56
62
  processLines('', 'stdout', true);
57
63
  processLines('', 'stderr', true);
58
64
  if (resolved)
@@ -70,8 +76,6 @@ export async function exec(command, options) {
70
76
  resolve(result);
71
77
  });
72
78
  child.on('close', (code) => {
73
- if (child.pid)
74
- runningChildren.delete(child.pid);
75
79
  processLines('', 'stdout', true);
76
80
  processLines('', 'stderr', true);
77
81
  if (resolved)
@@ -86,7 +90,3 @@ export async function exec(command, options) {
86
90
  });
87
91
  });
88
92
  }
89
- const runningChildren = new Map();
90
- onExit(() => {
91
- runningChildren.forEach(child => child.kill());
92
- });
package/utils/git.d.ts CHANGED
@@ -62,9 +62,15 @@ export declare class GitHelper {
62
62
  * `undefined` for a repository with no commits at all. The "since the beginning" boundary for
63
63
  * a package being released for the first time, with no earlier tag to measure from. */
64
64
  rootCommit(): Promise<string | undefined>;
65
- /** Stages and commits exactly `files` (relative to `cwd`, or absolute) with `message` - never a
66
- * blanket `git add -A`, so the commit only ever contains what the caller explicitly asked for. */
67
- commit(files: string[], message: string): Promise<void>;
65
+ /**
66
+ * Stages and commits exactly `files` (relative to `cwd`, or absolute) with `message` - never a
67
+ * blanket `git add -A`, so the commit only ever contains what the caller explicitly asked for.
68
+ *
69
+ * Returns the **short sha** of the commit it made. It used to return nothing, which left every
70
+ * caller unable to say what it had just done: `version` made up to one commit per group and
71
+ * reported none of them.
72
+ */
73
+ commit(files: string[], message: string): Promise<string>;
68
74
  /** Creates an annotated tag `name` pointing at HEAD, with `message` (defaults to `name`).
69
75
  * Throws if a tag with that name already exists - callers wanting idempotent tagging should
70
76
  * check `tagExists` first. */
package/utils/git.js CHANGED
@@ -207,12 +207,20 @@ export class GitHelper {
207
207
  return undefined;
208
208
  }
209
209
  }
210
- /** Stages and commits exactly `files` (relative to `cwd`, or absolute) with `message` - never a
211
- * blanket `git add -A`, so the commit only ever contains what the caller explicitly asked for. */
210
+ /**
211
+ * Stages and commits exactly `files` (relative to `cwd`, or absolute) with `message` - never a
212
+ * blanket `git add -A`, so the commit only ever contains what the caller explicitly asked for.
213
+ *
214
+ * Returns the **short sha** of the commit it made. It used to return nothing, which left every
215
+ * caller unable to say what it had just done: `version` made up to one commit per group and
216
+ * reported none of them.
217
+ */
212
218
  async commit(files, message) {
213
219
  try {
214
220
  await execFileAsync('git', ['add', '--', ...files], { cwd: this.cwd });
215
221
  await execFileAsync('git', ['commit', '-m', message], { cwd: this.cwd });
222
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], { cwd: this.cwd });
223
+ return stdout.trim();
216
224
  }
217
225
  catch (e) {
218
226
  throw new Error(`Unable to commit ${files.join(', ')}: ${e.message}`, { cause: e });
@@ -2,7 +2,9 @@ import type { Argv } from 'yargs';
2
2
  import type { Package } from '../core/package.js';
3
3
  /** Shared by every command that iterates packages (`run`/`build`/`test`/`exec`, `list`, `ci`,
4
4
  * `clean`, `version`, `publish`, `changelog`) - narrows *which* packages a command applies to,
5
- * independent of what the command actually does to them. */
5
+ * independent of what the command actually does to them. The repository's own standing filter,
6
+ * `.rmanrc "skip"`, is applied by `filterPackages` itself rather than being an option here: it is
7
+ * the repository's statement, not the caller's. */
6
8
  export interface PackageFilterOptions {
7
9
  /** Only include packages whose name matches at least one of these globs (e.g. `@scope/*`). */
8
10
  scope?: string | string[];
@@ -28,5 +30,34 @@ export declare function readPackageFilterOptions(args: any): PackageFilterOption
28
30
  * `Repository`'s own `_updateDependencies`) and their results are unioned in - so `--deps
29
31
  * --dependents` together never re-expands one direction's additions through the other, which
30
32
  * would otherwise tend to blow up toward "the whole repository" for any reasonably-connected graph.
33
+ *
34
+ * A package excluded by its own `.rmanrc "skip"` is dropped **first**, before any of that - so
35
+ * `--deps` never drags a skipped package back in through a dependency edge.
31
36
  */
32
- export declare function filterPackages(packages: Package[], options: PackageFilterOptions): Package[];
37
+ export declare function filterPackages(packages: Package[], options: PackageFilterOptions,
38
+ /**
39
+ * Whether each package's own `.rmanrc "skip"` applies. Default `true`: every command that *acts*
40
+ * on packages honours it, which is the point of one standing "leave this package alone" instead
41
+ * of a separate `skip` invented per command.
42
+ *
43
+ * `list` passes `false` - an inventory that hides part of the repository is answering a different
44
+ * question than the one asked. That is the only caller that does, and it is the test for whether a
45
+ * new command should: does it *do* something to the packages, or does it *report* on them?
46
+ */
47
+ applySkip?: boolean): Package[];
48
+ /**
49
+ * `--root`/`-r`, with one describe text instead of four near-identical ones.
50
+ *
51
+ * **It only means anything where a command scopes by the current directory** - `run`/`build`/`test`,
52
+ * `exec`, `clean`, `changelog` and `diff` narrow to `Repository.currentPackage` when you stand
53
+ * inside a package, and this is the escape hatch. On a command that already works across the whole
54
+ * repository (`version`, `publish`, `list`, `changed`) it would be a flag that does nothing, which
55
+ * is worse than not offering it: a no-op flag reads as a promise.
56
+ *
57
+ * `verb` is the command's own word for what it does, so the text stays the sentence each command was
58
+ * already saying.
59
+ */
60
+ export declare function applyRootOption<T>(cmd: Argv<T>, verb: string): Argv<T>;
61
+ /** The `--root` flag as the services read it - beside `readPackageFilterOptions`, so a command
62
+ * reads both the same way. */
63
+ export declare function readRootOption(args: any): boolean | undefined;
@@ -40,9 +40,22 @@ export function readPackageFilterOptions(args) {
40
40
  * `Repository`'s own `_updateDependencies`) and their results are unioned in - so `--deps
41
41
  * --dependents` together never re-expands one direction's additions through the other, which
42
42
  * would otherwise tend to blow up toward "the whole repository" for any reasonably-connected graph.
43
+ *
44
+ * A package excluded by its own `.rmanrc "skip"` is dropped **first**, before any of that - so
45
+ * `--deps` never drags a skipped package back in through a dependency edge.
43
46
  */
44
- export function filterPackages(packages, options) {
45
- let matched = packages;
47
+ export function filterPackages(packages, options,
48
+ /**
49
+ * Whether each package's own `.rmanrc "skip"` applies. Default `true`: every command that *acts*
50
+ * on packages honours it, which is the point of one standing "leave this package alone" instead
51
+ * of a separate `skip` invented per command.
52
+ *
53
+ * `list` passes `false` - an inventory that hides part of the repository is answering a different
54
+ * question than the one asked. That is the only caller that does, and it is the test for whether a
55
+ * new command should: does it *do* something to the packages, or does it *report* on them?
56
+ */
57
+ applySkip = true) {
58
+ let matched = applySkip ? packages.filter(p => p.config?.skip !== true) : packages;
46
59
  if (options.scope) {
47
60
  const patterns = toArray(options.scope);
48
61
  matched = matched.filter(p => micromatch.isMatch(p.name, patterns));
@@ -53,21 +66,48 @@ export function filterPackages(packages, options) {
53
66
  }
54
67
  if (!options.deps && !options.dependents)
55
68
  return matched;
56
- const included = new Set(matched.map(p => p.name));
69
+ /** A set of packages, not of names: `Package.dependencies` holds references now, so identity is
70
+ * what these comparisons are about. */
71
+ const included = new Set(matched);
57
72
  if (options.deps) {
58
73
  for (const p of matched)
59
74
  for (const dep of p.dependencies)
60
75
  included.add(dep);
61
76
  }
62
77
  if (options.dependents) {
63
- const matchedNames = new Set(matched.map(p => p.name));
78
+ const matchedSet = new Set(matched);
64
79
  for (const p of packages) {
65
- if (p.dependencies.some(d => matchedNames.has(d)))
66
- included.add(p.name);
80
+ if (p.dependencies.some(d => matchedSet.has(d)))
81
+ included.add(p);
67
82
  }
68
83
  }
69
- return packages.filter(p => included.has(p.name));
84
+ return packages.filter(p => included.has(p));
70
85
  }
71
86
  function toArray(value) {
72
87
  return Array.isArray(value) ? value : [value];
73
88
  }
89
+ /**
90
+ * `--root`/`-r`, with one describe text instead of four near-identical ones.
91
+ *
92
+ * **It only means anything where a command scopes by the current directory** - `run`/`build`/`test`,
93
+ * `exec`, `clean`, `changelog` and `diff` narrow to `Repository.currentPackage` when you stand
94
+ * inside a package, and this is the escape hatch. On a command that already works across the whole
95
+ * repository (`version`, `publish`, `list`, `changed`) it would be a flag that does nothing, which
96
+ * is worse than not offering it: a no-op flag reads as a promise.
97
+ *
98
+ * `verb` is the command's own word for what it does, so the text stays the sentence each command was
99
+ * already saying.
100
+ */
101
+ export function applyRootOption(cmd, verb) {
102
+ return cmd.option('root', {
103
+ alias: 'r',
104
+ describe: `${verb} across the whole repository even when the current directory is inside a single ` +
105
+ 'package (which otherwise scopes it to just that package). No effect elsewhere.',
106
+ type: 'boolean',
107
+ });
108
+ }
109
+ /** The `--root` flag as the services read it - beside `readPackageFilterOptions`, so a command
110
+ * reads both the same way. */
111
+ export function readRootOption(args) {
112
+ return args.root;
113
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A copy of a resolved config with every value a serializer cannot represent replaced by a short
3
+ * description of it - for `rman config` and `--config`, which exist to be *looked at*.
4
+ *
5
+ * Needed because a config legitimately holds functions now: a `run.<script>` or `version.<slot>`
6
+ * step written as JavaScript, and an `if` written the same way. It was already needed before that,
7
+ * though, which is the better argument for doing it here rather than at one call site - a
8
+ * `plugins` entry given in its object form carries the plugin's seams, and `rman config --root`
9
+ * died on one with `unacceptable kind of an object to dump [object Function]` (measured, on a
10
+ * repository whose shared config did nothing more unusual than `extends` a plugin package).
11
+ *
12
+ * A function prints as `[Function: copyDocs]`, so the output says *which* one - an anonymous step
13
+ * reads as `[Function]`, which is itself worth seeing.
14
+ */
15
+ export declare function printableConfig<T>(config: T): T;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A copy of a resolved config with every value a serializer cannot represent replaced by a short
3
+ * description of it - for `rman config` and `--config`, which exist to be *looked at*.
4
+ *
5
+ * Needed because a config legitimately holds functions now: a `run.<script>` or `version.<slot>`
6
+ * step written as JavaScript, and an `if` written the same way. It was already needed before that,
7
+ * though, which is the better argument for doing it here rather than at one call site - a
8
+ * `plugins` entry given in its object form carries the plugin's seams, and `rman config --root`
9
+ * died on one with `unacceptable kind of an object to dump [object Function]` (measured, on a
10
+ * repository whose shared config did nothing more unusual than `extends` a plugin package).
11
+ *
12
+ * A function prints as `[Function: copyDocs]`, so the output says *which* one - an anonymous step
13
+ * reads as `[Function]`, which is itself worth seeing.
14
+ */
15
+ export function printableConfig(config) {
16
+ return walk(config, new WeakSet());
17
+ }
18
+ function walk(value, seen) {
19
+ if (typeof value === 'function')
20
+ return `[Function${value.name ? `: ${value.name}` : ''}]`;
21
+ if (!value || typeof value !== 'object')
22
+ return value;
23
+ /** A resolved config is a tree, but a plugin object is arbitrary code's data and may not be.
24
+ * js-yaml's `noRefs` turns a repeat into a copy rather than an anchor, which on a true cycle
25
+ * never terminates - so the cycle is cut here, where it can be named. */
26
+ if (seen.has(value))
27
+ return '[Circular]';
28
+ seen.add(value);
29
+ try {
30
+ if (Array.isArray(value))
31
+ return value.map(item => walk(item, seen));
32
+ const result = {};
33
+ for (const [key, item] of Object.entries(value))
34
+ result[key] = walk(item, seen);
35
+ return result;
36
+ }
37
+ finally {
38
+ /** Released on the way out, so a value that merely appears twice in *different* branches - the
39
+ * ordinary case after merging - is printed both times rather than reported as a cycle. */
40
+ seen.delete(value);
41
+ }
42
+ }
@@ -1,4 +1,4 @@
1
- import { applyTagPattern, extractVersion } from './change-hash.js';
1
+ import { ChangeHashService } from '../services/change-hash.service.js';
2
2
  /**
3
3
  * A calendar release version: `YYYY.M.D-HHmm`, every part **unpadded** (`2026.9.5-930`, not
4
4
  * `2026.09.05-0930`). The padding isn't a style choice - semver forbids leading zeroes in numeric
@@ -52,7 +52,7 @@ export function usesCalendarVersion(options) {
52
52
  /** The tag naming a given repository release - `releaseTagPattern` run forward, the way
53
53
  * `expandTag` runs `changelog.tagPattern` forward for a package. */
54
54
  export function expandReleaseTag(root, version) {
55
- return applyTagPattern(releaseTagPattern(root), root.name, version);
55
+ return ChangeHashService.applyTagPattern(releaseTagPattern(root), root.name, version);
56
56
  }
57
57
  /** The version of the repository's most recent release, from its release tags (highest by version
58
58
  * sort) - `undefined` for a repo that has never cut one, or whose tags aren't available (a shallow
@@ -61,6 +61,6 @@ export function expandReleaseTag(root, version) {
61
61
  export async function findLastReleaseVersion(git, root) {
62
62
  const glob = releaseTagPattern(root).replace('{name}', root.name);
63
63
  const tag = (await git.listTags(glob))[0];
64
- return tag ? extractVersion(tag, glob) : undefined;
64
+ return tag ? ChangeHashService.extractVersion(tag, glob) : undefined;
65
65
  }
66
66
  const DEFAULT_RELEASE_TAG_PATTERN = 'release-*';
@@ -0,0 +1,46 @@
1
+ import { type LogLevel } from './logger.js';
2
+ export interface RunBinOptions {
3
+ /** Where to run it, and the directory `node_modules/.bin` is resolved from. Default `process.cwd()`. */
4
+ cwd?: string;
5
+ /** 'inherit' streams the child's output straight to the terminal; 'pipe' captures it and resolves
6
+ * with it instead - for a binary being asked a question rather than doing work. Defaults to
7
+ * whatever `logLevel` implies (see below). */
8
+ stdio?: 'inherit' | 'pipe';
9
+ /**
10
+ * Verbosity, defaulting to 'info'. This is what a caller holding the session's resolved level
11
+ * passes in - `CommandContext.runBin` does exactly that, so a repository's own command honors
12
+ * `--log-level` and `.rmanrc logLevel` without reading either itself.
13
+ *
14
+ * - 'verbose' also prints the command before running it.
15
+ * - 'info' streams the child's output through (`stdio: 'inherit'`).
16
+ * - 'error'/'silent' capture it instead, and surface it only if the command fails - 'silent' not
17
+ * even then. An explicit `stdio` still wins over all of this.
18
+ */
19
+ logLevel?: LogLevel;
20
+ env?: Record<string, string | undefined>;
21
+ }
22
+ export interface RunBinResult {
23
+ code: number;
24
+ /** Captured stdout+stderr, only with `stdio: 'pipe'`. */
25
+ output: string;
26
+ }
27
+ /**
28
+ * Runs one of the repository's locally installed binaries, passing **argv as an array** - no shell.
29
+ *
30
+ * Use this over `exec` whenever the arguments aren't a fixed string: `exec` runs through a shell, so
31
+ * an interpolated path containing a space silently becomes two arguments, and a value containing
32
+ * `;` or `&&` becomes another command. Here they cannot - each element of `argv` arrives as exactly
33
+ * one argument. `exec` remains the right tool for a command the config author wrote as a string,
34
+ * shell operators and all (`run.<script>`, `version` hooks).
35
+ *
36
+ * The binary is found by PATH, not spawned by path: `BinPath.env` prepends whatever the repository's
37
+ * plugins say a local install lives in (`node_modules/.bin`
38
+ * from `cwd` up the directory tree, which is what makes `eslint` resolve to the repository's own
39
+ * copy - and on Windows resolve `eslint.cmd`, which spawning a bare path would not.
40
+ *
41
+ * Rejects on a non-zero exit (the error names the command and the code) and on a missing binary,
42
+ * where it asks the useful question instead of reporting `ENOENT`. A non-zero exit has to be an
43
+ * error rather than a returned code: a lint or type-check step that passes in CI having checked
44
+ * nothing is the outcome worth ruling out, and that is what silently discarding a code produces.
45
+ */
46
+ export declare function runBin(bin: string, argv: string[], options?: RunBinOptions): Promise<RunBinResult>;