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,99 @@
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
+ export declare namespace ChangelogService {
5
+ /** Injectable dependencies shared by `getEntries`/`generate` - currently just the npm registry lookup
6
+ * `detectChangeHash` otherwise makes for itself; overridable so tests (and advanced callers)
7
+ * aren't forced through a real network call. */
8
+ interface Deps {
9
+ npmViewVersion?: (name: string, cwd: string) => Promise<string | undefined>;
10
+ }
11
+ interface Options extends PackageFilterOptions {
12
+ /** Generate the changelog since this commit/hash - applied the same way to every package.
13
+ * Default (also `"npm"` explicitly): auto-detect it per package instead, from that package's
14
+ * currently-published npm version (see `detectChangeHash`); a package this can't be resolved
15
+ * for (unpublished, no network, no matching tag) falls back to its own commits not yet
16
+ * pushed to the current branch's upstream (same reference point `--changed`/
17
+ * `--changed-since` use). */
18
+ from?: string;
19
+ /** Generate for the whole repository even when the current directory is inside a single
20
+ * package (which otherwise scopes it to just that package) - see `Repository.currentPackage`. */
21
+ root?: boolean;
22
+ /** Where a package's changelog file lives, relative to *that package's own* directory -
23
+ * default `'CHANGELOG.md'`. Applies the same way to every package; for a package that wants
24
+ * its own filename instead, use `.rmanrc changelog.filePath` (cascaded, per-package
25
+ * overridable) rather than this option - see `resolveFilePath`. Consulted even without
26
+ * `write`: when auto-detecting, if this file already exists its own last-modifying commit
27
+ * also lower-bounds the boundary, so a stale file (last updated for an older version than
28
+ * what's actually published) doesn't get changes silently skipped over - see
29
+ * `detectChangeHash`'s `catchUpFile`. */
30
+ filePath?: string;
31
+ }
32
+ /** One package's (root included) generated changelog entry - what `getEntries`/`generate`
33
+ * return. */
34
+ interface Entry {
35
+ package: Package;
36
+ /** Display name for this entry's heading - `"<repo dir name> repository"` for the root
37
+ * package, its own name otherwise (see `getEntries`'s doc comment on `{{package}}`). */
38
+ label: string;
39
+ /** Resolved from git tags, not package.json - see `resolveVersion`. */
40
+ version: string;
41
+ features: string[];
42
+ fixes: string[];
43
+ other: string[];
44
+ /** The fully rendered entry, via `.rmanrc changelog.template` (or the built-in default). */
45
+ content: string;
46
+ /** Where this entry would be (or, with `options.write`, was) written, relative to the
47
+ * package's own directory - see `GetOptions.filePath`. */
48
+ filePath: string;
49
+ }
50
+ /**
51
+ * Same as `getEntries`, and additionally - for every returned entry, when `options.write` is
52
+ * set - prepends `entry.content` into that package's own changelog file (see `Entry.filePath`).
53
+ * Still pure with respect to console output: writing a file is a real, callable-for-its-own-
54
+ * sake side effect (a "save this" request), not presentation, so it stays here rather than in
55
+ * the CLI command - printing what happened is the command's job.
56
+ */
57
+ function generateToFile(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
58
+ /**
59
+ * Computes a changelog entry per package (root included) from real commits only - either
60
+ * everything since a given `--from <hash>` (applied the same way to every package), or, by
61
+ * default, auto-detected per package instead (see `detectChangeHash`). Pure: returns the
62
+ * entries, never prints or touches `CHANGELOG.md` - see `generate` for that.
63
+ *
64
+ * A commit is attributed to every package its files fall under (root included, for anything
65
+ * outside every package) - unless it's broad enough to count as a repo-wide change (see
66
+ * `BROAD_COMMIT_THRESHOLD`/`ownersOf`), in which case it goes to root alone instead of being
67
+ * repeated verbatim across most of the repository. Subject lines are grouped ✨ Features/🐛 Bug
68
+ * Fixes/🔧 Other Changes on a best-effort Conventional Commits read; anything that doesn't parse
69
+ * just lands in Other Changes as-is, so a repo that doesn't follow that convention still gets a
70
+ * usable list. `.rmanrc changelog.ignoreTypes` (cascaded, e.g. `[chore, dev]`) drops commits of
71
+ * those types entirely instead - see `ignoreTypesConfig`. A bare version-bump commit
72
+ * (`"6.0.1"`) is always dropped outright, regardless of `ignoreTypes` - see
73
+ * `VERSION_BUMP_PATTERN`.
74
+ *
75
+ * By default (or with `--from npm` explicitly), the boundary is auto-detected per package
76
+ * instead of one shared one - see `detectChangeHash`. A package that can't be resolved this way
77
+ * (unpublished, no network, no matching tag) falls back to its own commits not yet pushed to
78
+ * the current branch's upstream (the same reference point `--changed`/`--changed-since` use
79
+ * elsewhere, via `GitHelper.listCommits`) - so the command still produces something useful even
80
+ * for a repo that's never been published or tagged at all. Packages that end up resolving to
81
+ * the same hash (an explicit one, or several packages sharing one tag under fixed versioning)
82
+ * only have their commits fetched once, not once per package.
83
+ *
84
+ * Formatting comes from `.rmanrc changelog.template` - a *path* to a template file (not the
85
+ * template text itself, to keep `.rmanrc` readable), with `{{package}}`/`{{version}}`/
86
+ * `{{date}}`/`{{commits}}` (the full grouped block) and `{{features}}`/`{{fixes}}`/`{{other}}`
87
+ * (their bullet lists alone, for templates that want their own headings/order) - see
88
+ * `resolveTemplate`. `{{package}}` for the repository root is `"<repo dir name> repository"`
89
+ * (e.g. "sqb repository"), not its raw package.json name - which is often a private,
90
+ * non-published placeholder (`"sqb.v4"`) that reads like a stray version marker rather than a
91
+ * recognizable label. `{{version}}` comes from git tags, not package.json (which can drift out
92
+ * of sync with what's actually been released) - see `resolveVersion`/`.rmanrc
93
+ * changelog.tagPattern`.
94
+ *
95
+ * Run from inside a single package's own directory, it only covers that package unless
96
+ * `options.root` says otherwise (see `Repository.currentPackage`).
97
+ */
98
+ function getEntries(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
99
+ }
@@ -0,0 +1,272 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { detectChangeHash, extractVersion, findLatestTag, tagPattern } from '../utils/change-hash.js';
4
+ import { parseConventionalCommit, VERSION_BUMP_PATTERN } from '../utils/conventional-commits.js';
5
+ import { GitHelper } from '../utils/git.js';
6
+ import { filterPackages } from '../utils/package-filter.js';
7
+ export var ChangelogService;
8
+ (function (ChangelogService) {
9
+ /**
10
+ * Same as `getEntries`, and additionally - for every returned entry, when `options.write` is
11
+ * set - prepends `entry.content` into that package's own changelog file (see `Entry.filePath`).
12
+ * Still pure with respect to console output: writing a file is a real, callable-for-its-own-
13
+ * sake side effect (a "save this" request), not presentation, so it stays here rather than in
14
+ * the CLI command - printing what happened is the command's job.
15
+ */
16
+ async function generateToFile(repository, options = {}, deps = {}) {
17
+ const entries = await getEntries(repository, options, deps);
18
+ for (const entry of entries)
19
+ prependToChangelogFile(entry.package, entry.filePath, entry.content);
20
+ return entries;
21
+ }
22
+ ChangelogService.generateToFile = generateToFile;
23
+ /**
24
+ * Computes a changelog entry per package (root included) from real commits only - either
25
+ * everything since a given `--from <hash>` (applied the same way to every package), or, by
26
+ * default, auto-detected per package instead (see `detectChangeHash`). Pure: returns the
27
+ * entries, never prints or touches `CHANGELOG.md` - see `generate` for that.
28
+ *
29
+ * A commit is attributed to every package its files fall under (root included, for anything
30
+ * outside every package) - unless it's broad enough to count as a repo-wide change (see
31
+ * `BROAD_COMMIT_THRESHOLD`/`ownersOf`), in which case it goes to root alone instead of being
32
+ * repeated verbatim across most of the repository. Subject lines are grouped ✨ Features/🐛 Bug
33
+ * Fixes/🔧 Other Changes on a best-effort Conventional Commits read; anything that doesn't parse
34
+ * just lands in Other Changes as-is, so a repo that doesn't follow that convention still gets a
35
+ * usable list. `.rmanrc changelog.ignoreTypes` (cascaded, e.g. `[chore, dev]`) drops commits of
36
+ * those types entirely instead - see `ignoreTypesConfig`. A bare version-bump commit
37
+ * (`"6.0.1"`) is always dropped outright, regardless of `ignoreTypes` - see
38
+ * `VERSION_BUMP_PATTERN`.
39
+ *
40
+ * By default (or with `--from npm` explicitly), the boundary is auto-detected per package
41
+ * instead of one shared one - see `detectChangeHash`. A package that can't be resolved this way
42
+ * (unpublished, no network, no matching tag) falls back to its own commits not yet pushed to
43
+ * the current branch's upstream (the same reference point `--changed`/`--changed-since` use
44
+ * elsewhere, via `GitHelper.listCommits`) - so the command still produces something useful even
45
+ * for a repo that's never been published or tagged at all. Packages that end up resolving to
46
+ * the same hash (an explicit one, or several packages sharing one tag under fixed versioning)
47
+ * only have their commits fetched once, not once per package.
48
+ *
49
+ * Formatting comes from `.rmanrc changelog.template` - a *path* to a template file (not the
50
+ * template text itself, to keep `.rmanrc` readable), with `{{package}}`/`{{version}}`/
51
+ * `{{date}}`/`{{commits}}` (the full grouped block) and `{{features}}`/`{{fixes}}`/`{{other}}`
52
+ * (their bullet lists alone, for templates that want their own headings/order) - see
53
+ * `resolveTemplate`. `{{package}}` for the repository root is `"<repo dir name> repository"`
54
+ * (e.g. "sqb repository"), not its raw package.json name - which is often a private,
55
+ * non-published placeholder (`"sqb.v4"`) that reads like a stray version marker rather than a
56
+ * recognizable label. `{{version}}` comes from git tags, not package.json (which can drift out
57
+ * of sync with what's actually been released) - see `resolveVersion`/`.rmanrc
58
+ * changelog.tagPattern`.
59
+ *
60
+ * Run from inside a single package's own directory, it only covers that package unless
61
+ * `options.root` says otherwise (see `Repository.currentPackage`).
62
+ */
63
+ async function getEntries(repository, options = {}, deps = {}) {
64
+ const cwdScope = options.root ? undefined : repository.currentPackage;
65
+ const packages = repository.getPackages().filter(p => p !== repository.rootPackage);
66
+ const targets = cwdScope ? [cwdScope] : filterPackages([repository.rootPackage, ...packages], options);
67
+ const git = new GitHelper({ cwd: repository.dirname });
68
+ // dropped up front, not just while grouping - a package whose only commits are version bumps
69
+ // should get no entry at all, rather than a heading with nothing real underneath it.
70
+ const dropVersionBumps = (commits) => commits.filter(c => !VERSION_BUMP_PATTERN.test(c.subject));
71
+ // Several packages often resolve to the identical hash (an explicit --from <hash> applies to
72
+ // all of them the same way; under fixed versioning, npm auto-detection usually does too) - so
73
+ // the git fetch for a given hash is cached, run once no matter how many packages share it.
74
+ const commitsByHash = new Map();
75
+ const listCommitsCached = (hash) => {
76
+ const key = hash ?? '';
77
+ let promise = commitsByHash.get(key);
78
+ if (!promise) {
79
+ promise = git.listCommits({ hash }).then(dropVersionBumps);
80
+ commitsByHash.set(key, promise);
81
+ }
82
+ return promise;
83
+ };
84
+ const commitsByTarget = await Promise.all(targets.map(async (pkg) => {
85
+ const catchUpFile = path.join(pkg.dirname, resolveFilePath(pkg, options.filePath));
86
+ const from = await detectChangeHash(git, pkg, {
87
+ from: options.from,
88
+ npmViewVersion: deps.npmViewVersion,
89
+ catchUpFile: fs.existsSync(catchUpFile) ? catchUpFile : undefined,
90
+ });
91
+ return listCommitsCached(from);
92
+ }));
93
+ const entries = [];
94
+ for (let i = 0; i < targets.length; i++) {
95
+ const pkg = targets[i];
96
+ const ownCommits = commitsByTarget[i].filter(c => ownersOf(repository, c).has(pkg));
97
+ if (!ownCommits.length)
98
+ continue;
99
+ const grouped = groupCommits(ownCommits.map(c => c.subject), ignoreTypesConfig(pkg));
100
+ // every commit could have been dropped by ignoreTypes - skip this package's entry entirely
101
+ // rather than rendering a heading with nothing real underneath it.
102
+ if (!grouped.features.length && !grouped.fixes.length && !grouped.other.length)
103
+ continue;
104
+ const label = pkg === repository.rootPackage ? `${path.basename(repository.dirname)} repository` : pkg.name;
105
+ const { version, content } = await renderEntry(repository, pkg, label, grouped, git);
106
+ entries.push({
107
+ package: pkg,
108
+ label,
109
+ version,
110
+ features: grouped.features,
111
+ fixes: grouped.fixes,
112
+ other: grouped.other,
113
+ content,
114
+ filePath: resolveFilePath(pkg, options.filePath),
115
+ });
116
+ }
117
+ return entries;
118
+ }
119
+ ChangelogService.getEntries = getEntries;
120
+ })(ChangelogService || (ChangelogService = {}));
121
+ /** `.rmanrc changelog.ignoreTypes` (cascaded, per-package overridable) - Conventional Commits
122
+ * `type`s to drop entirely (e.g. `[chore, dev]`), not just fold into "Other Changes". Only
123
+ * applies to commits that actually parse as `type: ...` - a non-conventional message always
124
+ * still lands in Other Changes, since it has no `type` to match against. */
125
+ function ignoreTypesConfig(pkg) {
126
+ const v = pkg.config?.changelog?.ignoreTypes;
127
+ return new Set(Array.isArray(v) ? v.map(t => String(t).toLowerCase()) : []);
128
+ }
129
+ function groupCommits(subjects, ignoreTypes = new Set()) {
130
+ const grouped = { features: [], fixes: [], other: [] };
131
+ for (const subject of subjects) {
132
+ if (VERSION_BUMP_PATTERN.test(subject))
133
+ continue;
134
+ const parsed = parseConventionalCommit(subject);
135
+ if (!parsed) {
136
+ grouped.other.push(subject);
137
+ continue;
138
+ }
139
+ const { type, scope, description } = parsed;
140
+ if (ignoreTypes.has(type))
141
+ continue;
142
+ const line = scope ? `**${scope}:** ${description}` : description;
143
+ if (type === 'feat')
144
+ grouped.features.push(line);
145
+ else if (type === 'fix')
146
+ grouped.fixes.push(line);
147
+ else
148
+ grouped.other.push(subject);
149
+ }
150
+ return grouped;
151
+ }
152
+ function bulletList(lines) {
153
+ return lines.map(l => `- ${l}`).join('\n');
154
+ }
155
+ function renderCommitsBlock(grouped) {
156
+ const sections = [];
157
+ if (grouped.features.length)
158
+ sections.push(`### ✨ Features\n\n${bulletList(grouped.features)}`);
159
+ if (grouped.fixes.length)
160
+ sections.push(`### 🐛 Bug Fixes\n\n${bulletList(grouped.fixes)}`);
161
+ if (grouped.other.length)
162
+ sections.push(`### 🔧 Other Changes\n\n${bulletList(grouped.other)}`);
163
+ return sections.join('\n\n');
164
+ }
165
+ const DEFAULT_TEMPLATE = '## {{package}} {{version}} ({{date}})\n\n{{commits}}\n';
166
+ /** `.rmanrc changelog.template` is a *path* to a template file (resolved relative to the
167
+ * repository root, regardless of which level's `.rmanrc` declared it), not the template text
168
+ * itself - keeping multi-line Markdown out of `.rmanrc` so it stays readable. Falls back to a
169
+ * small built-in template when unset. */
170
+ function resolveTemplate(repository, pkg) {
171
+ const templatePath = pkg.config?.changelog?.template;
172
+ if (typeof templatePath !== 'string' || !templatePath)
173
+ return DEFAULT_TEMPLATE;
174
+ const abs = path.resolve(repository.dirname, templatePath);
175
+ if (!fs.existsSync(abs)) {
176
+ throw new Error(`changelog.template not found for "${pkg.name}": "${templatePath}" (resolved to "${abs}")`);
177
+ }
178
+ return fs.readFileSync(abs, 'utf-8');
179
+ }
180
+ function render(template, values) {
181
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => values[key] ?? '');
182
+ }
183
+ const DEFAULT_CHANGELOG_FILE = 'CHANGELOG.md';
184
+ /** Where `write` prepends this package's entry, relative to its own directory. An explicit
185
+ * `optionsFilePath` (`Changelog.Options.filePath`, CLI `--file-path`) applies the same way to
186
+ * every package and wins over `.rmanrc changelog.filePath` (cascaded, per-package overridable),
187
+ * which in turn wins over the default `'CHANGELOG.md'`. */
188
+ function resolveFilePath(pkg, optionsFilePath) {
189
+ if (optionsFilePath)
190
+ return optionsFilePath;
191
+ const cfg = pkg.config?.changelog?.filePath;
192
+ return typeof cfg === 'string' && cfg ? cfg : DEFAULT_CHANGELOG_FILE;
193
+ }
194
+ /**
195
+ * This package's current version, from git tags rather than its (possibly stale - see the
196
+ * `{{version}}` doc on `Changelog.getEntries`) package.json. Falls back to package.json's version
197
+ * if no matching tag exists at all (never tagged, or a fresh package) - see `findLatestTag`.
198
+ */
199
+ async function resolveVersion(git, pkg) {
200
+ const tag = await findLatestTag(git, pkg);
201
+ return tag ? extractVersion(tag, tagPattern(pkg).replace('{name}', pkg.name)) : pkg.version || '';
202
+ }
203
+ /** The most specific package whose directory contains `file` - the repository root itself as the
204
+ * fallback for anything outside every package (e.g. root-level config files). Mirrors
205
+ * `Repository.currentPackage`'s longest-prefix logic, but always resolves to *something*
206
+ * (root), rather than `undefined`, since every file belongs to some changelog. */
207
+ function owningPackage(repository, file) {
208
+ let best = repository.rootPackage;
209
+ for (const pkg of repository.packages) {
210
+ const rel = path.relative(pkg.dirname, file);
211
+ const isSelfOrDescendant = rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
212
+ if (isSelfOrDescendant && pkg.dirname.length > best.dirname.length)
213
+ best = pkg;
214
+ }
215
+ return best;
216
+ }
217
+ /** A commit touching more than this fraction of all packages (a repo-wide relicense, a doc
218
+ * update stamped into every package's README, ...) is treated as a repo-wide change rather than
219
+ * attributed to each of them - see `ownersOf`. Without this, one such commit would show up
220
+ * verbatim in every single package's changelog, making them all look identical. */
221
+ const BROAD_COMMIT_THRESHOLD = 0.5;
222
+ /** ...but only once at least this many packages are actually touched - without a floor, a repo
223
+ * with only 1 or 2 packages would have *every* normal commit look "broad" (100% > 50% of a
224
+ * 1-package repo is trivially true), wrongly attributing ordinary changes to root alone. */
225
+ const BROAD_COMMIT_MIN_PACKAGES = 3;
226
+ /**
227
+ * A commit can touch more than one package at once (or a package plus root-level files) - it's
228
+ * attributed to every package its files map to, not just one. The exception is a commit broad
229
+ * enough to touch at least `BROAD_COMMIT_MIN_PACKAGES` packages *and* more than
230
+ * `BROAD_COMMIT_THRESHOLD` of all of them: that's a repo-wide maintenance change (relicensing, a
231
+ * doc pass across every package, ...), not something that belongs in each package's own release
232
+ * notes individually - it's attributed to the root alone.
233
+ */
234
+ function ownersOf(repository, commit) {
235
+ const owners = new Set();
236
+ for (const f of commit.files)
237
+ owners.add(owningPackage(repository, f));
238
+ const totalPackages = repository.packages.length;
239
+ const nonRootOwners = [...owners].filter(p => p !== repository.rootPackage).length;
240
+ if (totalPackages > 0 &&
241
+ nonRootOwners >= BROAD_COMMIT_MIN_PACKAGES &&
242
+ nonRootOwners > totalPackages * BROAD_COMMIT_THRESHOLD) {
243
+ return new Set([repository.rootPackage]);
244
+ }
245
+ return owners;
246
+ }
247
+ async function renderEntry(repository, pkg, label, grouped, git) {
248
+ const template = resolveTemplate(repository, pkg);
249
+ const version = await resolveVersion(git, pkg);
250
+ const content = render(template, {
251
+ package: label,
252
+ version,
253
+ date: new Date().toISOString().slice(0, 10),
254
+ commits: renderCommitsBlock(grouped),
255
+ features: bulletList(grouped.features),
256
+ fixes: bulletList(grouped.fixes),
257
+ other: bulletList(grouped.other),
258
+ });
259
+ return { version, content };
260
+ }
261
+ /** Prepends `content` right after the top-level "# Changelog" heading if the file already has
262
+ * one, otherwise creates the file (and any missing parent directory - `relFilePath` can nest one,
263
+ * e.g. `'docs/CHANGELOG.md'`) with one. Leaves everything already in the file untouched below it. */
264
+ function prependToChangelogFile(pkg, relFilePath, content) {
265
+ const file = path.join(pkg.dirname, relFilePath);
266
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
267
+ const headerMatch = /^# Changelog\r?\n+/.exec(existing);
268
+ const header = headerMatch ? headerMatch[0] : '# Changelog\n\n';
269
+ const rest = headerMatch ? existing.slice(headerMatch[0].length) : existing;
270
+ fs.mkdirSync(path.dirname(file), { recursive: true });
271
+ fs.writeFileSync(file, header + content.trimEnd() + '\n\n' + rest);
272
+ }
@@ -0,0 +1,40 @@
1
+ import type { Repository } from '../core/repository.js';
2
+ import { type LogLevel } from '../utils/logger.js';
3
+ import { type PackageFilterOptions } from '../utils/package-filter.js';
4
+ export declare namespace CiService {
5
+ type PackageManager = (typeof PACKAGE_MANAGERS)[number];
6
+ interface Options extends PackageFilterOptions {
7
+ packageManager?: PackageManager;
8
+ /** Show the live progress panel while running. Default true, same as `run`/`build`; auto-disabled
9
+ * when stdout isn't a TTY. Doesn't affect what's printed once done - see `run`. */
10
+ progress?: boolean;
11
+ /** Verbosity of the classic per-step log (only applies when the live panel is off). Falls back to
12
+ * the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
13
+ logLevel?: LogLevel;
14
+ }
15
+ /** `.rmanrc packageManager` (root only) picks the package manager used for the final install;
16
+ * explicit CLI value wins over it. Defaults to 'npm'. */
17
+ function resolvePackageManager(repository: Repository, cliValue?: PackageManager): PackageManager;
18
+ /** Deletes `node_modules` and any known lockfile directly under `dirname`. Returns the names
19
+ * that actually existed (and were removed), so the caller can log only those. */
20
+ function wipe(dirname: string): Promise<string[]>;
21
+ /**
22
+ * `ci`: a from-scratch, reproducible install for CI pipelines. For every package (root
23
+ * included), deletes `node_modules` and any lockfile - or, if the package defines its own
24
+ * `"ci"` script, runs that instead. Once every package is clean, installs once at the root
25
+ * with the configured package manager (`npm`/`yarn`/`pnpm`/`bun`).
26
+ *
27
+ * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) while it
28
+ * runs, falling back to a plain rmdir/clean/run/install log line per step when the panel is off.
29
+ *
30
+ * Unlike `run`/`build`, it does *not* end with a per-package success tally: `ci`'s packages don't
31
+ * have independently meaningful outcomes the way a build or test run does - wiping a package is
32
+ * trivial and the one step that can genuinely fail, the install, is a single operation for the
33
+ * whole repository. Counting "N succeeded" across packages would just be noise, so only actual
34
+ * failures get called out (by name, with whatever output they produced), followed by one plain
35
+ * completed/failed line.
36
+ */
37
+ function reinstall(repository: Repository, options?: Options): Promise<void>;
38
+ }
39
+ declare const PACKAGE_MANAGERS: readonly ["npm", "yarn", "pnpm", "bun"];
40
+ export {};
@@ -0,0 +1,204 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import colors from 'ansi-colors';
4
+ import { exec } from '../utils/exec.js';
5
+ import { Logger, resolveRootLogLevel } from '../utils/logger.js';
6
+ import { filterPackages } from '../utils/package-filter.js';
7
+ import { formatDuration, ProgressPanel } from '../utils/progress-panel.js';
8
+ export var CiService;
9
+ (function (CiService) {
10
+ /** `.rmanrc packageManager` (root only) picks the package manager used for the final install;
11
+ * explicit CLI value wins over it. Defaults to 'npm'. */
12
+ function resolvePackageManager(repository, cliValue) {
13
+ if (cliValue)
14
+ return cliValue;
15
+ const configured = repository.config?.packageManager;
16
+ if (configured === undefined)
17
+ return 'npm';
18
+ if (PACKAGE_MANAGERS.includes(configured))
19
+ return configured;
20
+ throw new Error(`Invalid "packageManager" in .rmanrc: "${configured}" (expected one of: ${PACKAGE_MANAGERS.join(', ')})`);
21
+ }
22
+ CiService.resolvePackageManager = resolvePackageManager;
23
+ /** Deletes `node_modules` and any known lockfile directly under `dirname`. Returns the names
24
+ * that actually existed (and were removed), so the caller can log only those. */
25
+ async function wipe(dirname) {
26
+ const removed = [];
27
+ for (const name of ['node_modules', ...LOCK_FILES]) {
28
+ const target = path.join(dirname, name);
29
+ const existed = await fs
30
+ .access(target)
31
+ .then(() => true)
32
+ .catch(() => false);
33
+ if (!existed)
34
+ continue;
35
+ await fs.rm(target, { recursive: true, force: true });
36
+ removed.push(name);
37
+ }
38
+ return removed;
39
+ }
40
+ CiService.wipe = wipe;
41
+ /**
42
+ * `ci`: a from-scratch, reproducible install for CI pipelines. For every package (root
43
+ * included), deletes `node_modules` and any lockfile - or, if the package defines its own
44
+ * `"ci"` script, runs that instead. Once every package is clean, installs once at the root
45
+ * with the configured package manager (`npm`/`yarn`/`pnpm`/`bun`).
46
+ *
47
+ * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) while it
48
+ * runs, falling back to a plain rmdir/clean/run/install log line per step when the panel is off.
49
+ *
50
+ * Unlike `run`/`build`, it does *not* end with a per-package success tally: `ci`'s packages don't
51
+ * have independently meaningful outcomes the way a build or test run does - wiping a package is
52
+ * trivial and the one step that can genuinely fail, the install, is a single operation for the
53
+ * whole repository. Counting "N succeeded" across packages would just be noise, so only actual
54
+ * failures get called out (by name, with whatever output they produced), followed by one plain
55
+ * completed/failed line.
56
+ */
57
+ async function reinstall(repository, options = {}) {
58
+ const packageManager = resolvePackageManager(repository, options.packageManager);
59
+ const logger = new Logger(options.logLevel ?? resolveRootLogLevel(repository));
60
+ // root is handled separately below - for a non-monorepo, getPackages() would otherwise
61
+ // include it a second time (it doubles as "the" package).
62
+ const packages = filterPackages(repository.getPackages().filter(p => p !== repository.rootPackage), options);
63
+ const progress = options.progress ?? true;
64
+ const panel = new ProgressPanel('CI', !!process.stdout.isTTY && progress);
65
+ const runStartedAt = Date.now();
66
+ panel.start();
67
+ const items = [];
68
+ let failed = false;
69
+ try {
70
+ const results = await Promise.allSettled(packages.map(pkg => {
71
+ const item = panel.addItem(pkg.name);
72
+ items.push(item);
73
+ return ciForPackage(pkg, item, panel.enabled, logger);
74
+ }));
75
+ if (results.some(r => r.status === 'rejected'))
76
+ failed = true;
77
+ const rootItem = panel.addItem('root');
78
+ items.push(rootItem);
79
+ try {
80
+ await ciForRoot(repository, rootItem, panel.enabled, packageManager, logger);
81
+ }
82
+ catch {
83
+ failed = true;
84
+ }
85
+ }
86
+ finally {
87
+ panel.stop();
88
+ }
89
+ for (const item of items) {
90
+ if (item.status !== 'failed')
91
+ continue;
92
+ logger.error(colors.red.bold('X'), item.name);
93
+ if (item.log.length)
94
+ logger.error(colors.red(item.log.join('\n')));
95
+ }
96
+ const totalElapsed = formatDuration(Date.now() - runStartedAt);
97
+ const summary = [failed ? colors.red('ci failed') : colors.green('ci completed'), colors.gray(`(${totalElapsed})`)];
98
+ if (failed)
99
+ logger.error(...summary);
100
+ else
101
+ logger.info(...summary);
102
+ if (failed) {
103
+ const err = new Error('"ci" failed');
104
+ err.logged = true;
105
+ throw err;
106
+ }
107
+ }
108
+ CiService.reinstall = reinstall;
109
+ })(CiService || (CiService = {}));
110
+ const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];
111
+ const LOCK_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb'];
112
+ /** Runs a shell `command` in `cwd`, driving `item` for the live panel when it's on, or falling
113
+ * back to the plain "classic" log line `logLine` prints first - the same split `run`'s classic
114
+ * log uses. `logLine` is expected to go through a `Logger` (so it respects `--log-level`), not a
115
+ * raw `console.log`. */
116
+ async function runStep(item, panelEnabled, cwd, command, logLine) {
117
+ if (panelEnabled) {
118
+ await exec(command, {
119
+ cwd,
120
+ stdio: 'pipe',
121
+ onLine: line => {
122
+ item.log.push(line);
123
+ item.lastLine = line;
124
+ },
125
+ });
126
+ }
127
+ else {
128
+ logLine();
129
+ await exec(command, { cwd, stdio: 'inherit' });
130
+ }
131
+ }
132
+ /** Logs (or reflects onto the live panel) the outcome of a `wipe()` call - including the "nothing
133
+ * to remove" case, which otherwise prints nothing at all in the classic log and can look like the
134
+ * package was never processed (most workspace layouts hoist deps to the root's `node_modules`,
135
+ * so an individual package legitimately has nothing of its own to wipe most of the time). */
136
+ function logWipeResult(item, panelEnabled, logger, label, removed) {
137
+ if (panelEnabled) {
138
+ item.lastLine = removed.length ? `removed ${removed.join(', ')}` : 'already clean';
139
+ return;
140
+ }
141
+ if (!removed.length) {
142
+ logger.info(colors.gray('clean'), colors.cyan(label));
143
+ return;
144
+ }
145
+ for (const name of removed)
146
+ logger.info(colors.yellow('rmdir'), colors.cyan(label), name);
147
+ }
148
+ /** A package can opt out of the default wipe by defining its own `"ci"` npm script - if present,
149
+ * that runs instead of the wipe, same as any other rman script. */
150
+ async function ciForPackage(pkg, item, panelEnabled, logger) {
151
+ item.status = 'running';
152
+ item.startedAt = Date.now();
153
+ try {
154
+ const script = pkg.json.scripts?.ci;
155
+ if (typeof script === 'string' && script) {
156
+ item.currentStep = 'ci';
157
+ await runStep(item, panelEnabled, pkg.dirname, script, () => logger.info(colors.cyan('run'), colors.cyan(item.name), script));
158
+ }
159
+ else {
160
+ item.currentStep = 'wipe';
161
+ const removed = await CiService.wipe(pkg.dirname);
162
+ logWipeResult(item, panelEnabled, logger, item.name, removed);
163
+ }
164
+ item.status = 'success';
165
+ }
166
+ catch (e) {
167
+ item.status = 'failed';
168
+ throw e;
169
+ }
170
+ finally {
171
+ item.finishedAt = Date.now();
172
+ }
173
+ }
174
+ /** The root gets one extra step over a regular package once it isn't opting out with its own
175
+ * `"ci"` script: after its own wipe, it installs once for the whole repository. */
176
+ async function ciForRoot(repository, item, panelEnabled, packageManager, logger) {
177
+ item.status = 'running';
178
+ item.startedAt = Date.now();
179
+ try {
180
+ const script = repository.rootPackage.json.scripts?.ci;
181
+ if (typeof script === 'string' && script) {
182
+ item.currentStep = 'ci';
183
+ await runStep(item, panelEnabled, repository.dirname, script, () => logger.info(colors.cyan('run'), colors.cyan('root'), script));
184
+ }
185
+ else {
186
+ item.stepsTotal = 2;
187
+ item.stepIndex = 0;
188
+ item.currentStep = 'wipe';
189
+ const removed = await CiService.wipe(repository.dirname);
190
+ logWipeResult(item, panelEnabled, logger, 'root', removed);
191
+ item.stepIndex = 1;
192
+ item.currentStep = 'install';
193
+ await runStep(item, panelEnabled, repository.dirname, `${packageManager} install`, () => logger.info(colors.cyan('install'), `Running "${packageManager} install"`));
194
+ }
195
+ item.status = 'success';
196
+ }
197
+ catch (e) {
198
+ item.status = 'failed';
199
+ throw e;
200
+ }
201
+ finally {
202
+ item.finishedAt = Date.now();
203
+ }
204
+ }
@@ -0,0 +1,42 @@
1
+ import type { Repository } from '../core/repository.js';
2
+ import { type LogLevel } from '../utils/logger.js';
3
+ import { type PackageFilterOptions } from '../utils/package-filter.js';
4
+ export declare namespace CleanService {
5
+ interface Options extends PackageFilterOptions {
6
+ /** Show the live progress panel. Default true; auto-disabled when stdout isn't a TTY. */
7
+ progress?: boolean;
8
+ /** Report what would be removed without actually removing anything. Default false. */
9
+ dryRun?: boolean;
10
+ /** Clean the whole repository even when the current directory is inside a single package
11
+ * (which otherwise scopes cleaning to just that package - see `Repository.currentPackage`).
12
+ * Has no effect when already at the repository root, or outside any known package. */
13
+ root?: boolean;
14
+ /** Verbosity of the classic per-item log (only applies when the live panel is off). Falls back
15
+ * to the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
16
+ logLevel?: LogLevel;
17
+ }
18
+ /**
19
+ * `clean`: removes build output across every package (root included) - the built-in replacement
20
+ * for `ts-cleanup`, plus whatever else `.rmanrc clean.include`/`clean.exclude` says to remove.
21
+ *
22
+ * For every package not opted out via its own (cascaded) `clean.skip: true`:
23
+ * - deletes compiled `.js`/`.js.map`/`.d.ts` files under its `src`/`test` (see `cleanTsArtifacts`);
24
+ * - deletes any `*.tsbuildinfo` incremental-build cache file anywhere in it;
25
+ * - deletes anything matching its own (cascaded) `clean.include` glob(s), minus `clean.exclude`.
26
+ *
27
+ * `include`/`exclude` are resolved relative to *that* package's own directory - a root-level
28
+ * `.rmanrc` pattern like `packages/*\/build` is naturally evaluated from the repository root
29
+ * (spanning every package in one pass), while a package's own override in its own `.rmanrc`
30
+ * (e.g. `include: ./cache`) only ever reaches that one package, since a package normally
31
+ * overrides rather than merges with the root's value for the same key.
32
+ *
33
+ * Never touches `node_modules` - that's `ci`'s job, not this one.
34
+ *
35
+ * Run from inside a single package's own directory, it only cleans that package unless
36
+ * `options.root` says otherwise (see `Repository.currentPackage`).
37
+ *
38
+ * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) - unlike
39
+ * `ci`, cleaning genuinely is independent per-package work, so the final per-package tally stays.
40
+ */
41
+ function clean(repository: Repository, options?: Options): Promise<void>;
42
+ }