Skip to content
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ gemini extensions install https://github.com/ix-infrastructure/ix-gemini-plugin
## Requirements

- macOS, Linux, or Windows
- Node.js 20 or newer
- Git installed
- Docker (for full functionality)

Expand Down
3 changes: 3 additions & 0 deletions ix-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"bin": {
"ix": "dist/cli/main.js"
},
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"build": "node scripts/build-core-ingestion.mjs && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
"test": "node scripts/build-core-ingestion.mjs && vitest run --exclude test/parser.test.ts && node test/parser.smoke.mjs",
Expand Down
5 changes: 3 additions & 2 deletions ix-cli/src/cli/commands/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IxClient } from "../../client/api.js";
import { getEndpoint } from "../config.js";
import { roundFloat } from "../format.js";
import { bootstrap } from "../bootstrap.js";
import { formatFetchError } from "../errors.js";
import { ingestFiles } from "./ingest.js";

export interface MapRegion {
Expand Down Expand Up @@ -159,7 +160,7 @@ Examples:
mapMode: true,
});
} catch (err: any) {
console.error(chalk.red("Error:"), err.message ?? err);
console.error(chalk.red("Error:"), formatFetchError(err));
process.exitCode = 1;
return;
}
Expand All @@ -183,7 +184,7 @@ Examples:
result = await client.map({ full: opts.full }) as MapResult;
} catch (err: any) {
if (mapInterval) { clearInterval(mapInterval); process.stderr.write('\r' + ' '.repeat(60) + '\r'); }
console.error(chalk.red("Error:"), err.message ?? err);
console.error(chalk.red("Error:"), formatFetchError(err));
process.exitCode = 1;
return;
}
Expand Down
33 changes: 33 additions & 0 deletions ix-cli/src/cli/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,39 @@ export class CliResolutionError extends Error {
}
}

/**
* Format an error for display, unwrapping fetch `TypeError: fetch failed`
* to expose the underlying transport cause (e.g. ECONNRESET, UND_ERR_SOCKET).
*
* Node's built-in fetch (undici) throws `TypeError('fetch failed')` on any
* transport-level failure and stashes the real error in `err.cause`. Without
* this unwrap, users see a bare "fetch failed" with no actionable detail —
* see the Node 18 EOL / undici 5.x transport-drop bug report.
*/
export function formatFetchError(err: unknown): string {
if (err === null || err === undefined) return String(err);
const e = err as { message?: unknown; cause?: unknown };
const base = typeof e.message === "string" && e.message.length > 0
? e.message
: String(err);

if (base.toLowerCase().includes("fetch failed") && e.cause) {
const cause = e.cause as { code?: unknown; message?: unknown };
const parts: string[] = [];
if (typeof cause.code === "string" && cause.code.length > 0) {
parts.push(cause.code);
}
if (typeof cause.message === "string" && cause.message.length > 0) {
parts.push(cause.message);
} else if (parts.length === 0) {
parts.push(String(e.cause));
}
return `${base} (${parts.join(": ")})`;
}

return base;
}

export function renderCliError(err: unknown, debug = false): void {
if (err instanceof CliUsageError || err instanceof CliResolutionError) {
process.stderr.write(chalk.red(`Error: ${err.message}\n`));
Expand Down
16 changes: 16 additions & 0 deletions ix-cli/src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ import { readFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";

// Runtime Node version guard. Runs before any command work so users on an
// unsupported Node get an actionable message instead of a cryptic "fetch
// failed" deep inside undici.
const MIN_NODE_MAJOR = 20;
{
const current = process.versions.node;
const major = parseInt(current.split(".")[0] ?? "0", 10);
if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
process.stderr.write(
`Ix requires Node.js ${MIN_NODE_MAJOR} or newer. You are running v${current}.\n` +
`Install a supported version from https://nodejs.org/ and re-run.\n`
);
process.exit(1);
}
}

let cliVersion = "0.0.0";
try {
const __dirname = dirname(fileURLToPath(import.meta.url));
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ git>=2
# STEP 1: Node.js (auto-installed/upgraded if missing or too old)
# ============================================================================

# Node.js >= 18 (installer targets Node 22 LTS)
# Node.js >= 20 (installer targets Node 22 LTS)
# macOS: brew install node OR official .pkg installer
# Linux: NodeSource apt/dnf/yum repo (setup_22.x) OR apk
# Windows: manual from https://nodejs.org/
node>=18
node>=20
npm>=8 # bundled with Node.js

# ============================================================================
Expand Down
6 changes: 3 additions & 3 deletions scripts/build-cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ CLI_DIR="$IX_DIR/ix-cli"
check_prerequisites() {
if ! command -v node &> /dev/null; then
echo "Error: Node.js is not installed."
echo " Install: https://nodejs.org/ (v18+ required)"
echo " Install: https://nodejs.org/ (v20+ required)"
exit 1
fi

NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo "Error: Node.js 18+ required (found $(node -v))"
if [ "$NODE_VERSION" -lt 20 ]; then
echo "Error: Node.js 20+ required (found $(node -v))"
exit 1
fi

Expand Down
2 changes: 1 addition & 1 deletion scripts/dev/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ if [ "${#MISSING_DEPS[@]}" -gt 0 ]; then
for dep in "${MISSING_DEPS[@]}"; do
case "$dep" in
docker) echo " docker: https://docs.docker.com/get-docker/" ;;
node) echo " node: https://nodejs.org (v18+ required)" ;;
node) echo " node: https://nodejs.org (v20+ required)" ;;
esac
done
echo ""
Expand Down
2 changes: 1 addition & 1 deletion scripts/install/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ $IxBin = "$IxHome\bin"
$ComposeDir = "$IxHome\backend"
$HealthUrl = "http://localhost:8090/v1/health"
$ArangoUrl = "http://localhost:8529/_api/version"
$NodeMinMajor = 18
$NodeMinMajor = 20

# ── Helpers ──────────────────────────────────────────────────────────────────

Expand Down
2 changes: 1 addition & 1 deletion scripts/install/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ COMPOSE_DIR="$IX_HOME/backend"
HEALTH_URL="http://localhost:8090/v1/health"
ARANGO_URL="http://localhost:8529/_api/version"

NODE_MIN_MAJOR=18
NODE_MIN_MAJOR=20

# -- Windows / POSIX docker compose wrapper --

Expand Down
Loading