mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
Merge 8d995d8357 into af4a62b69a
This commit is contained in:
commit
d9bbf257da
2 changed files with 97 additions and 2 deletions
|
|
@ -1,3 +1,6 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
SIDECAR_ENV,
|
||||
SIDECAR_MESSAGES,
|
||||
|
|
@ -6,6 +9,7 @@ import {
|
|||
import { requestJsonIpc } from "@open-design/sidecar";
|
||||
|
||||
export const DEFAULT_DAEMON_URL = "http://127.0.0.1:7456";
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
|
||||
export interface ResolveDaemonUrlOptions {
|
||||
/** Value passed via `--daemon-url`. Empty string is treated as unset. */
|
||||
|
|
@ -21,8 +25,9 @@ export interface ResolveDaemonUrlOptions {
|
|||
*
|
||||
* Spawn order: explicit `--daemon-url` flag, `OD_DAEMON_URL` env, then
|
||||
* a STATUS roundtrip to the concrete sidecar IPC endpoint supplied by
|
||||
* the lifecycle owner in `OD_SIDECAR_IPC_PATH`. Falls back to the
|
||||
* legacy default for direct `od` launches that do not run as a sidecar.
|
||||
* the lifecycle owner in `OD_SIDECAR_IPC_PATH`, then the default
|
||||
* `tools-dev status --json` runtime. Falls back to the legacy default
|
||||
* for direct `od` launches that do not run as a sidecar.
|
||||
*/
|
||||
export async function resolveDaemonUrl(
|
||||
options: ResolveDaemonUrlOptions = {},
|
||||
|
|
@ -34,6 +39,8 @@ export async function resolveDaemonUrl(
|
|||
if (envUrl != null && envUrl.length > 0) return envUrl;
|
||||
const discovered = await discoverDaemonUrlFromIpc(env, options.timeoutMs ?? 800);
|
||||
if (discovered != null) return discovered;
|
||||
const toolsDevUrl = await discoverDaemonUrlFromToolsDev(env, options.timeoutMs ?? 800);
|
||||
if (toolsDevUrl != null) return toolsDevUrl;
|
||||
return DEFAULT_DAEMON_URL;
|
||||
}
|
||||
|
||||
|
|
@ -54,3 +61,59 @@ async function discoverDaemonUrlFromIpc(
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function discoverDaemonUrlFromToolsDev(
|
||||
env: NodeJS.ProcessEnv,
|
||||
timeoutMs: number,
|
||||
): Promise<string | null> {
|
||||
return await new Promise<string | null>((resolve) => {
|
||||
let child;
|
||||
try {
|
||||
child = spawn("pnpm", ["--silent", "exec", "tools-dev", "status", "--json"], {
|
||||
cwd: REPO_ROOT,
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
} catch {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let settled = false;
|
||||
let stdout = "";
|
||||
const done = (url: string | null) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve(url);
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
child.kill();
|
||||
done(null);
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout?.on("data", (chunk: Buffer | string) => {
|
||||
stdout += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
||||
});
|
||||
child.on("error", () => done(null));
|
||||
child.on("close", (code) => {
|
||||
done(code === 0 ? extractDaemonUrlFromToolsDevStatus(stdout) : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function extractDaemonUrlFromToolsDevStatus(stdout: string): string | null {
|
||||
for (let i = stdout.indexOf("{"); i !== -1; i = stdout.indexOf("{", i + 1)) {
|
||||
try {
|
||||
const parsed = JSON.parse(stdout.slice(i)) as {
|
||||
apps?: { daemon?: { url?: string | null } };
|
||||
url?: string | null;
|
||||
};
|
||||
const url = parsed?.apps?.daemon?.url ?? parsed?.url ?? null;
|
||||
if (typeof url === "string" && url.length > 0) return url;
|
||||
} catch {
|
||||
// pnpm wrappers can print warnings before JSON; continue scanning.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,19 @@ import { resolveDaemonUrl, DEFAULT_DAEMON_URL } from "../src/daemon-url.js";
|
|||
|
||||
describe("resolveDaemonUrl", () => {
|
||||
let ipcBaseDir: string;
|
||||
let fakeBinDir: string;
|
||||
let emptyBinDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
ipcBaseDir = fs.mkdtempSync(path.join(os.tmpdir(), "od-mcp-resolve-"));
|
||||
fakeBinDir = fs.mkdtempSync(path.join(os.tmpdir(), "od-tools-dev-resolve-"));
|
||||
emptyBinDir = fs.mkdtempSync(path.join(os.tmpdir(), "od-tools-dev-empty-"));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(ipcBaseDir, { recursive: true, force: true });
|
||||
fs.rmSync(fakeBinDir, { recursive: true, force: true });
|
||||
fs.rmSync(emptyBinDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("prefers the explicit --daemon-url flag", async () => {
|
||||
|
|
@ -45,6 +51,7 @@ describe("resolveDaemonUrl", () => {
|
|||
it("returns the legacy default when no flag/env/socket is available", async () => {
|
||||
const url = await resolveDaemonUrl({
|
||||
env: {
|
||||
PATH: emptyBinDir,
|
||||
[SIDECAR_ENV.IPC_PATH]: path.join(ipcBaseDir, "missing.sock"),
|
||||
},
|
||||
timeoutMs: 200,
|
||||
|
|
@ -52,6 +59,31 @@ describe("resolveDaemonUrl", () => {
|
|||
expect(url).toBe(DEFAULT_DAEMON_URL);
|
||||
});
|
||||
|
||||
it("discovers the default tools-dev daemon URL when no sidecar IPC path is available", async () => {
|
||||
const pnpmBin = path.join(fakeBinDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm");
|
||||
const statusJson = JSON.stringify({
|
||||
apps: {
|
||||
daemon: {
|
||||
url: "http://127.0.0.1:60123",
|
||||
},
|
||||
},
|
||||
});
|
||||
if (process.platform === "win32") {
|
||||
fs.writeFileSync(pnpmBin, `@echo off\r\necho ${statusJson.replace(/"/g, '\\"')}\r\n`);
|
||||
} else {
|
||||
fs.writeFileSync(pnpmBin, `#!/bin/sh\nprintf '%s\\n' 'pnpm warning before json'\nprintf '%s\\n' '${statusJson}'\n`);
|
||||
fs.chmodSync(pnpmBin, 0o755);
|
||||
}
|
||||
|
||||
const url = await resolveDaemonUrl({
|
||||
env: {
|
||||
PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
},
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
expect(url).toBe("http://127.0.0.1:60123");
|
||||
});
|
||||
|
||||
it("discovers the live daemon URL via the concrete sidecar IPC status endpoint", async () => {
|
||||
const socketPath = process.platform === "win32"
|
||||
? `\\\\.\\pipe\\open-design-daemon-url-${process.pid}-${Date.now()}`
|
||||
|
|
|
|||
Loading…
Reference in a new issue