rman 1.3.0 → 2.0.0-beta.3

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 (187) hide show
  1. package/README.md +65 -31
  2. package/builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts +83 -0
  3. package/builtins/platforms/node/augmentation/system-info.augmentation.d.ts +26 -0
  4. package/builtins/platforms/node/augmentation/system-info.augmentation.js +79 -0
  5. package/builtins/platforms/node/commands/ci.command.d.ts +133 -0
  6. package/builtins/platforms/node/commands/ci.command.js +60 -0
  7. package/builtins/platforms/node/commands/clean.command.d.ts +185 -0
  8. package/builtins/platforms/node/commands/clean.command.js +74 -0
  9. package/builtins/platforms/node/index.d.ts +8 -0
  10. package/builtins/platforms/node/index.js +8 -0
  11. package/builtins/platforms/node/node-config.interface.d.ts +76 -0
  12. package/builtins/platforms/node/node-config.interface.js +7 -0
  13. package/builtins/platforms/node/node-manifest.provider.d.ts +68 -0
  14. package/builtins/platforms/node/node-manifest.provider.js +125 -0
  15. package/builtins/platforms/node/node.platform.d.ts +53 -0
  16. package/builtins/platforms/node/node.platform.js +134 -0
  17. package/builtins/platforms/node/services/ci.service.d.ts +47 -0
  18. package/builtins/platforms/node/services/ci.service.js +213 -0
  19. package/builtins/platforms/node/services/clean.service.d.ts +53 -0
  20. package/builtins/platforms/node/services/clean.service.js +237 -0
  21. package/builtins/platforms/node/services/publish.service.d.ts +114 -0
  22. package/builtins/platforms/node/services/publish.service.js +371 -0
  23. package/builtins/platforms/node/services/version-plan.service.d.ts +44 -0
  24. package/builtins/platforms/node/services/version-plan.service.js +58 -0
  25. package/builtins/platforms/node/utils/workspace-range.d.ts +26 -0
  26. package/builtins/platforms/node/utils/workspace-range.js +28 -0
  27. package/builtins/presets/index.d.ts +15 -0
  28. package/builtins/presets/index.js +57 -0
  29. package/builtins/presets/node.d.ts +16 -0
  30. package/builtins/presets/node.js +57 -0
  31. package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +52 -31
  32. package/{services → builtins/publish-targets/docker}/docker-publish.service.js +49 -26
  33. package/builtins/publish-targets/docker/docker.target.d.ts +54 -0
  34. package/builtins/publish-targets/docker/docker.target.js +40 -0
  35. package/builtins/publish-targets/docker/index.d.ts +2 -0
  36. package/builtins/publish-targets/docker/index.js +2 -0
  37. package/builtins/publish-targets/npm/index.d.ts +2 -0
  38. package/builtins/publish-targets/npm/index.js +2 -0
  39. package/builtins/publish-targets/npm/npm-publish-target.d.ts +73 -0
  40. package/builtins/publish-targets/npm/npm-publish-target.js +95 -0
  41. package/builtins/publish-targets/npm/npm-view.d.ts +48 -0
  42. package/builtins/publish-targets/npm/npm-view.js +71 -0
  43. package/cli.d.ts +8 -0
  44. package/cli.js +228 -93
  45. package/commands/build.command.d.ts +177 -3
  46. package/commands/build.command.js +20 -10
  47. package/commands/changelog.command.d.ts +296 -3
  48. package/commands/changelog.command.js +127 -43
  49. package/commands/config.command.d.ts +44 -3
  50. package/commands/config.command.js +53 -24
  51. package/commands/diff.command.d.ts +37 -3
  52. package/commands/diff.command.js +25 -16
  53. package/commands/exec.command.d.ts +193 -3
  54. package/commands/exec.command.js +60 -58
  55. package/commands/github-release.command.d.ts +154 -3
  56. package/commands/github-release.command.js +67 -37
  57. package/commands/import.command.d.ts +36 -3
  58. package/commands/import.command.js +28 -20
  59. package/commands/info.command.d.ts +35 -7
  60. package/commands/info.command.js +36 -30
  61. package/commands/lint.command.d.ts +176 -0
  62. package/commands/lint.command.js +34 -0
  63. package/commands/list.command.d.ts +163 -3
  64. package/commands/list.command.js +109 -71
  65. package/commands/publish.command.d.ts +231 -0
  66. package/commands/publish.command.js +304 -0
  67. package/commands/run.command.d.ts +186 -6
  68. package/commands/run.command.js +26 -72
  69. package/commands/test.command.d.ts +173 -3
  70. package/commands/test.command.js +16 -10
  71. package/commands/version.command.d.ts +330 -3
  72. package/commands/version.command.js +292 -78
  73. package/commands.d.ts +32 -0
  74. package/commands.js +28 -0
  75. package/constants.js +1 -1
  76. package/core/application.d.ts +116 -0
  77. package/core/application.js +160 -0
  78. package/core/classes/package.d.ts +219 -0
  79. package/core/classes/package.js +214 -0
  80. package/core/classes/registry.d.ts +30 -0
  81. package/core/classes/registry.js +47 -0
  82. package/core/{repository.d.ts → classes/repository.d.ts} +69 -20
  83. package/core/{repository.js → classes/repository.js} +195 -75
  84. package/core/classes/service.d.ts +49 -0
  85. package/core/classes/service.js +40 -0
  86. package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +23 -1
  87. package/core/{version-scheme.js → classes/version-scheme.js} +29 -1
  88. package/core/classes/workspace.d.ts +275 -0
  89. package/core/classes/workspace.js +537 -0
  90. package/core/command-builder.d.ts +14 -0
  91. package/core/command-builder.js +94 -0
  92. package/core/config/config-file-scope.d.ts +107 -0
  93. package/core/config/config-file-scope.js +226 -0
  94. package/core/config/config-interpolator.d.ts +164 -0
  95. package/core/config/config-interpolator.js +577 -0
  96. package/core/config/config-paths.d.ts +65 -0
  97. package/core/config/config-paths.js +78 -0
  98. package/core/config/config-reader.d.ts +192 -0
  99. package/core/config/config-reader.js +727 -0
  100. package/core/{merge-config.d.ts → config/merge-config.d.ts} +33 -34
  101. package/core/config/merge-config.js +260 -0
  102. package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +1 -1
  103. package/core/{resolve-target.js → config/resolve-config-target.js} +1 -1
  104. package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +46 -10
  105. package/core/{custom-command.js → interfaces/custom-command.js} +44 -17
  106. package/core/{manifest.d.ts → interfaces/manifest.d.ts} +49 -27
  107. package/core/interfaces/manifest.js +132 -0
  108. package/core/interfaces/plugin.d.ts +174 -0
  109. package/core/interfaces/plugin.js +124 -0
  110. package/core/interfaces/publish-target.d.ts +136 -0
  111. package/core/interfaces/publish-target.js +43 -0
  112. package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
  113. package/core/interfaces/run-step.js +1 -0
  114. package/core/plugin-loader.d.ts +25 -0
  115. package/core/plugin-loader.js +53 -0
  116. package/index.d.ts +122 -24
  117. package/index.js +66 -16
  118. package/interfaces/config-scope.interface.d.ts +223 -0
  119. package/interfaces/config-scope.interface.js +1 -0
  120. package/interfaces/rman-config.interface.d.ts +821 -203
  121. package/interfaces/rman-config.interface.js +78 -1
  122. package/package.json +3 -1
  123. package/services/change-hash.service.d.ts +42 -8
  124. package/services/change-hash.service.js +92 -13
  125. package/services/changelog.service.d.ts +91 -52
  126. package/services/changelog.service.js +460 -74
  127. package/services/config-service.d.ts +8 -0
  128. package/services/config-service.js +3 -0
  129. package/services/exec.service.d.ts +23 -12
  130. package/services/exec.service.js +14 -9
  131. package/services/github-release.service.d.ts +45 -34
  132. package/services/github-release.service.js +15 -12
  133. package/services/import.service.d.ts +25 -14
  134. package/services/import.service.js +9 -5
  135. package/services/list.service.d.ts +67 -13
  136. package/services/list.service.js +62 -15
  137. package/services/run.service.d.ts +35 -22
  138. package/services/run.service.js +316 -233
  139. package/services/system-info.d.ts +1 -1
  140. package/services/version-plan.service.d.ts +35 -8
  141. package/services/version-plan.service.js +96 -36
  142. package/services/version.service.d.ts +34 -14
  143. package/services/version.service.js +35 -16
  144. package/services.d.ts +1 -1
  145. package/services.js +1 -1
  146. package/utils/bin-path.d.ts +6 -7
  147. package/utils/bin-path.js +7 -18
  148. package/utils/branch-guard.d.ts +30 -1
  149. package/utils/branch-guard.js +31 -0
  150. package/utils/color-yaml.d.ts +8 -0
  151. package/utils/color-yaml.js +86 -0
  152. package/utils/exec.d.ts +10 -0
  153. package/utils/exec.js +1 -1
  154. package/utils/git.d.ts +26 -0
  155. package/utils/git.js +65 -0
  156. package/utils/logger.d.ts +2 -2
  157. package/utils/logger.js +1 -1
  158. package/utils/package-filter.d.ts +135 -8
  159. package/utils/package-filter.js +204 -16
  160. package/utils/printable-config.d.ts +9 -1
  161. package/utils/printable-config.js +46 -1
  162. package/utils/progress-panel.d.ts +9 -0
  163. package/utils/progress-panel.js +23 -1
  164. package/utils/release-version.d.ts +1 -1
  165. package/utils/run-bin.d.ts +10 -0
  166. package/utils/run-bin.js +1 -1
  167. package/utils/run-options.d.ts +97 -0
  168. package/utils/run-options.js +81 -0
  169. package/utils/version-group.d.ts +31 -0
  170. package/utils/version-group.js +43 -0
  171. package/utils/version-stamp.d.ts +1 -1
  172. package/utils/version-stamp.js +1 -1
  173. package/commands/changed.command.d.ts +0 -3
  174. package/commands/changed.command.js +0 -29
  175. package/core/config.d.ts +0 -409
  176. package/core/config.js +0 -939
  177. package/core/extends-config.d.ts +0 -27
  178. package/core/extends-config.js +0 -89
  179. package/core/manifest.js +0 -150
  180. package/core/merge-config.js +0 -219
  181. package/core/package.d.ts +0 -83
  182. package/core/package.js +0 -106
  183. package/core/plugin.d.ts +0 -112
  184. package/core/plugin.js +0 -189
  185. package/core/workspace.d.ts +0 -68
  186. package/core/workspace.js +0 -83
  187. /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Prepares the `package.json` that `npm publish` will actually read, and returns a function that
3
+ * undoes it - always call that from a `finally`, so a failed publish leaves nothing behind.
4
+ *
5
+ * Which file that is depends on where the output lives:
6
+ *
7
+ * - **Publishing the package directory itself** - its own `package.json` is the manifest, rewritten
8
+ * in place: every `"workspace:"` range becomes a real, registry-consumable one (the same
9
+ * substitution pnpm/yarn's own publish performs - see `resolveWorkspaceRange`), since `npm
10
+ * publish` reads what is on disk rather than packing from a staging tarball.
11
+ * - **Publishing a build directory** - there is no manifest there until something writes one, and
12
+ * that something is this: see `derivePublishManifest`.
13
+ *
14
+ * Returns `undefined` when there is nothing to do at all (the package directory, with no
15
+ * `"workspace:"` range in it) - no disk write, nothing to restore.
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { exec } from '../../../../utils/exec.js';
20
+ import { GitHelper } from '../../../../utils/git.js';
21
+ import { filterPackages } from '../../../../utils/package-filter.js';
22
+ import { isCalendarVersion } from '../../../../utils/release-version.js';
23
+ import { npmViewPackage } from '../../../publish-targets/npm/npm-view.js';
24
+ import { DEPENDENCY_KEYS } from '../node-manifest.provider.js';
25
+ import { parseWorkspaceRange, resolveWorkspaceRange } from '../utils/workspace-range.js';
26
+ import { CiService } from './ci.service.js';
27
+ function preparePublishManifest(pkg, publishDir, packagesByName) {
28
+ if (path.resolve(publishDir) !== path.resolve(pkg.dirname)) {
29
+ const file = path.join(publishDir, 'package.json');
30
+ const previous = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : undefined;
31
+ fs.mkdirSync(publishDir, { recursive: true });
32
+ fs.writeFileSync(file, JSON.stringify(derivePublishManifest(pkg, packagesByName), undefined, 2) + '\n', 'utf-8');
33
+ return () => {
34
+ if (previous === undefined)
35
+ fs.rmSync(file, { force: true });
36
+ else
37
+ fs.writeFileSync(file, previous, 'utf-8');
38
+ };
39
+ }
40
+ const hasWorkspaceRange = DEPENDENCY_KEYS.some(depKey => {
41
+ const deps = pkg.manifest.raw[depKey];
42
+ return deps && Object.values(deps).some(v => parseWorkspaceRange(v));
43
+ });
44
+ if (!hasWorkspaceRange)
45
+ return undefined;
46
+ /** The file's exact bytes, not a re-serialization: this is restored verbatim afterwards, so a
47
+ * round-trip through `JSON.stringify` would rewrite the author's formatting as a side effect of
48
+ * publishing. */
49
+ const original = fs.readFileSync(pkg.manifestFileName, 'utf-8');
50
+ resolveWorkspaceRanges(pkg.manifest.raw, packagesByName);
51
+ pkg.writeManifest();
52
+ return () => {
53
+ fs.writeFileSync(pkg.manifestFileName, original, 'utf-8');
54
+ pkg.reloadManifest();
55
+ };
56
+ }
57
+ /**
58
+ * The manifest to publish from a build directory, derived from the package's own - generated here
59
+ * rather than by a build script, and deliberately with nothing to configure.
60
+ *
61
+ * Generated at *publish* time, which is the whole point: a build script writes it when the build
62
+ * runs, so bumping the version afterwards (or building before a bump) publishes a manifest that
63
+ * disagrees with the package - and the `"workspace:"` rewrite above, which only ever touched the
64
+ * package's own file, never reached the copy at all.
65
+ *
66
+ * What comes out is the package's `package.json` minus what a consumer of the tarball can neither
67
+ * see nor use:
68
+ *
69
+ * - `devDependencies` - npm never installs a dependency's own, so they are pure noise.
70
+ * - `scripts`, **except** `preinstall`/`install`/`postinstall`. Those three are the only ones a
71
+ * consumer's install actually runs, and dropping them would silently break every package that
72
+ * builds a native module on install. The rest (`build`, `test`, `prepare`, ...) never reach the
73
+ * consumer - `prepare` runs for a git dependency, which builds from the repository, not from this
74
+ * tarball.
75
+ * - `private` - rman refuses to publish a private package in the first place, so carrying the flag
76
+ * into a manifest that is being published can only be wrong.
77
+ * - `publishConfig.directory` - it pointed *here*; kept, it would point one level deeper again.
78
+ */
79
+ function derivePublishManifest(pkg, packagesByName) {
80
+ const json = structuredClone(pkg.manifest.raw);
81
+ resolveWorkspaceRanges(json, packagesByName);
82
+ delete json.devDependencies;
83
+ delete json.private;
84
+ if (json.scripts) {
85
+ const kept = Object.fromEntries(Object.entries(json.scripts).filter(([name]) => CONSUMER_SCRIPTS.has(name)));
86
+ if (Object.keys(kept).length)
87
+ json.scripts = kept;
88
+ else
89
+ delete json.scripts;
90
+ }
91
+ if (json.publishConfig) {
92
+ delete json.publishConfig.directory;
93
+ if (!Object.keys(json.publishConfig).length)
94
+ delete json.publishConfig;
95
+ }
96
+ return json;
97
+ }
98
+ /** The only lifecycle scripts a consumer's `npm install` of this package runs - see
99
+ * https://docs.npmjs.com/cli/using-npm/scripts. */
100
+ const CONSUMER_SCRIPTS = new Set(['preinstall', 'install', 'postinstall']);
101
+ /** Rewrites `json`'s `"workspace:"` ranges in place, resolving each against the in-repo package it
102
+ * names. Shared by both manifest paths, so they can never disagree about the substitution. */
103
+ function resolveWorkspaceRanges(json, packagesByName) {
104
+ for (const depKey of DEPENDENCY_KEYS) {
105
+ const deps = json[depKey];
106
+ if (!deps)
107
+ continue;
108
+ for (const depName of Object.keys(deps)) {
109
+ const parsed = parseWorkspaceRange(deps[depName]);
110
+ if (!parsed)
111
+ continue;
112
+ const depPkg = packagesByName.get(depName);
113
+ if (depPkg)
114
+ deps[depName] = resolveWorkspaceRange(parsed, depPkg.version);
115
+ }
116
+ }
117
+ }
118
+ export var PublishService;
119
+ (function (PublishService) {
120
+ /**
121
+ * Computes what `publish` *would* do, across every non-private package (topological order,
122
+ * dependencies before dependents) - never touches the registry to publish anything, just
123
+ * queries it to decide, so it's safe to call any time, including as the plan a bare `rman
124
+ * publish` shows before asking for confirmation.
125
+ *
126
+ * A `private: true` package is always `'skip'`ped outright. Each remaining package's **dist-tag**
127
+ * is settled next (`distTagFor`): usually derived from the version itself, `'error'` in the two
128
+ * cases with nothing to derive - that one is about the invocation rather than the package, so it
129
+ * is answered before anything touches the network. A package with uncommitted local changes is
130
+ * `'error'` too (aborts the whole plan) unless `options.ignoreDirty` downgrades it to `'skip'`
131
+ * instead - same rule `version` uses, since publishing untracked local edits is worse than a bad
132
+ * commit.
133
+ *
134
+ * Otherwise the registry decides, via one `npmViewPackage` per remaining package (run
135
+ * concurrently): **the local version being among the published `versions`** is `'up-to-date'`,
136
+ * anything else - including never having been published at all - is `'publish'`. Deliberately not
137
+ * a comparison against `latest`, which answers a different question and disagrees with this one
138
+ * the moment a prerelease ships under its own dist-tag; see `npmViewPackage`.
139
+ *
140
+ * Deliberately decoupled from `version`: this only ever looks at what's *currently* on disk and
141
+ * on the registry, never at whether `version` was just run - so it works equally well right
142
+ * after a version bump, or standing alone in a release pipeline that bumped days earlier.
143
+ *
144
+ * A monorepo's root package is never a candidate at all - `repository.getPackages()` already
145
+ * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
146
+ * where it's a normal candidate like any other).
147
+ */
148
+ async function getPlan(repository, options = {}, deps = {}) {
149
+ const git = new GitHelper({ cwd: repository.dirname });
150
+ const packages = filterPackages(repository.getPackages({ toposort: true }), options);
151
+ const dirtyFiles = await git.listDirtyFiles({ absolute: true });
152
+ const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
153
+ const viewPackage = deps.npmViewPackage ?? ((name, cwd) => npmViewPackage(name, cwd, options));
154
+ const entries = new Map();
155
+ const toCheck = [];
156
+ for (const pkg of packages) {
157
+ /** Asked before the registry is, because it is about the *invocation* rather than the
158
+ * package: a prerelease heading for `latest` is wrong whatever the registry says, and
159
+ * finding that out after a round trip per package would be a slower way to the same
160
+ * refusal. */
161
+ const distTag = distTagFor(pkg, options.tag);
162
+ if (retiredDirectoryKey(pkg)) {
163
+ entries.set(pkg.name, {
164
+ package: pkg,
165
+ version: pkg.version,
166
+ status: 'error',
167
+ reason: '.rmanrc "publish.directory" is now "publish.npm.directory" - see the npm publish target',
168
+ });
169
+ }
170
+ else if (pkg.config.publish?.skip) {
171
+ entries.set(pkg.name, {
172
+ package: pkg,
173
+ version: pkg.version,
174
+ status: 'skip',
175
+ reason: 'excluded via .rmanrc "publish.skip"',
176
+ });
177
+ }
178
+ else if (pkg.isPrivate) {
179
+ entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
180
+ }
181
+ else if (distTag.error) {
182
+ entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'error', reason: distTag.error });
183
+ }
184
+ else if (isDirty(pkg)) {
185
+ entries.set(pkg.name, {
186
+ package: pkg,
187
+ version: pkg.version,
188
+ status: options.ignoreDirty ? 'skip' : 'error',
189
+ reason: 'uncommitted local changes',
190
+ });
191
+ }
192
+ else {
193
+ toCheck.push(pkg);
194
+ }
195
+ }
196
+ await Promise.all(toCheck.map(async (pkg) => {
197
+ const view = await viewPackage(pkg.name, pkg.dirname);
198
+ const registryVersion = view?.latest;
199
+ /** **This version**, not `latest` - the two part company the moment a prerelease is
200
+ * published under its own dist-tag, and `latest` then never moves however many betas go
201
+ * out. Asking it kept proposing an already-published version until npm answered 403. */
202
+ const published = !!view?.versions.includes(pkg.version);
203
+ const distTag = distTagFor(pkg, options.tag).tag;
204
+ entries.set(pkg.name, {
205
+ package: pkg,
206
+ version: pkg.version,
207
+ registryVersion,
208
+ distTag,
209
+ /** Printed beside the package in the plan, so where a version is going is something the
210
+ * reader confirms rather than something they have to infer from the version string. */
211
+ detail: distTag ? `-> dist-tag "${distTag}"` : undefined,
212
+ status: published ? 'up-to-date' : 'publish',
213
+ reason: published
214
+ ? `registry already has ${pkg.version}`
215
+ : registryVersion
216
+ ? `registry has ${registryVersion}`
217
+ : 'never published',
218
+ });
219
+ }));
220
+ return packages.map(pkg => entries.get(pkg.name));
221
+ }
222
+ PublishService.getPlan = getPlan;
223
+ /**
224
+ * Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
225
+ * see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
226
+ * concurrent: unlike `run`/`build`, a package genuinely needs its own dependencies to have
227
+ * landed on the registry first, and publishing is rare enough (once per release) that the
228
+ * simplicity is worth more than the parallelism `run` gets from `power-tasks`.
229
+ *
230
+ * If a package fails, every other still-pending entry depending on it (transitively) is marked
231
+ * `'error'` too and never attempted - publishing a package whose own new dependency range points
232
+ * at a version that never actually reached the registry would hand consumers a broken install.
233
+ * A package's own failure doesn't stop unrelated packages elsewhere in the plan, though.
234
+ */
235
+ async function applyPlan(repository, plan, options = {}) {
236
+ const packageManager = CiService.resolvePackageManager(repository, options.packageManager);
237
+ const failed = new Set();
238
+ const result = [];
239
+ /** The tuple is explicit: `[p.name, p]` widens to `(string | Package)[]`, and whether `new Map`
240
+ * still infers `Map<string, Package>` from that came down to which declaration of
241
+ * `getPackages()` was in scope - the source one inferred it, the built `.d.ts` did not
242
+ * (measured, once the tests started type-checking). */
243
+ const packagesByName = new Map(repository.getPackages().map(p => [p.name, p]));
244
+ for (const entry of plan) {
245
+ if (entry.status !== 'publish') {
246
+ result.push(entry);
247
+ continue;
248
+ }
249
+ const pkg = entry.package;
250
+ const blocker = pkg.dependencies.find(d => failed.has(d.name));
251
+ if (blocker) {
252
+ failed.add(pkg.name);
253
+ result.push({ ...entry, status: 'error', reason: `dependency "${blocker.name}" failed to publish` });
254
+ continue;
255
+ }
256
+ const publishDir = resolvePublishDir(pkg, options.contents);
257
+ const restore = preparePublishManifest(pkg, publishDir, packagesByName);
258
+ try {
259
+ /** The plan's own tag, not a recomputed one: `getPlan` decided where this package goes,
260
+ * the reader confirmed that, and `applyPlan` is here to carry it out. `options.tag` is
261
+ * the fallback only for a plan built by something other than `getPlan`. */
262
+ await exec(buildPublishCommand(packageManager, { ...options, tag: entry.distTag ?? options.tag }), {
263
+ cwd: publishDir,
264
+ app: pkg.repository.app,
265
+ stdio: 'inherit',
266
+ });
267
+ result.push(entry);
268
+ }
269
+ catch (e) {
270
+ failed.add(pkg.name);
271
+ result.push({ ...entry, status: 'error', reason: e.message });
272
+ }
273
+ finally {
274
+ restore?.();
275
+ }
276
+ }
277
+ return result;
278
+ }
279
+ PublishService.applyPlan = applyPlan;
280
+ })(PublishService || (PublishService = {}));
281
+ /** Where the publishable output lives, most specific statement first: the package's own
282
+ * `publishConfig.directory` (npm/pnpm's native spelling, and a statement about that one package),
283
+ * then `.rmanrc "publish.npm.directory"` (which a `"[*]"` block can say once for a whole repository
284
+ * instead of repeating in every `package.json`), then `--contents` for a single run. */
285
+ function resolvePublishDir(pkg, contentsOverride) {
286
+ const native = pkg.manifest.raw.publishConfig?.directory;
287
+ const configured = pkg.config?.publish?.npm?.directory;
288
+ const rel = (typeof native === 'string' && native) || configured || contentsOverride;
289
+ return rel ? path.resolve(pkg.dirname, rel) : pkg.dirname;
290
+ }
291
+ /**
292
+ * The retired `publish.directory` spelling, if a config still carries it.
293
+ *
294
+ * **Caught and refused, never ignored**, and the reason is what ignoring it would do: the key would
295
+ * silently stop being read, `resolvePublishDir` would fall back to the package's own directory, and
296
+ * the run would publish the *source tree* to npm instead of the build output. A rename that fails
297
+ * loudly costs one error message; one that goes quiet ships TypeScript sources to the registry.
298
+ *
299
+ * Checked in `getPlan` rather than at the write, for the same reason `version` validates its stamp
300
+ * list first: a configuration mistake has to surface before anything is published, and an `'error'`
301
+ * entry aborts the whole plan.
302
+ *
303
+ * YAML and JSON configs are unchecked at author time - there is no JSON Schema any more - so this is
304
+ * the only thing standing between the old spelling and a wrong publish.
305
+ */
306
+ function retiredDirectoryKey(pkg) {
307
+ return pkg.config?.publish?.directory !== undefined;
308
+ }
309
+ /**
310
+ * Which dist-tag a package publishes under, or why the plan must refuse to publish it at all.
311
+ *
312
+ * **A prerelease names its own tag, so rman uses it**: `2.0.0-beta.1` goes to `beta`. That is a
313
+ * reading rather than a guess - the identifier is written in the version - and it is the whole
314
+ * point, because `npm publish` with no `--tag` writes **`latest`**. A beta that lands there is what
315
+ * every plain `npm install <name>` resolves to from then on; npm is content to point `latest` at a
316
+ * prerelease, and `npm dist-tag` can move it back only after everyone who installed in between
317
+ * already has the beta.
318
+ *
319
+ * **It is recorded in the plan rather than applied silently.** The derived tag becomes the entry's
320
+ * `distTag` and is printed beside the package, so where a version is going is something the reader
321
+ * confirms. Deriving it and saying nothing would be the same class of mistake in the other
322
+ * direction - the wrong tag is recoverable (`npm dist-tag add`), but only by someone who noticed.
323
+ *
324
+ * Two cases still refuse, because there is no honest answer to derive:
325
+ *
326
+ * - **an explicit `--tag latest` on a prerelease.** Someone who typed it is far likelier to have
327
+ * confused themselves than to mean it, and the escape hatch for genuinely meaning it - a bare
328
+ * `npm publish --tag latest` - is one command away.
329
+ * - **a prerelease with no identifier to name** (`2.0.0-1`, whose prerelease part is numeric).
330
+ * `VersionScheme.prereleaseId` answers `undefined` there rather than inventing a tag called `1`.
331
+ *
332
+ * **Both questions are the scheme's, not semver's** - `isPrerelease` and `prereleaseId` - and a
333
+ * **calendar version has to be ruled out first**: `2026.9.15-1430` carries a semver prerelease
334
+ * identifier by construction, because that is how the time is spelled, and it says nothing about
335
+ * the release being a preview. `github-release`'s own `resolvePrerelease` makes the same pair of
336
+ * checks; they agree deliberately.
337
+ */
338
+ function distTagFor(pkg, tag) {
339
+ const isPreview = !isCalendarVersion(pkg.version) && pkg.versionScheme.isPrerelease(pkg.version);
340
+ if (!isPreview)
341
+ return { tag };
342
+ if (tag) {
343
+ if (tag !== 'latest')
344
+ return { tag };
345
+ return {
346
+ error: `${pkg.version} is a prerelease, so --tag latest would make it what every plain ` +
347
+ 'install resolves to - drop the flag to publish it under its own identifier instead',
348
+ };
349
+ }
350
+ const derived = pkg.versionScheme.prereleaseId(pkg.version);
351
+ if (derived)
352
+ return { tag: derived };
353
+ return {
354
+ error: `${pkg.version} is a prerelease with no identifier to name a dist-tag after, and with ` +
355
+ 'no --tag npm would put it on "latest" - pass --tag <name>',
356
+ };
357
+ }
358
+ function buildPublishCommand(packageManager, options) {
359
+ const args = ['publish'];
360
+ if (options.access)
361
+ args.push('--access', options.access);
362
+ if (options.tag)
363
+ args.push('--tag', options.tag);
364
+ if (options.otp)
365
+ args.push('--otp', options.otp);
366
+ if (options.registry)
367
+ args.push('--registry', options.registry);
368
+ if (options.userconfig)
369
+ args.push('--userconfig', options.userconfig);
370
+ return `${packageManager} ${args.join(' ')}`;
371
+ }
@@ -0,0 +1,44 @@
1
+ import type { Package } from '../../../../core/classes/package.js';
2
+ import { VersionPlanService } from '../../../../services/version-plan.service.js';
3
+ import type { GitHelper } from '../../../../utils/git.js';
4
+ /**
5
+ * How a release is planned for an npm repository.
6
+ *
7
+ * rman's core holds the parts that are true of any repository - groups, conventional-commit
8
+ * severities, the cascade mechanics, the monorepo root's release identity - and leaves two decisions
9
+ * abstract because they are statements about an *ecosystem* rather than about releases. This is
10
+ * npm's pair of answers.
11
+ *
12
+ * Registered through the plugin's `versionPlanner`, so `rman version`/`rman changed` work in a
13
+ * repository that registered the `node` built-in and say what is missing in one that did not.
14
+ */
15
+ export declare class NodeVersionPlanService extends VersionPlanService {
16
+ /**
17
+ * The shared `ChangeHashService.detect`, unmodified: this package's own latest release tag, and failing
18
+ * that whatever `Plugin.publishedVersion` reports, mapped back onto a tag name.
19
+ *
20
+ * **Nothing npm-specific is passed in any more**, and that is the point of the seam moving: the
21
+ * registry lookup is `packageJsonManifest.publishedVersion`'s now, dispatched per package, so this
22
+ * override exists only because `detectBoundary` is abstract. If the core ever makes it a concrete
23
+ * default, this method can go entirely.
24
+ */
25
+ protected detectBoundary(git: GitHelper, pkg: Package): Promise<string | undefined>;
26
+ /**
27
+ * npm's dependency ranges, read as a release policy. What decides each case is whether a
28
+ * dependent's **range floor** has to move for a consumer to get a correct install:
29
+ *
30
+ * - **patch** - only the changed packages. A dependent's `^1.2.0` already resolves to `1.2.1`,
31
+ * and a patch adds nothing for the dependent to require, so nothing downstream has to ship for
32
+ * consumers to receive it.
33
+ * - **minor** - also every transitive in-group dependent. A minor adds API; a dependent that uses
34
+ * it is only correct once its own published range requires the new floor, and a range lives in
35
+ * a manifest, which only a release puts on the registry.
36
+ * - **major** - the whole group, changed or not. A breaking change restates every member's
37
+ * compatibility, including the members that merely point at one.
38
+ *
39
+ * None of this is about versions, which is why it is not in the core: an ecosystem that pins exact
40
+ * versions instead has to release every dependent for a patch as well, and one that resolves
41
+ * dependencies from source may not need a release for any of it.
42
+ */
43
+ protected cascade(bump: string): VersionPlanService.Cascade;
44
+ }
@@ -0,0 +1,58 @@
1
+ import { ChangeHashService } from '../../../../services/change-hash.service.js';
2
+ import { VersionPlanService } from '../../../../services/version-plan.service.js';
3
+ /**
4
+ * How a release is planned for an npm repository.
5
+ *
6
+ * rman's core holds the parts that are true of any repository - groups, conventional-commit
7
+ * severities, the cascade mechanics, the monorepo root's release identity - and leaves two decisions
8
+ * abstract because they are statements about an *ecosystem* rather than about releases. This is
9
+ * npm's pair of answers.
10
+ *
11
+ * Registered through the plugin's `versionPlanner`, so `rman version`/`rman changed` work in a
12
+ * repository that registered the `node` built-in and say what is missing in one that did not.
13
+ */
14
+ export class NodeVersionPlanService extends VersionPlanService {
15
+ /**
16
+ * The shared `ChangeHashService.detect`, unmodified: this package's own latest release tag, and failing
17
+ * that whatever `Plugin.publishedVersion` reports, mapped back onto a tag name.
18
+ *
19
+ * **Nothing npm-specific is passed in any more**, and that is the point of the seam moving: the
20
+ * registry lookup is `packageJsonManifest.publishedVersion`'s now, dispatched per package, so this
21
+ * override exists only because `detectBoundary` is abstract. If the core ever makes it a concrete
22
+ * default, this method can go entirely.
23
+ */
24
+ detectBoundary(git, pkg) {
25
+ return ChangeHashService.detect(git, pkg);
26
+ }
27
+ /**
28
+ * npm's dependency ranges, read as a release policy. What decides each case is whether a
29
+ * dependent's **range floor** has to move for a consumer to get a correct install:
30
+ *
31
+ * - **patch** - only the changed packages. A dependent's `^1.2.0` already resolves to `1.2.1`,
32
+ * and a patch adds nothing for the dependent to require, so nothing downstream has to ship for
33
+ * consumers to receive it.
34
+ * - **minor** - also every transitive in-group dependent. A minor adds API; a dependent that uses
35
+ * it is only correct once its own published range requires the new floor, and a range lives in
36
+ * a manifest, which only a release puts on the registry.
37
+ * - **major** - the whole group, changed or not. A breaking change restates every member's
38
+ * compatibility, including the members that merely point at one.
39
+ *
40
+ * None of this is about versions, which is why it is not in the core: an ecosystem that pins exact
41
+ * versions instead has to release every dependent for a patch as well, and one that resolves
42
+ * dependencies from source may not need a release for any of it.
43
+ */
44
+ cascade(bump) {
45
+ /** Only semver's three can arrive - `getPlan` validates against `bumpNames` and
46
+ * `packageJsonManifest` leaves the scheme at the semver default. The fallback is for a scheme
47
+ * someone swaps in underneath this planner, and it over-reaches on purpose: releasing a package
48
+ * that did not need it is noise, while under-reaching ships a dependent whose published range
49
+ * floor is wrong, which is a broken install. */
50
+ return CASCADE_BY_BUMP[bump] ?? 'group';
51
+ }
52
+ }
53
+ /** semver's bump names against how far each has to reach - see `NodeVersionPlanService.cascade`. */
54
+ const CASCADE_BY_BUMP = {
55
+ patch: 'changed',
56
+ minor: 'dependents',
57
+ major: 'group',
58
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The `"workspace:"` dependency protocol - pnpm/yarn's spelling for "this dependency is a sibling in
3
+ * this repository", which npm's own workspaces understand too.
4
+ *
5
+ * **The `node` built-in's, because it is a statement about a `package.json` dependency field.** It sat in
6
+ * rman's core with a comment admitting it was only there because `publish` needed it from out here -
7
+ * and now `publish`, the manifest provider and the dependency-range rewrite all live in this
8
+ * package, so nothing in the core ever looked at it.
9
+ */
10
+ export interface ParsedWorkspaceRange {
11
+ /** `'*'`/`'^'`/`'~'` for the bare selector forms; `'explicit'` when the protocol is followed by
12
+ * a concrete semver version/range instead (e.g. `"workspace:^1.0.0"`, `"workspace:1.0.0"`). */
13
+ selector: '*' | '^' | '~' | 'explicit';
14
+ /** Only set when `selector === 'explicit'` - the literal range following `"workspace:"`. */
15
+ range?: string;
16
+ }
17
+ /** Parses a dependency range value for the pnpm/yarn `"workspace:"` protocol - `undefined` when
18
+ * `value` isn't a workspace range at all (a plain semver range, or not a string). */
19
+ export declare function parseWorkspaceRange(value: unknown): ParsedWorkspaceRange | undefined;
20
+ /**
21
+ * Resolves a parsed workspace range against `version` (the dependency's actual current version)
22
+ * into the real range a registry consumer would need - the same substitution pnpm/yarn's own
23
+ * publish performs: `"*"` pins the exact version (no operator), `"^"`/`"~"` prepend themselves to
24
+ * it, and an explicit range is used verbatim (it was already a real range, just workspace-prefixed).
25
+ */
26
+ export declare function resolveWorkspaceRange(parsed: ParsedWorkspaceRange, version: string): string;
@@ -0,0 +1,28 @@
1
+ /** Parses a dependency range value for the pnpm/yarn `"workspace:"` protocol - `undefined` when
2
+ * `value` isn't a workspace range at all (a plain semver range, or not a string). */
3
+ export function parseWorkspaceRange(value) {
4
+ if (typeof value !== 'string' || !value.startsWith('workspace:'))
5
+ return undefined;
6
+ const rest = value.slice('workspace:'.length);
7
+ if (rest === '*' || rest === '^' || rest === '~')
8
+ return { selector: rest };
9
+ return { selector: 'explicit', range: rest };
10
+ }
11
+ /**
12
+ * Resolves a parsed workspace range against `version` (the dependency's actual current version)
13
+ * into the real range a registry consumer would need - the same substitution pnpm/yarn's own
14
+ * publish performs: `"*"` pins the exact version (no operator), `"^"`/`"~"` prepend themselves to
15
+ * it, and an explicit range is used verbatim (it was already a real range, just workspace-prefixed).
16
+ */
17
+ export function resolveWorkspaceRange(parsed, version) {
18
+ switch (parsed.selector) {
19
+ case '*':
20
+ return version;
21
+ case '^':
22
+ return `^${version}`;
23
+ case '~':
24
+ return `~${version}`;
25
+ case 'explicit':
26
+ return parsed.range;
27
+ }
28
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * **Where the presets are.** `ConfigReader` resolves `extends: "rman:<name>"` against this.
3
+ */
4
+ export declare const PRESETS_DIR: string;
5
+ /** Every preset rman ships, read off the directory rather than written down again. */
6
+ export declare function presetNames(): string[];
7
+ /**
8
+ * **The presets rman ships, laid underneath every repository's own config.**
9
+ *
10
+ * A repository writing nothing at all still gets these, exactly as if its `.rmanrc` had opened with
11
+ * `extends: ['rman:node']` - so `rman list` finds the workspace and `rman clean` exists without a
12
+ * config file having to say so. A repository naming one itself changes nothing: the key appends, and
13
+ * a technology already present by name is skipped.
14
+ */
15
+ export declare const DEFAULT_PRESETS: readonly string[];
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * **Where the presets are.** `ConfigReader` resolves `extends: "rman:<name>"` against this.
6
+ */
7
+ /* **The directory says where it is, rather than the reader computing a path to it.** It was
8
+ * `path.resolve(dirname(config-reader), '..', 'presets')` - two relative hops between two files
9
+ * that nothing keeps in step, and moving either one breaks it silently at *runtime*: measured, the
10
+ * `core/config/` + `builtins/presets/` reorganization left every repository answering
11
+ * `"presets" has no preset "node". rman ships: node.` - a message that named the preset it had
12
+ * just failed to find, because the list behind it was a hardcoded `['node']` somewhere else again.
13
+ * `tsc` cannot see a path built with `path.resolve`, so only a run says. */
14
+ export const PRESETS_DIR = path.dirname(fileURLToPath(import.meta.url));
15
+ /** Every preset rman ships, read off the directory rather than written down again. */
16
+ /* One `readdirSync`, on an error path only - `_resolvePreset` asks after a lookup already failed.
17
+ * Reading the directory is what keeps the message true: the hardcoded list it replaces could say
18
+ * `node` while `node.js` was not where the resolver looked. `index` is this file. */
19
+ export function presetNames() {
20
+ try {
21
+ return [
22
+ ...new Set(fs
23
+ .readdirSync(PRESETS_DIR)
24
+ .filter(f => /\.(js|ts)$/.test(f) && !/\.d\.ts$/.test(f))
25
+ .map(f => f.replace(/\.(js|ts)$/, ''))
26
+ .filter(n => n !== 'index')),
27
+ ].sort();
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ }
33
+ /**
34
+ * **The presets rman ships, laid underneath every repository's own config.**
35
+ *
36
+ * A repository writing nothing at all still gets these, exactly as if its `.rmanrc` had opened with
37
+ * `extends: ['rman:node']` - so `rman list` finds the workspace and `rman clean` exists without a
38
+ * config file having to say so. A repository naming one itself changes nothing: the key appends, and
39
+ * a technology already present by name is skipped.
40
+ */
41
+ /* **Names, not imports, and that is load-bearing rather than tidy.** A preset's module pulls in a
42
+ * platform, its commands and its services, and one of those services extends a core service - so a
43
+ * static import from `core/` closes the cycle that stopped the *built* CLI dead on every command
44
+ * (`Cannot access 'VersionPlanService' before initialization`, invisible to the suite, which is why
45
+ * `npm run smoke` exists). `ConfigReader` resolves these through the same dynamic `import()` an
46
+ * `extends: "rman:node"` goes through, so this file stays a list of strings and imports nothing.
47
+ *
48
+ * **This replaced detection**, which asked each built-in "is this directory yours?" before turning
49
+ * anything on, and merged the matching preset underneath. What that bought was a repository not
50
+ * growing a technology's commands unasked; what it cost was a catalogue module, a `Builtin` type, a
51
+ * per-directory memo and a gate that had to decide what counts as having "said something". The
52
+ * platform answers the same question now - `manifestProvider.read` is what detection asked - and it
53
+ * answers it from the ordinary registry, once loaded. The cost, stated rather than hidden: a
54
+ * repository of some other technology carries node's `clean` and `ci` in `rman --help`, and
55
+ * `rman info` reports npm's tooling. Split this list's presets in two - the platform apart from the
56
+ * commands - if that ever stops being acceptable. */
57
+ export const DEFAULT_PRESETS = ['node'];
@@ -0,0 +1,16 @@
1
+ import type { RmanConfig } from '../../interfaces/rman-config.interface.js';
2
+ /**
3
+ * **The `node` preset** - everything rman knows about the npm ecosystem, as a config.
4
+ *
5
+ * ```yaml
6
+ * # .rmanrc.yml
7
+ * extends: node
8
+ * ```
9
+ *
10
+ * It contributes the technology (how a `package.json` is read, where `workspaces` point, how
11
+ * versions are planned), the two commands that are npm's alone (`clean`, `ci`), and the `npm`
12
+ * publish target. A polyglot repository names several: `extends: ['node', 'cargo']` holds both,
13
+ * because every key here appends.
14
+ */
15
+ export declare function nodePreset(): RmanConfig;
16
+ export default nodePreset;