fix: improve daemon node selection
This commit is contained in:
@@ -32,7 +32,11 @@ import {
|
||||
import { resolveGatewayLogPaths } from "../daemon/launchd.js";
|
||||
import { findLegacyGatewayServices } from "../daemon/legacy.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
import {
|
||||
renderSystemNodeWarning,
|
||||
resolvePreferredNodePath,
|
||||
resolveSystemNodeInfo,
|
||||
} from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import type { ServiceConfigAudit } from "../daemon/service-audit.js";
|
||||
import { auditGatewayServiceConfig } from "../daemon/service-audit.js";
|
||||
@@ -923,6 +927,11 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
runtime: runtimeRaw,
|
||||
nodePath,
|
||||
});
|
||||
if (runtimeRaw === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({ env: process.env });
|
||||
const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
|
||||
if (warning) defaultRuntime.log(warning);
|
||||
}
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
} from "../config/config.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
import {
|
||||
renderSystemNodeWarning,
|
||||
resolvePreferredNodePath,
|
||||
resolveSystemNodeInfo,
|
||||
} from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||
@@ -448,6 +452,11 @@ async function maybeInstallDaemon(params: {
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
if (daemonRuntime === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({ env: process.env });
|
||||
const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
|
||||
if (warning) note(warning, "Gateway runtime");
|
||||
}
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port: params.port,
|
||||
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
} from "../daemon/legacy.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import {
|
||||
renderSystemNodeWarning,
|
||||
resolvePreferredNodePath,
|
||||
resolveSystemNodePath,
|
||||
resolveSystemNodeInfo,
|
||||
} from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import {
|
||||
@@ -183,10 +184,13 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
command,
|
||||
});
|
||||
const needsNodeRuntime = needsNodeRuntimeMigration(audit.issues);
|
||||
const systemNodePath = needsNodeRuntime
|
||||
? await resolveSystemNodePath(process.env)
|
||||
const systemNodeInfo = needsNodeRuntime
|
||||
? await resolveSystemNodeInfo({ env: process.env })
|
||||
: null;
|
||||
const systemNodePath = systemNodeInfo?.supported ? systemNodeInfo.path : null;
|
||||
if (needsNodeRuntime && !systemNodePath) {
|
||||
const warning = renderSystemNodeWarning(systemNodeInfo);
|
||||
if (warning) note(warning, "Gateway runtime");
|
||||
note(
|
||||
"System Node 22+ not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.",
|
||||
"Gateway runtime",
|
||||
|
||||
@@ -25,7 +25,11 @@ import {
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
import {
|
||||
renderSystemNodeWarning,
|
||||
resolvePreferredNodePath,
|
||||
resolveSystemNodeInfo,
|
||||
} from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
@@ -621,6 +625,16 @@ export async function doctorCommand(
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
if (daemonRuntime === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({
|
||||
env: process.env,
|
||||
});
|
||||
const warning = renderSystemNodeWarning(
|
||||
systemNode,
|
||||
programArguments[0],
|
||||
);
|
||||
if (warning) note(warning, "Gateway runtime");
|
||||
}
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
|
||||
@@ -16,7 +16,11 @@ import {
|
||||
} from "../config/config.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
import {
|
||||
renderSystemNodeWarning,
|
||||
resolvePreferredNodePath,
|
||||
resolveSystemNodeInfo,
|
||||
} from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||
@@ -535,6 +539,14 @@ export async function runNonInteractiveOnboarding(
|
||||
runtime: daemonRuntimeRaw,
|
||||
nodePath,
|
||||
});
|
||||
if (daemonRuntimeRaw === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({ env: process.env });
|
||||
const warning = renderSystemNodeWarning(
|
||||
systemNode,
|
||||
programArguments[0],
|
||||
);
|
||||
if (warning) runtime.log(warning);
|
||||
}
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
|
||||
123
src/daemon/runtime-paths.test.ts
Normal file
123
src/daemon/runtime-paths.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
access: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs/promises", () => ({
|
||||
default: { access: fsMocks.access },
|
||||
access: fsMocks.access,
|
||||
}));
|
||||
|
||||
import {
|
||||
renderSystemNodeWarning,
|
||||
resolvePreferredNodePath,
|
||||
resolveSystemNodeInfo,
|
||||
} from "./runtime-paths.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("resolvePreferredNodePath", () => {
|
||||
const darwinNode = "/opt/homebrew/bin/node";
|
||||
|
||||
it("uses system node when it meets the minimum version", async () => {
|
||||
fsMocks.access.mockImplementation(async (target: string) => {
|
||||
if (target === darwinNode) return;
|
||||
throw new Error("missing");
|
||||
});
|
||||
|
||||
const execFile = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ stdout: "22.1.0\n", stderr: "" });
|
||||
|
||||
const result = await resolvePreferredNodePath({
|
||||
env: {},
|
||||
runtime: "node",
|
||||
platform: "darwin",
|
||||
execFile,
|
||||
});
|
||||
|
||||
expect(result).toBe(darwinNode);
|
||||
expect(execFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("skips system node when it is too old", async () => {
|
||||
fsMocks.access.mockImplementation(async (target: string) => {
|
||||
if (target === darwinNode) return;
|
||||
throw new Error("missing");
|
||||
});
|
||||
|
||||
const execFile = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ stdout: "18.19.0\n", stderr: "" });
|
||||
|
||||
const result = await resolvePreferredNodePath({
|
||||
env: {},
|
||||
runtime: "node",
|
||||
platform: "darwin",
|
||||
execFile,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(execFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns undefined when no system node is found", async () => {
|
||||
fsMocks.access.mockRejectedValue(new Error("missing"));
|
||||
|
||||
const execFile = vi.fn();
|
||||
|
||||
const result = await resolvePreferredNodePath({
|
||||
env: {},
|
||||
runtime: "node",
|
||||
platform: "darwin",
|
||||
execFile,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(execFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSystemNodeInfo", () => {
|
||||
const darwinNode = "/opt/homebrew/bin/node";
|
||||
|
||||
it("returns supported info when version is new enough", async () => {
|
||||
fsMocks.access.mockImplementation(async (target: string) => {
|
||||
if (target === darwinNode) return;
|
||||
throw new Error("missing");
|
||||
});
|
||||
|
||||
const execFile = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ stdout: "22.0.0\n", stderr: "" });
|
||||
|
||||
const result = await resolveSystemNodeInfo({
|
||||
env: {},
|
||||
platform: "darwin",
|
||||
execFile,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
path: darwinNode,
|
||||
version: "22.0.0",
|
||||
supported: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a warning when system node is too old", () => {
|
||||
const warning = renderSystemNodeWarning(
|
||||
{
|
||||
path: darwinNode,
|
||||
version: "18.19.0",
|
||||
supported: false,
|
||||
},
|
||||
"/Users/me/.fnm/node-22/bin/node",
|
||||
);
|
||||
|
||||
expect(warning).toContain("below the required Node 22+");
|
||||
expect(warning).toContain(darwinNode);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { isSupportedNodeVersion } from "../infra/runtime-guard.js";
|
||||
|
||||
const VERSION_MANAGER_MARKERS = [
|
||||
"/.nvm/",
|
||||
@@ -48,6 +52,37 @@ function buildSystemNodeCandidates(
|
||||
return [];
|
||||
}
|
||||
|
||||
type ExecFileAsync = (
|
||||
file: string,
|
||||
args: readonly string[],
|
||||
options: { encoding: "utf8" },
|
||||
) => Promise<{ stdout: string; stderr: string }>;
|
||||
|
||||
const execFileAsync = promisify(execFile) as unknown as ExecFileAsync;
|
||||
|
||||
async function resolveNodeVersion(
|
||||
nodePath: string,
|
||||
execFileImpl: ExecFileAsync,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execFileImpl(
|
||||
nodePath,
|
||||
["-p", "process.versions.node"],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const value = stdout.trim();
|
||||
return value ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type SystemNodeInfo = {
|
||||
path: string;
|
||||
version: string | null;
|
||||
supported: boolean;
|
||||
};
|
||||
|
||||
export function isVersionManagedNodePath(
|
||||
nodePath: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
@@ -84,11 +119,47 @@ export async function resolveSystemNodePath(
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function resolveSystemNodeInfo(params: {
|
||||
env?: Record<string, string | undefined>;
|
||||
platform?: NodeJS.Platform;
|
||||
execFile?: ExecFileAsync;
|
||||
}): Promise<SystemNodeInfo | null> {
|
||||
const env = params.env ?? process.env;
|
||||
const platform = params.platform ?? process.platform;
|
||||
const systemNode = await resolveSystemNodePath(env, platform);
|
||||
if (!systemNode) return null;
|
||||
|
||||
const version = await resolveNodeVersion(
|
||||
systemNode,
|
||||
params.execFile ?? execFileAsync,
|
||||
);
|
||||
return {
|
||||
path: systemNode,
|
||||
version,
|
||||
supported: isSupportedNodeVersion(version),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderSystemNodeWarning(
|
||||
systemNode: SystemNodeInfo | null,
|
||||
selectedNodePath?: string,
|
||||
): string | null {
|
||||
if (!systemNode || systemNode.supported) return null;
|
||||
const versionLabel = systemNode.version ?? "unknown";
|
||||
const selectedLabel = selectedNodePath
|
||||
? ` Using ${selectedNodePath} for the daemon.`
|
||||
: "";
|
||||
return `System Node ${versionLabel} at ${systemNode.path} is below the required Node 22+.${selectedLabel} Install Node 22+ from nodejs.org or Homebrew.`;
|
||||
}
|
||||
|
||||
export async function resolvePreferredNodePath(params: {
|
||||
env?: Record<string, string | undefined>;
|
||||
runtime?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
execFile?: ExecFileAsync;
|
||||
}): Promise<string | undefined> {
|
||||
if (params.runtime !== "node") return undefined;
|
||||
const systemNode = await resolveSystemNodePath(params.env);
|
||||
return systemNode ?? undefined;
|
||||
const systemNode = await resolveSystemNodeInfo(params);
|
||||
if (!systemNode?.supported) return undefined;
|
||||
return systemNode.path;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ export function runtimeSatisfies(details: RuntimeDetails): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isSupportedNodeVersion(version: string | null): boolean {
|
||||
return isAtLeast(parseSemver(version), MIN_NODE);
|
||||
}
|
||||
|
||||
export function assertSupportedRuntime(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
details: RuntimeDetails = detectRuntime(),
|
||||
|
||||
@@ -53,7 +53,11 @@ import {
|
||||
} from "../config/config.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
|
||||
import {
|
||||
renderSystemNodeWarning,
|
||||
resolvePreferredNodePath,
|
||||
resolveSystemNodeInfo,
|
||||
} from "../daemon/runtime-paths.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||
@@ -672,6 +676,14 @@ export async function runOnboardingWizard(
|
||||
runtime: daemonRuntime,
|
||||
nodePath,
|
||||
});
|
||||
if (daemonRuntime === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({ env: process.env });
|
||||
const warning = renderSystemNodeWarning(
|
||||
systemNode,
|
||||
programArguments[0],
|
||||
);
|
||||
if (warning) await prompter.note(warning, "Gateway runtime");
|
||||
}
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
|
||||
Reference in New Issue
Block a user