@hivemind-os/collective-shim 0.2.5 → 0.2.7
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 +64 -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 +17 -4
- package/tests/bridge.test.ts +203 -0
- package/tsup.config.ts +6 -0
- package/vitest.config.ts +6 -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.02 KB[39m
|
|
10
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m25.75 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 28ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 2412ms
|
|
14
14
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m20.00 B[39m
|
package/dist/index.js
CHANGED
|
@@ -8,12 +8,14 @@ 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";
|
|
14
15
|
import { resolve } from "path";
|
|
15
16
|
import { setTimeout as delay } from "timers/promises";
|
|
16
17
|
import { fileURLToPath } from "url";
|
|
18
|
+
var SHIM_VERSION = "0.2.6";
|
|
17
19
|
var require2 = createRequire(import.meta.url);
|
|
18
20
|
var LEGACY_WINDOWS_PIPE_PATH = "\\\\.\\pipe\\hivemind-collective";
|
|
19
21
|
function getDefaultIpcPath() {
|
|
@@ -101,10 +103,51 @@ 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
|
|
106
148
|
import { randomUUID } from "crypto";
|
|
107
149
|
import net2 from "net";
|
|
150
|
+
var SHIM_VERSION2 = "0.2.6";
|
|
108
151
|
var IpcClient = class {
|
|
109
152
|
constructor(ipcPath) {
|
|
110
153
|
this.ipcPath = ipcPath;
|
|
@@ -140,7 +183,7 @@ var IpcClient = class {
|
|
|
140
183
|
});
|
|
141
184
|
socket.on("error", () => void 0);
|
|
142
185
|
this.helloId = `shim-hello-${randomUUID()}`;
|
|
143
|
-
|
|
186
|
+
return new Promise((resolve2, reject) => {
|
|
144
187
|
this.helloWaiter = { resolve: resolve2, reject };
|
|
145
188
|
this.send({
|
|
146
189
|
jsonrpc: "2.0",
|
|
@@ -149,6 +192,7 @@ var IpcClient = class {
|
|
|
149
192
|
params: {
|
|
150
193
|
appName,
|
|
151
194
|
pid: process.pid,
|
|
195
|
+
shimVersion: SHIM_VERSION2,
|
|
152
196
|
...process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}
|
|
153
197
|
}
|
|
154
198
|
});
|
|
@@ -194,7 +238,11 @@ var IpcClient = class {
|
|
|
194
238
|
if (message.error && typeof message.error === "object") {
|
|
195
239
|
waiter.reject(new Error(String(message.error.message ?? "shim_hello failed")));
|
|
196
240
|
} else {
|
|
197
|
-
|
|
241
|
+
const result = message.result ?? {};
|
|
242
|
+
waiter.resolve({
|
|
243
|
+
daemonVersion: typeof result.daemonVersion === "string" ? result.daemonVersion : void 0,
|
|
244
|
+
connectionId: typeof result.connectionId === "string" ? result.connectionId : void 0
|
|
245
|
+
});
|
|
198
246
|
}
|
|
199
247
|
} else {
|
|
200
248
|
for (const handler of this.messageHandlers) {
|
|
@@ -238,6 +286,7 @@ async function createBridge(options = {}) {
|
|
|
238
286
|
const stderr = options.stderr ?? process.stderr;
|
|
239
287
|
const exit = options.exit ?? ((code) => process.exit(code));
|
|
240
288
|
const ensureDaemon = options.ensureDaemon ?? ensureDaemonRunning;
|
|
289
|
+
const stopDaemonFn = options.stopDaemon ?? stopDaemon;
|
|
241
290
|
const launcherOptions = {
|
|
242
291
|
ipcPath: options.ipcPath ?? getDefaultIpcPath(),
|
|
243
292
|
pidFile: options.pidFile ?? getDefaultPidFile(),
|
|
@@ -269,6 +318,7 @@ async function createBridge(options = {}) {
|
|
|
269
318
|
}
|
|
270
319
|
}
|
|
271
320
|
};
|
|
321
|
+
let upgraded = false;
|
|
272
322
|
const connect = async () => {
|
|
273
323
|
await ensureDaemon(launcherOptions);
|
|
274
324
|
const deadline = Date.now() + launcherOptions.startupTimeoutMs;
|
|
@@ -285,7 +335,18 @@ async function createBridge(options = {}) {
|
|
|
285
335
|
}
|
|
286
336
|
});
|
|
287
337
|
try {
|
|
288
|
-
await next.connect(appName);
|
|
338
|
+
const hello = await next.connect(appName);
|
|
339
|
+
if (!upgraded && hello.daemonVersion && hello.daemonVersion !== SHIM_VERSION) {
|
|
340
|
+
upgraded = true;
|
|
341
|
+
stderr.write(
|
|
342
|
+
`mesh-shim: Upgrading daemon from v${hello.daemonVersion} to v${SHIM_VERSION}
|
|
343
|
+
`
|
|
344
|
+
);
|
|
345
|
+
next.close();
|
|
346
|
+
await stopDaemonFn({ pidFile: launcherOptions.pidFile, ipcPath: launcherOptions.ipcPath });
|
|
347
|
+
await ensureDaemon(launcherOptions);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
289
350
|
client = next;
|
|
290
351
|
flushPending();
|
|
291
352
|
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\ndeclare const PKG_VERSION: string;\nexport const SHIM_VERSION = PKG_VERSION;\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\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 { randomUUID } from 'node:crypto';\nimport net from 'node:net';\n\ndeclare const PKG_VERSION: string;\nconst SHIM_VERSION = PKG_VERSION;\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","net","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.7",
|
|
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.7"
|
|
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';
|
|
@@ -6,6 +7,9 @@ import { resolve } from 'node:path';
|
|
|
6
7
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
|
|
10
|
+
declare const PKG_VERSION: string;
|
|
11
|
+
export const SHIM_VERSION = PKG_VERSION;
|
|
12
|
+
|
|
9
13
|
const require = createRequire(import.meta.url);
|
|
10
14
|
|
|
11
15
|
export interface LauncherOptions {
|
|
@@ -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,28 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
|
|
4
|
+
declare const PKG_VERSION: string;
|
|
5
|
+
const SHIM_VERSION = PKG_VERSION;
|
|
6
|
+
|
|
4
7
|
type MessageHandler = (message: object) => void;
|
|
5
8
|
type CloseHandler = () => void;
|
|
6
9
|
|
|
10
|
+
export interface HelloResult {
|
|
11
|
+
daemonVersion?: string;
|
|
12
|
+
connectionId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export class IpcClient {
|
|
8
16
|
private socket?: net.Socket;
|
|
9
17
|
private buffer = '';
|
|
10
18
|
private readonly messageHandlers = new Set<MessageHandler>();
|
|
11
19
|
private readonly closeHandlers = new Set<CloseHandler>();
|
|
12
20
|
private helloId?: string;
|
|
13
|
-
private helloWaiter?: { resolve: () => void; reject: (error: Error) => void };
|
|
21
|
+
private helloWaiter?: { resolve: (result: HelloResult) => void; reject: (error: Error) => void };
|
|
14
22
|
|
|
15
23
|
constructor(private readonly ipcPath: string) {}
|
|
16
24
|
|
|
17
|
-
async connect(appName = 'unknown'): Promise<
|
|
25
|
+
async connect(appName = 'unknown'): Promise<HelloResult> {
|
|
18
26
|
this.close();
|
|
19
27
|
const socket = await new Promise<net.Socket>((resolve, reject) => {
|
|
20
28
|
const client = net.connect(this.ipcPath, () => {
|
|
@@ -40,7 +48,7 @@ export class IpcClient {
|
|
|
40
48
|
socket.on('error', () => undefined);
|
|
41
49
|
|
|
42
50
|
this.helloId = `shim-hello-${randomUUID()}`;
|
|
43
|
-
|
|
51
|
+
return new Promise<HelloResult>((resolve, reject) => {
|
|
44
52
|
this.helloWaiter = { resolve, reject };
|
|
45
53
|
this.send({
|
|
46
54
|
jsonrpc: '2.0',
|
|
@@ -49,6 +57,7 @@ export class IpcClient {
|
|
|
49
57
|
params: {
|
|
50
58
|
appName,
|
|
51
59
|
pid: process.pid,
|
|
60
|
+
shimVersion: SHIM_VERSION,
|
|
52
61
|
...(process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}),
|
|
53
62
|
},
|
|
54
63
|
});
|
|
@@ -100,7 +109,11 @@ export class IpcClient {
|
|
|
100
109
|
if (message.error && typeof message.error === 'object') {
|
|
101
110
|
waiter.reject(new Error(String((message.error as { message?: unknown }).message ?? 'shim_hello failed')));
|
|
102
111
|
} else {
|
|
103
|
-
|
|
112
|
+
const result = (message.result ?? {}) as Record<string, unknown>;
|
|
113
|
+
waiter.resolve({
|
|
114
|
+
daemonVersion: typeof result.daemonVersion === 'string' ? result.daemonVersion : undefined,
|
|
115
|
+
connectionId: typeof result.connectionId === 'string' ? result.connectionId : undefined,
|
|
116
|
+
});
|
|
104
117
|
}
|
|
105
118
|
} else {
|
|
106
119
|
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
|
});
|
package/tsup.config.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
import { defineConfig } from 'tsup';
|
|
2
3
|
|
|
4
|
+
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
|
5
|
+
|
|
3
6
|
export default defineConfig({
|
|
4
7
|
entry: ['src/index.ts'],
|
|
5
8
|
format: ['esm'],
|
|
@@ -7,4 +10,7 @@ export default defineConfig({
|
|
|
7
10
|
clean: true,
|
|
8
11
|
sourcemap: true,
|
|
9
12
|
target: 'es2022',
|
|
13
|
+
define: {
|
|
14
|
+
'PKG_VERSION': JSON.stringify(version),
|
|
15
|
+
},
|
|
10
16
|
});
|
package/vitest.config.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
import { defineConfig } from 'vitest/config';
|
|
2
3
|
|
|
4
|
+
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
|
5
|
+
|
|
3
6
|
export default defineConfig({
|
|
7
|
+
define: {
|
|
8
|
+
PKG_VERSION: JSON.stringify(version),
|
|
9
|
+
},
|
|
4
10
|
test: {
|
|
5
11
|
environment: 'node',
|
|
6
12
|
passWithNoTests: false,
|