@williamthorsen/nmr 0.47.0 → 0.49.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.
Files changed (117) hide show
  1. package/.meta/changelog.json +206 -0
  2. package/.readyup/kits/default.js +1534 -1466
  3. package/.readyup/manifest.json +142 -12
  4. package/CHANGELOG.md +55 -0
  5. package/README.md +44 -6
  6. package/agents/guidance/rulebooks/nmr.md +2 -1
  7. package/dist/esm/bin-path.js +4 -3
  8. package/dist/esm/check-cache-build-output.d.ts +24 -0
  9. package/dist/esm/check-cache-build-output.js +49 -0
  10. package/dist/esm/check-cache-delegation.d.ts +27 -0
  11. package/dist/esm/check-cache-delegation.js +92 -0
  12. package/dist/esm/check-cache-entries.d.ts +109 -0
  13. package/dist/esm/check-cache-entries.js +87 -0
  14. package/dist/esm/check-cache-gate.d.ts +64 -0
  15. package/dist/esm/check-cache-gate.js +226 -0
  16. package/dist/esm/check-cache-key.d.ts +47 -0
  17. package/dist/esm/check-cache-key.js +77 -0
  18. package/dist/esm/check-cache-record.d.ts +69 -0
  19. package/dist/esm/check-cache-record.js +167 -0
  20. package/dist/esm/check-cache.d.ts +8 -176
  21. package/dist/esm/check-cache.js +9 -214
  22. package/dist/esm/child-env.d.ts +57 -0
  23. package/dist/esm/child-env.js +59 -0
  24. package/dist/esm/child-process.d.ts +55 -0
  25. package/dist/esm/child-process.js +112 -0
  26. package/dist/esm/commands/build.d.ts +0 -16
  27. package/dist/esm/commands/build.js +9 -214
  28. package/dist/esm/commands/clean.js +3 -2
  29. package/dist/esm/commands/compiler.d.ts +18 -0
  30. package/dist/esm/commands/compiler.js +49 -0
  31. package/dist/esm/commands/fmt.d.ts +0 -39
  32. package/dist/esm/commands/fmt.js +4 -81
  33. package/dist/esm/commands/format-targets.d.ts +39 -0
  34. package/dist/esm/commands/format-targets.js +83 -0
  35. package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
  36. package/dist/esm/commands/rewrite-specifiers.js +122 -0
  37. package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
  38. package/dist/esm/commands/tsconfig-chain.js +63 -0
  39. package/dist/esm/config-check-cache.d.ts +17 -0
  40. package/dist/esm/config-check-cache.js +63 -0
  41. package/dist/esm/config-validation.d.ts +20 -0
  42. package/dist/esm/config-validation.js +236 -0
  43. package/dist/esm/config.js +4 -286
  44. package/dist/esm/crossing-diagnostics.d.ts +39 -0
  45. package/dist/esm/crossing-diagnostics.js +132 -0
  46. package/dist/esm/default-scripts.js +1 -1
  47. package/dist/esm/delegate-host.d.ts +43 -0
  48. package/dist/esm/delegate-host.js +178 -0
  49. package/dist/esm/delegation-target.d.ts +52 -0
  50. package/dist/esm/delegation-target.js +71 -0
  51. package/dist/esm/fan-out.d.ts +57 -0
  52. package/dist/esm/fan-out.js +146 -0
  53. package/dist/esm/glyphs.d.ts +1 -1
  54. package/dist/esm/help.js +10 -0
  55. package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
  56. package/dist/esm/helpers/deriveExcerpt.js +1 -1
  57. package/dist/esm/helpers/findClosestName.js +4 -3
  58. package/dist/esm/helpers/quoteForShell.d.ts +5 -0
  59. package/dist/esm/helpers/quoteForShell.js +3 -0
  60. package/dist/esm/helpers/renderQuotedList.d.ts +2 -0
  61. package/dist/esm/helpers/renderQuotedList.js +9 -0
  62. package/dist/esm/hooks.d.ts +17 -0
  63. package/dist/esm/hooks.js +23 -0
  64. package/dist/esm/invocation-args.d.ts +40 -0
  65. package/dist/esm/invocation-args.js +178 -0
  66. package/dist/esm/invocation.d.ts +67 -0
  67. package/dist/esm/invocation.js +3 -0
  68. package/dist/esm/machine-lock.d.ts +2 -0
  69. package/dist/esm/machine-lock.js +1 -1
  70. package/dist/esm/output-channel.d.ts +23 -0
  71. package/dist/esm/output-channel.js +14 -0
  72. package/dist/esm/pass-manifest.d.ts +53 -0
  73. package/dist/esm/pass-manifest.js +89 -0
  74. package/dist/esm/presentation.d.ts +47 -0
  75. package/dist/esm/presentation.js +32 -0
  76. package/dist/esm/recording.d.ts +1 -1
  77. package/dist/esm/recording.js +2 -2
  78. package/dist/esm/remote-host-session.d.ts +62 -0
  79. package/dist/esm/remote-host-session.js +277 -0
  80. package/dist/esm/remote-probe-host.d.ts +22 -0
  81. package/dist/esm/remote-probe-host.js +50 -0
  82. package/dist/esm/remote-probe.d.ts +31 -0
  83. package/dist/esm/remote-probe.js +78 -0
  84. package/dist/esm/remote-run.d.ts +91 -0
  85. package/dist/esm/remote-run.js +189 -0
  86. package/dist/esm/remote-session.d.ts +40 -0
  87. package/dist/esm/remote-session.js +151 -0
  88. package/dist/esm/remote-snapshot.d.ts +21 -0
  89. package/dist/esm/remote-snapshot.js +80 -0
  90. package/dist/esm/replay-assembly.d.ts +2 -2
  91. package/dist/esm/replay-assembly.js +1 -1
  92. package/dist/esm/run-chain.d.ts +21 -0
  93. package/dist/esm/run-chain.js +108 -0
  94. package/dist/esm/runCli.d.ts +1 -9
  95. package/dist/esm/runCli.js +41 -895
  96. package/dist/esm/runner.d.ts +1 -54
  97. package/dist/esm/runner.js +2 -125
  98. package/dist/esm/shell-syntax.d.ts +30 -0
  99. package/dist/esm/shell-syntax.js +120 -0
  100. package/dist/esm/steps.d.ts +0 -2
  101. package/dist/esm/steps.js +3 -119
  102. package/dist/esm/types.d.ts +5 -0
  103. package/dist/esm/user-config.d.ts +34 -0
  104. package/dist/esm/user-config.js +93 -0
  105. package/dist/esm/verbosity.d.ts +6 -0
  106. package/dist/esm/verbosity.js +7 -7
  107. package/dist/esm/verdict.d.ts +3 -1
  108. package/dist/esm/verdict.js +4 -3
  109. package/dist/esm/vitest-source-resolution.js +2 -3
  110. package/docs/check-cache.md +30 -14
  111. package/docs/delegate.md +141 -0
  112. package/docs/machine-lock.md +2 -1
  113. package/docs/reporting.md +6 -1
  114. package/docs/scripts.md +2 -1
  115. package/package.json +5 -5
  116. /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
  117. /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
@@ -0,0 +1,80 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
6
+ export function createSnapshot(cwd, env) {
7
+ const toplevelResult = runGit(['rev-parse', '--show-toplevel'], cwd, env);
8
+ if (!toplevelResult.ok) {
9
+ return { ok: false, isUnsupported: true, reason: `not a git repository: ${toplevelResult.error}` };
10
+ }
11
+ const toplevel = toplevelResult.stdout.trim();
12
+ if (fs.existsSync(path.join(toplevel, '.gitmodules'))) {
13
+ return { ok: false, isUnsupported: true, reason: 'the repository declares submodules' };
14
+ }
15
+ const indexPathResult = runGit(['rev-parse', '--git-path', 'index'], toplevel, env);
16
+ if (!indexPathResult.ok) {
17
+ return { ok: false, isUnsupported: false, reason: indexPathResult.error };
18
+ }
19
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nmr-snapshot-'));
20
+ try {
21
+ const tempIndex = path.join(tempDir, 'index');
22
+ const indexPath = path.resolve(toplevel, indexPathResult.stdout.trim());
23
+ if (fs.existsSync(indexPath)) {
24
+ copyIndex(indexPath, tempIndex);
25
+ }
26
+ const indexEnv = { ...env, GIT_INDEX_FILE: tempIndex };
27
+ const addResult = runGit(['add', '--all'], toplevel, indexEnv);
28
+ if (!addResult.ok) {
29
+ return { ok: false, isUnsupported: false, reason: addResult.error };
30
+ }
31
+ const stageResult = runGit(['ls-files', '--stage', '-z'], toplevel, indexEnv);
32
+ if (!stageResult.ok) {
33
+ return { ok: false, isUnsupported: false, reason: stageResult.error };
34
+ }
35
+ if (stageResult.stdout.split('\0').some((entry) => entry.startsWith('160000 '))) {
36
+ return { ok: false, isUnsupported: true, reason: 'the working tree contains a submodule or a nested repository' };
37
+ }
38
+ const treeResult = runGit(['write-tree'], toplevel, indexEnv);
39
+ if (!treeResult.ok) {
40
+ return { ok: false, isUnsupported: false, reason: treeResult.error };
41
+ }
42
+ const headResult = runGit(['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], toplevel, env);
43
+ const parentArgs = headResult.ok ? ['-p', headResult.stdout.trim()] : [];
44
+ const treeId = treeResult.stdout.trim();
45
+ const commitResult = runGit(['commit-tree', treeId, ...parentArgs, '-m', 'nmr delegate snapshot'], toplevel, {
46
+ ...env,
47
+ ...SNAPSHOT_IDENTITY_ENV,
48
+ });
49
+ if (!commitResult.ok) {
50
+ return { ok: false, isUnsupported: false, reason: commitResult.error };
51
+ }
52
+ return { ok: true, sha: commitResult.stdout.trim(), toplevel, treeId };
53
+ }
54
+ finally {
55
+ fs.rmSync(tempDir, { force: true, recursive: true });
56
+ }
57
+ }
58
+ const SNAPSHOT_IDENTITY_ENV = {
59
+ GIT_AUTHOR_DATE: '@0 +0000',
60
+ GIT_AUTHOR_EMAIL: 'nmr@localhost',
61
+ GIT_AUTHOR_NAME: 'nmr delegate',
62
+ GIT_COMMITTER_DATE: '@0 +0000',
63
+ GIT_COMMITTER_EMAIL: 'nmr@localhost',
64
+ GIT_COMMITTER_NAME: 'nmr delegate',
65
+ };
66
+ function copyIndex(source, destination) {
67
+ fs.copyFileSync(source, destination);
68
+ const { atime, mtime } = fs.statSync(source);
69
+ fs.utimesSync(destination, atime, mtime);
70
+ }
71
+ function runGit(args, cwd, env) {
72
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8', env, maxBuffer: GIT_OUTPUT_LIMIT });
73
+ if (result.error !== undefined) {
74
+ return { ok: false, error: result.error.message };
75
+ }
76
+ if (result.status !== 0) {
77
+ return { ok: false, error: result.stderr.trim() || `git ${args[0] ?? ''} exited ${result.status ?? 'abnormally'}` };
78
+ }
79
+ return { ok: true, stdout: result.stdout };
80
+ }
@@ -1,4 +1,4 @@
1
- import { type ReplayLine } from './check-cache.js';
1
+ import { type ReplayLine } from './check-cache-entries.js';
2
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
@@ -14,7 +14,7 @@ import { type Step } from './steps.js';
14
14
  *
15
15
  * An entry is admissible when the run that certified it is this one and the tree that it describes is this one.
16
16
  * The witness is what a run stamps on an excerpt that it records and on one that it recalls and replays; the
17
- * tree hash bounds an identity carried by a process out of the run that issued it.
17
+ * tree id bounds an identity carried by a process out of the run that issued it.
18
18
  */
19
19
  export declare function assembleReplay(options: {
20
20
  anchorDir: string;
@@ -1,4 +1,4 @@
1
- import { readCheckCacheEntry } from "./check-cache.js";
1
+ import { readCheckCacheEntry } from "./check-cache-entries.js";
2
2
  import { readNmrStep } from "./steps.js";
3
3
  import { getWorkspacePackageDirs } from "./workspace.js";
4
4
  export async function assembleReplay(options) {
@@ -0,0 +1,21 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
2
+ import type { TreeSnapshot } from './check-cache.js';
3
+ import type { Invocation, PhaseResult, RunPlan } from './invocation.js';
4
+ import type { ScriptRegistry } from './resolve-scripts.js';
5
+ import { type ResolvedScript } from './resolver.js';
6
+ /**
7
+ * Assembles the chain that a resolved script runs at this scope, the hooks wrapped around it included, and the key
8
+ * under which a pass by that chain is recorded. Settles a no-op override and an argument that no step can take.
9
+ */
10
+ export declare function assembleChain(options: {
11
+ invocation: Invocation;
12
+ registry: ScriptRegistry;
13
+ resolvedScript: ResolvedScript;
14
+ snapshot: TreeSnapshot | undefined;
15
+ }): PhaseResult<Pick<RunPlan, 'commandString' | 'key' | 'ownSteps' | 'steps'>>;
16
+ /**
17
+ * Returns the line announcing that a `package.json` script is standing in for a built-in, or `undefined` under
18
+ * `quiet` or when none is due. Only a script replacing a name that the registry already defines is worth
19
+ * announcing; an ordinary tier-3 entry that happens to resolve is not standing in for anything.
20
+ */
21
+ export declare function formatOverrideNotice(resolvedScript: ResolvedScript, registry: ScriptRegistry, command: string, anchorDir: string, quiet: boolean, style: OutputStyle): string | undefined;
@@ -0,0 +1,108 @@
1
+ import path from 'node:path';
2
+ import { formatGlyphLine, reportError } from '@williamthorsen/nmr-core';
3
+ import { resolveCacheKey } from "./check-cache-gate.js";
4
+ import { reportNmrCrossing } from "./crossing-diagnostics.js";
5
+ import { NMR_GLYPHS } from "./glyphs.js";
6
+ import { isHookName } from "./helpers/hook-name.js";
7
+ import { quoteForShell } from "./helpers/quoteForShell.js";
8
+ import { wrapWithHooks } from "./hooks.js";
9
+ import { reportVerdict } from "./presentation.js";
10
+ import { applyDevBinToSteps } from "./resolver.js";
11
+ import { dropRecursiveSteps, renderChain } from "./steps.js";
12
+ export function assembleChain(options) {
13
+ const { invocation, registry, resolvedScript, snapshot } = options;
14
+ const { anchorDir, command, context, env, parsedArgs, stderr, stdout, styles } = invocation;
15
+ const { isEmptiedByWorkspace, steps: runnableSteps } = readRunnableSteps(resolvedScript, context.workspacePackageDirs);
16
+ const resolvedCommand = renderChain(runnableSteps);
17
+ const noOpReason = findNoOpReason(resolvedCommand, isEmptiedByWorkspace);
18
+ if (noOpReason !== undefined && !parsedArgs.shouldPrintLog) {
19
+ reportVerdict({ command, scope: invocation.scope, outcome: 'no-op', reason: noOpReason }, stdout, invocation.format, styles.stdout);
20
+ return { settled: true, result: { exitCode: 0 } };
21
+ }
22
+ const substitutedSteps = applyDevBinToSteps(runnableSteps, context.config.devBin, context.monorepoRoot);
23
+ const substitutedCommand = renderChain(substitutedSteps);
24
+ const bindResult = bindPassthrough(substitutedSteps, parsedArgs.passthrough, command);
25
+ if (!bindResult.ok) {
26
+ reportError(bindResult.error, stderr);
27
+ return { settled: true, result: { exitCode: 1 } };
28
+ }
29
+ const mainSteps = bindResult.steps;
30
+ const isHookInvocation = isHookName(command);
31
+ const fullSteps = isHookInvocation
32
+ ? mainSteps
33
+ : wrapWithHooks(command, mainSteps, registry, anchorDir, parsedArgs.isWorkspaceRoot);
34
+ const fullCommand = renderChain(fullSteps);
35
+ reportNmrCrossing({
36
+ config: context.config,
37
+ isReading: parsedArgs.shouldPrintLog,
38
+ isWorkspaceRoot: parsedArgs.isWorkspaceRoot,
39
+ monorepoRoot: context.monorepoRoot,
40
+ registry,
41
+ resolvedScript,
42
+ shouldUseRoot: invocation.shouldUseRoot,
43
+ stderr,
44
+ style: styles.stderr,
45
+ });
46
+ const key = resolveCacheKey({
47
+ anchorDir,
48
+ command,
49
+ commandString: fullCommand,
50
+ env,
51
+ monorepoRoot: context.monorepoRoot,
52
+ snapshot,
53
+ stderr,
54
+ substitution: substitutedCommand === resolvedCommand ? undefined : substitutedCommand,
55
+ });
56
+ return { settled: false, value: { commandString: fullCommand, key, ownSteps: mainSteps, steps: fullSteps } };
57
+ }
58
+ export function formatOverrideNotice(resolvedScript, registry, command, anchorDir, quiet, style) {
59
+ const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
60
+ if (quiet || resolvedScript.origin.tier !== 'package' || registryEntry === undefined) {
61
+ return undefined;
62
+ }
63
+ const notice = `${path.basename(anchorDir)}: Using override script: ${renderChain(resolvedScript.steps)}`;
64
+ return `${formatGlyphLine(NMR_GLYPHS, style, 'package', notice)}\n`;
65
+ }
66
+ function acceptsArguments(step) {
67
+ return step.kind === 'opaque' || step.shouldDeclineArguments !== true;
68
+ }
69
+ function bindPassthrough(steps, passthrough, command) {
70
+ if (passthrough.length === 0) {
71
+ return { ok: true, steps };
72
+ }
73
+ if (!steps.some(acceptsArguments)) {
74
+ return { ok: false, error: formatUnroutableArgumentsError(command) };
75
+ }
76
+ const boundSteps = steps.map((step) => {
77
+ if (!acceptsArguments(step)) {
78
+ return step;
79
+ }
80
+ return step.kind === 'structural'
81
+ ? { ...step, argv: [...step.argv, ...passthrough] }
82
+ : { kind: 'opaque', command: `${step.command} ${passthrough.map(quoteForShell).join(' ')}` };
83
+ });
84
+ return { ok: true, steps: boundSteps };
85
+ }
86
+ function findNoOpReason(resolvedCommand, isEmptiedByWorkspace) {
87
+ if (isEmptiedByWorkspace) {
88
+ return 'empty-workspace';
89
+ }
90
+ if (resolvedCommand === '') {
91
+ return 'empty-override';
92
+ }
93
+ if (resolvedCommand === ':') {
94
+ return 'noop-override';
95
+ }
96
+ return undefined;
97
+ }
98
+ function formatUnroutableArgumentsError(command) {
99
+ return (`\`${command}\` does not take trailing arguments: every step of its chain declines them. ` +
100
+ 'Running it unnarrowed is not what the arguments asked for, so nothing ran.');
101
+ }
102
+ function readRunnableSteps(resolvedScript, workspacePackageDirs) {
103
+ if (workspacePackageDirs.length > 0) {
104
+ return { isEmptiedByWorkspace: false, steps: resolvedScript.steps };
105
+ }
106
+ const steps = dropRecursiveSteps(resolvedScript.steps);
107
+ return { isEmptiedByWorkspace: steps.length === 0 && resolvedScript.steps.length > 0, steps };
108
+ }
@@ -1,9 +1,5 @@
1
1
  import type { Writable } from 'node:stream';
2
- /**
3
- * Marks a run made on a delegating caller's behalf, in which a command that the registry does not define, and a
4
- * `--log` with nothing to show, exit 0 rather than failing.
5
- */
6
- export declare const RUN_IF_PRESENT_ENV_VAR = "NMR_RUN_IF_PRESENT";
2
+ import { type RunCliResult } from './invocation.js';
7
3
  /** @internal */
8
4
  export interface RunCliOptions {
9
5
  /** Post-slice CLI arguments (equivalent to `process.argv.slice(2)`). */
@@ -17,10 +13,6 @@ export interface RunCliOptions {
17
13
  /** Stream for errors, warnings, and debug notes. */
18
14
  stderr: Writable;
19
15
  }
20
- /** @internal */
21
- export interface RunCliResult {
22
- exitCode: number;
23
- }
24
16
  /**
25
17
  * Executes the nmr CLI flow in-process and returns the resulting exit code.
26
18
  * Does not hold global state, does not read `process.*` globals, and never calls `process.exit`.