@williamthorsen/nmr 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.meta/changelog.json +164 -0
  2. package/.readyup/kits/default.js +51 -15
  3. package/.readyup/manifest.json +26 -21
  4. package/CHANGELOG.md +45 -0
  5. package/README.md +10 -4
  6. package/dist/esm/{check-cache-build-output.d.ts → check-cache/check-cache-build-output.d.ts} +1 -1
  7. package/dist/esm/{check-cache-build-output.js → check-cache/check-cache-build-output.js} +6 -6
  8. package/dist/esm/{check-cache-delegation.d.ts → check-cache/check-cache-delegation.d.ts} +3 -3
  9. package/dist/esm/{check-cache-delegation.js → check-cache/check-cache-delegation.js} +2 -2
  10. package/dist/esm/{check-cache-entries.js → check-cache/check-cache-entries.js} +1 -1
  11. package/dist/esm/{check-cache-gate.d.ts → check-cache/check-cache-gate.d.ts} +5 -4
  12. package/dist/esm/{check-cache-gate.js → check-cache/check-cache-gate.js} +10 -10
  13. package/dist/esm/{check-cache-key.d.ts → check-cache/check-cache-key.d.ts} +1 -1
  14. package/dist/esm/{check-cache-record.d.ts → check-cache/check-cache-record.d.ts} +6 -5
  15. package/dist/esm/{check-cache-record.js → check-cache/check-cache-record.js} +7 -6
  16. package/dist/esm/{check-cache.d.ts → check-cache/check-cache.d.ts} +10 -2
  17. package/dist/esm/{check-cache.js → check-cache/check-cache.js} +7 -4
  18. package/dist/esm/{config-check-cache.d.ts → check-cache/config-check-cache.d.ts} +1 -1
  19. package/dist/esm/{config-check-cache.js → check-cache/config-check-cache.js} +4 -4
  20. package/dist/esm/{pass-manifest.d.ts → check-cache/pass-manifest.d.ts} +1 -1
  21. package/dist/esm/{pass-manifest.js → check-cache/pass-manifest.js} +1 -1
  22. package/dist/esm/{recording.js → check-cache/recording.js} +2 -2
  23. package/dist/esm/{replay-assembly.d.ts → check-cache/replay-assembly.d.ts} +1 -1
  24. package/dist/esm/{replay-assembly.js → check-cache/replay-assembly.js} +2 -2
  25. package/dist/esm/child-env.d.ts +1 -1
  26. package/dist/esm/child-env.js +2 -2
  27. package/dist/esm/cli-build.js +3 -10
  28. package/dist/esm/cli-clean.js +1 -2
  29. package/dist/esm/cli-report-catalog.js +2 -3
  30. package/dist/esm/cli-report-overrides.js +3 -5
  31. package/dist/esm/commands/actionlint.d.ts +2 -7
  32. package/dist/esm/commands/actionlint.js +5 -11
  33. package/dist/esm/commands/build-output.d.ts +7 -32
  34. package/dist/esm/commands/build-output.js +8 -13
  35. package/dist/esm/commands/build-report.d.ts +6 -0
  36. package/dist/esm/commands/build-report.js +21 -0
  37. package/dist/esm/commands/build.d.ts +27 -4
  38. package/dist/esm/commands/build.js +24 -86
  39. package/dist/esm/commands/clean-report.d.ts +6 -0
  40. package/dist/esm/commands/clean-report.js +20 -0
  41. package/dist/esm/commands/clean.d.ts +39 -7
  42. package/dist/esm/commands/clean.js +55 -31
  43. package/dist/esm/commands/compile.d.ts +5 -0
  44. package/dist/esm/commands/compile.js +32 -0
  45. package/dist/esm/commands/fmt-cache.js +3 -2
  46. package/dist/esm/commands/fmt.d.ts +6 -2
  47. package/dist/esm/commands/fmt.js +48 -27
  48. package/dist/esm/commands/node-pin.d.ts +15 -5
  49. package/dist/esm/commands/node-pin.js +43 -10
  50. package/dist/esm/commands/package-manifest.d.ts +28 -0
  51. package/dist/esm/commands/package-manifest.js +32 -0
  52. package/dist/esm/commands/publish-output.d.ts +40 -0
  53. package/dist/esm/commands/publish-output.js +66 -0
  54. package/dist/esm/commands/report-catalog.d.ts +5 -0
  55. package/dist/esm/commands/report-catalog.js +19 -0
  56. package/dist/esm/commands/report-overrides.d.ts +5 -0
  57. package/dist/esm/commands/report-overrides.js +20 -1
  58. package/dist/esm/commands/run-vitest.js +21 -20
  59. package/dist/esm/commands/shellcheck.d.ts +2 -7
  60. package/dist/esm/commands/shellcheck.js +3 -11
  61. package/dist/esm/commands/spawn-inherited.d.ts +21 -0
  62. package/dist/esm/commands/spawn-inherited.js +11 -0
  63. package/dist/esm/commands/taze.d.ts +6 -6
  64. package/dist/esm/commands/taze.js +3 -7
  65. package/dist/esm/config-validation.js +1 -1
  66. package/dist/esm/config.js +3 -12
  67. package/dist/esm/{delegate-host.d.ts → delegation/delegate-host.d.ts} +3 -3
  68. package/dist/esm/{delegate-host.js → delegation/delegate-host.js} +9 -8
  69. package/dist/esm/{delegation-target.js → delegation/delegation-target.js} +1 -1
  70. package/dist/esm/{remote-probe.js → delegation/remote-probe.js} +2 -2
  71. package/dist/esm/{remote-run.js → delegation/remote-run.js} +5 -5
  72. package/dist/esm/{remote-session.js → delegation/remote-session.js} +1 -1
  73. package/dist/esm/{user-config.js → delegation/user-config.js} +4 -13
  74. package/dist/esm/env-switch.d.ts +25 -0
  75. package/dist/esm/env-switch.js +26 -0
  76. package/dist/esm/fan-out.d.ts +4 -1
  77. package/dist/esm/fan-out.js +1 -3
  78. package/dist/esm/help.js +1 -1
  79. package/dist/esm/helpers/filter-selection.d.ts +2 -0
  80. package/dist/esm/helpers/importConfigDefault.d.ts +7 -0
  81. package/dist/esm/helpers/importConfigDefault.js +45 -0
  82. package/dist/esm/invocation.d.ts +7 -3
  83. package/dist/esm/machine-lock.js +2 -1
  84. package/dist/esm/route.d.ts +6 -0
  85. package/dist/esm/route.js +15 -2
  86. package/dist/esm/run-chain.d.ts +1 -1
  87. package/dist/esm/run-chain.js +1 -1
  88. package/dist/esm/runCli.d.ts +9 -0
  89. package/dist/esm/runCli.js +24 -9
  90. package/dist/esm/runner.d.ts +2 -0
  91. package/dist/esm/runner.js +1 -1
  92. package/dist/esm/verdict.d.ts +1 -1
  93. package/docs/check-cache.md +7 -7
  94. package/docs/delegate.md +2 -2
  95. package/docs/machine-lock.md +4 -4
  96. package/docs/scripts.md +4 -0
  97. package/docs/upgrades.md +10 -2
  98. package/docs/utilities.md +6 -4
  99. package/package.json +7 -7
  100. /package/dist/esm/{check-cache-entries.d.ts → check-cache/check-cache-entries.d.ts} +0 -0
  101. /package/dist/esm/{check-cache-key.js → check-cache/check-cache-key.js} +0 -0
  102. /package/dist/esm/{recording.d.ts → check-cache/recording.d.ts} +0 -0
  103. /package/dist/esm/{delegation-target.d.ts → delegation/delegation-target.d.ts} +0 -0
  104. /package/dist/esm/{remote-host-session.d.ts → delegation/remote-host-session.d.ts} +0 -0
  105. /package/dist/esm/{remote-host-session.js → delegation/remote-host-session.js} +0 -0
  106. /package/dist/esm/{remote-probe-host.d.ts → delegation/remote-probe-host.d.ts} +0 -0
  107. /package/dist/esm/{remote-probe-host.js → delegation/remote-probe-host.js} +0 -0
  108. /package/dist/esm/{remote-probe.d.ts → delegation/remote-probe.d.ts} +0 -0
  109. /package/dist/esm/{remote-run.d.ts → delegation/remote-run.d.ts} +0 -0
  110. /package/dist/esm/{remote-session.d.ts → delegation/remote-session.d.ts} +0 -0
  111. /package/dist/esm/{remote-snapshot.d.ts → delegation/remote-snapshot.d.ts} +0 -0
  112. /package/dist/esm/{remote-snapshot.js → delegation/remote-snapshot.js} +0 -0
  113. /package/dist/esm/{user-config.d.ts → delegation/user-config.d.ts} +0 -0
@@ -1,20 +1,19 @@
1
1
  import { startTimer } from '@williamthorsen/nmr-core';
2
+ import { addLockEnv, takeMachineLock } from "../child-env.js";
3
+ import { runPlanOnHost } from "../delegation/delegate-host.js";
4
+ import { VERSION } from "../invocation.js";
5
+ import { formatFlagSpelling } from "../nmr-flags.js";
6
+ import { resolveChannel } from "../output-channel.js";
7
+ import { splitSegments } from "../shell-syntax.js";
2
8
  import { formatMisplacedNoCacheWarning, isCacheableCommand, resolveTreeSnapshot, writeDebugNote, } from "./check-cache.js";
3
9
  import { findStaleBuildOutput, readBuildOutputState } from "./check-cache-build-output.js";
4
10
  import { recordDelegatedPass } from "./check-cache-delegation.js";
5
11
  import { certifyRetention, readCheckCacheEntry } from "./check-cache-entries.js";
6
12
  import { computeCacheKey, computeRetentionKey } from "./check-cache-key.js";
7
13
  import { recordPass } from "./check-cache-record.js";
8
- import { addLockEnv, takeMachineLock } from "./child-env.js";
9
- import { runPlanOnHost } from "./delegate-host.js";
10
- import { VERSION } from "./invocation.js";
11
- import { formatFlagSpelling } from "./nmr-flags.js";
12
- import { resolveChannel } from "./output-channel.js";
13
14
  import { writePassManifestEntry } from "./pass-manifest.js";
14
- import { runSteps } from "./runner.js";
15
- import { splitSegments } from "./shell-syntax.js";
16
15
  export function openGate(options) {
17
- const { command, config, env, monorepoRoot, passthrough, stderr, style } = options;
16
+ const { command, config, env, monorepoRoot, passthrough, readTree, stderr, style } = options;
18
17
  if (!isCacheableCommand(config.checkCache, command)) {
19
18
  return undefined;
20
19
  }
@@ -25,7 +24,7 @@ export function openGate(options) {
25
24
  writeDebugNote(`gate disabled: ${command} was passed arguments`, env, stderr);
26
25
  return undefined;
27
26
  }
28
- const snapshot = resolveTreeSnapshot({ monorepoRoot, env });
27
+ const snapshot = resolveTreeSnapshot({ monorepoRoot, env, readTree });
29
28
  if (!snapshot.ok) {
30
29
  writeDebugNote(`gate disabled: ${snapshot.reason}`, env, stderr);
31
30
  return undefined;
@@ -83,7 +82,7 @@ export async function runGated(plan) {
83
82
  }
84
83
  const runOptions = { ...plan.runOptions, env: addLockEnv(plan.runOptions.env ?? env, lock) };
85
84
  const readElapsedMs = startTimer();
86
- const { exitCode, retainedOutput, statedOutcomes } = await runSteps(plan.steps, anchorDir, runOptions);
85
+ const { exitCode, retainedOutput, statedOutcomes } = await plan.invocation.runSteps(plan.steps, anchorDir, runOptions);
87
86
  const durationMs = readElapsedMs();
88
87
  if (exitCode === 0 && gate !== undefined) {
89
88
  await recordPass({
@@ -97,6 +96,7 @@ export async function runGated(plan) {
97
96
  key: gate.key,
98
97
  monorepoRoot,
99
98
  ownSteps: plan.ownSteps,
99
+ readTree: plan.invocation.readTree,
100
100
  retainedOutput,
101
101
  retentionKey: gate.retentionKey,
102
102
  runId: plan.runId,
@@ -1,5 +1,5 @@
1
+ import type { OutputChannels } from '../output-channel.js';
1
2
  import type { TreeSnapshot } from './check-cache.js';
2
- import type { OutputChannels } from './output-channel.js';
3
3
  /** The parts of the running interpreter that can change what a check concludes. */
4
4
  export interface RuntimeIdentity {
5
5
  arch: string;
@@ -1,11 +1,11 @@
1
1
  import type { Writable } from 'node:stream';
2
- import { type TreeSnapshot } from './check-cache.js';
2
+ import { type RunCliResult, type RunPlan } from '../invocation.js';
3
+ import type { RetainedOutput } from '../runner.js';
4
+ import type { Step } from '../steps.js';
5
+ import type { NmrConfig } from '../types.js';
6
+ import { type TreeReader, type TreeSnapshot } from './check-cache.js';
3
7
  import { type BuildOutputState } from './check-cache-build-output.js';
4
8
  import { type EntryRef, type Retention } from './check-cache-entries.js';
5
- import { type RunCliResult, type RunPlan } from './invocation.js';
6
- import type { RetainedOutput } from './runner.js';
7
- import type { Step } from './steps.js';
8
- import type { NmrConfig } from './types.js';
9
9
  /**
10
10
  * Records a pass that `host` earned, unless a pass earned here under the same key would still be recalled, because
11
11
  * that pass satisfies every invocation that the host pass would. One whose build output is missing or came from
@@ -53,6 +53,7 @@ export declare function recordPass(options: {
53
53
  monorepoRoot: string;
54
54
  retainedOutput: RetainedOutput | undefined;
55
55
  ownSteps: readonly Step[];
56
+ readTree: TreeReader;
56
57
  retentionKey: string;
57
58
  runId: string;
58
59
  snapshot: TreeSnapshot;
@@ -1,12 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
- import { isCacheableCommand, resolveTreeSnapshot, writeDebugNote } from "./check-cache.js";
3
+ import { isEnvSwitchOn } from "../env-switch.js";
4
+ import { deriveExcerpt } from "../helpers/deriveExcerpt.js";
5
+ import { composeTranscript } from "../helpers/transcript.js";
6
+ import { RUN_IF_PRESENT_ENV_VAR, VERSION } from "../invocation.js";
7
+ import { isCacheableCommand, resolveTreeSnapshot, writeDebugNote, } from "./check-cache.js";
4
8
  import { findStaleBuildOutput, readBuildOutputState } from "./check-cache-build-output.js";
5
9
  import { readCheckCacheEntry, recordTranscript, writeCheckCacheEntry, } from "./check-cache-entries.js";
6
10
  import { CURRENT_RUNTIME } from "./check-cache-key.js";
7
- import { deriveExcerpt } from "./helpers/deriveExcerpt.js";
8
- import { composeTranscript } from "./helpers/transcript.js";
9
- import { RUN_IF_PRESENT_ENV_VAR, VERSION } from "./invocation.js";
10
11
  import { writePassManifestEntry } from "./pass-manifest.js";
11
12
  import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
12
13
  import { assembleReplay } from "./replay-assembly.js";
@@ -43,7 +44,7 @@ export async function recordHostPass(options) {
43
44
  }
44
45
  export async function recordPass(options) {
45
46
  const { anchorDir, command, env, monorepoRoot, snapshot, stderr } = options;
46
- const currentRead = resolveTreeSnapshot({ monorepoRoot, env: {} });
47
+ const currentRead = resolveTreeSnapshot({ monorepoRoot, env: {}, readTree: options.readTree });
47
48
  if (!currentRead.ok) {
48
49
  writeDebugNote(`not recording ${command}: ${currentRead.reason}`, env, stderr);
49
50
  return;
@@ -124,7 +125,7 @@ export async function reportRecording(plan) {
124
125
  });
125
126
  if (!lookup.ok) {
126
127
  stderr.write(`${renderRefusal({ command, refusal: lookup.refusal, scope, style: styles.stderr })}\n`);
127
- return { exitCode: env[RUN_IF_PRESENT_ENV_VAR] === '1' ? 0 : 1 };
128
+ return { exitCode: isEnvSwitchOn(env, RUN_IF_PRESENT_ENV_VAR) ? 0 : 1 };
128
129
  }
129
130
  stdout.write(renderRecording({ command, recording: lookup.recording, scope, style: styles.stdout }));
130
131
  return { exitCode: 0 };
@@ -1,11 +1,18 @@
1
1
  import type { Writable } from 'node:stream';
2
- import { type OutputStyle } from '@williamthorsen/nmr-core';
3
- import type { CheckCacheConfig } from './types.js';
2
+ import { hashWorkingTree, type OutputStyle, readHeadSha } from '@williamthorsen/nmr-core';
3
+ import type { CheckCacheConfig } from '../types.js';
4
4
  /** One working tree, observed once per top-level invocation and shared with every process below it. */
5
5
  export interface TreeSnapshot {
6
6
  treeId: string;
7
7
  headSha: string;
8
8
  }
9
+ /** The git reads behind a tree snapshot, which a test replaces with a fake. */
10
+ export interface TreeReader {
11
+ hashWorkingTree: typeof hashWorkingTree;
12
+ readHeadSha: typeof readHeadSha;
13
+ }
14
+ /** The tree reader that asks git. */
15
+ export declare const GIT_TREE_READER: TreeReader;
9
16
  /** Set to `1` to hear why the gate did not skip, or why it is disabled. */
10
17
  export declare const DEBUG_ENV_VAR = "NMR_DEBUG";
11
18
  /**
@@ -72,6 +79,7 @@ export declare function resolveRunId(env: NodeJS.ProcessEnv): string;
72
79
  export declare function resolveTreeSnapshot(options: {
73
80
  monorepoRoot: string;
74
81
  env: NodeJS.ProcessEnv;
82
+ readTree: TreeReader;
75
83
  }): {
76
84
  ok: true;
77
85
  snapshot: TreeSnapshot;
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { realpathSync } from 'node:fs';
3
3
  import { hashWorkingTree, readHeadSha, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
4
- import { isHookName } from "./helpers/hook-name.js";
4
+ import { isEnvSwitchOn } from "../env-switch.js";
5
+ import { isHookName } from "../helpers/hook-name.js";
6
+ export const GIT_TREE_READER = { hashWorkingTree, readHeadSha };
5
7
  export const DEBUG_ENV_VAR = 'NMR_DEBUG';
6
8
  export const DEFAULT_CACHEABLE_COMMANDS = [
7
9
  'check',
@@ -54,10 +56,11 @@ export function resolveRunId(env) {
54
56
  }
55
57
  export function resolveTreeSnapshot(options) {
56
58
  const inheritedSnapshot = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
57
- if (inheritedSnapshot !== undefined && readHeadSha(options.monorepoRoot) === inheritedSnapshot.headSha) {
59
+ if (inheritedSnapshot !== undefined &&
60
+ options.readTree.readHeadSha(options.monorepoRoot) === inheritedSnapshot.headSha) {
58
61
  return { ok: true, snapshot: inheritedSnapshot };
59
62
  }
60
- const hashedTree = hashWorkingTree(options.monorepoRoot);
63
+ const hashedTree = options.readTree.hashWorkingTree(options.monorepoRoot);
61
64
  if (!hashedTree.ok) {
62
65
  return hashedTree;
63
66
  }
@@ -74,7 +77,7 @@ export function resolveTreeSnapshot(options) {
74
77
  return { ok: true, snapshot: { treeId: hashedTree.treeId, headSha: hashedTree.headSha } };
75
78
  }
76
79
  export function writeDebugNote(message, env, stderr) {
77
- if (env[DEBUG_ENV_VAR] === '1') {
80
+ if (isEnvSwitchOn(env, DEBUG_ENV_VAR)) {
78
81
  stderr.write(`nmr check-cache: ${message}\n`);
79
82
  }
80
83
  }
@@ -1,4 +1,4 @@
1
- import type { NmrConfig } from './types.js';
1
+ import type { NmrConfig } from '../types.js';
2
2
  /**
3
3
  * Rejects a `checkCache` name that does not resolve to any command. Both fields are read by name alone, so a
4
4
  * misspelt entry is inert: The command that it meant to name goes on running, indistinguishable from one that
@@ -1,8 +1,8 @@
1
1
  import { UserError } from '@williamthorsen/nmr-core';
2
- import { findClosestName } from "./helpers/findClosestName.js";
3
- import { isHookName } from "./helpers/hook-name.js";
4
- import { buildRootRegistry, buildWorkspaceRegistry, readPackageJsonScripts } from "./resolver.js";
5
- import { getWorkspacePackageDirs, isMonorepoRoot } from "./workspace.js";
2
+ import { findClosestName } from "../helpers/findClosestName.js";
3
+ import { isHookName } from "../helpers/hook-name.js";
4
+ import { buildRootRegistry, buildWorkspaceRegistry, readPackageJsonScripts } from "../resolver.js";
5
+ import { getWorkspacePackageDirs, isMonorepoRoot } from "../workspace.js";
6
6
  export function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
7
7
  const { checkCache } = config;
8
8
  if (checkCache === undefined || !isMonorepoRoot(baseDir)) {
@@ -1,6 +1,6 @@
1
1
  import type { Writable } from 'node:stream';
2
+ import type { CheckCacheConfig } from '../types.js';
2
3
  import { type ReplayLine } from './check-cache-entries.js';
3
- import type { CheckCacheConfig } from './types.js';
4
4
  /**
5
5
  * One pass that an nmr process on a host recorded or recalled during a delegated run, as the host session returns it
6
6
  * to the delegating machine.
@@ -4,9 +4,9 @@ import path from 'node:path';
4
4
  import process from 'node:process';
5
5
  import { isRecord } from '@williamthorsen/nmr-core';
6
6
  import { describeError } from '@williamthorsen/toolbelt.errors';
7
+ import { MAX_EXCERPT_BYTES } from "../helpers/deriveExcerpt.js";
7
8
  import { isCacheableCommand, PASS_MANIFEST_DIR_ENV_VAR, writeDebugNote } from "./check-cache.js";
8
9
  import { isReplayLine } from "./check-cache-entries.js";
9
- import { MAX_EXCERPT_BYTES } from "./helpers/deriveExcerpt.js";
10
10
  export function formatManifestScope(monorepoRoot, anchorDir) {
11
11
  const scope = path.relative(monorepoRoot, anchorDir);
12
12
  return scope === '' ? '.' : scope.split(path.sep).join('/');
@@ -1,7 +1,7 @@
1
1
  import { formatGlyphLine, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
2
+ import { NMR_GLYPHS } from "../glyphs.js";
3
+ import { formatDuration } from "../helpers/duration.js";
2
4
  import { readCheckCacheEntry, readTranscript } from "./check-cache-entries.js";
3
- import { NMR_GLYPHS } from "./glyphs.js";
4
- import { formatDuration } from "./helpers/duration.js";
5
5
  export function renderRecording(options) {
6
6
  const { entry } = options.recording;
7
7
  const age = formatDuration(Math.max(0, Date.now() - Date.parse(entry.recordedAt)));
@@ -1,5 +1,5 @@
1
+ import { type Step } from '../steps.js';
1
2
  import { type ReplayLine } from './check-cache-entries.js';
2
- import { type Step } from './steps.js';
3
3
  /**
4
4
  * Assembles what a composite's skip replays: the excerpts recorded by its constituents, in the order in which
5
5
  * its own steps name them.
@@ -1,6 +1,6 @@
1
+ import { readNmrStep } from "../steps.js";
2
+ import { getWorkspacePackageDirs } from "../workspace.js";
1
3
  import { readCheckCacheEntry } from "./check-cache-entries.js";
2
- import { readNmrStep } from "./steps.js";
3
- import { getWorkspacePackageDirs } from "./workspace.js";
4
4
  export async function assembleReplay(options) {
5
5
  const { anchorDir, monorepoRoot, runId, steps, treeHash } = options;
6
6
  const candidates = steps.flatMap((step) => {
@@ -1,5 +1,5 @@
1
1
  import type { StreamStyles } from '@williamthorsen/nmr-core';
2
- import { type TreeSnapshot } from './check-cache.js';
2
+ import { type TreeSnapshot } from './check-cache/check-cache.js';
3
3
  import type { Invocation } from './invocation.js';
4
4
  import { type MachineLock } from './machine-lock.js';
5
5
  import { type ReportFormat } from './report-format.js';
@@ -1,8 +1,8 @@
1
- import { DEBUG_ENV_VAR, encodeTreeSnapshot, NO_CACHE_ENV_VAR, RUN_ID_ENV_VAR, TREE_SNAPSHOT_ENV_VAR, } from "./check-cache.js";
1
+ import { DEBUG_ENV_VAR, encodeTreeSnapshot, NO_CACHE_ENV_VAR, RUN_ID_ENV_VAR, TREE_SNAPSHOT_ENV_VAR, } from "./check-cache/check-cache.js";
2
+ import { DELEGATE_ENV_VAR } from "./delegation/remote-run.js";
2
3
  import { acquireMachineLock, formatMisplacedNoLockWarning, HEAVY_COMMANDS, LOCK_TOKEN_ENV_VAR, NO_LOCK_ENV_VAR, } from "./machine-lock.js";
3
4
  import { formatFlagSpelling } from "./nmr-flags.js";
4
5
  import { OUTPUT_STYLE_ENV_VAR } from "./output-style.js";
5
- import { DELEGATE_ENV_VAR } from "./remote-run.js";
6
6
  import { REPORT_FORMAT_ENV_VAR } from "./report-format.js";
7
7
  import { COMMAND_VERBOSITY_ENV_VAR } from "./verbosity.js";
8
8
  export function addLockEnv(env, lock) {
@@ -1,16 +1,9 @@
1
+ import process from 'node:process';
1
2
  import { reportFailure } from '@williamthorsen/nmr-core';
2
3
  import { NMR_COMPILE_BIN } from "./bins.js";
3
- import { buildPackage } from "./commands/build.js";
4
- import { loadWorkspaceConfig } from "./config.js";
5
- import { resolveBinStyles } from "./resolveBinStyles.js";
4
+ import { runCompile } from "./commands/compile.js";
6
5
  try {
7
- const { stdout } = resolveBinStyles();
8
- const packageDir = process.cwd();
9
- const { build } = await loadWorkspaceConfig(packageDir);
10
- await buildPackage(packageDir, {
11
- style: stdout,
12
- ...(build?.extraIgnorePatterns !== undefined && { extraIgnorePatterns: build.extraIgnorePatterns }),
13
- });
6
+ process.exitCode = await runCompile(process.argv.slice(2), process.cwd());
14
7
  }
15
8
  catch (error) {
16
9
  reportFailure(NMR_COMPILE_BIN, error);
@@ -2,9 +2,8 @@ import process from 'node:process';
2
2
  import { reportFailure } from '@williamthorsen/nmr-core';
3
3
  import { NMR_CLEAN_BIN } from "./bins.js";
4
4
  import { runClean } from "./commands/clean.js";
5
- import { resolveBinStyles } from "./resolveBinStyles.js";
6
5
  try {
7
- await runClean(process.cwd(), resolveBinStyles().stdout);
6
+ process.exitCode = await runClean(process.argv.slice(2), process.cwd());
8
7
  }
9
8
  catch (error) {
10
9
  reportFailure(NMR_CLEAN_BIN, error);
@@ -1,10 +1,9 @@
1
1
  import process from 'node:process';
2
2
  import { reportFailure } from '@williamthorsen/nmr-core';
3
3
  import { NMR_REPORT_CATALOG_BIN } from "./bins.js";
4
- import { reportCatalog } from "./commands/report-catalog.js";
5
- import { resolveBinStyles } from "./resolveBinStyles.js";
4
+ import { runReportCatalog } from "./commands/report-catalog.js";
6
5
  try {
7
- reportCatalog(process.cwd(), resolveBinStyles().stderr);
6
+ process.exitCode = await runReportCatalog(process.argv.slice(2), process.cwd());
8
7
  }
9
8
  catch (error) {
10
9
  reportFailure(NMR_REPORT_CATALOG_BIN, error);
@@ -1,11 +1,9 @@
1
+ import process from 'node:process';
1
2
  import { reportFailure } from '@williamthorsen/nmr-core';
2
3
  import { NMR_REPORT_OVERRIDES_BIN } from "./bins.js";
3
- import { reportOverrides } from "./commands/report-overrides.js";
4
- import { resolveBinStyles } from "./resolveBinStyles.js";
5
- import { findMonorepoRoot } from "./workspace.js";
4
+ import { runReportOverrides } from "./commands/report-overrides.js";
6
5
  try {
7
- const monorepoRoot = findMonorepoRoot();
8
- reportOverrides(monorepoRoot, resolveBinStyles().stderr);
6
+ process.exitCode = await runReportOverrides(process.argv.slice(2), process.cwd());
9
7
  }
10
8
  catch (error) {
11
9
  reportFailure(NMR_REPORT_OVERRIDES_BIN, error);
@@ -1,15 +1,10 @@
1
+ import { type SpawnInherited } from './spawn-inherited.js';
1
2
  import { type WorkflowTool } from './workflow-tools.js';
2
- /** How a spawned actionlint ended. */
3
- export interface ActionlintOutcome {
4
- error?: Error | undefined;
5
- signal: NodeJS.Signals | null;
6
- status: number | null;
7
- }
8
3
  export interface RunActionlintOptions {
9
4
  /** Directories under which the pinned tools are cached, in lookup order; defaults to `resolveToolCacheRoots`. */
10
5
  cacheRoots?: readonly [string, ...string[]];
11
6
  ensure?: (tool: WorkflowTool, cacheRoots: readonly [string, ...string[]]) => Promise<string>;
12
- spawn?: (file: string, args: string[], cwd: string) => ActionlintOutcome;
7
+ spawn?: SpawnInherited;
13
8
  }
14
9
  /**
15
10
  * Lints the monorepo's GitHub Actions workflows with the pinned actionlint, running its shellcheck pass with the
@@ -1,8 +1,7 @@
1
- import { spawnSync } from 'node:child_process';
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import { UserError } from '@williamthorsen/nmr-core';
5
- import { describeSignalExitCode } from "../child-process.js";
4
+ import { resolveInheritedExitCode, spawnInherited } from "./spawn-inherited.js";
6
5
  import { ensureTool, resolveToolCacheRoots } from "./workflow-tools.js";
7
6
  const WORKFLOWS_DIR = path.join('.github', 'workflows');
8
7
  export async function runActionlint(monorepoRoot, args, options = {}) {
@@ -14,19 +13,14 @@ export async function runActionlint(monorepoRoot, args, options = {}) {
14
13
  const ensure = options.ensure ?? ((tool, roots) => ensureTool(tool, { cacheRoots: roots }));
15
14
  const actionlintPath = await ensure('actionlint', cacheRoots);
16
15
  const shellcheckPath = await ensure('shellcheck', cacheRoots);
17
- const outcome = (options.spawn ?? spawnInherited)(actionlintPath, buildActionlintArgs(shellcheckPath, args), monorepoRoot);
16
+ const outcome = (options.spawn ?? spawnInherited)(actionlintPath, buildActionlintArgs(shellcheckPath, args), {
17
+ cwd: monorepoRoot,
18
+ });
18
19
  if (outcome.error !== undefined) {
19
20
  throw new UserError(`Cannot run actionlint: ${outcome.error.message}`, { cause: outcome.error });
20
21
  }
21
- if (outcome.signal !== null) {
22
- return describeSignalExitCode(outcome.signal);
23
- }
24
- return outcome.status ?? 1;
22
+ return resolveInheritedExitCode(outcome);
25
23
  }
26
24
  export function buildActionlintArgs(shellcheckPath, args) {
27
25
  return [`-shellcheck=${shellcheckPath}`, '-pyflakes=', ...args];
28
26
  }
29
- function spawnInherited(file, args, cwd) {
30
- const result = spawnSync(file, args, { cwd, stdio: 'inherit' });
31
- return { error: result.error, signal: result.signal, status: result.status };
32
- }
@@ -6,28 +6,13 @@ export interface BuildOptions {
6
6
  ignorePatterns?: string[];
7
7
  outdir?: string;
8
8
  }
9
- /** The pair of directories through which a build publishes, both siblings of the emit directory. */
10
- export interface ScratchDirs {
11
- previousDir: string;
12
- stagingDir: string;
13
- }
14
- export declare const DEFAULT_ENTRY_GLOBS: string[];
15
- /**
16
- * Directories holding test scaffolding rather than published code, excluded from entry-point selection so that a
17
- * package does not publish its own helpers. The list differs from the vitest factory's `COVERAGE_EXCLUDE`, which
18
- * leaves `test-utils/` inside the coverage include set, and neither list can be derived from the other.
19
- *
20
- * Ignoring a file removes it as an entry point, not from the emit. The compiler still emits whatever the
21
- * surviving entry points import, which keeps a production module that uses a helper from emitting a dangling
22
- * specifier. Widening this list can therefore only drop files that production code does not import.
23
- */
24
- export declare const DEFAULT_IGNORE_PATTERNS: string[];
25
9
  export declare const DEFAULT_OUTDIR = "dist/esm/";
26
10
  /**
27
- * Reports whether the output that a build of `packageDir` would produce is currently on disk. Applies the build's
28
- * own rule, on the same options that the build was given, so a caller probing for output and the build deciding
29
- * whether to rebuild cannot disagree: A package whose entry points emit nothing does not expect any output, and
30
- * reports present. Passing options that the build was not given would make the two disagree.
11
+ * Reports whether the output that a build of `packageDir` would produce is currently on disk. Selects entry points
12
+ * through `resolveEntryPoints` and tests them through `hasExpectedBuildOutput`, as the build does, so a caller
13
+ * probing for output and the build deciding whether to rebuild cannot disagree: A package whose entry points emit
14
+ * nothing does not expect any output, and reports present. Passing options that the build was not given would make
15
+ * the two disagree.
31
16
  */
32
17
  export declare function hasBuildOutput(packageDir: string, options?: BuildOptions): Promise<boolean>;
33
18
  /**
@@ -65,18 +50,8 @@ export declare function resolveBuildCachePath(packageDir: string): string;
65
50
  * `readBuildDigest` returns.
66
51
  */
67
52
  export declare function resolveEmitDurationPath(packageDir: string): string;
68
- /**
69
- * Resolves the two scratch directories through which a build publishes: `staging`, which the emit is written to,
70
- * and `previous`, which the outgoing output is renamed aside to. Both are siblings of the emit directory, so a
71
- * rename between them never crosses a filesystem, and both are dot-prefixed so that a leftover stays out of the
72
- * globs that select sources.
73
- *
74
- * The names are fixed rather than unique because the build removes both before use. A unique name would need a
75
- * sweep to accomplish that removal, and a sweep cannot tell a directory orphaned by a killed run from one that a
76
- * concurrent build is still writing. Because a directory left behind by a failed build is cleared the same way,
77
- * the failure path does not need any cleanup of its own.
78
- */
79
- export declare function resolveScratchDirs(emitDir: string): ScratchDirs;
53
+ /** Selects the entry points that a build of `packageDir` compiles, as paths relative to `packageDir`. */
54
+ export declare function resolveEntryPoints(packageDir: string, options?: BuildOptions): Promise<string[]>;
80
55
  /**
81
56
  * Resolves the fingerprint of the nmr compiling `packageDir`, the `BuildToolchain` member that identifies nmr. It
82
57
  * is nmr's own build digest when one is on disk, which changes on a dev-loop edit that the version does not
@@ -3,17 +3,14 @@ import path from 'node:path';
3
3
  import { findPackageRoot, readCacheEntry, readGuardedCacheEntry, resolveCacheEntryPath, writeGuardedCacheEntry, } from '@williamthorsen/nmr-core';
4
4
  import { glob } from 'glob';
5
5
  import { readPackageJson } from "../helpers/package-json.js";
6
- export const DEFAULT_ENTRY_GLOBS = ['src/**/*.{ts,tsx}'];
7
- export const DEFAULT_IGNORE_PATTERNS = ['**/__fixtures__/**', '**/__mocks__/**', '**/__tests__/**', '**/test-utils/**'];
6
+ const DEFAULT_ENTRY_GLOBS = ['src/**/*.{ts,tsx}'];
7
+ const DEFAULT_IGNORE_PATTERNS = ['**/__fixtures__/**', '**/__mocks__/**', '**/__tests__/**', '**/test-utils/**'];
8
8
  export const DEFAULT_OUTDIR = 'dist/esm/';
9
9
  const BUILD_CACHE_TOOL = 'nmr-compile';
10
10
  const SELF_DIR = findPackageRoot(import.meta.url);
11
11
  export async function hasBuildOutput(packageDir, options = {}) {
12
12
  const outdir = options.outdir ?? DEFAULT_OUTDIR;
13
- const entryPoints = await glob(options.entryGlobs ?? DEFAULT_ENTRY_GLOBS, {
14
- cwd: packageDir,
15
- ignore: [...(options.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS), ...(options.extraIgnorePatterns ?? [])],
16
- });
13
+ const entryPoints = await resolveEntryPoints(packageDir, options);
17
14
  return hasExpectedBuildOutput(packageDir, outdir, entryPoints);
18
15
  }
19
16
  export function hasExpectedBuildOutput(packageDir, outdir, entryPoints) {
@@ -49,13 +46,11 @@ export function resolveEmitDurationPath(packageDir) {
49
46
  extension: '.duration.json',
50
47
  });
51
48
  }
52
- export function resolveScratchDirs(emitDir) {
53
- const parent = path.dirname(emitDir);
54
- const name = path.basename(emitDir);
55
- return {
56
- previousDir: path.join(parent, `.${name}.previous`),
57
- stagingDir: path.join(parent, `.${name}.staging`),
58
- };
49
+ export async function resolveEntryPoints(packageDir, options = {}) {
50
+ return glob(options.entryGlobs ?? DEFAULT_ENTRY_GLOBS, {
51
+ cwd: packageDir,
52
+ ignore: [...(options.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS), ...(options.extraIgnorePatterns ?? [])],
53
+ });
59
54
  }
60
55
  export async function resolveToolchainFingerprint(packageDir, selfDir = SELF_DIR) {
61
56
  if (!isSameDir(packageDir, selfDir)) {
@@ -0,0 +1,6 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
2
+ import type { BuildOutcome, BuildProgress } from './build.js';
3
+ /** Renders the line that closes a build: the skip, naming the saving when it is worth naming, or the emit. */
4
+ export declare function renderBuildOutcome(outcome: BuildOutcome, packageName: string, style: OutputStyle): string;
5
+ /** Renders the line that announces an emit, naming why the build is about to run. */
6
+ export declare function renderBuildProgress(progress: BuildProgress, packageName: string, style: OutputStyle): string;
@@ -0,0 +1,21 @@
1
+ import { formatGlyphLine, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
2
+ import { NMR_GLYPHS } from "../glyphs.js";
3
+ import { formatSaving } from "../helpers/duration.js";
4
+ export function renderBuildOutcome(outcome, packageName, style) {
5
+ if (outcome.kind === 'unchanged') {
6
+ const saving = formatSaving(outcome.savedMs ?? 0);
7
+ const savingClause = saving === undefined ? '' : `, ${saving}`;
8
+ return `${STATUS_GLYPHS[style].skipped.text} ${packageName}: No changes detected. Skipping build${savingClause}.`;
9
+ }
10
+ return formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: ${describeEmit(outcome.fileCount, outcome.outdir)}`);
11
+ }
12
+ export function renderBuildProgress(progress, packageName, style) {
13
+ const message = progress.kind === 'output-missing' ? 'Build output is missing. Rebuilding.' : 'Changes detected.';
14
+ return formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: ${message}`);
15
+ }
16
+ function describeEmit(fileCount, outdir) {
17
+ if (fileCount === 0) {
18
+ return 'Did not emit any output.';
19
+ }
20
+ return `Compiled ${fileCount} files to ${outdir.replace(/\/+$/u, '')}.`;
21
+ }
@@ -1,6 +1,23 @@
1
- import { type OutputStyle } from '@williamthorsen/nmr-core';
2
1
  import type * as TypeScript from 'typescript';
3
2
  import { type BuildOptions } from './build-output.js';
3
+ /**
4
+ * How a build ended: skipped because its inputs and output were unchanged, with the emit time recorded for those
5
+ * inputs when one was; or emitted, with the count of files that it published to `outdir`.
6
+ */
7
+ export type BuildOutcome = {
8
+ kind: 'unchanged';
9
+ savedMs?: number;
10
+ } | {
11
+ kind: 'emitted';
12
+ fileCount: number;
13
+ outdir: string;
14
+ };
15
+ /** Why a build is about to emit: its inputs changed, or its inputs match the cache and its output is missing. */
16
+ export type BuildProgress = {
17
+ kind: 'inputs-changed';
18
+ } | {
19
+ kind: 'output-missing';
20
+ };
4
21
  /**
5
22
  * The toolchain from which a build's emit comes: the resolved compiler and the nmr running it. `computeBuildHash`
6
23
  * folds both into the build hash, because the same sources can compile to different output under a different
@@ -10,9 +27,10 @@ export interface BuildToolchain {
10
27
  compilerVersion: string;
11
28
  fingerprint: string;
12
29
  }
13
- /** What a build needs beyond its sources: the options shaping the emit, and the style in which it reports. */
30
+ /** What a build needs beyond its sources: the options shaping the emit, and a listener for its progress. */
14
31
  export interface BuildPackageOptions extends BuildOptions {
15
- style: OutputStyle;
32
+ /** Called before the emit begins, so that a caller can report the build ahead of a slow compile. */
33
+ onProgress?: (progress: BuildProgress) => void;
16
34
  /**
17
35
  * Overridable so that a missing `typescript` peer is reachable without uninstalling it.
18
36
  *
@@ -30,10 +48,15 @@ export interface BuildPackageOptions extends BuildOptions {
30
48
  * current inputs rather than an accumulation of every build that ever ran. Assets belong outside it, or in a
31
49
  * `build:post` hook, which runs after the output is published.
32
50
  */
33
- export declare function buildPackage(packageDir: string, options: BuildPackageOptions): Promise<void>;
51
+ export declare function buildPackage(packageDir: string, options?: BuildPackageOptions): Promise<BuildOutcome>;
34
52
  /**
35
53
  * Produces a digest of the given files (paths and contents), the emit config, and the toolchain.
36
54
  * The file list is sorted so that the digest is order-invariant, and each path is folded in so that renames are
37
55
  * detected.
38
56
  */
39
57
  export declare function computeBuildHash(packageDir: string, files: string[], emitConfig: object, toolchain: BuildToolchain): Promise<string>;
58
+ /**
59
+ * Records a build skipped on unchanged inputs: states the `build-unchanged` outcome with the emit time recorded under
60
+ * `digest`, and returns that time, which is absent when none was recorded.
61
+ */
62
+ export declare function recordUnchangedBuild(packageDir: string, digest: string, env: NodeJS.ProcessEnv): Promise<number | undefined>;