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
@@ -0,0 +1,134 @@
1
+ /**
2
+ * What the commits said *happened*, in Conventional Commits' own terms: a `!` or `BREAKING CHANGE:`
3
+ * footer, a `feat:`, or anything else (a `fix:`, an unrecognized type, a non-conventional subject).
4
+ *
5
+ * Three, because three is how many sizes a commit message distinguishes - which is a fact about
6
+ * commit messages, not about any version format. It is deliberately **not** `patch`/`minor`/`major`:
7
+ * those are semver's words for how a *number* moves, and a scheme numbering
8
+ * `major.minor.build.revision` has four of those and no `patch` at all. `VersionScheme.bumpFor`
9
+ * translates one of these into a bump that scheme actually has a name for.
10
+ */
11
+ export type ChangeKind = 'fix' | 'feature' | 'breaking';
12
+ /**
13
+ * How a package's versions are numbered: named, ordered, validated, and advanced.
14
+ *
15
+ * rman's release model was written in semver - tag patterns, changelog headings, release identity,
16
+ * "is this version on the registry yet", `"workspace:"` range rewriting all assume versions that
17
+ * can be ordered and incremented. This is the seam for an ecosystem that numbers differently (PEP
18
+ * 440, a date-based scheme, a build counter), and it is a *meaning* seam rather than a storage one:
19
+ * `ManifestProvider` answers "where is the version written", this answers "what does the next one
20
+ * look like".
21
+ *
22
+ * **`bumpNames` and `bumpFor` are the interesting part.** `fix:` -> patch, `feat:` -> minor,
23
+ * `feat!:` -> major is a sentence in *semver's* vocabulary from end to end, so a scheme that numbers
24
+ * differently does not need a different `inc()` - it needs its own set of bump names and its own
25
+ * answer to "what does a `feat:` mean for one of my version numbers". A CalVer scheme can
26
+ * legitimately answer all three with the same name and return today's date; that is the scheme's
27
+ * decision to make, which is why the seam is here and not around `semver.inc`.
28
+ *
29
+ * **An abstract class rather than an interface**, for the reason `VersionPlanService` is one:
30
+ * `highestVersion`, `highestBump` and `smallestBump` all *derive* from the members below them, so
31
+ * requiring every scheme to write them would be boilerplate and a second place for two answers to
32
+ * disagree - but each is a real decision a scheme may need to make differently (a repo maintaining
33
+ * parallel lines has its own idea of "highest"; a four-part scheme may want the ripple bump to be
34
+ * `build` rather than its actual smallest). Implemented here, overridable there. Subclass
35
+ * `SemverScheme` to keep semver's numbering and change only one of these.
36
+ */
37
+ export declare abstract class VersionScheme {
38
+ /** For error messages, and for `info`. */
39
+ abstract readonly name: string;
40
+ /**
41
+ * Every bump this scheme accepts by name, **smallest first** - semver's
42
+ * `['patch', 'minor', 'major']`, a four-part `major.minor.build.revision` scheme's
43
+ * `['revision', 'build', 'minor', 'major']`.
44
+ *
45
+ * The scheme's, not rman's, because these are names for how a *version number* moves and only the
46
+ * scheme knows what parts it has. This is what `rman version <bump>` validates against and what
47
+ * `--help` lists, so a repository is offered the bumps its own numbering actually has.
48
+ *
49
+ * **The order is the default ranking** read by `highestBump` and `smallestBump`.
50
+ */
51
+ abstract readonly bumpNames: readonly string[];
52
+ /** Which of `bumpNames` a change of this kind calls for - the translation from what a commit said
53
+ * to how this scheme's numbers move. A scheme that does not distinguish them may answer all
54
+ * three the same. */
55
+ abstract bumpFor(kind: ChangeKind): string;
56
+ /** Is this a version this scheme recognizes? Used to accept an explicitly given version. */
57
+ abstract isValid(version: string): boolean;
58
+ /** Negative, zero or positive - `Array.prototype.sort`'s contract. */
59
+ abstract compare(a: string, b: string): number;
60
+ /** The version after `current`, given one of this scheme's own `bumpNames`. `preid` asks for a
61
+ * prerelease line (`--preid`), which a scheme without such a concept may ignore. */
62
+ abstract next(current: string, bump: string, options?: {
63
+ preid?: string;
64
+ }): string;
65
+ /** Is this version a preview rather than a release? `github-release` reads it. */
66
+ abstract isPrerelease(version: string): boolean;
67
+ /**
68
+ * The highest of `versions` - a group's current version is the highest among its members, and a
69
+ * monorepo root's release identity the highest among the groups.
70
+ *
71
+ * Empty in, `undefined` out: a group with no versions has no highest one, and guessing `0.0.0`
72
+ * would put a real version line at risk. Override to answer differently - a repository
73
+ * maintaining parallel lines (a 1.x still receiving fixes beside a 2.x) may want the line being
74
+ * released rather than the numerically largest.
75
+ */
76
+ highestVersion(versions: readonly string[]): string | undefined;
77
+ /**
78
+ * The largest of `bumps` - what a group takes when its changed members ask for different sizes.
79
+ * Empty in, `undefined` out.
80
+ *
81
+ * A name this scheme does not declare ranks **below** every name it does, rather than throwing -
82
+ * a comparison helper is the wrong place to fail a release. It is a second line of defence, not
83
+ * the guard: `detectBump` already drops an unrecognized `Release-As:` footer before it gets here,
84
+ * because ranking such a word low is not the same as ignoring it (ranked low it still *replaces*
85
+ * what the commit's own subject said - measured, and it turned a `feat:` into a patch).
86
+ *
87
+ * Override for bumps that are not totally ordered, or to refuse an unknown name outright.
88
+ */
89
+ highestBump(bumps: readonly string[]): string | undefined;
90
+ /**
91
+ * What a package bumped *only* because a dependency of it moved receives - there being nothing
92
+ * about the package itself for a larger bump to describe. `bumpNames`' first entry by default.
93
+ *
94
+ * Throws when this scheme declares no bumps at all: such a scheme cannot express a release, and
95
+ * finding out here beats silently leaving every rippled package unbumped. Override to nominate a
96
+ * different one - a four-part scheme may reserve `revision` for something else and want `build`.
97
+ */
98
+ smallestBump(): string;
99
+ }
100
+ /**
101
+ * Semver, exported as a class so a scheme that numbers in semver but decides one thing differently
102
+ * can subclass it instead of restating all of it.
103
+ */
104
+ export declare class SemverScheme extends VersionScheme {
105
+ readonly name = "semver";
106
+ readonly bumpNames: readonly ["patch", "minor", "major"];
107
+ bumpFor(kind: ChangeKind): string;
108
+ isValid(version: string): boolean;
109
+ compare(a: string, b: string): number;
110
+ next(current: string, bump: string, options?: {
111
+ preid?: string;
112
+ }): string;
113
+ isPrerelease(version: string): boolean;
114
+ }
115
+ /**
116
+ * The default, and what every package gets unless something says otherwise - so nothing about
117
+ * rman's behaviour changes by this seam existing.
118
+ */
119
+ export declare const semverScheme: VersionScheme;
120
+ /**
121
+ * Refuses a group whose packages do not agree on a scheme.
122
+ *
123
+ * A group is one version line: its members are compared against each other and bumped together, so
124
+ * two schemes in one group makes both of those undefined. Better to say so than to compare a PEP
125
+ * 440 version against a semver one and act on whatever falls out.
126
+ *
127
+ * A free function rather than a member, unlike the three above: this is a question about a *set of
128
+ * packages*, and asking one of the disagreeing schemes to arbitrate would be asking a party to the
129
+ * dispute.
130
+ */
131
+ export declare function assertOneScheme(schemes: {
132
+ packageName: string;
133
+ scheme: VersionScheme;
134
+ }[], group: string): void;
@@ -0,0 +1,148 @@
1
+ import semver from 'semver';
2
+ /**
3
+ * How a package's versions are numbered: named, ordered, validated, and advanced.
4
+ *
5
+ * rman's release model was written in semver - tag patterns, changelog headings, release identity,
6
+ * "is this version on the registry yet", `"workspace:"` range rewriting all assume versions that
7
+ * can be ordered and incremented. This is the seam for an ecosystem that numbers differently (PEP
8
+ * 440, a date-based scheme, a build counter), and it is a *meaning* seam rather than a storage one:
9
+ * `ManifestProvider` answers "where is the version written", this answers "what does the next one
10
+ * look like".
11
+ *
12
+ * **`bumpNames` and `bumpFor` are the interesting part.** `fix:` -> patch, `feat:` -> minor,
13
+ * `feat!:` -> major is a sentence in *semver's* vocabulary from end to end, so a scheme that numbers
14
+ * differently does not need a different `inc()` - it needs its own set of bump names and its own
15
+ * answer to "what does a `feat:` mean for one of my version numbers". A CalVer scheme can
16
+ * legitimately answer all three with the same name and return today's date; that is the scheme's
17
+ * decision to make, which is why the seam is here and not around `semver.inc`.
18
+ *
19
+ * **An abstract class rather than an interface**, for the reason `VersionPlanService` is one:
20
+ * `highestVersion`, `highestBump` and `smallestBump` all *derive* from the members below them, so
21
+ * requiring every scheme to write them would be boilerplate and a second place for two answers to
22
+ * disagree - but each is a real decision a scheme may need to make differently (a repo maintaining
23
+ * parallel lines has its own idea of "highest"; a four-part scheme may want the ripple bump to be
24
+ * `build` rather than its actual smallest). Implemented here, overridable there. Subclass
25
+ * `SemverScheme` to keep semver's numbering and change only one of these.
26
+ */
27
+ export class VersionScheme {
28
+ /**
29
+ * The highest of `versions` - a group's current version is the highest among its members, and a
30
+ * monorepo root's release identity the highest among the groups.
31
+ *
32
+ * Empty in, `undefined` out: a group with no versions has no highest one, and guessing `0.0.0`
33
+ * would put a real version line at risk. Override to answer differently - a repository
34
+ * maintaining parallel lines (a 1.x still receiving fixes beside a 2.x) may want the line being
35
+ * released rather than the numerically largest.
36
+ */
37
+ highestVersion(versions) {
38
+ if (!versions.length)
39
+ return undefined;
40
+ return versions.reduce((highest, v) => (this.compare(v, highest) > 0 ? v : highest));
41
+ }
42
+ /**
43
+ * The largest of `bumps` - what a group takes when its changed members ask for different sizes.
44
+ * Empty in, `undefined` out.
45
+ *
46
+ * A name this scheme does not declare ranks **below** every name it does, rather than throwing -
47
+ * a comparison helper is the wrong place to fail a release. It is a second line of defence, not
48
+ * the guard: `detectBump` already drops an unrecognized `Release-As:` footer before it gets here,
49
+ * because ranking such a word low is not the same as ignoring it (ranked low it still *replaces*
50
+ * what the commit's own subject said - measured, and it turned a `feat:` into a patch).
51
+ *
52
+ * Override for bumps that are not totally ordered, or to refuse an unknown name outright.
53
+ */
54
+ highestBump(bumps) {
55
+ if (!bumps.length)
56
+ return undefined;
57
+ return bumps.reduce((highest, b) => (this.bumpNames.indexOf(b) > this.bumpNames.indexOf(highest) ? b : highest));
58
+ }
59
+ /**
60
+ * What a package bumped *only* because a dependency of it moved receives - there being nothing
61
+ * about the package itself for a larger bump to describe. `bumpNames`' first entry by default.
62
+ *
63
+ * Throws when this scheme declares no bumps at all: such a scheme cannot express a release, and
64
+ * finding out here beats silently leaving every rippled package unbumped. Override to nominate a
65
+ * different one - a four-part scheme may reserve `revision` for something else and want `build`.
66
+ */
67
+ smallestBump() {
68
+ const smallest = this.bumpNames[0];
69
+ if (!smallest) {
70
+ throw new Error(`Version scheme "${this.name}" declares no "bumpNames", so no version can be bumped.`);
71
+ }
72
+ return smallest;
73
+ }
74
+ }
75
+ /**
76
+ * Semver, exported as a class so a scheme that numbers in semver but decides one thing differently
77
+ * can subclass it instead of restating all of it.
78
+ */
79
+ export class SemverScheme extends VersionScheme {
80
+ name = 'semver';
81
+ bumpNames = ['patch', 'minor', 'major'];
82
+ bumpFor(kind) {
83
+ switch (kind) {
84
+ case 'breaking':
85
+ return 'major';
86
+ case 'feature':
87
+ return 'minor';
88
+ case 'fix':
89
+ return 'patch';
90
+ }
91
+ }
92
+ isValid(version) {
93
+ return !!semver.valid(version);
94
+ }
95
+ compare(a, b) {
96
+ return semver.compare(a, b);
97
+ }
98
+ next(current, bump, options) {
99
+ const releaseType = bump;
100
+ const preid = options?.preid;
101
+ if (!preid)
102
+ return semver.inc(current, releaseType) ?? current;
103
+ /**
104
+ * Already on a prerelease under **this same identifier**: just advance its counter
105
+ * (`1.2.3-beta.0` -> `1.2.3-beta.1`), rather than jumping to a new base version every run of
106
+ * the same beta cycle. Anything else - a plain release, or a prerelease under a *different*
107
+ * identifier (`beta` -> `rc`) - starts a fresh prerelease of `bump`'s own type.
108
+ *
109
+ * Comparing the identifier, not merely "is a prerelease": treating any prerelease as the same
110
+ * line makes switching `beta` to `rc` silently continue the beta counter.
111
+ */
112
+ const existing = semver.prerelease(current);
113
+ const sameLine = existing && String(existing[0]) === preid;
114
+ const preType = sameLine ? 'prerelease' : `pre${releaseType}`;
115
+ return semver.inc(current, preType, preid) ?? current;
116
+ }
117
+ isPrerelease(version) {
118
+ return !!semver.prerelease(version);
119
+ }
120
+ }
121
+ /**
122
+ * The default, and what every package gets unless something says otherwise - so nothing about
123
+ * rman's behaviour changes by this seam existing.
124
+ */
125
+ export const semverScheme = new SemverScheme();
126
+ /**
127
+ * Refuses a group whose packages do not agree on a scheme.
128
+ *
129
+ * A group is one version line: its members are compared against each other and bumped together, so
130
+ * two schemes in one group makes both of those undefined. Better to say so than to compare a PEP
131
+ * 440 version against a semver one and act on whatever falls out.
132
+ *
133
+ * A free function rather than a member, unlike the three above: this is a question about a *set of
134
+ * packages*, and asking one of the disagreeing schemes to arbitrate would be asking a party to the
135
+ * dispute.
136
+ */
137
+ export function assertOneScheme(schemes, group) {
138
+ const first = schemes[0];
139
+ if (!first)
140
+ return;
141
+ const odd = schemes.find(s => s.scheme.name !== first.scheme.name);
142
+ if (!odd)
143
+ return;
144
+ throw new Error(`Group "${group}" mixes version schemes: "${first.packageName}" uses ${first.scheme.name} and ` +
145
+ `"${odd.packageName}" uses ${odd.scheme.name}.\n` +
146
+ ` A group is one version line - its packages are compared and bumped together, which two ` +
147
+ `schemes make meaningless. Put them in separate groups (.rmanrc "group").`);
148
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * How a repository is laid out, and who decides.
3
+ *
4
+ * A namespace for the same reason `Manifest` is one: it is what a plugin can *augment*
5
+ * (`declare module 'rman' { namespace Workspace { ... } }`), so whatever this seam grows later
6
+ * arrives as a member here rather than as another top-level export.
7
+ */
8
+ export declare namespace Workspace {
9
+ /** What a repository looks like: where its root is, and which directories hold its packages. */
10
+ interface Layout {
11
+ /** Absolute path to the repository root. */
12
+ root: string;
13
+ /**
14
+ * Absolute paths to the package **directories**, outside the root. Empty for a repository that
15
+ * is itself the one package.
16
+ *
17
+ * Directories rather than `Package` objects, and not for want of trying: a provider runs before
18
+ * any package exists - `Repository.create` constructs them from these paths afterwards, through
19
+ * the *manifest* provider. A discovery provider returning packages would have to construct them
20
+ * itself, making discovery the owner of identity too.
21
+ *
22
+ * Unlike `Package.dependencies`, there is no identity question here: these are paths, and a
23
+ * path is unique by construction where a name is only unique if the ecosystem says so.
24
+ */
25
+ packageDirs: string[];
26
+ }
27
+ /**
28
+ * How a repository's packages are found.
29
+ *
30
+ * **The core has no provider**, and that is the point: "packages are the `workspaces` globs in
31
+ * the root `package.json`, and a package is a directory with a `package.json` in it" is true of
32
+ * npm and of nothing else. `rman-node` contributes that one; a plugin for another ecosystem
33
+ * contributes its own (a Cargo workspace, a `go.work`, a `pyproject.toml`).
34
+ *
35
+ * Returns `undefined` for "this is not a repository I recognize", so the next provider gets a
36
+ * turn and a repository nobody recognizes falls back to being a single package.
37
+ */
38
+ type Provider = (root: string) => Layout | undefined;
39
+ /**
40
+ * Registers a provider. Called by `loadPlugins` for each plugin's `workspace`, in `plugins`
41
+ * declaration order - so which provider answers is a function of the repository's own config.
42
+ */
43
+ function addProvider(provider: Provider): void;
44
+ /** For tests, which would otherwise leak a provider into every later case in the process. */
45
+ function clearProviders(): void;
46
+ /** The first provider that recognizes `root`, in declaration order. */
47
+ function resolve(root: string): Layout | undefined;
48
+ /**
49
+ * Where the repository starts, decided **without knowing anything about any ecosystem** - it has
50
+ * to be, because the plugins that do know are named in the config file this walk is looking for.
51
+ *
52
+ * Walking up from `from`, stopping after a directory holding `.git`, the root is:
53
+ *
54
+ * 1. the **outermost** directory in that chain holding an `.rmanrc*` - outermost, because a
55
+ * *package* may have its own `.rmanrc` (that is a supported thing), and from inside such a
56
+ * package the nearest one is the package's, not the repository's;
57
+ * 2. otherwise the `.git` directory itself, the ordinary meaning of "repository root";
58
+ * 3. otherwise `from`, which is all that is left to go on.
59
+ *
60
+ * **What "outermost" costs**, since the two cases genuinely conflict and only one can win: a
61
+ * self-contained project nested inside a larger git repository *and sharing its `.git`* resolves
62
+ * to the outer root, not to itself (measured). That is the same repository by any definition git
63
+ * recognizes, so it is the defensible answer - and a nested project with a `.git` of its own is
64
+ * found correctly, because the walk stops there before the outer `.rmanrc` is ever seen (also
65
+ * measured). A per-package `.rmanrc` is the common case and it is the one served.
66
+ */
67
+ function findRoot(from: string, deep?: number): string;
68
+ }
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * How a repository is laid out, and who decides.
5
+ *
6
+ * A namespace for the same reason `Manifest` is one: it is what a plugin can *augment*
7
+ * (`declare module 'rman' { namespace Workspace { ... } }`), so whatever this seam grows later
8
+ * arrives as a member here rather than as another top-level export.
9
+ */
10
+ export var Workspace;
11
+ (function (Workspace) {
12
+ /**
13
+ * Registers a provider. Called by `loadPlugins` for each plugin's `workspace`, in `plugins`
14
+ * declaration order - so which provider answers is a function of the repository's own config.
15
+ */
16
+ function addProvider(provider) {
17
+ if (providers.includes(provider))
18
+ return;
19
+ providers.push(provider);
20
+ }
21
+ Workspace.addProvider = addProvider;
22
+ /** For tests, which would otherwise leak a provider into every later case in the process. */
23
+ function clearProviders() {
24
+ providers.length = 0;
25
+ }
26
+ Workspace.clearProviders = clearProviders;
27
+ /** The first provider that recognizes `root`, in declaration order. */
28
+ function resolve(root) {
29
+ for (const provider of providers) {
30
+ const layout = provider(root);
31
+ if (layout)
32
+ return layout;
33
+ }
34
+ return undefined;
35
+ }
36
+ Workspace.resolve = resolve;
37
+ /**
38
+ * Where the repository starts, decided **without knowing anything about any ecosystem** - it has
39
+ * to be, because the plugins that do know are named in the config file this walk is looking for.
40
+ *
41
+ * Walking up from `from`, stopping after a directory holding `.git`, the root is:
42
+ *
43
+ * 1. the **outermost** directory in that chain holding an `.rmanrc*` - outermost, because a
44
+ * *package* may have its own `.rmanrc` (that is a supported thing), and from inside such a
45
+ * package the nearest one is the package's, not the repository's;
46
+ * 2. otherwise the `.git` directory itself, the ordinary meaning of "repository root";
47
+ * 3. otherwise `from`, which is all that is left to go on.
48
+ *
49
+ * **What "outermost" costs**, since the two cases genuinely conflict and only one can win: a
50
+ * self-contained project nested inside a larger git repository *and sharing its `.git`* resolves
51
+ * to the outer root, not to itself (measured). That is the same repository by any definition git
52
+ * recognizes, so it is the defensible answer - and a nested project with a `.git` of its own is
53
+ * found correctly, because the walk stops there before the outer `.rmanrc` is ever seen (also
54
+ * measured). A per-package `.rmanrc` is the common case and it is the one served.
55
+ */
56
+ function findRoot(from, deep = 10) {
57
+ const chain = [];
58
+ let dir = path.resolve(from);
59
+ let remaining = deep;
60
+ while (remaining-- >= 0 && fs.existsSync(dir)) {
61
+ chain.push(dir);
62
+ if (fs.existsSync(path.join(dir, '.git')))
63
+ break;
64
+ const parent = path.resolve(dir, '..');
65
+ if (parent === dir)
66
+ break;
67
+ dir = parent;
68
+ }
69
+ const withConfig = chain.filter(hasRmanConfig);
70
+ if (withConfig.length)
71
+ return withConfig[withConfig.length - 1];
72
+ const gitRoot = chain.find(d => fs.existsSync(path.join(d, '.git')));
73
+ return gitRoot ?? path.resolve(from);
74
+ }
75
+ Workspace.findRoot = findRoot;
76
+ /** Every file form a `.rmanrc` comes in - `package.json#rman` is deliberately *not* one of them
77
+ * here: it would make the root question npm-shaped again. */
78
+ const CONFIG_FILES = ['.rmanrc', '.rmanrc.yml', '.rmanrc.cjs', '.rmanrc.mjs', '.rmanrc.js'];
79
+ function hasRmanConfig(dir) {
80
+ return CONFIG_FILES.some(name => fs.existsSync(path.join(dir, name)));
81
+ }
82
+ const providers = [];
83
+ })(Workspace || (Workspace = {}));
package/index.d.ts CHANGED
@@ -1,19 +1,66 @@
1
1
  /**
2
2
  * Programmatic API - the same logic the CLI commands run, importable directly without going
3
3
  * through yargs/argv. Each domain's logic lives in a `<Name>` namespace under `./services/*.ts`
4
- * (e.g. `ChangelogService`, `CiService`, `CleanService`, `ListService`, `RunService`,
5
- * `SystemInfo`, `VersionService`), re-exported here via `./services.js`. Purpose-specific functions, not one
6
- * generic `run`/`get` per domain (see e.g. `SystemInfo.getSystemInfo`/`getRepositoryInfo`, kept
7
- * separate since they're genuinely independent capabilities, not just steps of one operation) -
8
- * CLI-only concerns (argv parsing, `--help` text, and all console/file presentation) stay in
9
- * `cli.ts` and the individual `commands/*.command.ts` modules, which are not exported here.
4
+ * (e.g. `ChangelogService`, `ListService`, `RunService`, `VersionService`),
5
+ * re-exported here via `./services.js`. Purpose-specific functions, not one generic `run`/`get`
6
+ * per domain - CLI-only concerns (argv parsing, `--help` text, and all console/file presentation)
7
+ * stay in `cli.ts` and the individual `commands/*.command.ts` modules, which are not exported here.
8
+ *
9
+ * **This is also the plugin contract.** A plugin (`rman-node`, say) is an ordinary package that
10
+ * imports from here, so everything a command needs in order to live outside rman has to be
11
+ * exported - the progress panel, the package filter, the branch guard, the git helper. What is
12
+ * *not* exported is deliberately private: config resolution internals, the expression evaluator,
13
+ * the command registry.
10
14
  */
11
15
  export { defineConfig } from './core/config.js';
16
+ export type { CommandContext, CustomCommand } from './core/custom-command.js';
17
+ export { defineCommand } from './core/custom-command.js';
18
+ /** The manifest seam: where a package's name and version are written, and how it is numbered.
19
+ * The core has no provider - `package.json` is npm's answer, and lives in `rman-node`. */
20
+ export type { ManifestProvider } from './core/manifest.js';
21
+ /** Both the shape and the registry: `const m: Manifest` and `Manifest.read(dir)` - merged onto one
22
+ * name so a plugin can augment it the way it augments `SystemInfo`. */
23
+ export { Manifest } from './core/manifest.js';
12
24
  export { Package } from './core/package.js';
25
+ export type { RmanPlugin } from './core/plugin.js';
26
+ export { definePlugin } from './core/plugin.js';
13
27
  export { Repository } from './core/repository.js';
28
+ export type { RunConditionFn, RunStepContext, RunStepFn, RunStepValue } from './core/run-step.js';
29
+ export type { ChangeKind } from './core/version-scheme.js';
30
+ /** The numbering seam. `VersionScheme` is abstract - `highestVersion`/`highestBump`/`smallestBump`
31
+ * are implemented from the members around them, so a scheme states only what it must and still
32
+ * overrides any of the three. `SemverScheme` is exported to subclass rather than restate. */
33
+ export { assertOneScheme, SemverScheme, semverScheme, VersionScheme } from './core/version-scheme.js';
34
+ /** The workspace seam: how a repository's packages are found. A plugin contributes a provider
35
+ * (see `RmanPlugin.workspace`); the core has none, so `workspaces` is npm's idea and lives in
36
+ * `rman-node`. */
37
+ /** `Workspace.Layout`, `Workspace.Provider`, `Workspace.addProvider`, `Workspace.resolve`,
38
+ * `Workspace.findRoot` - one namespace, so a plugin can augment it. */
39
+ export { Workspace } from './core/workspace.js';
14
40
  export * from './interfaces/rman-config.interface.js';
15
41
  export * from './services.js';
16
- export type { DetectChangeHashOptions } from './utils/change-hash.js';
17
- export { detectChangeHash } from './utils/change-hash.js';
42
+ /** Branch guarding: `allowBranch`/`ignoreBranch`, so a plugin's release command refuses to run on
43
+ * the wrong branch exactly as `publish` and `version` do. */
44
+ export { applyBranchGuardOptions, assertAllowedBranch, type BranchGuardOptions, readBranchGuardOptions, } from './utils/branch-guard.js';
45
+ /** Where a repository's locally installed binaries live - the core spells the PATH variable, a
46
+ * plugin says which directories go on it (see `RmanPlugin.binPaths`). */
47
+ export { BinPath } from './utils/bin-path.js';
48
+ /** A shell command, with the repository's local binaries on PATH - for a command string an author
49
+ * wrote. `runBin` is the one to reach for when the arguments are assembled in code. */
50
+ export type { ExecOptions, ExecResult } from './utils/exec.js';
51
+ export { exec } from './utils/exec.js';
52
+ export { GitHelper } from './utils/git.js';
18
53
  export type { LogLevel } from './utils/logger.js';
19
54
  export { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
55
+ /** `--scope`/`--deps`/`--dependents`/`--private`, so a plugin's command filters packages the same
56
+ * way every built-in does rather than inventing its own flags. */
57
+ export { applyPackageFilterOptions, applyRootOption, filterPackages, type PackageFilterOptions, readPackageFilterOptions, readRootOption, } from './utils/package-filter.js';
58
+ /** The live panel `run`/`build`/`clean` print - a plugin's per-package command looks like the rest
59
+ * of rman instead of like a script someone bolted on. */
60
+ export { formatDuration, type ProgressItem, ProgressPanel, type ProgressStatus, type ProgressSummary, } from './utils/progress-panel.js';
61
+ /** Version stamping helpers a `ManifestProvider.stampVersion` can delegate to - the quoted-constant
62
+ * pattern most languages share, and the OCI Dockerfile label (which `version` stamps itself, since
63
+ * the label's value is by specification the package's version). */
64
+ export type { RunBinOptions, RunBinResult } from './utils/run-bin.js';
65
+ export { runBin } from './utils/run-bin.js';
66
+ export { OCI_VERSION_LABEL, stampVersionConstant, stampVersionLabel } from './utils/version-stamp.js';
package/index.js CHANGED
@@ -1,17 +1,52 @@
1
1
  /**
2
2
  * Programmatic API - the same logic the CLI commands run, importable directly without going
3
3
  * through yargs/argv. Each domain's logic lives in a `<Name>` namespace under `./services/*.ts`
4
- * (e.g. `ChangelogService`, `CiService`, `CleanService`, `ListService`, `RunService`,
5
- * `SystemInfo`, `VersionService`), re-exported here via `./services.js`. Purpose-specific functions, not one
6
- * generic `run`/`get` per domain (see e.g. `SystemInfo.getSystemInfo`/`getRepositoryInfo`, kept
7
- * separate since they're genuinely independent capabilities, not just steps of one operation) -
8
- * CLI-only concerns (argv parsing, `--help` text, and all console/file presentation) stay in
9
- * `cli.ts` and the individual `commands/*.command.ts` modules, which are not exported here.
4
+ * (e.g. `ChangelogService`, `ListService`, `RunService`, `VersionService`),
5
+ * re-exported here via `./services.js`. Purpose-specific functions, not one generic `run`/`get`
6
+ * per domain - CLI-only concerns (argv parsing, `--help` text, and all console/file presentation)
7
+ * stay in `cli.ts` and the individual `commands/*.command.ts` modules, which are not exported here.
8
+ *
9
+ * **This is also the plugin contract.** A plugin (`rman-node`, say) is an ordinary package that
10
+ * imports from here, so everything a command needs in order to live outside rman has to be
11
+ * exported - the progress panel, the package filter, the branch guard, the git helper. What is
12
+ * *not* exported is deliberately private: config resolution internals, the expression evaluator,
13
+ * the command registry.
10
14
  */
11
15
  export { defineConfig } from './core/config.js';
16
+ export { defineCommand } from './core/custom-command.js';
17
+ /** Both the shape and the registry: `const m: Manifest` and `Manifest.read(dir)` - merged onto one
18
+ * name so a plugin can augment it the way it augments `SystemInfo`. */
19
+ export { Manifest } from './core/manifest.js';
12
20
  export { Package } from './core/package.js';
21
+ export { definePlugin } from './core/plugin.js';
13
22
  export { Repository } from './core/repository.js';
23
+ /** The numbering seam. `VersionScheme` is abstract - `highestVersion`/`highestBump`/`smallestBump`
24
+ * are implemented from the members around them, so a scheme states only what it must and still
25
+ * overrides any of the three. `SemverScheme` is exported to subclass rather than restate. */
26
+ export { assertOneScheme, SemverScheme, semverScheme, VersionScheme } from './core/version-scheme.js';
27
+ /** The workspace seam: how a repository's packages are found. A plugin contributes a provider
28
+ * (see `RmanPlugin.workspace`); the core has none, so `workspaces` is npm's idea and lives in
29
+ * `rman-node`. */
30
+ /** `Workspace.Layout`, `Workspace.Provider`, `Workspace.addProvider`, `Workspace.resolve`,
31
+ * `Workspace.findRoot` - one namespace, so a plugin can augment it. */
32
+ export { Workspace } from './core/workspace.js';
14
33
  export * from './interfaces/rman-config.interface.js';
15
34
  export * from './services.js';
16
- export { detectChangeHash } from './utils/change-hash.js';
35
+ // --- what a command needs to behave like a built-in one -----------------------------------------
36
+ /** Branch guarding: `allowBranch`/`ignoreBranch`, so a plugin's release command refuses to run on
37
+ * the wrong branch exactly as `publish` and `version` do. */
38
+ export { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions, } from './utils/branch-guard.js';
39
+ /** Where a repository's locally installed binaries live - the core spells the PATH variable, a
40
+ * plugin says which directories go on it (see `RmanPlugin.binPaths`). */
41
+ export { BinPath } from './utils/bin-path.js';
42
+ export { exec } from './utils/exec.js';
43
+ export { GitHelper } from './utils/git.js';
17
44
  export { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
45
+ /** `--scope`/`--deps`/`--dependents`/`--private`, so a plugin's command filters packages the same
46
+ * way every built-in does rather than inventing its own flags. */
47
+ export { applyPackageFilterOptions, applyRootOption, filterPackages, readPackageFilterOptions, readRootOption, } from './utils/package-filter.js';
48
+ /** The live panel `run`/`build`/`clean` print - a plugin's per-package command looks like the rest
49
+ * of rman instead of like a script someone bolted on. */
50
+ export { formatDuration, ProgressPanel, } from './utils/progress-panel.js';
51
+ export { runBin } from './utils/run-bin.js';
52
+ export { OCI_VERSION_LABEL, stampVersionConstant, stampVersionLabel } from './utils/version-stamp.js';