mirror of
https://github.com/cline/cline.git
synced 2026-09-13 18:10:14 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54cd7fa3ee | ||
|
|
1f888a65e8 | ||
|
|
05c2b4bbcf |
@@ -153,6 +153,137 @@ describe("runDoctorCommand", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reports hub daemon processes that are not the active discovered hub", async () => {
|
||||
const cwd = "/workspace";
|
||||
mockReadHubDiscovery.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
});
|
||||
mockProbeHubServer.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
});
|
||||
mockSpawnSync.mockImplementation((command: string, args?: string[]) => {
|
||||
if (command === "lsof") {
|
||||
return { status: 0, stdout: "50174\n" };
|
||||
}
|
||||
if (
|
||||
command === "pgrep" &&
|
||||
Array.isArray(args) &&
|
||||
args[0] === "-fal" &&
|
||||
args[1] === "cline-hub-daemon"
|
||||
) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: [
|
||||
"50174 /usr/local/bin/cline --cline-hub-daemon --cwd /workspace",
|
||||
"50190 /usr/local/bin/cline --cline-hub-daemon --cwd /other",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
if (
|
||||
command === "pgrep" &&
|
||||
Array.isArray(args) &&
|
||||
args[0] === "-fal" &&
|
||||
args[1] === "/hub/daemon/entry.ts"
|
||||
) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout:
|
||||
"50191 /Users/example/.bun/bin/bun /repo/sdk/packages/core/src/hub/daemon/entry.ts --cwd /repo",
|
||||
};
|
||||
}
|
||||
return { status: 1, stdout: "" };
|
||||
});
|
||||
|
||||
const output: string[] = [];
|
||||
const code = await runDoctorCommand(
|
||||
{ cwd, json: true },
|
||||
{
|
||||
writeln: (text) => {
|
||||
output.push(text ?? "");
|
||||
},
|
||||
writeErr: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(0);
|
||||
const status = JSON.parse(output[0] || "");
|
||||
expect(
|
||||
status.orphanedHubDaemonProcesses.map(
|
||||
(record: { pid: number }) => record.pid,
|
||||
),
|
||||
).toEqual(process.platform === "win32" ? [] : [50190, 50191]);
|
||||
});
|
||||
|
||||
it("reports unmanaged connector processes for every registered connector", async () => {
|
||||
const cwd = "/workspace";
|
||||
mockReadHubDiscovery.mockResolvedValue(undefined);
|
||||
mockProbeHubServer.mockResolvedValue(undefined);
|
||||
mockSpawnSync.mockImplementation((command: string, args?: string[]) => {
|
||||
if (
|
||||
command === "pgrep" &&
|
||||
Array.isArray(args) &&
|
||||
args[0] === "-fal" &&
|
||||
args[1] === "connect"
|
||||
) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: [
|
||||
"61001 cline connect slack --bot-token xoxb-secret --signing-secret=super-signature -i",
|
||||
"61002 cline connect discord --bot-token discord-secret -i",
|
||||
"61003 cline connect gchat --service-account-key /tmp/gchat-key.json -i",
|
||||
"61004 cline connect linear --api-key lin-secret -i",
|
||||
"61005 cline connect telegram --bot-token telegram-secret -i",
|
||||
"61006 cline connect whatsapp --access-token whatsapp-secret -i",
|
||||
"61007 cline something-else connect nope",
|
||||
"61008 /usr/bin/some-tool connect slack --bot-token unrelated-secret",
|
||||
"61009 node ./connect slack-proxy.js --bot-token unrelated-secret",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
return { status: 1, stdout: "" };
|
||||
});
|
||||
|
||||
const output: string[] = [];
|
||||
const code = await runDoctorCommand(
|
||||
{ cwd, json: true },
|
||||
{
|
||||
writeln: (text) => {
|
||||
output.push(text ?? "");
|
||||
},
|
||||
writeErr: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(output).toHaveLength(1);
|
||||
const status = JSON.parse(output[0] || "");
|
||||
expect(
|
||||
status.unmanagedConnectorProcesses.map(
|
||||
(record: { type: string }) => record.type,
|
||||
),
|
||||
).toEqual(["discord", "gchat", "linear", "slack", "telegram", "whatsapp"]);
|
||||
expect(
|
||||
status.unmanagedConnectorProcesses.map(
|
||||
(record: { pid: number }) => record.pid,
|
||||
),
|
||||
).toEqual([61002, 61003, 61004, 61001, 61005, 61006]);
|
||||
expect(JSON.stringify(status.unmanagedConnectorProcesses)).not.toContain(
|
||||
"xoxb-secret",
|
||||
);
|
||||
expect(JSON.stringify(status.unmanagedConnectorProcesses)).not.toContain(
|
||||
"super-signature",
|
||||
);
|
||||
expect(
|
||||
status.unmanagedConnectorProcesses.find(
|
||||
(record: { pid: number }) => record.pid === 61001,
|
||||
)?.command,
|
||||
).toContain("--signing-secret=[REDACTED]");
|
||||
});
|
||||
|
||||
it("doctor --fix clears wedged hub startup artifacts when no server is actually running", async () => {
|
||||
const cwd = mkdtempSync(path.join(os.tmpdir(), "doctor-hub-fix-"));
|
||||
tempDirs.push(cwd);
|
||||
@@ -252,6 +383,193 @@ describe("runDoctorCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("doctor --fix kills orphaned hub daemon processes", async () => {
|
||||
const cwd = "/workspace";
|
||||
mockReadHubDiscovery.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
});
|
||||
mockProbeHubServer.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
});
|
||||
mockSpawnSync.mockImplementation((command: string, args?: string[]) => {
|
||||
if (command === "lsof") {
|
||||
return { status: 0, stdout: "50174\n" };
|
||||
}
|
||||
if (
|
||||
command === "pgrep" &&
|
||||
Array.isArray(args) &&
|
||||
args[0] === "-fal" &&
|
||||
args[1] === "cline-hub-daemon"
|
||||
) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: [
|
||||
"50174 /usr/local/bin/cline --cline-hub-daemon --cwd /workspace",
|
||||
"50190 /usr/local/bin/cline --cline-hub-daemon --cwd /other",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
return { status: 1, stdout: "" };
|
||||
});
|
||||
mockStopLocalHubServerGracefully.mockResolvedValue(true);
|
||||
const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true);
|
||||
|
||||
const output: string[] = [];
|
||||
const code = await runDoctorCommand(
|
||||
{ cwd, json: true, fix: true },
|
||||
{
|
||||
writeln: (text) => {
|
||||
output.push(text ?? "");
|
||||
},
|
||||
writeErr: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(killSpy).toHaveBeenCalledWith(50190, "SIGKILL");
|
||||
expect(killSpy).not.toHaveBeenCalledWith(50174, "SIGKILL");
|
||||
expect(JSON.parse(output[0] || "")).toMatchObject({
|
||||
before: {
|
||||
orphanedHubDaemonProcesses:
|
||||
process.platform === "win32" ? [] : [{ pid: 50190 }],
|
||||
},
|
||||
killed: {
|
||||
orphanedHubDaemons: process.platform === "win32" ? 0 : 1,
|
||||
},
|
||||
});
|
||||
killSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("doctor --fix kills a previously active hub if it becomes orphaned after graceful stop", async () => {
|
||||
const cwd = "/workspace";
|
||||
let collection = 0;
|
||||
mockReadHubDiscovery.mockImplementation(async () => {
|
||||
collection += 1;
|
||||
return collection === 1
|
||||
? {
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
}
|
||||
: {
|
||||
url: "ws://127.0.0.1:25464/hub",
|
||||
port: 25464,
|
||||
pid: 50190,
|
||||
};
|
||||
});
|
||||
mockProbeHubServer.mockImplementation(async (url: string) => {
|
||||
return url.includes(":25463/")
|
||||
? {
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
port: 25463,
|
||||
pid: 50174,
|
||||
}
|
||||
: {
|
||||
url: "ws://127.0.0.1:25464/hub",
|
||||
port: 25464,
|
||||
pid: 50190,
|
||||
};
|
||||
});
|
||||
mockSpawnSync.mockImplementation((command: string, args?: string[]) => {
|
||||
if (command === "lsof") {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: args?.includes("-tiTCP:25463") ? "50174\n" : "50190\n",
|
||||
};
|
||||
}
|
||||
if (
|
||||
command === "pgrep" &&
|
||||
Array.isArray(args) &&
|
||||
args[0] === "-fal" &&
|
||||
args[1] === "cline-hub-daemon"
|
||||
) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: [
|
||||
"50174 /usr/local/bin/cline --cline-hub-daemon --cwd /old",
|
||||
"50190 /usr/local/bin/cline --cline-hub-daemon --cwd /new",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
return { status: 1, stdout: "" };
|
||||
});
|
||||
mockStopLocalHubServerGracefully.mockResolvedValue(true);
|
||||
const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true);
|
||||
|
||||
const output: string[] = [];
|
||||
const code = await runDoctorCommand(
|
||||
{ cwd, json: true, fix: true },
|
||||
{
|
||||
writeln: (text) => {
|
||||
output.push(text ?? "");
|
||||
},
|
||||
writeErr: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(killSpy).toHaveBeenCalledWith(50174, "SIGKILL");
|
||||
expect(killSpy).not.toHaveBeenCalledWith(50190, "SIGKILL");
|
||||
expect(JSON.parse(output[0] || "")).toMatchObject({
|
||||
killed: {
|
||||
orphanedHubDaemons: process.platform === "win32" ? 0 : 1,
|
||||
},
|
||||
});
|
||||
killSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("doctor --fix kills unmanaged connector processes discovered from process args", async () => {
|
||||
const cwd = "/workspace";
|
||||
mockReadHubDiscovery.mockResolvedValue(undefined);
|
||||
mockProbeHubServer.mockResolvedValue(undefined);
|
||||
mockSpawnSync.mockImplementation((command: string, args?: string[]) => {
|
||||
if (
|
||||
command === "pgrep" &&
|
||||
Array.isArray(args) &&
|
||||
args[0] === "-fal" &&
|
||||
args[1] === "connect"
|
||||
) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: [
|
||||
"62001 cline connect slack --bot-token xoxb-secret -i",
|
||||
"62002 /usr/bin/some-tool connect slack --bot-token unrelated-secret",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
return { status: 1, stdout: "" };
|
||||
});
|
||||
const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true);
|
||||
|
||||
const output: string[] = [];
|
||||
const code = await runDoctorCommand(
|
||||
{ cwd, json: true, fix: true },
|
||||
{
|
||||
writeln: (text) => {
|
||||
output.push(text ?? "");
|
||||
},
|
||||
writeErr: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(killSpy).toHaveBeenCalledWith(62001, "SIGKILL");
|
||||
expect(killSpy).not.toHaveBeenCalledWith(62002, "SIGKILL");
|
||||
expect(JSON.parse(output[0] || "")).toMatchObject({
|
||||
before: {
|
||||
unmanagedConnectorProcesses: [{ pid: 62001, type: "slack" }],
|
||||
},
|
||||
killed: {
|
||||
unmanagedConnectorProcesses: 1,
|
||||
},
|
||||
});
|
||||
killSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("doctor --fix kills stale code sidecar processes", async () => {
|
||||
const cwd = "/workspace";
|
||||
mockReadHubDiscovery.mockResolvedValue(undefined);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { formatUptime } from "@cline/shared";
|
||||
import { Command } from "commander";
|
||||
import open from "open";
|
||||
import { listConnectorCatalog } from "../connectors/catalog";
|
||||
import { isProcessRunning } from "../connectors/common";
|
||||
import {
|
||||
type ActiveConnectorRecord,
|
||||
@@ -46,6 +47,17 @@ type SpawnedProcessRecord = {
|
||||
detached?: boolean;
|
||||
};
|
||||
|
||||
type UnmanagedConnectorProcessRecord = {
|
||||
pid: number;
|
||||
type: string;
|
||||
command: string;
|
||||
};
|
||||
|
||||
type HubDaemonProcessRecord = {
|
||||
pid: number;
|
||||
command: string;
|
||||
};
|
||||
|
||||
type DoctorStatus = {
|
||||
cwd: string;
|
||||
hubUrl?: string;
|
||||
@@ -54,10 +66,12 @@ type DoctorStatus = {
|
||||
hubStartedAt?: string;
|
||||
hubUptime?: string;
|
||||
listeningPids: number[];
|
||||
orphanedHubDaemonProcesses: HubDaemonProcessRecord[];
|
||||
hubStartupLocks: StartupArtifact[];
|
||||
staleCliPids: number[];
|
||||
staleSidecarPids: number[];
|
||||
activeConnectors: ActiveConnectorRecord[];
|
||||
unmanagedConnectorProcesses: UnmanagedConnectorProcessRecord[];
|
||||
recentSpawnedProcesses: SpawnedProcessRecord[];
|
||||
};
|
||||
|
||||
@@ -148,6 +162,144 @@ function listStaleCliPids(): number[] {
|
||||
.map((record) => record.pid);
|
||||
}
|
||||
|
||||
function listHubDaemonProcesses(): ProcessRecord[] {
|
||||
const patterns = ["cline-hub-daemon", "/hub/daemon/entry.ts"];
|
||||
const records = new Map<number, ProcessRecord>();
|
||||
for (const pattern of patterns) {
|
||||
for (const record of listMatchingProcesses(pattern)) {
|
||||
records.set(record.pid, record);
|
||||
}
|
||||
}
|
||||
return [...records.values()].sort((a, b) => a.pid - b.pid);
|
||||
}
|
||||
|
||||
function listOrphanedHubDaemonProcesses(options: {
|
||||
hubHealthy: boolean;
|
||||
hubPid?: number;
|
||||
listeningPids: number[];
|
||||
}): HubDaemonProcessRecord[] {
|
||||
const protectedPids = new Set<number>();
|
||||
if (options.hubHealthy) {
|
||||
if (options.hubPid) {
|
||||
protectedPids.add(options.hubPid);
|
||||
}
|
||||
for (const pid of options.listeningPids) {
|
||||
protectedPids.add(pid);
|
||||
}
|
||||
}
|
||||
return listHubDaemonProcesses()
|
||||
.filter((record) => !protectedPids.has(record.pid))
|
||||
.map((record) => ({
|
||||
pid: record.pid,
|
||||
command: redactSensitiveProcessArgs(record.command),
|
||||
}));
|
||||
}
|
||||
|
||||
function redactSensitiveProcessArgs(command: string): string {
|
||||
return command.replace(
|
||||
/(--[^\s=]*(?:token|secret|password|key)[^\s=]*)(=|\s+)(?:"[^"]*"|'[^']*'|[^\s]+)/gi,
|
||||
"$1$2[REDACTED]",
|
||||
);
|
||||
}
|
||||
|
||||
function processCommandTokens(command: string): string[] {
|
||||
return command
|
||||
.split(/\s+/)
|
||||
.map((token) => token.trim().replace(/^["']|["']$/g, ""))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function tokenBasename(token: string): string {
|
||||
const normalized = token.replace(/\\/g, "/");
|
||||
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
function isRuntimeLauncherToken(token: string): boolean {
|
||||
const basename = tokenBasename(token).toLowerCase();
|
||||
return basename === "bun" || basename === "node" || basename === "env";
|
||||
}
|
||||
|
||||
function isClineCliEntrypointToken(token: string): boolean {
|
||||
const normalized = token.replace(/\\/g, "/");
|
||||
const basename = tokenBasename(normalized).toLowerCase();
|
||||
return (
|
||||
basename === "cline" ||
|
||||
normalized.includes("/apps/cli/src/index.ts") ||
|
||||
normalized.includes("/apps/cli/dist/index.js") ||
|
||||
normalized.includes("/dist/cline")
|
||||
);
|
||||
}
|
||||
|
||||
function isClineCliPrefix(tokens: string[], connectIndex: number): boolean {
|
||||
const entrypointIndex = connectIndex - 1;
|
||||
if (entrypointIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
if (!isClineCliEntrypointToken(tokens[entrypointIndex] ?? "")) {
|
||||
return false;
|
||||
}
|
||||
if (entrypointIndex === 0) {
|
||||
return true;
|
||||
}
|
||||
const launcher = tokens[0];
|
||||
if (!launcher || !isRuntimeLauncherToken(launcher)) {
|
||||
return false;
|
||||
}
|
||||
return tokens
|
||||
.slice(1, entrypointIndex)
|
||||
.every((token) => token.startsWith("-"));
|
||||
}
|
||||
|
||||
function parseClineConnectorProcessType(
|
||||
command: string,
|
||||
connectorNames: Set<string>,
|
||||
): string | undefined {
|
||||
const tokens = processCommandTokens(command);
|
||||
for (let index = 0; index < tokens.length - 1; index += 1) {
|
||||
if (tokens[index] !== "connect" || !isClineCliPrefix(tokens, index)) {
|
||||
continue;
|
||||
}
|
||||
const type = tokens[index + 1]?.toLowerCase();
|
||||
if (type && connectorNames.has(type)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function listUnmanagedConnectorProcesses(
|
||||
activeConnectors: ActiveConnectorRecord[],
|
||||
): UnmanagedConnectorProcessRecord[] {
|
||||
const connectorNames = new Set(
|
||||
listConnectorCatalog().map((connector) => connector.name.toLowerCase()),
|
||||
);
|
||||
if (connectorNames.size === 0) {
|
||||
return [];
|
||||
}
|
||||
const activePids = new Set(activeConnectors.map((record) => record.pid));
|
||||
const records = new Map<number, UnmanagedConnectorProcessRecord>();
|
||||
for (const record of listMatchingProcesses("connect")) {
|
||||
if (activePids.has(record.pid)) {
|
||||
continue;
|
||||
}
|
||||
const type = parseClineConnectorProcessType(record.command, connectorNames);
|
||||
if (!type) {
|
||||
continue;
|
||||
}
|
||||
records.set(record.pid, {
|
||||
pid: record.pid,
|
||||
type,
|
||||
command: redactSensitiveProcessArgs(record.command),
|
||||
});
|
||||
}
|
||||
return [...records.values()].sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type.localeCompare(b.type);
|
||||
}
|
||||
return a.pid - b.pid;
|
||||
});
|
||||
}
|
||||
|
||||
function listStaleSidecarPids(): number[] {
|
||||
const patterns = [
|
||||
"/apps/examples/desktop-app/sidecar/index.ts",
|
||||
@@ -299,18 +451,29 @@ async function collectDoctorStatus(cwd: string): Promise<DoctorStatus> {
|
||||
: undefined;
|
||||
const current = health ?? discovery;
|
||||
const hubUptime = formatHubUptimeFromStartedAt(health?.startedAt);
|
||||
const activeConnectors = listActiveConnectors();
|
||||
const listeningPids = listListeningPids(current?.port);
|
||||
const hubHealthy = !!health?.url;
|
||||
const hubPid = current?.pid;
|
||||
return {
|
||||
cwd,
|
||||
hubUrl: current?.url,
|
||||
hubHealthy: !!health?.url,
|
||||
hubPid: current?.pid,
|
||||
hubHealthy,
|
||||
hubPid,
|
||||
hubStartedAt: health?.startedAt,
|
||||
hubUptime,
|
||||
listeningPids: listListeningPids(current?.port),
|
||||
listeningPids,
|
||||
orphanedHubDaemonProcesses: listOrphanedHubDaemonProcesses({
|
||||
hubHealthy,
|
||||
hubPid,
|
||||
listeningPids,
|
||||
}),
|
||||
hubStartupLocks: listHubStartupLocks(cwd),
|
||||
staleCliPids: listStaleCliPids(),
|
||||
staleSidecarPids: listStaleSidecarPids(),
|
||||
activeConnectors: listActiveConnectors(),
|
||||
activeConnectors,
|
||||
unmanagedConnectorProcesses:
|
||||
listUnmanagedConnectorProcesses(activeConnectors),
|
||||
recentSpawnedProcesses: readRecentSpawnedProcesses(),
|
||||
};
|
||||
}
|
||||
@@ -355,6 +518,16 @@ function formatActiveConnector(record: ActiveConnectorRecord): string {
|
||||
return pieces.join(" | ");
|
||||
}
|
||||
|
||||
function formatUnmanagedConnectorProcess(
|
||||
record: UnmanagedConnectorProcessRecord,
|
||||
): string {
|
||||
return [record.type, `pid=${record.pid}`, record.command].join(" | ");
|
||||
}
|
||||
|
||||
function formatHubDaemonProcess(record: HubDaemonProcessRecord): string {
|
||||
return [`pid=${record.pid}`, record.command].join(" | ");
|
||||
}
|
||||
|
||||
function killPids(pids: number[]): number {
|
||||
let killed = 0;
|
||||
for (const pid of pids) {
|
||||
@@ -388,6 +561,14 @@ export async function runDoctorCommand(
|
||||
);
|
||||
writeln(`hub uptime ${c.dim}${before.hubUptime ?? "n/a"}${c.reset}`);
|
||||
writeln(formatPidList("hub listeners", before.listeningPids));
|
||||
if (before.orphanedHubDaemonProcesses.length === 0) {
|
||||
writeln(`orphaned hub daemons ${c.dim}0${c.reset}`);
|
||||
} else {
|
||||
writeln("orphaned hub daemons:");
|
||||
for (const record of before.orphanedHubDaemonProcesses) {
|
||||
writeln(`- ${c.dim}${formatHubDaemonProcess(record)}${c.reset}`);
|
||||
}
|
||||
}
|
||||
writeln(
|
||||
formatPidList(
|
||||
"hub startup locks",
|
||||
@@ -404,6 +585,17 @@ export async function runDoctorCommand(
|
||||
writeln(`- ${c.dim}${formatActiveConnector(record)}${c.reset}`);
|
||||
}
|
||||
}
|
||||
if (before.unmanagedConnectorProcesses.length > 0) {
|
||||
writeln("unmanaged connector processes:");
|
||||
for (const record of before.unmanagedConnectorProcesses) {
|
||||
writeln(
|
||||
`- ${c.dim}${formatUnmanagedConnectorProcess(record)}${c.reset}`,
|
||||
);
|
||||
}
|
||||
io.writeln(
|
||||
"\nThese connector-looking processes do not have valid connector state files. Run `cline doctor fix` to stop them.",
|
||||
);
|
||||
}
|
||||
if (verbose && before.recentSpawnedProcesses.length > 0) {
|
||||
writeln("recent spawned processes:");
|
||||
for (const record of before.recentSpawnedProcesses) {
|
||||
@@ -412,11 +604,12 @@ export async function runDoctorCommand(
|
||||
}
|
||||
if (
|
||||
before.listeningPids.length > 0 ||
|
||||
before.orphanedHubDaemonProcesses.length > 0 ||
|
||||
before.staleCliPids.length > 0 ||
|
||||
before.staleSidecarPids.length > 0
|
||||
) {
|
||||
io.writeln(
|
||||
"\nRun `cline doctor fix` to kill all stale local processes, including stale sidecars.",
|
||||
"\nRun `cline doctor fix` to kill all stale local processes, including stale hubs.",
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
@@ -431,8 +624,21 @@ export async function runDoctorCommand(
|
||||
const killedHub = gracefullyStoppedHub
|
||||
? 0
|
||||
: killPids(refreshedAfterGracefulStop.listeningPids);
|
||||
const orphanedHubDaemonTargets = [
|
||||
...before.orphanedHubDaemonProcesses,
|
||||
...refreshedAfterGracefulStop.orphanedHubDaemonProcesses,
|
||||
]
|
||||
.map((record) => record.pid)
|
||||
.filter(
|
||||
(pid, index, pids) =>
|
||||
pids.indexOf(pid) === index &&
|
||||
!refreshedAfterGracefulStop.listeningPids.includes(pid),
|
||||
);
|
||||
const killedOrphanedHubDaemons = killPids(orphanedHubDaemonTargets);
|
||||
const staleCliTargets = before.staleCliPids.filter(
|
||||
(pid) => !refreshedAfterGracefulStop.listeningPids.includes(pid),
|
||||
(pid) =>
|
||||
!refreshedAfterGracefulStop.listeningPids.includes(pid) &&
|
||||
!orphanedHubDaemonTargets.includes(pid),
|
||||
);
|
||||
const killedCli = killPids(staleCliTargets);
|
||||
const staleSidecarTargets = before.staleSidecarPids.filter(
|
||||
@@ -441,6 +647,15 @@ export async function runDoctorCommand(
|
||||
!staleCliTargets.includes(pid),
|
||||
);
|
||||
const killedSidecars = killPids(staleSidecarTargets);
|
||||
const unmanagedConnectorTargets = before.unmanagedConnectorProcesses
|
||||
.map((record) => record.pid)
|
||||
.filter(
|
||||
(pid) =>
|
||||
!refreshedAfterGracefulStop.listeningPids.includes(pid) &&
|
||||
!staleCliTargets.includes(pid) &&
|
||||
!staleSidecarTargets.includes(pid),
|
||||
);
|
||||
const killedUnmanagedConnectors = killPids(unmanagedConnectorTargets);
|
||||
const stoppedConnectors = await stopAllConnectors({
|
||||
writeln: () => {},
|
||||
writeErr: () => {},
|
||||
@@ -459,8 +674,10 @@ export async function runDoctorCommand(
|
||||
after,
|
||||
killed: {
|
||||
hubListeners: killedHub,
|
||||
orphanedHubDaemons: killedOrphanedHubDaemons,
|
||||
cliProcesses: killedCli,
|
||||
sidecarProcesses: killedSidecars,
|
||||
unmanagedConnectorProcesses: killedUnmanagedConnectors,
|
||||
connectorProcesses: stoppedConnectors.stoppedProcesses,
|
||||
connectorSessions: stoppedConnectors.stoppedSessions,
|
||||
hubStartupLocks: clearedArtifacts.startupLocks,
|
||||
@@ -471,8 +688,14 @@ export async function runDoctorCommand(
|
||||
return 0;
|
||||
}
|
||||
writeln(`killed hub listeners ${c.dim}${killedHub}${c.reset}`);
|
||||
writeln(
|
||||
`killed orphaned hub daemons ${c.dim}${killedOrphanedHubDaemons}${c.reset}`,
|
||||
);
|
||||
writeln(`killed cli processes ${c.dim}${killedCli}${c.reset}`);
|
||||
writeln(`killed sidecar processes ${c.dim}${killedSidecars}${c.reset}`);
|
||||
writeln(
|
||||
`killed unmanaged connector processes ${c.dim}${killedUnmanagedConnectors}${c.reset}`,
|
||||
);
|
||||
writeln(
|
||||
`stopped connector processes ${c.dim}${stoppedConnectors.stoppedProcesses}${c.reset}`,
|
||||
);
|
||||
@@ -487,6 +710,12 @@ export async function runDoctorCommand(
|
||||
);
|
||||
writeln(`hub healthy after fix: ${after.hubHealthy ? "yes" : "no"}`);
|
||||
writeln(formatPidList("remaining hub listeners", after.listeningPids));
|
||||
writeln(
|
||||
formatPidList(
|
||||
"remaining orphaned hub daemons",
|
||||
after.orphanedHubDaemonProcesses.map((record) => record.pid),
|
||||
),
|
||||
);
|
||||
writeln(
|
||||
formatPidList(
|
||||
"remaining hub startup locks",
|
||||
@@ -495,6 +724,12 @@ export async function runDoctorCommand(
|
||||
);
|
||||
writeln(formatPidList("remaining cli processes", after.staleCliPids));
|
||||
writeln(formatPidList("remaining sidecar processes", after.staleSidecarPids));
|
||||
writeln(
|
||||
formatPidList(
|
||||
"remaining unmanaged connector processes",
|
||||
after.unmanagedConnectorProcesses.map((record) => record.pid),
|
||||
),
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
@@ -789,11 +788,7 @@ class DiscordConnector extends ConnectorBase<
|
||||
"--hook-command <command>",
|
||||
"Run a shell command for connector events",
|
||||
)
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
)
|
||||
.option("--rpc-address <host:port>", "RPC address")
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
.option(
|
||||
@@ -888,9 +883,7 @@ class DiscordConnector extends ConnectorBase<
|
||||
interactive: Boolean(opts.interactive),
|
||||
enableTools: Boolean(opts.enableTools),
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
opts.rpcAddress?.trim() || process.env.CLINE_RPC_ADDRESS?.trim(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
|
||||
@@ -15,7 +15,6 @@ import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
@@ -250,11 +249,7 @@ class GoogleChatConnector extends ConnectorBase<
|
||||
"--hook-command <command>",
|
||||
"Run a shell command for connector events",
|
||||
)
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
)
|
||||
.option("--rpc-address <host:port>", "RPC address")
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
.option("--base-url <url>", "Public base URL for webhook configuration")
|
||||
@@ -317,9 +312,7 @@ class GoogleChatConnector extends ConnectorBase<
|
||||
interactive: Boolean(opts.interactive),
|
||||
enableTools: Boolean(opts.enableTools),
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
opts.rpcAddress?.trim() || process.env.CLINE_RPC_ADDRESS?.trim(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
|
||||
@@ -11,7 +11,6 @@ import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
@@ -310,11 +309,7 @@ class LinearConnector extends ConnectorBase<
|
||||
"--hook-command <command>",
|
||||
"Run a shell command for connector events",
|
||||
)
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
)
|
||||
.option("--rpc-address <host:port>", "RPC address")
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
.option("--base-url <url>", "Public base URL for webhook configuration")
|
||||
@@ -397,9 +392,7 @@ class LinearConnector extends ConnectorBase<
|
||||
interactive: Boolean(opts.interactive),
|
||||
enableTools: Boolean(opts.enableTools),
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
opts.rpcAddress?.trim() || process.env.CLINE_RPC_ADDRESS?.trim(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ConnectSlackOptions } from "@cline/shared";
|
||||
import { type Message, ThreadImpl } from "chat";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { __test__, slackConnector } from "./slack";
|
||||
|
||||
const parseSlackArgs = (rawArgs: string[]): ConnectSlackOptions =>
|
||||
@@ -10,6 +10,16 @@ const parseSlackArgs = (rawArgs: string[]): ConnectSlackOptions =>
|
||||
}
|
||||
).parseArgs(rawArgs);
|
||||
|
||||
const originalClineRpcAddress = process.env.CLINE_RPC_ADDRESS;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalClineRpcAddress === undefined) {
|
||||
delete process.env.CLINE_RPC_ADDRESS;
|
||||
} else {
|
||||
process.env.CLINE_RPC_ADDRESS = originalClineRpcAddress;
|
||||
}
|
||||
});
|
||||
|
||||
describe("slack binding lookup", () => {
|
||||
const participantKey = __test__.buildSlackParticipantKey("T123", "U123");
|
||||
|
||||
@@ -41,6 +51,7 @@ describe("slack binding lookup", () => {
|
||||
|
||||
it("uses socket mode when Slack args omit a base URL", () => {
|
||||
const previousBaseUrl = process.env.BASE_URL;
|
||||
delete process.env.CLINE_RPC_ADDRESS;
|
||||
delete process.env.BASE_URL;
|
||||
let options: ConnectSlackOptions;
|
||||
try {
|
||||
@@ -61,6 +72,28 @@ describe("slack binding lookup", () => {
|
||||
expect(options.connectionMode).toBe("socket");
|
||||
expect(options.baseUrl).toBeUndefined();
|
||||
expect(options.appToken).toBe("xapp-token");
|
||||
expect(options.rpcAddress).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses an explicit RPC address only when configured", () => {
|
||||
delete process.env.CLINE_RPC_ADDRESS;
|
||||
|
||||
expect(
|
||||
parseSlackArgs([
|
||||
"--bot-token",
|
||||
"xoxb-token",
|
||||
"--app-token",
|
||||
"xapp-token",
|
||||
"--rpc-address",
|
||||
"127.0.0.1:4317",
|
||||
]).rpcAddress,
|
||||
).toBe("127.0.0.1:4317");
|
||||
|
||||
process.env.CLINE_RPC_ADDRESS = "127.0.0.1:4318";
|
||||
expect(
|
||||
parseSlackArgs(["--bot-token", "xoxb-token", "--app-token", "xapp-token"])
|
||||
.rpcAddress,
|
||||
).toBe("127.0.0.1:4318");
|
||||
});
|
||||
|
||||
it("falls back to channel identity when a restarted connector gets a new thread id", () => {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
@@ -478,11 +477,7 @@ class SlackConnector extends ConnectorBase<
|
||||
"--hook-command <command>",
|
||||
"Run a shell command for connector events",
|
||||
)
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
)
|
||||
.option("--rpc-address <host:port>", "RPC address")
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
.option(
|
||||
@@ -590,9 +585,7 @@ class SlackConnector extends ConnectorBase<
|
||||
interactive: Boolean(opts.interactive),
|
||||
enableTools: Boolean(opts.enableTools),
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
opts.rpcAddress?.trim() || process.env.CLINE_RPC_ADDRESS?.trim(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
|
||||
@@ -15,7 +15,6 @@ import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
@@ -440,11 +439,7 @@ class TelegramConnector extends ConnectorBase<
|
||||
"--hook-command <command>",
|
||||
"Run a shell command for connector events",
|
||||
)
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
)
|
||||
.option("--rpc-address <host:port>", "RPC address")
|
||||
.addHelpText(
|
||||
"after",
|
||||
[
|
||||
@@ -506,9 +501,7 @@ class TelegramConnector extends ConnectorBase<
|
||||
interactive: Boolean(opts.interactive),
|
||||
enableTools: opts.tools !== false,
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
opts.rpcAddress?.trim() || process.env.CLINE_RPC_ADDRESS?.trim(),
|
||||
hookCommand: allowedUserId
|
||||
? buildTelegramAllowedUserHookCommand(
|
||||
normalizeAllowedTelegramUserId(allowedUserId),
|
||||
|
||||
@@ -15,7 +15,6 @@ import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
@@ -293,11 +292,7 @@ class WhatsAppConnector extends ConnectorBase<
|
||||
"--hook-command <command>",
|
||||
"Run a shell command for connector events",
|
||||
)
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
)
|
||||
.option("--rpc-address <host:port>", "RPC address")
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
.option("--base-url <url>", "Public base URL for webhook configuration")
|
||||
@@ -365,9 +360,7 @@ class WhatsAppConnector extends ConnectorBase<
|
||||
interactive: Boolean(opts.interactive),
|
||||
enableTools: Boolean(opts.enableTools),
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
opts.rpcAddress?.trim() || process.env.CLINE_RPC_ADDRESS?.trim(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
|
||||
@@ -101,23 +101,17 @@ describe("ensureDetachedHubServer", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("lets the daemon bind port 0 when the configured endpoint is occupied", async () => {
|
||||
readHubDiscovery.mockResolvedValue(undefined);
|
||||
probeHubServer
|
||||
.mockResolvedValueOnce({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
buildId: "current-build",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
url: "ws://127.0.0.1:5555/hub",
|
||||
buildId: "current-build",
|
||||
});
|
||||
verifyHubConnection.mockResolvedValueOnce(true);
|
||||
it("starts the daemon on the resolved default hub port", async () => {
|
||||
readHubDiscovery.mockResolvedValueOnce(undefined).mockResolvedValueOnce({
|
||||
url: "ws://127.0.0.1:5555/hub",
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
buildId: "current-build",
|
||||
authToken: "new-token",
|
||||
});
|
||||
probeHubServer.mockResolvedValueOnce(undefined).mockResolvedValueOnce({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
buildId: "current-build",
|
||||
});
|
||||
verifyHubConnection.mockResolvedValueOnce(true);
|
||||
|
||||
const { ensureDetachedHubServer } = await import(".");
|
||||
const result = await ensureDetachedHubServer("/workspace");
|
||||
@@ -129,12 +123,12 @@ describe("ensureDetachedHubServer", () => {
|
||||
| undefined;
|
||||
|
||||
expect(result).toEqual({
|
||||
url: "ws://127.0.0.1:5555/hub",
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
authToken: "new-token",
|
||||
});
|
||||
expect(spawn).toHaveBeenCalledOnce();
|
||||
expect(spawnArgs).toContain("--port");
|
||||
expect(spawnArgs).toContain("0");
|
||||
expect(spawnArgs).toContain("25463");
|
||||
expect(spawnOptions?.env?.[CLINE_RUN_AS_HUB_DAEMON_ENV]).toBe("1");
|
||||
});
|
||||
|
||||
|
||||
@@ -206,8 +206,6 @@ export function prewarmDetachedHubServer(
|
||||
return;
|
||||
}
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const hasExplicitPort =
|
||||
endpoint.port !== undefined || !!process.env.CLINE_HUB_PORT?.trim();
|
||||
const resolvedEndpoint = resolveHubEndpointOptions(endpoint);
|
||||
const expectedUrl = createHubServerUrl(
|
||||
resolvedEndpoint.host,
|
||||
@@ -240,12 +238,7 @@ export function prewarmDetachedHubServer(
|
||||
if (expected?.url) {
|
||||
await retireIncompatibleHub(expected, owner.discoveryPath);
|
||||
}
|
||||
const shouldUseFallbackPort =
|
||||
!hasExplicitPort && resolvedEndpoint.port !== 0;
|
||||
const spawnEndpoint = shouldUseFallbackPort
|
||||
? { ...resolvedEndpoint, port: 0 }
|
||||
: resolvedEndpoint;
|
||||
await spawnDetachedHubServerWithRetry(workspaceRoot, spawnEndpoint);
|
||||
await spawnDetachedHubServerWithRetry(workspaceRoot, resolvedEndpoint);
|
||||
})
|
||||
.catch(() => {
|
||||
// best-effort prewarm only
|
||||
@@ -267,9 +260,6 @@ export async function ensureDetachedHubServer(
|
||||
endpointOverrides.port !== undefined ||
|
||||
endpointOverrides.pathname !== undefined ||
|
||||
!!process.env.CLINE_HUB_PORT?.trim();
|
||||
const hasExplicitPort =
|
||||
endpointOverrides.port !== undefined ||
|
||||
!!process.env.CLINE_HUB_PORT?.trim();
|
||||
const endpoint = resolveHubEndpointOptions(endpointOverrides);
|
||||
const expectedUrl = createHubServerUrl(
|
||||
endpoint.host,
|
||||
@@ -312,11 +302,7 @@ export async function ensureDetachedHubServer(
|
||||
if (expected?.url) {
|
||||
await retireIncompatibleHub(expected, owner.discoveryPath);
|
||||
}
|
||||
const shouldUseFallbackPort = !hasExplicitPort && endpoint.port !== 0;
|
||||
const spawnEndpoint = shouldUseFallbackPort
|
||||
? { ...endpoint, port: 0 }
|
||||
: endpoint;
|
||||
await spawnDetachedHubServerWithRetry(workspaceRoot, spawnEndpoint);
|
||||
await spawnDetachedHubServerWithRetry(workspaceRoot, endpoint);
|
||||
const deadline = Date.now() + HUB_STARTUP_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
const nextDiscovery = await readHubDiscovery(owner.discoveryPath);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { resolveSharedHubOwnerContext } from "./workspace";
|
||||
|
||||
const envSnapshot = {
|
||||
CLINE_BUILD_ENV: process.env.CLINE_BUILD_ENV,
|
||||
CLINE_DATA_DIR: process.env.CLINE_DATA_DIR,
|
||||
CLINE_HUB_DISCOVERY_PATH: process.env.CLINE_HUB_DISCOVERY_PATH,
|
||||
};
|
||||
|
||||
function restoreEnv(): void {
|
||||
if (envSnapshot.CLINE_BUILD_ENV === undefined) {
|
||||
delete process.env.CLINE_BUILD_ENV;
|
||||
} else {
|
||||
process.env.CLINE_BUILD_ENV = envSnapshot.CLINE_BUILD_ENV;
|
||||
}
|
||||
if (envSnapshot.CLINE_DATA_DIR === undefined) {
|
||||
delete process.env.CLINE_DATA_DIR;
|
||||
} else {
|
||||
process.env.CLINE_DATA_DIR = envSnapshot.CLINE_DATA_DIR;
|
||||
}
|
||||
if (envSnapshot.CLINE_HUB_DISCOVERY_PATH === undefined) {
|
||||
delete process.env.CLINE_HUB_DISCOVERY_PATH;
|
||||
} else {
|
||||
process.env.CLINE_HUB_DISCOVERY_PATH = envSnapshot.CLINE_HUB_DISCOVERY_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
describe("resolveSharedHubOwnerContext", () => {
|
||||
afterEach(() => {
|
||||
restoreEnv();
|
||||
});
|
||||
|
||||
it("uses separate discovery owners for production and development hubs", () => {
|
||||
delete process.env.CLINE_HUB_DISCOVERY_PATH;
|
||||
process.env.CLINE_DATA_DIR = "/tmp/cline-data";
|
||||
|
||||
process.env.CLINE_BUILD_ENV = "production";
|
||||
const production = resolveSharedHubOwnerContext();
|
||||
|
||||
process.env.CLINE_BUILD_ENV = "development";
|
||||
const development = resolveSharedHubOwnerContext();
|
||||
|
||||
expect(development.ownerId).not.toBe(production.ownerId);
|
||||
expect(development.discoveryPath).not.toBe(production.discoveryPath);
|
||||
});
|
||||
|
||||
it("honors an explicit shared owner label", () => {
|
||||
delete process.env.CLINE_HUB_DISCOVERY_PATH;
|
||||
process.env.CLINE_DATA_DIR = "/tmp/cline-data";
|
||||
process.env.CLINE_BUILD_ENV = "development";
|
||||
|
||||
const development = resolveSharedHubOwnerContext("shared:custom");
|
||||
|
||||
process.env.CLINE_BUILD_ENV = "production";
|
||||
const production = resolveSharedHubOwnerContext("shared:custom");
|
||||
|
||||
expect(development).toEqual(production);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,12 @@
|
||||
import { resolveClineBuildEnv } from "@cline/shared";
|
||||
import { normalizeWorkspacePath } from "../../services/workspace/workspace-manifest";
|
||||
import { type HubOwnerContext, resolveHubOwnerContext } from ".";
|
||||
|
||||
const DEFAULT_SHARED_HUB_OWNER_LABEL = "shared:cline";
|
||||
const SHARED_HUB_OWNER_LABEL_PREFIX = "shared:cline";
|
||||
|
||||
function resolveDefaultSharedHubOwnerLabel(): string {
|
||||
return `${SHARED_HUB_OWNER_LABEL_PREFIX}:${resolveClineBuildEnv()}`;
|
||||
}
|
||||
|
||||
export function resolveWorkspaceHubOwnerContext(
|
||||
workspaceRoot: string,
|
||||
@@ -13,7 +18,7 @@ export function resolveWorkspaceHubOwnerContext(
|
||||
}
|
||||
|
||||
export function resolveSharedHubOwnerContext(
|
||||
label = DEFAULT_SHARED_HUB_OWNER_LABEL,
|
||||
label = resolveDefaultSharedHubOwnerLabel(),
|
||||
): HubOwnerContext {
|
||||
return resolveHubOwnerContext(label);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export type ConnectWhatsAppOptions = {
|
||||
interactive: boolean;
|
||||
maxIterations?: number;
|
||||
enableTools: boolean;
|
||||
rpcAddress: string;
|
||||
rpcAddress?: string;
|
||||
hookCommand?: string;
|
||||
port: number;
|
||||
host: string;
|
||||
@@ -44,7 +44,7 @@ export type ConnectTelegramOptions = {
|
||||
interactive: boolean;
|
||||
maxIterations?: number;
|
||||
enableTools: boolean;
|
||||
rpcAddress: string;
|
||||
rpcAddress?: string;
|
||||
hookCommand?: string;
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ export type ConnectSlackOptions = {
|
||||
interactive: boolean;
|
||||
maxIterations?: number;
|
||||
enableTools: boolean;
|
||||
rpcAddress: string;
|
||||
rpcAddress?: string;
|
||||
hookCommand?: string;
|
||||
port: number;
|
||||
host: string;
|
||||
@@ -109,7 +109,7 @@ export type ConnectDiscordOptions = {
|
||||
interactive: boolean;
|
||||
maxIterations?: number;
|
||||
enableTools: boolean;
|
||||
rpcAddress: string;
|
||||
rpcAddress?: string;
|
||||
hookCommand?: string;
|
||||
port: number;
|
||||
host: string;
|
||||
@@ -137,7 +137,7 @@ export type ConnectGoogleChatOptions = {
|
||||
interactive: boolean;
|
||||
maxIterations?: number;
|
||||
enableTools: boolean;
|
||||
rpcAddress: string;
|
||||
rpcAddress?: string;
|
||||
hookCommand?: string;
|
||||
port: number;
|
||||
host: string;
|
||||
@@ -173,7 +173,7 @@ export type ConnectLinearOptions = {
|
||||
interactive: boolean;
|
||||
maxIterations?: number;
|
||||
enableTools: boolean;
|
||||
rpcAddress: string;
|
||||
rpcAddress?: string;
|
||||
hookCommand?: string;
|
||||
port: number;
|
||||
host: string;
|
||||
|
||||
Reference in New Issue
Block a user