@williamthorsen/nmr 0.37.0 → 0.38.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/.meta/changelog.json +30 -0
- package/.readyup/kits/default.js +9 -9
- package/.readyup/manifest.json +5 -5
- package/CHANGELOG.md +19 -0
- package/README.md +17 -34
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache.d.ts +2 -1
- package/dist/esm/check-cache.js +4 -4
- package/dist/esm/cli-build.js +3 -0
- package/dist/esm/cli-clean.js +3 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +3 -1
- package/dist/esm/cli-report-catalog.js +2 -1
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/commands/build.d.ts +6 -1
- package/dist/esm/commands/build.js +9 -10
- package/dist/esm/commands/clean.d.ts +3 -2
- package/dist/esm/commands/clean.js +15 -14
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +3 -2
- package/dist/esm/commands/ensure-prepublish-hooks.js +8 -6
- package/dist/esm/commands/report-catalog.d.ts +2 -1
- package/dist/esm/commands/report-catalog.js +5 -4
- package/dist/esm/commands/report-overrides.d.ts +2 -1
- package/dist/esm/commands/report-overrides.js +5 -4
- package/dist/esm/config.js +14 -4
- package/dist/esm/default-scripts.d.ts +2 -2
- package/dist/esm/default-scripts.js +5 -5
- package/dist/esm/glyphs.d.ts +9 -0
- package/dist/esm/glyphs.js +9 -0
- package/dist/esm/help.js +15 -9
- package/dist/esm/output-style.d.ts +41 -0
- package/dist/esm/output-style.js +17 -0
- package/dist/esm/recording.d.ts +3 -0
- package/dist/esm/recording.js +6 -3
- package/dist/esm/resolveBinStyles.d.ts +12 -0
- package/dist/esm/resolveBinStyles.js +16 -0
- package/dist/esm/resolver.js +2 -2
- package/dist/esm/runCli.js +82 -60
- package/dist/esm/steps.d.ts +5 -5
- package/dist/esm/steps.js +6 -2
- package/dist/esm/verdict.d.ts +6 -2
- package/dist/esm/verdict.js +16 -10
- package/docs/check-cache.md +5 -5
- package/docs/reporting.md +14 -0
- package/docs/scripts.md +2 -2
- package/package.json +8 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
3
4
|
import { readPackageJson } from "../helpers/package-json.js";
|
|
4
5
|
import { reportClosing } from "../helpers/reportClosing.js";
|
|
5
6
|
import { isObject } from "../helpers/type-guards.js";
|
|
@@ -60,14 +61,14 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
|
60
61
|
const hasFailures = packages.some((p) => p.action === 'missing');
|
|
61
62
|
return { packages, hasFailures };
|
|
62
63
|
}
|
|
63
|
-
export function reportPrepublishHooks(result, hookCommand) {
|
|
64
|
+
export function reportPrepublishHooks(result, hookCommand, style) {
|
|
64
65
|
const publishable = result.packages.filter((pkg) => !pkg.isPrivate);
|
|
65
66
|
if (publishable.length === 0) {
|
|
66
67
|
console.info('No publishable packages found.');
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
for (const pkg of publishable) {
|
|
70
|
-
console.info(renderHookStatus(pkg, hookCommand));
|
|
71
|
+
console.info(renderHookStatus(pkg, hookCommand, style));
|
|
71
72
|
}
|
|
72
73
|
reportClosing(describeHookRun(publishable));
|
|
73
74
|
}
|
|
@@ -102,14 +103,15 @@ function describeHookRun(publishable) {
|
|
|
102
103
|
}
|
|
103
104
|
return `${packages} ${publishable.length === 1 ? 'has' : 'have'} prepublishOnly.`;
|
|
104
105
|
}
|
|
105
|
-
function renderHookStatus(pkg, hookCommand) {
|
|
106
|
+
function renderHookStatus(pkg, hookCommand, style) {
|
|
107
|
+
const statuses = STATUS_GLYPHS[style];
|
|
106
108
|
switch (pkg.action) {
|
|
107
109
|
case 'ok':
|
|
108
|
-
return
|
|
110
|
+
return `${statuses.passed.text} ${pkg.packageName}: prepublishOnly = "${pkg.prepublishOnly}"`;
|
|
109
111
|
case 'missing':
|
|
110
|
-
return
|
|
112
|
+
return `${statuses.failed.text} ${pkg.packageName}: missing prepublishOnly`;
|
|
111
113
|
case 'fixed':
|
|
112
|
-
return
|
|
114
|
+
return `${statuses.passed.text} ${pkg.packageName}: added prepublishOnly = "${hookCommand}"`;
|
|
113
115
|
case 'would-fix':
|
|
114
116
|
return `~ ${pkg.packageName}: would add prepublishOnly = "${hookCommand}"`;
|
|
115
117
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
1
2
|
/**
|
|
2
3
|
* Reports the catalogued dependencies a package-scoped upgrade pass cannot see.
|
|
3
4
|
*
|
|
@@ -8,4 +9,4 @@
|
|
|
8
9
|
*
|
|
9
10
|
* Runs ahead of the report produced by the workspace `upgrade` script.
|
|
10
11
|
*/
|
|
11
|
-
export declare function reportCatalog(cwd: string): void;
|
|
12
|
+
export declare function reportCatalog(cwd: string, style: OutputStyle): void;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { formatGlyphLine } from '@williamthorsen/nmr-core';
|
|
1
2
|
import { findContainingPackageDir } from "../context.js";
|
|
3
|
+
import { NMR_GLYPHS } from "../glyphs.js";
|
|
2
4
|
import { DEPENDENCY_FIELDS, readPackageJson } from "../helpers/package-json.js";
|
|
3
5
|
import { reportClosing } from "../helpers/reportClosing.js";
|
|
4
6
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
5
|
-
const CATALOG_ICON = '📚';
|
|
6
7
|
const CATALOG_PROTOCOL = 'catalog:';
|
|
7
|
-
export function reportCatalog(cwd) {
|
|
8
|
+
export function reportCatalog(cwd, style) {
|
|
8
9
|
const monorepoRoot = findMonorepoRoot(cwd);
|
|
9
10
|
const packageDir = findContainingPackageDir(cwd, getWorkspacePackageDirs(monorepoRoot));
|
|
10
11
|
if (packageDir === undefined) {
|
|
@@ -14,11 +15,11 @@ export function reportCatalog(cwd) {
|
|
|
14
15
|
if (dependencies.length === 0) {
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
|
-
console.warn(
|
|
18
|
+
console.warn(formatGlyphLine(NMR_GLYPHS, style, 'catalog', 'WARN: A package-scoped upgrade does not read the catalogs these come from:'));
|
|
18
19
|
for (const { name, specifier } of dependencies) {
|
|
19
20
|
console.warn(`- ${name} → ${specifier}`);
|
|
20
21
|
}
|
|
21
|
-
reportClosing(
|
|
22
|
+
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'catalog', describeCatalog(dependencies.length, monorepoRoot)), console.warn);
|
|
22
23
|
}
|
|
23
24
|
function describeCatalog(count, monorepoRoot) {
|
|
24
25
|
const subject = count === 1 ? '1 catalogued dependency went' : `${count} catalogued dependencies went`;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
1
2
|
/**
|
|
2
3
|
* Reports the pnpm dependency overrides declared in the monorepo root's `pnpm-workspace.yaml`, and rejects a
|
|
3
4
|
* `pnpm.overrides` block left behind in the root `package.json`.
|
|
4
5
|
*
|
|
5
6
|
* Runs ahead of the report produced by the root `upgrade` script.
|
|
6
7
|
*/
|
|
7
|
-
export declare function reportOverrides(monorepoRoot: string): void;
|
|
8
|
+
export declare function reportOverrides(monorepoRoot: string, style: OutputStyle): void;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { formatGlyphLine } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { NMR_GLYPHS } from "../glyphs.js";
|
|
1
3
|
import { getPnpmOverrides, readPackageJson } from "../helpers/package-json.js";
|
|
2
4
|
import { reportClosing } from "../helpers/reportClosing.js";
|
|
3
5
|
import { UserError } from "../UserError.js";
|
|
4
6
|
import { readWorkspaceOverrides } from "../workspace.js";
|
|
5
|
-
|
|
6
|
-
export function reportOverrides(monorepoRoot) {
|
|
7
|
+
export function reportOverrides(monorepoRoot, style) {
|
|
7
8
|
const declared = listEntries(readWorkspaceOverrides(monorepoRoot));
|
|
8
9
|
if (declared.length > 0) {
|
|
9
|
-
console.warn(
|
|
10
|
+
console.warn(formatGlyphLine(NMR_GLYPHS, style, 'overrides', 'WARN: pnpm overrides are active:'));
|
|
10
11
|
for (const [name, version] of declared) {
|
|
11
12
|
console.warn(`- ${name} → ${version}`);
|
|
12
13
|
}
|
|
13
|
-
reportClosing(
|
|
14
|
+
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(declared.length)), console.warn);
|
|
14
15
|
}
|
|
15
16
|
rejectLegacyOverrides(monorepoRoot);
|
|
16
17
|
}
|
package/dist/esm/config.js
CHANGED
|
@@ -33,7 +33,8 @@ const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.works
|
|
|
33
33
|
const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
|
|
34
34
|
const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
|
|
35
35
|
const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
|
|
36
|
-
const RECOGNIZED_STEP_KEYS = ['
|
|
36
|
+
const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments'];
|
|
37
|
+
const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
|
|
37
38
|
function isScriptRecord(value) {
|
|
38
39
|
if (!isObject(value))
|
|
39
40
|
return false;
|
|
@@ -50,8 +51,8 @@ function isScriptElement(value) {
|
|
|
50
51
|
return true;
|
|
51
52
|
if (!isObject(value) || typeof value['run'] !== 'string')
|
|
52
53
|
return false;
|
|
53
|
-
const
|
|
54
|
-
return
|
|
54
|
+
const shouldDeclineArguments = value['shouldDeclineArguments'];
|
|
55
|
+
return shouldDeclineArguments === undefined || typeof shouldDeclineArguments === 'boolean';
|
|
55
56
|
}
|
|
56
57
|
function validateScriptField(value, fieldName, configPath) {
|
|
57
58
|
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
@@ -60,7 +61,7 @@ function validateScriptField(value, fieldName, configPath) {
|
|
|
60
61
|
const scripts = value[fieldName];
|
|
61
62
|
if (!isScriptRecord(scripts)) {
|
|
62
63
|
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
|
|
63
|
-
'where an element is a command string or `{ run: string,
|
|
64
|
+
'where an element is a command string or `{ run: string, shouldDeclineArguments?: boolean }`');
|
|
64
65
|
}
|
|
65
66
|
assertValidElements(scripts, fieldName, configPath);
|
|
66
67
|
return scripts;
|
|
@@ -71,6 +72,7 @@ function assertValidElements(scripts, fieldName, configPath) {
|
|
|
71
72
|
continue;
|
|
72
73
|
for (const spec of script) {
|
|
73
74
|
if (typeof spec !== 'string') {
|
|
75
|
+
assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
74
76
|
assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
75
77
|
}
|
|
76
78
|
const element = typeof spec === 'string' ? spec : spec.run;
|
|
@@ -270,6 +272,14 @@ export async function loadWorkspaceConfig(packageDir) {
|
|
|
270
272
|
assertTierKeys(config, CONFIG_TIERS.workspace, resolveConfigPath(packageDir));
|
|
271
273
|
return config;
|
|
272
274
|
}
|
|
275
|
+
function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
276
|
+
for (const key of Object.keys(value)) {
|
|
277
|
+
const replacement = retiredKeys.get(key);
|
|
278
|
+
if (replacement === undefined)
|
|
279
|
+
continue;
|
|
280
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
273
283
|
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
274
284
|
const unrecognized = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
275
285
|
if (unrecognized.length === 0) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One element of a composite, paired with what it does with the invocation's trailing arguments. The bare
|
|
3
|
-
* string form of an element is this spec with `
|
|
3
|
+
* string form of an element is this spec with `shouldDeclineArguments` left at its default.
|
|
4
4
|
*/
|
|
5
5
|
export interface StepSpec {
|
|
6
6
|
/** A command name, optionally preceded by nmr's own flags, as it would be typed after `nmr`. */
|
|
@@ -9,7 +9,7 @@ export interface StepSpec {
|
|
|
9
9
|
* Set where the trailing arguments cannot narrow this step's work: it is a prerequisite the narrowed steps
|
|
10
10
|
* run against, or the tool it reaches would be misled by them. A declining step runs unnarrowed.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
shouldDeclineArguments?: boolean;
|
|
13
13
|
}
|
|
14
14
|
export type ScriptValue = string | ReadonlyArray<string | StepSpec>;
|
|
15
15
|
export type ScriptRegistry = Record<string, ScriptValue>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const GATE_PROJECTS = '--project unit --project tool';
|
|
2
|
-
const TYPECHECK_STEP = { run: 'typecheck',
|
|
3
|
-
const ROOT_TYPECHECK_STEP = { run: 'root:typecheck',
|
|
2
|
+
const TYPECHECK_STEP = { run: 'typecheck', shouldDeclineArguments: true };
|
|
3
|
+
const ROOT_TYPECHECK_STEP = { run: 'root:typecheck', shouldDeclineArguments: true };
|
|
4
4
|
export const workspaceScripts = {
|
|
5
5
|
build: ['compile'],
|
|
6
6
|
check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
|
|
@@ -32,7 +32,7 @@ export const rootScripts = {
|
|
|
32
32
|
build: ['-R build'],
|
|
33
33
|
check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
|
|
34
34
|
'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test'],
|
|
35
|
-
ci: [{ run: 'build',
|
|
35
|
+
ci: [{ run: 'build', shouldDeclineArguments: true }, 'check:strict'],
|
|
36
36
|
clean: 'nmr-clean',
|
|
37
37
|
fix: ['lint', 'fmt'],
|
|
38
38
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
@@ -41,7 +41,7 @@ export const rootScripts = {
|
|
|
41
41
|
lint: 'eslint --fix .',
|
|
42
42
|
'lint:check': 'eslint .',
|
|
43
43
|
'lint:strict': 'strict-lint',
|
|
44
|
-
prepush: [{ run: 'audit',
|
|
44
|
+
prepush: [{ run: 'audit', shouldDeclineArguments: true }, 'ci'],
|
|
45
45
|
'report-overrides': 'nmr-report-overrides',
|
|
46
46
|
'root:check': [ROOT_TYPECHECK_STEP, 'fmt:check', 'root:lint:check', 'root:test'],
|
|
47
47
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
|
@@ -59,6 +59,6 @@ export const rootScripts = {
|
|
|
59
59
|
'test:tool': ['root:test:tool', '-R test:tool'],
|
|
60
60
|
'test:unit': ['root:test:unit', '-R test:unit'],
|
|
61
61
|
'test:watch': `vitest ${GATE_PROJECTS} --watch`,
|
|
62
|
-
typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck',
|
|
62
|
+
typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', shouldDeclineArguments: true }],
|
|
63
63
|
upgrade: 'nmr-report-overrides && nmr-taze --recursive',
|
|
64
64
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glyphs that name no status; a status marker comes from nmr-core's `STATUS_GLYPHS`. A plain variant is empty
|
|
3
|
+
* where a word always follows the glyph, and a word where the glyph opens a line of its own.
|
|
4
|
+
*
|
|
5
|
+
* `noop` is a neutral circle: nothing ran because the repo asked for nothing to run, which is neither a
|
|
6
|
+
* failure nor a block.
|
|
7
|
+
*/
|
|
8
|
+
export declare const NMR_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"overrides" | "clean" | "package" | "catalog" | "noop" | "recording", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
|
|
9
|
+
export type NmrGlyphName = keyof (typeof NMR_GLYPHS)['rich'];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineGlyphSet } from '@williamthorsen/nmr-core';
|
|
2
|
+
export const NMR_GLYPHS = defineGlyphSet({
|
|
3
|
+
catalog: { plain: '', rich: '\u{1F4DA}' },
|
|
4
|
+
clean: { plain: '', rich: '\u{1F9F9}' },
|
|
5
|
+
noop: { plain: 'NOOP', rich: '\u{26AA}' },
|
|
6
|
+
overrides: { plain: '', rich: '\u{1F512}' },
|
|
7
|
+
package: { plain: '', rich: '\u{1F4E6}' },
|
|
8
|
+
recording: { plain: '', rich: '\u{1F4BE}' },
|
|
9
|
+
});
|
package/dist/esm/help.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { isHookName } from "./helpers/hook-name.js";
|
|
2
|
+
import { OUTPUT_STYLE_ENV_VAR, OUTPUT_STYLE_FLAG } from "./output-style.js";
|
|
2
3
|
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
3
4
|
export function generateHelp(config, packageDir, useRoot) {
|
|
4
5
|
const lines = [
|
|
5
6
|
'Usage: nmr [flags] <command> [args...]',
|
|
6
7
|
'',
|
|
7
8
|
'Flags:',
|
|
8
|
-
' -F, --filter <pattern>
|
|
9
|
-
' -R, --recursive
|
|
10
|
-
' -w, --workspace-root
|
|
11
|
-
" -q, --quiet
|
|
12
|
-
' --json
|
|
13
|
-
' --log
|
|
14
|
-
' --no-cache
|
|
15
|
-
|
|
16
|
-
'
|
|
9
|
+
' -F, --filter <pattern> Run command in packages whose manifest name matches',
|
|
10
|
+
' -R, --recursive Run command in all packages',
|
|
11
|
+
' -w, --workspace-root Use root scripts, running at the monorepo root',
|
|
12
|
+
" -q, --quiet Suppress command output, keeping nmr's verdicts",
|
|
13
|
+
' --json Report one JSON object per command, withholding command output',
|
|
14
|
+
' --log Print the recorded run instead of running (up to 256 KiB)',
|
|
15
|
+
' --no-cache Run even if this tree already passed; record the result',
|
|
16
|
+
` ${OUTPUT_STYLE_FLAG} <style> Output style: auto (default), plain, or rich`,
|
|
17
|
+
' -?, --help Show this help',
|
|
18
|
+
' -V, --version Show version number',
|
|
19
|
+
'',
|
|
20
|
+
'Environment:',
|
|
21
|
+
` ${OUTPUT_STYLE_ENV_VAR} Output style: auto (default), plain, or rich. auto prints`,
|
|
22
|
+
' plain, without emoji, when CI is set or the stream is not a terminal.',
|
|
17
23
|
'',
|
|
18
24
|
'Workspace commands:',
|
|
19
25
|
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import { type StreamStyleResolution } from '@williamthorsen/nmr-core';
|
|
3
|
+
/**
|
|
4
|
+
* Reports whether a stream is backed by a terminal, which is what detection reads where no source names a
|
|
5
|
+
* style. A stream carrying no `isTTY`, which is the one a test injects, is not one.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isTerminalStream(stream: Writable): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Carries the resolved style down the spawned chain, so the flag reaches every process rather than the first
|
|
10
|
+
* and a child writing to a pipe renders as its parent does rather than detecting plain on its own descriptor.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately not a keyed variable, on neither the pass key nor the retention key: it changes how a run
|
|
13
|
+
* renders and never what a command concludes, so folding it into either would stop a plain run from hitting a
|
|
14
|
+
* pass a rich one recorded.
|
|
15
|
+
*/
|
|
16
|
+
export declare const OUTPUT_STYLE_ENV_VAR = "NMR_OUTPUT_STYLE";
|
|
17
|
+
/**
|
|
18
|
+
* The flag naming a style for one invocation, which outranks `OUTPUT_STYLE_ENV_VAR`.
|
|
19
|
+
*
|
|
20
|
+
* `nmr` alone carries it. A standalone bin takes the variable, which `nmr <command>` exports to it, so adding
|
|
21
|
+
* a style to a bin does not mean adding a flag to each bin's own parser.
|
|
22
|
+
*/
|
|
23
|
+
export declare const OUTPUT_STYLE_FLAG = "--output-style";
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the style of each output stream: the flag, then `NMR_OUTPUT_STYLE`, then detection from `CI`, the
|
|
26
|
+
* stream's terminal state, and `TERM`.
|
|
27
|
+
*
|
|
28
|
+
* Never throws. A value naming no style is returned in `invalid`, for the caller to report with
|
|
29
|
+
* `describeInvalidOutputStyle`, because the caller has to render that complaint in some style.
|
|
30
|
+
*
|
|
31
|
+
* The flag's raw value is handed to nmr-core as an assignment rather than narrowed here, so nmr rejects a
|
|
32
|
+
* misspelling in the words its siblings use and no second ladder can come to name a different one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveOutputStyles(options: ResolveOutputStylesOptions): StreamStyleResolution;
|
|
35
|
+
export interface ResolveOutputStylesOptions {
|
|
36
|
+
env: NodeJS.ProcessEnv;
|
|
37
|
+
/** The value the flag was given, absent where the invocation carries none, as a bin's invocation always does. */
|
|
38
|
+
flagValue?: string | undefined;
|
|
39
|
+
stderrIsTty: boolean;
|
|
40
|
+
stdoutIsTty: boolean;
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { resolveStreamStyles } from '@williamthorsen/nmr-core';
|
|
2
|
+
export function isTerminalStream(stream) {
|
|
3
|
+
return 'isTTY' in stream && stream.isTTY === true;
|
|
4
|
+
}
|
|
5
|
+
export const OUTPUT_STYLE_ENV_VAR = 'NMR_OUTPUT_STYLE';
|
|
6
|
+
export const OUTPUT_STYLE_FLAG = '--output-style';
|
|
7
|
+
export function resolveOutputStyles(options) {
|
|
8
|
+
const { env, flagValue, stderrIsTty, stdoutIsTty } = options;
|
|
9
|
+
return resolveStreamStyles({
|
|
10
|
+
argv: flagValue === undefined ? [] : [`${OUTPUT_STYLE_FLAG}=${flagValue}`],
|
|
11
|
+
env,
|
|
12
|
+
envVar: OUTPUT_STYLE_ENV_VAR,
|
|
13
|
+
flag: OUTPUT_STYLE_FLAG,
|
|
14
|
+
stderrIsTty,
|
|
15
|
+
stdoutIsTty,
|
|
16
|
+
});
|
|
17
|
+
}
|
package/dist/esm/recording.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
1
2
|
import { type CheckCacheEntry } from './check-cache.js';
|
|
2
3
|
/**
|
|
3
4
|
* What this invocation would be recorded under, insofar as an entry records the same facts. A mismatch is read
|
|
@@ -75,6 +76,7 @@ export declare function renderRecording(options: {
|
|
|
75
76
|
command: string;
|
|
76
77
|
recording: Recording;
|
|
77
78
|
scope: string;
|
|
79
|
+
style: OutputStyle;
|
|
78
80
|
}): string;
|
|
79
81
|
/**
|
|
80
82
|
* Renders a refusal on the one line a fan-out can attribute, in the grammar a verdict uses: the scope, the
|
|
@@ -84,6 +86,7 @@ export declare function renderRefusal(options: {
|
|
|
84
86
|
command: string;
|
|
85
87
|
refusal: RecordingRefusal;
|
|
86
88
|
scope: string;
|
|
89
|
+
style: OutputStyle;
|
|
87
90
|
}): string;
|
|
88
91
|
/**
|
|
89
92
|
* Resolves what one scope has to show for one command.
|
package/dist/esm/recording.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import { formatGlyphLine, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
1
2
|
import { readCheckCacheEntry, readTranscript } from "./check-cache.js";
|
|
3
|
+
import { NMR_GLYPHS } from "./glyphs.js";
|
|
2
4
|
import { formatDuration } from "./helpers/duration.js";
|
|
3
5
|
export function renderRecording(options) {
|
|
4
6
|
const { entry } = options.recording;
|
|
5
7
|
const age = formatDuration(Math.max(0, Date.now() - Date.parse(entry.recordedAt)));
|
|
6
|
-
const header =
|
|
7
|
-
`ran in ${formatDuration(entry.durationMs)}
|
|
8
|
+
const header = formatGlyphLine(NMR_GLYPHS, options.style, 'recording', `${options.scope}: ${options.command} — recorded ${entry.recordedAt} (${age} ago), ` +
|
|
9
|
+
`ran in ${formatDuration(entry.durationMs)}`) + `\n$ ${entry.commandString}\n\n`;
|
|
8
10
|
return `${header}${appendNewline(renderBody(options.recording))}`;
|
|
9
11
|
}
|
|
10
12
|
export function renderRefusal(options) {
|
|
11
|
-
|
|
13
|
+
const { text } = STATUS_GLYPHS[options.style].warning;
|
|
14
|
+
return `${text} ${options.scope}: ${options.command}: no recording; ${describeRefusal(options.command, options.refusal)}`;
|
|
12
15
|
}
|
|
13
16
|
export async function resolveRecording(options) {
|
|
14
17
|
const { anchorDir, command, monorepoRoot } = options;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type StreamStyles } from '@williamthorsen/nmr-core';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the style of the running process's own output streams, and rejects a variable naming no style.
|
|
4
|
+
*
|
|
5
|
+
* What a standalone bin calls. A bin carries no `--output-style` of its own: the flag is `nmr`'s, and what
|
|
6
|
+
* reaches a bin is the variable that `nmr <command>` exports to it, so a bin run through nmr renders as nmr
|
|
7
|
+
* does and one run on its own detects its streams.
|
|
8
|
+
*
|
|
9
|
+
* Throws rather than exiting, so the bin's own boundary reports the message and sets the exit code, as it does
|
|
10
|
+
* for every other failure in what the caller declared.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveBinStyles(): StreamStyles;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { describeInvalidOutputStyle } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { resolveOutputStyles } from "./output-style.js";
|
|
4
|
+
import { UserError } from "./UserError.js";
|
|
5
|
+
export function resolveBinStyles() {
|
|
6
|
+
const { invalid, styles } = resolveOutputStyles({
|
|
7
|
+
env: process.env,
|
|
8
|
+
flagValue: undefined,
|
|
9
|
+
stderrIsTty: process.stderr.isTTY,
|
|
10
|
+
stdoutIsTty: process.stdout.isTTY,
|
|
11
|
+
});
|
|
12
|
+
if (invalid !== undefined) {
|
|
13
|
+
throw new UserError(describeInvalidOutputStyle(invalid));
|
|
14
|
+
}
|
|
15
|
+
return styles;
|
|
16
|
+
}
|
package/dist/esm/resolver.js
CHANGED
|
@@ -46,7 +46,7 @@ export function expandScript(script, workspaceRoot) {
|
|
|
46
46
|
}
|
|
47
47
|
return script.map((element) => typeof element === 'string'
|
|
48
48
|
? composeNmrStep(element, workspaceRoot)
|
|
49
|
-
: composeNmrStep(element.run, workspaceRoot, element.
|
|
49
|
+
: composeNmrStep(element.run, workspaceRoot, element.shouldDeclineArguments));
|
|
50
50
|
}
|
|
51
51
|
export function describeScript(script) {
|
|
52
52
|
return typeof script === 'string' ? script : `[${script.map(describeElement).join(', ')}]`;
|
|
@@ -55,7 +55,7 @@ function describeElement(element) {
|
|
|
55
55
|
if (typeof element === 'string') {
|
|
56
56
|
return element;
|
|
57
57
|
}
|
|
58
|
-
return element.
|
|
58
|
+
return element.shouldDeclineArguments === true ? `${element.run} (no args)` : element.run;
|
|
59
59
|
}
|
|
60
60
|
export function readPackageJsonScripts(packageDir) {
|
|
61
61
|
const file = resolvePackageJsonPath(packageDir);
|