@nathapp/nax 0.75.2 → 0.75.3
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/dist/nax.js +1487 -541
- package/flows/nax-finish/exec.ts +78 -29
- package/flows/nax-finish/steps/quality.ts +11 -2
- package/flows/nax-finish/steps/result.ts +9 -1
- package/package.json +3 -2
package/flows/nax-finish/exec.ts
CHANGED
|
@@ -14,7 +14,20 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Both cap wall-clock time: an unbounded gate would hang `acpx flow run`, and
|
|
16
16
|
* the post-run plugin awaits that subprocess.
|
|
17
|
+
*
|
|
18
|
+
* ## Why `node:child_process` and not `Bun.spawn`
|
|
19
|
+
*
|
|
20
|
+
* The rest of nax is Bun-native (see `.claude/rules/project-conventions.md`),
|
|
21
|
+
* but this module is **not** loaded by nax. `acpx flow run` loads it, in acpx's
|
|
22
|
+
* own process, and the published `acpx` binary is a Node program
|
|
23
|
+
* (`#!/usr/bin/env node`). Under Node the `Bun` global does not exist, so
|
|
24
|
+
* `Bun.spawn` threw `ReferenceError: Bun is not defined` on the flow's very
|
|
25
|
+
* first git call — aborting the flow before any node completed and before the
|
|
26
|
+
* result file was written. Everything under `flows/` must therefore stay on
|
|
27
|
+
* Node built-ins; `Bun.*` is banned here and only here, enforced by
|
|
28
|
+
* `scripts/check-flows-no-bun.ts`.
|
|
17
29
|
*/
|
|
30
|
+
import { spawn } from "node:child_process";
|
|
18
31
|
import type { RunResult } from "./types";
|
|
19
32
|
|
|
20
33
|
/** Fallbacks used when the plugin passes no explicit budget in the flow input. */
|
|
@@ -28,35 +41,71 @@ export interface ExecOptions {
|
|
|
28
41
|
timeoutMs?: number;
|
|
29
42
|
}
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
/** Exit code reported when the wall-clock cap kills the process, matching `timeout(1)`. */
|
|
45
|
+
const TIMEOUT_EXIT_CODE = 124;
|
|
46
|
+
/** Exit code reported when the binary is missing, matching a shell's "command not found". */
|
|
47
|
+
const NOT_FOUND_EXIT_CODE = 127;
|
|
48
|
+
|
|
49
|
+
function spawnCapture(cmd: string[], opts: ExecOptions): Promise<RunResult> {
|
|
50
|
+
return new Promise<RunResult>((resolve) => {
|
|
51
|
+
const [file, ...args] = cmd;
|
|
52
|
+
const proc = spawn(file as string, args, { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
53
|
+
let stdout = "";
|
|
54
|
+
let stderr = "";
|
|
55
|
+
let timedOut = false;
|
|
56
|
+
let settled = false;
|
|
57
|
+
|
|
58
|
+
proc.stdout.setEncoding("utf8");
|
|
59
|
+
proc.stderr.setEncoding("utf8");
|
|
60
|
+
proc.stdout.on("data", (chunk: string) => {
|
|
61
|
+
stdout += chunk;
|
|
62
|
+
});
|
|
63
|
+
proc.stderr.on("data", (chunk: string) => {
|
|
64
|
+
stderr += chunk;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// setTimeout (not a sleep) because the handle must be cancellable the moment
|
|
68
|
+
// the process exits — the documented exception in forbidden-patterns.md.
|
|
69
|
+
const timer =
|
|
70
|
+
opts.timeoutMs && opts.timeoutMs > 0
|
|
71
|
+
? setTimeout(() => {
|
|
72
|
+
timedOut = true;
|
|
73
|
+
proc.kill();
|
|
74
|
+
}, opts.timeoutMs)
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
77
|
+
const settle = (result: RunResult): void => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
if (timer) clearTimeout(timer);
|
|
81
|
+
resolve(result);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// A missing binary (`gh`/`glab` not installed) surfaces as an `error` event
|
|
85
|
+
// under Node, where `Bun.spawn` used to throw. Resolving with 127 instead of
|
|
86
|
+
// rejecting keeps it a readable gate failure the flow can route on, rather
|
|
87
|
+
// than an exception that kills `acpx flow run` with no result file.
|
|
88
|
+
proc.on("error", (err: Error) => {
|
|
89
|
+
settle({ exitCode: NOT_FOUND_EXIT_CODE, stdout, stderr: `${stderr}${err.message}`, timedOut });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// `close` (not `exit`) so both pipes are fully drained before we read them.
|
|
93
|
+
// `code` is null when the process died from a signal — including our own
|
|
94
|
+
// timeout kill — so it maps to a non-zero code rather than a false green.
|
|
95
|
+
proc.on("close", (code: number | null) => {
|
|
96
|
+
const exitCode = code ?? (timedOut ? TIMEOUT_EXIT_CODE : 1);
|
|
97
|
+
settle(
|
|
98
|
+
timedOut
|
|
99
|
+
? {
|
|
100
|
+
exitCode: exitCode === 0 ? TIMEOUT_EXIT_CODE : exitCode,
|
|
101
|
+
stdout,
|
|
102
|
+
stderr: `${stderr}\n[nax-finish] killed after ${opts.timeoutMs}ms timeout`,
|
|
103
|
+
timedOut: true,
|
|
104
|
+
}
|
|
105
|
+
: { exitCode, stdout, stderr },
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
60
109
|
}
|
|
61
110
|
|
|
62
111
|
/** Spawn an argv array directly — no shell. For flow-constructed commands. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { FinishError } from "../errors";
|
|
2
3
|
import { DEFAULT_GATE_TIMEOUT_MS, runShell } from "../exec";
|
|
3
4
|
import type { ShellRunFn } from "../types";
|
|
@@ -12,9 +13,17 @@ export interface QualityCommands {
|
|
|
12
13
|
|
|
13
14
|
export const _qualityDeps: { runShell: ShellRunFn; readText: (path: string) => Promise<string | null> } = {
|
|
14
15
|
runShell,
|
|
16
|
+
// node:fs, not Bun.file — this module runs inside acpx's Node process, where
|
|
17
|
+
// the `Bun` global does not exist (see the header of `../exec.ts`). A single
|
|
18
|
+
// read that treats ENOENT as "absent" also avoids the exists()-then-read race
|
|
19
|
+
// the Bun version had.
|
|
15
20
|
readText: async (path) => {
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
try {
|
|
22
|
+
return await readFile(path, "utf8");
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
18
27
|
},
|
|
19
28
|
};
|
|
20
29
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
1
3
|
import type { FinishResult } from "../types";
|
|
2
4
|
|
|
3
5
|
export function resultPath(repoRoot: string): string {
|
|
@@ -5,8 +7,14 @@ export function resultPath(repoRoot: string): string {
|
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export const _resultDeps: { writeText: (p: string, s: string) => Promise<void> } = {
|
|
10
|
+
// node:fs, not Bun.write — this module runs inside acpx's Node process, where
|
|
11
|
+
// the `Bun` global does not exist (see the header of `../exec.ts`). The mkdir
|
|
12
|
+
// is not redundant: Bun.write creates missing parent directories implicitly,
|
|
13
|
+
// writeFile does not, and this is the one artifact the plugin needs on disk to
|
|
14
|
+
// report an outcome at all.
|
|
8
15
|
writeText: async (p, s) => {
|
|
9
|
-
await
|
|
16
|
+
await mkdir(dirname(p), { recursive: true });
|
|
17
|
+
await writeFile(p, s, "utf8");
|
|
10
18
|
},
|
|
11
19
|
};
|
|
12
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax",
|
|
3
|
-
"version": "0.75.
|
|
3
|
+
"version": "0.75.3",
|
|
4
4
|
"description": "AI Coding Agent Orchestrator — loops until done",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
"dev": "bun run bin/nax.ts",
|
|
12
12
|
"build": "bun build bin/nax.ts --outdir dist --target bun --define \"GIT_COMMIT=\\\"$(git rev-parse --short HEAD)\\\"\"",
|
|
13
13
|
"typecheck": "bun x tsc --noEmit && bun x tsc --noEmit -p tsconfig.contracts.json",
|
|
14
|
-
"lint": "bun x biome check src/ bin/ flows/ && bun run check:no-real-global-nax && bun run check:alias-internals && bun run check:deep-relatives && bun run check:nax-error && bun run check:logger-storyid && bun run check:log-format-layering && bun run check:file-sizes",
|
|
14
|
+
"lint": "bun x biome check src/ bin/ flows/ && bun run check:flows-no-bun && bun run check:no-real-global-nax && bun run check:alias-internals && bun run check:deep-relatives && bun run check:nax-error && bun run check:logger-storyid && bun run check:log-format-layering && bun run check:file-sizes",
|
|
15
15
|
"lint:json": "bun x biome check src/ bin/ flows/ --reporter json && bun run check:nax-error 1>&2 && bun run check:logger-storyid 1>&2",
|
|
16
16
|
"lint:fix": "bun x biome check --write src/ bin/ flows/",
|
|
17
|
+
"check:flows-no-bun": "bun run scripts/check-flows-no-bun.ts",
|
|
17
18
|
"check:no-real-global-nax": "bun run scripts/check-no-real-global-nax.ts",
|
|
18
19
|
"check:alias-internals": "bun run scripts/check-alias-internals.ts",
|
|
19
20
|
"check:deep-relatives": "bun run scripts/check-deep-relatives.ts",
|