Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 64 additions & 26 deletions scripts/dev-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ WARN_COUNT=0
FAIL_COUNT=0
OUTPUT_FORMAT="human"
JSON_RESULTS=""
NODE_HEAP_REMEDIATION_CLI="Run: NODE_OPTIONS=--max-old-space-size=5120 npm run typecheck:cli"
NODE_HEAP_REMEDIATION_PLUGIN="Run: NODE_OPTIONS=--max-old-space-size=5120 npm --prefix nemoclaw run build"

usage() {
cat <<'EOF'
Expand Down Expand Up @@ -227,33 +229,40 @@ check_executable() {
fi
}

check_quiet_command() {
is_node_heap_oom_output() {
local output_file="$1"

grep -Eq \
"JavaScript heap out of memory|Ineffective mark-compacts near heap limit|Allocation failed - JavaScript heap out of memory" \
"${output_file}" 2>/dev/null
}

print_node_heap_oom_guidance() {
local remediation="$1"

printf 'Node.js exhausted its V8 heap while running this type check.\n' >&2
printf 'Next: %s\n' "${remediation}" >&2
}

check_quiet_command_with_heap_hint() {
local label="$1"
local remediation="$2"
local classification command_status matcher_status pipeline_result
local -a pipeline_status
shift 2
local heap_remediation="$3"
local output_file
shift 3

# Drain the complete stream and retain one classification word. This bounds
# shell memory and keeps command output out of the doctor report.
pipeline_result="$(
"$@" 2>&1 | awk '
index($0, "JavaScript heap out of memory") { found = 1 }
END { printf "%s", found ? "heap" : "other" }
'
pipeline_status=("${PIPESTATUS[@]}")
printf '|%s|%s' "${pipeline_status[0]}" "${pipeline_status[1]}"
)"
IFS='|' read -r classification command_status matcher_status <<<"${pipeline_result}"

if [ "${command_status}" = "0" ] && [ "${matcher_status}" = "0" ]; then
output_file="$(mktemp "${TMPDIR:-/tmp}/nemoclaw-dev-doctor.XXXXXX")" || {
fail "${label}: failed" "${remediation}"
return
}
if "$@" >"${output_file}" 2>&1; then
rm -f "${output_file}"
pass "${label}"
elif [ "${classification}" = "heap" ] && [ "${matcher_status}" = "0" ]; then
# Node.js derives its default old-space limit from host memory, so this
# reports the host size rather than a fault in the checked sources.
fail "${label}: ran out of Node.js heap" \
"Raise the limit, then rerun: export NODE_OPTIONS=--max-old-space-size=5120"
elif is_node_heap_oom_output "${output_file}"; then
rm -f "${output_file}"
fail "${label}: ran out of Node.js heap" "${heap_remediation}"
else
rm -f "${output_file}"
fail "${label}: failed" "${remediation}"
fi
}
Expand Down Expand Up @@ -332,6 +341,31 @@ run_setup_step() {
return 1
}

run_setup_step_with_heap_hint() {
local label="$1"
local heap_remediation="$2"
local output_file
shift 2

printf '\n==> %s\n' "${label}"
output_file="$(mktemp "${TMPDIR:-/tmp}/nemoclaw-dev-setup.XXXXXX")" || {
printf 'Setup stopped while attempting: %s\n' "${label}" >&2
return 1
}
if "$@" >"${output_file}" 2>&1; then
rm -f "${output_file}"
return 0
fi
if is_node_heap_oom_output "${output_file}"; then
print_node_heap_oom_guidance "${heap_remediation}"
else
cat "${output_file}" >&2
fi
rm -f "${output_file}"
printf 'Setup stopped while attempting: %s\n' "${label}" >&2
return 1
}

repair_repository() {
local setup_failed=0 hooks_path

Expand Down Expand Up @@ -382,8 +416,10 @@ repair_repository() {
run_setup_step "Build the CLI" npm run build:cli || return 1
run_setup_step "Build and type-check the plugin" npm --prefix nemoclaw run build || return 1
# Keep the explicit checks aligned with the broader pre-push and CI contracts.
run_setup_step "Type-check the CLI" npm run typecheck:cli || return 1
run_setup_step "Type-check the plugin without emitting files" \
run_setup_step_with_heap_hint "Type-check the CLI" "${NODE_HEAP_REMEDIATION_CLI}" \
npm run typecheck:cli || return 1
run_setup_step_with_heap_hint "Type-check the plugin without emitting files" \
"${NODE_HEAP_REMEDIATION_PLUGIN}" \
"${REPO_ROOT}/nemoclaw/node_modules/.bin/tsc" --noEmit \
-p "${REPO_ROOT}/nemoclaw/tsconfig.json" || return 1
run_setup_step "Install repository Git hooks" "${REPO_ROOT}/node_modules/.bin/prek" install || return 1
Expand Down Expand Up @@ -612,11 +648,13 @@ run_doctor() {
"Run: cd nemoclaw && npm run build" "${REPO_ROOT}/nemoclaw/src" \
"${REPO_ROOT}/nemoclaw/tsconfig.json" "${REPO_ROOT}/nemoclaw/package.json"
if [ -x "${root_tsc}" ]; then
check_quiet_command "CLI type check" "Run: npm run typecheck:cli" \
check_quiet_command_with_heap_hint "CLI type check" "Run: npm run typecheck:cli" \
"${NODE_HEAP_REMEDIATION_CLI}" \
"${root_tsc}" -p "${REPO_ROOT}/tsconfig.cli.json"
fi
if [ -x "${plugin_tsc}" ]; then
check_quiet_command "Plugin type check" "Run: npm --prefix nemoclaw run build" \
check_quiet_command_with_heap_hint "Plugin type check" "Run: npm --prefix nemoclaw run build" \
"${NODE_HEAP_REMEDIATION_PLUGIN}" \
"${plugin_tsc}" --noEmit -p "${REPO_ROOT}/nemoclaw/tsconfig.json"
fi

Expand Down
92 changes: 92 additions & 0 deletions test/dev-setup-doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ ${body}
);
}

function writeNodeHeapOomTool(filePath: string): void {
writeExecutable(
filePath,
`#!/usr/bin/env bash
printf '%s\\n' '<--- Last few GCs --->' >&2
printf '%s\\n' 'FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory' >&2
printf '%s\\n' '----- Native stack trace -----' >&2
exit 134
`,
);
}

function writeManagedCliShim(
fakeBin: string,
repo: string,
Expand Down Expand Up @@ -132,6 +144,11 @@ elif [ "\${FAKE_NPM_ROOT_INSTALL_FAIL:-}" = "1" ] && [ "\${1:-}" = "install" ];
exit 1
elif [ "\${FAKE_NPM_PLUGIN_INSTALL_FAIL:-}" = "1" ] && [ "\${1:-}" = "--prefix" ] && [ "\${2:-}" = "nemoclaw" ] && [ "\${3:-}" = "install" ]; then
exit 1
elif [ "\${FAKE_NPM_CLI_TYPECHECK_OOM:-}" = "1" ] && [ "\${1:-}" = "run" ] && [ "\${2:-}" = "typecheck:cli" ]; then
printf '%s\\n' '<--- Last few GCs --->' >&2
printf '%s\\n' 'FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory' >&2
printf '%s\\n' '----- Native stack trace -----' >&2
exit 134
else
echo "10.9.0"
fi`,
Expand Down Expand Up @@ -491,6 +508,34 @@ describe("contributor environment doctor", () => {
expect(readCommandLog(fixture)).not.toMatch(/^npm .* exec(?: |$)/m);
});

it("reports an actionable heap-limit remedy when the CLI type check exhausts V8 memory", () => {
const fixture = createFixture();
writeNodeHeapOomTool(path.join(fixture.repo, "node_modules", ".bin", "tsc"));

const result = runDoctor(fixture);

expect(result.status).toBe(1);
expect(result.output).toContain("CLI type check: ran out of Node.js heap");
expect(result.output).toContain(
"Next: Run: NODE_OPTIONS=--max-old-space-size=5120 npm run typecheck:cli",
);
expect(result.output).not.toContain("Native stack trace");
});

it("reports an actionable heap-limit remedy when the plugin type check exhausts V8 memory", () => {
const fixture = createFixture();
writeNodeHeapOomTool(path.join(fixture.repo, "nemoclaw", "node_modules", ".bin", "tsc"));

const result = runDoctor(fixture);

expect(result.status).toBe(1);
expect(result.output).toContain("Plugin type check: ran out of Node.js heap");
expect(result.output).toContain(
"Next: Run: NODE_OPTIONS=--max-old-space-size=5120 npm --prefix nemoclaw run build",
);
expect(result.output).not.toContain("Native stack trace");
});

it("rejects a foreign PATH CLI even when the global package links to this checkout", () => {
const fixture = createFixture();
fs.rmSync(path.join(fixture.fakeBin, "nemoclaw"));
Expand Down Expand Up @@ -740,6 +785,53 @@ describe("contributor repository setup", () => {
expect(commands).not.toContain("npm --prefix nemoclaw install");
});

it("stops CLI type-check setup failures with a heap-limit remedy instead of the V8 stack", () => {
const fixture = createFixture();

const result = runSetup(fixture, [], { FAKE_NPM_CLI_TYPECHECK_OOM: "1" });

expect(result.status).toBe(1);
expect(result.output).toContain("Node.js exhausted its V8 heap while running this type check.");
expect(result.output).toContain(
"Next: Run: NODE_OPTIONS=--max-old-space-size=5120 npm run typecheck:cli",
);
expect(result.output).toContain("Setup stopped while attempting: Type-check the CLI");
expect(result.output).not.toContain("Native stack trace");
expect(result.output).not.toContain("Type-check the plugin without emitting files");
expect(readCommandLog(fixture)).toContain("npm run typecheck:cli");
});

it("stops plugin type-check setup failures with a heap-limit remedy instead of the V8 stack", () => {
const fixture = createFixture();
writeNodeHeapOomTool(path.join(fixture.repo, "nemoclaw", "node_modules", ".bin", "tsc"));

const result = runSetup(fixture);

expect(result.status).toBe(1);
expect(result.output).toContain("Node.js exhausted its V8 heap while running this type check.");
expect(result.output).toContain(
"Next: Run: NODE_OPTIONS=--max-old-space-size=5120 npm --prefix nemoclaw run build",
);
expect(result.output).toContain(
"Setup stopped while attempting: Type-check the plugin without emitting files",
);
expect(result.output).not.toContain("Native stack trace");
const commands = readCommandLog(fixture);
expect(commands).toContain("npm --prefix nemoclaw run build");
expect(commands).not.toContain("prek install");
});

it("names the setup step when the heap-check output capture cannot be created", () => {
const fixture = createFixture();
writeTool(fixture.fakeBin, "mktemp", "exit 1");

const result = runSetup(fixture);

expect(result.status).toBe(1);
expect(result.output).toContain("Setup stopped while attempting: Type-check the CLI");
expect(result.output).not.toContain("Install repository Git hooks");
});

it.each([
["default setup", []],
["repair", ["--repair"]],
Expand Down
Loading