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,201 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import colors from 'ansi-colors';
4
+ import { exec, filterPackages, formatDuration, Logger, ProgressPanel, resolveRootLogLevel, } from 'rman';
5
+ export var CiService;
6
+ (function (CiService) {
7
+ /** `.rmanrc packageManager` (root only) picks the package manager used for the final install;
8
+ * explicit CLI value wins over it. Defaults to 'npm'. */
9
+ function resolvePackageManager(repository, cliValue) {
10
+ if (cliValue)
11
+ return cliValue;
12
+ const configured = repository.config?.packageManager;
13
+ if (configured === undefined)
14
+ return 'npm';
15
+ if (PACKAGE_MANAGERS.includes(configured))
16
+ return configured;
17
+ throw new Error(`Invalid "packageManager" in .rmanrc: "${configured}" (expected one of: ${PACKAGE_MANAGERS.join(', ')})`);
18
+ }
19
+ CiService.resolvePackageManager = resolvePackageManager;
20
+ /** Deletes `node_modules` and any known lockfile directly under `dirname`. Returns the names
21
+ * that actually existed (and were removed), so the caller can log only those. */
22
+ async function wipe(dirname) {
23
+ const removed = [];
24
+ for (const name of ['node_modules', ...LOCK_FILES]) {
25
+ const target = path.join(dirname, name);
26
+ const existed = await fs
27
+ .access(target)
28
+ .then(() => true)
29
+ .catch(() => false);
30
+ if (!existed)
31
+ continue;
32
+ await fs.rm(target, { recursive: true, force: true });
33
+ removed.push(name);
34
+ }
35
+ return removed;
36
+ }
37
+ CiService.wipe = wipe;
38
+ /**
39
+ * `ci`: a from-scratch, reproducible install for CI pipelines. For every package (root
40
+ * included), deletes `node_modules` and any lockfile - or, if the package defines its own
41
+ * `"ci"` script, runs that instead. Once every package is clean, installs once at the root
42
+ * with the configured package manager (`npm`/`yarn`/`pnpm`/`bun`).
43
+ *
44
+ * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) while it
45
+ * runs, falling back to a plain rmdir/clean/run/install log line per step when the panel is off.
46
+ *
47
+ * Unlike `run`/`build`, it does *not* end with a per-package success tally: `ci`'s packages don't
48
+ * have independently meaningful outcomes the way a build or test run does - wiping a package is
49
+ * trivial and the one step that can genuinely fail, the install, is a single operation for the
50
+ * whole repository. Counting "N succeeded" across packages would just be noise, so only actual
51
+ * failures get called out (by name, with whatever output they produced), followed by one plain
52
+ * completed/failed line.
53
+ */
54
+ async function reinstall(repository, options = {}) {
55
+ const packageManager = resolvePackageManager(repository, options.packageManager);
56
+ const logger = new Logger(options.logLevel ?? resolveRootLogLevel(repository));
57
+ // root is handled separately below - for a non-monorepo, getPackages() would otherwise
58
+ // include it a second time (it doubles as "the" package).
59
+ const packages = filterPackages(repository.getPackages().filter(p => p !== repository.rootPackage), options);
60
+ const progress = options.progress ?? true;
61
+ const panel = new ProgressPanel('CI', !!process.stdout.isTTY && progress);
62
+ const runStartedAt = Date.now();
63
+ panel.start();
64
+ const items = [];
65
+ let failed = false;
66
+ try {
67
+ const results = await Promise.allSettled(packages.map(pkg => {
68
+ const item = panel.addItem(pkg.name);
69
+ items.push(item);
70
+ return ciForPackage(pkg, item, panel.enabled, logger);
71
+ }));
72
+ if (results.some(r => r.status === 'rejected'))
73
+ failed = true;
74
+ const rootItem = panel.addItem('root');
75
+ items.push(rootItem);
76
+ try {
77
+ await ciForRoot(repository, rootItem, panel.enabled, packageManager, logger);
78
+ }
79
+ catch {
80
+ failed = true;
81
+ }
82
+ }
83
+ finally {
84
+ panel.stop();
85
+ }
86
+ for (const item of items) {
87
+ if (item.status !== 'failed')
88
+ continue;
89
+ logger.error(colors.red.bold('X'), item.name);
90
+ if (item.log.length)
91
+ logger.error(colors.red(item.log.join('\n')));
92
+ }
93
+ const totalElapsed = formatDuration(Date.now() - runStartedAt);
94
+ const summary = [failed ? colors.red('ci failed') : colors.green('ci completed'), colors.gray(`(${totalElapsed})`)];
95
+ if (failed)
96
+ logger.error(...summary);
97
+ else
98
+ logger.info(...summary);
99
+ if (failed) {
100
+ const err = new Error('"ci" failed');
101
+ err.logged = true;
102
+ throw err;
103
+ }
104
+ }
105
+ CiService.reinstall = reinstall;
106
+ })(CiService || (CiService = {}));
107
+ const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];
108
+ const LOCK_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb'];
109
+ /** Runs a shell `command` in `cwd`, driving `item` for the live panel when it's on, or falling
110
+ * back to the plain "classic" log line `logLine` prints first - the same split `run`'s classic
111
+ * log uses. `logLine` is expected to go through a `Logger` (so it respects `--log-level`), not a
112
+ * raw `console.log`. */
113
+ async function runStep(item, panelEnabled, cwd, command, logLine) {
114
+ if (panelEnabled) {
115
+ await exec(command, {
116
+ cwd,
117
+ stdio: 'pipe',
118
+ onLine: line => {
119
+ item.log.push(line);
120
+ item.lastLine = line;
121
+ },
122
+ });
123
+ }
124
+ else {
125
+ logLine();
126
+ await exec(command, { cwd, stdio: 'inherit' });
127
+ }
128
+ }
129
+ /** Logs (or reflects onto the live panel) the outcome of a `wipe()` call - including the "nothing
130
+ * to remove" case, which otherwise prints nothing at all in the classic log and can look like the
131
+ * package was never processed (most workspace layouts hoist deps to the root's `node_modules`,
132
+ * so an individual package legitimately has nothing of its own to wipe most of the time). */
133
+ function logWipeResult(item, panelEnabled, logger, label, removed) {
134
+ if (panelEnabled) {
135
+ item.lastLine = removed.length ? `removed ${removed.join(', ')}` : 'already clean';
136
+ return;
137
+ }
138
+ if (!removed.length) {
139
+ logger.info(colors.gray('clean'), colors.cyan(label));
140
+ return;
141
+ }
142
+ for (const name of removed)
143
+ logger.info(colors.yellow('rmdir'), colors.cyan(label), name);
144
+ }
145
+ /** A package can opt out of the default wipe by defining its own `"ci"` npm script - if present,
146
+ * that runs instead of the wipe, same as any other rman script. */
147
+ async function ciForPackage(pkg, item, panelEnabled, logger) {
148
+ item.status = 'running';
149
+ item.startedAt = Date.now();
150
+ try {
151
+ const script = pkg.manifest.raw.scripts?.ci;
152
+ if (typeof script === 'string' && script) {
153
+ item.currentStep = 'ci';
154
+ await runStep(item, panelEnabled, pkg.dirname, script, () => logger.info(colors.cyan('run'), colors.cyan(item.name), script));
155
+ }
156
+ else {
157
+ item.currentStep = 'wipe';
158
+ const removed = await CiService.wipe(pkg.dirname);
159
+ logWipeResult(item, panelEnabled, logger, item.name, removed);
160
+ }
161
+ item.status = 'success';
162
+ }
163
+ catch (e) {
164
+ item.status = 'failed';
165
+ throw e;
166
+ }
167
+ finally {
168
+ item.finishedAt = Date.now();
169
+ }
170
+ }
171
+ /** The root gets one extra step over a regular package once it isn't opting out with its own
172
+ * `"ci"` script: after its own wipe, it installs once for the whole repository. */
173
+ async function ciForRoot(repository, item, panelEnabled, packageManager, logger) {
174
+ item.status = 'running';
175
+ item.startedAt = Date.now();
176
+ try {
177
+ const script = repository.rootPackage.manifest.raw.scripts?.ci;
178
+ if (typeof script === 'string' && script) {
179
+ item.currentStep = 'ci';
180
+ await runStep(item, panelEnabled, repository.dirname, script, () => logger.info(colors.cyan('run'), colors.cyan('root'), script));
181
+ }
182
+ else {
183
+ item.stepsTotal = 2;
184
+ item.stepIndex = 0;
185
+ item.currentStep = 'wipe';
186
+ const removed = await CiService.wipe(repository.dirname);
187
+ logWipeResult(item, panelEnabled, logger, 'root', removed);
188
+ item.stepIndex = 1;
189
+ item.currentStep = 'install';
190
+ await runStep(item, panelEnabled, repository.dirname, `${packageManager} install`, () => logger.info(colors.cyan('install'), `Running "${packageManager} install"`));
191
+ }
192
+ item.status = 'success';
193
+ }
194
+ catch (e) {
195
+ item.status = 'failed';
196
+ throw e;
197
+ }
198
+ finally {
199
+ item.finishedAt = Date.now();
200
+ }
201
+ }
@@ -0,0 +1,51 @@
1
+ import { type LogLevel, type PackageFilterOptions, type Repository } from 'rman';
2
+ export declare namespace CleanService {
3
+ interface Options extends PackageFilterOptions {
4
+ /** Show the live progress panel. Default true; auto-disabled when stdout isn't a TTY. */
5
+ progress?: boolean;
6
+ /** Report what would be removed without actually removing anything. Default false. */
7
+ dryRun?: boolean;
8
+ /** Clean the whole repository even when the current directory is inside a single package
9
+ * (which otherwise scopes cleaning to just that package - see `Repository.currentPackage`).
10
+ * Has no effect when already at the repository root, or outside any known package. */
11
+ root?: boolean;
12
+ /** Verbosity of the classic per-item log (only applies when the live panel is off). Falls back
13
+ * to the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
14
+ logLevel?: LogLevel;
15
+ }
16
+ /**
17
+ * `clean`: removes build output across every package (root included) - the replacement for
18
+ * `ts-cleanup`, plus whatever else `.rmanrc clean.include`/`clean.exclude` says to remove.
19
+ *
20
+ * **In `rman-node` rather than rman's core, because what it knows how to delete is not
21
+ * repository-shaped knowledge but TypeScript-shaped**: a compiled `.js`/`.js.map`/`.d.ts` beside
22
+ * its `.ts` source, a `*.tsbuildinfo`, and a `node_modules` to skip while looking. A Cargo or Go
23
+ * repository has `cargo clean` and `go clean` and nothing here would fire for it, so a core
24
+ * `clean` was a command that only appeared to be general.
25
+ *
26
+ * The `clean.include`/`clean.exclude` globs move with it, and that is the one cost worth naming:
27
+ * they are ecosystem-neutral, so a repository wanting only those now has to name this plugin (or
28
+ * write the two `rm` lines as a `run` script). Keeping a stub `clean` in core for them would mean
29
+ * two commands with one name and a precedence rule between them - worse than the honest move.
30
+ *
31
+ * For every package not opted out via its own (cascaded) `clean.skip: true`:
32
+ * - deletes compiled `.js`/`.js.map`/`.d.ts` files under its `src`/`test` (see `cleanTsArtifacts`);
33
+ * - deletes any `*.tsbuildinfo` incremental-build cache file anywhere in it;
34
+ * - deletes anything matching its own (cascaded) `clean.include` glob(s), minus `clean.exclude`.
35
+ *
36
+ * `include`/`exclude` are resolved relative to *that* package's own directory - a root-level
37
+ * `.rmanrc` pattern like `packages/*\/build` is naturally evaluated from the repository root
38
+ * (spanning every package in one pass), while a package's own override in its own `.rmanrc`
39
+ * (e.g. `include: ./cache`) only ever reaches that one package, since a package normally
40
+ * overrides rather than merges with the root's value for the same key.
41
+ *
42
+ * Never touches `node_modules` - that's `ci`'s job, not this one.
43
+ *
44
+ * Run from inside a single package's own directory, it only cleans that package unless
45
+ * `options.root` says otherwise (see `Repository.currentPackage`).
46
+ *
47
+ * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) - unlike
48
+ * `ci`, cleaning genuinely is independent per-package work, so the final per-package tally stays.
49
+ */
50
+ function clean(repository: Repository, options?: Options): Promise<void>;
51
+ }
@@ -0,0 +1,235 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import colors from 'ansi-colors';
4
+ import fg from 'fast-glob';
5
+ import { filterPackages, Logger, ProgressPanel, resolveRootLogLevel, } from 'rman';
6
+ export var CleanService;
7
+ (function (CleanService) {
8
+ /**
9
+ * `clean`: removes build output across every package (root included) - the replacement for
10
+ * `ts-cleanup`, plus whatever else `.rmanrc clean.include`/`clean.exclude` says to remove.
11
+ *
12
+ * **In `rman-node` rather than rman's core, because what it knows how to delete is not
13
+ * repository-shaped knowledge but TypeScript-shaped**: a compiled `.js`/`.js.map`/`.d.ts` beside
14
+ * its `.ts` source, a `*.tsbuildinfo`, and a `node_modules` to skip while looking. A Cargo or Go
15
+ * repository has `cargo clean` and `go clean` and nothing here would fire for it, so a core
16
+ * `clean` was a command that only appeared to be general.
17
+ *
18
+ * The `clean.include`/`clean.exclude` globs move with it, and that is the one cost worth naming:
19
+ * they are ecosystem-neutral, so a repository wanting only those now has to name this plugin (or
20
+ * write the two `rm` lines as a `run` script). Keeping a stub `clean` in core for them would mean
21
+ * two commands with one name and a precedence rule between them - worse than the honest move.
22
+ *
23
+ * For every package not opted out via its own (cascaded) `clean.skip: true`:
24
+ * - deletes compiled `.js`/`.js.map`/`.d.ts` files under its `src`/`test` (see `cleanTsArtifacts`);
25
+ * - deletes any `*.tsbuildinfo` incremental-build cache file anywhere in it;
26
+ * - deletes anything matching its own (cascaded) `clean.include` glob(s), minus `clean.exclude`.
27
+ *
28
+ * `include`/`exclude` are resolved relative to *that* package's own directory - a root-level
29
+ * `.rmanrc` pattern like `packages/*\/build` is naturally evaluated from the repository root
30
+ * (spanning every package in one pass), while a package's own override in its own `.rmanrc`
31
+ * (e.g. `include: ./cache`) only ever reaches that one package, since a package normally
32
+ * overrides rather than merges with the root's value for the same key.
33
+ *
34
+ * Never touches `node_modules` - that's `ci`'s job, not this one.
35
+ *
36
+ * Run from inside a single package's own directory, it only cleans that package unless
37
+ * `options.root` says otherwise (see `Repository.currentPackage`).
38
+ *
39
+ * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) - unlike
40
+ * `ci`, cleaning genuinely is independent per-package work, so the final per-package tally stays.
41
+ */
42
+ async function clean(repository, options = {}) {
43
+ /** Standing inside a single package's own directory scopes cleaning to just that package
44
+ * (root's own artifacts included) unless `--root` asks for the whole repository anyway - a
45
+ * no-op when already at the root, or outside any known package. */
46
+ const cwdScope = options.root ? undefined : repository.currentPackage;
47
+ const packages = repository.getPackages().filter(p => p !== repository.rootPackage);
48
+ const allTargets = cwdScope ? [cwdScope] : [repository.rootPackage, ...packages];
49
+ const targets = filterPackages(allTargets, options).filter(pkg => !cleanConfig(pkg).skip);
50
+ const dryRun = options.dryRun ?? false;
51
+ const progress = options.progress ?? true;
52
+ const logger = new Logger(options.logLevel ?? resolveRootLogLevel(repository));
53
+ const panel = new ProgressPanel(dryRun ? 'CLEAN (dry-run)' : 'CLEAN', !!process.stdout.isTTY && progress);
54
+ panel.start();
55
+ let failed = false;
56
+ try {
57
+ const results = await Promise.allSettled(targets.map(pkg => {
58
+ const label = pkg === repository.rootPackage ? 'root' : pkg.name;
59
+ return cleanPackage(pkg, panel.addItem(label), panel.enabled, dryRun, logger);
60
+ }));
61
+ if (results.some(r => r.status === 'rejected'))
62
+ failed = true;
63
+ }
64
+ finally {
65
+ panel.stop();
66
+ }
67
+ panel.printSummary();
68
+ if (failed) {
69
+ const err = new Error('"clean" failed');
70
+ err.logged = true;
71
+ throw err;
72
+ }
73
+ }
74
+ CleanService.clean = clean;
75
+ })(CleanService || (CleanService = {}));
76
+ /** Directories a package's TypeScript output gets cleaned from, mirroring `ts-cleanup -s src`
77
+ * and `-s test` (the only way it was ever actually invoked in this project's own scripts). */
78
+ const TS_SOURCE_DIRS = ['src', 'test'];
79
+ /** A package's (cascaded) `.rmanrc clean.include`/`clean.exclude`/`clean.skip` - a package that
80
+ * declares its own `clean` block replaces the root's entirely for itself (same as any other
81
+ * rman config key), rather than combining with it. `include`/`exclude` accept a single glob
82
+ * string or an array of them. */
83
+ function cleanConfig(pkg) {
84
+ const cfg = pkg.config?.clean;
85
+ const normalize = (v) => {
86
+ if (Array.isArray(v))
87
+ return v.map(String);
88
+ return typeof v === 'string' && v ? [v] : [];
89
+ };
90
+ return { include: normalize(cfg?.include), exclude: normalize(cfg?.exclude), skip: cfg?.skip === true };
91
+ }
92
+ /** Deletes `file`, unless `dryRun` - either way the caller treats it as removed for reporting. */
93
+ async function remove(file, dryRun) {
94
+ if (!dryRun)
95
+ await fs.promises.rm(file, { force: true });
96
+ }
97
+ /** Removes now-empty directories under `dir`, deepest first (so a parent left empty once its
98
+ * only child is removed gets caught in the same pass). Also removes `dir` itself once empty
99
+ * when `includeSelf` is set - used when `dir` is itself something the user asked to delete
100
+ * (e.g. an `include: build` match), as opposed to a source root like `src`/`test` that should
101
+ * stay even if everything inside it was cleaned out. No-op in dry-run mode: nothing was actually
102
+ * deleted, so there's nothing that could have been left empty. */
103
+ function pruneEmptyDirs(dir, includeSelf, dryRun) {
104
+ if (dryRun || !fs.existsSync(dir))
105
+ return;
106
+ const entries = fg.sync('**', { cwd: dir, onlyDirectories: true, dot: true });
107
+ entries.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
108
+ for (const rel of entries) {
109
+ const abs = path.join(dir, rel);
110
+ if (fs.existsSync(abs) && fs.readdirSync(abs).length === 0)
111
+ fs.rmdirSync(abs);
112
+ }
113
+ if (includeSelf && fs.readdirSync(dir).length === 0)
114
+ fs.rmdirSync(dir);
115
+ }
116
+ /**
117
+ * Deletes every path matched by `include` (minus `exclude`) under `dirname` - files and
118
+ * directories alike, gulp-`src()`-style. Returns what was actually removed (or, in dry-run mode,
119
+ * what *would* be), relative to `dirname`, so the caller only logs what really happened.
120
+ *
121
+ * `exclude` protects at two levels: a pattern matching a whole `include` result directly (e.g.
122
+ * `packages/pkg1/*` against a matched `packages/pkg1/build` directory) drops that result before
123
+ * anything under it is touched; a finer pattern (e.g. `build/*.json`) instead protects just the
124
+ * matching files *inside* an otherwise-deleted directory, leaving the rest of it gone and the
125
+ * protected files (and their now non-empty parent) in place.
126
+ */
127
+ async function cleanGlobs(dirname, include, exclude, dryRun) {
128
+ if (!include.length)
129
+ return [];
130
+ const matches = await fg(include, { cwd: dirname, ignore: exclude, onlyFiles: false, dot: true, absolute: true });
131
+ const protectedFiles = exclude.length
132
+ ? new Set(await fg(exclude, { cwd: dirname, onlyFiles: true, dot: true, absolute: true }))
133
+ : new Set();
134
+ const removed = [];
135
+ for (const m of matches) {
136
+ const stat = await fs.promises.stat(m).catch(() => undefined);
137
+ if (!stat)
138
+ continue;
139
+ if (stat.isDirectory()) {
140
+ const filesInside = await fg('**', { cwd: m, onlyFiles: true, dot: true, absolute: true });
141
+ for (const f of filesInside) {
142
+ if (protectedFiles.has(f))
143
+ continue;
144
+ await remove(f, dryRun);
145
+ removed.push(path.relative(dirname, f));
146
+ }
147
+ pruneEmptyDirs(m, true, dryRun);
148
+ }
149
+ else if (!protectedFiles.has(m)) {
150
+ await remove(m, dryRun);
151
+ removed.push(path.relative(dirname, m));
152
+ }
153
+ }
154
+ return removed;
155
+ }
156
+ /**
157
+ * Removes compiled TypeScript output (`.js`, `.js.map`, `.d.ts`) sitting next to its `.ts`
158
+ * source under `src`/`test` - the same job `ts-cleanup -s <dir> --all` did (the only mode this
159
+ * project ever actually used it in). A `.d.ts` with no matching `.ts`/`.tsx` is left alone - it's
160
+ * presumably a hand-written declaration file, not build output. Prunes directories left empty
161
+ * afterward.
162
+ */
163
+ async function cleanTsArtifacts(dirname, dryRun) {
164
+ const removed = [];
165
+ for (const sub of TS_SOURCE_DIRS) {
166
+ const dir = path.join(dirname, sub);
167
+ if (!fs.existsSync(dir))
168
+ continue;
169
+ const files = await fg('**/*.{js,js.map,d.ts}', { cwd: dir, onlyFiles: true, dot: true, absolute: true });
170
+ for (const f of files) {
171
+ if (f.endsWith('.d.ts')) {
172
+ const base = f.slice(0, -'.d.ts'.length);
173
+ if (!fs.existsSync(base + '.ts') && !fs.existsSync(base + '.tsx'))
174
+ continue;
175
+ }
176
+ await remove(f, dryRun);
177
+ removed.push(path.relative(dirname, f));
178
+ }
179
+ pruneEmptyDirs(dir, false, dryRun);
180
+ }
181
+ return removed;
182
+ }
183
+ /**
184
+ * Removes TypeScript's incremental-build cache files (`*.tsbuildinfo`) anywhere under `dirname` -
185
+ * `tsc --build`/`composite`/`incremental` output that `ts-cleanup` never touched, but which can
186
+ * leave a project in a stale state if a clean rebuild is expected to start from nothing. Skips
187
+ * `node_modules` (dependencies may ship their own, irrelevant to this package's own build).
188
+ */
189
+ async function cleanTsBuildInfo(dirname, dryRun) {
190
+ const files = await fg('**/*.tsbuildinfo', {
191
+ cwd: dirname,
192
+ ignore: ['**/node_modules/**'],
193
+ onlyFiles: true,
194
+ dot: true,
195
+ absolute: true,
196
+ });
197
+ for (const f of files)
198
+ await remove(f, dryRun);
199
+ return files.map(f => path.relative(dirname, f));
200
+ }
201
+ async function cleanPackage(pkg, item, panelEnabled, dryRun, logger) {
202
+ item.status = 'running';
203
+ item.startedAt = Date.now();
204
+ try {
205
+ item.currentStep = 'ts';
206
+ const tsRemoved = await cleanTsArtifacts(pkg.dirname, dryRun);
207
+ const buildInfoRemoved = await cleanTsBuildInfo(pkg.dirname, dryRun);
208
+ item.currentStep = 'glob';
209
+ const { include, exclude } = cleanConfig(pkg);
210
+ const globRemoved = await cleanGlobs(pkg.dirname, include, exclude, dryRun);
211
+ const removed = [...tsRemoved, ...buildInfoRemoved, ...globRemoved];
212
+ const verb = dryRun ? 'would rm' : 'rm';
213
+ if (panelEnabled) {
214
+ item.lastLine = removed.length
215
+ ? `${dryRun ? 'would remove' : 'removed'} ${removed.length} item(s)`
216
+ : 'already clean';
217
+ }
218
+ else if (removed.length) {
219
+ for (const r of removed)
220
+ logger.info(colors.yellow(verb), colors.cyan(item.name), r);
221
+ }
222
+ else {
223
+ logger.info(colors.gray('clean'), colors.cyan(item.name));
224
+ }
225
+ item.status = 'success';
226
+ }
227
+ catch (e) {
228
+ item.status = 'failed';
229
+ item.log.push(e?.message ?? String(e));
230
+ throw e;
231
+ }
232
+ finally {
233
+ item.finishedAt = Date.now();
234
+ }
235
+ }
@@ -0,0 +1,77 @@
1
+ import { type Package, type PackageFilterOptions, type Repository } from 'rman';
2
+ import { CiService } from './ci.service.js';
3
+ export declare namespace PublishService {
4
+ /** Injectable registry lookup - mainly for tests, so they don't depend on network access or a
5
+ * real published package. Same shape as `detectChangeHash`'s own `npmViewVersion`. */
6
+ interface Deps {
7
+ npmViewVersion?: (name: string, cwd: string) => Promise<string | undefined>;
8
+ }
9
+ interface Options extends PackageFilterOptions {
10
+ /** A package with uncommitted local changes is excluded (status `'skip'`) instead of aborting
11
+ * the whole plan (status `'error'`) - same as `version`'s own option. Default false. */
12
+ ignoreDirty?: boolean;
13
+ /** Registry to check against (and, in `applyPlan`, publish to) - `.npmrc`'s own configured
14
+ * registry is used when omitted. */
15
+ registry?: string;
16
+ /** Path to a custom `.npmrc`, for both the registry check and the actual publish. */
17
+ userconfig?: string;
18
+ }
19
+ interface ApplyOptions extends Options {
20
+ packageManager?: CiService.PackageManager;
21
+ /** `npm publish --access <access>` - required by the registry for a *new* scoped package. */
22
+ access?: 'public' | 'restricted';
23
+ /** `npm publish --tag <tag>` - the dist-tag this version is published under (default `latest`). */
24
+ tag?: string;
25
+ /** `npm publish --otp <otp>` - a 2FA one-time password, for registries that require it. */
26
+ otp?: string;
27
+ /** Subdirectory to publish from, relative to the package's own directory - only consulted when
28
+ * the package doesn't already declare its own `package.json` `publishConfig.directory` (npm's
29
+ * native mechanism for this, which always wins when present). */
30
+ contents?: string;
31
+ }
32
+ /** One package's outcome in a publish plan - see `getPlan`. */
33
+ interface Entry {
34
+ package: Package;
35
+ version: string;
36
+ status: 'publish' | 'skip' | 'up-to-date' | 'error';
37
+ /** What's currently on the registry, if anything - only set once a registry check actually ran. */
38
+ registryVersion?: string;
39
+ reason?: string;
40
+ }
41
+ /**
42
+ * Computes what `publish` *would* do, across every non-private package (topological order,
43
+ * dependencies before dependents) - never touches the registry to publish anything, just
44
+ * queries it to decide, so it's safe to call any time, including as the plan a bare `rman
45
+ * publish` shows before asking for confirmation.
46
+ *
47
+ * A `private: true` package is always `'skip'`ped outright. A package with uncommitted local
48
+ * changes is `'error'` (aborts the whole plan) unless `options.ignoreDirty` downgrades it to
49
+ * `'skip'` instead - same rule `version` uses, since publishing untracked local edits is worse
50
+ * than a bad commit. Otherwise, its currently-published registry version (via `npm view`,
51
+ * queried concurrently across every remaining package) decides the rest: identical to the local
52
+ * `package.json` version is `'up-to-date'`; anything else (including never having been
53
+ * published at all) is `'publish'`.
54
+ *
55
+ * Deliberately decoupled from `version`: this only ever looks at what's *currently* on disk and
56
+ * on the registry, never at whether `version` was just run - so it works equally well right
57
+ * after a version bump, or standing alone in a release pipeline that bumped days earlier.
58
+ *
59
+ * A monorepo's root package is never a candidate at all - `repository.getPackages()` already
60
+ * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
61
+ * where it's a normal candidate like any other).
62
+ */
63
+ function getPlan(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
64
+ /**
65
+ * Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
66
+ * see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
67
+ * concurrent: unlike `run`/`build`, a package genuinely needs its own dependencies to have
68
+ * landed on the registry first, and publishing is rare enough (once per release) that the
69
+ * simplicity is worth more than the parallelism `run` gets from `power-tasks`.
70
+ *
71
+ * If a package fails, every other still-pending entry depending on it (transitively) is marked
72
+ * `'error'` too and never attempted - publishing a package whose own new dependency range points
73
+ * at a version that never actually reached the registry would hand consumers a broken install.
74
+ * A package's own failure doesn't stop unrelated packages elsewhere in the plan, though.
75
+ */
76
+ function applyPlan(repository: Repository, plan: Entry[], options?: ApplyOptions): Promise<Entry[]>;
77
+ }