rman 0.38.0 → 1.0.2

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 (119) hide show
  1. package/README.md +371 -21
  2. package/cli.js +51 -29
  3. package/commands/build.command.d.ts +3 -0
  4. package/commands/build.command.js +14 -0
  5. package/commands/changed.command.d.ts +3 -0
  6. package/commands/changed.command.js +29 -0
  7. package/commands/changelog.command.d.ts +3 -0
  8. package/commands/changelog.command.js +63 -0
  9. package/commands/ci.command.d.ts +3 -0
  10. package/commands/ci.command.js +30 -0
  11. package/commands/clean.command.d.ts +3 -0
  12. package/commands/clean.command.js +36 -0
  13. package/commands/diff.command.d.ts +3 -0
  14. package/commands/diff.command.js +51 -0
  15. package/commands/exec.command.d.ts +3 -0
  16. package/commands/exec.command.js +83 -0
  17. package/commands/import.command.d.ts +3 -0
  18. package/commands/import.command.js +27 -0
  19. package/commands/info.command.d.ts +3 -0
  20. package/commands/info.command.js +61 -0
  21. package/commands/list.command.d.ts +3 -0
  22. package/commands/list.command.js +113 -0
  23. package/commands/publish.command.d.ts +3 -0
  24. package/commands/publish.command.js +147 -0
  25. package/commands/run.command.d.ts +6 -0
  26. package/commands/run.command.js +80 -0
  27. package/commands/test.command.d.ts +3 -0
  28. package/commands/test.command.js +14 -0
  29. package/commands/version.command.d.ts +3 -0
  30. package/commands/version.command.js +125 -0
  31. package/constants.js +1 -1
  32. package/core/config.d.ts +13 -0
  33. package/core/config.js +61 -0
  34. package/core/package.d.ts +2 -0
  35. package/core/package.js +2 -0
  36. package/core/repository.d.ts +36 -3
  37. package/core/repository.js +90 -45
  38. package/index.d.ts +17 -1
  39. package/index.js +15 -1
  40. package/package.json +8 -4
  41. package/rmanrc.schema.json +202 -0
  42. package/services/changelog.service.d.ts +99 -0
  43. package/services/changelog.service.js +272 -0
  44. package/services/ci.service.d.ts +40 -0
  45. package/services/ci.service.js +204 -0
  46. package/services/clean.service.d.ts +42 -0
  47. package/services/clean.service.js +226 -0
  48. package/services/exec.service.d.ts +35 -0
  49. package/services/exec.service.js +114 -0
  50. package/services/import.service.d.ts +30 -0
  51. package/services/import.service.js +84 -0
  52. package/services/list.service.d.ts +27 -0
  53. package/services/list.service.js +25 -0
  54. package/services/publish.service.d.ts +79 -0
  55. package/services/publish.service.js +199 -0
  56. package/services/run.service.d.ts +99 -0
  57. package/services/run.service.js +432 -0
  58. package/services/system-info.d.ts +16 -0
  59. package/services/system-info.js +25 -0
  60. package/services/version.service.d.ts +95 -0
  61. package/services/version.service.js +450 -0
  62. package/services.d.ts +10 -0
  63. package/services.js +10 -0
  64. package/utils/branch-guard.d.ts +25 -0
  65. package/utils/branch-guard.js +61 -0
  66. package/utils/change-hash.d.ts +55 -0
  67. package/utils/change-hash.js +80 -0
  68. package/utils/conventional-commits.d.ts +39 -0
  69. package/utils/conventional-commits.js +43 -0
  70. package/utils/exec.d.ts +5 -6
  71. package/utils/exec.js +45 -59
  72. package/utils/git.d.ts +86 -0
  73. package/utils/git.js +276 -0
  74. package/utils/live-region.d.ts +16 -0
  75. package/utils/live-region.js +59 -0
  76. package/utils/logger.d.ts +24 -0
  77. package/utils/logger.js +39 -0
  78. package/utils/npm-run-path.d.ts +1 -1
  79. package/utils/npm-run-path.js +10 -10
  80. package/utils/package-filter.d.ts +32 -0
  81. package/utils/package-filter.js +73 -0
  82. package/utils/progress-panel.d.ts +59 -0
  83. package/utils/progress-panel.js +149 -0
  84. package/utils/workspace-range.d.ts +17 -0
  85. package/utils/workspace-range.js +28 -0
  86. package/commands/build-command.d.ts +0 -11
  87. package/commands/build-command.js +0 -26
  88. package/commands/changed-command.d.ts +0 -16
  89. package/commands/changed-command.js +0 -35
  90. package/commands/ci-command.d.ts +0 -24
  91. package/commands/ci-command.js +0 -71
  92. package/commands/execute-command.d.ts +0 -19
  93. package/commands/execute-command.js +0 -75
  94. package/commands/info-command.d.ts +0 -10
  95. package/commands/info-command.js +0 -57
  96. package/commands/list-command.d.ts +0 -38
  97. package/commands/list-command.js +0 -137
  98. package/commands/multi-task-command.d.ts +0 -22
  99. package/commands/multi-task-command.js +0 -68
  100. package/commands/publish-command.d.ts +0 -24
  101. package/commands/publish-command.js +0 -135
  102. package/commands/run-command.d.ts +0 -28
  103. package/commands/run-command.js +0 -134
  104. package/commands/version-command.d.ts +0 -25
  105. package/commands/version-command.js +0 -198
  106. package/core/command.d.ts +0 -35
  107. package/core/command.js +0 -117
  108. package/core/constants.d.ts +0 -1
  109. package/core/constants.js +0 -2
  110. package/core/logger.d.ts +0 -12
  111. package/core/logger.js +0 -2
  112. package/utils/file-utils.d.ts +0 -4
  113. package/utils/file-utils.js +0 -29
  114. package/utils/git-utils.d.ts +0 -25
  115. package/utils/git-utils.js +0 -66
  116. package/utils/npm-utils.d.ts +0 -11
  117. package/utils/npm-utils.js +0 -33
  118. package/utils/package-not-found-error.d.ts +0 -2
  119. package/utils/package-not-found-error.js +0 -2
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { GitHelper } from '../utils/git.js';
5
+ export var ImportService;
6
+ (function (ImportService) {
7
+ /**
8
+ * Imports `sourcePath` (a local clone of some other git repository) as a new package under this
9
+ * repository, preserving its **entire commit history** - every original commit, author, date and
10
+ * message stays intact, just as if the package had always lived at its new path. Unlike copying
11
+ * the files over and making one "import" commit, `git blame`/`git log --follow` keep working on
12
+ * the imported files afterward.
13
+ *
14
+ * How: every commit reachable from `sourcePath`'s HEAD is turned into a patch (`git format-patch
15
+ * --root`, oldest first), each patch's file paths are rewritten to be prefixed with the new
16
+ * subdirectory, then replayed onto this repository via `git am` (preserving authorship). A
17
+ * source repo with merge commits or binary-file renames can still trip up a patch here or there -
18
+ * same caveat `lerna import` has, since both use this same replay approach rather than a real
19
+ * merge (which real `git subtree` would give, at the cost of far less predictable behavior
20
+ * across git versions - the reason this - and lerna - avoid it).
21
+ */
22
+ async function importRepo(repository, sourcePath, options = {}) {
23
+ const absSource = path.resolve(sourcePath);
24
+ if (!fs.existsSync(path.join(absSource, '.git'))) {
25
+ throw new Error(`"${sourcePath}" is not a git repository (no .git found) - clone it locally first`);
26
+ }
27
+ const name = resolveSourceName(absSource);
28
+ const dirName = name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
29
+ const destDir = options.dest ?? 'packages';
30
+ const targetDir = path.join(repository.dirname, destDir, dirName);
31
+ if (fs.existsSync(targetDir)) {
32
+ throw new Error(`"${path.relative(repository.dirname, targetDir)}" already exists`);
33
+ }
34
+ const sourceGit = new GitHelper({ cwd: absSource });
35
+ const patchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rman-import-'));
36
+ try {
37
+ const patches = await sourceGit.formatPatches(patchDir);
38
+ if (!patches.length)
39
+ throw new Error(`"${sourcePath}" has no commits to import`);
40
+ const relTargetDir = path.relative(repository.dirname, targetDir).split(path.sep).join('/');
41
+ for (const patchFile of patches)
42
+ rewritePatchPaths(patchFile, relTargetDir);
43
+ const targetGit = new GitHelper({ cwd: repository.dirname });
44
+ await targetGit.applyPatches(patches);
45
+ return { name, targetDir, commitCount: patches.length };
46
+ }
47
+ finally {
48
+ fs.rmSync(patchDir, { recursive: true, force: true });
49
+ }
50
+ }
51
+ ImportService.importRepo = importRepo;
52
+ })(ImportService || (ImportService = {}));
53
+ /** The source repo's own `package.json` name, or its directory's basename when it has none (or
54
+ * isn't even a package yet - a plain library repo being folded in). */
55
+ function resolveSourceName(absSource) {
56
+ const pkgJsonFile = path.join(absSource, 'package.json');
57
+ if (fs.existsSync(pkgJsonFile)) {
58
+ try {
59
+ const json = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
60
+ if (typeof json.name === 'string' && json.name)
61
+ return json.name;
62
+ }
63
+ catch {
64
+ // malformed package.json - fall through to the directory name below.
65
+ }
66
+ }
67
+ return path.basename(absSource);
68
+ }
69
+ /**
70
+ * Rewrites a `git format-patch`-produced file in place so every path it touches is prefixed with
71
+ * `prefix` (the new subdirectory, forward-slash-separated) - `diff --git`/`---`/`+++`/rename
72
+ * headers only; `--- /dev/null`/`+++ /dev/null` (new/deleted file markers) never match, since they
73
+ * have no `a/`/`b/` prefix to rewrite in the first place.
74
+ */
75
+ function rewritePatchPaths(patchFile, prefix) {
76
+ const content = fs.readFileSync(patchFile, 'utf-8');
77
+ const rewritten = content
78
+ .replace(/^diff --git a\/(.+?) b\/(.+)$/gm, (_m, a, b) => `diff --git a/${prefix}/${a} b/${prefix}/${b}`)
79
+ .replace(/^--- a\/(.+)$/gm, (_m, p) => `--- a/${prefix}/${p}`)
80
+ .replace(/^\+\+\+ b\/(.+)$/gm, (_m, p) => `+++ b/${prefix}/${p}`)
81
+ .replace(/^rename from (.+)$/gm, (_m, p) => `rename from ${prefix}/${p}`)
82
+ .replace(/^rename to (.+)$/gm, (_m, p) => `rename to ${prefix}/${p}`);
83
+ fs.writeFileSync(patchFile, rewritten);
84
+ }
@@ -0,0 +1,27 @@
1
+ import type { Repository } from '../core/repository.js';
2
+ import { type PackageFilterOptions } from '../utils/package-filter.js';
3
+ export declare namespace ListService {
4
+ interface Options extends PackageFilterOptions {
5
+ /** Topological order (dependencies before dependents) instead of lexical by directory. */
6
+ toposort?: boolean;
7
+ /** Only include packages that have changed since the last publish (dirty or committed but
8
+ * not yet published) - or, with `changedSince`, since that specific commit/hash. */
9
+ changed?: boolean;
10
+ changedSince?: string;
11
+ }
12
+ interface Item {
13
+ name: string;
14
+ version: string;
15
+ location: string;
16
+ private: boolean;
17
+ status: Repository.PackageStatus;
18
+ /** In-repo package names this one depends on - enough to build a dependency graph without a
19
+ * second call, e.g. `Object.fromEntries(items.map(i => [i.name, i.dependencies]))`. */
20
+ dependencies: string[];
21
+ }
22
+ /** `list`: every package in the repository (or, with `changed`/`changedSince`, only the ones
23
+ * that have actually changed), each with its version, location, private flag, and change
24
+ * status relative to upstream. Pure data - no console output; `rman list`'s own command
25
+ * decides how to present it (table, JSON, parseable, names only, or a dependency graph). */
26
+ function getPackages(repository: Repository, options?: Options): Promise<Item[]>;
27
+ }
@@ -0,0 +1,25 @@
1
+ import path from 'path';
2
+ import { filterPackages } from '../utils/package-filter.js';
3
+ export var ListService;
4
+ (function (ListService) {
5
+ /** `list`: every package in the repository (or, with `changed`/`changedSince`, only the ones
6
+ * that have actually changed), each with its version, location, private flag, and change
7
+ * status relative to upstream. Pure data - no console output; `rman list`'s own command
8
+ * decides how to present it (table, JSON, parseable, names only, or a dependency graph). */
9
+ async function getPackages(repository, options = {}) {
10
+ const packages = filterPackages(repository.getPackages({ toposort: options.toposort }), options);
11
+ const status = await repository.listStatus({ hash: options.changedSince });
12
+ let items = packages.map(p => ({
13
+ name: p.name,
14
+ version: p.version,
15
+ location: path.relative(repository.dirname, p.dirname) || '.',
16
+ private: p.isPrivate,
17
+ status: status[p.name],
18
+ dependencies: [...p.dependencies],
19
+ }));
20
+ if (options.changed || options.changedSince)
21
+ items = items.filter(it => it.status !== 'clean');
22
+ return items;
23
+ }
24
+ ListService.getPackages = getPackages;
25
+ })(ListService || (ListService = {}));
@@ -0,0 +1,79 @@
1
+ import type { Package } from '../core/package.js';
2
+ import type { Repository } from '../core/repository.js';
3
+ import { type PackageFilterOptions } from '../utils/package-filter.js';
4
+ import { CiService } from './ci.service.js';
5
+ export declare namespace PublishService {
6
+ /** Injectable registry lookup - mainly for tests, so they don't depend on network access or a
7
+ * real published package. Same shape as `detectChangeHash`'s own `npmViewVersion`. */
8
+ interface Deps {
9
+ npmViewVersion?: (name: string, cwd: string) => Promise<string | undefined>;
10
+ }
11
+ interface Options extends PackageFilterOptions {
12
+ /** A package with uncommitted local changes is excluded (status `'skip'`) instead of aborting
13
+ * the whole plan (status `'error'`) - same as `version`'s own option. Default false. */
14
+ ignoreDirty?: boolean;
15
+ /** Registry to check against (and, in `applyPlan`, publish to) - `.npmrc`'s own configured
16
+ * registry is used when omitted. */
17
+ registry?: string;
18
+ /** Path to a custom `.npmrc`, for both the registry check and the actual publish. */
19
+ userconfig?: string;
20
+ }
21
+ interface ApplyOptions extends Options {
22
+ packageManager?: CiService.PackageManager;
23
+ /** `npm publish --access <access>` - required by the registry for a *new* scoped package. */
24
+ access?: 'public' | 'restricted';
25
+ /** `npm publish --tag <tag>` - the dist-tag this version is published under (default `latest`). */
26
+ tag?: string;
27
+ /** `npm publish --otp <otp>` - a 2FA one-time password, for registries that require it. */
28
+ otp?: string;
29
+ /** Subdirectory to publish from, relative to the package's own directory - only consulted when
30
+ * the package doesn't already declare its own `package.json` `publishConfig.directory` (npm's
31
+ * native mechanism for this, which always wins when present). */
32
+ contents?: string;
33
+ }
34
+ /** One package's outcome in a publish plan - see `getPlan`. */
35
+ interface Entry {
36
+ package: Package;
37
+ version: string;
38
+ status: 'publish' | 'skip' | 'up-to-date' | 'error';
39
+ /** What's currently on the registry, if anything - only set once a registry check actually ran. */
40
+ registryVersion?: string;
41
+ reason?: string;
42
+ }
43
+ /**
44
+ * Computes what `publish` *would* do, across every non-private package (topological order,
45
+ * dependencies before dependents) - never touches the registry to publish anything, just
46
+ * queries it to decide, so it's safe to call any time, including as the plan a bare `rman
47
+ * publish` shows before asking for confirmation.
48
+ *
49
+ * A `private: true` package is always `'skip'`ped outright. A package with uncommitted local
50
+ * changes is `'error'` (aborts the whole plan) unless `options.ignoreDirty` downgrades it to
51
+ * `'skip'` instead - same rule `version` uses, since publishing untracked local edits is worse
52
+ * than a bad commit. Otherwise, its currently-published registry version (via `npm view`,
53
+ * queried concurrently across every remaining package) decides the rest: identical to the local
54
+ * `package.json` version is `'up-to-date'`; anything else (including never having been
55
+ * published at all) is `'publish'`.
56
+ *
57
+ * Deliberately decoupled from `version`: this only ever looks at what's *currently* on disk and
58
+ * on the registry, never at whether `version` was just run - so it works equally well right
59
+ * after a version bump, or standing alone in a release pipeline that bumped days earlier.
60
+ *
61
+ * A monorepo's root package is never a candidate at all - `repository.getPackages()` already
62
+ * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
63
+ * where it's a normal candidate like any other).
64
+ */
65
+ function getPlan(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
66
+ /**
67
+ * Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
68
+ * see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
69
+ * concurrent: unlike `run`/`build`, a package genuinely needs its own dependencies to have
70
+ * landed on the registry first, and publishing is rare enough (once per release) that the
71
+ * simplicity is worth more than the parallelism `run` gets from `power-tasks`.
72
+ *
73
+ * If a package fails, every other still-pending entry depending on it (transitively) is marked
74
+ * `'error'` too and never attempted - publishing a package whose own new dependency range points
75
+ * at a version that never actually reached the registry would hand consumers a broken install.
76
+ * A package's own failure doesn't stop unrelated packages elsewhere in the plan, though.
77
+ */
78
+ function applyPlan(repository: Repository, plan: Entry[], options?: ApplyOptions): Promise<Entry[]>;
79
+ }
@@ -0,0 +1,199 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { exec } from '../utils/exec.js';
6
+ import { GitHelper } from '../utils/git.js';
7
+ import { filterPackages } from '../utils/package-filter.js';
8
+ import { parseWorkspaceRange, resolveWorkspaceRange } from '../utils/workspace-range.js';
9
+ import { CiService } from './ci.service.js';
10
+ import { DEPENDENCY_KEYS } from './version.service.js';
11
+ export var PublishService;
12
+ (function (PublishService) {
13
+ /**
14
+ * Computes what `publish` *would* do, across every non-private package (topological order,
15
+ * dependencies before dependents) - never touches the registry to publish anything, just
16
+ * queries it to decide, so it's safe to call any time, including as the plan a bare `rman
17
+ * publish` shows before asking for confirmation.
18
+ *
19
+ * A `private: true` package is always `'skip'`ped outright. A package with uncommitted local
20
+ * changes is `'error'` (aborts the whole plan) unless `options.ignoreDirty` downgrades it to
21
+ * `'skip'` instead - same rule `version` uses, since publishing untracked local edits is worse
22
+ * than a bad commit. Otherwise, its currently-published registry version (via `npm view`,
23
+ * queried concurrently across every remaining package) decides the rest: identical to the local
24
+ * `package.json` version is `'up-to-date'`; anything else (including never having been
25
+ * published at all) is `'publish'`.
26
+ *
27
+ * Deliberately decoupled from `version`: this only ever looks at what's *currently* on disk and
28
+ * on the registry, never at whether `version` was just run - so it works equally well right
29
+ * after a version bump, or standing alone in a release pipeline that bumped days earlier.
30
+ *
31
+ * A monorepo's root package is never a candidate at all - `repository.getPackages()` already
32
+ * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
33
+ * where it's a normal candidate like any other).
34
+ */
35
+ async function getPlan(repository, options = {}, deps = {}) {
36
+ const git = new GitHelper({ cwd: repository.dirname });
37
+ const packages = filterPackages(repository.getPackages({ toposort: true }), options);
38
+ const dirtyFiles = await git.listDirtyFiles({ absolute: true });
39
+ const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
40
+ const npmViewVersion = deps.npmViewVersion ?? ((name, cwd) => defaultNpmViewVersion(name, cwd, options));
41
+ const entries = new Map();
42
+ const toCheck = [];
43
+ for (const pkg of packages) {
44
+ if (pkg.isPrivate) {
45
+ entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
46
+ }
47
+ else if (isDirty(pkg)) {
48
+ entries.set(pkg.name, {
49
+ package: pkg,
50
+ version: pkg.version,
51
+ status: options.ignoreDirty ? 'skip' : 'error',
52
+ reason: 'uncommitted local changes',
53
+ });
54
+ }
55
+ else {
56
+ toCheck.push(pkg);
57
+ }
58
+ }
59
+ await Promise.all(toCheck.map(async (pkg) => {
60
+ const registryVersion = await npmViewVersion(pkg.name, pkg.dirname);
61
+ entries.set(pkg.name, {
62
+ package: pkg,
63
+ version: pkg.version,
64
+ registryVersion,
65
+ status: registryVersion === pkg.version ? 'up-to-date' : 'publish',
66
+ reason: registryVersion ? `registry has ${registryVersion}` : 'never published',
67
+ });
68
+ }));
69
+ return packages.map(pkg => entries.get(pkg.name));
70
+ }
71
+ PublishService.getPlan = getPlan;
72
+ /**
73
+ * Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
74
+ * see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
75
+ * concurrent: unlike `run`/`build`, a package genuinely needs its own dependencies to have
76
+ * landed on the registry first, and publishing is rare enough (once per release) that the
77
+ * simplicity is worth more than the parallelism `run` gets from `power-tasks`.
78
+ *
79
+ * If a package fails, every other still-pending entry depending on it (transitively) is marked
80
+ * `'error'` too and never attempted - publishing a package whose own new dependency range points
81
+ * at a version that never actually reached the registry would hand consumers a broken install.
82
+ * A package's own failure doesn't stop unrelated packages elsewhere in the plan, though.
83
+ */
84
+ async function applyPlan(repository, plan, options = {}) {
85
+ const packageManager = CiService.resolvePackageManager(repository, options.packageManager);
86
+ const failed = new Set();
87
+ const result = [];
88
+ const packagesByName = new Map(repository.getPackages().map(p => [p.name, p]));
89
+ for (const entry of plan) {
90
+ if (entry.status !== 'publish') {
91
+ result.push(entry);
92
+ continue;
93
+ }
94
+ const pkg = entry.package;
95
+ const blocker = pkg.dependencies.find(d => failed.has(d));
96
+ if (blocker) {
97
+ failed.add(pkg.name);
98
+ result.push({ ...entry, status: 'error', reason: `dependency "${blocker}" failed to publish` });
99
+ continue;
100
+ }
101
+ const restore = rewriteWorkspaceRangesForPublish(pkg, packagesByName);
102
+ try {
103
+ await exec(buildPublishCommand(packageManager, options), {
104
+ cwd: resolvePublishDir(pkg, options.contents),
105
+ stdio: 'inherit',
106
+ });
107
+ result.push(entry);
108
+ }
109
+ catch (e) {
110
+ failed.add(pkg.name);
111
+ result.push({ ...entry, status: 'error', reason: e.message });
112
+ }
113
+ finally {
114
+ restore?.();
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+ PublishService.applyPlan = applyPlan;
120
+ })(PublishService || (PublishService = {}));
121
+ const execFileAsync = promisify(execFile);
122
+ /** `npm view <name> version`, optionally against a custom registry/`.npmrc` - `undefined` for any
123
+ * failure (never published, no network, private/restricted with no access, ...), same
124
+ * catch-everything shape as `change-hash.ts`'s own `defaultNpmViewVersion` (kept separate here
125
+ * since this one also needs `--registry`/`--userconfig`, which that one has no use for). */
126
+ async function defaultNpmViewVersion(name, cwd, options) {
127
+ const argv = ['view', name, 'version'];
128
+ if (options.registry)
129
+ argv.push('--registry', options.registry);
130
+ if (options.userconfig)
131
+ argv.push('--userconfig', options.userconfig);
132
+ try {
133
+ const { stdout } = await execFileAsync('npm', argv, { cwd });
134
+ return stdout.trim() || undefined;
135
+ }
136
+ catch {
137
+ return undefined;
138
+ }
139
+ }
140
+ /** npm's own native `publishConfig.directory` (if the package declares one) always wins over
141
+ * `options.contents` - the package's own package.json is the more authoritative, persistent
142
+ * statement of "this is where the publishable output lives", `--contents` is just a fallback for
143
+ * when it doesn't declare one at all. */
144
+ function resolvePublishDir(pkg, contentsOverride) {
145
+ const native = pkg.json.publishConfig?.directory;
146
+ const rel = (typeof native === 'string' && native) || contentsOverride;
147
+ return rel ? path.resolve(pkg.dirname, rel) : pkg.dirname;
148
+ }
149
+ function buildPublishCommand(packageManager, options) {
150
+ const args = ['publish'];
151
+ if (options.access)
152
+ args.push('--access', options.access);
153
+ if (options.tag)
154
+ args.push('--tag', options.tag);
155
+ if (options.otp)
156
+ args.push('--otp', options.otp);
157
+ if (options.registry)
158
+ args.push('--registry', options.registry);
159
+ if (options.userconfig)
160
+ args.push('--userconfig', options.userconfig);
161
+ return `${packageManager} ${args.join(' ')}`;
162
+ }
163
+ /**
164
+ * Rewrites every `"workspace:"` dependency range in `pkg`'s `package.json` to a real,
165
+ * registry-publishable range (the same substitution pnpm/yarn's own publish performs - see
166
+ * `resolveWorkspaceRange`), just before shelling out to `npm publish` - which reads whatever is
167
+ * actually on disk, unlike pnpm/yarn's own publish this never packs into a staging tarball first.
168
+ * Returns a function that restores the file's original bytes (and `pkg`'s in-memory state)
169
+ * verbatim; always invoke it from a `finally`, so a failed publish never leaves a rewritten
170
+ * `package.json` behind. Returns `undefined` (nothing to restore, no disk write at all) when `pkg`
171
+ * has no `"workspace:"` ranges to begin with.
172
+ */
173
+ function rewriteWorkspaceRangesForPublish(pkg, packagesByName) {
174
+ const hasWorkspaceRange = DEPENDENCY_KEYS.some(depKey => {
175
+ const deps = pkg.json[depKey];
176
+ return deps && Object.values(deps).some(v => parseWorkspaceRange(v));
177
+ });
178
+ if (!hasWorkspaceRange)
179
+ return undefined;
180
+ const original = fs.readFileSync(pkg.jsonFileName, 'utf-8');
181
+ for (const depKey of DEPENDENCY_KEYS) {
182
+ const deps = pkg.json[depKey];
183
+ if (!deps)
184
+ continue;
185
+ for (const depName of Object.keys(deps)) {
186
+ const parsed = parseWorkspaceRange(deps[depName]);
187
+ if (!parsed)
188
+ continue;
189
+ const depPkg = packagesByName.get(depName);
190
+ if (depPkg)
191
+ deps[depName] = resolveWorkspaceRange(parsed, depPkg.version);
192
+ }
193
+ }
194
+ pkg.writeJson();
195
+ return () => {
196
+ fs.writeFileSync(pkg.jsonFileName, original, 'utf-8');
197
+ pkg.reloadJson();
198
+ };
199
+ }
@@ -0,0 +1,99 @@
1
+ import type { Package } from '../core/package.js';
2
+ import type { Repository } from '../core/repository.js';
3
+ import { type LogLevel } from '../utils/logger.js';
4
+ import { type PackageFilterOptions } from '../utils/package-filter.js';
5
+ export declare namespace RunService {
6
+ interface Options extends PackageFilterOptions {
7
+ /** Max packages built at once: `true`/omitted = CPU count, a number = that many, `false` = serial (1). */
8
+ parallel?: boolean | number;
9
+ /** Respect the package dependency graph: a package waits for its dependencies and is skipped
10
+ * if one fails. Default true (right for `build`). Set false for scripts like `lint`/`test`
11
+ * where packages are independent - order becomes alphabetical and one package's failure
12
+ * (or its dependency's) never skips another. */
13
+ topo?: boolean;
14
+ bail?: boolean;
15
+ changed?: boolean;
16
+ changedSince?: string;
17
+ /** Show the live progress panel. Default true; auto-disabled when stdout isn't a TTY. */
18
+ progress?: boolean;
19
+ /** Verbosity of the classic per-step log (only applies when the live panel is off). Falls back to
20
+ * the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
21
+ logLevel?: LogLevel;
22
+ /** Run across the whole repository even when the current directory is inside a single package
23
+ * (which otherwise scopes the run to just that package, and skips the root's own pre/post
24
+ * bookend - see `Repository.currentPackage`). Has no effect when already at the repository
25
+ * root, or outside any known package. */
26
+ root?: boolean;
27
+ }
28
+ /**
29
+ * A package's `.rmanrc` (cascaded) can configure `run.<script>.*` - e.g.
30
+ * run:
31
+ * build:
32
+ * concurrency: 2
33
+ * lint:
34
+ * topo: false
35
+ * bail: false
36
+ * and a package can opt itself out of a script entirely:
37
+ * run:
38
+ * build:
39
+ * skip: true
40
+ * or supply the command(s) to run when its own package.json doesn't define this script (or its
41
+ * pre/post hooks) at all - a single string, or an array to run several in sequence - see
42
+ * `getScriptSteps`:
43
+ * run:
44
+ * build:
45
+ * script: tsc -b
46
+ * preScript: [node ./generate.js, node ./validate.js]
47
+ * postScript: node ./copy-assets.js
48
+ * override: true # use these even if the package *does* define its own
49
+ */
50
+ function getConfig(pkg: Package, script: string): Record<string, unknown>;
51
+ /**
52
+ * `.rmanrc` conditional execution, GitHub Actions-`if`-flavored but a small closed grammar
53
+ * instead of a full expression language (less to get wrong, still covers what's asked for) -
54
+ * atoms combined with `and`/`or` (`and` binds tighter, same as most languages) and `(...)`:
55
+ *
56
+ * run:
57
+ * build:
58
+ * if: changed # changed since the last publish
59
+ * test:
60
+ * if: changed = a1b2c3d # changed since a specific commit
61
+ * if: changed = {CHANGE_HASH} # {NAME} -> process.env.NAME first
62
+ * if: (changed or dirty) and not committed
63
+ */
64
+ type IfNode = {
65
+ kind: 'atom';
66
+ name: string;
67
+ value?: string;
68
+ } | {
69
+ kind: 'not';
70
+ node: IfNode;
71
+ } | {
72
+ kind: 'and';
73
+ left: IfNode;
74
+ right: IfNode;
75
+ } | {
76
+ kind: 'or';
77
+ left: IfNode;
78
+ right: IfNode;
79
+ };
80
+ /** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
81
+ * and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
82
+ function parseIfExpr(raw: unknown): IfNode | undefined;
83
+ /** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
84
+ * for the same reference hash across packages/scripts in a single run. */
85
+ function evaluateIf(repository: Repository, pkg: Package, node: IfNode, statusCache: Map<string, Record<string, Repository.PackageStatus>>): Promise<boolean>;
86
+ function runScript(repository: Repository, script: string, options?: Options & {
87
+ commandName?: string;
88
+ }): Promise<void>;
89
+ }
90
+ /** Resolution order: explicit CLI flag > the package's resolved `.rmanrc` > `fallback`. */
91
+ export declare function resolveBool(cliValue: boolean | undefined, pkg: Package, script: string, key: string, fallback: boolean): boolean;
92
+ /**
93
+ * Bail is the one setting where a package's own `.rmanrc` outranks even an explicit CLI flag:
94
+ * "this package's failure must always stop the batch" is a more specific, intentional statement
95
+ * than a broad `--no-bail` meant for the run as a whole, and shouldn't be silently overridable by it.
96
+ */
97
+ export declare function resolveBail(cliValue: boolean | undefined, pkg: Package, script: string, fallback: boolean): boolean;
98
+ export declare function resolveNumber(cliValue: number | undefined, pkg: Package, script: string, key: string, fallback: number): number;
99
+ export declare function resolveLogLevel(cliValue: LogLevel | undefined, pkg: Package, script: string, fallback: LogLevel): LogLevel;