rman 0.38.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +371 -21
- package/cli.js +51 -29
- package/commands/build.command.d.ts +3 -0
- package/commands/build.command.js +14 -0
- package/commands/changed.command.d.ts +3 -0
- package/commands/changed.command.js +29 -0
- package/commands/changelog.command.d.ts +3 -0
- package/commands/changelog.command.js +63 -0
- package/commands/ci.command.d.ts +3 -0
- package/commands/ci.command.js +30 -0
- package/commands/clean.command.d.ts +3 -0
- package/commands/clean.command.js +36 -0
- package/commands/diff.command.d.ts +3 -0
- package/commands/diff.command.js +51 -0
- package/commands/exec.command.d.ts +3 -0
- package/commands/exec.command.js +83 -0
- package/commands/import.command.d.ts +3 -0
- package/commands/import.command.js +27 -0
- package/commands/info.command.d.ts +3 -0
- package/commands/info.command.js +61 -0
- package/commands/list.command.d.ts +3 -0
- package/commands/list.command.js +113 -0
- package/commands/publish.command.d.ts +3 -0
- package/commands/publish.command.js +147 -0
- package/commands/run.command.d.ts +6 -0
- package/commands/run.command.js +80 -0
- package/commands/test.command.d.ts +3 -0
- package/commands/test.command.js +14 -0
- package/commands/version.command.d.ts +3 -0
- package/commands/version.command.js +125 -0
- package/constants.js +1 -1
- package/core/config.d.ts +13 -0
- package/core/config.js +61 -0
- package/core/package.d.ts +2 -0
- package/core/package.js +2 -0
- package/core/repository.d.ts +36 -3
- package/core/repository.js +90 -45
- package/index.d.ts +17 -1
- package/index.js +15 -1
- package/package.json +8 -4
- package/rmanrc.schema.json +202 -0
- package/services/changelog.service.d.ts +99 -0
- package/services/changelog.service.js +272 -0
- package/services/ci.service.d.ts +40 -0
- package/services/ci.service.js +204 -0
- package/services/clean.service.d.ts +42 -0
- package/services/clean.service.js +226 -0
- package/services/exec.service.d.ts +35 -0
- package/services/exec.service.js +114 -0
- package/services/import.service.d.ts +30 -0
- package/services/import.service.js +84 -0
- package/services/list.service.d.ts +27 -0
- package/services/list.service.js +25 -0
- package/services/publish.service.d.ts +79 -0
- package/services/publish.service.js +199 -0
- package/services/run.service.d.ts +99 -0
- package/services/run.service.js +432 -0
- package/services/system-info.d.ts +16 -0
- package/services/system-info.js +25 -0
- package/services/version.service.d.ts +95 -0
- package/services/version.service.js +450 -0
- package/services.d.ts +10 -0
- package/services.js +10 -0
- package/utils/branch-guard.d.ts +25 -0
- package/utils/branch-guard.js +61 -0
- package/utils/change-hash.d.ts +55 -0
- package/utils/change-hash.js +80 -0
- package/utils/conventional-commits.d.ts +39 -0
- package/utils/conventional-commits.js +43 -0
- package/utils/exec.d.ts +5 -6
- package/utils/exec.js +45 -59
- package/utils/git.d.ts +86 -0
- package/utils/git.js +276 -0
- package/utils/live-region.d.ts +16 -0
- package/utils/live-region.js +59 -0
- package/utils/logger.d.ts +24 -0
- package/utils/logger.js +39 -0
- package/utils/npm-run-path.d.ts +1 -1
- package/utils/npm-run-path.js +10 -10
- package/utils/package-filter.d.ts +32 -0
- package/utils/package-filter.js +73 -0
- package/utils/progress-panel.d.ts +59 -0
- package/utils/progress-panel.js +149 -0
- package/utils/workspace-range.d.ts +17 -0
- package/utils/workspace-range.js +28 -0
- package/commands/build-command.d.ts +0 -11
- package/commands/build-command.js +0 -26
- package/commands/changed-command.d.ts +0 -16
- package/commands/changed-command.js +0 -35
- package/commands/ci-command.d.ts +0 -24
- package/commands/ci-command.js +0 -71
- package/commands/execute-command.d.ts +0 -19
- package/commands/execute-command.js +0 -75
- package/commands/info-command.d.ts +0 -10
- package/commands/info-command.js +0 -57
- package/commands/list-command.d.ts +0 -38
- package/commands/list-command.js +0 -137
- package/commands/multi-task-command.d.ts +0 -22
- package/commands/multi-task-command.js +0 -68
- package/commands/publish-command.d.ts +0 -24
- package/commands/publish-command.js +0 -135
- package/commands/run-command.d.ts +0 -28
- package/commands/run-command.js +0 -134
- package/commands/version-command.d.ts +0 -25
- package/commands/version-command.js +0 -198
- package/core/command.d.ts +0 -35
- package/core/command.js +0 -117
- package/core/constants.d.ts +0 -1
- package/core/constants.js +0 -2
- package/core/logger.d.ts +0 -12
- package/core/logger.js +0 -2
- package/utils/file-utils.d.ts +0 -4
- package/utils/file-utils.js +0 -29
- package/utils/git-utils.d.ts +0 -25
- package/utils/git-utils.js +0 -66
- package/utils/npm-utils.d.ts +0 -11
- package/utils/npm-utils.js +0 -33
- package/utils/package-not-found-error.d.ts +0 -2
- package/utils/package-not-found-error.js +0 -2
package/utils/git.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
export class GitHelper {
|
|
5
|
+
cwd;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.cwd = options?.cwd || process.cwd();
|
|
8
|
+
}
|
|
9
|
+
/** The current branch name, or `undefined` in detached HEAD state (or outside a git repository) -
|
|
10
|
+
* what `branch-guard.ts` checks `allowBranch`/`ignoreBranch` against. */
|
|
11
|
+
async currentBranch() {
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: this.cwd });
|
|
14
|
+
const branch = stdout.trim();
|
|
15
|
+
return branch && branch !== 'HEAD' ? branch : undefined;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Files with uncommitted local changes (working tree + index). */
|
|
22
|
+
async listDirtyFiles(options) {
|
|
23
|
+
let stdout;
|
|
24
|
+
try {
|
|
25
|
+
({ stdout } = await execFileAsync('git', ['status', '--porcelain'], { cwd: this.cwd }));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
/** Don't trim the whole output first - porcelain lines have a fixed-width status
|
|
31
|
+
* prefix (e.g. " M "), and trimming would eat the leading space of the first line. */
|
|
32
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
33
|
+
const files = lines.map(line => line.slice(3).trim());
|
|
34
|
+
return options?.absolute ? files.map(f => path.join(this.cwd, f)) : files;
|
|
35
|
+
}
|
|
36
|
+
/** Files committed on the current branch but not yet in its upstream. */
|
|
37
|
+
async listCommittedFiles(options) {
|
|
38
|
+
let shaList;
|
|
39
|
+
try {
|
|
40
|
+
({ stdout: shaList } = await execFileAsync('git', ['cherry'], { cwd: this.cwd }));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/** No upstream configured, or not a git repository - nothing to report. */
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const shas = Array.from(shaList.matchAll(/[a-f0-9]{7,40}/gi)).map(m => m[0]);
|
|
47
|
+
const files = [];
|
|
48
|
+
for (const sha of shas) {
|
|
49
|
+
const { stdout } = await execFileAsync('git', ['show', sha, '--name-only', '--pretty=format:'], {
|
|
50
|
+
cwd: this.cwd,
|
|
51
|
+
});
|
|
52
|
+
files.push(...(stdout.trim() ? stdout.trim().split(/\r?\n/) : []));
|
|
53
|
+
}
|
|
54
|
+
return options?.absolute ? files.map(f => path.join(this.cwd, f)) : files;
|
|
55
|
+
}
|
|
56
|
+
/** Files changed since `hash` (committed diffs plus any uncommitted local changes). */
|
|
57
|
+
async listChangedSince(hash, options) {
|
|
58
|
+
let stdout;
|
|
59
|
+
try {
|
|
60
|
+
({ stdout } = await execFileAsync('git', ['diff', '--name-only', hash], { cwd: this.cwd }));
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
throw new Error(`Unable to compute changes since "${hash}": ${e.message}`, { cause: e });
|
|
64
|
+
}
|
|
65
|
+
const files = stdout.trim() ? stdout.trim().split(/\r?\n/) : [];
|
|
66
|
+
return options?.absolute ? files.map(f => path.join(this.cwd, f)) : files;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Real commits only (never uncommitted/dirty changes - those have no message and can't be
|
|
70
|
+
* described in a changelog), oldest first: with `hash`, everything committed since that ref;
|
|
71
|
+
* without one, whatever's committed on the current branch but not yet in its upstream (same
|
|
72
|
+
* source as `listCommittedFiles`, via `git cherry`).
|
|
73
|
+
*/
|
|
74
|
+
async listCommits(options) {
|
|
75
|
+
const hash = options?.hash;
|
|
76
|
+
let shas;
|
|
77
|
+
if (hash) {
|
|
78
|
+
let stdout;
|
|
79
|
+
try {
|
|
80
|
+
({ stdout } = await execFileAsync('git', ['log', '--reverse', '--format=%H', `${hash}..HEAD`], {
|
|
81
|
+
cwd: this.cwd,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
throw new Error(`Unable to list commits since "${hash}": ${e.message}`, { cause: e });
|
|
86
|
+
}
|
|
87
|
+
shas = stdout.trim() ? stdout.trim().split(/\r?\n/) : [];
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
let cherryOut;
|
|
91
|
+
try {
|
|
92
|
+
({ stdout: cherryOut } = await execFileAsync('git', ['cherry'], { cwd: this.cwd }));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
shas = Array.from(cherryOut.matchAll(/[a-f0-9]{7,40}/gi)).map(m => m[0]);
|
|
98
|
+
}
|
|
99
|
+
return this._commitInfoFor(shas);
|
|
100
|
+
}
|
|
101
|
+
/** Every commit reachable from HEAD, oldest first - for a boundary-free "everything so far"
|
|
102
|
+
* view (e.g. a package that's never been tagged/released at all, so there's no "since" ref to
|
|
103
|
+
* measure from and `listCommits()`'s own no-hash fallback, upstream push status, doesn't apply -
|
|
104
|
+
* a repo with no configured remote at all is common and shouldn't read as "nothing happened"). */
|
|
105
|
+
async listAllCommits() {
|
|
106
|
+
let stdout;
|
|
107
|
+
try {
|
|
108
|
+
({ stdout } = await execFileAsync('git', ['log', '--reverse', '--format=%H'], { cwd: this.cwd }));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const shas = stdout.trim() ? stdout.trim().split(/\r?\n/) : [];
|
|
114
|
+
return this._commitInfoFor(shas);
|
|
115
|
+
}
|
|
116
|
+
async _commitInfoFor(shas) {
|
|
117
|
+
const commits = [];
|
|
118
|
+
for (const sha of shas) {
|
|
119
|
+
// subject and body in one call (NUL-separated, since a commit message itself never
|
|
120
|
+
// contains one) - halves the process-spawns per commit compared to two separate `git show`s.
|
|
121
|
+
const { stdout: header } = await execFileAsync('git', ['show', sha, '--no-patch', '--format=%s%x00%b'], {
|
|
122
|
+
cwd: this.cwd,
|
|
123
|
+
});
|
|
124
|
+
const [subject = '', body = ''] = header.split('\0');
|
|
125
|
+
const { stdout: filesOut } = await execFileAsync('git', ['show', sha, '--name-only', '--pretty=format:'], {
|
|
126
|
+
cwd: this.cwd,
|
|
127
|
+
});
|
|
128
|
+
const files = filesOut.trim() ? filesOut.trim().split(/\r?\n/) : [];
|
|
129
|
+
commits.push({
|
|
130
|
+
sha,
|
|
131
|
+
subject: subject.trim(),
|
|
132
|
+
body: body.trim(),
|
|
133
|
+
files: files.map(f => path.join(this.cwd, f)),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return commits;
|
|
137
|
+
}
|
|
138
|
+
/** Tag names matching the glob `pattern` (e.g. `"v*"`, `"pkg-a@*"`), newest version first. */
|
|
139
|
+
async listTags(pattern) {
|
|
140
|
+
try {
|
|
141
|
+
const { stdout } = await execFileAsync('git', ['tag', '--list', pattern, '--sort=-v:refname'], {
|
|
142
|
+
cwd: this.cwd,
|
|
143
|
+
});
|
|
144
|
+
return stdout.trim() ? stdout.trim().split(/\r?\n/) : [];
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Whether `tag` exists as an exact git tag name (not a glob - a literal ref check). */
|
|
151
|
+
async tagExists(tag) {
|
|
152
|
+
try {
|
|
153
|
+
await execFileAsync('git', ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`], { cwd: this.cwd });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** The most recent commit that changed `file` (tracked history only) - `undefined` if it has
|
|
161
|
+
* never been committed. Answers "since when has this file last been updated", independent of
|
|
162
|
+
* any tag/version scheme - e.g. to check whether a changelog file has fallen behind. */
|
|
163
|
+
async lastCommitTouching(file) {
|
|
164
|
+
try {
|
|
165
|
+
const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%H', '--', file], { cwd: this.cwd });
|
|
166
|
+
return stdout.trim() || undefined;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** The best common ancestor of `a` and `b` - `undefined` if none exists (unrelated histories,
|
|
173
|
+
* an invalid ref, ...). */
|
|
174
|
+
async mergeBase(a, b) {
|
|
175
|
+
try {
|
|
176
|
+
const { stdout } = await execFileAsync('git', ['merge-base', a, b], { cwd: this.cwd });
|
|
177
|
+
return stdout.trim() || undefined;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** The most recent tag matching the glob `pattern` that HEAD actually descends from (unlike
|
|
184
|
+
* `listTags`, this follows commit ancestry rather than just sorting tag names - the right
|
|
185
|
+
* choice for a single repo-wide tag scheme, where a package has no tag of its own). */
|
|
186
|
+
async describeTag(pattern) {
|
|
187
|
+
try {
|
|
188
|
+
const { stdout } = await execFileAsync('git', ['describe', '--tags', '--abbrev=0', '--match', pattern], {
|
|
189
|
+
cwd: this.cwd,
|
|
190
|
+
});
|
|
191
|
+
return stdout.trim() || undefined;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Stages and commits exactly `files` (relative to `cwd`, or absolute) with `message` - never a
|
|
198
|
+
* blanket `git add -A`, so the commit only ever contains what the caller explicitly asked for. */
|
|
199
|
+
async commit(files, message) {
|
|
200
|
+
try {
|
|
201
|
+
await execFileAsync('git', ['add', '--', ...files], { cwd: this.cwd });
|
|
202
|
+
await execFileAsync('git', ['commit', '-m', message], { cwd: this.cwd });
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
throw new Error(`Unable to commit ${files.join(', ')}: ${e.message}`, { cause: e });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Creates an annotated tag `name` pointing at HEAD, with `message` (defaults to `name`).
|
|
209
|
+
* Throws if a tag with that name already exists - callers wanting idempotent tagging should
|
|
210
|
+
* check `tagExists` first. */
|
|
211
|
+
async createTag(name, message) {
|
|
212
|
+
try {
|
|
213
|
+
await execFileAsync('git', ['tag', '-a', name, '-m', message ?? name], { cwd: this.cwd });
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
throw new Error(`Unable to create tag "${name}": ${e.message}`, { cause: e });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** Pushes the current branch to `remote` (default `"origin"`), and its tags too unless
|
|
220
|
+
* `options.tags` is `false`. */
|
|
221
|
+
async push(options) {
|
|
222
|
+
const remote = options?.remote ?? 'origin';
|
|
223
|
+
try {
|
|
224
|
+
await execFileAsync('git', ['push', remote], { cwd: this.cwd });
|
|
225
|
+
if (options?.tags !== false)
|
|
226
|
+
await execFileAsync('git', ['push', remote, '--tags'], { cwd: this.cwd });
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
throw new Error(`Unable to push to "${remote}": ${e.message}`, { cause: e });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Raw `git diff <hash>..HEAD` text (committed changes plus any uncommitted local ones, same as
|
|
233
|
+
* plain `git diff <hash>`) - unlike `listChangedSince`, the actual patch content, not just
|
|
234
|
+
* which files changed. `pathspec`, if given, narrows it to just that file/directory. */
|
|
235
|
+
async diff(hash, pathspec) {
|
|
236
|
+
const args = ['diff', hash];
|
|
237
|
+
if (pathspec)
|
|
238
|
+
args.push('--', pathspec);
|
|
239
|
+
try {
|
|
240
|
+
const { stdout } = await execFileAsync('git', args, { cwd: this.cwd, maxBuffer: 64 * 1024 * 1024 });
|
|
241
|
+
return stdout;
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
throw new Error(`Unable to diff since "${hash}": ${e.message}`, { cause: e });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** `git format-patch --root -o <outputDir> HEAD` - one `.patch` file per commit reachable from
|
|
248
|
+
* HEAD (oldest first), `--root` included so the very first commit gets one too. Returns the
|
|
249
|
+
* patch file paths, in commit order - what `import` replays into another repository. */
|
|
250
|
+
async formatPatches(outputDir) {
|
|
251
|
+
try {
|
|
252
|
+
const { stdout } = await execFileAsync('git', ['format-patch', '--root', '-o', outputDir, 'HEAD'], {
|
|
253
|
+
cwd: this.cwd,
|
|
254
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
255
|
+
});
|
|
256
|
+
return stdout.trim() ? stdout.trim().split(/\r?\n/) : [];
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
throw new Error(`Unable to format patches: ${e.message}`, { cause: e });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Applies `patchFiles` (as produced by `formatPatches`, in order) via `git am`, preserving each
|
|
263
|
+
* original commit's author/date/message. `--3way` lets a patch that no longer applies cleanly
|
|
264
|
+
* fall back to a content-level three-way merge instead of failing outright. */
|
|
265
|
+
async applyPatches(patchFiles) {
|
|
266
|
+
if (!patchFiles.length)
|
|
267
|
+
return;
|
|
268
|
+
try {
|
|
269
|
+
await execFileAsync('git', ['am', '--3way', ...patchFiles], { cwd: this.cwd, maxBuffer: 64 * 1024 * 1024 });
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
throw new Error(`Unable to apply patches: ${e.message}`, { cause: e });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const execFileAsync = promisify(execFile);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a fixed block of lines at the bottom of the terminal that gets
|
|
3
|
+
* redrawn in place (cursor-up + clear-line) instead of scrolling, the way
|
|
4
|
+
* Turborepo/pnpm show "currently running" status without flooding the log.
|
|
5
|
+
*
|
|
6
|
+
* No-op when stdout isn't a TTY (CI, piped output) - cursor-movement escape
|
|
7
|
+
* codes are meaningless there and would just corrupt the log.
|
|
8
|
+
*/
|
|
9
|
+
export declare class LiveRegion {
|
|
10
|
+
private lineCount;
|
|
11
|
+
readonly enabled: boolean;
|
|
12
|
+
constructor(enabled?: boolean);
|
|
13
|
+
render(lines: string[]): void;
|
|
14
|
+
/** Erases the block entirely, leaving the cursor where it started. */
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a fixed block of lines at the bottom of the terminal that gets
|
|
3
|
+
* redrawn in place (cursor-up + clear-line) instead of scrolling, the way
|
|
4
|
+
* Turborepo/pnpm show "currently running" status without flooding the log.
|
|
5
|
+
*
|
|
6
|
+
* No-op when stdout isn't a TTY (CI, piped output) - cursor-movement escape
|
|
7
|
+
* codes are meaningless there and would just corrupt the log.
|
|
8
|
+
*/
|
|
9
|
+
export class LiveRegion {
|
|
10
|
+
lineCount = 0;
|
|
11
|
+
enabled;
|
|
12
|
+
constructor(enabled = !!process.stdout.isTTY) {
|
|
13
|
+
this.enabled = enabled;
|
|
14
|
+
}
|
|
15
|
+
render(lines) {
|
|
16
|
+
if (!this.enabled)
|
|
17
|
+
return;
|
|
18
|
+
const width = process.stdout.columns || 80;
|
|
19
|
+
/** Measure by visible length, not raw length - a heavily-colored line has far more
|
|
20
|
+
* bytes than visible characters, and slicing the raw string would cut mid-escape-code. */
|
|
21
|
+
let clipped = lines.map(l => {
|
|
22
|
+
if (visibleLength(l) <= width)
|
|
23
|
+
return l;
|
|
24
|
+
return l.replace(ANSI_PATTERN, '').slice(0, Math.max(0, width - 1));
|
|
25
|
+
});
|
|
26
|
+
/** A block taller than the terminal breaks the cursor-up math below: once printing it
|
|
27
|
+
* scrolls the screen, "move up N rows" can't actually get back N rows, and every
|
|
28
|
+
* redraw lands lower than the last, leaving a trail instead of a single updating block.
|
|
29
|
+
* Callers should size their own content to fit, but this is a hard backstop. */
|
|
30
|
+
const maxRows = Math.max(1, (process.stdout.rows || 24) - 1);
|
|
31
|
+
if (clipped.length > maxRows)
|
|
32
|
+
clipped = clipped.slice(0, maxRows);
|
|
33
|
+
let out = this.lineCount ? `\x1b[${this.lineCount}A` : '';
|
|
34
|
+
for (const line of clipped)
|
|
35
|
+
out += `\r\x1b[2K${line}\n`;
|
|
36
|
+
for (let i = clipped.length; i < this.lineCount; i++)
|
|
37
|
+
out += '\r\x1b[2K\n';
|
|
38
|
+
if (clipped.length < this.lineCount)
|
|
39
|
+
out += `\x1b[${this.lineCount - clipped.length}A`;
|
|
40
|
+
process.stdout.write(out);
|
|
41
|
+
this.lineCount = clipped.length;
|
|
42
|
+
}
|
|
43
|
+
/** Erases the block entirely, leaving the cursor where it started. */
|
|
44
|
+
clear() {
|
|
45
|
+
if (!this.enabled || !this.lineCount)
|
|
46
|
+
return;
|
|
47
|
+
let out = `\x1b[${this.lineCount}A`;
|
|
48
|
+
for (let i = 0; i < this.lineCount; i++)
|
|
49
|
+
out += '\r\x1b[2K\n';
|
|
50
|
+
out += `\x1b[${this.lineCount}A`;
|
|
51
|
+
process.stdout.write(out);
|
|
52
|
+
this.lineCount = 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line no-control-regex
|
|
56
|
+
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
57
|
+
function visibleLength(s) {
|
|
58
|
+
return s.replace(ANSI_PATTERN, '').length;
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Repository } from '../core/repository.js';
|
|
2
|
+
export type LogLevel = 'silent' | 'error' | 'info' | 'verbose';
|
|
3
|
+
export declare const LOG_LEVELS: LogLevel[];
|
|
4
|
+
/**
|
|
5
|
+
* Gates console output by verbosity (`--log-level`/`.rmanrc logLevel`) so a command's routine
|
|
6
|
+
* narration can be turned down without losing failure output - the same semantics `run`'s own
|
|
7
|
+
* classic per-step log already used (see `resolveLogLevel` in `commands/run.ts`), centralized here
|
|
8
|
+
* so every command can respect `--log-level` instead of reimplementing the checks, or ignoring it
|
|
9
|
+
* altogether via plain `console.log`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Logger {
|
|
12
|
+
level: LogLevel;
|
|
13
|
+
constructor(level: LogLevel);
|
|
14
|
+
/** Routine narration - hidden at 'silent'/'error', shown at 'info'/'verbose'. */
|
|
15
|
+
info(...args: unknown[]): void;
|
|
16
|
+
/** Extra detail, shown only at 'verbose'. */
|
|
17
|
+
verbose(...args: unknown[]): void;
|
|
18
|
+
/** A real failure - hidden only at 'silent', shown at every other level regardless of 'info'/'verbose'. */
|
|
19
|
+
error(...args: unknown[]): void;
|
|
20
|
+
}
|
|
21
|
+
/** The root's own `.rmanrc logLevel` (a plain top-level key) - the base default a command falls
|
|
22
|
+
* back to once no explicit `--log-level`/option override applies. An invalid value is ignored,
|
|
23
|
+
* falling back to 'info' rather than failing the whole command. */
|
|
24
|
+
export declare function resolveRootLogLevel(repository: Repository): LogLevel;
|
package/utils/logger.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const LOG_LEVELS = ['silent', 'error', 'info', 'verbose'];
|
|
2
|
+
/**
|
|
3
|
+
* Gates console output by verbosity (`--log-level`/`.rmanrc logLevel`) so a command's routine
|
|
4
|
+
* narration can be turned down without losing failure output - the same semantics `run`'s own
|
|
5
|
+
* classic per-step log already used (see `resolveLogLevel` in `commands/run.ts`), centralized here
|
|
6
|
+
* so every command can respect `--log-level` instead of reimplementing the checks, or ignoring it
|
|
7
|
+
* altogether via plain `console.log`.
|
|
8
|
+
*/
|
|
9
|
+
export class Logger {
|
|
10
|
+
level;
|
|
11
|
+
constructor(level) {
|
|
12
|
+
this.level = level;
|
|
13
|
+
}
|
|
14
|
+
/** Routine narration - hidden at 'silent'/'error', shown at 'info'/'verbose'. */
|
|
15
|
+
info(...args) {
|
|
16
|
+
if (LOG_LEVELS.indexOf(this.level) < LOG_LEVELS.indexOf('info'))
|
|
17
|
+
return;
|
|
18
|
+
console.log(...args);
|
|
19
|
+
}
|
|
20
|
+
/** Extra detail, shown only at 'verbose'. */
|
|
21
|
+
verbose(...args) {
|
|
22
|
+
if (LOG_LEVELS.indexOf(this.level) < LOG_LEVELS.indexOf('verbose'))
|
|
23
|
+
return;
|
|
24
|
+
console.log(...args);
|
|
25
|
+
}
|
|
26
|
+
/** A real failure - hidden only at 'silent', shown at every other level regardless of 'info'/'verbose'. */
|
|
27
|
+
error(...args) {
|
|
28
|
+
if (this.level === 'silent')
|
|
29
|
+
return;
|
|
30
|
+
console.log(...args);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The root's own `.rmanrc logLevel` (a plain top-level key) - the base default a command falls
|
|
34
|
+
* back to once no explicit `--log-level`/option override applies. An invalid value is ignored,
|
|
35
|
+
* falling back to 'info' rather than failing the whole command. */
|
|
36
|
+
export function resolveRootLogLevel(repository) {
|
|
37
|
+
const v = repository.config?.logLevel;
|
|
38
|
+
return typeof v === 'string' && LOG_LEVELS.includes(v) ? v : 'info';
|
|
39
|
+
}
|
package/utils/npm-run-path.d.ts
CHANGED
package/utils/npm-run-path.js
CHANGED
|
@@ -32,16 +32,6 @@ export function npmRunPath(options = {}) {
|
|
|
32
32
|
result.push(path.resolve(cwd, execPath, '..'));
|
|
33
33
|
return [...result, path_].join(path.delimiter);
|
|
34
34
|
}
|
|
35
|
-
function pathKey(options) {
|
|
36
|
-
const env = options?.env || process.env;
|
|
37
|
-
const platform = options?.platform || process.platform;
|
|
38
|
-
if (platform !== 'win32') {
|
|
39
|
-
return 'PATH';
|
|
40
|
-
}
|
|
41
|
-
return (Object.keys(env)
|
|
42
|
-
.reverse()
|
|
43
|
-
.find(key => key.toUpperCase() === 'PATH') || 'Path');
|
|
44
|
-
}
|
|
45
35
|
/**
|
|
46
36
|
@returns The augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object.
|
|
47
37
|
@example
|
|
@@ -61,3 +51,13 @@ export function npmRunPathEnv(options = {}) {
|
|
|
61
51
|
env[path_] = npmRunPath(opts);
|
|
62
52
|
return env;
|
|
63
53
|
}
|
|
54
|
+
function pathKey(options) {
|
|
55
|
+
const env = options?.env || process.env;
|
|
56
|
+
const platform = options?.platform || process.platform;
|
|
57
|
+
if (platform !== 'win32') {
|
|
58
|
+
return 'PATH';
|
|
59
|
+
}
|
|
60
|
+
return (Object.keys(env)
|
|
61
|
+
.reverse()
|
|
62
|
+
.find(key => key.toUpperCase() === 'PATH') || 'Path');
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Argv } from 'yargs';
|
|
2
|
+
import type { Package } from '../core/package.js';
|
|
3
|
+
/** Shared by every command that iterates packages (`run`/`build`/`test`/`exec`, `list`, `ci`,
|
|
4
|
+
* `clean`, `version`, `publish`, `changelog`) - narrows *which* packages a command applies to,
|
|
5
|
+
* independent of what the command actually does to them. */
|
|
6
|
+
export interface PackageFilterOptions {
|
|
7
|
+
/** Only include packages whose name matches at least one of these globs (e.g. `@scope/*`). */
|
|
8
|
+
scope?: string | string[];
|
|
9
|
+
/** Exclude packages whose name matches at least one of these globs - applied after `scope`. */
|
|
10
|
+
ignore?: string | string[];
|
|
11
|
+
/** Also include every package the matched set depends on (transitively) - e.g. to build
|
|
12
|
+
* everything a scoped app actually needs. */
|
|
13
|
+
deps?: boolean;
|
|
14
|
+
/** Also include every package that depends on the matched set (transitively) - e.g. to test
|
|
15
|
+
* everything that could be affected by a scoped library's change. */
|
|
16
|
+
dependents?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** `--scope`/`--ignore`/`--deps`/`--dependents`, the same shape and describe text in every command
|
|
19
|
+
* that supports them - mirrors `run.command.ts`'s own `applyRunOptions`. */
|
|
20
|
+
export declare function applyPackageFilterOptions<T>(cmd: Argv<T>): Argv<T>;
|
|
21
|
+
export declare function readPackageFilterOptions(args: any): PackageFilterOptions;
|
|
22
|
+
/**
|
|
23
|
+
* Narrows `packages` (the full, already-resolved list - toposort order, if any, is preserved)
|
|
24
|
+
* down to what `options` selects. `scope`/`ignore` match against each package's bare name (glob
|
|
25
|
+
* syntax via `micromatch` - `*`, `**`, `{a,b}`, ...); `ignore` is applied after `scope`, on
|
|
26
|
+
* whatever it left. `deps`/`dependents` then each independently expand *that* matched set along
|
|
27
|
+
* `Package.dependencies` (already the full transitive closure - see
|
|
28
|
+
* `Repository`'s own `_updateDependencies`) and their results are unioned in - so `--deps
|
|
29
|
+
* --dependents` together never re-expands one direction's additions through the other, which
|
|
30
|
+
* would otherwise tend to blow up toward "the whole repository" for any reasonably-connected graph.
|
|
31
|
+
*/
|
|
32
|
+
export declare function filterPackages(packages: Package[], options: PackageFilterOptions): Package[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import micromatch from 'micromatch';
|
|
2
|
+
/** `--scope`/`--ignore`/`--deps`/`--dependents`, the same shape and describe text in every command
|
|
3
|
+
* that supports them - mirrors `run.command.ts`'s own `applyRunOptions`. */
|
|
4
|
+
export function applyPackageFilterOptions(cmd) {
|
|
5
|
+
return cmd
|
|
6
|
+
.option('scope', {
|
|
7
|
+
describe: 'Only include packages whose name matches this glob (repeatable)',
|
|
8
|
+
// Deliberately 'string', not 'array': an array-typed option greedily swallows every
|
|
9
|
+
// following bare word as its own value, which would eat "exec"'s [command..] positional
|
|
10
|
+
// whole. yargs still collects repeated "--scope a --scope b" into an array either way.
|
|
11
|
+
type: 'string',
|
|
12
|
+
})
|
|
13
|
+
.option('ignore', {
|
|
14
|
+
describe: 'Exclude packages whose name matches this glob (repeatable) - applied after --scope',
|
|
15
|
+
type: 'string',
|
|
16
|
+
})
|
|
17
|
+
.option('deps', {
|
|
18
|
+
describe: 'Also include every package the matched set depends on',
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
})
|
|
21
|
+
.option('dependents', {
|
|
22
|
+
describe: 'Also include every package that depends on the matched set',
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function readPackageFilterOptions(args) {
|
|
27
|
+
return {
|
|
28
|
+
scope: args.scope,
|
|
29
|
+
ignore: args.ignore,
|
|
30
|
+
deps: args.deps,
|
|
31
|
+
dependents: args.dependents,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Narrows `packages` (the full, already-resolved list - toposort order, if any, is preserved)
|
|
36
|
+
* down to what `options` selects. `scope`/`ignore` match against each package's bare name (glob
|
|
37
|
+
* syntax via `micromatch` - `*`, `**`, `{a,b}`, ...); `ignore` is applied after `scope`, on
|
|
38
|
+
* whatever it left. `deps`/`dependents` then each independently expand *that* matched set along
|
|
39
|
+
* `Package.dependencies` (already the full transitive closure - see
|
|
40
|
+
* `Repository`'s own `_updateDependencies`) and their results are unioned in - so `--deps
|
|
41
|
+
* --dependents` together never re-expands one direction's additions through the other, which
|
|
42
|
+
* would otherwise tend to blow up toward "the whole repository" for any reasonably-connected graph.
|
|
43
|
+
*/
|
|
44
|
+
export function filterPackages(packages, options) {
|
|
45
|
+
let matched = packages;
|
|
46
|
+
if (options.scope) {
|
|
47
|
+
const patterns = toArray(options.scope);
|
|
48
|
+
matched = matched.filter(p => micromatch.isMatch(p.name, patterns));
|
|
49
|
+
}
|
|
50
|
+
if (options.ignore) {
|
|
51
|
+
const patterns = toArray(options.ignore);
|
|
52
|
+
matched = matched.filter(p => !micromatch.isMatch(p.name, patterns));
|
|
53
|
+
}
|
|
54
|
+
if (!options.deps && !options.dependents)
|
|
55
|
+
return matched;
|
|
56
|
+
const included = new Set(matched.map(p => p.name));
|
|
57
|
+
if (options.deps) {
|
|
58
|
+
for (const p of matched)
|
|
59
|
+
for (const dep of p.dependencies)
|
|
60
|
+
included.add(dep);
|
|
61
|
+
}
|
|
62
|
+
if (options.dependents) {
|
|
63
|
+
const matchedNames = new Set(matched.map(p => p.name));
|
|
64
|
+
for (const p of packages) {
|
|
65
|
+
if (p.dependencies.some(d => matchedNames.has(d)))
|
|
66
|
+
included.add(p.name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return packages.filter(p => included.has(p.name));
|
|
70
|
+
}
|
|
71
|
+
function toArray(value) {
|
|
72
|
+
return Array.isArray(value) ? value : [value];
|
|
73
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { LiveRegion } from './live-region.js';
|
|
2
|
+
export type ProgressStatus = 'pending' | 'running' | 'success' | 'failed' | 'skipped';
|
|
3
|
+
export interface ProgressItem {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
status: ProgressStatus;
|
|
6
|
+
/** Label of the step currently running, for work with more than one step (e.g. `run`'s
|
|
7
|
+
* npm-script steps) - omit `stepsTotal` entirely for single-step work like a plain command. */
|
|
8
|
+
currentStep?: string;
|
|
9
|
+
stepIndex?: number;
|
|
10
|
+
stepsTotal?: number;
|
|
11
|
+
/** Last captured output line, shown dimmed under the item while it's running. */
|
|
12
|
+
lastLine?: string;
|
|
13
|
+
startedAt?: number;
|
|
14
|
+
finishedAt?: number;
|
|
15
|
+
/** Captured output, printed back out if the item ends up failing. */
|
|
16
|
+
log: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ProgressSummary {
|
|
19
|
+
successCount: number;
|
|
20
|
+
failedCount: number;
|
|
21
|
+
skippedCount: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function formatDuration(ms: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Shared live progress panel for any command that fans work out across packages: a spinner
|
|
26
|
+
* header (progress bar, running/failed counts, elapsed time) plus one or two lines per
|
|
27
|
+
* currently-running item, redrawn in place instead of scrolling - the same panel `run`/`build`
|
|
28
|
+
* use, pulled out so other package-fanning commands (e.g. `ci`) can reuse it instead of
|
|
29
|
+
* reimplementing it.
|
|
30
|
+
*
|
|
31
|
+
* Renders nothing when `enabled` is false (a non-TTY, or a command's own `--no-progress`) -
|
|
32
|
+
* callers are expected to fall back to their own plain logging in that case, the way `run`
|
|
33
|
+
* falls back to its "classic" per-step log.
|
|
34
|
+
*/
|
|
35
|
+
export declare class ProgressPanel {
|
|
36
|
+
private readonly title;
|
|
37
|
+
readonly live: LiveRegion;
|
|
38
|
+
private readonly items;
|
|
39
|
+
private renderLoop?;
|
|
40
|
+
private spinnerFrame;
|
|
41
|
+
private startedAt;
|
|
42
|
+
constructor(title: string, enabled: boolean);
|
|
43
|
+
get enabled(): boolean;
|
|
44
|
+
/** Registers an item and returns it - callers mutate the returned object directly (`status`,
|
|
45
|
+
* `currentStep`, `lastLine`, ...) to drive the next render tick. */
|
|
46
|
+
addItem(name: string, stepsTotal?: number): ProgressItem;
|
|
47
|
+
/** Starts the redraw loop. No-op (and no timer) when disabled. */
|
|
48
|
+
start(): void;
|
|
49
|
+
private render;
|
|
50
|
+
/** Stops the redraw loop and erases the panel, leaving the cursor where it started. */
|
|
51
|
+
stop(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Prints the final per-item recap (✓/X/○) and the "N succeeded, M failed (Xs)" summary line,
|
|
54
|
+
* and returns the tally. Any item still 'pending' is counted (and printed) as 'skipped'. The
|
|
55
|
+
* per-item recap lines are only printed when the live panel was actually on - with it off, the
|
|
56
|
+
* caller's own plain logging already showed each item's outcome as it happened.
|
|
57
|
+
*/
|
|
58
|
+
printSummary(): ProgressSummary;
|
|
59
|
+
}
|