mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +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 {
|
import {
|
||||||
SIDECAR_ENV,
|
SIDECAR_ENV,
|
||||||
SIDECAR_MESSAGES,
|
SIDECAR_MESSAGES,
|
||||||
|
|
@ -6,6 +9,7 @@ import {
|
||||||
import { requestJsonIpc } from "@open-design/sidecar";
|
import { requestJsonIpc } from "@open-design/sidecar";
|
||||||
|
|
||||||
export const DEFAULT_DAEMON_URL = "http://127.0.0.1:7456";
|
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 {
|
export interface ResolveDaemonUrlOptions {
|
||||||
/** Value passed via `--daemon-url`. Empty string is treated as unset. */
|
/** 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
|
* Spawn order: explicit `--daemon-url` flag, `OD_DAEMON_URL` env, then
|
||||||
* a STATUS roundtrip to the concrete sidecar IPC endpoint supplied by
|
* a STATUS roundtrip to the concrete sidecar IPC endpoint supplied by
|
||||||
* the lifecycle owner in `OD_SIDECAR_IPC_PATH`. Falls back to the
|
* the lifecycle owner in `OD_SIDECAR_IPC_PATH`, then the default
|
||||||
* legacy default for direct `od` launches that do not run as a sidecar.
|
* `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(
|
export async function resolveDaemonUrl(
|
||||||
options: ResolveDaemonUrlOptions = {},
|
options: ResolveDaemonUrlOptions = {},
|
||||||
|
|
@ -34,6 +39,8 @@ export async function resolveDaemonUrl(
|
||||||
if (envUrl != null && envUrl.length > 0) return envUrl;
|
if (envUrl != null && envUrl.length > 0) return envUrl;
|
||||||
const discovered = await discoverDaemonUrlFromIpc(env, options.timeoutMs ?? 800);
|
const discovered = await discoverDaemonUrlFromIpc(env, options.timeoutMs ?? 800);
|
||||||
if (discovered != null) return discovered;
|
if (discovered != null) return discovered;
|
||||||
|
const toolsDevUrl = await discoverDaemonUrlFromToolsDev(env, options.timeoutMs ?? 800);
|
||||||
|
if (toolsDevUrl != null) return toolsDevUrl;
|
||||||
return DEFAULT_DAEMON_URL;
|
return DEFAULT_DAEMON_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,3 +61,59 @@ async function discoverDaemonUrlFromIpc(
|
||||||
return null;
|
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", () => {
|
describe("resolveDaemonUrl", () => {
|
||||||
let ipcBaseDir: string;
|
let ipcBaseDir: string;
|
||||||
|
let fakeBinDir: string;
|
||||||
|
let emptyBinDir: string;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
ipcBaseDir = fs.mkdtempSync(path.join(os.tmpdir(), "od-mcp-resolve-"));
|
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(() => {
|
afterAll(() => {
|
||||||
fs.rmSync(ipcBaseDir, { recursive: true, force: true });
|
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 () => {
|
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 () => {
|
it("returns the legacy default when no flag/env/socket is available", async () => {
|
||||||
const url = await resolveDaemonUrl({
|
const url = await resolveDaemonUrl({
|
||||||
env: {
|
env: {
|
||||||
|
PATH: emptyBinDir,
|
||||||
[SIDECAR_ENV.IPC_PATH]: path.join(ipcBaseDir, "missing.sock"),
|
[SIDECAR_ENV.IPC_PATH]: path.join(ipcBaseDir, "missing.sock"),
|
||||||
},
|
},
|
||||||
timeoutMs: 200,
|
timeoutMs: 200,
|
||||||
|
|
@ -52,6 +59,31 @@ describe("resolveDaemonUrl", () => {
|
||||||
expect(url).toBe(DEFAULT_DAEMON_URL);
|
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 () => {
|
it("discovers the live daemon URL via the concrete sidecar IPC status endpoint", async () => {
|
||||||
const socketPath = process.platform === "win32"
|
const socketPath = process.platform === "win32"
|
||||||
? `\\\\.\\pipe\\open-design-daemon-url-${process.pid}-${Date.now()}`
|
? `\\\\.\\pipe\\open-design-daemon-url-${process.pid}-${Date.now()}`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue