rman 2.10.0 → 2.11.0
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/builtins/platforms/node/services/ci.service.d.ts +1 -1
- package/builtins/platforms/node/services/ci.service.js +1 -1
- package/builtins/platforms/node/services/clean.service.d.ts +1 -1
- package/builtins/platforms/node/services/clean.service.js +1 -1
- package/cli.js +81 -23
- package/commands/changelog.command.js +1 -1
- package/commands/version.command.js +53 -31
- package/constants.js +1 -1
- package/core/application.d.ts +8 -1
- package/core/application.js +7 -1
- package/core/classes/log-sink.d.ts +99 -0
- package/core/classes/log-sink.js +101 -0
- package/{utils → core/classes}/logger.d.ts +1 -1
- package/core/classes/run-reporters.d.ts +64 -0
- package/core/classes/run-reporters.js +135 -0
- package/core/classes/service.d.ts +1 -1
- package/core/command-builder.js +3 -0
- package/core/interfaces/custom-command.d.ts +5 -1
- package/core/interfaces/run-step.d.ts +5 -5
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/package.json +1 -1
- package/services/exec.service.d.ts +1 -1
- package/services/exec.service.js +1 -1
- package/services/run.service.d.ts +22 -3
- package/services/run.service.js +190 -125
- package/services/version-plan.service.js +3 -3
- package/utils/progress-panel.d.ts +6 -0
- package/utils/progress-panel.js +22 -7
- package/utils/run-bin.d.ts +10 -2
- package/utils/run-bin.js +57 -11
- package/utils/version-group.d.ts +19 -0
- package/utils/version-group.js +23 -0
- /package/{utils → core/classes}/logger.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { LogLevel } from '../../../../core/classes/logger.js';
|
|
1
2
|
import type { Repository } from '../../../../core/classes/repository.js';
|
|
2
|
-
import type { LogLevel } from '../../../../utils/logger.js';
|
|
3
3
|
import type { PackageFilterOptions } from '../../../../utils/package-filter.js';
|
|
4
4
|
export declare namespace CiService {
|
|
5
5
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import colors from 'ansi-colors';
|
|
4
|
+
import { Logger, resolveRootLogLevel } from '../../../../core/classes/logger.js';
|
|
4
5
|
import { exec } from '../../../../utils/exec.js';
|
|
5
|
-
import { Logger, resolveRootLogLevel } from '../../../../utils/logger.js';
|
|
6
6
|
import { filterPackages } from '../../../../utils/package-filter.js';
|
|
7
7
|
import { formatDuration, ProgressPanel } from '../../../../utils/progress-panel.js';
|
|
8
8
|
export var CiService;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { LogLevel } from '../../../../core/classes/logger.js';
|
|
1
2
|
import type { Repository } from '../../../../core/classes/repository.js';
|
|
2
|
-
import type { LogLevel } from '../../../../utils/logger.js';
|
|
3
3
|
import type { PackageFilterOptions } from '../../../../utils/package-filter.js';
|
|
4
4
|
export declare namespace CleanService {
|
|
5
5
|
interface Options extends PackageFilterOptions {
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import colors from 'ansi-colors';
|
|
4
4
|
import fg from 'fast-glob';
|
|
5
|
-
import { Logger, resolveRootLogLevel } from '../../../../
|
|
5
|
+
import { Logger, resolveRootLogLevel } from '../../../../core/classes/logger.js';
|
|
6
6
|
import { filterPackages } from '../../../../utils/package-filter.js';
|
|
7
7
|
import { ProgressPanel } from '../../../../utils/progress-panel.js';
|
|
8
8
|
export var CleanService;
|
package/cli.js
CHANGED
|
@@ -11,13 +11,14 @@ import yargs, {} from 'yargs';
|
|
|
11
11
|
import { hideBin } from 'yargs/helpers';
|
|
12
12
|
import { version } from './constants.js';
|
|
13
13
|
import { RmanApplication } from './core/application.js';
|
|
14
|
+
import { LogSink } from './core/classes/log-sink.js';
|
|
15
|
+
import { LOG_LEVELS, Logger, resolveRootLogLevel } from './core/classes/logger.js';
|
|
14
16
|
import { Repository } from './core/classes/repository.js';
|
|
15
17
|
import { commandName, toYargsCommand } from './core/command-builder.js';
|
|
16
18
|
import { assertNoBuiltinShadowing, defaultCommandGlobs, describeCommandSource, loadCustomCommands, } from './core/interfaces/custom-command.js';
|
|
17
19
|
import { checkCustomCommand } from './core/plugin-loader.js';
|
|
18
20
|
import { commandRegistry } from './interfaces/rman-config.interface.js';
|
|
19
21
|
import { colorYaml } from './utils/color-yaml.js';
|
|
20
|
-
import { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
|
|
21
22
|
import { filterPackages, readFromRootOption, readPackageFilterOptions } from './utils/package-filter.js';
|
|
22
23
|
import { printableConfig, withoutContributions } from './utils/printable-config.js';
|
|
23
24
|
import { runBin } from './utils/run-bin.js';
|
|
@@ -58,6 +59,20 @@ export async function runCli(options) {
|
|
|
58
59
|
'on, and the .rmanrc keys it reads - and run nothing',
|
|
59
60
|
type: 'boolean',
|
|
60
61
|
default: false,
|
|
62
|
+
})
|
|
63
|
+
/** **Global, not per command** - the format of what rman writes, the way `--log-level` is its
|
|
64
|
+
* volume. A command that declares its own `--json` (`list`, `version`, `publish`, ...) keeps
|
|
65
|
+
* that declaration and its meaning: its *result* as JSON, which the shared release workflow
|
|
66
|
+
* parses. See `LogSink`. */
|
|
67
|
+
.option('json', {
|
|
68
|
+
describe: 'Write the log as JSON Lines - one event per line on stdout, and no panel, spinner or prose. ' +
|
|
69
|
+
'A command that prints a result (list, version, publish, ...) prints that result as JSON',
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
})
|
|
72
|
+
.option('log-file', {
|
|
73
|
+
describe: 'Also write the log to this file - JSON Lines under --json, plain text lines otherwise. ' +
|
|
74
|
+
'Overwritten each run; relative to the current directory',
|
|
75
|
+
type: 'string',
|
|
61
76
|
})
|
|
62
77
|
.showHelpOnFail(false, 'Run with --help for available options')
|
|
63
78
|
.fail((msg, err) => {
|
|
@@ -384,37 +399,80 @@ function interceptStatusLines(repository, program) {
|
|
|
384
399
|
return register({
|
|
385
400
|
...spec,
|
|
386
401
|
handler: async (args) => {
|
|
387
|
-
/** The flag when it was given, the repository's own `logLevel` otherwise - the same
|
|
388
|
-
* precedence every other reader of it applies. `argvLogLevel` is not used here: that one
|
|
389
|
-
* exists for diagnostics emitted *before* parsing, and by now yargs has the answer. */
|
|
390
|
-
const level = args.logLevel ?? repository.config?.logLevel;
|
|
391
|
-
if (spec.printsDocument || args.json || args.config || level === 'silent')
|
|
392
|
-
return run(args);
|
|
393
402
|
const name = commandName(spec.command ?? '');
|
|
394
|
-
/**
|
|
395
|
-
*
|
|
396
|
-
const
|
|
397
|
-
repository.app.
|
|
398
|
-
region.start();
|
|
403
|
+
/** Built for every command, used by whichever writes a log - see `LogSink`. A command owning
|
|
404
|
+
* `--json` prints its result as JSON, so the log must not be written to stdout beside it. */
|
|
405
|
+
const sink = new LogSink({ json: !!args.json && !spec.ownsJson, file: args.logFile, cwd: repository.cwd });
|
|
406
|
+
repository.app.logSink = sink;
|
|
399
407
|
try {
|
|
400
|
-
|
|
401
|
-
region.stop('ok');
|
|
402
|
-
return result;
|
|
403
|
-
}
|
|
404
|
-
catch (e) {
|
|
405
|
-
/** **The bracket is closed even on a failure.** A spinner that simply stops, leaving its
|
|
406
|
-
* last frame on screen, reads as a hang - which is the thing this exists to prevent. The
|
|
407
|
-
* error itself is left to the caller, which already prints it. */
|
|
408
|
-
region.stop('fail');
|
|
409
|
-
throw e;
|
|
408
|
+
return await runWithStatusLine(args, name);
|
|
410
409
|
}
|
|
411
410
|
finally {
|
|
412
|
-
|
|
411
|
+
sink.close();
|
|
412
|
+
repository.app.logSink = undefined;
|
|
413
|
+
warnUnhonoured(name, args, sink, !!spec.ownsJson);
|
|
413
414
|
}
|
|
414
415
|
},
|
|
415
416
|
});
|
|
417
|
+
async function runWithStatusLine(args, name) {
|
|
418
|
+
/** The flag when it was given, the repository's own `logLevel` otherwise - the same
|
|
419
|
+
* precedence every other reader of it applies. `argvLogLevel` is not used here: that one
|
|
420
|
+
* exists for diagnostics emitted *before* parsing, and by now yargs has the answer. */
|
|
421
|
+
const level = args.logLevel ?? repository.config?.logLevel;
|
|
422
|
+
if (spec.printsDocument || args.json || args.config || level === 'silent')
|
|
423
|
+
return run(args);
|
|
424
|
+
/** Held on the application so `runBin` can find it through the `app` it already receives -
|
|
425
|
+
* see `RmanApplication.statusRegion` for why that rather than ambient state.
|
|
426
|
+
*
|
|
427
|
+
* **Not drawn under `--no-progress`**, which asks for *no* live output rather than "no
|
|
428
|
+
* panel": the spinner is progress too, and it would redraw over the plain lines such a run
|
|
429
|
+
* prints. Its result line still prints, as it does without a TTY. Children are not told -
|
|
430
|
+
* they are run without a terminal instead, see `RunService`'s panel-off path. */
|
|
431
|
+
const region = new StatusRegion(name, repository.name ?? '', args.progress === false ? false : undefined);
|
|
432
|
+
repository.app.statusRegion = region;
|
|
433
|
+
region.start();
|
|
434
|
+
try {
|
|
435
|
+
const result = await run(args);
|
|
436
|
+
region.stop('ok');
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
/** **The bracket is closed even on a failure.** A spinner that simply stops, leaving its
|
|
441
|
+
* last frame on screen, reads as a hang - which is the thing this exists to prevent. The
|
|
442
|
+
* error itself is left to the caller, which already prints it. */
|
|
443
|
+
region.stop('fail');
|
|
444
|
+
throw e;
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
repository.app.statusRegion = undefined;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
416
450
|
};
|
|
417
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* One line on stderr for a `--json` or `--log-file` that the command did not honour - so a flag that
|
|
454
|
+
* does nothing does not pass for one that worked.
|
|
455
|
+
*
|
|
456
|
+
* **Detected, not listed.** Whether a command writes a log is whatever went through the sink, so a
|
|
457
|
+
* command that starts writing one stops being warned about with no list to update - the rule
|
|
458
|
+
* `builtInNames` follows. On stderr because under `--json` stdout belongs to the events.
|
|
459
|
+
*/
|
|
460
|
+
/* **Why warn rather than refuse.** Both flags are global now, so yargs accepts them on every command,
|
|
461
|
+
* and `rman clean --json` - which `.strict()` used to reject as an unknown argument - would otherwise
|
|
462
|
+
* be quietly ignored. Refusing would turn a log option into a reason for a release step to fail. The
|
|
463
|
+
* commands that write a log so far are the ones whose steps run through `RunService` - `run`,
|
|
464
|
+
* `build`, `test`, and a command using `forEachPackage`. */
|
|
465
|
+
function warnUnhonoured(name, args, sink, ownsJson) {
|
|
466
|
+
/** `--config` runs nothing by design, so nothing written is the right outcome there. */
|
|
467
|
+
if (sink.used || args.config)
|
|
468
|
+
return;
|
|
469
|
+
if (args.logFile) {
|
|
470
|
+
process.stderr.write(colors.yellow(`--log-file: "rman ${name}" does not write a log yet, so ${args.logFile} was not created.\n`));
|
|
471
|
+
}
|
|
472
|
+
if (args.json && !ownsJson) {
|
|
473
|
+
process.stderr.write(colors.yellow(`--json: "rman ${name}" has no JSON output yet; what it printed is unchanged.\n`));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
418
476
|
/**
|
|
419
477
|
* What a command would run with: its resolved options, the packages it would act on, and the
|
|
420
478
|
* `.rmanrc` those packages carry.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import colors from 'ansi-colors';
|
|
3
|
+
import { Logger, resolveRootLogLevel } from '../core/classes/logger.js';
|
|
3
4
|
import { registerCommand } from '../interfaces/rman-config.interface.js';
|
|
4
5
|
import { ChangeHashService } from '../services/change-hash.service.js';
|
|
5
|
-
import { Logger, resolveRootLogLevel } from '../utils/logger.js';
|
|
6
6
|
import { fromRootOption, packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
|
|
7
7
|
import { ProgressPanel } from '../utils/progress-panel.js';
|
|
8
8
|
const COMMAND = 'changelog';
|
|
@@ -5,6 +5,7 @@ import { registerCommand } from '../interfaces/rman-config.interface.js';
|
|
|
5
5
|
import { VersionPlanService } from '../services/version-plan.service.js';
|
|
6
6
|
import { assertAllowedBranch, branchGuardOptions, readBranchGuardOptions } from '../utils/branch-guard.js';
|
|
7
7
|
import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
|
|
8
|
+
import { isNamedGroupKey, isSoloGroupKey, ROOT_GROUP_KEY } from '../utils/version-group.js';
|
|
8
9
|
/**
|
|
9
10
|
* Hoisted out of the metadata literal so the handler can be annotated against them - see
|
|
10
11
|
* `RmanConfig.ArgsOf` for why an inferred `argv` and the metadata's own typo checking cannot both
|
|
@@ -289,12 +290,12 @@ function printApplied(result) {
|
|
|
289
290
|
function printPlan(entries) {
|
|
290
291
|
const table = new EasyTable();
|
|
291
292
|
let first = true;
|
|
292
|
-
for (const { members
|
|
293
|
+
for (const { members } of planBlocks(entries)) {
|
|
293
294
|
if (!first)
|
|
294
295
|
table.pushDelimeter(PLAN_COLUMNS);
|
|
295
296
|
first = false;
|
|
296
297
|
for (const e of members)
|
|
297
|
-
printPlanRow(table, e
|
|
298
|
+
printPlanRow(table, e);
|
|
298
299
|
}
|
|
299
300
|
console.log(table.toString().trim());
|
|
300
301
|
}
|
|
@@ -321,9 +322,9 @@ function printPlan(entries) {
|
|
|
321
322
|
* repository that has nothing to group - noise standing in for structure. They share a block
|
|
322
323
|
* because what they have in common is real: none of them is tied to anyone else.
|
|
323
324
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
325
|
+
* The Group column is decided per row from the group key (`groupCell`), not from the block: both ask
|
|
326
|
+
* the same key the same question - is this package in a group, and did anybody name it - so they
|
|
327
|
+
* cannot disagree.
|
|
327
328
|
*/
|
|
328
329
|
function planBlocks(entries) {
|
|
329
330
|
/**
|
|
@@ -344,42 +345,31 @@ function planBlocks(entries) {
|
|
|
344
345
|
else
|
|
345
346
|
byGroup.set(e.groupKey, [e]);
|
|
346
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* **Classified by the key, not by how many members a group has.** This was `group.length > 1`,
|
|
350
|
+
* which stands in for "is this a group" and is wrong in exactly one case: a group the repository
|
|
351
|
+
* *named* that happens to have a single member. Reported on a real repository - a package alone in
|
|
352
|
+
* `group: "abisena-iomt"` landed among the ungrouped and printed an empty Group cell, reading
|
|
353
|
+
* exactly like a package that had opted out. Its name decides its changelog file and its tag, so
|
|
354
|
+
* it is a line of its own whatever its size.
|
|
355
|
+
*/
|
|
347
356
|
const shared = [];
|
|
348
357
|
const solo = [];
|
|
349
|
-
for (const group of byGroup
|
|
350
|
-
if (
|
|
351
|
-
|
|
358
|
+
for (const [key, group] of byGroup) {
|
|
359
|
+
if (isSoloGroupKey(key))
|
|
360
|
+
solo.push(...group);
|
|
352
361
|
else
|
|
353
|
-
|
|
362
|
+
shared.push({ members: group });
|
|
354
363
|
}
|
|
355
|
-
return [
|
|
356
|
-
...(root.length ? [{ members: root, shared: false }] : []),
|
|
357
|
-
...shared,
|
|
358
|
-
...(solo.length ? [{ members: solo, shared: false }] : []),
|
|
359
|
-
];
|
|
364
|
+
return [...(root.length ? [{ members: root }] : []), ...shared, ...(solo.length ? [{ members: solo }] : [])];
|
|
360
365
|
}
|
|
361
|
-
/** `buildRootEntry`'s display label. Named here because `printPlanRow` has to recognize the one
|
|
362
|
-
* singleton whose group is worth printing, and a bare `'root'` in a condition reads as a guess. */
|
|
363
|
-
const ROOT_GROUP = 'root';
|
|
364
366
|
/** The column names in the order `printPlanRow` writes them - `pushDelimeter` has to be handed the
|
|
365
367
|
* same set, or the dashes appear under a column that does not exist yet. */
|
|
366
368
|
const PLAN_COLUMNS = ['Status', 'Package', 'Group', 'From', '', 'To', 'Reason'];
|
|
367
|
-
function printPlanRow(table, e
|
|
369
|
+
function printPlanRow(table, e) {
|
|
368
370
|
table.cell('Status', statusLabel(e.status));
|
|
369
371
|
table.cell('Package', colors.cyan(e.package.name));
|
|
370
|
-
|
|
371
|
-
* **Blank for a package that shares its version line with nobody.** `resolveGroupKey` gives an
|
|
372
|
-
* ungrouped package a group of one named after the package, so the cell printed the Package
|
|
373
|
-
* column again, one column to the right - a word that looks like information and carries none.
|
|
374
|
-
* Worse, it made `(default)` and `(@panates/tsconfig)` read as the same *kind* of answer when one
|
|
375
|
-
* names a line four packages move along and the other names nothing at all.
|
|
376
|
-
*
|
|
377
|
-
* The root keeps its label: `root` is not its package name but what the row *is*, and it is the
|
|
378
|
-
* only thing on the line saying that the number below is the repository's identity rather than a
|
|
379
|
-
* release. So the test is `shared`, plus that one entry - not "does this string repeat the name",
|
|
380
|
-
* which would blank a genuine group that happened to be called after one of its members.
|
|
381
|
-
*/
|
|
382
|
-
table.cell('Group', shared || e.group === ROOT_GROUP ? colors.gray(`(${e.group})`) : '');
|
|
372
|
+
table.cell('Group', groupCell(e));
|
|
383
373
|
table.cell('From', e.from);
|
|
384
374
|
/**
|
|
385
375
|
* Gated on `to`, not on `status === 'bump'`, which is what kept the column blank on the one
|
|
@@ -394,6 +384,38 @@ function printPlanRow(table, e, shared) {
|
|
|
394
384
|
table.cell('Reason', e.status === 'error' ? colors.red(e.reason ?? '') : colors.gray(e.reason ?? ''));
|
|
395
385
|
table.newRow();
|
|
396
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* What the Group column says for one row - a **name** where the repository gave the group one, a
|
|
389
|
+
* **description** in grey parentheses where it did not, and nothing where there is no group at all.
|
|
390
|
+
*/
|
|
391
|
+
/* **A name and a description are different kinds of thing, and the cell says which.** `(default)`
|
|
392
|
+
* and `(root)` are not names anybody wrote - the default group is the line every package is on until
|
|
393
|
+
* told otherwise, and `root` is what the row *is*: the repository's release identity rather than a
|
|
394
|
+
* release. Both are notes about the row, so they read as notes. A named group (`group: "core"`) is a
|
|
395
|
+
* value the repository chose, decides its changelog file and its tag, and is printed as a value -
|
|
396
|
+
* bare, in the ordinary colour. Asked for directly: printed `(abisena-iomt)` in grey, a real group
|
|
397
|
+
* read like an annotation beside the default one.
|
|
398
|
+
*
|
|
399
|
+
* **Decided by the group key, never by the label.** A group may be *named* `default` or `root` - both
|
|
400
|
+
* pass `assertGroupName` - and testing the label would print it as the description it is spelling.
|
|
401
|
+
*
|
|
402
|
+
* **Blank for a package in no group** (`group: false`): its group of one is named after the package,
|
|
403
|
+
* so the cell would repeat the Package column - a word that looks like information and carries none.
|
|
404
|
+
*
|
|
405
|
+
* **Blank for a single-package repository's one package** too, which is the root and is in the
|
|
406
|
+
* default group: there is nothing there to group, and `(default)` on the only row would be noise. It
|
|
407
|
+
* was blank before this cell distinguished names from descriptions, and stays so. */
|
|
408
|
+
function groupCell(e) {
|
|
409
|
+
if (e.groupKey === ROOT_GROUP_KEY)
|
|
410
|
+
return colors.gray(`(${e.group})`);
|
|
411
|
+
if (e.package.isRoot || isSoloGroupKey(e.groupKey))
|
|
412
|
+
return '';
|
|
413
|
+
if (isNamedGroupKey(e.groupKey))
|
|
414
|
+
return e.group;
|
|
415
|
+
/** The default group - and any key a custom planner's `resolveGroupKey` returns, which is a
|
|
416
|
+
* description of its own making rather than a name the repository wrote. */
|
|
417
|
+
return colors.gray(`(${e.group})`);
|
|
418
|
+
}
|
|
397
419
|
function statusLabel(status) {
|
|
398
420
|
switch (status) {
|
|
399
421
|
case 'bump':
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.11.0';
|
package/core/application.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { VersionPlanService } from '../services/version-plan.service.js';
|
|
2
|
-
import { Logger, type LogLevel } from '../utils/logger.js';
|
|
3
2
|
import type { StatusRegion } from '../utils/status-region.js';
|
|
3
|
+
import type { LogSink } from './classes/log-sink.js';
|
|
4
|
+
import { Logger, type LogLevel } from './classes/logger.js';
|
|
4
5
|
import { Registry } from './classes/registry.js';
|
|
5
6
|
import type { Repository } from './classes/repository.js';
|
|
6
7
|
import type { ServiceFactory, ServiceMap } from './classes/service.js';
|
|
@@ -42,6 +43,12 @@ export declare class RmanApplication {
|
|
|
42
43
|
* routes its output through `passThrough`.
|
|
43
44
|
*/
|
|
44
45
|
statusRegion?: StatusRegion;
|
|
46
|
+
/**
|
|
47
|
+
* Where the run's log goes besides the screen - the global `--json` and `--log-file`. Set and
|
|
48
|
+
* closed by `cli.ts` around each command, like `statusRegion`; absent for a caller that did not
|
|
49
|
+
* come through the CLI, which writes no log.
|
|
50
|
+
*/
|
|
51
|
+
logSink?: LogSink;
|
|
45
52
|
/**
|
|
46
53
|
* The **plugins** themselves, in declaration order - what a config named, before the platforms
|
|
47
54
|
* were taken out of them.
|
package/core/application.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ImportService } from '../services/import.service.js';
|
|
|
7
7
|
import { ListService } from '../services/list.service.js';
|
|
8
8
|
import { RunService } from '../services/run.service.js';
|
|
9
9
|
import { VersionService } from '../services/version.service.js';
|
|
10
|
-
import { Logger } from '
|
|
10
|
+
import { Logger } from './classes/logger.js';
|
|
11
11
|
import { Registry } from './classes/registry.js';
|
|
12
12
|
import { basePlatform } from './interfaces/plugin.js';
|
|
13
13
|
/**
|
|
@@ -50,6 +50,12 @@ export class RmanApplication {
|
|
|
50
50
|
* state, and a spec that builds its own application cannot be affected by one that built
|
|
51
51
|
* another. The root-hook era this repository removed is what that rule is about. */
|
|
52
52
|
statusRegion;
|
|
53
|
+
/**
|
|
54
|
+
* Where the run's log goes besides the screen - the global `--json` and `--log-file`. Set and
|
|
55
|
+
* closed by `cli.ts` around each command, like `statusRegion`; absent for a caller that did not
|
|
56
|
+
* come through the CLI, which writes no log.
|
|
57
|
+
*/
|
|
58
|
+
logSink;
|
|
53
59
|
/**
|
|
54
60
|
* The **plugins** themselves, in declaration order - what a config named, before the platforms
|
|
55
61
|
* were taken out of them.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ProgressItem } from '../../utils/progress-panel.js';
|
|
2
|
+
import type { LogLevel } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* One thing that happened during a run, in the order it happened. The JSON form is exactly this
|
|
5
|
+
* object with a `time` added; the text form is one line per event.
|
|
6
|
+
*/
|
|
7
|
+
export type LogEvent = {
|
|
8
|
+
event: 'start';
|
|
9
|
+
package: string;
|
|
10
|
+
step: string;
|
|
11
|
+
command?: string;
|
|
12
|
+
} | {
|
|
13
|
+
event: 'output';
|
|
14
|
+
package: string;
|
|
15
|
+
stream: 'stdout' | 'stderr';
|
|
16
|
+
line: string;
|
|
17
|
+
/** Set on a line rman itself wrote to say why a step failed - never inferred from the stream. */
|
|
18
|
+
level?: 'error';
|
|
19
|
+
} | {
|
|
20
|
+
event: 'end';
|
|
21
|
+
package: string;
|
|
22
|
+
step: string;
|
|
23
|
+
status: 'success' | 'failed';
|
|
24
|
+
ms: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
} | {
|
|
27
|
+
event: 'summary';
|
|
28
|
+
succeeded: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
skipped: number;
|
|
31
|
+
ms: number;
|
|
32
|
+
} | {
|
|
33
|
+
event: 'message';
|
|
34
|
+
level: 'info' | 'error';
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Something a run's events are rendered by - the screen, the `--json` stream, a log file.
|
|
39
|
+
*
|
|
40
|
+
* The scheduler produces each event once and hands it to every reporter the run has; none of them
|
|
41
|
+
* knows the others exist. Adding a destination is a reporter, never another branch where the event
|
|
42
|
+
* is produced.
|
|
43
|
+
*/
|
|
44
|
+
export interface Reporter {
|
|
45
|
+
/**
|
|
46
|
+
* One event. `origin` is given for every event that belongs to a package - which row it is, the
|
|
47
|
+
* package's own log level, and the step's label - and is absent for the run's own (`summary`,
|
|
48
|
+
* `message`).
|
|
49
|
+
*/
|
|
50
|
+
report(event: LogEvent, origin?: ReportOrigin): void;
|
|
51
|
+
/**
|
|
52
|
+
* Environment a child should be started with for this reporter to render its lines - `FORCE_COLOR`
|
|
53
|
+
* where a reporter prints them to a terminal, nothing where colour would only be stripped again.
|
|
54
|
+
*/
|
|
55
|
+
readonly childEnv?: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
/** Which package an event is about, as a screen reporter needs to know it. */
|
|
58
|
+
export interface ReportOrigin {
|
|
59
|
+
/** The package's row - the run's state for it, whether or not a panel is drawing it. */
|
|
60
|
+
item: ProgressItem;
|
|
61
|
+
/** The package's own `run.<script>.logLevel`, which decides what a plain screen prints. */
|
|
62
|
+
logLevel: LogLevel;
|
|
63
|
+
/** The step's label for a one-line-per-step log; empty for an anonymous function. */
|
|
64
|
+
label?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Where a run's log goes besides the screen - the global `--json` and `--log-file`.
|
|
68
|
+
*
|
|
69
|
+
* - **`--json`**: the log is written to stdout as JSON Lines, one event per line, and nothing else
|
|
70
|
+
* is - no panel, no spinner, no prose.
|
|
71
|
+
* - **`--log-file <path>`**: the log is written to that file, in the same format the console uses -
|
|
72
|
+
* JSON Lines under `--json`, plain text lines otherwise.
|
|
73
|
+
*
|
|
74
|
+
* One per invocation, on `RmanApplication.logSink`, beside `statusRegion` and for the same reason:
|
|
75
|
+
* what writes to it is handed the application already, so nothing reaches for ambient state.
|
|
76
|
+
*/
|
|
77
|
+
export declare class LogSink implements Reporter {
|
|
78
|
+
/** The console format is JSON Lines - so nothing but events may reach stdout. */
|
|
79
|
+
readonly json: boolean;
|
|
80
|
+
/** Whether anything was written - read by the CLI to warn about a flag nothing honoured. */
|
|
81
|
+
used: boolean;
|
|
82
|
+
private fd?;
|
|
83
|
+
private readonly file?;
|
|
84
|
+
constructor(options: {
|
|
85
|
+
/** Write the log to stdout as JSON Lines, and use that format for the file. */
|
|
86
|
+
json?: boolean;
|
|
87
|
+
/** Where to write the log; resolved against `cwd`. */
|
|
88
|
+
file?: string;
|
|
89
|
+
cwd?: string;
|
|
90
|
+
});
|
|
91
|
+
/** Whether an event would go anywhere. A caller can skip building one when it would not. */
|
|
92
|
+
get active(): boolean;
|
|
93
|
+
/** The resolved file, for the message that reports it. */
|
|
94
|
+
get filePath(): string | undefined;
|
|
95
|
+
report(event: LogEvent): void;
|
|
96
|
+
/** Closes the file, if one was opened. Safe to call twice. */
|
|
97
|
+
close(): void;
|
|
98
|
+
protected append(text: string): void;
|
|
99
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Where a run's log goes besides the screen - the global `--json` and `--log-file`.
|
|
5
|
+
*
|
|
6
|
+
* - **`--json`**: the log is written to stdout as JSON Lines, one event per line, and nothing else
|
|
7
|
+
* is - no panel, no spinner, no prose.
|
|
8
|
+
* - **`--log-file <path>`**: the log is written to that file, in the same format the console uses -
|
|
9
|
+
* JSON Lines under `--json`, plain text lines otherwise.
|
|
10
|
+
*
|
|
11
|
+
* One per invocation, on `RmanApplication.logSink`, beside `statusRegion` and for the same reason:
|
|
12
|
+
* what writes to it is handed the application already, so nothing reaches for ambient state.
|
|
13
|
+
*/
|
|
14
|
+
/* **A log, not an answer.** `list`, `version`, `publish`, `config`, `info` and `github-release`
|
|
15
|
+
* declare their own `--json`, and what it prints is the command's *result* - a contract the shared
|
|
16
|
+
* release workflow reads with `jq` (`rman publish --dry-run --json`, `rman list --json`). This never
|
|
17
|
+
* writes to stdout for one of those: the result stays where it was, in the shape it had.
|
|
18
|
+
*
|
|
19
|
+
* **Every line is stripped of escape sequences** before it is written anywhere. A child piped with
|
|
20
|
+
* `FORCE_COLOR` carries colour, and a JSON string or a log file holding `\u001b[32m` is noise to every
|
|
21
|
+
* reader that is not a terminal - which is every reader of either.
|
|
22
|
+
*
|
|
23
|
+
* **The file is opened on the first event, not up front**, so a command that writes no log leaves
|
|
24
|
+
* no empty file behind - and `used` stays false, which is how the CLI can say so. Written
|
|
25
|
+
* synchronously, line by line: an interrupted run keeps every line written before the interrupt,
|
|
26
|
+
* and lines from concurrent packages stay whole and in order. */
|
|
27
|
+
export class LogSink {
|
|
28
|
+
/** The console format is JSON Lines - so nothing but events may reach stdout. */
|
|
29
|
+
json;
|
|
30
|
+
/** Whether anything was written - read by the CLI to warn about a flag nothing honoured. */
|
|
31
|
+
used = false;
|
|
32
|
+
fd;
|
|
33
|
+
file;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.json = !!options.json;
|
|
36
|
+
this.file = options.file ? path.resolve(options.cwd ?? process.cwd(), options.file) : undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Whether an event would go anywhere. A caller can skip building one when it would not. */
|
|
39
|
+
get active() {
|
|
40
|
+
return this.json || !!this.file;
|
|
41
|
+
}
|
|
42
|
+
/** The resolved file, for the message that reports it. */
|
|
43
|
+
get filePath() {
|
|
44
|
+
return this.file;
|
|
45
|
+
}
|
|
46
|
+
report(event) {
|
|
47
|
+
if (!this.active)
|
|
48
|
+
return;
|
|
49
|
+
this.used = true;
|
|
50
|
+
const time = new Date().toISOString();
|
|
51
|
+
const clean = stripEvent(event);
|
|
52
|
+
const json = JSON.stringify({ time, ...clean });
|
|
53
|
+
if (this.json)
|
|
54
|
+
process.stdout.write(json + '\n');
|
|
55
|
+
if (this.file)
|
|
56
|
+
this.append((this.json ? json : textLine(time, clean)) + '\n');
|
|
57
|
+
}
|
|
58
|
+
/** Closes the file, if one was opened. Safe to call twice. */
|
|
59
|
+
close() {
|
|
60
|
+
if (this.fd === undefined)
|
|
61
|
+
return;
|
|
62
|
+
fs.closeSync(this.fd);
|
|
63
|
+
this.fd = undefined;
|
|
64
|
+
}
|
|
65
|
+
append(text) {
|
|
66
|
+
if (this.fd === undefined) {
|
|
67
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
68
|
+
this.fd = fs.openSync(this.file, 'w');
|
|
69
|
+
}
|
|
70
|
+
fs.writeSync(this.fd, text);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** One event as a line of the text log: the time, the package in brackets, and what happened. */
|
|
74
|
+
function textLine(time, e) {
|
|
75
|
+
switch (e.event) {
|
|
76
|
+
case 'start':
|
|
77
|
+
return `${time} [${e.package}] ▶ ${e.step}${e.command ? ` | ${e.command}` : ''}`;
|
|
78
|
+
case 'output':
|
|
79
|
+
return `${time} [${e.package}] ${e.line}`;
|
|
80
|
+
case 'end':
|
|
81
|
+
return (`${time} [${e.package}] ${e.status === 'success' ? '✔' : '✖'} ${e.step} ${e.status} (${e.ms} ms)` +
|
|
82
|
+
(e.error ? `\n${time} [${e.package}] ${e.error.split('\n').join(`\n${time} [${e.package}] `)}` : ''));
|
|
83
|
+
case 'summary':
|
|
84
|
+
return (`${time} ${e.succeeded} succeeded, ${e.failed} failed` +
|
|
85
|
+
(e.skipped ? `, ${e.skipped} skipped` : '') +
|
|
86
|
+
` (${e.ms} ms)`);
|
|
87
|
+
case 'message':
|
|
88
|
+
return `${time} ${e.message}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Every string field of an event, with escape sequences removed. */
|
|
92
|
+
function stripEvent(e) {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [k, v] of Object.entries(e))
|
|
95
|
+
out[k] = typeof v === 'string' ? stripAnsi(v) : v;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function stripAnsi(s) {
|
|
99
|
+
// eslint-disable-next-line no-control-regex
|
|
100
|
+
return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
101
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ProgressPanel } from '../../utils/progress-panel.js';
|
|
2
|
+
import type { LogEvent, Reporter, ReportOrigin } from './log-sink.js';
|
|
3
|
+
import { type LogLevel } from './logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* The screen with no live panel: each line a step prints, led by its package, and one line per step
|
|
6
|
+
* when it ends - the output `--no-progress`, a pipe or CI gets.
|
|
7
|
+
*/
|
|
8
|
+
export declare class PlainReporter implements Reporter {
|
|
9
|
+
/** What the per-step lines call the run - `run`, `build`, `check`. */
|
|
10
|
+
protected readonly commandName: string;
|
|
11
|
+
/** Whose tally the recap prints; absent for a reporter that only says rman's own messages. */
|
|
12
|
+
protected readonly panel?: ProgressPanel | undefined;
|
|
13
|
+
readonly childEnv: Record<string, string> | undefined;
|
|
14
|
+
/** Four letters per level, as winston and log4j-style formats shorten them - `ERROR` keeps its five. */
|
|
15
|
+
protected readonly levelTags: Readonly<Record<Exclude<LogLevel, 'silent'>, string>>;
|
|
16
|
+
constructor(
|
|
17
|
+
/** What the per-step lines call the run - `run`, `build`, `check`. */
|
|
18
|
+
commandName: string,
|
|
19
|
+
/** Whose tally the recap prints; absent for a reporter that only says rman's own messages. */
|
|
20
|
+
panel?: ProgressPanel | undefined);
|
|
21
|
+
report(event: LogEvent, origin?: ReportOrigin): void;
|
|
22
|
+
/** The `executing` line, at `verbose` only - rman's old behaviour, hidden by default. */
|
|
23
|
+
protected started(event: Extract<LogEvent, {
|
|
24
|
+
event: 'start';
|
|
25
|
+
}>, origin: ReportOrigin): void;
|
|
26
|
+
/** One line of a step's output, to the stream it came from, led by its package - in red when rman
|
|
27
|
+
* itself wrote the line to say why the step failed (`level: 'error'`). */
|
|
28
|
+
protected output(event: Extract<LogEvent, {
|
|
29
|
+
event: 'output';
|
|
30
|
+
}>, origin: ReportOrigin): void;
|
|
31
|
+
/** `[INFO] build pkg ┆ exec success ┆ tsc -b (123 ms)` - suppressed below `info` unless it failed,
|
|
32
|
+
* and under `silent` even then. */
|
|
33
|
+
protected ended(event: Extract<LogEvent, {
|
|
34
|
+
event: 'end';
|
|
35
|
+
}>, origin: ReportOrigin): void;
|
|
36
|
+
/**
|
|
37
|
+
* A line's own level as `[INFO]`, `[VERB]`, `[ERROR]` - upper case, four letters but for `ERROR`, in
|
|
38
|
+
* the level's colour.
|
|
39
|
+
*/
|
|
40
|
+
protected levelTag(level: Exclude<LogLevel, 'silent'>): string;
|
|
41
|
+
/** Whether `level` prints what `wanted` does. */
|
|
42
|
+
protected atLeast(level: LogLevel, wanted: LogLevel): boolean;
|
|
43
|
+
/** The `┆ <what it runs>` tail of a step line, **or nothing at all** when the step has no label of
|
|
44
|
+
* its own - an anonymous function handed to `forEachPackage`, where the slot name already said
|
|
45
|
+
* everything and a separator with nothing after it is noise. */
|
|
46
|
+
protected describing(origin: ReportOrigin): string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The screen while the live panel draws: a step's lines go to its package's row - the last one
|
|
50
|
+
* shown under it, all of them replayed if the package fails - and the recap lists every package.
|
|
51
|
+
*/
|
|
52
|
+
export declare class PanelReporter extends PlainReporter {
|
|
53
|
+
/** A child's lines are only stored, and replayed into a row whose width is measured - colour
|
|
54
|
+
* would only be escape codes counted as text there. */
|
|
55
|
+
readonly childEnv: undefined;
|
|
56
|
+
constructor(commandName: string, panel: ProgressPanel);
|
|
57
|
+
/** The row names the step itself; there is no separate line to print. */
|
|
58
|
+
protected started(): void;
|
|
59
|
+
protected output(event: Extract<LogEvent, {
|
|
60
|
+
event: 'output';
|
|
61
|
+
}>, origin: ReportOrigin): void;
|
|
62
|
+
/** The row shows the step's outcome; the recap prints the package's. */
|
|
63
|
+
protected ended(): void;
|
|
64
|
+
}
|