@hivemind-os/collective-shim 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/dist/index.js +66 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge.ts +20 -1
- package/src/daemon-launcher.ts +49 -0
- package/src/ipc-client.ts +18 -4
- package/tests/bridge.test.ts +203 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -6,9 +6,9 @@ $ tsup
|
|
|
6
6
|
[34mCLI[39m Target: es2022
|
|
7
7
|
[34mCLI[39m Cleaning output folder
|
|
8
8
|
[34mESM[39m Build start
|
|
9
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
10
|
-
[32mESM[39m [1mdist/index.js.map [22m[
|
|
11
|
-
[32mESM[39m ⚡️ Build success in
|
|
9
|
+
[32mESM[39m [1mdist/index.js [22m[32m13.19 KB[39m
|
|
10
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m26.04 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 38ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 2285ms
|
|
14
14
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m20.00 B[39m
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { promisify } from "util";
|
|
|
8
8
|
|
|
9
9
|
// src/daemon-launcher.ts
|
|
10
10
|
import { spawn, spawnSync } from "child_process";
|
|
11
|
+
import { readFileSync, unlinkSync } from "fs";
|
|
11
12
|
import { createRequire } from "module";
|
|
12
13
|
import net from "net";
|
|
13
14
|
import { homedir, userInfo } from "os";
|
|
@@ -15,6 +16,7 @@ import { resolve } from "path";
|
|
|
15
16
|
import { setTimeout as delay } from "timers/promises";
|
|
16
17
|
import { fileURLToPath } from "url";
|
|
17
18
|
var require2 = createRequire(import.meta.url);
|
|
19
|
+
var { version: SHIM_VERSION } = require2("../package.json");
|
|
18
20
|
var LEGACY_WINDOWS_PIPE_PATH = "\\\\.\\pipe\\hivemind-collective";
|
|
19
21
|
function getDefaultIpcPath() {
|
|
20
22
|
return process.env.COLLECTIVE_IPC_PATH ?? (process.platform === "win32" ? `${LEGACY_WINDOWS_PIPE_PATH}-${sanitizePipeSegment(getCurrentUsername())}` : resolve(homedir(), ".hivemind-os/collective", "mesh.sock"));
|
|
@@ -101,10 +103,53 @@ function sanitizePipeSegment(value) {
|
|
|
101
103
|
const sanitized = leaf.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
102
104
|
return sanitized || "unknown-user";
|
|
103
105
|
}
|
|
106
|
+
async function stopDaemon(options) {
|
|
107
|
+
const { pidFile, ipcPath, timeoutMs = 1e4 } = options;
|
|
108
|
+
let pid;
|
|
109
|
+
try {
|
|
110
|
+
pid = parseInt(readFileSync(pidFile, "utf8").trim(), 10);
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (isNaN(pid)) {
|
|
115
|
+
try {
|
|
116
|
+
unlinkSync(pidFile);
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, "SIGTERM");
|
|
123
|
+
} catch {
|
|
124
|
+
try {
|
|
125
|
+
unlinkSync(pidFile);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const deadline = Date.now() + timeoutMs;
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
if (!await isDaemonRunning(ipcPath)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await delay(200);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
process.kill(pid, "SIGKILL");
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
unlinkSync(pidFile);
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
104
146
|
|
|
105
147
|
// src/ipc-client.ts
|
|
148
|
+
import { createRequire as createRequire2 } from "module";
|
|
106
149
|
import { randomUUID } from "crypto";
|
|
107
150
|
import net2 from "net";
|
|
151
|
+
var require3 = createRequire2(import.meta.url);
|
|
152
|
+
var { version: SHIM_VERSION2 } = require3("../package.json");
|
|
108
153
|
var IpcClient = class {
|
|
109
154
|
constructor(ipcPath) {
|
|
110
155
|
this.ipcPath = ipcPath;
|
|
@@ -140,7 +185,7 @@ var IpcClient = class {
|
|
|
140
185
|
});
|
|
141
186
|
socket.on("error", () => void 0);
|
|
142
187
|
this.helloId = `shim-hello-${randomUUID()}`;
|
|
143
|
-
|
|
188
|
+
return new Promise((resolve2, reject) => {
|
|
144
189
|
this.helloWaiter = { resolve: resolve2, reject };
|
|
145
190
|
this.send({
|
|
146
191
|
jsonrpc: "2.0",
|
|
@@ -149,6 +194,7 @@ var IpcClient = class {
|
|
|
149
194
|
params: {
|
|
150
195
|
appName,
|
|
151
196
|
pid: process.pid,
|
|
197
|
+
shimVersion: SHIM_VERSION2,
|
|
152
198
|
...process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}
|
|
153
199
|
}
|
|
154
200
|
});
|
|
@@ -194,7 +240,11 @@ var IpcClient = class {
|
|
|
194
240
|
if (message.error && typeof message.error === "object") {
|
|
195
241
|
waiter.reject(new Error(String(message.error.message ?? "shim_hello failed")));
|
|
196
242
|
} else {
|
|
197
|
-
|
|
243
|
+
const result = message.result ?? {};
|
|
244
|
+
waiter.resolve({
|
|
245
|
+
daemonVersion: typeof result.daemonVersion === "string" ? result.daemonVersion : void 0,
|
|
246
|
+
connectionId: typeof result.connectionId === "string" ? result.connectionId : void 0
|
|
247
|
+
});
|
|
198
248
|
}
|
|
199
249
|
} else {
|
|
200
250
|
for (const handler of this.messageHandlers) {
|
|
@@ -238,6 +288,7 @@ async function createBridge(options = {}) {
|
|
|
238
288
|
const stderr = options.stderr ?? process.stderr;
|
|
239
289
|
const exit = options.exit ?? ((code) => process.exit(code));
|
|
240
290
|
const ensureDaemon = options.ensureDaemon ?? ensureDaemonRunning;
|
|
291
|
+
const stopDaemonFn = options.stopDaemon ?? stopDaemon;
|
|
241
292
|
const launcherOptions = {
|
|
242
293
|
ipcPath: options.ipcPath ?? getDefaultIpcPath(),
|
|
243
294
|
pidFile: options.pidFile ?? getDefaultPidFile(),
|
|
@@ -269,6 +320,7 @@ async function createBridge(options = {}) {
|
|
|
269
320
|
}
|
|
270
321
|
}
|
|
271
322
|
};
|
|
323
|
+
let upgraded = false;
|
|
272
324
|
const connect = async () => {
|
|
273
325
|
await ensureDaemon(launcherOptions);
|
|
274
326
|
const deadline = Date.now() + launcherOptions.startupTimeoutMs;
|
|
@@ -285,7 +337,18 @@ async function createBridge(options = {}) {
|
|
|
285
337
|
}
|
|
286
338
|
});
|
|
287
339
|
try {
|
|
288
|
-
await next.connect(appName);
|
|
340
|
+
const hello = await next.connect(appName);
|
|
341
|
+
if (!upgraded && hello.daemonVersion && hello.daemonVersion !== SHIM_VERSION) {
|
|
342
|
+
upgraded = true;
|
|
343
|
+
stderr.write(
|
|
344
|
+
`mesh-shim: Upgrading daemon from v${hello.daemonVersion} to v${SHIM_VERSION}
|
|
345
|
+
`
|
|
346
|
+
);
|
|
347
|
+
next.close();
|
|
348
|
+
await stopDaemonFn({ pidFile: launcherOptions.pidFile, ipcPath: launcherOptions.ipcPath });
|
|
349
|
+
await ensureDaemon(launcherOptions);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
289
352
|
client = next;
|
|
290
353
|
flushPending();
|
|
291
354
|
return;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bridge.ts","../src/daemon-launcher.ts","../src/ipc-client.ts","../src/index.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { basename } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { promisify } from 'node:util';\n\nimport {\n ensureDaemonRunning,\n getDefaultIpcPath,\n getDefaultPidFile,\n resolveDaemonBin,\n type LauncherOptions,\n} from './daemon-launcher.js';\nimport { IpcClient } from './ipc-client.js';\n\nconst execFileAsync = promisify(execFile);\n\nexport interface BridgeOptions {\n ipcPath?: string;\n pidFile?: string;\n daemonBin?: string;\n startupTimeoutMs?: number;\n appName?: string;\n stdin?: NodeJS.ReadableStream;\n stdout?: NodeJS.WritableStream;\n stderr?: NodeJS.WritableStream;\n exit?: (code: number) => void;\n ensureDaemon?: (options: LauncherOptions) => Promise<void>;\n}\n\nexport interface BridgeHandle {\n close(): void;\n}\n\nexport async function startShim(): Promise<void> {\n await createBridge();\n}\n\nexport async function createBridge(options: BridgeOptions = {}): Promise<BridgeHandle> {\n const stdin = options.stdin ?? process.stdin;\n const stdout = options.stdout ?? process.stdout;\n const stderr = options.stderr ?? process.stderr;\n const exit = options.exit ?? ((code: number) => process.exit(code));\n const ensureDaemon = options.ensureDaemon ?? ensureDaemonRunning;\n const launcherOptions: LauncherOptions = {\n ipcPath: options.ipcPath ?? getDefaultIpcPath(),\n pidFile: options.pidFile ?? getDefaultPidFile(),\n daemonBin: options.daemonBin ?? resolveDaemonBin(),\n startupTimeoutMs: options.startupTimeoutMs ?? 30_000,\n };\n const appName = options.appName ?? (await guessAppName());\n\n let client: IpcClient | undefined;\n let stdinBuffer = '';\n let closing = false;\n let reconnecting: Promise<void> | undefined;\n const pending: string[] = [];\n\n const writeError = (message: string) => {\n stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32000, message } })}\\n`);\n stderr.write(`mesh-shim: ${message}\\n`);\n };\n\n const flushPending = () => {\n if (!client) {\n return;\n }\n\n while (pending.length > 0) {\n try {\n client.sendRaw(pending[0] as string);\n pending.shift();\n } catch {\n return;\n }\n }\n };\n\n const connect = async () => {\n await ensureDaemon(launcherOptions);\n const deadline = Date.now() + launcherOptions.startupTimeoutMs;\n while (true) {\n const next = new IpcClient(launcherOptions.ipcPath);\n next.onMessage((message) => {\n stdout.write(`${JSON.stringify(message)}\\n`);\n });\n next.onClose(() => {\n if (!closing && client === next) {\n client = undefined;\n void reconnect();\n }\n });\n\n try {\n await next.connect(appName);\n client = next;\n flushPending();\n return;\n } catch (error) {\n next.close();\n if (Date.now() >= deadline) {\n throw error;\n }\n await delay(200);\n }\n }\n };\n\n const reconnect = async () => {\n if (reconnecting) {\n return reconnecting;\n }\n\n reconnecting = (async () => {\n try {\n await connect();\n } catch (error) {\n if (!closing) {\n closing = true;\n client?.close();\n writeError(`Unable to reach mesh daemon: ${(error as Error).message}`);\n exit(1);\n }\n } finally {\n reconnecting = undefined;\n }\n })();\n\n return reconnecting;\n };\n\n try {\n await connect();\n } catch (error) {\n closing = true;\n writeError(`Unable to start mesh daemon: ${(error as Error).message}`);\n exit(1);\n return { close: () => undefined };\n }\n\n if ('setEncoding' in stdin && typeof stdin.setEncoding === 'function') {\n stdin.setEncoding('utf8');\n }\n\n const handleChunk = (chunk: string | Buffer) => {\n stdinBuffer += chunk.toString();\n let newlineIndex = stdinBuffer.indexOf('\\n');\n while (newlineIndex >= 0) {\n const line = stdinBuffer.slice(0, newlineIndex).trim();\n stdinBuffer = stdinBuffer.slice(newlineIndex + 1);\n if (line) {\n if (client) {\n try {\n client.sendRaw(line);\n } catch {\n pending.push(line);\n void reconnect();\n }\n } else {\n pending.push(line);\n void reconnect();\n }\n }\n newlineIndex = stdinBuffer.indexOf('\\n');\n }\n };\n\n const shutdown = () => {\n if (closing) {\n return;\n }\n\n closing = true;\n stdin.off('data', handleChunk);\n stdin.off('end', shutdown);\n stdin.off('close', shutdown);\n client?.close();\n exit(0);\n };\n\n stdin.on('data', handleChunk);\n stdin.on('end', shutdown);\n stdin.on('close', shutdown);\n if ('resume' in stdin && typeof stdin.resume === 'function') {\n stdin.resume();\n }\n\n return { close: shutdown };\n}\n\nasync function guessAppName(): Promise<string> {\n if (process.env.COLLECTIVE_APP_NAME) {\n return process.env.COLLECTIVE_APP_NAME;\n }\n\n const arg = process.argv\n .map((value) => basename(value).toLowerCase())\n .find((value) => value && !['node', 'node.exe', 'mesh-shim', 'index.js'].includes(value));\n if (arg) {\n return arg.replace(/\\.(cmd|exe)$/i, '');\n }\n\n try {\n const result =\n process.platform === 'win32'\n ? await execFileAsync(\n 'powershell',\n ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter \"ProcessId = ${process.ppid}\").Name`],\n { windowsHide: true },\n )\n : await execFileAsync('ps', ['-o', 'comm=', '-p', String(process.ppid)]);\n const name = basename(result.stdout.trim()).replace(/\\.(cmd|exe)$/i, '').toLowerCase();\n return name || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n","import { spawn, spawnSync } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport net from 'node:net';\nimport { homedir, userInfo } from 'node:os';\nimport { resolve } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { fileURLToPath } from 'node:url';\n\nconst require = createRequire(import.meta.url);\n\nexport interface LauncherOptions {\n ipcPath: string;\n pidFile: string;\n daemonBin: string;\n startupTimeoutMs: number;\n}\n\nconst LEGACY_WINDOWS_PIPE_PATH = '\\\\\\\\.\\\\pipe\\\\hivemind-collective';\n\nexport function getDefaultIpcPath(): string {\n return process.env.COLLECTIVE_IPC_PATH ??\n (process.platform === 'win32'\n ? `${LEGACY_WINDOWS_PIPE_PATH}-${sanitizePipeSegment(getCurrentUsername())}`\n : resolve(homedir(), '.hivemind-os/collective', 'mesh.sock'));\n}\n\nexport function getDefaultPidFile(): string {\n return process.env.COLLECTIVE_PID_FILE ?? resolve(homedir(), '.hivemind-os/collective', 'daemon.pid');\n}\n\nexport function resolveDaemonBin(): string {\n if (process.env.COLLECTIVE_DAEMON_BIN) {\n return process.env.COLLECTIVE_DAEMON_BIN;\n }\n\n try {\n return require.resolve('@hivemind-os/collective-daemon');\n } catch {\n if (commandExists('collective-daemon')) {\n return 'collective-daemon';\n }\n\n // Monorepo fallback (only works when running from packages/shim/)\n return fileURLToPath(new URL('../../daemon/dist/index.js', import.meta.url));\n }\n}\n\nexport async function isDaemonRunning(ipcPath: string): Promise<boolean> {\n return new Promise((resolvePromise) => {\n const socket = net.connect(ipcPath);\n const finish = (running: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolvePromise(running);\n };\n\n socket.once('connect', () => {\n finish(true);\n });\n socket.once('error', () => {\n finish(false);\n });\n });\n}\n\nexport async function ensureDaemonRunning(options: LauncherOptions): Promise<void> {\n if (await isDaemonRunning(options.ipcPath)) {\n return;\n }\n\n const command = options.daemonBin.endsWith('.js') ? process.execPath : options.daemonBin;\n const args = options.daemonBin.endsWith('.js') ? [options.daemonBin] : [];\n const child = spawn(command, args, { detached: true, stdio: ['ignore', 'ignore', 'pipe'] });\n let stderrOutput = '';\n child.stderr?.setEncoding('utf8');\n child.stderr?.on('data', (chunk: string) => {\n stderrOutput += chunk;\n });\n child.unref();\n child.stderr?.unref();\n\n let exited = false;\n let exitCode: number | null = null;\n child.once('exit', (code) => {\n exited = true;\n exitCode = code;\n });\n\n const deadline = Date.now() + options.startupTimeoutMs;\n while (Date.now() < deadline) {\n await delay(200);\n if (await isDaemonRunning(options.ipcPath)) {\n return;\n }\n if (exited) {\n const detail = stderrOutput.trim() ? `\\n${stderrOutput.trim()}` : '';\n throw new Error(`Daemon process exited with code ${exitCode} before IPC was ready.${detail}`);\n }\n }\n\n throw new Error(`Timed out waiting for daemon IPC at ${options.ipcPath} (pid file: ${options.pidFile})`);\n}\n\nfunction commandExists(command: string): boolean {\n const probe = process.platform === 'win32' ? 'where' : 'which';\n return spawnSync(probe, [command], { stdio: 'ignore' }).status === 0;\n}\n\nfunction getCurrentUsername(): string {\n try {\n return userInfo().username;\n } catch {\n return process.env.USERNAME ?? process.env.USER ?? 'unknown-user';\n }\n}\n\nfunction sanitizePipeSegment(value: string): string {\n const leaf = value.split(/[\\\\/]+/).filter(Boolean).at(-1) ?? value;\n const sanitized = leaf.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');\n return sanitized || 'unknown-user';\n}\n","import { randomUUID } from 'node:crypto';\nimport net from 'node:net';\n\ntype MessageHandler = (message: object) => void;\ntype CloseHandler = () => void;\n\nexport class IpcClient {\n private socket?: net.Socket;\n private buffer = '';\n private readonly messageHandlers = new Set<MessageHandler>();\n private readonly closeHandlers = new Set<CloseHandler>();\n private helloId?: string;\n private helloWaiter?: { resolve: () => void; reject: (error: Error) => void };\n\n constructor(private readonly ipcPath: string) {}\n\n async connect(appName = 'unknown'): Promise<void> {\n this.close();\n const socket = await new Promise<net.Socket>((resolve, reject) => {\n const client = net.connect(this.ipcPath, () => {\n client.off('error', reject);\n resolve(client);\n });\n client.once('error', reject);\n });\n\n this.socket = socket;\n socket.setEncoding('utf8');\n socket.setNoDelay(true);\n socket.on('data', (chunk: string | Buffer) => {\n this.buffer += chunk.toString();\n this.drainBuffer();\n });\n socket.on('close', () => {\n this.handleClose();\n });\n socket.on('end', () => {\n this.handleClose();\n });\n socket.on('error', () => undefined);\n\n this.helloId = `shim-hello-${randomUUID()}`;\n await new Promise<void>((resolve, reject) => {\n this.helloWaiter = { resolve, reject };\n this.send({\n jsonrpc: '2.0',\n id: this.helloId as string,\n method: 'shim_hello',\n params: {\n appName,\n pid: process.pid,\n ...(process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}),\n },\n });\n });\n }\n\n send(message: object): void {\n this.sendRaw(JSON.stringify(message));\n }\n\n sendRaw(line: string): void {\n const socket = this.socket;\n if (!socket || socket.destroyed) {\n throw new Error('IPC client is not connected');\n }\n\n socket.write(`${line.trimEnd()}\\n`);\n }\n\n onMessage(handler: (message: object) => void): void {\n this.messageHandlers.add(handler);\n }\n\n onClose(handler: () => void): void {\n this.closeHandlers.add(handler);\n }\n\n close(): void {\n const socket = this.socket;\n this.socket = undefined;\n this.buffer = '';\n this.rejectHello(new Error('IPC connection closed'));\n if (socket && !socket.destroyed) {\n socket.destroy();\n }\n }\n\n private drainBuffer(): void {\n let newlineIndex = this.buffer.indexOf('\\n');\n while (newlineIndex >= 0) {\n const line = this.buffer.slice(0, newlineIndex).trim();\n this.buffer = this.buffer.slice(newlineIndex + 1);\n if (line) {\n const message = JSON.parse(line) as Record<string, unknown>;\n if (this.helloWaiter && message.id === this.helloId) {\n const waiter = this.helloWaiter;\n this.helloWaiter = undefined;\n this.helloId = undefined;\n if (message.error && typeof message.error === 'object') {\n waiter.reject(new Error(String((message.error as { message?: unknown }).message ?? 'shim_hello failed')));\n } else {\n waiter.resolve();\n }\n } else {\n for (const handler of this.messageHandlers) {\n handler(message);\n }\n }\n }\n newlineIndex = this.buffer.indexOf('\\n');\n }\n }\n\n private handleClose(): void {\n if (!this.socket && !this.helloWaiter) {\n return;\n }\n\n this.socket = undefined;\n this.buffer = '';\n this.rejectHello(new Error('IPC connection closed'));\n for (const handler of this.closeHandlers) {\n handler();\n }\n }\n\n private rejectHello(error: Error): void {\n if (!this.helloWaiter) {\n return;\n }\n\n const waiter = this.helloWaiter;\n this.helloWaiter = undefined;\n this.helloId = undefined;\n waiter.reject(error);\n }\n}\n","#!/usr/bin/env node\n\nimport { startShim } from './bridge.js';\n\nstartShim().catch((err) => {\n process.stderr.write(`mesh-shim fatal: ${err.message}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":["delay","require","net","resolve","delay"]}
|
|
1
|
+
{"version":3,"sources":["../src/bridge.ts","../src/daemon-launcher.ts","../src/ipc-client.ts","../src/index.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { basename } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { promisify } from 'node:util';\n\nimport {\n ensureDaemonRunning,\n getDefaultIpcPath,\n getDefaultPidFile,\n resolveDaemonBin,\n stopDaemon,\n SHIM_VERSION,\n type LauncherOptions,\n} from './daemon-launcher.js';\nimport { IpcClient } from './ipc-client.js';\n\nconst execFileAsync = promisify(execFile);\n\nexport interface BridgeOptions {\n ipcPath?: string;\n pidFile?: string;\n daemonBin?: string;\n startupTimeoutMs?: number;\n appName?: string;\n stdin?: NodeJS.ReadableStream;\n stdout?: NodeJS.WritableStream;\n stderr?: NodeJS.WritableStream;\n exit?: (code: number) => void;\n ensureDaemon?: (options: LauncherOptions) => Promise<void>;\n stopDaemon?: (options: { pidFile: string; ipcPath: string }) => Promise<void>;\n}\n\nexport interface BridgeHandle {\n close(): void;\n}\n\nexport async function startShim(): Promise<void> {\n await createBridge();\n}\n\nexport async function createBridge(options: BridgeOptions = {}): Promise<BridgeHandle> {\n const stdin = options.stdin ?? process.stdin;\n const stdout = options.stdout ?? process.stdout;\n const stderr = options.stderr ?? process.stderr;\n const exit = options.exit ?? ((code: number) => process.exit(code));\n const ensureDaemon = options.ensureDaemon ?? ensureDaemonRunning;\n const stopDaemonFn = options.stopDaemon ?? stopDaemon;\n const launcherOptions: LauncherOptions = {\n ipcPath: options.ipcPath ?? getDefaultIpcPath(),\n pidFile: options.pidFile ?? getDefaultPidFile(),\n daemonBin: options.daemonBin ?? resolveDaemonBin(),\n startupTimeoutMs: options.startupTimeoutMs ?? 30_000,\n };\n const appName = options.appName ?? (await guessAppName());\n\n let client: IpcClient | undefined;\n let stdinBuffer = '';\n let closing = false;\n let reconnecting: Promise<void> | undefined;\n const pending: string[] = [];\n\n const writeError = (message: string) => {\n stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32000, message } })}\\n`);\n stderr.write(`mesh-shim: ${message}\\n`);\n };\n\n const flushPending = () => {\n if (!client) {\n return;\n }\n\n while (pending.length > 0) {\n try {\n client.sendRaw(pending[0] as string);\n pending.shift();\n } catch {\n return;\n }\n }\n };\n\n let upgraded = false;\n\n const connect = async () => {\n await ensureDaemon(launcherOptions);\n const deadline = Date.now() + launcherOptions.startupTimeoutMs;\n while (true) {\n const next = new IpcClient(launcherOptions.ipcPath);\n next.onMessage((message) => {\n stdout.write(`${JSON.stringify(message)}\\n`);\n });\n next.onClose(() => {\n if (!closing && client === next) {\n client = undefined;\n void reconnect();\n }\n });\n\n try {\n const hello = await next.connect(appName);\n\n // Version mismatch: stop old daemon and restart (once per session)\n if (!upgraded && hello.daemonVersion && hello.daemonVersion !== SHIM_VERSION) {\n upgraded = true;\n stderr.write(\n `mesh-shim: Upgrading daemon from v${hello.daemonVersion} to v${SHIM_VERSION}\\n`,\n );\n next.close();\n await stopDaemonFn({ pidFile: launcherOptions.pidFile, ipcPath: launcherOptions.ipcPath });\n await ensureDaemon(launcherOptions);\n continue;\n }\n\n client = next;\n flushPending();\n return;\n } catch (error) {\n next.close();\n if (Date.now() >= deadline) {\n throw error;\n }\n await delay(200);\n }\n }\n };\n\n const reconnect = async () => {\n if (reconnecting) {\n return reconnecting;\n }\n\n reconnecting = (async () => {\n try {\n await connect();\n } catch (error) {\n if (!closing) {\n closing = true;\n client?.close();\n writeError(`Unable to reach mesh daemon: ${(error as Error).message}`);\n exit(1);\n }\n } finally {\n reconnecting = undefined;\n }\n })();\n\n return reconnecting;\n };\n\n try {\n await connect();\n } catch (error) {\n closing = true;\n writeError(`Unable to start mesh daemon: ${(error as Error).message}`);\n exit(1);\n return { close: () => undefined };\n }\n\n if ('setEncoding' in stdin && typeof stdin.setEncoding === 'function') {\n stdin.setEncoding('utf8');\n }\n\n const handleChunk = (chunk: string | Buffer) => {\n stdinBuffer += chunk.toString();\n let newlineIndex = stdinBuffer.indexOf('\\n');\n while (newlineIndex >= 0) {\n const line = stdinBuffer.slice(0, newlineIndex).trim();\n stdinBuffer = stdinBuffer.slice(newlineIndex + 1);\n if (line) {\n if (client) {\n try {\n client.sendRaw(line);\n } catch {\n pending.push(line);\n void reconnect();\n }\n } else {\n pending.push(line);\n void reconnect();\n }\n }\n newlineIndex = stdinBuffer.indexOf('\\n');\n }\n };\n\n const shutdown = () => {\n if (closing) {\n return;\n }\n\n closing = true;\n stdin.off('data', handleChunk);\n stdin.off('end', shutdown);\n stdin.off('close', shutdown);\n client?.close();\n exit(0);\n };\n\n stdin.on('data', handleChunk);\n stdin.on('end', shutdown);\n stdin.on('close', shutdown);\n if ('resume' in stdin && typeof stdin.resume === 'function') {\n stdin.resume();\n }\n\n return { close: shutdown };\n}\n\nasync function guessAppName(): Promise<string> {\n if (process.env.COLLECTIVE_APP_NAME) {\n return process.env.COLLECTIVE_APP_NAME;\n }\n\n const arg = process.argv\n .map((value) => basename(value).toLowerCase())\n .find((value) => value && !['node', 'node.exe', 'mesh-shim', 'index.js'].includes(value));\n if (arg) {\n return arg.replace(/\\.(cmd|exe)$/i, '');\n }\n\n try {\n const result =\n process.platform === 'win32'\n ? await execFileAsync(\n 'powershell',\n ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter \"ProcessId = ${process.ppid}\").Name`],\n { windowsHide: true },\n )\n : await execFileAsync('ps', ['-o', 'comm=', '-p', String(process.ppid)]);\n const name = basename(result.stdout.trim()).replace(/\\.(cmd|exe)$/i, '').toLowerCase();\n return name || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n","import { spawn, spawnSync } from 'node:child_process';\nimport { readFileSync, unlinkSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport net from 'node:net';\nimport { homedir, userInfo } from 'node:os';\nimport { resolve } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { fileURLToPath } from 'node:url';\n\nconst require = createRequire(import.meta.url);\nconst { version: SHIM_VERSION } = require('../package.json') as { version: string };\n\nexport { SHIM_VERSION };\n\nexport interface LauncherOptions {\n ipcPath: string;\n pidFile: string;\n daemonBin: string;\n startupTimeoutMs: number;\n}\n\nconst LEGACY_WINDOWS_PIPE_PATH = '\\\\\\\\.\\\\pipe\\\\hivemind-collective';\n\nexport function getDefaultIpcPath(): string {\n return process.env.COLLECTIVE_IPC_PATH ??\n (process.platform === 'win32'\n ? `${LEGACY_WINDOWS_PIPE_PATH}-${sanitizePipeSegment(getCurrentUsername())}`\n : resolve(homedir(), '.hivemind-os/collective', 'mesh.sock'));\n}\n\nexport function getDefaultPidFile(): string {\n return process.env.COLLECTIVE_PID_FILE ?? resolve(homedir(), '.hivemind-os/collective', 'daemon.pid');\n}\n\nexport function resolveDaemonBin(): string {\n if (process.env.COLLECTIVE_DAEMON_BIN) {\n return process.env.COLLECTIVE_DAEMON_BIN;\n }\n\n try {\n return require.resolve('@hivemind-os/collective-daemon');\n } catch {\n if (commandExists('collective-daemon')) {\n return 'collective-daemon';\n }\n\n // Monorepo fallback (only works when running from packages/shim/)\n return fileURLToPath(new URL('../../daemon/dist/index.js', import.meta.url));\n }\n}\n\nexport async function isDaemonRunning(ipcPath: string): Promise<boolean> {\n return new Promise((resolvePromise) => {\n const socket = net.connect(ipcPath);\n const finish = (running: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolvePromise(running);\n };\n\n socket.once('connect', () => {\n finish(true);\n });\n socket.once('error', () => {\n finish(false);\n });\n });\n}\n\nexport async function ensureDaemonRunning(options: LauncherOptions): Promise<void> {\n if (await isDaemonRunning(options.ipcPath)) {\n return;\n }\n\n const command = options.daemonBin.endsWith('.js') ? process.execPath : options.daemonBin;\n const args = options.daemonBin.endsWith('.js') ? [options.daemonBin] : [];\n const child = spawn(command, args, { detached: true, stdio: ['ignore', 'ignore', 'pipe'] });\n let stderrOutput = '';\n child.stderr?.setEncoding('utf8');\n child.stderr?.on('data', (chunk: string) => {\n stderrOutput += chunk;\n });\n child.unref();\n child.stderr?.unref();\n\n let exited = false;\n let exitCode: number | null = null;\n child.once('exit', (code) => {\n exited = true;\n exitCode = code;\n });\n\n const deadline = Date.now() + options.startupTimeoutMs;\n while (Date.now() < deadline) {\n await delay(200);\n if (await isDaemonRunning(options.ipcPath)) {\n return;\n }\n if (exited) {\n const detail = stderrOutput.trim() ? `\\n${stderrOutput.trim()}` : '';\n throw new Error(`Daemon process exited with code ${exitCode} before IPC was ready.${detail}`);\n }\n }\n\n throw new Error(`Timed out waiting for daemon IPC at ${options.ipcPath} (pid file: ${options.pidFile})`);\n}\n\nfunction commandExists(command: string): boolean {\n const probe = process.platform === 'win32' ? 'where' : 'which';\n return spawnSync(probe, [command], { stdio: 'ignore' }).status === 0;\n}\n\nfunction getCurrentUsername(): string {\n try {\n return userInfo().username;\n } catch {\n return process.env.USERNAME ?? process.env.USER ?? 'unknown-user';\n }\n}\n\nfunction sanitizePipeSegment(value: string): string {\n const leaf = value.split(/[\\\\/]+/).filter(Boolean).at(-1) ?? value;\n const sanitized = leaf.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');\n return sanitized || 'unknown-user';\n}\n\n/**\n * Stop the running daemon by reading its PID file and sending SIGTERM.\n * Waits for the IPC socket to go down before returning.\n */\nexport async function stopDaemon(options: { pidFile: string; ipcPath: string; timeoutMs?: number }): Promise<void> {\n const { pidFile, ipcPath, timeoutMs = 10_000 } = options;\n\n let pid: number;\n try {\n pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);\n } catch {\n // No PID file — daemon may already be dead\n return;\n }\n\n if (isNaN(pid)) {\n try { unlinkSync(pidFile); } catch { /* ignore */ }\n return;\n }\n\n // Send SIGTERM (on Windows, process.kill sends a termination signal)\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Process already gone\n try { unlinkSync(pidFile); } catch { /* ignore */ }\n return;\n }\n\n // Wait for the IPC socket to go down\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!(await isDaemonRunning(ipcPath))) {\n return;\n }\n await delay(200);\n }\n\n // Force kill if still alive\n try {\n process.kill(pid, 'SIGKILL');\n } catch { /* already gone */ }\n try { unlinkSync(pidFile); } catch { /* ignore */ }\n}\n","import { createRequire } from 'node:module';\nimport { randomUUID } from 'node:crypto';\nimport net from 'node:net';\n\nconst require = createRequire(import.meta.url);\nconst { version: SHIM_VERSION } = require('../package.json') as { version: string };\n\ntype MessageHandler = (message: object) => void;\ntype CloseHandler = () => void;\n\nexport interface HelloResult {\n daemonVersion?: string;\n connectionId?: string;\n}\n\nexport class IpcClient {\n private socket?: net.Socket;\n private buffer = '';\n private readonly messageHandlers = new Set<MessageHandler>();\n private readonly closeHandlers = new Set<CloseHandler>();\n private helloId?: string;\n private helloWaiter?: { resolve: (result: HelloResult) => void; reject: (error: Error) => void };\n\n constructor(private readonly ipcPath: string) {}\n\n async connect(appName = 'unknown'): Promise<HelloResult> {\n this.close();\n const socket = await new Promise<net.Socket>((resolve, reject) => {\n const client = net.connect(this.ipcPath, () => {\n client.off('error', reject);\n resolve(client);\n });\n client.once('error', reject);\n });\n\n this.socket = socket;\n socket.setEncoding('utf8');\n socket.setNoDelay(true);\n socket.on('data', (chunk: string | Buffer) => {\n this.buffer += chunk.toString();\n this.drainBuffer();\n });\n socket.on('close', () => {\n this.handleClose();\n });\n socket.on('end', () => {\n this.handleClose();\n });\n socket.on('error', () => undefined);\n\n this.helloId = `shim-hello-${randomUUID()}`;\n return new Promise<HelloResult>((resolve, reject) => {\n this.helloWaiter = { resolve, reject };\n this.send({\n jsonrpc: '2.0',\n id: this.helloId as string,\n method: 'shim_hello',\n params: {\n appName,\n pid: process.pid,\n shimVersion: SHIM_VERSION,\n ...(process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}),\n },\n });\n });\n }\n\n send(message: object): void {\n this.sendRaw(JSON.stringify(message));\n }\n\n sendRaw(line: string): void {\n const socket = this.socket;\n if (!socket || socket.destroyed) {\n throw new Error('IPC client is not connected');\n }\n\n socket.write(`${line.trimEnd()}\\n`);\n }\n\n onMessage(handler: (message: object) => void): void {\n this.messageHandlers.add(handler);\n }\n\n onClose(handler: () => void): void {\n this.closeHandlers.add(handler);\n }\n\n close(): void {\n const socket = this.socket;\n this.socket = undefined;\n this.buffer = '';\n this.rejectHello(new Error('IPC connection closed'));\n if (socket && !socket.destroyed) {\n socket.destroy();\n }\n }\n\n private drainBuffer(): void {\n let newlineIndex = this.buffer.indexOf('\\n');\n while (newlineIndex >= 0) {\n const line = this.buffer.slice(0, newlineIndex).trim();\n this.buffer = this.buffer.slice(newlineIndex + 1);\n if (line) {\n const message = JSON.parse(line) as Record<string, unknown>;\n if (this.helloWaiter && message.id === this.helloId) {\n const waiter = this.helloWaiter;\n this.helloWaiter = undefined;\n this.helloId = undefined;\n if (message.error && typeof message.error === 'object') {\n waiter.reject(new Error(String((message.error as { message?: unknown }).message ?? 'shim_hello failed')));\n } else {\n const result = (message.result ?? {}) as Record<string, unknown>;\n waiter.resolve({\n daemonVersion: typeof result.daemonVersion === 'string' ? result.daemonVersion : undefined,\n connectionId: typeof result.connectionId === 'string' ? result.connectionId : undefined,\n });\n }\n } else {\n for (const handler of this.messageHandlers) {\n handler(message);\n }\n }\n }\n newlineIndex = this.buffer.indexOf('\\n');\n }\n }\n\n private handleClose(): void {\n if (!this.socket && !this.helloWaiter) {\n return;\n }\n\n this.socket = undefined;\n this.buffer = '';\n this.rejectHello(new Error('IPC connection closed'));\n for (const handler of this.closeHandlers) {\n handler();\n }\n }\n\n private rejectHello(error: Error): void {\n if (!this.helloWaiter) {\n return;\n }\n\n const waiter = this.helloWaiter;\n this.helloWaiter = undefined;\n this.helloId = undefined;\n waiter.reject(error);\n }\n}\n","#!/usr/bin/env node\n\nimport { startShim } from './bridge.js';\n\nstartShim().catch((err) => {\n process.stderr.write(`mesh-shim fatal: ${err.message}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":["delay","require","require","createRequire","net","require","SHIM_VERSION","resolve","delay"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hivemind-os/collective-shim",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"pino": "^9.0.0",
|
|
12
|
-
"@hivemind-os/collective-daemon": "0.2.
|
|
12
|
+
"@hivemind-os/collective-daemon": "0.2.6"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"vitest": "^3.0.0",
|
package/src/bridge.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
getDefaultIpcPath,
|
|
9
9
|
getDefaultPidFile,
|
|
10
10
|
resolveDaemonBin,
|
|
11
|
+
stopDaemon,
|
|
12
|
+
SHIM_VERSION,
|
|
11
13
|
type LauncherOptions,
|
|
12
14
|
} from './daemon-launcher.js';
|
|
13
15
|
import { IpcClient } from './ipc-client.js';
|
|
@@ -25,6 +27,7 @@ export interface BridgeOptions {
|
|
|
25
27
|
stderr?: NodeJS.WritableStream;
|
|
26
28
|
exit?: (code: number) => void;
|
|
27
29
|
ensureDaemon?: (options: LauncherOptions) => Promise<void>;
|
|
30
|
+
stopDaemon?: (options: { pidFile: string; ipcPath: string }) => Promise<void>;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export interface BridgeHandle {
|
|
@@ -41,6 +44,7 @@ export async function createBridge(options: BridgeOptions = {}): Promise<BridgeH
|
|
|
41
44
|
const stderr = options.stderr ?? process.stderr;
|
|
42
45
|
const exit = options.exit ?? ((code: number) => process.exit(code));
|
|
43
46
|
const ensureDaemon = options.ensureDaemon ?? ensureDaemonRunning;
|
|
47
|
+
const stopDaemonFn = options.stopDaemon ?? stopDaemon;
|
|
44
48
|
const launcherOptions: LauncherOptions = {
|
|
45
49
|
ipcPath: options.ipcPath ?? getDefaultIpcPath(),
|
|
46
50
|
pidFile: options.pidFile ?? getDefaultPidFile(),
|
|
@@ -75,6 +79,8 @@ export async function createBridge(options: BridgeOptions = {}): Promise<BridgeH
|
|
|
75
79
|
}
|
|
76
80
|
};
|
|
77
81
|
|
|
82
|
+
let upgraded = false;
|
|
83
|
+
|
|
78
84
|
const connect = async () => {
|
|
79
85
|
await ensureDaemon(launcherOptions);
|
|
80
86
|
const deadline = Date.now() + launcherOptions.startupTimeoutMs;
|
|
@@ -91,7 +97,20 @@ export async function createBridge(options: BridgeOptions = {}): Promise<BridgeH
|
|
|
91
97
|
});
|
|
92
98
|
|
|
93
99
|
try {
|
|
94
|
-
await next.connect(appName);
|
|
100
|
+
const hello = await next.connect(appName);
|
|
101
|
+
|
|
102
|
+
// Version mismatch: stop old daemon and restart (once per session)
|
|
103
|
+
if (!upgraded && hello.daemonVersion && hello.daemonVersion !== SHIM_VERSION) {
|
|
104
|
+
upgraded = true;
|
|
105
|
+
stderr.write(
|
|
106
|
+
`mesh-shim: Upgrading daemon from v${hello.daemonVersion} to v${SHIM_VERSION}\n`,
|
|
107
|
+
);
|
|
108
|
+
next.close();
|
|
109
|
+
await stopDaemonFn({ pidFile: launcherOptions.pidFile, ipcPath: launcherOptions.ipcPath });
|
|
110
|
+
await ensureDaemon(launcherOptions);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
95
114
|
client = next;
|
|
96
115
|
flushPending();
|
|
97
116
|
return;
|
package/src/daemon-launcher.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, unlinkSync } from 'node:fs';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import net from 'node:net';
|
|
4
5
|
import { homedir, userInfo } from 'node:os';
|
|
@@ -7,6 +8,9 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
11
|
+
const { version: SHIM_VERSION } = require('../package.json') as { version: string };
|
|
12
|
+
|
|
13
|
+
export { SHIM_VERSION };
|
|
10
14
|
|
|
11
15
|
export interface LauncherOptions {
|
|
12
16
|
ipcPath: string;
|
|
@@ -119,3 +123,48 @@ function sanitizePipeSegment(value: string): string {
|
|
|
119
123
|
const sanitized = leaf.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
120
124
|
return sanitized || 'unknown-user';
|
|
121
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Stop the running daemon by reading its PID file and sending SIGTERM.
|
|
129
|
+
* Waits for the IPC socket to go down before returning.
|
|
130
|
+
*/
|
|
131
|
+
export async function stopDaemon(options: { pidFile: string; ipcPath: string; timeoutMs?: number }): Promise<void> {
|
|
132
|
+
const { pidFile, ipcPath, timeoutMs = 10_000 } = options;
|
|
133
|
+
|
|
134
|
+
let pid: number;
|
|
135
|
+
try {
|
|
136
|
+
pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
|
|
137
|
+
} catch {
|
|
138
|
+
// No PID file — daemon may already be dead
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (isNaN(pid)) {
|
|
143
|
+
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Send SIGTERM (on Windows, process.kill sends a termination signal)
|
|
148
|
+
try {
|
|
149
|
+
process.kill(pid, 'SIGTERM');
|
|
150
|
+
} catch {
|
|
151
|
+
// Process already gone
|
|
152
|
+
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Wait for the IPC socket to go down
|
|
157
|
+
const deadline = Date.now() + timeoutMs;
|
|
158
|
+
while (Date.now() < deadline) {
|
|
159
|
+
if (!(await isDaemonRunning(ipcPath))) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await delay(200);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Force kill if still alive
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 'SIGKILL');
|
|
168
|
+
} catch { /* already gone */ }
|
|
169
|
+
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
|
170
|
+
}
|
package/src/ipc-client.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import net from 'node:net';
|
|
3
4
|
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { version: SHIM_VERSION } = require('../package.json') as { version: string };
|
|
7
|
+
|
|
4
8
|
type MessageHandler = (message: object) => void;
|
|
5
9
|
type CloseHandler = () => void;
|
|
6
10
|
|
|
11
|
+
export interface HelloResult {
|
|
12
|
+
daemonVersion?: string;
|
|
13
|
+
connectionId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export class IpcClient {
|
|
8
17
|
private socket?: net.Socket;
|
|
9
18
|
private buffer = '';
|
|
10
19
|
private readonly messageHandlers = new Set<MessageHandler>();
|
|
11
20
|
private readonly closeHandlers = new Set<CloseHandler>();
|
|
12
21
|
private helloId?: string;
|
|
13
|
-
private helloWaiter?: { resolve: () => void; reject: (error: Error) => void };
|
|
22
|
+
private helloWaiter?: { resolve: (result: HelloResult) => void; reject: (error: Error) => void };
|
|
14
23
|
|
|
15
24
|
constructor(private readonly ipcPath: string) {}
|
|
16
25
|
|
|
17
|
-
async connect(appName = 'unknown'): Promise<
|
|
26
|
+
async connect(appName = 'unknown'): Promise<HelloResult> {
|
|
18
27
|
this.close();
|
|
19
28
|
const socket = await new Promise<net.Socket>((resolve, reject) => {
|
|
20
29
|
const client = net.connect(this.ipcPath, () => {
|
|
@@ -40,7 +49,7 @@ export class IpcClient {
|
|
|
40
49
|
socket.on('error', () => undefined);
|
|
41
50
|
|
|
42
51
|
this.helloId = `shim-hello-${randomUUID()}`;
|
|
43
|
-
|
|
52
|
+
return new Promise<HelloResult>((resolve, reject) => {
|
|
44
53
|
this.helloWaiter = { resolve, reject };
|
|
45
54
|
this.send({
|
|
46
55
|
jsonrpc: '2.0',
|
|
@@ -49,6 +58,7 @@ export class IpcClient {
|
|
|
49
58
|
params: {
|
|
50
59
|
appName,
|
|
51
60
|
pid: process.pid,
|
|
61
|
+
shimVersion: SHIM_VERSION,
|
|
52
62
|
...(process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}),
|
|
53
63
|
},
|
|
54
64
|
});
|
|
@@ -100,7 +110,11 @@ export class IpcClient {
|
|
|
100
110
|
if (message.error && typeof message.error === 'object') {
|
|
101
111
|
waiter.reject(new Error(String((message.error as { message?: unknown }).message ?? 'shim_hello failed')));
|
|
102
112
|
} else {
|
|
103
|
-
|
|
113
|
+
const result = (message.result ?? {}) as Record<string, unknown>;
|
|
114
|
+
waiter.resolve({
|
|
115
|
+
daemonVersion: typeof result.daemonVersion === 'string' ? result.daemonVersion : undefined,
|
|
116
|
+
connectionId: typeof result.connectionId === 'string' ? result.connectionId : undefined,
|
|
117
|
+
});
|
|
104
118
|
}
|
|
105
119
|
} else {
|
|
106
120
|
for (const handler of this.messageHandlers) {
|
package/tests/bridge.test.ts
CHANGED
|
@@ -80,6 +80,23 @@ class NdjsonReader {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
class NdjsonLineReader {
|
|
84
|
+
private collected: string[] = [];
|
|
85
|
+
|
|
86
|
+
constructor(stream: NodeJS.ReadableStream) {
|
|
87
|
+
if ('setEncoding' in stream && typeof stream.setEncoding === 'function') {
|
|
88
|
+
stream.setEncoding('utf8');
|
|
89
|
+
}
|
|
90
|
+
stream.on('data', (chunk: string | Buffer) => {
|
|
91
|
+
this.collected.push(chunk.toString());
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lines(): string[] {
|
|
96
|
+
return [...this.collected];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
class MockIpcServer {
|
|
84
101
|
private server?: net.Server;
|
|
85
102
|
private socket?: net.Socket;
|
|
@@ -284,4 +301,190 @@ describe('bridge', () => {
|
|
|
284
301
|
expect(exit).toHaveBeenCalledWith(0);
|
|
285
302
|
expect(ensureDaemon).toHaveBeenCalledTimes(2);
|
|
286
303
|
});
|
|
304
|
+
|
|
305
|
+
it('restarts daemon when version mismatch is detected', async () => {
|
|
306
|
+
const dir = await createTestDir();
|
|
307
|
+
const ipcPath = createIpcPath(dir);
|
|
308
|
+
const pidFile = resolve(dir, 'daemon.pid');
|
|
309
|
+
|
|
310
|
+
// Phase 1: old daemon with mismatched version
|
|
311
|
+
let oldServer = new MockIpcServer(ipcPath);
|
|
312
|
+
await oldServer.start();
|
|
313
|
+
|
|
314
|
+
const stopDaemonMock = vi.fn(async () => {
|
|
315
|
+
// Simulate stopDaemon: tear down the old server
|
|
316
|
+
await oldServer.stop();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Track ensureDaemon calls — on the 2nd call (after upgrade), start the "new" daemon
|
|
320
|
+
let newServer: MockIpcServer | undefined;
|
|
321
|
+
let newServerReady: () => void;
|
|
322
|
+
const newServerPromise = new Promise<void>((r) => { newServerReady = r; });
|
|
323
|
+
const ensureDaemon = vi.fn(async () => {
|
|
324
|
+
if (ensureDaemon.mock.calls.length > 1) {
|
|
325
|
+
// Start the "new" daemon after stopDaemon cleaned up the old one
|
|
326
|
+
newServer = new MockIpcServer(ipcPath);
|
|
327
|
+
await newServer.start();
|
|
328
|
+
newServerReady();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const stdin = new PassThrough();
|
|
333
|
+
const stdout = new PassThrough();
|
|
334
|
+
const stderr = new PassThrough();
|
|
335
|
+
const stderrReader = new NdjsonLineReader(stderr);
|
|
336
|
+
const exit = vi.fn();
|
|
337
|
+
|
|
338
|
+
const { SHIM_VERSION: shimVersion } = await import('../src/daemon-launcher.js');
|
|
339
|
+
|
|
340
|
+
const bridgePromise = createBridge({
|
|
341
|
+
ipcPath,
|
|
342
|
+
pidFile,
|
|
343
|
+
daemonBin: 'mesh-daemon',
|
|
344
|
+
appName: 'claude-desktop',
|
|
345
|
+
stdin,
|
|
346
|
+
stdout,
|
|
347
|
+
stderr,
|
|
348
|
+
exit,
|
|
349
|
+
ensureDaemon,
|
|
350
|
+
stopDaemon: stopDaemonMock,
|
|
351
|
+
startupTimeoutMs: 5_000,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Old daemon receives hello, responds with OLD version
|
|
355
|
+
const oldHello = await oldServer.nextMessage();
|
|
356
|
+
expect(oldHello).toMatchObject({ method: 'shim_hello' });
|
|
357
|
+
oldServer.send({
|
|
358
|
+
jsonrpc: '2.0',
|
|
359
|
+
id: oldHello.id,
|
|
360
|
+
result: { acknowledged: true, connectionId: 'old', daemonVersion: '0.0.1' },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// After mismatch, shim should call stopDaemon and ensureDaemon again
|
|
364
|
+
// Wait for the new server to be ready
|
|
365
|
+
await newServerPromise;
|
|
366
|
+
// New daemon receives hello, responds with CURRENT version
|
|
367
|
+
const newHello = await newServer!.nextMessage();
|
|
368
|
+
expect(newHello).toMatchObject({ method: 'shim_hello' });
|
|
369
|
+
newServer!.send({
|
|
370
|
+
jsonrpc: '2.0',
|
|
371
|
+
id: newHello.id,
|
|
372
|
+
result: { acknowledged: true, connectionId: 'new', daemonVersion: shimVersion },
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const bridge = await bridgePromise;
|
|
376
|
+
|
|
377
|
+
// Verify the bridge works after upgrade
|
|
378
|
+
stdin.write('{"jsonrpc":"2.0","id":"test","method":"ping"}\n');
|
|
379
|
+
const forwarded = await newServer!.nextMessage();
|
|
380
|
+
expect(forwarded).toEqual({ jsonrpc: '2.0', id: 'test', method: 'ping' });
|
|
381
|
+
|
|
382
|
+
// Verify stopDaemon was called
|
|
383
|
+
expect(stopDaemonMock).toHaveBeenCalledTimes(1);
|
|
384
|
+
// ensureDaemon called twice: initial + after upgrade
|
|
385
|
+
expect(ensureDaemon).toHaveBeenCalledTimes(2);
|
|
386
|
+
|
|
387
|
+
// Verify stderr logged the upgrade
|
|
388
|
+
const stderrOutput = stderrReader.lines().join('');
|
|
389
|
+
expect(stderrOutput).toContain('Upgrading daemon from v0.0.1');
|
|
390
|
+
expect(stderrOutput).toContain(shimVersion);
|
|
391
|
+
|
|
392
|
+
bridge.close();
|
|
393
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('accepts matching daemon version without restart', async () => {
|
|
397
|
+
const dir = await createTestDir();
|
|
398
|
+
const ipcPath = createIpcPath(dir);
|
|
399
|
+
|
|
400
|
+
const server = new MockIpcServer(ipcPath);
|
|
401
|
+
await server.start();
|
|
402
|
+
|
|
403
|
+
const stopDaemonMock = vi.fn();
|
|
404
|
+
const ensureDaemon = vi.fn(async () => undefined);
|
|
405
|
+
|
|
406
|
+
const stdin = new PassThrough();
|
|
407
|
+
const stdout = new PassThrough();
|
|
408
|
+
const stderr = new PassThrough();
|
|
409
|
+
const exit = vi.fn();
|
|
410
|
+
|
|
411
|
+
const { SHIM_VERSION: shimVersion } = await import('../src/daemon-launcher.js');
|
|
412
|
+
|
|
413
|
+
const bridgePromise = createBridge({
|
|
414
|
+
ipcPath,
|
|
415
|
+
pidFile: resolve(dir, 'daemon.pid'),
|
|
416
|
+
daemonBin: 'mesh-daemon',
|
|
417
|
+
appName: 'claude-desktop',
|
|
418
|
+
stdin,
|
|
419
|
+
stdout,
|
|
420
|
+
stderr,
|
|
421
|
+
exit,
|
|
422
|
+
ensureDaemon,
|
|
423
|
+
stopDaemon: stopDaemonMock,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const hello = await server.nextMessage();
|
|
427
|
+
server.send({
|
|
428
|
+
jsonrpc: '2.0',
|
|
429
|
+
id: hello.id,
|
|
430
|
+
result: { acknowledged: true, connectionId: 'ok', daemonVersion: shimVersion },
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const bridge = await bridgePromise;
|
|
434
|
+
|
|
435
|
+
// Should NOT have called stopDaemon
|
|
436
|
+
expect(stopDaemonMock).not.toHaveBeenCalled();
|
|
437
|
+
expect(ensureDaemon).toHaveBeenCalledTimes(1);
|
|
438
|
+
|
|
439
|
+
bridge.close();
|
|
440
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('skips version check for old daemons without daemonVersion', async () => {
|
|
444
|
+
const dir = await createTestDir();
|
|
445
|
+
const ipcPath = createIpcPath(dir);
|
|
446
|
+
|
|
447
|
+
const server = new MockIpcServer(ipcPath);
|
|
448
|
+
await server.start();
|
|
449
|
+
|
|
450
|
+
const stopDaemonMock = vi.fn();
|
|
451
|
+
const ensureDaemon = vi.fn(async () => undefined);
|
|
452
|
+
|
|
453
|
+
const stdin = new PassThrough();
|
|
454
|
+
const stdout = new PassThrough();
|
|
455
|
+
const stderr = new PassThrough();
|
|
456
|
+
const exit = vi.fn();
|
|
457
|
+
|
|
458
|
+
const bridgePromise = createBridge({
|
|
459
|
+
ipcPath,
|
|
460
|
+
pidFile: resolve(dir, 'daemon.pid'),
|
|
461
|
+
daemonBin: 'mesh-daemon',
|
|
462
|
+
appName: 'claude-desktop',
|
|
463
|
+
stdin,
|
|
464
|
+
stdout,
|
|
465
|
+
stderr,
|
|
466
|
+
exit,
|
|
467
|
+
ensureDaemon,
|
|
468
|
+
stopDaemon: stopDaemonMock,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const hello = await server.nextMessage();
|
|
472
|
+
// Old daemon that doesn't send daemonVersion
|
|
473
|
+
server.send({
|
|
474
|
+
jsonrpc: '2.0',
|
|
475
|
+
id: hello.id,
|
|
476
|
+
result: { acknowledged: true, connectionId: 'legacy' },
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const bridge = await bridgePromise;
|
|
480
|
+
|
|
481
|
+
// Old daemons without version should be force-upgraded
|
|
482
|
+
// (daemonVersion is undefined, which !== SHIM_VERSION, but
|
|
483
|
+
// we only upgrade when daemonVersion is present and differs)
|
|
484
|
+
expect(stopDaemonMock).not.toHaveBeenCalled();
|
|
485
|
+
expect(ensureDaemon).toHaveBeenCalledTimes(1);
|
|
486
|
+
|
|
487
|
+
bridge.close();
|
|
488
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
489
|
+
});
|
|
287
490
|
});
|