mirror of
https://github.com/cline/cline.git
synced 2026-09-21 05:10:09 +08:00
fix: CLI hub commands to use detached daemon (#267)
Fix CLI hub start and ensure so they launch and reuse the detached hub daemon instead of starting an in-process server that dies when the CLI exits. Also make detached hub startup fall back to an ephemeral port when the default port is unavailable, matching the existing in-process fallback behavior.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
clearHubDiscovery,
|
||||
createLocalHubScheduleRuntimeHandlers,
|
||||
ensureHubServer,
|
||||
ensureDetachedHubServer,
|
||||
probeHubServer,
|
||||
readHubDiscovery,
|
||||
resolveSharedHubOwnerContext,
|
||||
@@ -71,13 +70,12 @@ export function createHubCommand(
|
||||
port?: number;
|
||||
pathname?: string;
|
||||
}>();
|
||||
const result = await ensureHubServer({
|
||||
const url = await ensureDetachedHubServer(opts.cwd, {
|
||||
host: opts.host,
|
||||
port: opts.port,
|
||||
pathname: opts.pathname,
|
||||
runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
|
||||
});
|
||||
io.writeln(result.url);
|
||||
io.writeln(url);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -89,22 +87,12 @@ export function createHubCommand(
|
||||
port?: number;
|
||||
pathname?: string;
|
||||
}>();
|
||||
const result = await ensureHubServer({
|
||||
const url = await ensureDetachedHubServer(opts.cwd, {
|
||||
host: opts.host,
|
||||
port: opts.port,
|
||||
pathname: opts.pathname,
|
||||
runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
|
||||
});
|
||||
io.writeln(result.url);
|
||||
if (!result.server) {
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
const shutdown = () => resolve();
|
||||
process.once("SIGINT", shutdown);
|
||||
process.once("SIGTERM", shutdown);
|
||||
});
|
||||
await result.server.close();
|
||||
io.writeln(url);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,23 @@ vi.mock("@clinebot/core", () => ({
|
||||
|
||||
vi.mock("../utils/hub-runtime", () => ({
|
||||
ensureCliHubServer: mockEnsureCliHubServer,
|
||||
parseHubEndpointOverride: (rawAddress: string | undefined) => {
|
||||
const trimmed = rawAddress?.trim();
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
const parsed = new URL(
|
||||
trimmed.includes("://") ? trimmed : `ws://${trimmed}`,
|
||||
);
|
||||
return {
|
||||
host: parsed.hostname || undefined,
|
||||
port: parsed.port ? Number(parsed.port) : undefined,
|
||||
pathname:
|
||||
parsed.pathname && parsed.pathname !== "/"
|
||||
? parsed.pathname
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
async function runScheduleCommand(
|
||||
@@ -54,7 +71,7 @@ describe("runScheduleCommand list output", () => {
|
||||
expect(errors).toEqual([]);
|
||||
expect(output).toEqual(["No schedules found."]);
|
||||
expect(mockSendHubCommand).toHaveBeenCalledWith(
|
||||
{},
|
||||
{ host: "127.0.0.1", port: 25463, pathname: "/hub" },
|
||||
{
|
||||
clientId: "clite-schedule",
|
||||
command: "schedule.list",
|
||||
@@ -138,7 +155,7 @@ describe("runScheduleCommand import", () => {
|
||||
expect(errors).toEqual([]);
|
||||
expect(output).toEqual(['{\n "scheduleId": "sched_123"\n}']);
|
||||
expect(mockSendHubCommand).toHaveBeenCalledWith(
|
||||
{},
|
||||
{ host: "127.0.0.1", port: 25463, pathname: "/hub" },
|
||||
{
|
||||
clientId: "clite-schedule",
|
||||
command: "schedule.create",
|
||||
@@ -199,7 +216,7 @@ describe("runScheduleCommand export", () => {
|
||||
const written = await readFile(targetPath, "utf8");
|
||||
expect(written).toBe(JSON.stringify(scheduleRecord, null, 2));
|
||||
expect(mockSendHubCommand).toHaveBeenCalledWith(
|
||||
{},
|
||||
{ host: "127.0.0.1", port: 25463, pathname: "/hub" },
|
||||
{
|
||||
clientId: "clite-schedule",
|
||||
command: "schedule.get",
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
import { sendHubCommand } from "@clinebot/core";
|
||||
import { ensureCliHubServer } from "../../utils/hub-runtime";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
} from "../../utils/hub-runtime";
|
||||
import type { CommandIo } from "./types";
|
||||
|
||||
export function parseHubAddress(address: string | undefined): {
|
||||
host?: string;
|
||||
port?: number;
|
||||
pathname?: string;
|
||||
} {
|
||||
const trimmed = address?.trim();
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
const [host, portRaw] = trimmed.split(":", 2);
|
||||
const port = Number.parseInt(portRaw ?? "", 10);
|
||||
return {
|
||||
host: host?.trim() || undefined,
|
||||
port: Number.isInteger(port) ? port : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export class HubScheduleClient {
|
||||
constructor(
|
||||
private readonly endpoint: {
|
||||
@@ -110,8 +96,9 @@ export async function ensureSchedulerHub(
|
||||
client: HubScheduleClient;
|
||||
}> {
|
||||
try {
|
||||
const endpoint = parseHubAddress(address);
|
||||
await ensureCliHubServer(workspaceRoot, endpoint);
|
||||
const requestedEndpoint = parseHubEndpointOverride(address);
|
||||
const hubUrl = await ensureCliHubServer(workspaceRoot, requestedEndpoint);
|
||||
const endpoint = parseHubEndpointOverride(hubUrl);
|
||||
return {
|
||||
ok: true,
|
||||
client: new HubScheduleClient(endpoint),
|
||||
|
||||
@@ -105,6 +105,8 @@ export function prewarmDetachedHubServer(
|
||||
endpoint: HubEndpointOverrides = {},
|
||||
): void {
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const hasExplicitPort =
|
||||
endpoint.port !== undefined || !!process.env.CLINE_HUB_PORT?.trim();
|
||||
const resolvedEndpoint = resolveHubEndpointOptions(endpoint);
|
||||
const expectedUrl = createHubServerUrl(
|
||||
resolvedEndpoint.host,
|
||||
@@ -124,10 +126,11 @@ export function prewarmDetachedHubServer(
|
||||
await writeHubDiscovery(owner.discoveryPath, expected);
|
||||
return;
|
||||
}
|
||||
const spawnEndpoint =
|
||||
expected?.url && resolvedEndpoint.port !== 0
|
||||
? { ...resolvedEndpoint, port: 0 }
|
||||
: resolvedEndpoint;
|
||||
const shouldUseFallbackPort =
|
||||
!hasExplicitPort && resolvedEndpoint.port !== 0;
|
||||
const spawnEndpoint = shouldUseFallbackPort
|
||||
? { ...resolvedEndpoint, port: 0 }
|
||||
: resolvedEndpoint;
|
||||
spawnDetachedHubServer(workspaceRoot, spawnEndpoint);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -140,6 +143,9 @@ export async function ensureDetachedHubServer(
|
||||
endpointOverrides: HubEndpointOverrides = {},
|
||||
): Promise<string> {
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const hasExplicitPort =
|
||||
endpointOverrides.port !== undefined ||
|
||||
!!process.env.CLINE_HUB_PORT?.trim();
|
||||
const endpoint = resolveHubEndpointOptions(endpointOverrides);
|
||||
const expectedUrl = createHubServerUrl(
|
||||
endpoint.host,
|
||||
@@ -158,8 +164,10 @@ export async function ensureDetachedHubServer(
|
||||
await writeHubDiscovery(owner.discoveryPath, expected);
|
||||
return expected.url;
|
||||
}
|
||||
const spawnEndpoint =
|
||||
expected?.url && endpoint.port !== 0 ? { ...endpoint, port: 0 } : endpoint;
|
||||
const shouldUseFallbackPort = !hasExplicitPort && endpoint.port !== 0;
|
||||
const spawnEndpoint = shouldUseFallbackPort
|
||||
? { ...endpoint, port: 0 }
|
||||
: endpoint;
|
||||
spawnDetachedHubServer(workspaceRoot, spawnEndpoint);
|
||||
const deadline = Date.now() + HUB_STARTUP_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
|
||||
Reference in New Issue
Block a user