rman-node 1.1.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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/augmentation/manifest.augmentation.d.ts +17 -0
  4. package/augmentation/manifest.augmentation.js +129 -0
  5. package/augmentation/rman.augmentation.d.ts +45 -0
  6. package/augmentation/rman.augmentation.js +1 -0
  7. package/augmentation/run.augmentation.d.ts +15 -0
  8. package/augmentation/run.augmentation.js +61 -0
  9. package/augmentation/system-info.augmentation.d.ts +26 -0
  10. package/augmentation/system-info.augmentation.js +79 -0
  11. package/augmentation/workspace.augmentation.d.ts +18 -0
  12. package/augmentation/workspace.augmentation.js +49 -0
  13. package/commands/ci.command.d.ts +9 -0
  14. package/commands/ci.command.js +35 -0
  15. package/commands/clean.command.d.ts +6 -0
  16. package/commands/clean.command.js +32 -0
  17. package/commands/publish.command.d.ts +9 -0
  18. package/commands/publish.command.js +228 -0
  19. package/index.d.ts +31 -0
  20. package/index.js +100 -0
  21. package/interfaces/rman-config.interface.d.ts +80 -0
  22. package/interfaces/rman-config.interface.js +7 -0
  23. package/package.json +54 -0
  24. package/services/ci.service.d.ts +38 -0
  25. package/services/ci.service.js +201 -0
  26. package/services/clean.service.d.ts +51 -0
  27. package/services/clean.service.js +235 -0
  28. package/services/publish.service.d.ts +77 -0
  29. package/services/publish.service.js +258 -0
  30. package/services/version-plan.service.d.ts +50 -0
  31. package/services/version-plan.service.js +67 -0
  32. package/utils/npm-run-path.d.ts +21 -0
  33. package/utils/npm-run-path.js +36 -0
  34. package/utils/npm-view.d.ts +15 -0
  35. package/utils/npm-view.js +28 -0
  36. package/utils/workspace-range.d.ts +26 -0
  37. package/utils/workspace-range.js +28 -0
@@ -0,0 +1,258 @@
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, filterPackages, GitHelper } from 'rman';
20
+ import { DEPENDENCY_KEYS } from '../augmentation/manifest.augmentation.js';
21
+ import { npmViewVersion } from '../utils/npm-view.js';
22
+ import { parseWorkspaceRange, resolveWorkspaceRange } from '../utils/workspace-range.js';
23
+ import { CiService } from './ci.service.js';
24
+ function preparePublishManifest(pkg, publishDir, packagesByName) {
25
+ if (path.resolve(publishDir) !== path.resolve(pkg.dirname)) {
26
+ const file = path.join(publishDir, 'package.json');
27
+ const previous = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : undefined;
28
+ fs.mkdirSync(publishDir, { recursive: true });
29
+ fs.writeFileSync(file, JSON.stringify(derivePublishManifest(pkg, packagesByName), undefined, 2) + '\n', 'utf-8');
30
+ return () => {
31
+ if (previous === undefined)
32
+ fs.rmSync(file, { force: true });
33
+ else
34
+ fs.writeFileSync(file, previous, 'utf-8');
35
+ };
36
+ }
37
+ const hasWorkspaceRange = DEPENDENCY_KEYS.some(depKey => {
38
+ const deps = pkg.manifest.raw[depKey];
39
+ return deps && Object.values(deps).some(v => parseWorkspaceRange(v));
40
+ });
41
+ if (!hasWorkspaceRange)
42
+ return undefined;
43
+ /** The file's exact bytes, not a re-serialization: this is restored verbatim afterwards, so a
44
+ * round-trip through `JSON.stringify` would rewrite the author's formatting as a side effect of
45
+ * publishing. */
46
+ const original = fs.readFileSync(pkg.manifestFileName, 'utf-8');
47
+ resolveWorkspaceRanges(pkg.manifest.raw, packagesByName);
48
+ pkg.writeManifest();
49
+ return () => {
50
+ fs.writeFileSync(pkg.manifestFileName, original, 'utf-8');
51
+ pkg.reloadManifest();
52
+ };
53
+ }
54
+ /**
55
+ * The manifest to publish from a build directory, derived from the package's own - generated here
56
+ * rather than by a build script, and deliberately with nothing to configure.
57
+ *
58
+ * Generated at *publish* time, which is the whole point: a build script writes it when the build
59
+ * runs, so bumping the version afterwards (or building before a bump) publishes a manifest that
60
+ * disagrees with the package - and the `"workspace:"` rewrite above, which only ever touched the
61
+ * package's own file, never reached the copy at all.
62
+ *
63
+ * What comes out is the package's `package.json` minus what a consumer of the tarball can neither
64
+ * see nor use:
65
+ *
66
+ * - `devDependencies` - npm never installs a dependency's own, so they are pure noise.
67
+ * - `scripts`, **except** `preinstall`/`install`/`postinstall`. Those three are the only ones a
68
+ * consumer's install actually runs, and dropping them would silently break every package that
69
+ * builds a native module on install. The rest (`build`, `test`, `prepare`, ...) never reach the
70
+ * consumer - `prepare` runs for a git dependency, which builds from the repository, not from this
71
+ * tarball.
72
+ * - `private` - rman refuses to publish a private package in the first place, so carrying the flag
73
+ * into a manifest that is being published can only be wrong.
74
+ * - `publishConfig.directory` - it pointed *here*; kept, it would point one level deeper again.
75
+ */
76
+ function derivePublishManifest(pkg, packagesByName) {
77
+ const json = structuredClone(pkg.manifest.raw);
78
+ resolveWorkspaceRanges(json, packagesByName);
79
+ delete json.devDependencies;
80
+ delete json.private;
81
+ if (json.scripts) {
82
+ const kept = Object.fromEntries(Object.entries(json.scripts).filter(([name]) => CONSUMER_SCRIPTS.has(name)));
83
+ if (Object.keys(kept).length)
84
+ json.scripts = kept;
85
+ else
86
+ delete json.scripts;
87
+ }
88
+ if (json.publishConfig) {
89
+ delete json.publishConfig.directory;
90
+ if (!Object.keys(json.publishConfig).length)
91
+ delete json.publishConfig;
92
+ }
93
+ return json;
94
+ }
95
+ /** The only lifecycle scripts a consumer's `npm install` of this package runs - see
96
+ * https://docs.npmjs.com/cli/using-npm/scripts. */
97
+ const CONSUMER_SCRIPTS = new Set(['preinstall', 'install', 'postinstall']);
98
+ /** Rewrites `json`'s `"workspace:"` ranges in place, resolving each against the in-repo package it
99
+ * names. Shared by both manifest paths, so they can never disagree about the substitution. */
100
+ function resolveWorkspaceRanges(json, packagesByName) {
101
+ for (const depKey of DEPENDENCY_KEYS) {
102
+ const deps = json[depKey];
103
+ if (!deps)
104
+ continue;
105
+ for (const depName of Object.keys(deps)) {
106
+ const parsed = parseWorkspaceRange(deps[depName]);
107
+ if (!parsed)
108
+ continue;
109
+ const depPkg = packagesByName.get(depName);
110
+ if (depPkg)
111
+ deps[depName] = resolveWorkspaceRange(parsed, depPkg.version);
112
+ }
113
+ }
114
+ }
115
+ export var PublishService;
116
+ (function (PublishService) {
117
+ /**
118
+ * Computes what `publish` *would* do, across every non-private package (topological order,
119
+ * dependencies before dependents) - never touches the registry to publish anything, just
120
+ * queries it to decide, so it's safe to call any time, including as the plan a bare `rman
121
+ * publish` shows before asking for confirmation.
122
+ *
123
+ * A `private: true` package is always `'skip'`ped outright. A package with uncommitted local
124
+ * changes is `'error'` (aborts the whole plan) unless `options.ignoreDirty` downgrades it to
125
+ * `'skip'` instead - same rule `version` uses, since publishing untracked local edits is worse
126
+ * than a bad commit. Otherwise, its currently-published registry version (via `npm view`,
127
+ * queried concurrently across every remaining package) decides the rest: identical to the local
128
+ * `package.json` version is `'up-to-date'`; anything else (including never having been
129
+ * published at all) is `'publish'`.
130
+ *
131
+ * Deliberately decoupled from `version`: this only ever looks at what's *currently* on disk and
132
+ * on the registry, never at whether `version` was just run - so it works equally well right
133
+ * after a version bump, or standing alone in a release pipeline that bumped days earlier.
134
+ *
135
+ * A monorepo's root package is never a candidate at all - `repository.getPackages()` already
136
+ * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
137
+ * where it's a normal candidate like any other).
138
+ */
139
+ async function getPlan(repository, options = {}, deps = {}) {
140
+ const git = new GitHelper({ cwd: repository.dirname });
141
+ const packages = filterPackages(repository.getPackages({ toposort: true }), options);
142
+ const dirtyFiles = await git.listDirtyFiles({ absolute: true });
143
+ const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
144
+ const viewVersion = deps.npmViewVersion ?? ((name, cwd) => npmViewVersion(name, cwd, options));
145
+ const entries = new Map();
146
+ const toCheck = [];
147
+ for (const pkg of packages) {
148
+ if (pkg.config.publish?.skip) {
149
+ entries.set(pkg.name, {
150
+ package: pkg,
151
+ version: pkg.version,
152
+ status: 'skip',
153
+ reason: 'excluded via .rmanrc "publish.skip"',
154
+ });
155
+ }
156
+ else if (pkg.isPrivate) {
157
+ entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
158
+ }
159
+ else if (isDirty(pkg)) {
160
+ entries.set(pkg.name, {
161
+ package: pkg,
162
+ version: pkg.version,
163
+ status: options.ignoreDirty ? 'skip' : 'error',
164
+ reason: 'uncommitted local changes',
165
+ });
166
+ }
167
+ else {
168
+ toCheck.push(pkg);
169
+ }
170
+ }
171
+ await Promise.all(toCheck.map(async (pkg) => {
172
+ const registryVersion = await viewVersion(pkg.name, pkg.dirname);
173
+ entries.set(pkg.name, {
174
+ package: pkg,
175
+ version: pkg.version,
176
+ registryVersion,
177
+ status: registryVersion === pkg.version ? 'up-to-date' : 'publish',
178
+ reason: registryVersion ? `registry has ${registryVersion}` : 'never published',
179
+ });
180
+ }));
181
+ return packages.map(pkg => entries.get(pkg.name));
182
+ }
183
+ PublishService.getPlan = getPlan;
184
+ /**
185
+ * Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
186
+ * see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
187
+ * concurrent: unlike `run`/`build`, a package genuinely needs its own dependencies to have
188
+ * landed on the registry first, and publishing is rare enough (once per release) that the
189
+ * simplicity is worth more than the parallelism `run` gets from `power-tasks`.
190
+ *
191
+ * If a package fails, every other still-pending entry depending on it (transitively) is marked
192
+ * `'error'` too and never attempted - publishing a package whose own new dependency range points
193
+ * at a version that never actually reached the registry would hand consumers a broken install.
194
+ * A package's own failure doesn't stop unrelated packages elsewhere in the plan, though.
195
+ */
196
+ async function applyPlan(repository, plan, options = {}) {
197
+ const packageManager = CiService.resolvePackageManager(repository, options.packageManager);
198
+ const failed = new Set();
199
+ const result = [];
200
+ /** The tuple is explicit: `[p.name, p]` widens to `(string | Package)[]`, and whether `new Map`
201
+ * still infers `Map<string, Package>` from that came down to which declaration of
202
+ * `getPackages()` was in scope - the source one inferred it, the built `.d.ts` did not
203
+ * (measured, once the tests started type-checking). */
204
+ const packagesByName = new Map(repository.getPackages().map(p => [p.name, p]));
205
+ for (const entry of plan) {
206
+ if (entry.status !== 'publish') {
207
+ result.push(entry);
208
+ continue;
209
+ }
210
+ const pkg = entry.package;
211
+ const blocker = pkg.dependencies.find(d => failed.has(d.name));
212
+ if (blocker) {
213
+ failed.add(pkg.name);
214
+ result.push({ ...entry, status: 'error', reason: `dependency "${blocker.name}" failed to publish` });
215
+ continue;
216
+ }
217
+ const publishDir = resolvePublishDir(pkg, options.contents);
218
+ const restore = preparePublishManifest(pkg, publishDir, packagesByName);
219
+ try {
220
+ await exec(buildPublishCommand(packageManager, options), { cwd: publishDir, stdio: 'inherit' });
221
+ result.push(entry);
222
+ }
223
+ catch (e) {
224
+ failed.add(pkg.name);
225
+ result.push({ ...entry, status: 'error', reason: e.message });
226
+ }
227
+ finally {
228
+ restore?.();
229
+ }
230
+ }
231
+ return result;
232
+ }
233
+ PublishService.applyPlan = applyPlan;
234
+ })(PublishService || (PublishService = {}));
235
+ /** Where the publishable output lives, most specific statement first: the package's own
236
+ * `publishConfig.directory` (npm/pnpm's native spelling, and a statement about that one package),
237
+ * then `.rmanrc "publish.directory"` (which a `"[*]"` block can say once for a whole repository
238
+ * instead of repeating in every `package.json`), then `--contents` for a single run. */
239
+ function resolvePublishDir(pkg, contentsOverride) {
240
+ const native = pkg.manifest.raw.publishConfig?.directory;
241
+ const configured = pkg.config?.publish?.directory;
242
+ const rel = (typeof native === 'string' && native) || configured || contentsOverride;
243
+ return rel ? path.resolve(pkg.dirname, rel) : pkg.dirname;
244
+ }
245
+ function buildPublishCommand(packageManager, options) {
246
+ const args = ['publish'];
247
+ if (options.access)
248
+ args.push('--access', options.access);
249
+ if (options.tag)
250
+ args.push('--tag', options.tag);
251
+ if (options.otp)
252
+ args.push('--otp', options.otp);
253
+ if (options.registry)
254
+ args.push('--registry', options.registry);
255
+ if (options.userconfig)
256
+ args.push('--userconfig', options.userconfig);
257
+ return `${packageManager} ${args.join(' ')}`;
258
+ }
@@ -0,0 +1,50 @@
1
+ import { type GitHelper, type Package, VersionPlanService } from 'rman';
2
+ /**
3
+ * How a release is planned for an npm repository.
4
+ *
5
+ * rman's core holds the parts that are true of any repository - groups, conventional-commit
6
+ * severities, the cascade mechanics, the monorepo root's release identity - and leaves two decisions
7
+ * abstract because they are statements about an *ecosystem* rather than about releases. This is
8
+ * npm's pair of answers.
9
+ *
10
+ * Registered through the plugin's `versionPlanner`, so `rman version`/`rman changed` work in a
11
+ * repository that names `rman-node` and say what is missing in one that does not.
12
+ */
13
+ export declare class NodeVersionPlanService extends VersionPlanService {
14
+ /**
15
+ * The shared `ChangeHashService.detect`, unmodified: this package's own latest release tag, and failing
16
+ * that whatever `ManifestProvider.publishedVersion` reports, mapped back onto a tag name.
17
+ *
18
+ * **Nothing npm-specific is passed in any more**, and that is the point of the seam moving: the
19
+ * registry lookup is `packageJsonManifest.publishedVersion`'s now, dispatched per package, so this
20
+ * override exists only because `detectBoundary` is abstract. If the core ever makes it a concrete
21
+ * default, this method can go entirely.
22
+ */
23
+ protected detectBoundary(git: GitHelper, pkg: Package): Promise<string | undefined>;
24
+ /**
25
+ * npm's dependency ranges, read as a release policy. What decides each case is whether a
26
+ * dependent's **range floor** has to move for a consumer to get a correct install:
27
+ *
28
+ * - **patch** - only the changed packages. A dependent's `^1.2.0` already resolves to `1.2.1`,
29
+ * and a patch adds nothing for the dependent to require, so nothing downstream has to ship for
30
+ * consumers to receive it.
31
+ * - **minor** - also every transitive in-group dependent. A minor adds API; a dependent that uses
32
+ * it is only correct once its own published range requires the new floor, and a range lives in
33
+ * a manifest, which only a release puts on the registry.
34
+ * - **major** - the whole group, changed or not. A breaking change restates every member's
35
+ * compatibility, including the members that merely point at one.
36
+ *
37
+ * None of this is about versions, which is why it is not in the core: an ecosystem that pins exact
38
+ * versions instead has to release every dependent for a patch as well, and one that resolves
39
+ * dependencies from source may not need a release for any of it.
40
+ */
41
+ protected cascade(bump: string): VersionPlanService.Cascade;
42
+ }
43
+ /** Registers the planner with rman. Called by the plugin entry point, once - and declared in the
44
+ * plugin object as well, since a plugin loaded through `plugins` should not need an import side
45
+ * effect to work. */
46
+ export declare function augmentVersionPlan(): void;
47
+ /** The instance the plugin registers. One is enough: a planner holds no per-run state, and a test
48
+ * wanting a different registry answer registers its own `ManifestProvider` instead - which
49
+ * exercises the real path rather than a bypass. */
50
+ export declare const nodeVersionPlanner: NodeVersionPlanService;
@@ -0,0 +1,67 @@
1
+ import { ChangeHashService, VersionPlanService } from 'rman';
2
+ /**
3
+ * How a release is planned for an npm repository.
4
+ *
5
+ * rman's core holds the parts that are true of any repository - groups, conventional-commit
6
+ * severities, the cascade mechanics, the monorepo root's release identity - and leaves two decisions
7
+ * abstract because they are statements about an *ecosystem* rather than about releases. This is
8
+ * npm's pair of answers.
9
+ *
10
+ * Registered through the plugin's `versionPlanner`, so `rman version`/`rman changed` work in a
11
+ * repository that names `rman-node` and say what is missing in one that does not.
12
+ */
13
+ export class NodeVersionPlanService extends VersionPlanService {
14
+ /**
15
+ * The shared `ChangeHashService.detect`, unmodified: this package's own latest release tag, and failing
16
+ * that whatever `ManifestProvider.publishedVersion` reports, mapped back onto a tag name.
17
+ *
18
+ * **Nothing npm-specific is passed in any more**, and that is the point of the seam moving: the
19
+ * registry lookup is `packageJsonManifest.publishedVersion`'s now, dispatched per package, so this
20
+ * override exists only because `detectBoundary` is abstract. If the core ever makes it a concrete
21
+ * default, this method can go entirely.
22
+ */
23
+ detectBoundary(git, pkg) {
24
+ return ChangeHashService.detect(git, pkg);
25
+ }
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
+ cascade(bump) {
44
+ /** Only semver's three can arrive - `getPlan` validates against `bumpNames` and
45
+ * `packageJsonManifest` leaves the scheme at the semver default. The fallback is for a scheme
46
+ * someone swaps in underneath this planner, and it over-reaches on purpose: releasing a package
47
+ * that did not need it is noise, while under-reaching ships a dependent whose published range
48
+ * floor is wrong, which is a broken install. */
49
+ return CASCADE_BY_BUMP[bump] ?? 'group';
50
+ }
51
+ }
52
+ /** Registers the planner with rman. Called by the plugin entry point, once - and declared in the
53
+ * plugin object as well, since a plugin loaded through `plugins` should not need an import side
54
+ * effect to work. */
55
+ export function augmentVersionPlan() {
56
+ VersionPlanService.setPlanner(nodeVersionPlanner);
57
+ }
58
+ /** The instance the plugin registers. One is enough: a planner holds no per-run state, and a test
59
+ * wanting a different registry answer registers its own `ManifestProvider` instead - which
60
+ * exercises the real path rather than a bypass. */
61
+ export const nodeVersionPlanner = new NodeVersionPlanService();
62
+ /** semver's bump names against how far each has to reach - see `NodeVersionPlanService.cascade`. */
63
+ const CASCADE_BY_BUMP = {
64
+ patch: 'changed',
65
+ minor: 'dependents',
66
+ major: 'group',
67
+ };
@@ -0,0 +1,21 @@
1
+ import { BinPath } from 'rman';
2
+ /**
3
+ * Where npm puts a repository's locally installed executables: `node_modules/.bin`, at **every**
4
+ * level from `cwd` up to the filesystem root - which is how npm itself resolves a binary, so a
5
+ * package's `eslint` is found whether it was installed in that package or hoisted to the workspace
6
+ * root.
7
+ *
8
+ * Adapted from [npm-run-path](https://github.com/sindresorhus/npm-run-path), and it used to sit in
9
+ * rman's core. It is npm's directory layout from end to end: a Cargo or Go repository has no
10
+ * `node_modules` to walk, and nothing here would ever fire for it.
11
+ *
12
+ * **The running `node`'s own directory goes last**, after the walk, and its position is
13
+ * load-bearing. It is there so a script calling `node` gets the interpreter rman itself runs on
14
+ * rather than whatever the shell would pick. It also puts rman's own bin directory ahead of the
15
+ * inherited PATH, which is a measured trap: a nested `rman` invocation inside a `run` script
16
+ * resolves to the globally installed one, not to the repository's. Shim it in
17
+ * `<root>/node_modules/.bin` when that has to be overridden - the walk above reaches there first.
18
+ */
19
+ export declare const npmBinPaths: BinPath.Provider;
20
+ /** Registers the provider with rman. Called by the plugin entry point, once. */
21
+ export declare function augmentBinPath(): void;
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { BinPath } from 'rman';
4
+ /**
5
+ * Where npm puts a repository's locally installed executables: `node_modules/.bin`, at **every**
6
+ * level from `cwd` up to the filesystem root - which is how npm itself resolves a binary, so a
7
+ * package's `eslint` is found whether it was installed in that package or hoisted to the workspace
8
+ * root.
9
+ *
10
+ * Adapted from [npm-run-path](https://github.com/sindresorhus/npm-run-path), and it used to sit in
11
+ * rman's core. It is npm's directory layout from end to end: a Cargo or Go repository has no
12
+ * `node_modules` to walk, and nothing here would ever fire for it.
13
+ *
14
+ * **The running `node`'s own directory goes last**, after the walk, and its position is
15
+ * load-bearing. It is there so a script calling `node` gets the interpreter rman itself runs on
16
+ * rather than whatever the shell would pick. It also puts rman's own bin directory ahead of the
17
+ * inherited PATH, which is a measured trap: a nested `rman` invocation inside a `run` script
18
+ * resolves to the globally installed one, not to the repository's. Shim it in
19
+ * `<root>/node_modules/.bin` when that has to be overridden - the walk above reaches there first.
20
+ */
21
+ export const npmBinPaths = (cwd) => {
22
+ const result = [];
23
+ let previous;
24
+ let dir = path.resolve(cwd);
25
+ while (previous !== dir) {
26
+ result.push(path.join(dir, 'node_modules/.bin'));
27
+ previous = dir;
28
+ dir = path.resolve(dir, '..');
29
+ }
30
+ result.push(path.resolve(cwd, process.execPath, '..'));
31
+ return result;
32
+ };
33
+ /** Registers the provider with rman. Called by the plugin entry point, once. */
34
+ export function augmentBinPath() {
35
+ BinPath.addProvider(npmBinPaths);
36
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `npm view <name> version` - `undefined` for anything that is not an answer (never published, no
3
+ * network, private or restricted with no access). Catch-everything on purpose: every caller treats
4
+ * "no answer" as a legitimate state rather than a failure.
5
+ *
6
+ * One copy, two callers with different needs: `publish` asks in order to decide whether this exact
7
+ * version is already out there, and can be pointed at another registry from the CLI
8
+ * (`--registry`/`--userconfig`); `packageJsonManifest.publishedVersion` asks so that
9
+ * `detectChangeHash` can guess a tag name, and passes nothing - a bare `npm view` already picks up
10
+ * the repository's own `.npmrc` from `cwd`, which is what that path wants.
11
+ */
12
+ export declare function npmViewVersion(name: string, cwd: string, options?: {
13
+ registry?: string;
14
+ userconfig?: string;
15
+ }): Promise<string | undefined>;
@@ -0,0 +1,28 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ /**
4
+ * `npm view <name> version` - `undefined` for anything that is not an answer (never published, no
5
+ * network, private or restricted with no access). Catch-everything on purpose: every caller treats
6
+ * "no answer" as a legitimate state rather than a failure.
7
+ *
8
+ * One copy, two callers with different needs: `publish` asks in order to decide whether this exact
9
+ * version is already out there, and can be pointed at another registry from the CLI
10
+ * (`--registry`/`--userconfig`); `packageJsonManifest.publishedVersion` asks so that
11
+ * `detectChangeHash` can guess a tag name, and passes nothing - a bare `npm view` already picks up
12
+ * the repository's own `.npmrc` from `cwd`, which is what that path wants.
13
+ */
14
+ export async function npmViewVersion(name, cwd, options = {}) {
15
+ const argv = ['view', name, 'version'];
16
+ if (options.registry)
17
+ argv.push('--registry', options.registry);
18
+ if (options.userconfig)
19
+ argv.push('--userconfig', options.userconfig);
20
+ try {
21
+ const { stdout } = await execFileAsync('npm', argv, { cwd });
22
+ return stdout.trim() || undefined;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ const execFileAsync = promisify(execFile);
@@ -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
+ * **In `rman-node`, 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
+ }