rman 0.37.4 → 1.0.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 (120) hide show
  1. package/README.md +364 -21
  2. package/cli.d.ts +1 -0
  3. package/cli.js +52 -28
  4. package/commands/build.command.d.ts +3 -0
  5. package/commands/build.command.js +14 -0
  6. package/commands/changed.command.d.ts +3 -0
  7. package/commands/changed.command.js +29 -0
  8. package/commands/changelog.command.d.ts +3 -0
  9. package/commands/changelog.command.js +63 -0
  10. package/commands/ci.command.d.ts +3 -0
  11. package/commands/ci.command.js +30 -0
  12. package/commands/clean.command.d.ts +3 -0
  13. package/commands/clean.command.js +36 -0
  14. package/commands/diff.command.d.ts +3 -0
  15. package/commands/diff.command.js +51 -0
  16. package/commands/exec.command.d.ts +3 -0
  17. package/commands/exec.command.js +83 -0
  18. package/commands/import.command.d.ts +3 -0
  19. package/commands/import.command.js +27 -0
  20. package/commands/info.command.d.ts +3 -0
  21. package/commands/info.command.js +61 -0
  22. package/commands/list.command.d.ts +3 -0
  23. package/commands/list.command.js +113 -0
  24. package/commands/publish.command.d.ts +3 -0
  25. package/commands/publish.command.js +147 -0
  26. package/commands/run.command.d.ts +6 -0
  27. package/commands/run.command.js +80 -0
  28. package/commands/test.command.d.ts +3 -0
  29. package/commands/test.command.js +14 -0
  30. package/commands/version.command.d.ts +3 -0
  31. package/commands/version.command.js +125 -0
  32. package/constants.js +1 -1
  33. package/core/config.d.ts +13 -0
  34. package/core/config.js +61 -0
  35. package/core/package.d.ts +2 -0
  36. package/core/package.js +2 -0
  37. package/core/repository.d.ts +36 -3
  38. package/core/repository.js +90 -45
  39. package/index.d.ts +17 -1
  40. package/index.js +15 -1
  41. package/package.json +11 -8
  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 +46 -60
  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/bin/rman.mjs +0 -4
  87. package/commands/build-command.d.ts +0 -11
  88. package/commands/build-command.js +0 -24
  89. package/commands/changed-command.d.ts +0 -16
  90. package/commands/changed-command.js +0 -32
  91. package/commands/ci-command.d.ts +0 -24
  92. package/commands/ci-command.js +0 -68
  93. package/commands/execute-command.d.ts +0 -19
  94. package/commands/execute-command.js +0 -72
  95. package/commands/info-command.d.ts +0 -10
  96. package/commands/info-command.js +0 -55
  97. package/commands/list-command.d.ts +0 -38
  98. package/commands/list-command.js +0 -134
  99. package/commands/multi-task-command.d.ts +0 -22
  100. package/commands/multi-task-command.js +0 -65
  101. package/commands/publish-command.d.ts +0 -24
  102. package/commands/publish-command.js +0 -131
  103. package/commands/run-command.d.ts +0 -28
  104. package/commands/run-command.js +0 -131
  105. package/commands/version-command.d.ts +0 -25
  106. package/commands/version-command.js +0 -195
  107. package/core/command.d.ts +0 -35
  108. package/core/command.js +0 -116
  109. package/core/constants.d.ts +0 -1
  110. package/core/constants.js +0 -2
  111. package/core/logger.d.ts +0 -12
  112. package/core/logger.js +0 -2
  113. package/utils/file-utils.d.ts +0 -4
  114. package/utils/file-utils.js +0 -28
  115. package/utils/git-utils.d.ts +0 -25
  116. package/utils/git-utils.js +0 -66
  117. package/utils/npm-utils.d.ts +0 -11
  118. package/utils/npm-utils.js +0 -33
  119. package/utils/package-not-found-error.d.ts +0 -2
  120. package/utils/package-not-found-error.js +0 -2
@@ -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;