* feat(plugins): expose Paseo API to local plugins Plugin surfaces borrow the selected host connection while subprocess handlers receive an exclusively owned IPC session, so plugins can use normal workspace and agent operations without inventing another transport. * feat(plugins): reload global enablement safely Treat the global plugin switch as a live daemon setting and teach plugin automation to inspect it before acting. Enabling remains permission-gated because plugin backend code can access the daemon machine and client contributions execute inside the app. * feat(plugins): expose plugin logs Capture plugin stdout and stderr separately from IPC so authors can debug initialization, RPC, cleanup, and crash behavior from the app or CLI. Keep a bounded tail across lifecycle recovery while preserving structured output in the daemon log. * fix(build): order plugin dependency builds * test(plugins): align startup and CLI expectations
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { Buffer } from "node:buffer";
|
|
import type { PluginProcessRequest } from "./plugin-process-protocol.js";
|
|
|
|
interface PluginProcessSender {
|
|
send(message: PluginProcessRequest, callback?: (error: Error | null) => void): boolean;
|
|
}
|
|
|
|
type SocketEvent = "message" | "close" | "error";
|
|
type SocketListener = (...args: unknown[]) => void;
|
|
|
|
function normalizeBinaryFrame(data: Uint8Array | ArrayBuffer): Uint8Array {
|
|
if (data instanceof Uint8Array) {
|
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
}
|
|
return new Uint8Array(data);
|
|
}
|
|
|
|
function frameByteLength(data: string | Uint8Array): number {
|
|
return typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
|
|
}
|
|
|
|
export class PluginSessionSocket {
|
|
readyState = 1;
|
|
bufferedAmount = 0;
|
|
private readonly listeners = new Map<SocketEvent, Set<SocketListener>>();
|
|
private readonly onceListeners = new Map<SocketEvent, Set<SocketListener>>();
|
|
|
|
constructor(private readonly child: PluginProcessSender) {}
|
|
|
|
send(data: string | Uint8Array | ArrayBuffer, callback?: (error?: Error) => void): void {
|
|
if (this.readyState !== 1) {
|
|
callback?.(new Error("Plugin session socket is closed"));
|
|
return;
|
|
}
|
|
const frame = typeof data === "string" ? data : normalizeBinaryFrame(data);
|
|
const byteLength = frameByteLength(frame);
|
|
this.bufferedAmount += byteLength;
|
|
try {
|
|
this.child.send(
|
|
{ type: "paseo_frame", data: frame, isBinary: typeof frame !== "string" },
|
|
(error) => {
|
|
this.bufferedAmount = Math.max(0, this.bufferedAmount - byteLength);
|
|
callback?.(error ?? undefined);
|
|
},
|
|
);
|
|
} catch (error) {
|
|
this.bufferedAmount = Math.max(0, this.bufferedAmount - byteLength);
|
|
callback?.(error instanceof Error ? error : new Error(String(error)));
|
|
}
|
|
}
|
|
|
|
receive(data: string | Uint8Array, isBinary: boolean): void {
|
|
if (this.readyState !== 1) return;
|
|
this.emit("message", data, isBinary);
|
|
}
|
|
|
|
close(code?: number, reason?: string): void {
|
|
if (this.readyState === 3) return;
|
|
this.readyState = 3;
|
|
try {
|
|
this.child.send({ type: "paseo_close" });
|
|
} catch {
|
|
// The child has already gone; local session cleanup still has to run.
|
|
}
|
|
this.emit("close", code, reason);
|
|
}
|
|
|
|
peerClosed(): void {
|
|
if (this.readyState === 3) return;
|
|
this.readyState = 3;
|
|
this.emit("close");
|
|
}
|
|
|
|
on(event: SocketEvent, listener: SocketListener): void {
|
|
const listeners = this.listeners.get(event) ?? new Set<SocketListener>();
|
|
listeners.add(listener);
|
|
this.listeners.set(event, listeners);
|
|
}
|
|
|
|
once(event: "close" | "error", listener: SocketListener): void {
|
|
const listeners = this.onceListeners.get(event) ?? new Set<SocketListener>();
|
|
listeners.add(listener);
|
|
this.onceListeners.set(event, listeners);
|
|
}
|
|
|
|
private emit(event: SocketEvent, ...args: unknown[]): void {
|
|
for (const listener of this.listeners.get(event) ?? []) listener(...args);
|
|
const once = this.onceListeners.get(event);
|
|
if (!once) return;
|
|
this.onceListeners.delete(event);
|
|
for (const listener of once) listener(...args);
|
|
}
|
|
}
|