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
@@ -2,20 +2,15 @@ import type { Package } from '../core/package.js';
2
2
  import type { Repository } from '../core/repository.js';
3
3
  import { type PackageFilterOptions } from '../utils/package-filter.js';
4
4
  export declare namespace ChangelogService {
5
- /** Injectable dependencies shared by `getEntries`/`generate` - currently just the npm registry lookup
6
- * `detectChangeHash` otherwise makes for itself; overridable so tests (and advanced callers)
7
- * aren't forced through a real network call. */
8
- interface Deps {
9
- npmViewVersion?: (name: string, cwd: string) => Promise<string | undefined>;
10
- }
11
5
  interface Options extends PackageFilterOptions {
12
6
  /** Generate the changelog since this commit/hash - applied the same way to every package.
13
- * Default (also `"npm"` explicitly): auto-detect it per package instead, from that package's
7
+ * Default (also `"auto"` explicitly): auto-detect it per package instead, from that package's
14
8
  * own most recent release tag first - the same lookup `VersionService`/`changed` use, so this
15
- * never disagrees with them - falling back to its currently-published npm version only when
16
- * it has no tag yet (see `detectChangeHash`); a package this can't be resolved for either way
17
- * (never tagged, unpublished, no network) has never been released at all, so its whole
18
- * history counts as unreleased - the same view `version` takes. */
9
+ * never disagrees with them - falling back to whatever its own ecosystem's registry reports
10
+ * only when it has no tag yet (`ManifestProvider.publishedVersion`, and only ever to guess a
11
+ * tag name - see `detectChangeHash`); a package this can't be resolved for either way (never
12
+ * tagged, unpublished, no plugin) has never been released at all, so its whole history counts
13
+ * as unreleased - the same view `version` takes. */
19
14
  from?: string;
20
15
  /** Generate for the whole repository even when the current directory is inside a single
21
16
  * package (which otherwise scopes it to just that package) - see `Repository.currentPackage`. */
@@ -67,7 +62,7 @@ export declare namespace ChangelogService {
67
62
  * sake side effect (a "save this" request), not presentation, so it stays here rather than in
68
63
  * the CLI command - printing what happened is the command's job.
69
64
  */
70
- function generateToFile(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
65
+ function generateToFile(repository: Repository, options?: Options): Promise<Entry[]>;
71
66
  /**
72
67
  * Computes a changelog entry per package (root included) from real commits only - either
73
68
  * everything since a given `--from <hash>` (applied the same way to every package), or, by
@@ -107,5 +102,5 @@ export declare namespace ChangelogService {
107
102
  * Run from inside a single package's own directory, it only covers that package unless
108
103
  * `options.root` says otherwise (see `Repository.currentPackage`).
109
104
  */
110
- function getEntries(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
105
+ function getEntries(repository: Repository, options?: Options): Promise<Entry[]>;
111
106
  }
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { detectChangeHash, extractVersion, findLatestTag, tagPattern } from '../utils/change-hash.js';
4
- import { isReleaseCommit, parseConventionalCommit } from '../utils/conventional-commits.js';
5
3
  import { GitHelper } from '../utils/git.js';
6
4
  import { filterPackages } from '../utils/package-filter.js';
5
+ import { ChangeHashService } from './change-hash.service.js';
6
+ import { ConventionalCommitsService } from './conventional-commits.service.js';
7
7
  export var ChangelogService;
8
8
  (function (ChangelogService) {
9
9
  /**
@@ -13,8 +13,8 @@ export var ChangelogService;
13
13
  * sake side effect (a "save this" request), not presentation, so it stays here rather than in
14
14
  * the CLI command - printing what happened is the command's job.
15
15
  */
16
- async function generateToFile(repository, options = {}, deps = {}) {
17
- const entries = await getEntries(repository, options, deps);
16
+ async function generateToFile(repository, options = {}) {
17
+ const entries = await getEntries(repository, options);
18
18
  for (const entry of entries)
19
19
  prependToChangelogFile(entry.package, entry.filePath, entry.content);
20
20
  return entries;
@@ -59,7 +59,7 @@ export var ChangelogService;
59
59
  * Run from inside a single package's own directory, it only covers that package unless
60
60
  * `options.root` says otherwise (see `Repository.currentPackage`).
61
61
  */
62
- async function getEntries(repository, options = {}, deps = {}) {
62
+ async function getEntries(repository, options = {}) {
63
63
  const cwdScope = options.root ? undefined : repository.currentPackage;
64
64
  const packages = repository.getPackages().filter(p => p !== repository.rootPackage);
65
65
  const targets = (cwdScope ? [cwdScope] : filterPackages([repository.rootPackage, ...packages], options)).filter(pkg => options.includeSkipped || !pkg.config.publish?.skip);
@@ -67,7 +67,7 @@ export var ChangelogService;
67
67
  const commitMessage = repository.rootPackage.config?.version?.commitMessage;
68
68
  // dropped up front, not just while grouping - a package whose only commits are release markers
69
69
  // should get no entry at all, rather than a heading with nothing real underneath it.
70
- const dropVersionBumps = (commits) => commits.filter(c => !isReleaseCommit(c.subject, commitMessage));
70
+ const dropVersionBumps = (commits) => commits.filter(c => !ConventionalCommitsService.isReleaseCommit(c.subject, commitMessage));
71
71
  // Several packages often resolve to the identical hash (an explicit --from <hash> applies to
72
72
  // all of them the same way; under fixed versioning, npm auto-detection usually does too) - so
73
73
  // the git fetch for a given hash is cached, run once no matter how many packages share it.
@@ -86,9 +86,8 @@ export var ChangelogService;
86
86
  };
87
87
  const commitsByTarget = await Promise.all(targets.map(async (pkg) => {
88
88
  const catchUpFile = path.join(pkg.dirname, resolveFilePath(pkg, options.filePath));
89
- const from = await detectChangeHash(git, pkg, {
89
+ const from = await ChangeHashService.detect(git, pkg, {
90
90
  from: options.from,
91
- npmViewVersion: deps.npmViewVersion,
92
91
  catchUpFile: fs.existsSync(catchUpFile) ? catchUpFile : undefined,
93
92
  });
94
93
  return listCommitsCached(from);
@@ -132,7 +131,7 @@ function ignoreTypesConfig(pkg) {
132
131
  function groupCommits(subjects, ignoreTypes = new Set()) {
133
132
  const grouped = { features: [], fixes: [], other: [] };
134
133
  for (const subject of subjects) {
135
- const parsed = parseConventionalCommit(subject);
134
+ const parsed = ConventionalCommitsService.parseSubject(subject);
136
135
  if (!parsed) {
137
136
  grouped.other.push(subject);
138
137
  continue;
@@ -198,8 +197,10 @@ function resolveFilePath(pkg, optionsFilePath) {
198
197
  * if no matching tag exists at all (never tagged, or a fresh package) - see `findLatestTag`.
199
198
  */
200
199
  async function resolveVersion(git, pkg) {
201
- const tag = await findLatestTag(git, pkg);
202
- return tag ? extractVersion(tag, tagPattern(pkg).replace('{name}', pkg.name)) : pkg.version || '';
200
+ const tag = await ChangeHashService.findLatestTag(git, pkg);
201
+ return tag
202
+ ? ChangeHashService.extractVersion(tag, ChangeHashService.tagPattern(pkg).replace('{name}', pkg.name))
203
+ : pkg.version || '';
203
204
  }
204
205
  /** The most specific package whose directory contains `file` - the repository root itself as the
205
206
  * fallback for anything outside every package (e.g. root-level config files). Mirrors
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Conventional Commits, as rman reads them: the subject-line shape, the breaking-change markers,
3
+ * the `Release-As:` escape hatch, and the counter-question of whether a commit is a *release
4
+ * marker* rather than a change at all.
5
+ *
6
+ * A namespace for the reason `ChangeHashService` is one - one subject, several operations, named for
7
+ * what they do rather than repeating the subject (`parseSubject`, not `parseConventionalCommit`).
8
+ *
9
+ * **Nothing here knows about versions.** A commit says what happened; `VersionScheme.bumpFor` turns
10
+ * that into how a number moves. `parseReleaseAs` is the one place the two meet, and it deliberately
11
+ * returns the word as written rather than judging it - see its own doc.
12
+ */
13
+ export declare namespace ConventionalCommitsService {
14
+ /**
15
+ * `type(scope): description`, optionally with a `!` breaking-change marker - Conventional
16
+ * Commits' subject-line shape. Anything that doesn't match falls into "Other Changes" as-is (for
17
+ * changelog entries) or defaults to a patch-level change (for version bump severity) - see
18
+ * `parseSubject`.
19
+ */
20
+ const SUBJECT_PATTERN: RegExp;
21
+ /** A bare version-bump commit (`"6.0.1"`, `"v2.3.0-beta.1"`, ...) - many release tools commit the
22
+ * version bump itself with just the new version number as the message. That's a release marker,
23
+ * not a real change worth describing (or worth bumping a version over on its own), so it's
24
+ * dropped everywhere a real change is being looked for. */
25
+ const VERSION_BUMP_PATTERN: RegExp;
26
+ /**
27
+ * Whether `subject` is a release marker rather than a real change - dropped everywhere real
28
+ * changes are looked for (changelog entries, and what counts as "changed" for a version bump).
29
+ *
30
+ * Covers the bare-version form other tools use (`VERSION_BUMP_PATTERN`) plus every message shape
31
+ * `version` itself writes: its commit message (`commitMessageTemplate`, or the built-in
32
+ * `"chore(release): v{version}"` when a repo doesn't override it), the multi-version form that
33
+ * template falls back to when one commit spans several versions (`chore(release): a@1.2.0,
34
+ * b@1.3.0`), and the monorepo root's own version-sync commit. Without this, rman's own release
35
+ * commits show up in the changelogs it generates - visible whenever the boundary reaches back past
36
+ * a previous release (see `ChangeHashService.detect`'s `catchUpFile`).
37
+ */
38
+ function isReleaseCommit(subject: string, commitMessageTemplate?: string): boolean;
39
+ interface ParsedSubject {
40
+ type: string;
41
+ scope?: string;
42
+ /** A `!` right before the `:` (e.g. `feat!:`) - Conventional Commits' inline breaking-change
43
+ * marker. Doesn't cover a `BREAKING CHANGE:` footer, since only the subject line is available. */
44
+ breaking: boolean;
45
+ description: string;
46
+ }
47
+ /** Parses a commit subject as Conventional Commits, or `undefined` if it doesn't match at all
48
+ * (a non-conventional message - still a real change, just with no `type` to key off of). */
49
+ function parseSubject(subject: string): ParsedSubject | undefined;
50
+ /** Whether a commit `body` carries a Conventional Commits `BREAKING CHANGE:` (or
51
+ * `BREAKING-CHANGE:`) footer - the other, footer-based way to mark a breaking change, alongside
52
+ * the inline `!` the subject line alone can carry (see `parseSubject`, whose own
53
+ * `breaking` only ever reflects that marker, never a footer, since it only sees the subject). */
54
+ function hasBreakingChangeFooter(body: string): boolean;
55
+ /**
56
+ * A `Release-As: <bump>` footer in a commit `body` - lets that one commit's own contribution to the
57
+ * detected bump be overridden by hand, regardless of what its subject line (or a `BREAKING CHANGE:`
58
+ * footer) would otherwise imply. The motivating case: a `feat:` commit that needs to ship right now
59
+ * as a patch, without waiting for the rest of a minor's worth of work to land - `Release-As: patch`
60
+ * on just that commit ships it alone, at patch size, while a later genuine `feat:` (with no
61
+ * override) still correctly triggers a minor of its own. Case-insensitive; the last match wins if a
62
+ * body somehow has more than one, matching how multiple git trailers of the same key are
63
+ * conventionally read (later overrides earlier).
64
+ *
65
+ * **Returns the word as written, and does not judge it** - which bumps exist is the package's
66
+ * `VersionScheme.bumpNames`, and this module is about commit messages. The caller checks it against
67
+ * those and treats anything else as *no override at all*, falling back to what the commit's own
68
+ * subject said (see `VersionPlanService.detectBump`). That keeps two cases harmless: a typo, and release-please's own
69
+ * `Release-As: 1.2.3`, which uses the same trailer for a literal version and appears in real
70
+ * repositories rman gets adopted onto.
71
+ */
72
+ function parseReleaseAs(body: string): string | undefined;
73
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Conventional Commits, as rman reads them: the subject-line shape, the breaking-change markers,
3
+ * the `Release-As:` escape hatch, and the counter-question of whether a commit is a *release
4
+ * marker* rather than a change at all.
5
+ *
6
+ * A namespace for the reason `ChangeHashService` is one - one subject, several operations, named for
7
+ * what they do rather than repeating the subject (`parseSubject`, not `parseConventionalCommit`).
8
+ *
9
+ * **Nothing here knows about versions.** A commit says what happened; `VersionScheme.bumpFor` turns
10
+ * that into how a number moves. `parseReleaseAs` is the one place the two meet, and it deliberately
11
+ * returns the word as written rather than judging it - see its own doc.
12
+ */
13
+ export var ConventionalCommitsService;
14
+ (function (ConventionalCommitsService) {
15
+ /**
16
+ * `type(scope): description`, optionally with a `!` breaking-change marker - Conventional
17
+ * Commits' subject-line shape. Anything that doesn't match falls into "Other Changes" as-is (for
18
+ * changelog entries) or defaults to a patch-level change (for version bump severity) - see
19
+ * `parseSubject`.
20
+ */
21
+ ConventionalCommitsService.SUBJECT_PATTERN = /^(\w+)(\(([^)]+)\))?(!)?:\s*(.+)$/;
22
+ /** A bare version-bump commit (`"6.0.1"`, `"v2.3.0-beta.1"`, ...) - many release tools commit the
23
+ * version bump itself with just the new version number as the message. That's a release marker,
24
+ * not a real change worth describing (or worth bumping a version over on its own), so it's
25
+ * dropped everywhere a real change is being looked for. */
26
+ ConventionalCommitsService.VERSION_BUMP_PATTERN = /^v?\d+\.\d+\.\d+(?:[-+][\w.]+)?$/;
27
+ /**
28
+ * Whether `subject` is a release marker rather than a real change - dropped everywhere real
29
+ * changes are looked for (changelog entries, and what counts as "changed" for a version bump).
30
+ *
31
+ * Covers the bare-version form other tools use (`VERSION_BUMP_PATTERN`) plus every message shape
32
+ * `version` itself writes: its commit message (`commitMessageTemplate`, or the built-in
33
+ * `"chore(release): v{version}"` when a repo doesn't override it), the multi-version form that
34
+ * template falls back to when one commit spans several versions (`chore(release): a@1.2.0,
35
+ * b@1.3.0`), and the monorepo root's own version-sync commit. Without this, rman's own release
36
+ * commits show up in the changelogs it generates - visible whenever the boundary reaches back past
37
+ * a previous release (see `ChangeHashService.detect`'s `catchUpFile`).
38
+ */
39
+ function isReleaseCommit(subject, commitMessageTemplate) {
40
+ if (ConventionalCommitsService.VERSION_BUMP_PATTERN.test(subject))
41
+ return true;
42
+ if (ROOT_SYNC_PATTERN.test(subject))
43
+ return true;
44
+ if (MULTI_VERSION_RELEASE_PATTERN.test(subject))
45
+ return true;
46
+ // The built-in message is checked even when a repo overrides it: the override only applies to
47
+ // commits spanning a single version (see `buildCommitMessage`), and a repo that adopted one later
48
+ // still has older releases committed under the default.
49
+ if (templatePattern(DEFAULT_COMMIT_MESSAGE).test(subject))
50
+ return true;
51
+ return !!commitMessageTemplate && templatePattern(commitMessageTemplate).test(subject);
52
+ }
53
+ ConventionalCommitsService.isReleaseCommit = isReleaseCommit;
54
+ /** Parses a commit subject as Conventional Commits, or `undefined` if it doesn't match at all
55
+ * (a non-conventional message - still a real change, just with no `type` to key off of). */
56
+ function parseSubject(subject) {
57
+ const m = ConventionalCommitsService.SUBJECT_PATTERN.exec(subject);
58
+ if (!m)
59
+ return undefined;
60
+ const [, type, , scope, breakingMark, description] = m;
61
+ return { type: type.toLowerCase(), scope, breaking: !!breakingMark, description };
62
+ }
63
+ ConventionalCommitsService.parseSubject = parseSubject;
64
+ /** Whether a commit `body` carries a Conventional Commits `BREAKING CHANGE:` (or
65
+ * `BREAKING-CHANGE:`) footer - the other, footer-based way to mark a breaking change, alongside
66
+ * the inline `!` the subject line alone can carry (see `parseSubject`, whose own
67
+ * `breaking` only ever reflects that marker, never a footer, since it only sees the subject). */
68
+ function hasBreakingChangeFooter(body) {
69
+ return /^BREAKING[ -]CHANGE:/im.test(body);
70
+ }
71
+ ConventionalCommitsService.hasBreakingChangeFooter = hasBreakingChangeFooter;
72
+ /**
73
+ * A `Release-As: <bump>` footer in a commit `body` - lets that one commit's own contribution to the
74
+ * detected bump be overridden by hand, regardless of what its subject line (or a `BREAKING CHANGE:`
75
+ * footer) would otherwise imply. The motivating case: a `feat:` commit that needs to ship right now
76
+ * as a patch, without waiting for the rest of a minor's worth of work to land - `Release-As: patch`
77
+ * on just that commit ships it alone, at patch size, while a later genuine `feat:` (with no
78
+ * override) still correctly triggers a minor of its own. Case-insensitive; the last match wins if a
79
+ * body somehow has more than one, matching how multiple git trailers of the same key are
80
+ * conventionally read (later overrides earlier).
81
+ *
82
+ * **Returns the word as written, and does not judge it** - which bumps exist is the package's
83
+ * `VersionScheme.bumpNames`, and this module is about commit messages. The caller checks it against
84
+ * those and treats anything else as *no override at all*, falling back to what the commit's own
85
+ * subject said (see `VersionPlanService.detectBump`). That keeps two cases harmless: a typo, and release-please's own
86
+ * `Release-As: 1.2.3`, which uses the same trailer for a literal version and appears in real
87
+ * repositories rman gets adopted onto.
88
+ */
89
+ function parseReleaseAs(body) {
90
+ const matches = [...body.matchAll(/^release-as:\s*([\w.+-]+)\s*$/gim)];
91
+ const last = matches.at(-1);
92
+ return last ? last[1].toLowerCase() : undefined;
93
+ }
94
+ ConventionalCommitsService.parseReleaseAs = parseReleaseAs;
95
+ })(ConventionalCommitsService || (ConventionalCommitsService = {}));
96
+ /** A semver version, as it appears inside a commit subject - the `\d+\.\d+\.\d+` core of
97
+ * `VERSION_BUMP_PATTERN`, reusable inside the larger patterns below. */
98
+ const SEMVER_SOURCE = String.raw `\d+\.\d+\.\d+(?:[-+][\w.]+)?`;
99
+ /** Mirrors `VersionService`'s own default `version.commitMessage` - kept in sync by
100
+ * `version.command.ts`'s documented default, not imported, to keep this module dependency-free. */
101
+ const DEFAULT_COMMIT_MESSAGE = 'chore(release): v{version}';
102
+ /** `VersionService.applyPlan`'s trailing commit for a monorepo root's informational version. */
103
+ const ROOT_SYNC_PATTERN = new RegExp(String.raw `^chore: sync root version to ${SEMVER_SOURCE}$`);
104
+ /** What the commit-message template falls back to when one commit covers several versions at once
105
+ * (a cross-group ripple) - `{version}` has nothing single to substitute, so each bumped package is
106
+ * listed by name instead. */
107
+ const MULTI_VERSION_RELEASE_PATTERN = new RegExp(String.raw `^chore\(release\): \S+@${SEMVER_SOURCE}(?:, \S+@${SEMVER_SOURCE})*$`);
108
+ function escapeRegExp(value) {
109
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
110
+ }
111
+ /** Turns a `version.commitMessage` template into a matcher for the commits it produces: every
112
+ * literal part escaped, each `{version}` placeholder standing in for any semver. */
113
+ function templatePattern(template) {
114
+ const source = template.split('{version}').map(escapeRegExp).join(SEMVER_SOURCE);
115
+ return new RegExp(`^${source}$`);
116
+ }
@@ -185,7 +185,7 @@ async function updateDescription(entry) {
185
185
  const res = await fetch(`https://hub.docker.com/v2/repositories/${namespace}/${imageName}/`, {
186
186
  method: 'PATCH',
187
187
  headers: { Authorization: `JWT ${token}`, 'Content-Type': 'application/json' },
188
- body: JSON.stringify({ description: pkg.json.description, full_description: readme }),
188
+ body: JSON.stringify({ description: pkg.manifest.raw.description, full_description: readme }),
189
189
  });
190
190
  if (!res.ok)
191
191
  throw new Error(`DockerHub description update failed: ${res.status}`);
@@ -47,7 +47,7 @@ export var ExecService;
47
47
  const names = new Set(packages.map(p => p.name));
48
48
  const children = packages.map(pkg => {
49
49
  const ctx = panel.addItem(pkg.name);
50
- const dependencies = topo ? pkg.dependencies.filter(d => names.has(d)) : [];
50
+ const dependencies = topo ? pkg.dependencies.filter(d => names.has(d.name)).map(d => d.name) : [];
51
51
  return new Task(() => execForPackage(ctx, panel.enabled, pkg.dirname, command, logger, bail ? () => rootTask?.abort() : () => { }), { name: pkg.name, dependencies });
52
52
  });
53
53
  panel.start();
@@ -1,5 +1,5 @@
1
- import type { Package } from '../core/package.js';
2
- import type { Repository } from '../core/repository.js';
1
+ import { Package } from '../core/package.js';
2
+ import { Repository } from '../core/repository.js';
3
3
  export declare namespace GithubReleaseService {
4
4
  /** Injectable "does this release already exist" check - mainly for tests, so they don't depend
5
5
  * on network access or a real GitHub token. Same shape as `DockerPublishService.Deps`' own
@@ -1,10 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import fastGlob from 'fast-glob';
4
- import semver from 'semver';
5
- import { expandTag, tagPattern } from '../utils/change-hash.js';
4
+ import { Package } from '../core/package.js';
5
+ import { Repository } from '../core/repository.js';
6
6
  import { GitHelper } from '../utils/git.js';
7
7
  import { expandReleaseTag, isCalendarVersion, releaseTagPattern } from '../utils/release-version.js';
8
+ import { ChangeHashService } from './change-hash.service.js';
8
9
  import { ChangelogService } from './changelog.service.js';
9
10
  export var GithubReleaseService;
10
11
  (function (GithubReleaseService) {
@@ -129,18 +130,22 @@ const GITHUB_UPLOADS = 'https://uploads.github.com';
129
130
  * so the release needs a name of its own (`release-*`); a plain one means every package shares it,
130
131
  * and that shared version's tag already *is* the release. */
131
132
  function releaseTagFor(root) {
132
- return isCalendarVersion(root.version) ? expandReleaseTag(root, root.version) : expandTag(root, root.version);
133
+ return isCalendarVersion(root.version)
134
+ ? expandReleaseTag(root, root.version)
135
+ : ChangeHashService.expandTag(root, root.version);
133
136
  }
134
137
  /** The glob matching the tags `releaseTagFor` produces - for stepping back to the previous one. */
135
138
  function releaseTagGlob(root) {
136
- const pattern = isCalendarVersion(root.version) ? releaseTagPattern(root) : tagPattern(root);
139
+ const pattern = isCalendarVersion(root.version) ? releaseTagPattern(root) : ChangeHashService.tagPattern(root);
137
140
  return pattern.replace('{name}', root.name);
138
141
  }
139
142
  function resolvePrerelease(root, version) {
140
143
  const configured = root.config?.githubRelease?.prerelease;
141
144
  // A calendar version's time part is a semver prerelease identifier by construction - it says
142
145
  // nothing about the release being a preview, so it must not be read as one.
143
- return configured ?? (!isCalendarVersion(version) && !!semver.prerelease(version));
146
+ /** The root's own scheme decides what a preview looks like - `semver.prerelease` was that
147
+ * question with semver's answer baked in. */
148
+ return configured ?? (!isCalendarVersion(version) && root.versionScheme.isPrerelease(version));
144
149
  }
145
150
  /** `owner/repo` out of either remote URL form git hands back - `git@github.com:owner/repo.git`
146
151
  * (SSH) or `https://github.com/owner/repo.git` (HTTPS, credentials and all). `undefined` for
@@ -7,7 +7,10 @@ export var ListService;
7
7
  * status relative to upstream. Pure data - no console output; `rman list`'s own command
8
8
  * decides how to present it (table, JSON, parseable, names only, or a dependency graph). */
9
9
  async function getPackages(repository, options = {}) {
10
- const packages = filterPackages(repository.getPackages({ toposort: options.toposort }), options);
10
+ /** `false`: `list` is the inventory, so a package its own `.rmanrc "skip"` excludes is still
11
+ * *in* the repository and still listed - hiding it would answer a different question. Every
12
+ * other caller honours it, being a command that acts rather than reports. */
13
+ const packages = filterPackages(repository.getPackages({ toposort: options.toposort }), options, false);
11
14
  const status = await repository.listStatus({ hash: options.changedSince });
12
15
  let items = packages.map(p => {
13
16
  const target = p.config.publish?.target;
@@ -18,7 +21,7 @@ export var ListService;
18
21
  location: path.relative(repository.dirname, p.dirname) || '.',
19
22
  private: p.isPrivate,
20
23
  status: status[p.name],
21
- dependencies: [...p.dependencies],
24
+ dependencies: p.dependencies.map(d => d.name),
22
25
  publishTargets: [...publishTargets],
23
26
  docker: publishTargets.includes('docker') ? p.config.publish?.docker : undefined,
24
27
  };
@@ -1,9 +1,10 @@
1
1
  import type { Package } from '../core/package.js';
2
2
  import type { Repository } from '../core/repository.js';
3
+ import type { RunStepContext, RunStepFn, RunStepValue } from '../core/run-step.js';
3
4
  import { type LogLevel } from '../utils/logger.js';
4
5
  import { type PackageFilterOptions } from '../utils/package-filter.js';
5
6
  export declare namespace RunService {
6
- interface Options extends PackageFilterOptions {
7
+ export interface Options extends PackageFilterOptions {
7
8
  /** Max packages built at once: `true`/omitted = CPU count, a number = that many, `false` = serial (1). */
8
9
  parallel?: boolean | number;
9
10
  /** Respect the package dependency graph: a package waits for its dependencies and is skipped
@@ -52,7 +53,111 @@ export declare namespace RunService {
52
53
  * run:
53
54
  * test: mocha # same as test: { exec: mocha }
54
55
  */
55
- function getConfig(pkg: Package, script: string): Record<string, unknown>;
56
+ /**
57
+ * One step of a script - a shell command, or a function ([`RunStepFn`](../core/run-step.ts)).
58
+ *
59
+ * A union rather than one shape with two optional fields, so every consumer is made to say which
60
+ * it is handling: the executor that forgets is the one that silently runs nothing, which is
61
+ * exactly the bug this type replaces (a function in `after` used to be dropped by
62
+ * `normalizeScriptValue` and reported as a step that succeeded).
63
+ */
64
+ export type ScriptStep = CommandStep | FunctionStep;
65
+ interface StepBase {
66
+ /** The slot it came from - `before`/`exec`/`after`, which is what the log line shows. */
67
+ name: string;
68
+ /** What the progress panel and the per-step log print: the command itself, or the function's
69
+ * own name. */
70
+ label: string;
71
+ }
72
+ export interface CommandStep extends StepBase {
73
+ command: string;
74
+ run?: undefined;
75
+ }
76
+ export interface FunctionStep extends StepBase {
77
+ run: RunStepFn;
78
+ command?: undefined;
79
+ }
80
+ /** The three slots a script is made of, each one step or several run in sequence. The same three
81
+ * names a `.rmanrc "run.<script>"` block uses, because they are the same three things. */
82
+ export interface ScriptSlots {
83
+ before?: RunStepValue[];
84
+ exec?: RunStepValue[];
85
+ after?: RunStepValue[];
86
+ }
87
+ /**
88
+ * Where a package's steps can come from besides its `.rmanrc`.
89
+ *
90
+ * The core knows one source: the config. **`package.json#scripts` is not a source the core has**,
91
+ * because "a script lives in package.json" is true of a Node repository and of nothing else -
92
+ * `rman-node` contributes that one (with npm's `pre<script>`/`post<script>` convention and its
93
+ * `&&` splitting), and a plugin for another ecosystem would contribute its own.
94
+ *
95
+ * Returns `undefined` for "this package declares nothing", not empty slots - the difference
96
+ * decides whether the config's value applies.
97
+ */
98
+ export type StepSource = (pkg: Package, script: string) => ScriptSlots | undefined;
99
+ /**
100
+ * Registers a source. Called by `loadPlugins` for each plugin's `runSteps`, in `plugins`
101
+ * declaration order - never as an import side effect, so what is registered is exactly what the
102
+ * repository's `.rmanrc` asked for.
103
+ */
104
+ export function addStepSource(source: StepSource): void;
105
+ /** For tests, which would otherwise leak a source into every later case in the process. */
106
+ export function clearStepSources(): void;
107
+ /**
108
+ * What the *package itself* declares for the lifecycle `script`, from the contributed sources
109
+ * alone - no `.rmanrc` involved. `undefined` when it declares nothing.
110
+ *
111
+ * Exported because `run` is not the only lifecycle rman wraps: `version` runs hooks around the
112
+ * version write, and npm spells those `preversion`/`version`/`postversion` in `package.json` -
113
+ * which is the same `pre<script>`/`<script>`/`post<script>` shape a step source already answers.
114
+ * So `VersionService` asks here for `'version'` instead of reading `manifest.raw.scripts` itself,
115
+ * and npm's version lifecycle keeps working with no second seam and no extra line in any plugin.
116
+ * A plugin for another ecosystem gets its own lifecycle hooks the moment it contributes steps.
117
+ */
118
+ export function contributedSlots(pkg: Package, script: string): ScriptSlots | undefined;
119
+ /**
120
+ * Runs one slot of a lifecycle belonging to some operation other than `run` itself - `version`'s
121
+ * hooks around the version write are the only one so far.
122
+ *
123
+ * **Here rather than in `VersionService`, because the rule it applies is this service's**: the
124
+ * package's own declaration for `script` wins over the caller's `fallback`, slot by slot, exactly
125
+ * as `getScriptSteps` decides it for `run`. Kept in two places that rule would drift, and one of
126
+ * the copies would sit in the file that writes versions - which now runs no command of its own at
127
+ * all.
128
+ *
129
+ * `fallback` is the caller's own configured step(s), **already evaluated**: `version`'s three
130
+ * paths are in `DEFERRED_PATHS` precisely because only the caller can bind
131
+ * `${{ pkg.targetVersion }}`, so interpolating here would either be too early or need a scope this
132
+ * service has no business holding.
133
+ *
134
+ * **A list, not one joined string.** `VersionService` used to `join(' && ')` an array into a
135
+ * single shell line, which a function step cannot be part of - and which quietly changed the
136
+ * semantics of the shell case too, since `cd x && y` in one process is not the same as two.
137
+ */
138
+ export function runLifecycleSlot(pkg: Package, script: string, slot: keyof ScriptSlots, fallback?: RunStepValue[]): Promise<void>;
139
+ export function getConfig(pkg: Package, script: string): Record<string, unknown>;
140
+ /**
141
+ * The context a function step or `if` is handed - see [`RunStepContext`](../core/run-step.ts).
142
+ *
143
+ * `runBin` and `logger` are bound to *this run* rather than left to be imported, which is the
144
+ * whole reason they are handed over: an imported `runBin` knows neither the cwd nor the resolved
145
+ * log level.
146
+ */
147
+ /**
148
+ * A `run.<script>.before`/`.exec`/`.after` (or `version.<slot>`) value: one step, or several to
149
+ * run in sequence. A shell command or a function, and a list may mix them.
150
+ *
151
+ * **Anything else throws, naming the path.** It used to `return []`, which meant a value rman did
152
+ * not recognize was dropped with no trace: writing a function here - the obvious guess, and now
153
+ * the supported form - produced `1 succeeded, 0 failed` with the step never run (measured). A
154
+ * configuration mistake has to be loud; silence here reads as success.
155
+ *
156
+ * Exported, and the only implementation: `VersionService` used to carry a second one that behaved
157
+ * differently, which is how `version.<slot>` came to join its array with `' && '`.
158
+ */
159
+ export function normalizeScriptValue(value: unknown, at: string): RunStepValue[];
160
+ export function createStepContext(pkg: Package, cwd: string): RunStepContext;
56
161
  /**
57
162
  * `.rmanrc` conditional execution, GitHub Actions-`if`-flavored but a small closed grammar
58
163
  * instead of a full expression language (less to get wrong, still covers what's asked for) -
@@ -66,7 +171,7 @@ export declare namespace RunService {
66
171
  * if: changed = {CHANGE_HASH} # {NAME} -> process.env.NAME first
67
172
  * if: (changed or dirty) and not committed
68
173
  */
69
- type IfNode = {
174
+ export type IfNode = {
70
175
  kind: 'atom';
71
176
  name: string;
72
177
  value?: string;
@@ -84,13 +189,14 @@ export declare namespace RunService {
84
189
  };
85
190
  /** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
86
191
  * and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
87
- function parseIfExpr(raw: unknown): IfNode | undefined;
192
+ export function parseIfExpr(raw: unknown): IfNode | undefined;
88
193
  /** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
89
194
  * for the same reference hash across packages/scripts in a single run. */
90
- function evaluateIf(repository: Repository, pkg: Package, node: IfNode, statusCache: Map<string, Record<string, Repository.PackageStatus>>): Promise<boolean>;
91
- function runScript(repository: Repository, script: string, options?: Options & {
195
+ export function evaluateIf(repository: Repository, pkg: Package, node: IfNode, statusCache: Map<string, Record<string, Repository.PackageStatus>>): Promise<boolean>;
196
+ export function runScript(repository: Repository, script: string, options?: Options & {
92
197
  commandName?: string;
93
198
  }): Promise<void>;
199
+ export {};
94
200
  }
95
201
  /** Resolution order: explicit CLI flag > the package's resolved `.rmanrc` > `fallback`. */
96
202
  export declare function resolveBool(cliValue: boolean | undefined, pkg: Package, script: string, key: string, fallback: boolean): boolean;