rman 2.20.0 → 2.21.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.
@@ -119,7 +119,9 @@ async function remove(file, dryRun) {
119
119
  function pruneEmptyDirs(dir, includeSelf, dryRun) {
120
120
  if (dryRun || !fs.existsSync(dir))
121
121
  return;
122
- const entries = fg.sync('**', { cwd: dir, onlyDirectories: true, dot: true });
122
+ /** Not through a link: a link to a directory is not a directory of this one to prune - removing
123
+ * it as one is `ENOTDIR`, and walking into it reaches somewhere else entirely. */
124
+ const entries = fg.sync('**', { cwd: dir, onlyDirectories: true, dot: true, followSymbolicLinks: false });
123
125
  entries.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
124
126
  for (const rel of entries) {
125
127
  const abs = path.join(dir, rel);
@@ -140,23 +142,48 @@ function pruneEmptyDirs(dir, includeSelf, dryRun) {
140
142
  * matching files *inside* an otherwise-deleted directory, leaving the rest of it gone and the
141
143
  * protected files (and their now non-empty parent) in place.
142
144
  */
145
+ /* **A symbolic link is removed as a link, and never followed.** A build directory routinely holds
146
+ * one - `build/node_modules/@scope/sibling` pointing at another package's own build - and walking
147
+ * through it deleted *that* package's output, then failed `rmdir` on the link with `ENOTDIR`.
148
+ * Measured on `panates/syncbridge`: cleaning `syncbuild` emptied `common/build`, and the run
149
+ * reported only `0 succeeded, 1 failed`. fast-glob follows links by default, so every glob here
150
+ * says `followSymbolicLinks: false`. */
143
151
  async function cleanGlobs(dirname, include, exclude, dryRun) {
144
152
  if (!include.length)
145
153
  return [];
146
- const matches = await fg(include, { cwd: dirname, ignore: exclude, onlyFiles: false, dot: true, absolute: true });
154
+ const matches = await fg(include, {
155
+ cwd: dirname,
156
+ ignore: exclude,
157
+ onlyFiles: false,
158
+ dot: true,
159
+ absolute: true,
160
+ followSymbolicLinks: false,
161
+ });
147
162
  const protectedFiles = exclude.length
148
- ? new Set(await fg(exclude, { cwd: dirname, onlyFiles: true, dot: true, absolute: true }))
163
+ ? new Set(await fg(exclude, { cwd: dirname, onlyFiles: true, dot: true, absolute: true, followSymbolicLinks: false }))
149
164
  : new Set();
150
165
  const removed = [];
151
166
  for (const m of matches) {
152
- const stat = await fs.promises.stat(m).catch(() => undefined);
167
+ /** `lstat`, not `stat`: a link is removed as the link it is, and never read as the thing it
168
+ * points at. */
169
+ const stat = await fs.promises.lstat(m).catch(() => undefined);
153
170
  if (!stat)
154
171
  continue;
155
172
  if (stat.isDirectory()) {
156
- const filesInside = await fg('**', { cwd: m, onlyFiles: true, dot: true, absolute: true });
157
- for (const f of filesInside) {
173
+ /** Files *and* links inside, and a link never entered - see `cleanGlobs`' own note. */
174
+ const inside = await fg('**', {
175
+ cwd: m,
176
+ onlyFiles: false,
177
+ dot: true,
178
+ absolute: true,
179
+ followSymbolicLinks: false,
180
+ });
181
+ for (const f of inside) {
158
182
  if (protectedFiles.has(f))
159
183
  continue;
184
+ const entry = await fs.promises.lstat(f).catch(() => undefined);
185
+ if (!entry || entry.isDirectory())
186
+ continue;
160
187
  await remove(f, dryRun);
161
188
  removed.push(path.relative(dirname, f));
162
189
  }
@@ -208,6 +235,8 @@ async function cleanTsArtifacts(pkg, dryRun) {
208
235
  onlyFiles: true,
209
236
  dot: true,
210
237
  absolute: true,
238
+ /** A link to a directory elsewhere is not this package's source to sweep. */
239
+ followSymbolicLinks: false,
211
240
  });
212
241
  const removed = [];
213
242
  for (const f of files) {
@@ -277,6 +306,7 @@ async function cleanTsBuildInfo(dirname, dryRun) {
277
306
  onlyFiles: true,
278
307
  dot: true,
279
308
  absolute: true,
309
+ followSymbolicLinks: false,
280
310
  });
281
311
  for (const f of files)
282
312
  await remove(f, dryRun);
@@ -310,7 +340,12 @@ async function cleanPackage(pkg, item, panelEnabled, dryRun, logger) {
310
340
  }
311
341
  catch (e) {
312
342
  item.status = 'failed';
313
- item.log.push(e?.message ?? String(e));
343
+ const message = e?.message ?? String(e);
344
+ item.log.push(message);
345
+ /** The panel replays `log` in its recap; with no panel nothing does, and the run said only
346
+ * `0 succeeded, 1 failed` - which is how the link bug above went unexplained. */
347
+ if (!panelEnabled)
348
+ logger.error(colors.red('clean'), colors.cyan(item.name), message);
314
349
  throw e;
315
350
  }
316
351
  finally {
package/cli.js CHANGED
@@ -99,6 +99,12 @@ export async function runCli(options) {
99
99
  ? err.message
100
100
  : '';
101
101
  console.log('\n' + colors.red(text));
102
+ /** The handler's own error is marked too, because it is the one that comes back: yargs
103
+ * rejects `parseAsync` with what the handler threw, not with what this throws, so
104
+ * `runCli`'s catch saw it unmarked and printed the same line again on stderr - every
105
+ * failing command said its reason twice. */
106
+ if (err && typeof err === 'object')
107
+ err.logged = true;
102
108
  /** A real `Error`, marked `logged` since the text above just went out: yargs hands the
103
109
  * reason over as a bare string, and rethrowing that raw made bad argv indistinguishable
104
110
  * from success to anything holding the promise - or the shell. */
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.20.0';
1
+ export const version = '2.21.1';
@@ -24,6 +24,20 @@ export interface ProgressItem {
24
24
  finishedAt?: number;
25
25
  /** Captured output, printed back out if the item ends up failing. */
26
26
  log: string[];
27
+ /**
28
+ * The same output split by the step that printed it, when the driver reports steps (`run`'s
29
+ * scheduler does). A failed item's recap then names the steps that passed in a line each and
30
+ * prints the output of the one that failed; without it, `log` is printed whole.
31
+ */
32
+ steps?: StepLog[];
33
+ }
34
+ /** One step of an item, as the recap shows it - see `ProgressItem.steps`. */
35
+ export interface StepLog {
36
+ /** `before: rman check` - the slot and what ran in it. */
37
+ label: string;
38
+ /** Unset while it runs, and for a step that never got to end. */
39
+ status?: 'success' | 'failed';
40
+ lines: string[];
27
41
  }
28
42
  export interface ProgressSummary {
29
43
  successCount: number;
@@ -81,6 +95,11 @@ export declare class ProgressPanel implements TerminalRegion {
81
95
  * `TerminalRegion` half of the takeover, so a `runBin` child's output goes somewhere sane while
82
96
  * a panel is up. */
83
97
  passThrough(text: string): void;
98
+ /**
99
+ * A failed item's steps: one line for each that passed, and the one that failed - or never got to
100
+ * end - with its whole output under it.
101
+ */
102
+ protected stepRecap(steps: readonly StepLog[]): string;
84
103
  /** Every line of a replayed log moved two columns right - an empty line left empty. */
85
104
  protected indentLog(text: string): string;
86
105
  private render;
@@ -79,6 +79,28 @@ export class ProgressPanel {
79
79
  process.stdout.write(text);
80
80
  this.render();
81
81
  }
82
+ /**
83
+ * A failed item's steps: one line for each that passed, and the one that failed - or never got to
84
+ * end - with its whole output under it.
85
+ */
86
+ /* **Only the failing step's output, and the user's call.** The whole log used to be replayed, on
87
+ * the reasoning that an earlier step's output can explain a later failure. In a real build that
88
+ * put `rman check`'s file count and its own recap above every lint error, four lines of success
89
+ * the reader had to skip to reach the one that mattered. The passing steps keep a line each, so
90
+ * the order is still there; `--log-file` keeps every line. */
91
+ stepRecap(steps) {
92
+ const out = [];
93
+ for (const step of steps) {
94
+ if (step.status === 'success') {
95
+ out.push(`${colors.green('✔')} ${colors.gray(step.label)}`);
96
+ continue;
97
+ }
98
+ out.push(`${colors.red('✖')} ${step.label}`);
99
+ if (step.lines.length)
100
+ out.push(this.indentLog(step.lines.join('\n')));
101
+ }
102
+ return out.join('\n');
103
+ }
82
104
  /** Every line of a replayed log moved two columns right - an empty line left empty. */
83
105
  indentLog(text) {
84
106
  return text
@@ -284,7 +306,9 @@ export class ProgressPanel {
284
306
  *
285
307
  * **Indented two columns**, so the log reads as belonging to the `X` line above it - level
286
308
  * with the recap's own lines it ran together with them, a nested rman's recap most of all. */
287
- if (item.log.length)
309
+ if (item.steps?.length)
310
+ console.log(this.indentLog(this.stepRecap(item.steps)));
311
+ else if (item.log.length)
288
312
  console.log(this.indentLog(item.log.join('\n')));
289
313
  }
290
314
  }
@@ -349,6 +373,7 @@ class PanelItem {
349
373
  startedAt;
350
374
  finishedAt;
351
375
  log = [];
376
+ steps = [];
352
377
  _currentStep;
353
378
  _currentCommand;
354
379
  constructor(name, stepsTotal) {
@@ -54,11 +54,16 @@ export declare class PanelReporter extends PlainReporter {
54
54
  * would only be escape codes counted as text there. */
55
55
  readonly childEnv: undefined;
56
56
  constructor(commandName: string, panel: ProgressPanel);
57
- /** The row names the step itself; there is no separate line to print. */
58
- protected started(): void;
57
+ /** The row names the step itself; there is no separate line to print - but the recap needs to
58
+ * know which lines were whose, so a step is opened here. */
59
+ protected started(event: Extract<LogEvent, {
60
+ event: 'start';
61
+ }>, origin: ReportOrigin): void;
59
62
  protected output(event: Extract<LogEvent, {
60
63
  event: 'output';
61
64
  }>, origin: ReportOrigin): void;
62
- /** The row shows the step's outcome; the recap prints the package's. */
63
- protected ended(): void;
65
+ /** The row shows the step's outcome; the recap prints the package's - and reads it from here. */
66
+ protected ended(event: Extract<LogEvent, {
67
+ event: 'end';
68
+ }>, origin: ReportOrigin): void;
64
69
  }
@@ -124,12 +124,20 @@ export class PanelReporter extends PlainReporter {
124
124
  constructor(commandName, panel) {
125
125
  super(commandName, panel);
126
126
  }
127
- /** The row names the step itself; there is no separate line to print. */
128
- started() { }
127
+ /** The row names the step itself; there is no separate line to print - but the recap needs to
128
+ * know which lines were whose, so a step is opened here. */
129
+ started(event, origin) {
130
+ origin.item.steps?.push({ label: event.command ? `${event.step}: ${event.command}` : event.step, lines: [] });
131
+ }
129
132
  output(event, origin) {
130
133
  origin.item.log.push(event.line);
134
+ origin.item.steps?.at(-1)?.lines.push(event.line);
131
135
  origin.item.lastLine = event.line;
132
136
  }
133
- /** The row shows the step's outcome; the recap prints the package's. */
134
- ended() { }
137
+ /** The row shows the step's outcome; the recap prints the package's - and reads it from here. */
138
+ ended(event, origin) {
139
+ const step = origin.item.steps?.at(-1);
140
+ if (step)
141
+ step.status = event.status;
142
+ }
135
143
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.20.0",
4
+ "version": "2.21.1",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {