fix(cli): discover tools-dev daemon URL

This commit is contained in:
Vivaan Dhawan 2026-05-23 22:16:31 +05:30
parent ad37fd30cf
commit 8d995d8357
2 changed files with 97 additions and 2 deletions

View file

@ -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;
}

View file

@ -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()}`