mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
0688faaec3
* reland(sidecar): converge product clients after weekly release
Restore the sidecar convergence from #7244 after the release-scheduling revert in #7634. This tree matches the previously reviewed and cross-platform QA-accepted implementation at d83d4e032f.
* fix(sidecar): clarify identity and cache witnesses
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(packaged): probe cross-source desktop owners
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(sidecar): preserve cross-source launch ownership
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* test(pack): assert cross-source Windows launch ownership
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(sidecar): align HyperFrames with client boundary
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(e2e): keep HyperFrames fixture in helper lane
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* docs(sidecar): restore review and handoff contracts
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(sidecar): restore cache and daemon boundaries
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(pack): restore sourcemaps and Linux smoke discovery
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(sidecar): make payload handoff generation-atomic
* fix(sidecar): preserve packaged action and mode parity
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* test(pack): align Linux cleanup resource count
Generated-By: looper 0.11.2 (runner=fixer, agent=codex)
* fix(packaged): preserve headless sidecar mode
* test: normalize source line endings
* fix(daemon): launch Codex cmd shims on Windows
---------
Co-authored-by: Looper <looper@noreply.github.com>
283 lines
9.3 KiB
TypeScript
283 lines
9.3 KiB
TypeScript
import { mkdir } from "node:fs/promises";
|
|
|
|
import {
|
|
APP_KEYS,
|
|
SIDECAR_SOURCES,
|
|
} from "@open-design/sidecar-proto";
|
|
import {
|
|
getSidecarStatus,
|
|
registerSidecarProcess,
|
|
readCurrentSidecarStamp,
|
|
SidecarFactory,
|
|
type SidecarClient,
|
|
type SidecarRuntimeContext,
|
|
type SidecarStamp,
|
|
} from "@open-design/sidecar";
|
|
import { releaseChannelFromNamespace, releaseChannelFromVersion } from "@open-design/release";
|
|
|
|
import type { PackagedConfig } from "./config.js";
|
|
import { confirmPackagedLauncherRuntime, resolvePackagedLauncherRuntime } from "./launcher-runtime.js";
|
|
import { resolvePackagedNamespacePaths } from "./paths.js";
|
|
import type { PackagedSidecarHandle } from "./sidecars.js";
|
|
import { startPackagedSidecars } from "./sidecars.js";
|
|
|
|
const PACKAGED_SIDECAR_SOURCES = [SIDECAR_SOURCES.TOOLS_PACK, SIDECAR_SOURCES.PACKAGED] as const;
|
|
|
|
function colorize(text: string): string {
|
|
if (process.stdout.isTTY !== true || process.env.NO_COLOR != null) return text;
|
|
return `\x1b[36m\x1b[4m${text}\x1b[0m`;
|
|
}
|
|
|
|
export interface PackagedMcpBootstrapLaunch {
|
|
args: string[];
|
|
command: string;
|
|
}
|
|
|
|
export interface PackagedHeadlessRequest {
|
|
headless: boolean;
|
|
mcpInstallAgent: "codex" | null;
|
|
}
|
|
|
|
export interface RunPackagedHeadlessOptions {
|
|
mcpBootstrapLaunch?: PackagedMcpBootstrapLaunch;
|
|
}
|
|
|
|
export async function runPackagedMcpActionAgainstExistingDaemon(
|
|
request: PackagedHeadlessRequest,
|
|
stamp: SidecarStamp,
|
|
dependencies: {
|
|
getStatus?: typeof getSidecarStatus;
|
|
installMcp?: (daemonUrl: string | null) => Promise<void>;
|
|
} = {},
|
|
): Promise<boolean> {
|
|
if (request.mcpInstallAgent == null) return false;
|
|
let status: { state?: unknown; url?: unknown } | null = null;
|
|
for (const source of [stamp.source, ...PACKAGED_SIDECAR_SOURCES.filter((candidate) => candidate !== stamp.source)]) {
|
|
status = await (dependencies.getStatus ?? getSidecarStatus)<{ state?: unknown; url?: unknown }>(
|
|
{ ...stamp, app: APP_KEYS.DAEMON, mode: stamp.mode, source },
|
|
{ timeoutMs: 350 },
|
|
).catch(() => null);
|
|
if (status?.state === "running" && typeof status.url === "string" && status.url.length > 0) break;
|
|
}
|
|
if (status?.state !== "running" || typeof status.url !== "string" || status.url.length === 0) return false;
|
|
await (dependencies.installMcp ?? installCodexMcp)(status.url);
|
|
return true;
|
|
}
|
|
|
|
export interface PackagedHeadlessStartupDependencies {
|
|
confirmRuntime(): Promise<void>;
|
|
installMcp(daemonUrl: string | null): Promise<void>;
|
|
startSidecars(): Promise<PackagedSidecarHandle>;
|
|
}
|
|
|
|
export interface PackagedHeadlessStartupHandle {
|
|
shutdown(): Promise<void>;
|
|
webUrl: string;
|
|
}
|
|
|
|
export async function acquirePackagedHeadlessStartup(
|
|
dependencies: PackagedHeadlessStartupDependencies,
|
|
): Promise<PackagedHeadlessStartupHandle> {
|
|
let sidecars: PackagedSidecarHandle | null = null;
|
|
let closed = false;
|
|
|
|
const close = async (): Promise<void> => {
|
|
if (closed) return;
|
|
closed = true;
|
|
await sidecars?.close().catch(() => undefined);
|
|
};
|
|
const shutdown = async (): Promise<void> => {
|
|
await close();
|
|
};
|
|
|
|
try {
|
|
sidecars = await dependencies.startSidecars();
|
|
const webUrl = sidecars.web.url;
|
|
if (!webUrl) {
|
|
throw new Error(
|
|
"web sidecar failed to produce URL — check logs/desktop/latest.log",
|
|
);
|
|
}
|
|
await dependencies.installMcp(sidecars.daemon.url);
|
|
await dependencies.confirmRuntime();
|
|
return { shutdown, webUrl };
|
|
} catch (error) {
|
|
await close();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function parsePackagedHeadlessRequest(
|
|
argv: readonly string[],
|
|
): PackagedHeadlessRequest {
|
|
const headless = argv.includes("--headless");
|
|
const installIndex = argv.indexOf("--mcp-install");
|
|
if (installIndex === -1) return { headless, mcpInstallAgent: null };
|
|
if (!headless) {
|
|
throw new Error("--mcp-install requires --headless");
|
|
}
|
|
const agent = argv[installIndex + 1];
|
|
if (agent !== "codex") {
|
|
throw new Error(
|
|
"Packaged headless MCP installation currently only supports codex.",
|
|
);
|
|
}
|
|
return { headless: true, mcpInstallAgent: agent };
|
|
}
|
|
|
|
export function resolvePackagedMcpBootstrapLaunch(options: {
|
|
currentExecutablePath?: string;
|
|
installedLaunchPath: string | null;
|
|
platform?: NodeJS.Platform;
|
|
}): PackagedMcpBootstrapLaunch {
|
|
const platform = options.platform ?? process.platform;
|
|
const currentExecutablePath =
|
|
options.currentExecutablePath ?? process.execPath;
|
|
if (
|
|
platform === "darwin"
|
|
&& options.installedLaunchPath?.endsWith(".app")
|
|
) {
|
|
return {
|
|
command: "/usr/bin/open",
|
|
args: [
|
|
"-g",
|
|
"-j",
|
|
options.installedLaunchPath,
|
|
"--args",
|
|
"--headless",
|
|
],
|
|
};
|
|
}
|
|
return {
|
|
command: options.installedLaunchPath ?? currentExecutablePath,
|
|
args: ["--headless"],
|
|
};
|
|
}
|
|
|
|
export async function runPackagedHeadless(
|
|
config: PackagedConfig,
|
|
request: PackagedHeadlessRequest = {
|
|
headless: true,
|
|
mcpInstallAgent: null,
|
|
},
|
|
options: RunPackagedHeadlessOptions = {},
|
|
): Promise<void> {
|
|
const initialPaths = resolvePackagedNamespacePaths(
|
|
config,
|
|
config.namespace,
|
|
process.env,
|
|
);
|
|
const launcherRuntime = await resolvePackagedLauncherRuntime(config, initialPaths);
|
|
const activeConfig = launcherRuntime.config;
|
|
const paths = launcherRuntime.paths;
|
|
const argvStamp = (() => {
|
|
try { return readCurrentSidecarStamp(); } catch { return null; }
|
|
})();
|
|
const stamp = {
|
|
app: APP_KEYS.DESKTOP,
|
|
channel: argvStamp?.channel ?? releaseChannelFromVersion(activeConfig.appVersion)
|
|
?? releaseChannelFromNamespace(config.namespace, "default")
|
|
?? "stable",
|
|
mode: "headless",
|
|
namespace: config.namespace,
|
|
source: argvStamp?.source ?? SIDECAR_SOURCES.PACKAGED,
|
|
};
|
|
const mcpBootstrap =
|
|
options.mcpBootstrapLaunch
|
|
?? resolvePackagedMcpBootstrapLaunch({
|
|
installedLaunchPath: launcherRuntime.installedLaunchPath,
|
|
});
|
|
|
|
await mkdir(paths.runtimeRoot, { recursive: true });
|
|
registerSidecarProcess(stamp, {
|
|
dataRoot: paths.dataRoot,
|
|
ownerPid: null,
|
|
port: 0,
|
|
runtimeRoot: paths.runtimeRoot,
|
|
});
|
|
const runtime: SidecarRuntimeContext<SidecarStamp> = {
|
|
app: APP_KEYS.DESKTOP,
|
|
base: paths.runtimeRoot,
|
|
mode: "headless",
|
|
namespace: config.namespace,
|
|
source: stamp.source,
|
|
};
|
|
|
|
type HeadlessRuntime = PackagedHeadlessStartupHandle;
|
|
let client!: SidecarClient<HeadlessRuntime>;
|
|
client = SidecarFactory.create<HeadlessRuntime>({
|
|
lifecycle: {
|
|
async start() {
|
|
return await acquirePackagedHeadlessStartup({
|
|
confirmRuntime: async () => await confirmPackagedLauncherRuntime(launcherRuntime),
|
|
installMcp: async (daemonUrl) => {
|
|
if (request.mcpInstallAgent === "codex") {
|
|
await installCodexMcp(daemonUrl);
|
|
}
|
|
},
|
|
startSidecars: async () =>
|
|
await startPackagedSidecars(runtime, paths, {
|
|
appVersion: activeConfig.appVersion,
|
|
amrProfile: activeConfig.amrProfile,
|
|
daemonCliEntry: activeConfig.daemonCliEntry,
|
|
daemonSidecarEntry: activeConfig.daemonSidecarEntry,
|
|
electronNodeCommand: launcherRuntime.electronNodeCommand,
|
|
mcpBootstrapArgs: mcpBootstrap.args,
|
|
mcpBootstrapCommand: mcpBootstrap.command,
|
|
nodeCommand: activeConfig.nodeCommand,
|
|
telemetryRelayUrl: activeConfig.telemetryRelayUrl,
|
|
posthogKey: activeConfig.posthogKey,
|
|
posthogHost: activeConfig.posthogHost,
|
|
velaWebUrl: activeConfig.velaWebUrl,
|
|
velaWebUrls: activeConfig.velaWebUrls,
|
|
// PR #974 round-5 (lefarcen P2): headless packaged mode uses the signed
|
|
// Electron entry as a lifecycle owner, but creates no BrowserWindow and
|
|
// exposes no privileged shell.openPath surface.
|
|
// Pinning OD_REQUIRE_DESKTOP_AUTH here would arm a gate no client
|
|
// can ever satisfy (no desktop window/main bridge to register a secret),
|
|
// so folder import would permanently return DESKTOP_AUTH_PENDING.
|
|
// The Electron entry counterpart in `apps/packaged/src/index.ts`
|
|
// passes `true` because it does start that desktop bridge.
|
|
requireDesktopAuth: false,
|
|
webSidecarEntry: activeConfig.webSidecarEntry,
|
|
webStandaloneRoot: activeConfig.webStandaloneRoot,
|
|
webOutputMode: activeConfig.webOutputMode,
|
|
}),
|
|
});
|
|
},
|
|
status: (started) => ({
|
|
pid: process.pid,
|
|
state: "running",
|
|
updatedAt: new Date().toISOString(),
|
|
url: started.webUrl,
|
|
windowVisible: false,
|
|
}),
|
|
async stop(started) {
|
|
await started.shutdown();
|
|
},
|
|
},
|
|
});
|
|
await client.start();
|
|
const webUrl = (await client.status<{ url: string }>(APP_KEYS.DESKTOP)).url;
|
|
|
|
process.stdout.write(`\n Open Design is running\n\n`);
|
|
process.stdout.write(` ➜ ${colorize(webUrl)}\n\n`);
|
|
process.stdout.write(` Press Ctrl+C to stop\n\n`);
|
|
|
|
}
|
|
|
|
async function installCodexMcp(daemonUrl: string | null): Promise<void> {
|
|
if (daemonUrl == null || daemonUrl.length === 0) {
|
|
throw new Error("daemon sidecar failed to produce a URL for MCP install");
|
|
}
|
|
const url = `${daemonUrl.replace(/\/$/u, "")}/api/mcp/install/codex`;
|
|
const response = await fetch(url, { method: "POST" });
|
|
if (!response.ok) {
|
|
const detail = await response.text().catch(() => response.statusText);
|
|
throw new Error(
|
|
`Codex MCP install failed (${response.status}): ${detail}`,
|
|
);
|
|
}
|
|
process.stdout.write(" Open Design MCP installed for Codex\n");
|
|
}
|