rman 2.10.1 → 2.11.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.
- 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/builtins/platforms/node/services/publish.service.d.ts +10 -5
- package/builtins/platforms/node/services/publish.service.js +46 -11
- package/builtins/publish-targets/npm/npm-publish-target.js +3 -1
- package/cli.js +81 -23
- package/commands/changelog.command.js +1 -1
- package/commands/version.command.js +42 -29
- 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 +9 -0
- package/utils/version-group.js +11 -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;
|
|
@@ -43,6 +43,11 @@ export declare namespace PublishService {
|
|
|
43
43
|
* goes green" is the most important sentence in the plan when it applies.
|
|
44
44
|
*/
|
|
45
45
|
staged?: boolean;
|
|
46
|
+
/** Subdirectory to publish from, relative to the package's own directory - only consulted when
|
|
47
|
+
* the package doesn't already declare its own `package.json` `publishConfig.directory` (npm's
|
|
48
|
+
* native mechanism for this, which always wins when present). A plan option, because the
|
|
49
|
+
* manifest in that directory is what the plan decides from. */
|
|
50
|
+
contents?: string;
|
|
46
51
|
}
|
|
47
52
|
interface ApplyOptions extends Options {
|
|
48
53
|
packageManager?: CiService.PackageManager;
|
|
@@ -50,10 +55,6 @@ export declare namespace PublishService {
|
|
|
50
55
|
access?: 'public' | 'restricted';
|
|
51
56
|
/** `npm publish --otp <otp>` - a 2FA one-time password, for registries that require it. */
|
|
52
57
|
otp?: string;
|
|
53
|
-
/** Subdirectory to publish from, relative to the package's own directory - only consulted when
|
|
54
|
-
* the package doesn't already declare its own `package.json` `publishConfig.directory` (npm's
|
|
55
|
-
* native mechanism for this, which always wins when present). */
|
|
56
|
-
contents?: string;
|
|
57
58
|
}
|
|
58
59
|
/** One package's outcome in a publish plan - see `getPlan`. */
|
|
59
60
|
interface Entry {
|
|
@@ -101,7 +102,11 @@ export declare namespace PublishService {
|
|
|
101
102
|
* queries it to decide, so it's safe to call any time, including as the plan a bare `rman
|
|
102
103
|
* publish` shows before asking for confirmation.
|
|
103
104
|
*
|
|
104
|
-
*
|
|
105
|
+
* **Whether a package is private is read from the manifest that would be published** - the one in
|
|
106
|
+
* its build directory when it publishes from one, its own otherwise. A build directory with no
|
|
107
|
+
* `package.json` is an `'error'`: nothing has been built to publish. A package `private` in its
|
|
108
|
+
* source with no `publishConfig` is skipped before that is asked, since it stays private in any
|
|
109
|
+
* manifest derived from it. Each remaining package's **dist-tag**
|
|
105
110
|
* is settled next (`distTagFor`): usually derived from the version itself, `'error'` in the two
|
|
106
111
|
* cases with nothing to derive - that one is about the invocation rather than the package, so it
|
|
107
112
|
* is answered before anything touches the network. A package with uncommitted local changes is
|
|
@@ -72,21 +72,19 @@ function preparePublishManifest(pkg, publishDir, packagesByName) {
|
|
|
72
72
|
* builds a native module on install. The rest (`build`, `test`, `prepare`, ...) never reach the
|
|
73
73
|
* consumer - `prepare` runs for a git dependency, which builds from the repository, not from this
|
|
74
74
|
* tarball.
|
|
75
|
-
* - `private` -
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* `rman publish` became its own release path and the gate started refusing the thing it ships.
|
|
81
|
-
* A narrower guard (`prepublishOnly`, which never reaches a consumer either) says the same thing
|
|
82
|
-
* without claiming the artifact is unpublishable.
|
|
75
|
+
* - `private` - **only when the package declares a `publishConfig`**. A package configured to be
|
|
76
|
+
* published that is also `private` is guarding its source tree against a stray `npm publish`,
|
|
77
|
+
* not refusing to ship; one with no `publishConfig` means it, and keeps the flag. `getPlan`
|
|
78
|
+
* decides from the manifest this produces, so the rule here *is* the publish rule - a preset
|
|
79
|
+
* writing the build directory's manifest must apply the same one.
|
|
83
80
|
* - `publishConfig.directory` - it pointed *here*; kept, it would point one level deeper again.
|
|
84
81
|
*/
|
|
85
82
|
function derivePublishManifest(pkg, packagesByName) {
|
|
86
83
|
const json = structuredClone(pkg.manifest.raw);
|
|
87
84
|
resolveWorkspaceRanges(json, packagesByName);
|
|
88
85
|
delete json.devDependencies;
|
|
89
|
-
|
|
86
|
+
if (json.publishConfig)
|
|
87
|
+
delete json.private;
|
|
90
88
|
if (json.scripts) {
|
|
91
89
|
const kept = Object.fromEntries(Object.entries(json.scripts).filter(([name]) => CONSUMER_SCRIPTS.has(name)));
|
|
92
90
|
if (Object.keys(kept).length)
|
|
@@ -129,7 +127,11 @@ export var PublishService;
|
|
|
129
127
|
* queries it to decide, so it's safe to call any time, including as the plan a bare `rman
|
|
130
128
|
* publish` shows before asking for confirmation.
|
|
131
129
|
*
|
|
132
|
-
*
|
|
130
|
+
* **Whether a package is private is read from the manifest that would be published** - the one in
|
|
131
|
+
* its build directory when it publishes from one, its own otherwise. A build directory with no
|
|
132
|
+
* `package.json` is an `'error'`: nothing has been built to publish. A package `private` in its
|
|
133
|
+
* source with no `publishConfig` is skipped before that is asked, since it stays private in any
|
|
134
|
+
* manifest derived from it. Each remaining package's **dist-tag**
|
|
133
135
|
* is settled next (`distTagFor`): usually derived from the version itself, `'error'` in the two
|
|
134
136
|
* cases with nothing to derive - that one is about the invocation rather than the package, so it
|
|
135
137
|
* is answered before anything touches the network. A package with uncommitted local changes is
|
|
@@ -187,7 +189,19 @@ export var PublishService;
|
|
|
187
189
|
reason: 'excluded via .rmanrc "publish.skip"',
|
|
188
190
|
});
|
|
189
191
|
}
|
|
190
|
-
else if (pkg
|
|
192
|
+
else if (privateBySource(pkg)) {
|
|
193
|
+
entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
|
|
194
|
+
}
|
|
195
|
+
else if (!fs.existsSync(path.join(resolvePublishDir(pkg, options.contents), 'package.json'))) {
|
|
196
|
+
const dir = path.relative(pkg.dirname, resolvePublishDir(pkg, options.contents)) || '.';
|
|
197
|
+
entries.set(pkg.name, {
|
|
198
|
+
package: pkg,
|
|
199
|
+
version: pkg.version,
|
|
200
|
+
status: 'error',
|
|
201
|
+
reason: `nothing to publish in "${dir}" - it has no package.json; build the package first`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else if (publishedManifestOf(pkg, options.contents).private) {
|
|
191
205
|
entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
|
|
192
206
|
}
|
|
193
207
|
else if (distTag.error) {
|
|
@@ -310,6 +324,27 @@ function resolvePublishDir(pkg, contentsOverride) {
|
|
|
310
324
|
const rel = (typeof native === 'string' && native) || configured || contentsOverride;
|
|
311
325
|
return rel ? path.resolve(pkg.dirname, rel) : pkg.dirname;
|
|
312
326
|
}
|
|
327
|
+
/** The manifest `npm publish` will read for `pkg` - its build directory's, or its own when it
|
|
328
|
+
* publishes in place. */
|
|
329
|
+
/* **Decided from the artifact, not the source**, because the two disagree about `private` and only
|
|
330
|
+
* one of them is published. Read before any build, the plan answered `skip - private package` for a
|
|
331
|
+
* single-package repository whose source carries `private: true` as a guard against a stray
|
|
332
|
+
* `npm publish` - while `derivePublishManifest` was removing that very flag from what it ships.
|
|
333
|
+
* Reported from `postgrejs-kysely`: version, tag and GitHub release all went through, and the
|
|
334
|
+
* registry got nothing. */
|
|
335
|
+
function publishedManifestOf(pkg, contentsOverride) {
|
|
336
|
+
const dir = resolvePublishDir(pkg, contentsOverride);
|
|
337
|
+
if (path.resolve(dir) === path.resolve(pkg.dirname))
|
|
338
|
+
return pkg.manifest.raw;
|
|
339
|
+
return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
340
|
+
}
|
|
341
|
+
/** `private` in the source with no `publishConfig` - private in every manifest derived from it too,
|
|
342
|
+
* so it is known without a build. */
|
|
343
|
+
/* Asked first so an unbuilt private package is skipped rather than reported as unbuilt: opra's
|
|
344
|
+
* examples inherit `publish.npm.directory` from the shared preset and skip their build. */
|
|
345
|
+
function privateBySource(pkg) {
|
|
346
|
+
return !!pkg.manifest.raw.private && !pkg.manifest.raw.publishConfig;
|
|
347
|
+
}
|
|
313
348
|
/**
|
|
314
349
|
* Which registry a package's own version is asked about, and published to.
|
|
315
350
|
*
|
|
@@ -84,7 +84,6 @@ export class NpmPublishTarget {
|
|
|
84
84
|
packageManager: ctx.args.packageManager,
|
|
85
85
|
access: ctx.args.access,
|
|
86
86
|
otp: ctx.args.otp,
|
|
87
|
-
contents: ctx.args.contents,
|
|
88
87
|
});
|
|
89
88
|
}
|
|
90
89
|
/** The shared filters plus the two flags both halves need - read here rather than in the
|
|
@@ -103,6 +102,9 @@ export class NpmPublishTarget {
|
|
|
103
102
|
* flag was not given, so each package's own `publish.npm.staged` still decides - `false`
|
|
104
103
|
* here would mean `--no-staged` and silently overrule the config. */
|
|
105
104
|
staged: ctx.args.staged,
|
|
105
|
+
/** The plan decides from the manifest in the directory being published, so it has to know
|
|
106
|
+
* which directory that is. */
|
|
107
|
+
contents: ctx.args.contents,
|
|
106
108
|
};
|
|
107
109
|
}
|
|
108
110
|
}
|
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,7 +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 { isSoloGroupKey } from '../utils/version-group.js';
|
|
8
|
+
import { isNamedGroupKey, isSoloGroupKey, ROOT_GROUP_KEY } from '../utils/version-group.js';
|
|
9
9
|
/**
|
|
10
10
|
* Hoisted out of the metadata literal so the handler can be annotated against them - see
|
|
11
11
|
* `RmanConfig.ArgsOf` for why an inferred `argv` and the metadata's own typo checking cannot both
|
|
@@ -290,12 +290,12 @@ function printApplied(result) {
|
|
|
290
290
|
function printPlan(entries) {
|
|
291
291
|
const table = new EasyTable();
|
|
292
292
|
let first = true;
|
|
293
|
-
for (const { members
|
|
293
|
+
for (const { members } of planBlocks(entries)) {
|
|
294
294
|
if (!first)
|
|
295
295
|
table.pushDelimeter(PLAN_COLUMNS);
|
|
296
296
|
first = false;
|
|
297
297
|
for (const e of members)
|
|
298
|
-
printPlanRow(table, e
|
|
298
|
+
printPlanRow(table, e);
|
|
299
299
|
}
|
|
300
300
|
console.log(table.toString().trim());
|
|
301
301
|
}
|
|
@@ -322,9 +322,9 @@ function printPlan(entries) {
|
|
|
322
322
|
* repository that has nothing to group - noise standing in for structure. They share a block
|
|
323
323
|
* because what they have in common is real: none of them is tied to anyone else.
|
|
324
324
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
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.
|
|
328
328
|
*/
|
|
329
329
|
function planBlocks(entries) {
|
|
330
330
|
/**
|
|
@@ -359,36 +359,17 @@ function planBlocks(entries) {
|
|
|
359
359
|
if (isSoloGroupKey(key))
|
|
360
360
|
solo.push(...group);
|
|
361
361
|
else
|
|
362
|
-
shared.push({ members: group
|
|
362
|
+
shared.push({ members: group });
|
|
363
363
|
}
|
|
364
|
-
return [
|
|
365
|
-
...(root.length ? [{ members: root, shared: false }] : []),
|
|
366
|
-
...shared,
|
|
367
|
-
...(solo.length ? [{ members: solo, shared: false }] : []),
|
|
368
|
-
];
|
|
364
|
+
return [...(root.length ? [{ members: root }] : []), ...shared, ...(solo.length ? [{ members: solo }] : [])];
|
|
369
365
|
}
|
|
370
|
-
/** `buildRootEntry`'s display label. Named here because `printPlanRow` has to recognize the one
|
|
371
|
-
* singleton whose group is worth printing, and a bare `'root'` in a condition reads as a guess. */
|
|
372
|
-
const ROOT_GROUP = 'root';
|
|
373
366
|
/** The column names in the order `printPlanRow` writes them - `pushDelimeter` has to be handed the
|
|
374
367
|
* same set, or the dashes appear under a column that does not exist yet. */
|
|
375
368
|
const PLAN_COLUMNS = ['Status', 'Package', 'Group', 'From', '', 'To', 'Reason'];
|
|
376
|
-
function printPlanRow(table, e
|
|
369
|
+
function printPlanRow(table, e) {
|
|
377
370
|
table.cell('Status', statusLabel(e.status));
|
|
378
371
|
table.cell('Package', colors.cyan(e.package.name));
|
|
379
|
-
|
|
380
|
-
* **Blank for a package that shares its version line with nobody.** `resolveGroupKey` gives an
|
|
381
|
-
* ungrouped package a group of one named after the package, so the cell printed the Package
|
|
382
|
-
* column again, one column to the right - a word that looks like information and carries none.
|
|
383
|
-
* Worse, it made `(default)` and `(@panates/tsconfig)` read as the same *kind* of answer when one
|
|
384
|
-
* names a line four packages move along and the other names nothing at all.
|
|
385
|
-
*
|
|
386
|
-
* The root keeps its label: `root` is not its package name but what the row *is*, and it is the
|
|
387
|
-
* only thing on the line saying that the number below is the repository's identity rather than a
|
|
388
|
-
* release. So the test is `shared`, plus that one entry - not "does this string repeat the name",
|
|
389
|
-
* which would blank a genuine group that happened to be called after one of its members.
|
|
390
|
-
*/
|
|
391
|
-
table.cell('Group', shared || e.group === ROOT_GROUP ? colors.gray(`(${e.group})`) : '');
|
|
372
|
+
table.cell('Group', groupCell(e));
|
|
392
373
|
table.cell('From', e.from);
|
|
393
374
|
/**
|
|
394
375
|
* Gated on `to`, not on `status === 'bump'`, which is what kept the column blank on the one
|
|
@@ -403,6 +384,38 @@ function printPlanRow(table, e, shared) {
|
|
|
403
384
|
table.cell('Reason', e.status === 'error' ? colors.red(e.reason ?? '') : colors.gray(e.reason ?? ''));
|
|
404
385
|
table.newRow();
|
|
405
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
|
+
}
|
|
406
419
|
function statusLabel(status) {
|
|
407
420
|
switch (status) {
|
|
408
421
|
case 'bump':
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.11.1';
|
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
|
+
}
|