From 7ece19b0e866c40d0f19ba6ed2d1884e01faf2c3 Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:16:55 +0200 Subject: [PATCH 01/36] chore: package structure --- package.json | 2 +- packages/orchestrator/CHANGELOG.md | 3 ++ packages/orchestrator/README.md | 9 +++++ packages/orchestrator/package.json | 46 +++++++++++++++++++++++ packages/orchestrator/src/cli.ts | 30 +++++++++++++++ packages/orchestrator/src/index.ts | 0 packages/orchestrator/tsconfig.build.json | 9 +++++ tsconfig.json | 2 + 8 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 packages/orchestrator/CHANGELOG.md create mode 100644 packages/orchestrator/README.md create mode 100644 packages/orchestrator/package.json create mode 100644 packages/orchestrator/src/cli.ts create mode 100644 packages/orchestrator/src/index.ts create mode 100644 packages/orchestrator/tsconfig.build.json diff --git a/package.json b/package.json index c88d7123..537fb1de 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ], "scripts": { "clean": "npm run clean --workspaces", - "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build", + "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../orchestrator && npm run build", "check": "biome check --write --error-on-warnings . && npm run check:pinned-deps && npm run check:ts-imports && npm run check:shrinkwrap && tsgo --noEmit && npm run check:browser-smoke", "check:browser-smoke": "node scripts/check-browser-smoke.mjs", "check:pinned-deps": "node scripts/check-pinned-deps.mjs", diff --git a/packages/orchestrator/CHANGELOG.md b/packages/orchestrator/CHANGELOG.md new file mode 100644 index 00000000..1a588142 --- /dev/null +++ b/packages/orchestrator/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## [Unreleased] diff --git a/packages/orchestrator/README.md b/packages/orchestrator/README.md new file mode 100644 index 00000000..ee05c9eb --- /dev/null +++ b/packages/orchestrator/README.md @@ -0,0 +1,9 @@ +# orchestrator + +Orchestrator package for pi. + +## CLI + +```bash +orchestrator --help +``` diff --git a/packages/orchestrator/package.json b/packages/orchestrator/package.json new file mode 100644 index 00000000..a614b8da --- /dev/null +++ b/packages/orchestrator/package.json @@ -0,0 +1,46 @@ +{ + "name": "@earendil-works/pi-orchestrator", + "version": "0.79.6", + "description": "orchestrator package for pi", + "type": "module", + "bin": { + "orchestrator": "dist/cli.js" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "CHANGELOG.md" + ], + "scripts": { + "clean": "shx rm -rf dist", + "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", + "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js", + "prepublishOnly": "npm run clean && npm run build" + }, + "keywords": [ + "pi", + "orchestrator" + ], + "author": "Earendil Works", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/badlogic/pi-mono.git", + "directory": "packages/orchestrator" + }, + "engines": { + "node": ">=22.19.0" + }, + "dependencies": {}, + "devDependencies": { + "shx": "0.4.0" + } +} diff --git a/packages/orchestrator/src/cli.ts b/packages/orchestrator/src/cli.ts new file mode 100644 index 00000000..0ad2da39 --- /dev/null +++ b/packages/orchestrator/src/cli.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")) as { + version: string; +}; + +function printHelp(): void { + console.log(`orchestrator v${packageJson.version}\n\nUsage:\n orchestrator --help\n orchestrator --version`); +} + +const args = process.argv.slice(2); + +if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { + printHelp(); + process.exit(0); +} + +if (args[0] === "--version" || args[0] === "-v") { + console.log(packageJson.version); + process.exit(0); +} + +console.error(`Unknown command: ${args[0]}`); +printHelp(); +process.exit(1); diff --git a/packages/orchestrator/src/index.ts b/packages/orchestrator/src/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/orchestrator/tsconfig.build.json b/packages/orchestrator/tsconfig.build.json new file mode 100644 index 00000000..695dd9ad --- /dev/null +++ b/packages/orchestrator/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 903f80f3..1e212e9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,8 @@ "@earendil-works/pi-coding-agent": ["./packages/coding-agent/src/index.ts"], "@earendil-works/pi-coding-agent/hooks": ["./packages/coding-agent/src/core/hooks/index.ts"], "@earendil-works/pi-coding-agent/*": ["./packages/coding-agent/src/*"], + "@earendil-works/pi-orchestrator": ["./packages/orchestrator/src/index.ts"], + "@earendil-works/pi-orchestrator/*": ["./packages/orchestrator/src/*"], "typebox": ["./node_modules/typebox"], "@earendil-works/pi-tui": ["./packages/tui/src/index.ts"], "@earendil-works/pi-tui/*": ["./packages/tui/src/*"], From d799c722000739de661e44940342115e8ec84979 Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:56:33 +0200 Subject: [PATCH 02/36] feat: ipc socket --- packages/orchestrator/src/config.ts | 27 +++++++++ packages/orchestrator/src/index.ts | 3 + packages/orchestrator/src/ipc/client.ts | 63 +++++++++++++++++++ packages/orchestrator/src/ipc/protocol.ts | 73 +++++++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 packages/orchestrator/src/config.ts create mode 100644 packages/orchestrator/src/ipc/client.ts create mode 100644 packages/orchestrator/src/ipc/protocol.ts diff --git a/packages/orchestrator/src/config.ts b/packages/orchestrator/src/config.ts new file mode 100644 index 00000000..64fb1008 --- /dev/null +++ b/packages/orchestrator/src/config.ts @@ -0,0 +1,27 @@ +import { homedir } from "node:os"; +import { join } from "node:path"; + +const CONFIG_DIR_NAME = ".pi"; +const ENV_ORCHESTRATOR_DIR = "PI_ORCHESTRATOR_DIR"; + +export function getOrchestratorDir(): string { + const envDir = process.env[ENV_ORCHESTRATOR_DIR]; + if (envDir) { + return envDir; + } + + const piDir = process.env.PI_CONFIG_DIR || join(homedir(), CONFIG_DIR_NAME); + return join(piDir, "orchestrator"); +} + +export function getMachinePath(): string { + return join(getOrchestratorDir(), "machine.json"); +} + +export function getInstancesPath(): string { + return join(getOrchestratorDir(), "instances.json"); +} + +export function getSocketPath(): string { + return join(getOrchestratorDir(), "orchestrator.sock"); +} diff --git a/packages/orchestrator/src/index.ts b/packages/orchestrator/src/index.ts index e69de29b..67ecbe7a 100644 --- a/packages/orchestrator/src/index.ts +++ b/packages/orchestrator/src/index.ts @@ -0,0 +1,3 @@ +export * from "./config.ts"; +export * from "./ipc/client.ts"; +export * from "./ipc/protocol.ts"; diff --git a/packages/orchestrator/src/ipc/client.ts b/packages/orchestrator/src/ipc/client.ts new file mode 100644 index 00000000..8029654a --- /dev/null +++ b/packages/orchestrator/src/ipc/client.ts @@ -0,0 +1,63 @@ +import { createConnection } from "node:net"; +import { getSocketPath } from "../config.ts"; +import { encodeMessage, type OrchestratorRequest, type OrchestratorResponse, parseResponseLine } from "./protocol.ts"; + +export async function sendIpcRequest(request: OrchestratorRequest): Promise { + const socketPath = getSocketPath(); + + return new Promise((resolve, reject) => { + const socket = createConnection(socketPath); + let buffer = ""; + let settled = false; + + const cleanup = () => { + socket.removeAllListeners(); + socket.end(); + }; + + socket.on("connect", () => { + socket.write(encodeMessage(request)); + }); + + socket.on("data", (chunk: Buffer | string) => { + buffer += chunk.toString(); + const newlineIndex = buffer.indexOf("\n"); + if (newlineIndex === -1) { + return; + } + + const line = buffer.slice(0, newlineIndex).trim(); + if (!line) { + return; + } + + try { + settled = true; + resolve(parseResponseLine(line)); + cleanup(); + } catch (error) { + settled = true; + reject(error); + cleanup(); + } + }); + + socket.on("error", (error) => { + if (settled) { + return; + } + settled = true; + reject(error); + cleanup(); + }); + + socket.on("end", () => { + if (settled) { + return; + } + settled = true; + reject(new Error(`Orchestrator socket closed before a response was received: ${socketPath}`)); + cleanup(); + }); + }); +} diff --git a/packages/orchestrator/src/ipc/protocol.ts b/packages/orchestrator/src/ipc/protocol.ts new file mode 100644 index 00000000..7c275279 --- /dev/null +++ b/packages/orchestrator/src/ipc/protocol.ts @@ -0,0 +1,73 @@ +export interface SpawnRequest { + type: "spawn"; + cwd: string; + label?: string; + provider?: string; + model?: string; +} + +export interface ListRequest { + type: "list"; +} + +export interface StopRequest { + type: "stop"; + instanceId: string; +} + +export interface StatusRequest { + type: "status"; + instanceId: string; +} + +export type OrchestratorRequest = SpawnRequest | ListRequest | StopRequest | StatusRequest; + +export interface InstanceSummary { + id: string; + status: "starting" | "online" | "stopping" | "stopped" | "error"; + cwd: string; + label?: string; + sessionId?: string; +} + +export interface ResponseBase { + type: string; + ok: boolean; + error?: string; +} + +export interface SpawnResponse extends ResponseBase { + type: "spawn_result"; + instance?: InstanceSummary; +} + +export interface ListResponse extends ResponseBase { + type: "list_result"; + instances?: InstanceSummary[]; +} + +export interface StopResponse extends ResponseBase { + type: "stop_result"; + instanceId?: string; +} + +export interface StatusResponse extends ResponseBase { + type: "status_result"; + instance?: InstanceSummary; +} + +export type OrchestratorResponse = SpawnResponse | ListResponse | StopResponse | StatusResponse; + +export function encodeMessage(message: OrchestratorRequest | OrchestratorResponse): string { + return `${JSON.stringify(message)}\n`; +} + +export function parseRequestLine(line: string): OrchestratorRequest { + const value = JSON.parse(line) as OrchestratorRequest; + return value; +} + +export function parseResponseLine(line: string): OrchestratorResponse { + const value = JSON.parse(line) as OrchestratorResponse; + return value; +} From 60fb6dc6a6324d00df2a4a1b42856910705af315 Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:59:15 +0200 Subject: [PATCH 03/36] chore: add auth path getter too --- packages/orchestrator/src/config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/orchestrator/src/config.ts b/packages/orchestrator/src/config.ts index 64fb1008..fab11bed 100644 --- a/packages/orchestrator/src/config.ts +++ b/packages/orchestrator/src/config.ts @@ -14,6 +14,10 @@ export function getOrchestratorDir(): string { return join(piDir, "orchestrator"); } +export function getAuthPath(): string { + return join(getOrchestratorDir(), "auth.json"); +} + export function getMachinePath(): string { return join(getOrchestratorDir(), "machine.json"); } From 92e28e9c513f31fbfce29479b1f4a151dd69561e Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:06:34 +0200 Subject: [PATCH 04/36] feat: ipc server --- packages/orchestrator/src/index.ts | 1 + packages/orchestrator/src/ipc/protocol.ts | 31 ++++++- packages/orchestrator/src/ipc/server.ts | 107 ++++++++++++++++++++++ 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 packages/orchestrator/src/ipc/server.ts diff --git a/packages/orchestrator/src/index.ts b/packages/orchestrator/src/index.ts index 67ecbe7a..196332d8 100644 --- a/packages/orchestrator/src/index.ts +++ b/packages/orchestrator/src/index.ts @@ -1,3 +1,4 @@ export * from "./config.ts"; export * from "./ipc/client.ts"; export * from "./ipc/protocol.ts"; +export * from "./ipc/server.ts"; diff --git a/packages/orchestrator/src/ipc/protocol.ts b/packages/orchestrator/src/ipc/protocol.ts index 7c275279..ebb2815f 100644 --- a/packages/orchestrator/src/ipc/protocol.ts +++ b/packages/orchestrator/src/ipc/protocol.ts @@ -20,7 +20,14 @@ export interface StatusRequest { instanceId: string; } -export type OrchestratorRequest = SpawnRequest | ListRequest | StopRequest | StatusRequest; +export interface RequestMap { + spawn: SpawnRequest; + list: ListRequest; + stop: StopRequest; + status: StatusRequest; +} + +export type OrchestratorRequest = RequestMap[keyof RequestMap]; export interface InstanceSummary { id: string; @@ -31,7 +38,6 @@ export interface InstanceSummary { } export interface ResponseBase { - type: string; ok: boolean; error?: string; } @@ -56,7 +62,26 @@ export interface StatusResponse extends ResponseBase { instance?: InstanceSummary; } -export type OrchestratorResponse = SpawnResponse | ListResponse | StopResponse | StatusResponse; +export interface ErrorResponse extends ResponseBase { + type: "error"; + ok: false; + error: string; +} + +export interface ResponseMap { + spawn: SpawnResponse; + list: ListResponse; + stop: StopResponse; + status: StatusResponse; +} + +export type OrchestratorResponse = ResponseMap[keyof ResponseMap] | ErrorResponse; + +export type ResponseFor = T extends { type: infer K } + ? K extends keyof ResponseMap + ? ResponseMap[K] | ErrorResponse + : ErrorResponse + : ErrorResponse; export function encodeMessage(message: OrchestratorRequest | OrchestratorResponse): string { return `${JSON.stringify(message)}\n`; diff --git a/packages/orchestrator/src/ipc/server.ts b/packages/orchestrator/src/ipc/server.ts new file mode 100644 index 00000000..59d91b57 --- /dev/null +++ b/packages/orchestrator/src/ipc/server.ts @@ -0,0 +1,107 @@ +import { existsSync, unlinkSync } from "node:fs"; +import { createConnection, createServer, type Server } from "node:net"; +import { getSocketPath } from "../config.ts"; +import { + type ErrorResponse, + encodeMessage, + type OrchestratorRequest, + parseRequestLine, + type ResponseFor, +} from "./protocol.ts"; + +export type IpcRequestHandler = (request: T) => Promise> | ResponseFor; + +export async function startIpcServer(handler: IpcRequestHandler): Promise { + const socketPath = getSocketPath(); + await removeStaleSocketIfNeeded(socketPath); + + const server = createServer((socket) => { + let buffer = ""; + + socket.on("data", async (chunk: Buffer | string) => { + buffer += chunk.toString(); + const newlineIndex = buffer.indexOf("\n"); + if (newlineIndex === -1) { + return; + } + + const line = buffer.slice(0, newlineIndex).trim(); + buffer = buffer.slice(newlineIndex + 1); + if (!line) { + return; + } + + try { + const request = parseRequestLine(line); + const response = await handler(request); + socket.end(encodeMessage(response)); + } catch (error) { + const response: ErrorResponse = { + type: "error", + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + socket.end(encodeMessage(response)); + } + }); + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(socketPath, () => { + server.off("error", reject); + resolve(); + }); + }); + + return server; +} + +async function removeStaleSocketIfNeeded(socketPath: string): Promise { + if (!existsSync(socketPath)) { + return; + } + + const isLive = await isSocketLive(socketPath); + if (isLive) { + throw new Error(`orchestrator is already running: ${socketPath}`); + } + + unlinkSync(socketPath); +} + +async function isSocketLive(socketPath: string): Promise { + return new Promise((resolve, reject) => { + const socket = createConnection(socketPath); + let settled = false; + + const finish = (result: boolean) => { + if (settled) { + return; + } + settled = true; + socket.removeAllListeners(); + socket.destroy(); + resolve(result); + }; + + socket.on("connect", () => finish(true)); + socket.on("error", (error: NodeJS.ErrnoException) => { + if (error.code === "ECONNREFUSED" || error.code === "ENOENT") { + finish(false); + return; + } + if (error.code === "EPIPE" || error.code === "ECONNRESET") { + finish(false); + return; + } + if (settled) { + return; + } + settled = true; + socket.removeAllListeners(); + socket.destroy(); + reject(error); + }); + }); +} From 5f60fc01d748594318318d1f160bd4a93595c3b3 Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:09:48 +0200 Subject: [PATCH 05/36] feat: machine and instance storage --- packages/orchestrator/src/index.ts | 2 + packages/orchestrator/src/ipc/protocol.ts | 4 +- packages/orchestrator/src/storage.ts | 62 +++++++++++++++++++++++ packages/orchestrator/src/types.ts | 18 +++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 packages/orchestrator/src/storage.ts create mode 100644 packages/orchestrator/src/types.ts diff --git a/packages/orchestrator/src/index.ts b/packages/orchestrator/src/index.ts index 196332d8..4f017322 100644 --- a/packages/orchestrator/src/index.ts +++ b/packages/orchestrator/src/index.ts @@ -2,3 +2,5 @@ export * from "./config.ts"; export * from "./ipc/client.ts"; export * from "./ipc/protocol.ts"; export * from "./ipc/server.ts"; +export * from "./storage.ts"; +export * from "./types.ts"; diff --git a/packages/orchestrator/src/ipc/protocol.ts b/packages/orchestrator/src/ipc/protocol.ts index ebb2815f..87c24c7a 100644 --- a/packages/orchestrator/src/ipc/protocol.ts +++ b/packages/orchestrator/src/ipc/protocol.ts @@ -1,3 +1,5 @@ +import type { InstanceStatus } from "../types.ts"; + export interface SpawnRequest { type: "spawn"; cwd: string; @@ -31,7 +33,7 @@ export type OrchestratorRequest = RequestMap[keyof RequestMap]; export interface InstanceSummary { id: string; - status: "starting" | "online" | "stopping" | "stopped" | "error"; + status: InstanceStatus; cwd: string; label?: string; sessionId?: string; diff --git a/packages/orchestrator/src/storage.ts b/packages/orchestrator/src/storage.ts new file mode 100644 index 00000000..29eb3213 --- /dev/null +++ b/packages/orchestrator/src/storage.ts @@ -0,0 +1,62 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { getInstancesPath, getMachinePath, getOrchestratorDir } from "./config.ts"; +import type { InstanceRecord, MachineRecord } from "./types.ts"; + +function ensureOrchestratorDir(): void { + const orchestratorDir = getOrchestratorDir(); + if (!existsSync(orchestratorDir)) { + mkdirSync(orchestratorDir, { recursive: true }); + } +} + +export function loadMachine(): MachineRecord | undefined { + const machinePath = getMachinePath(); + if (!existsSync(machinePath)) { + return undefined; + } + + const data = readFileSync(machinePath, "utf-8"); + return JSON.parse(data) as MachineRecord; +} + +export function saveMachine(machine: MachineRecord): void { + ensureOrchestratorDir(); + writeFileSync(getMachinePath(), JSON.stringify(machine, null, 2)); +} + +export function loadInstances(): InstanceRecord[] { + const instancesPath = getInstancesPath(); + if (!existsSync(instancesPath)) { + return []; + } + + const data = readFileSync(instancesPath, "utf-8"); + return JSON.parse(data) as InstanceRecord[]; +} + +export function saveInstances(instances: InstanceRecord[]): void { + ensureOrchestratorDir(); + writeFileSync(getInstancesPath(), JSON.stringify(instances, null, 2)); +} + +export function getInstance(instanceId: string): InstanceRecord | undefined { + return loadInstances().find((instance) => instance.id === instanceId); +} + +export function upsertInstance(instance: InstanceRecord): void { + const instances = loadInstances(); + const index = instances.findIndex((existing) => existing.id === instance.id); + if (index === -1) { + instances.push(instance); + saveInstances(instances); + return; + } + + instances[index] = instance; + saveInstances(instances); +} + +export function removeInstance(instanceId: string): void { + const instances = loadInstances().filter((instance) => instance.id !== instanceId); + saveInstances(instances); +} diff --git a/packages/orchestrator/src/types.ts b/packages/orchestrator/src/types.ts new file mode 100644 index 00000000..d2e4d06e --- /dev/null +++ b/packages/orchestrator/src/types.ts @@ -0,0 +1,18 @@ +export type InstanceStatus = "starting" | "online" | "stopping" | "stopped" | "error"; + +export interface MachineRecord { + id: string; + createdAt: string; + lastSeenAt?: string; + label?: string; +} + +export interface InstanceRecord { + id: string; + status: InstanceStatus; + cwd: string; + createdAt: string; + lastSeenAt?: string; + label?: string; + sessionId?: string; +} From 83e8c3d39ba6576e3c902f8777ec3d95b05a74bd Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:14:01 +0200 Subject: [PATCH 06/36] feat: handler --- packages/orchestrator/src/handler.ts | 96 +++++++++++++++++++++++++ packages/orchestrator/src/index.ts | 1 + packages/orchestrator/src/ipc/server.ts | 18 ++++- 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 packages/orchestrator/src/handler.ts diff --git a/packages/orchestrator/src/handler.ts b/packages/orchestrator/src/handler.ts new file mode 100644 index 00000000..894308bb --- /dev/null +++ b/packages/orchestrator/src/handler.ts @@ -0,0 +1,96 @@ +import { randomUUID } from "node:crypto"; +import type { + ErrorResponse, + InstanceSummary, + ListRequest, + ListResponse, + OrchestratorRequest, + OrchestratorResponse, + SpawnRequest, + SpawnResponse, + StatusRequest, + StatusResponse, + StopRequest, + StopResponse, +} from "./ipc/protocol.ts"; +import { getInstance, loadInstances, removeInstance, upsertInstance } from "./storage.ts"; +import type { InstanceRecord } from "./types.ts"; + +function toInstanceSummary(instance: InstanceRecord): InstanceSummary { + return { + id: instance.id, + status: instance.status, + cwd: instance.cwd, + label: instance.label, + sessionId: instance.sessionId, + }; +} + +function unknownInstanceError(instanceId: string): ErrorResponse { + return { + type: "error", + ok: false, + error: `Unknown instance: ${instanceId}`, + }; +} + +// Overhead types +export async function handleIpcRequest(request: SpawnRequest): Promise; +export async function handleIpcRequest(request: ListRequest): Promise; +export async function handleIpcRequest(request: StopRequest): Promise; +export async function handleIpcRequest(request: StatusRequest): Promise; +export async function handleIpcRequest(request: OrchestratorRequest): Promise { + switch (request.type) { + case "spawn": { + const instance: InstanceRecord = { + id: randomUUID(), + status: "starting", + cwd: request.cwd, + createdAt: new Date().toISOString(), + lastSeenAt: new Date().toISOString(), + label: request.label, + }; + upsertInstance(instance); + return { + type: "spawn_result", + ok: true, + instance: toInstanceSummary(instance), + }; + } + + case "list": { + return { + type: "list_result", + ok: true, + instances: loadInstances().map(toInstanceSummary), + }; + } + + case "status": { + const instance = getInstance(request.instanceId); + if (!instance) { + return unknownInstanceError(request.instanceId); + } + + return { + type: "status_result", + ok: true, + instance: toInstanceSummary(instance), + }; + } + + case "stop": { + const instance = getInstance(request.instanceId); + if (!instance) { + return unknownInstanceError(request.instanceId); + } + + removeInstance(request.instanceId); + return { + type: "stop_result", + ok: true, + instanceId: request.instanceId, + }; + } + } +} diff --git a/packages/orchestrator/src/index.ts b/packages/orchestrator/src/index.ts index 4f017322..e16fbc5f 100644 --- a/packages/orchestrator/src/index.ts +++ b/packages/orchestrator/src/index.ts @@ -1,4 +1,5 @@ export * from "./config.ts"; +export * from "./handler.ts"; export * from "./ipc/client.ts"; export * from "./ipc/protocol.ts"; export * from "./ipc/server.ts"; diff --git a/packages/orchestrator/src/ipc/server.ts b/packages/orchestrator/src/ipc/server.ts index 59d91b57..860181b9 100644 --- a/packages/orchestrator/src/ipc/server.ts +++ b/packages/orchestrator/src/ipc/server.ts @@ -4,12 +4,26 @@ import { getSocketPath } from "../config.ts"; import { type ErrorResponse, encodeMessage, + type ListRequest, + type ListResponse, type OrchestratorRequest, + type OrchestratorResponse, parseRequestLine, - type ResponseFor, + type SpawnRequest, + type SpawnResponse, + type StatusRequest, + type StatusResponse, + type StopRequest, + type StopResponse, } from "./protocol.ts"; -export type IpcRequestHandler = (request: T) => Promise> | ResponseFor; +export interface IpcRequestHandler { + (request: SpawnRequest): Promise | SpawnResponse | ErrorResponse; + (request: ListRequest): Promise | ListResponse | ErrorResponse; + (request: StopRequest): Promise | StopResponse | ErrorResponse; + (request: StatusRequest): Promise | StatusResponse | ErrorResponse; + (request: OrchestratorRequest): Promise | OrchestratorResponse; +} export async function startIpcServer(handler: IpcRequestHandler): Promise { const socketPath = getSocketPath(); From 946b9c7dee4ffc89d125616d6c000afa9a0c27f5 Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:16:10 +0200 Subject: [PATCH 07/36] feat: serve and command --- packages/orchestrator/src/cli.ts | 36 ++++++++++++++++++---------- packages/orchestrator/src/handler.ts | 1 + packages/orchestrator/src/index.ts | 1 + packages/orchestrator/src/serve.ts | 26 ++++++++++++++++++++ 4 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 packages/orchestrator/src/serve.ts diff --git a/packages/orchestrator/src/cli.ts b/packages/orchestrator/src/cli.ts index 0ad2da39..e6b7da5f 100644 --- a/packages/orchestrator/src/cli.ts +++ b/packages/orchestrator/src/cli.ts @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { serve } from "./serve.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -10,21 +11,32 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), }; function printHelp(): void { - console.log(`orchestrator v${packageJson.version}\n\nUsage:\n orchestrator --help\n orchestrator --version`); + console.log( + `orchestrator v${packageJson.version}\n\nUsage:\n orchestrator serve\n orchestrator --help\n orchestrator --version`, + ); } -const args = process.argv.slice(2); +async function main(): Promise { + const args = process.argv.slice(2); -if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { + if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { + printHelp(); + process.exit(0); + } + + if (args[0] === "--version" || args[0] === "-v") { + console.log(packageJson.version); + process.exit(0); + } + + if (args[0] === "serve") { + await serve(); + return; + } + + console.error(`Unknown command: ${args[0]}`); printHelp(); - process.exit(0); + process.exit(1); } -if (args[0] === "--version" || args[0] === "-v") { - console.log(packageJson.version); - process.exit(0); -} - -console.error(`Unknown command: ${args[0]}`); -printHelp(); -process.exit(1); +await main(); diff --git a/packages/orchestrator/src/handler.ts b/packages/orchestrator/src/handler.ts index 894308bb..0e894ba0 100644 --- a/packages/orchestrator/src/handler.ts +++ b/packages/orchestrator/src/handler.ts @@ -39,6 +39,7 @@ export async function handleIpcRequest(request: SpawnRequest): Promise; export async function handleIpcRequest(request: StopRequest): Promise; export async function handleIpcRequest(request: StatusRequest): Promise; +export async function handleIpcRequest(request: OrchestratorRequest): Promise; export async function handleIpcRequest(request: OrchestratorRequest): Promise { switch (request.type) { case "spawn": { diff --git a/packages/orchestrator/src/index.ts b/packages/orchestrator/src/index.ts index e16fbc5f..459cd58f 100644 --- a/packages/orchestrator/src/index.ts +++ b/packages/orchestrator/src/index.ts @@ -3,5 +3,6 @@ export * from "./handler.ts"; export * from "./ipc/client.ts"; export * from "./ipc/protocol.ts"; export * from "./ipc/server.ts"; +export * from "./serve.ts"; export * from "./storage.ts"; export * from "./types.ts"; diff --git a/packages/orchestrator/src/serve.ts b/packages/orchestrator/src/serve.ts new file mode 100644 index 00000000..1651086b --- /dev/null +++ b/packages/orchestrator/src/serve.ts @@ -0,0 +1,26 @@ +import { existsSync, unlinkSync } from "node:fs"; +import { getSocketPath } from "./config.ts"; +import { handleIpcRequest } from "./handler.ts"; +import { startIpcServer } from "./ipc/server.ts"; + +export async function serve(): Promise { + const socketPath = getSocketPath(); + const server = await startIpcServer(handleIpcRequest); + + const cleanup = () => { + server.close(); + if (existsSync(socketPath)) { + unlinkSync(socketPath); + } + }; + + process.on("SIGINT", () => { + cleanup(); + process.exit(0); + }); + + process.on("SIGTERM", () => { + cleanup(); + process.exit(0); + }); +} From 2a2dc0e99f1aabacdd346fb792b3a6fb222fa6b9 Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:19:03 +0200 Subject: [PATCH 08/36] feat: commands --- packages/orchestrator/src/cli.ts | 48 +++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/orchestrator/src/cli.ts b/packages/orchestrator/src/cli.ts index e6b7da5f..7e057634 100644 --- a/packages/orchestrator/src/cli.ts +++ b/packages/orchestrator/src/cli.ts @@ -1,7 +1,9 @@ #!/usr/bin/env node import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; +import { cwd } from "node:process"; import { fileURLToPath } from "node:url"; +import { sendIpcRequest } from "./ipc/client.ts"; import { serve } from "./serve.ts"; const __filename = fileURLToPath(import.meta.url); @@ -12,10 +14,22 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), function printHelp(): void { console.log( - `orchestrator v${packageJson.version}\n\nUsage:\n orchestrator serve\n orchestrator --help\n orchestrator --version`, + `orchestrator v${packageJson.version}\n\nUsage:\n orchestrator serve\n orchestrator list\n orchestrator spawn [--cwd ] [--label