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
7 changes: 4 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ repos:
files: "^scripts/.*\\.sh$" # only format our scripts/
exclude: "^docker/" # avoid parsing docker/orchestrate.sh for now

- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
- repo: local
hooks:
- id: shellcheck
args: ["--severity", "warning"]
name: ShellCheck (system)
entry: shellcheck --severity warning
language: system
files: "^scripts/.*\\.sh$"
exclude: "^docker/"

Expand Down
6 changes: 3 additions & 3 deletions dashboard/src/engine/__tests__/nlxErrorSanitizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("sanitizeNlxError", () => {
expect(result.originalSuppressed).toBe(true);
expect(result.message).toContain("nlx is not installed");
expect(result.message).toContain("git worktree");
expect(result.fixCommand).toBe("bash scripts/dev-setup.sh");
expect(result.fixCommand).toBe("bash scripts/dev-setup.sh --repair-env");
expect(result.context.isWorktree).toBe(true);
});

Expand All @@ -65,7 +65,7 @@ describe("sanitizeNlxError", () => {

expect(result.originalSuppressed).toBe(true);
expect(result.message).toContain("Python error");
expect(result.message).toContain("bash scripts/dev-setup.sh");
expect(result.message).toContain("bash scripts/dev-setup.sh --repair-env");
expect(result.message).not.toContain("Traceback");
expect(result.message).not.toContain("File \"/usr/lib");
});
Expand All @@ -89,6 +89,6 @@ describe("sanitizeNlxError", () => {
const result = sanitizeNlxError("missing_nlx", "", NON_WORKTREE_SHELL);

expect(result.message).not.toContain("worktree");
expect(result.message).toContain("bash scripts/dev-setup.sh");
expect(result.message).toContain("bash scripts/dev-setup.sh --repair-env");
});
});
86 changes: 86 additions & 0 deletions dashboard/src/engine/__tests__/nlxService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { runAllowlistedNlxCommand } from "../nlxService";
import { runCommandArgv } from "../runner";

vi.mock("../runner", () => ({
runCommandArgv: vi.fn(),
}));

vi.mock("../nlxErrorSanitizer", () => ({
sanitizeNlxError: vi.fn((_errorType: string, stderr: string) => ({
message: stderr,
fixCommand: "bash scripts/dev-setup.sh --repair-env",
context: {
cwd: "/tmp",
gitTopLevel: null,
isWorktree: false,
interpreterPath: null,
nlxAvailable: false,
},
originalSuppressed: false,
})),
}));

describe("nlxService diagnose handling", () => {
const mockedRunner = vi.mocked(runCommandArgv);

beforeEach(() => {
mockedRunner.mockReset();
});

it("coerces diagnose nonzero health output into a structured response", async () => {
mockedRunner
.mockResolvedValueOnce({
argv: ["nlx", "diagnose"],
stdout: "",
stderr: "missing",
exitCode: 127,
timedOut: false,
aborted: false,
errorType: "missing_nlx",
})
.mockResolvedValueOnce({
argv: ["poetry", "run", "nlx", "diagnose"],
stdout:
'DNS_MODE=local-private RESOLVER=192.168.64.2 PIHOLE=running PIHOLE_UPSTREAM=host.docker.internal#5053 CLOUDFLARED=down PLAINTEXT_DNS=no NOTES="cloudflared-down"',
stderr: "",
exitCode: 1,
timedOut: false,
aborted: false,
errorType: "nonzero_exit",
});

const result = await runAllowlistedNlxCommand("diagnose");

expect(result.ok).toBe(true);
expect(result.errorType).toBe("none");
expect(result.diagnose?.badge).toBe("DEGRADED");
expect(mockedRunner).toHaveBeenCalledTimes(2);
});

it("keeps diagnose as failure when output cannot be parsed", async () => {
mockedRunner
.mockResolvedValueOnce({
argv: ["nlx", "diagnose"],
stdout: "not a diagnose line",
stderr: "",
exitCode: 1,
timedOut: false,
aborted: false,
errorType: "nonzero_exit",
})
.mockResolvedValueOnce({
argv: ["poetry", "run", "nlx", "diagnose"],
stdout: "still not valid",
stderr: "",
exitCode: 1,
timedOut: false,
aborted: false,
errorType: "nonzero_exit",
});

const result = await runAllowlistedNlxCommand("diagnose");

expect(result.ok).toBe(false);
expect(result.diagnose).toBeUndefined();
});
});
2 changes: 1 addition & 1 deletion dashboard/src/engine/nlxErrorSanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const TRACEBACK_PATTERNS: RegExp[] = [
/No module named/,
];

const CANONICAL_FIX = "bash scripts/dev-setup.sh";
const CANONICAL_FIX = "bash scripts/dev-setup.sh --repair-env";

export function containsTraceback(stderr: string): boolean {
return TRACEBACK_PATTERNS.some((pattern) => pattern.test(stderr));
Expand Down
24 changes: 18 additions & 6 deletions dashboard/src/engine/nlxService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,32 @@ export async function runAllowlistedNlxCommand(
response.taskNames = parseTaskNamesFromListTasks(response.stdout);
}

if (spec.commandId === "diagnose" && response.ok) {
if (spec.commandId === "diagnose") {
const shouldCoerceDiagnoseHealth =
!response.ok &&
response.errorType === "nonzero_exit" &&
response.stderr.trim().length === 0 &&
response.stdout.trim().length > 0;

try {
const firstLine = response.stdout.split(/\r?\n/).find((line) => line.trim().length > 0) ?? "";
const summary = parseDiagnoseLine(firstLine);
response.diagnose = {
summary,
badge: classifyDiagnose(summary),
};
if (shouldCoerceDiagnoseHealth) {
response.ok = true;
response.errorType = "none";
}
} catch {
response.ok = false;
response.errorType = "spawn_error";
response.stderr = response.stderr
? `${response.stderr}\nDiagnose output parsing failed.`
: "Diagnose output parsing failed.";
if (response.ok) {
response.ok = false;
response.errorType = "spawn_error";
response.stderr = response.stderr
? `${response.stderr}\nDiagnose output parsing failed.`
: "Diagnose output parsing failed.";
}
}
}

Expand Down
48 changes: 46 additions & 2 deletions scripts/dev-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
# Installs Python (Poetry) and dashboard (npm) dependencies.
#
# Usage:
# bash scripts/dev-setup.sh # full setup (network required for first run)
# bash scripts/dev-setup.sh --offline # skip network-dependent steps
# bash scripts/dev-setup.sh # full setup (network required for first run)
# bash scripts/dev-setup.sh --offline # skip network-dependent steps
# bash scripts/dev-setup.sh --repair-env # force clean venv rebuild
set -euo pipefail

OFFLINE=false
REPAIR_ENV=false
for arg in "$@"; do
case "$arg" in
--offline) OFFLINE=true ;;
--repair-env) REPAIR_ENV=true ;;
esac
done

Expand All @@ -34,6 +37,39 @@ echo "=== NextLevelApex dev-setup ==="
echo "Repo root: $REPO_ROOT"
echo "Worktree: $IS_WORKTREE"
echo "Offline: $OFFLINE"
echo "Repair env: $REPAIR_ENV"

# 0) Scrub AppleDouble metadata from generated directories.
echo ""
echo "[0/2] Scrubbing generated artifact metadata..."
bash "$REPO_ROOT/scripts/env-integrity.sh" scrub

# Decide whether the local virtualenv must be rebuilt.
VENV_REBUILD_REQUIRED=false
if [ "$REPAIR_ENV" = true ]; then
VENV_REBUILD_REQUIRED=true
fi

if [ -d "$REPO_ROOT/.venv" ]; then
if find "$REPO_ROOT/.venv" -type f -name '._*' -print -quit 2>/dev/null | grep -q .; then
echo "[dev-setup] detected AppleDouble metadata in .venv; scheduling rebuild."
VENV_REBUILD_REQUIRED=true
fi

if ! (cd "$REPO_ROOT" && ./.venv/bin/python -I -W ignore -c 'import sys; print(sys.prefix)' >/dev/null 2>&1); then
echo "[dev-setup] .venv isolated startup failed; scheduling rebuild."
VENV_REBUILD_REQUIRED=true
fi
fi

if [ "$VENV_REBUILD_REQUIRED" = true ]; then
if [ "$OFFLINE" = true ]; then
echo "[dev-setup] cannot rebuild .venv in --offline mode." >&2
exit 3
fi
echo "[dev-setup] rebuilding .venv..."
rm -rf "$REPO_ROOT/.venv"
fi

# 1) Poetry: install Python deps + register nlx entrypoint
echo ""
Expand All @@ -56,6 +92,14 @@ else
echo " Dashboard install complete."
fi

echo ""
echo "[post] Scrubbing generated artifact metadata..."
bash "$REPO_ROOT/scripts/env-integrity.sh" scrub

echo ""
echo "[post] Validating environment integrity..."
bash "$REPO_ROOT/scripts/env-integrity.sh" check

# Summary
echo ""
echo "=== Setup complete ==="
Expand Down
Loading
Loading