@openspecui/server 1.0.4 → 1.1.2
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/dist/index.mjs +634 -652
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,413 @@
|
|
|
1
1
|
import { serve } from "@hono/node-server";
|
|
2
|
-
import {
|
|
3
|
-
import { cors } from "hono/cors";
|
|
2
|
+
import { CliExecutor, ConfigManager, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, initWatcherPool, isWatcherPoolInitialized, sniffGlobalCli } from "@openspecui/core";
|
|
4
3
|
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
5
4
|
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { cors } from "hono/cors";
|
|
6
7
|
import { WebSocketServer } from "ws";
|
|
7
|
-
import {
|
|
8
|
+
import { createServer as createServer$1 } from "node:net";
|
|
9
|
+
import * as pty from "@lydell/node-pty";
|
|
10
|
+
import { EventEmitter } from "events";
|
|
11
|
+
import { SearchQuerySchema } from "@openspecui/search";
|
|
8
12
|
import { initTRPC } from "@trpc/server";
|
|
9
13
|
import { observable } from "@trpc/server/observable";
|
|
10
14
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
11
|
-
import { dirname, join,
|
|
15
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
12
16
|
import { z } from "zod";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
18
|
+
import { NodeWorkerSearchProvider } from "@openspecui/search/node";
|
|
19
|
+
|
|
20
|
+
//#region src/port-utils.ts
|
|
21
|
+
/**
|
|
22
|
+
* Check if a port is available by trying to listen on it.
|
|
23
|
+
* Uses default binding (both IPv4 and IPv6) to detect conflicts.
|
|
24
|
+
*/
|
|
25
|
+
function isPortAvailable(port) {
|
|
26
|
+
return new Promise((resolve$1) => {
|
|
27
|
+
const server = createServer$1();
|
|
28
|
+
server.once("error", () => {
|
|
29
|
+
resolve$1(false);
|
|
30
|
+
});
|
|
31
|
+
server.once("listening", () => {
|
|
32
|
+
server.close(() => resolve$1(true));
|
|
33
|
+
});
|
|
34
|
+
server.listen(port);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Find an available port starting from the given port.
|
|
39
|
+
* Will try up to maxAttempts ports sequentially.
|
|
40
|
+
*
|
|
41
|
+
* @param startPort - The preferred port to start checking from
|
|
42
|
+
* @param maxAttempts - Maximum number of ports to try (default: 10)
|
|
43
|
+
* @returns The first available port found
|
|
44
|
+
* @throws Error if no available port is found in the range
|
|
45
|
+
*/
|
|
46
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
47
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
48
|
+
const port = startPort + i;
|
|
49
|
+
if (await isPortAvailable(port)) return port;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/pty-manager.ts
|
|
56
|
+
const DEFAULT_SCROLLBACK = 1e3;
|
|
57
|
+
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
58
|
+
function detectPtyPlatform() {
|
|
59
|
+
if (process.platform === "win32") return "windows";
|
|
60
|
+
if (process.platform === "darwin") return "macos";
|
|
61
|
+
return "common";
|
|
62
|
+
}
|
|
63
|
+
function resolveDefaultShell(platform, env) {
|
|
64
|
+
if (platform === "windows") return env.ComSpec?.trim() || "cmd.exe";
|
|
65
|
+
return env.SHELL?.trim() || "/bin/sh";
|
|
66
|
+
}
|
|
67
|
+
function resolvePtyCommand(opts) {
|
|
68
|
+
const command = opts.command?.trim();
|
|
69
|
+
if (command) return {
|
|
70
|
+
command,
|
|
71
|
+
args: opts.args ?? []
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
command: resolveDefaultShell(opts.platform, opts.env),
|
|
75
|
+
args: []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var PtySession = class extends EventEmitter {
|
|
79
|
+
id;
|
|
80
|
+
command;
|
|
81
|
+
args;
|
|
82
|
+
platform;
|
|
83
|
+
createdAt;
|
|
84
|
+
process;
|
|
85
|
+
titleInterval = null;
|
|
86
|
+
lastTitle = "";
|
|
87
|
+
buffer = [];
|
|
88
|
+
bufferByteLength = 0;
|
|
89
|
+
maxBufferLines;
|
|
90
|
+
maxBufferBytes;
|
|
91
|
+
isExited = false;
|
|
92
|
+
exitCode = null;
|
|
93
|
+
constructor(id, opts) {
|
|
94
|
+
super();
|
|
95
|
+
this.id = id;
|
|
96
|
+
this.createdAt = Date.now();
|
|
97
|
+
const resolvedCommand = resolvePtyCommand({
|
|
98
|
+
platform: opts.platform,
|
|
99
|
+
command: opts.command,
|
|
100
|
+
args: opts.args,
|
|
101
|
+
env: process.env
|
|
102
|
+
});
|
|
103
|
+
this.command = resolvedCommand.command;
|
|
104
|
+
this.args = resolvedCommand.args;
|
|
105
|
+
this.platform = opts.platform;
|
|
106
|
+
this.maxBufferLines = opts.scrollback ?? DEFAULT_SCROLLBACK;
|
|
107
|
+
this.maxBufferBytes = opts.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
108
|
+
this.process = pty.spawn(this.command, this.args, {
|
|
109
|
+
name: "xterm-256color",
|
|
110
|
+
cols: opts.cols ?? 80,
|
|
111
|
+
rows: opts.rows ?? 24,
|
|
112
|
+
cwd: opts.cwd,
|
|
113
|
+
env: {
|
|
114
|
+
...process.env,
|
|
115
|
+
TERM: "xterm-256color"
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
this.process.onData((data) => {
|
|
119
|
+
this.appendBuffer(data);
|
|
120
|
+
this.emit("data", data);
|
|
121
|
+
});
|
|
122
|
+
this.process.onExit(({ exitCode }) => {
|
|
123
|
+
if (this.titleInterval) {
|
|
124
|
+
clearInterval(this.titleInterval);
|
|
125
|
+
this.titleInterval = null;
|
|
126
|
+
}
|
|
127
|
+
this.isExited = true;
|
|
128
|
+
this.exitCode = exitCode;
|
|
129
|
+
this.emit("exit", exitCode);
|
|
130
|
+
});
|
|
131
|
+
this.titleInterval = setInterval(() => {
|
|
132
|
+
try {
|
|
133
|
+
const title = this.process.process;
|
|
134
|
+
if (title && title !== this.lastTitle) {
|
|
135
|
+
this.lastTitle = title;
|
|
136
|
+
this.emit("title", title);
|
|
137
|
+
}
|
|
138
|
+
} catch {}
|
|
139
|
+
}, 1e3);
|
|
140
|
+
}
|
|
141
|
+
get title() {
|
|
142
|
+
return this.lastTitle;
|
|
143
|
+
}
|
|
144
|
+
appendBuffer(data) {
|
|
145
|
+
let chunk = data;
|
|
146
|
+
if (chunk.length > this.maxBufferBytes) chunk = chunk.slice(-this.maxBufferBytes);
|
|
147
|
+
this.buffer.push(chunk);
|
|
148
|
+
this.bufferByteLength += chunk.length;
|
|
149
|
+
while (this.bufferByteLength > this.maxBufferBytes && this.buffer.length > 0) {
|
|
150
|
+
const removed = this.buffer.shift();
|
|
151
|
+
this.bufferByteLength -= removed.length;
|
|
152
|
+
}
|
|
153
|
+
while (this.buffer.length > this.maxBufferLines) {
|
|
154
|
+
const removed = this.buffer.shift();
|
|
155
|
+
this.bufferByteLength -= removed.length;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
getBuffer() {
|
|
159
|
+
return this.buffer.join("");
|
|
160
|
+
}
|
|
161
|
+
write(data) {
|
|
162
|
+
if (!this.isExited) this.process.write(data);
|
|
163
|
+
}
|
|
164
|
+
resize(cols, rows) {
|
|
165
|
+
if (!this.isExited) this.process.resize(cols, rows);
|
|
166
|
+
}
|
|
167
|
+
close() {
|
|
168
|
+
if (this.titleInterval) {
|
|
169
|
+
clearInterval(this.titleInterval);
|
|
170
|
+
this.titleInterval = null;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
this.process.kill();
|
|
174
|
+
} catch {}
|
|
175
|
+
this.removeAllListeners();
|
|
176
|
+
}
|
|
177
|
+
toInfo() {
|
|
178
|
+
return {
|
|
179
|
+
id: this.id,
|
|
180
|
+
title: this.lastTitle,
|
|
181
|
+
command: this.command,
|
|
182
|
+
args: this.args,
|
|
183
|
+
platform: this.platform,
|
|
184
|
+
isExited: this.isExited,
|
|
185
|
+
exitCode: this.exitCode,
|
|
186
|
+
createdAt: this.createdAt
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var PtyManager = class {
|
|
191
|
+
sessions = /* @__PURE__ */ new Map();
|
|
192
|
+
idCounter = 0;
|
|
193
|
+
platform;
|
|
194
|
+
constructor(defaultCwd) {
|
|
195
|
+
this.defaultCwd = defaultCwd;
|
|
196
|
+
this.platform = detectPtyPlatform();
|
|
197
|
+
}
|
|
198
|
+
create(opts) {
|
|
199
|
+
const id = `pty-${++this.idCounter}`;
|
|
200
|
+
const session = new PtySession(id, {
|
|
201
|
+
cols: opts.cols,
|
|
202
|
+
rows: opts.rows,
|
|
203
|
+
command: opts.command,
|
|
204
|
+
args: opts.args,
|
|
205
|
+
cwd: this.defaultCwd,
|
|
206
|
+
scrollback: opts.scrollback,
|
|
207
|
+
maxBufferBytes: opts.maxBufferBytes,
|
|
208
|
+
platform: this.platform
|
|
209
|
+
});
|
|
210
|
+
this.sessions.set(id, session);
|
|
211
|
+
return session;
|
|
212
|
+
}
|
|
213
|
+
get(id) {
|
|
214
|
+
return this.sessions.get(id);
|
|
215
|
+
}
|
|
216
|
+
list() {
|
|
217
|
+
const result = [];
|
|
218
|
+
for (const session of this.sessions.values()) result.push(session.toInfo());
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
write(id, data) {
|
|
222
|
+
this.sessions.get(id)?.write(data);
|
|
223
|
+
}
|
|
224
|
+
resize(id, cols, rows) {
|
|
225
|
+
this.sessions.get(id)?.resize(cols, rows);
|
|
226
|
+
}
|
|
227
|
+
close(id) {
|
|
228
|
+
const session = this.sessions.get(id);
|
|
229
|
+
if (session) {
|
|
230
|
+
session.close();
|
|
231
|
+
this.sessions.delete(id);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
closeAll() {
|
|
235
|
+
for (const session of this.sessions.values()) session.close();
|
|
236
|
+
this.sessions.clear();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/pty-websocket.ts
|
|
242
|
+
function createPtyWebSocketHandler(ptyManager) {
|
|
243
|
+
return (ws) => {
|
|
244
|
+
const cleanups = /* @__PURE__ */ new Map();
|
|
245
|
+
const send = (msg) => {
|
|
246
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
|
|
247
|
+
};
|
|
248
|
+
const sendError = (code, message, opts) => {
|
|
249
|
+
send({
|
|
250
|
+
type: "error",
|
|
251
|
+
code,
|
|
252
|
+
message,
|
|
253
|
+
sessionId: opts?.sessionId
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const attachToSession = (session, opts) => {
|
|
257
|
+
const sessionId = session.id;
|
|
258
|
+
cleanups.get(sessionId)?.();
|
|
259
|
+
if (opts?.cols && opts?.rows && !session.isExited) session.resize(opts.cols, opts.rows);
|
|
260
|
+
const onData = (data) => {
|
|
261
|
+
send({
|
|
262
|
+
type: "output",
|
|
263
|
+
sessionId,
|
|
264
|
+
data
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
const onExit = (exitCode) => {
|
|
268
|
+
send({
|
|
269
|
+
type: "exit",
|
|
270
|
+
sessionId,
|
|
271
|
+
exitCode
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
const onTitle = (title) => {
|
|
275
|
+
send({
|
|
276
|
+
type: "title",
|
|
277
|
+
sessionId,
|
|
278
|
+
title
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
session.on("data", onData);
|
|
282
|
+
session.on("exit", onExit);
|
|
283
|
+
session.on("title", onTitle);
|
|
284
|
+
cleanups.set(sessionId, () => {
|
|
285
|
+
session.removeListener("data", onData);
|
|
286
|
+
session.removeListener("exit", onExit);
|
|
287
|
+
session.removeListener("title", onTitle);
|
|
288
|
+
cleanups.delete(sessionId);
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
ws.on("message", (raw) => {
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(String(raw));
|
|
295
|
+
} catch {
|
|
296
|
+
sendError("INVALID_JSON", "Invalid JSON payload");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const parsedMessage = PtyClientMessageSchema.safeParse(parsed);
|
|
300
|
+
if (!parsedMessage.success) {
|
|
301
|
+
const firstIssue = parsedMessage.error.issues[0]?.message;
|
|
302
|
+
sendError("INVALID_MESSAGE", firstIssue ?? "Invalid PTY message");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const msg = parsedMessage.data;
|
|
306
|
+
switch (msg.type) {
|
|
307
|
+
case "create":
|
|
308
|
+
try {
|
|
309
|
+
const session = ptyManager.create({
|
|
310
|
+
cols: msg.cols,
|
|
311
|
+
rows: msg.rows,
|
|
312
|
+
command: msg.command,
|
|
313
|
+
args: msg.args
|
|
314
|
+
});
|
|
315
|
+
send({
|
|
316
|
+
type: "created",
|
|
317
|
+
requestId: msg.requestId,
|
|
318
|
+
sessionId: session.id,
|
|
319
|
+
platform: session.platform
|
|
320
|
+
});
|
|
321
|
+
attachToSession(session);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
sendError("PTY_CREATE_FAILED", err instanceof Error ? err.message : String(err), { sessionId: msg.requestId });
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
case "attach": {
|
|
327
|
+
const session = ptyManager.get(msg.sessionId);
|
|
328
|
+
if (!session) {
|
|
329
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
330
|
+
send({
|
|
331
|
+
type: "exit",
|
|
332
|
+
sessionId: msg.sessionId,
|
|
333
|
+
exitCode: -1
|
|
334
|
+
});
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
attachToSession(session, {
|
|
338
|
+
cols: msg.cols,
|
|
339
|
+
rows: msg.rows
|
|
340
|
+
});
|
|
341
|
+
const buffer = session.getBuffer();
|
|
342
|
+
if (buffer) send({
|
|
343
|
+
type: "buffer",
|
|
344
|
+
sessionId: session.id,
|
|
345
|
+
data: buffer
|
|
346
|
+
});
|
|
347
|
+
if (session.title) send({
|
|
348
|
+
type: "title",
|
|
349
|
+
sessionId: session.id,
|
|
350
|
+
title: session.title
|
|
351
|
+
});
|
|
352
|
+
if (session.isExited) send({
|
|
353
|
+
type: "exit",
|
|
354
|
+
sessionId: session.id,
|
|
355
|
+
exitCode: session.exitCode ?? -1
|
|
356
|
+
});
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case "list":
|
|
360
|
+
send({
|
|
361
|
+
type: "list",
|
|
362
|
+
sessions: ptyManager.list().map((s) => ({
|
|
363
|
+
id: s.id,
|
|
364
|
+
title: s.title,
|
|
365
|
+
command: s.command,
|
|
366
|
+
args: s.args,
|
|
367
|
+
platform: s.platform,
|
|
368
|
+
isExited: s.isExited,
|
|
369
|
+
exitCode: s.exitCode
|
|
370
|
+
}))
|
|
371
|
+
});
|
|
372
|
+
break;
|
|
373
|
+
case "input": {
|
|
374
|
+
const session = ptyManager.get(msg.sessionId);
|
|
375
|
+
if (!session) {
|
|
376
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
session.write(msg.data);
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case "resize": {
|
|
383
|
+
const session = ptyManager.get(msg.sessionId);
|
|
384
|
+
if (!session) {
|
|
385
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
session.resize(msg.cols, msg.rows);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case "close": {
|
|
392
|
+
const session = ptyManager.get(msg.sessionId);
|
|
393
|
+
if (!session) {
|
|
394
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
cleanups.get(msg.sessionId)?.();
|
|
398
|
+
ptyManager.close(session.id);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
ws.on("close", () => {
|
|
404
|
+
for (const cleanup of cleanups.values()) cleanup();
|
|
405
|
+
cleanups.clear();
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
}
|
|
18
409
|
|
|
410
|
+
//#endregion
|
|
19
411
|
//#region src/cli-stream-observable.ts
|
|
20
412
|
/**
|
|
21
413
|
* 创建安全的 CLI 流式 observable
|
|
@@ -72,53 +464,6 @@ function createCliStreamObservable(startStream) {
|
|
|
72
464
|
});
|
|
73
465
|
}
|
|
74
466
|
|
|
75
|
-
//#endregion
|
|
76
|
-
//#region src/opsx-schema.ts
|
|
77
|
-
const SchemaYamlArtifactSchema = z.object({
|
|
78
|
-
id: z.string(),
|
|
79
|
-
generates: z.string(),
|
|
80
|
-
description: z.string().optional(),
|
|
81
|
-
template: z.string().optional(),
|
|
82
|
-
instruction: z.string().optional(),
|
|
83
|
-
requires: z.array(z.string()).optional()
|
|
84
|
-
});
|
|
85
|
-
const SchemaYamlSchema = z.object({
|
|
86
|
-
name: z.string(),
|
|
87
|
-
version: z.union([z.string(), z.number()]).optional(),
|
|
88
|
-
description: z.string().optional(),
|
|
89
|
-
artifacts: z.array(SchemaYamlArtifactSchema),
|
|
90
|
-
apply: z.object({
|
|
91
|
-
requires: z.array(z.string()).optional(),
|
|
92
|
-
tracks: z.string().optional(),
|
|
93
|
-
instruction: z.string().optional()
|
|
94
|
-
}).optional()
|
|
95
|
-
});
|
|
96
|
-
function parseSchemaYaml(content) {
|
|
97
|
-
const raw = parse(content);
|
|
98
|
-
const parsed = SchemaYamlSchema.safeParse(raw);
|
|
99
|
-
if (!parsed.success) throw new Error(`Invalid schema.yaml: ${parsed.error.message}`);
|
|
100
|
-
const { artifacts, apply, name, description, version } = parsed.data;
|
|
101
|
-
const detail = {
|
|
102
|
-
name,
|
|
103
|
-
description,
|
|
104
|
-
version,
|
|
105
|
-
artifacts: artifacts.map((artifact) => ({
|
|
106
|
-
id: artifact.id,
|
|
107
|
-
outputPath: artifact.generates,
|
|
108
|
-
description: artifact.description,
|
|
109
|
-
template: artifact.template,
|
|
110
|
-
instruction: artifact.instruction,
|
|
111
|
-
requires: artifact.requires ?? []
|
|
112
|
-
})),
|
|
113
|
-
applyRequires: apply?.requires ?? [],
|
|
114
|
-
applyTracks: apply?.tracks,
|
|
115
|
-
applyInstruction: apply?.instruction
|
|
116
|
-
};
|
|
117
|
-
const validated = SchemaDetailSchema.safeParse(detail);
|
|
118
|
-
if (!validated.success) throw new Error(`Invalid schema detail: ${validated.error.message}`);
|
|
119
|
-
return validated.data;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
467
|
//#endregion
|
|
123
468
|
//#region src/reactive-kv.ts
|
|
124
469
|
/**
|
|
@@ -131,7 +476,7 @@ function parseSchemaYaml(content) {
|
|
|
131
476
|
*/
|
|
132
477
|
var ReactiveKV = class {
|
|
133
478
|
store = /* @__PURE__ */ new Map();
|
|
134
|
-
emitter = new EventEmitter();
|
|
479
|
+
emitter = new EventEmitter$1();
|
|
135
480
|
constructor() {
|
|
136
481
|
this.emitter.setMaxListeners(200);
|
|
137
482
|
}
|
|
@@ -246,20 +591,6 @@ function requireChangeId(changeId) {
|
|
|
246
591
|
function ensureEditableSource(source, label) {
|
|
247
592
|
if (source === "package") throw new Error(`${label} is read-only (package source)`);
|
|
248
593
|
}
|
|
249
|
-
function parseCliJson(raw, schema, label) {
|
|
250
|
-
const trimmed = raw.trim();
|
|
251
|
-
if (!trimmed) throw new Error(`${label} returned empty output`);
|
|
252
|
-
let parsed;
|
|
253
|
-
try {
|
|
254
|
-
parsed = JSON.parse(trimmed);
|
|
255
|
-
} catch (err) {
|
|
256
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
257
|
-
throw new Error(`${label} returned invalid JSON: ${message}`);
|
|
258
|
-
}
|
|
259
|
-
const result = schema.safeParse(parsed);
|
|
260
|
-
if (!result.success) throw new Error(`${label} returned unexpected JSON: ${result.error.message}`);
|
|
261
|
-
return result.data;
|
|
262
|
-
}
|
|
263
594
|
function resolveEntryPath(root, entryPath) {
|
|
264
595
|
const normalizedRoot = resolve(root);
|
|
265
596
|
const resolvedPath = resolve(normalizedRoot, entryPath);
|
|
@@ -267,153 +598,62 @@ function resolveEntryPath(root, entryPath) {
|
|
|
267
598
|
if (resolvedPath !== normalizedRoot && !resolvedPath.startsWith(rootPrefix)) throw new Error("Invalid path: outside schema root");
|
|
268
599
|
return resolvedPath;
|
|
269
600
|
}
|
|
270
|
-
function toRelativePath(root, absolutePath) {
|
|
271
|
-
return relative(root, absolutePath).split(sep).join("/");
|
|
272
|
-
}
|
|
273
|
-
async function readEntriesUnderRoot(root) {
|
|
274
|
-
if (!(await reactiveStat(root))?.isDirectory) return [];
|
|
275
|
-
const collectEntries = async (dir) => {
|
|
276
|
-
const names = await reactiveReadDir(dir, { includeHidden: false });
|
|
277
|
-
const entries = [];
|
|
278
|
-
for (const name of names) {
|
|
279
|
-
const fullPath = join(dir, name);
|
|
280
|
-
const statInfo = await reactiveStat(fullPath);
|
|
281
|
-
if (!statInfo) continue;
|
|
282
|
-
const relativePath = toRelativePath(root, fullPath);
|
|
283
|
-
if (statInfo.isDirectory) {
|
|
284
|
-
entries.push({
|
|
285
|
-
path: relativePath,
|
|
286
|
-
type: "directory"
|
|
287
|
-
});
|
|
288
|
-
entries.push(...await collectEntries(fullPath));
|
|
289
|
-
} else {
|
|
290
|
-
const content = await reactiveReadFile(fullPath);
|
|
291
|
-
const size = content ? Buffer.byteLength(content, "utf-8") : void 0;
|
|
292
|
-
entries.push({
|
|
293
|
-
path: relativePath,
|
|
294
|
-
type: "file",
|
|
295
|
-
content: content ?? void 0,
|
|
296
|
-
size
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return entries;
|
|
301
|
-
};
|
|
302
|
-
return collectEntries(root);
|
|
303
|
-
}
|
|
304
|
-
async function touchOpsxProjectDeps(projectDir) {
|
|
305
|
-
const openspecDir = join(projectDir, "openspec");
|
|
306
|
-
await reactiveReadFile(join(openspecDir, "config.yaml"));
|
|
307
|
-
const schemaRoot = join(openspecDir, "schemas");
|
|
308
|
-
const schemaDirs = await reactiveReadDir(schemaRoot, {
|
|
309
|
-
directoriesOnly: true,
|
|
310
|
-
includeHidden: true
|
|
311
|
-
});
|
|
312
|
-
await Promise.all(schemaDirs.map((name) => reactiveReadFile(join(schemaRoot, name, "schema.yaml"))));
|
|
313
|
-
await reactiveReadDir(join(openspecDir, "changes"), {
|
|
314
|
-
directoriesOnly: true,
|
|
315
|
-
includeHidden: true,
|
|
316
|
-
exclude: ["archive"]
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
async function touchOpsxChangeDeps(projectDir, changeId) {
|
|
320
|
-
const changeDir = join(projectDir, "openspec", "changes", changeId);
|
|
321
|
-
await reactiveReadDir(changeDir, { includeHidden: true });
|
|
322
|
-
await reactiveReadFile(join(changeDir, ".openspec.yaml"));
|
|
323
|
-
}
|
|
324
|
-
async function readGlobArtifactFiles(projectDir, changeId, outputPath) {
|
|
325
|
-
return (await readEntriesUnderRoot(join(projectDir, "openspec", "changes", changeId))).filter((entry) => entry.type === "file" && matchesGlob(entry.path, outputPath)).map((entry) => ({
|
|
326
|
-
path: entry.path,
|
|
327
|
-
type: "file",
|
|
328
|
-
content: entry.content ?? ""
|
|
329
|
-
}));
|
|
330
|
-
}
|
|
331
601
|
async function fetchOpsxStatus(ctx, input) {
|
|
332
602
|
const changeId = requireChangeId(input.change);
|
|
333
|
-
await
|
|
334
|
-
await
|
|
335
|
-
|
|
336
|
-
"status",
|
|
337
|
-
"--json",
|
|
338
|
-
"--change",
|
|
339
|
-
changeId
|
|
340
|
-
];
|
|
341
|
-
if (input.schema) args.push("--schema", input.schema);
|
|
342
|
-
const result = await ctx.cliExecutor.execute(args);
|
|
343
|
-
if (!result.success) throw new Error(result.stderr || `openspec status failed (exit ${result.exitCode ?? "null"})`);
|
|
344
|
-
const status = parseCliJson(result.stdout, ChangeStatusSchema, "openspec status");
|
|
345
|
-
const changeRelDir = `openspec/changes/${changeId}`;
|
|
346
|
-
for (const artifact of status.artifacts) artifact.relativePath = `${changeRelDir}/${artifact.outputPath}`;
|
|
347
|
-
return status;
|
|
603
|
+
await ctx.kernel.waitForWarmup();
|
|
604
|
+
await ctx.kernel.ensureStatus(changeId, input.schema);
|
|
605
|
+
return ctx.kernel.getStatus(changeId, input.schema);
|
|
348
606
|
}
|
|
349
607
|
async function fetchOpsxStatusList(ctx) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
exclude: ["archive"]
|
|
354
|
-
});
|
|
355
|
-
return await Promise.all(changeIds.map((changeId) => fetchOpsxStatus(ctx, { change: changeId })));
|
|
608
|
+
await ctx.kernel.waitForWarmup();
|
|
609
|
+
await ctx.kernel.ensureStatusList();
|
|
610
|
+
return ctx.kernel.getStatusList();
|
|
356
611
|
}
|
|
357
612
|
async function fetchOpsxInstructions(ctx, input) {
|
|
358
613
|
const changeId = requireChangeId(input.change);
|
|
359
|
-
await
|
|
360
|
-
await
|
|
361
|
-
|
|
362
|
-
"instructions",
|
|
363
|
-
input.artifact,
|
|
364
|
-
"--json",
|
|
365
|
-
"--change",
|
|
366
|
-
changeId
|
|
367
|
-
];
|
|
368
|
-
if (input.schema) args.push("--schema", input.schema);
|
|
369
|
-
const result = await ctx.cliExecutor.execute(args);
|
|
370
|
-
if (!result.success) throw new Error(result.stderr || `openspec instructions failed (exit ${result.exitCode ?? "null"})`);
|
|
371
|
-
return parseCliJson(result.stdout, ArtifactInstructionsSchema, "openspec instructions");
|
|
614
|
+
await ctx.kernel.waitForWarmup();
|
|
615
|
+
await ctx.kernel.ensureInstructions(changeId, input.artifact, input.schema);
|
|
616
|
+
return ctx.kernel.getInstructions(changeId, input.artifact, input.schema);
|
|
372
617
|
}
|
|
373
618
|
async function fetchOpsxApplyInstructions(ctx, input) {
|
|
374
619
|
const changeId = requireChangeId(input.change);
|
|
375
|
-
await
|
|
376
|
-
await
|
|
377
|
-
|
|
378
|
-
"instructions",
|
|
379
|
-
"apply",
|
|
380
|
-
"--json",
|
|
381
|
-
"--change",
|
|
382
|
-
changeId
|
|
383
|
-
];
|
|
384
|
-
if (input.schema) args.push("--schema", input.schema);
|
|
385
|
-
const result = await ctx.cliExecutor.execute(args);
|
|
386
|
-
if (!result.success) throw new Error(result.stderr || `openspec instructions apply failed (exit ${result.exitCode ?? "null"})`);
|
|
387
|
-
return parseCliJson(result.stdout, ApplyInstructionsSchema, "openspec instructions apply");
|
|
620
|
+
await ctx.kernel.waitForWarmup();
|
|
621
|
+
await ctx.kernel.ensureApplyInstructions(changeId, input.schema);
|
|
622
|
+
return ctx.kernel.getApplyInstructions(changeId, input.schema);
|
|
388
623
|
}
|
|
389
|
-
async function
|
|
390
|
-
await
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
624
|
+
async function fetchOpsxConfigBundle(ctx) {
|
|
625
|
+
await ctx.kernel.ensureSchemas();
|
|
626
|
+
const schemas = ctx.kernel.getSchemas();
|
|
627
|
+
for (const schema of schemas) {
|
|
628
|
+
ctx.kernel.ensureSchemaDetail(schema.name).catch(() => {});
|
|
629
|
+
ctx.kernel.ensureSchemaResolution(schema.name).catch(() => {});
|
|
630
|
+
}
|
|
631
|
+
const schemaDetails = {};
|
|
632
|
+
const schemaResolutions = {};
|
|
633
|
+
for (const schema of schemas) {
|
|
634
|
+
schemaDetails[schema.name] = ctx.kernel.peekSchemaDetail(schema.name);
|
|
635
|
+
schemaResolutions[schema.name] = ctx.kernel.peekSchemaResolution(schema.name);
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
schemas,
|
|
639
|
+
schemaDetails,
|
|
640
|
+
schemaResolutions
|
|
641
|
+
};
|
|
394
642
|
}
|
|
395
643
|
async function fetchOpsxSchemaResolution(ctx, name) {
|
|
396
|
-
await
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return parseCliJson(result.stdout, SchemaResolutionSchema, "openspec schema which");
|
|
644
|
+
await ctx.kernel.waitForWarmup();
|
|
645
|
+
await ctx.kernel.ensureSchemaResolution(name);
|
|
646
|
+
return ctx.kernel.getSchemaResolution(name);
|
|
400
647
|
}
|
|
401
648
|
async function fetchOpsxTemplates(ctx, schema) {
|
|
402
|
-
await
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return parseCliJson(result.stdout, TemplatesSchema, "openspec templates");
|
|
649
|
+
await ctx.kernel.waitForWarmup();
|
|
650
|
+
await ctx.kernel.ensureTemplates(schema);
|
|
651
|
+
return ctx.kernel.getTemplates(schema);
|
|
406
652
|
}
|
|
407
653
|
async function fetchOpsxTemplateContents(ctx, schema) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
content: await reactiveReadFile(info.path),
|
|
412
|
-
path: info.path,
|
|
413
|
-
source: info.source
|
|
414
|
-
}];
|
|
415
|
-
}));
|
|
416
|
-
return Object.fromEntries(entries);
|
|
654
|
+
await ctx.kernel.waitForWarmup();
|
|
655
|
+
await ctx.kernel.ensureTemplateContents(schema);
|
|
656
|
+
return ctx.kernel.getTemplateContents(schema);
|
|
417
657
|
}
|
|
418
658
|
/**
|
|
419
659
|
* Spec router - spec CRUD operations
|
|
@@ -803,11 +1043,11 @@ const opsxRouter = router({
|
|
|
803
1043
|
})).subscription(({ ctx, input }) => {
|
|
804
1044
|
return createReactiveSubscription(() => fetchOpsxApplyInstructions(ctx, input));
|
|
805
1045
|
}),
|
|
806
|
-
|
|
807
|
-
return
|
|
1046
|
+
configBundle: publicProcedure.query(async ({ ctx }) => {
|
|
1047
|
+
return fetchOpsxConfigBundle(ctx);
|
|
808
1048
|
}),
|
|
809
|
-
|
|
810
|
-
return createReactiveSubscription(() =>
|
|
1049
|
+
subscribeConfigBundle: publicProcedure.subscription(({ ctx }) => {
|
|
1050
|
+
return createReactiveSubscription(() => fetchOpsxConfigBundle(ctx));
|
|
811
1051
|
}),
|
|
812
1052
|
templates: publicProcedure.input(z.object({ schema: z.string().optional() }).optional()).query(async ({ ctx, input }) => {
|
|
813
1053
|
return fetchOpsxTemplates(ctx, input?.schema);
|
|
@@ -821,53 +1061,34 @@ const opsxRouter = router({
|
|
|
821
1061
|
subscribeTemplateContents: publicProcedure.input(z.object({ schema: z.string().optional() }).optional()).subscription(({ ctx, input }) => {
|
|
822
1062
|
return createReactiveSubscription(() => fetchOpsxTemplateContents(ctx, input?.schema));
|
|
823
1063
|
}),
|
|
824
|
-
schemaResolution: publicProcedure.input(z.object({ name: z.string() })).query(async ({ ctx, input }) => {
|
|
825
|
-
return fetchOpsxSchemaResolution(ctx, input.name);
|
|
826
|
-
}),
|
|
827
|
-
subscribeSchemaResolution: publicProcedure.input(z.object({ name: z.string() })).subscription(({ ctx, input }) => {
|
|
828
|
-
return createReactiveSubscription(() => fetchOpsxSchemaResolution(ctx, input.name));
|
|
829
|
-
}),
|
|
830
|
-
schemaDetail: publicProcedure.input(z.object({ name: z.string() })).query(async ({ ctx, input }) => {
|
|
831
|
-
await touchOpsxProjectDeps(ctx.projectDir);
|
|
832
|
-
const schemaPath = join((await fetchOpsxSchemaResolution(ctx, input.name)).path, "schema.yaml");
|
|
833
|
-
const content = await reactiveReadFile(schemaPath);
|
|
834
|
-
if (!content) throw new Error(`schema.yaml not found at ${schemaPath}`);
|
|
835
|
-
return parseSchemaYaml(content);
|
|
836
|
-
}),
|
|
837
|
-
subscribeSchemaDetail: publicProcedure.input(z.object({ name: z.string() })).subscription(({ ctx, input }) => {
|
|
838
|
-
return createReactiveSubscription(async () => {
|
|
839
|
-
await touchOpsxProjectDeps(ctx.projectDir);
|
|
840
|
-
const schemaPath = join((await fetchOpsxSchemaResolution(ctx, input.name)).path, "schema.yaml");
|
|
841
|
-
const content = await reactiveReadFile(schemaPath);
|
|
842
|
-
if (!content) throw new Error(`schema.yaml not found at ${schemaPath}`);
|
|
843
|
-
return parseSchemaYaml(content);
|
|
844
|
-
});
|
|
845
|
-
}),
|
|
846
1064
|
schemaFiles: publicProcedure.input(z.object({ name: z.string() })).query(async ({ ctx, input }) => {
|
|
847
|
-
await
|
|
848
|
-
|
|
1065
|
+
await ctx.kernel.waitForWarmup();
|
|
1066
|
+
await ctx.kernel.ensureSchemaFiles(input.name);
|
|
1067
|
+
return ctx.kernel.getSchemaFiles(input.name);
|
|
849
1068
|
}),
|
|
850
1069
|
subscribeSchemaFiles: publicProcedure.input(z.object({ name: z.string() })).subscription(({ ctx, input }) => {
|
|
851
1070
|
return createReactiveSubscription(async () => {
|
|
852
|
-
await
|
|
853
|
-
|
|
1071
|
+
await ctx.kernel.waitForWarmup();
|
|
1072
|
+
await ctx.kernel.ensureSchemaFiles(input.name);
|
|
1073
|
+
return ctx.kernel.getSchemaFiles(input.name);
|
|
854
1074
|
});
|
|
855
1075
|
}),
|
|
856
1076
|
schemaYaml: publicProcedure.input(z.object({ name: z.string() })).query(async ({ ctx, input }) => {
|
|
857
|
-
await
|
|
858
|
-
|
|
1077
|
+
await ctx.kernel.waitForWarmup();
|
|
1078
|
+
await ctx.kernel.ensureSchemaYaml(input.name);
|
|
1079
|
+
return ctx.kernel.getSchemaYaml(input.name);
|
|
859
1080
|
}),
|
|
860
1081
|
subscribeSchemaYaml: publicProcedure.input(z.object({ name: z.string() })).subscription(({ ctx, input }) => {
|
|
861
1082
|
return createReactiveSubscription(async () => {
|
|
862
|
-
await
|
|
863
|
-
|
|
1083
|
+
await ctx.kernel.waitForWarmup();
|
|
1084
|
+
await ctx.kernel.ensureSchemaYaml(input.name);
|
|
1085
|
+
return ctx.kernel.getSchemaYaml(input.name);
|
|
864
1086
|
});
|
|
865
1087
|
}),
|
|
866
1088
|
writeSchemaYaml: publicProcedure.input(z.object({
|
|
867
1089
|
name: z.string(),
|
|
868
1090
|
content: z.string()
|
|
869
1091
|
})).mutation(async ({ ctx, input }) => {
|
|
870
|
-
await touchOpsxProjectDeps(ctx.projectDir);
|
|
871
1092
|
const resolution = await fetchOpsxSchemaResolution(ctx, input.name);
|
|
872
1093
|
ensureEditableSource(resolution.source, "schema.yaml");
|
|
873
1094
|
const schemaPath = join(resolution.path, "schema.yaml");
|
|
@@ -880,7 +1101,6 @@ const opsxRouter = router({
|
|
|
880
1101
|
path: z.string(),
|
|
881
1102
|
content: z.string()
|
|
882
1103
|
})).mutation(async ({ ctx, input }) => {
|
|
883
|
-
await touchOpsxProjectDeps(ctx.projectDir);
|
|
884
1104
|
const resolution = await fetchOpsxSchemaResolution(ctx, input.schema);
|
|
885
1105
|
ensureEditableSource(resolution.source, "schema file");
|
|
886
1106
|
if (!input.path.trim()) throw new Error("path is required");
|
|
@@ -894,7 +1114,6 @@ const opsxRouter = router({
|
|
|
894
1114
|
path: z.string(),
|
|
895
1115
|
content: z.string().optional()
|
|
896
1116
|
})).mutation(async ({ ctx, input }) => {
|
|
897
|
-
await touchOpsxProjectDeps(ctx.projectDir);
|
|
898
1117
|
const resolution = await fetchOpsxSchemaResolution(ctx, input.schema);
|
|
899
1118
|
ensureEditableSource(resolution.source, "schema file");
|
|
900
1119
|
if (!input.path.trim()) throw new Error("path is required");
|
|
@@ -907,7 +1126,6 @@ const opsxRouter = router({
|
|
|
907
1126
|
schema: z.string(),
|
|
908
1127
|
path: z.string()
|
|
909
1128
|
})).mutation(async ({ ctx, input }) => {
|
|
910
|
-
await touchOpsxProjectDeps(ctx.projectDir);
|
|
911
1129
|
const resolution = await fetchOpsxSchemaResolution(ctx, input.schema);
|
|
912
1130
|
ensureEditableSource(resolution.source, "schema directory");
|
|
913
1131
|
if (!input.path.trim()) throw new Error("path is required");
|
|
@@ -918,7 +1136,6 @@ const opsxRouter = router({
|
|
|
918
1136
|
schema: z.string(),
|
|
919
1137
|
path: z.string()
|
|
920
1138
|
})).mutation(async ({ ctx, input }) => {
|
|
921
|
-
await touchOpsxProjectDeps(ctx.projectDir);
|
|
922
1139
|
const resolution = await fetchOpsxSchemaResolution(ctx, input.schema);
|
|
923
1140
|
ensureEditableSource(resolution.source, "schema entry");
|
|
924
1141
|
if (!input.path.trim()) throw new Error("path is required");
|
|
@@ -934,26 +1151,18 @@ const opsxRouter = router({
|
|
|
934
1151
|
schema: z.string(),
|
|
935
1152
|
artifactId: z.string()
|
|
936
1153
|
})).query(async ({ ctx, input }) => {
|
|
937
|
-
const info = (await
|
|
1154
|
+
const info = (await fetchOpsxTemplateContents(ctx, input.schema))[input.artifactId];
|
|
938
1155
|
if (!info) throw new Error(`Template not found for ${input.schema}:${input.artifactId}`);
|
|
939
|
-
return
|
|
940
|
-
content: await reactiveReadFile(info.path),
|
|
941
|
-
path: info.path,
|
|
942
|
-
source: info.source
|
|
943
|
-
};
|
|
1156
|
+
return info;
|
|
944
1157
|
}),
|
|
945
1158
|
subscribeTemplateContent: publicProcedure.input(z.object({
|
|
946
1159
|
schema: z.string(),
|
|
947
1160
|
artifactId: z.string()
|
|
948
1161
|
})).subscription(({ ctx, input }) => {
|
|
949
1162
|
return createReactiveSubscription(async () => {
|
|
950
|
-
const info = (await
|
|
1163
|
+
const info = (await fetchOpsxTemplateContents(ctx, input.schema))[input.artifactId];
|
|
951
1164
|
if (!info) throw new Error(`Template not found for ${input.schema}:${input.artifactId}`);
|
|
952
|
-
return
|
|
953
|
-
content: await reactiveReadFile(info.path),
|
|
954
|
-
path: info.path,
|
|
955
|
-
source: info.source
|
|
956
|
-
};
|
|
1165
|
+
return info;
|
|
957
1166
|
});
|
|
958
1167
|
}),
|
|
959
1168
|
writeTemplateContent: publicProcedure.input(z.object({
|
|
@@ -969,7 +1178,6 @@ const opsxRouter = router({
|
|
|
969
1178
|
return { success: true };
|
|
970
1179
|
}),
|
|
971
1180
|
deleteSchema: publicProcedure.input(z.object({ name: z.string() })).mutation(async ({ ctx, input }) => {
|
|
972
|
-
await touchOpsxProjectDeps(ctx.projectDir);
|
|
973
1181
|
const resolution = await fetchOpsxSchemaResolution(ctx, input.name);
|
|
974
1182
|
ensureEditableSource(resolution.source, "schema");
|
|
975
1183
|
await rm(resolution.path, {
|
|
@@ -979,11 +1187,15 @@ const opsxRouter = router({
|
|
|
979
1187
|
return { success: true };
|
|
980
1188
|
}),
|
|
981
1189
|
projectConfig: publicProcedure.query(async ({ ctx }) => {
|
|
982
|
-
|
|
1190
|
+
await ctx.kernel.waitForWarmup();
|
|
1191
|
+
await ctx.kernel.ensureProjectConfig();
|
|
1192
|
+
return ctx.kernel.getProjectConfig();
|
|
983
1193
|
}),
|
|
984
1194
|
subscribeProjectConfig: publicProcedure.subscription(({ ctx }) => {
|
|
985
1195
|
return createReactiveSubscription(async () => {
|
|
986
|
-
|
|
1196
|
+
await ctx.kernel.waitForWarmup();
|
|
1197
|
+
await ctx.kernel.ensureProjectConfig();
|
|
1198
|
+
return ctx.kernel.getProjectConfig();
|
|
987
1199
|
});
|
|
988
1200
|
}),
|
|
989
1201
|
writeProjectConfig: publicProcedure.input(z.object({ content: z.string() })).mutation(async ({ ctx, input }) => {
|
|
@@ -993,55 +1205,63 @@ const opsxRouter = router({
|
|
|
993
1205
|
return { success: true };
|
|
994
1206
|
}),
|
|
995
1207
|
listChanges: publicProcedure.query(async ({ ctx }) => {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
includeHidden: false
|
|
1000
|
-
});
|
|
1208
|
+
await ctx.kernel.waitForWarmup();
|
|
1209
|
+
await ctx.kernel.ensureChangeIds();
|
|
1210
|
+
return ctx.kernel.getChangeIds();
|
|
1001
1211
|
}),
|
|
1002
1212
|
subscribeChanges: publicProcedure.subscription(({ ctx }) => {
|
|
1003
1213
|
return createReactiveSubscription(async () => {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
includeHidden: false
|
|
1008
|
-
});
|
|
1214
|
+
await ctx.kernel.waitForWarmup();
|
|
1215
|
+
await ctx.kernel.ensureChangeIds();
|
|
1216
|
+
return ctx.kernel.getChangeIds();
|
|
1009
1217
|
});
|
|
1010
1218
|
}),
|
|
1011
1219
|
changeMetadata: publicProcedure.input(z.object({ changeId: z.string() })).query(async ({ ctx, input }) => {
|
|
1012
|
-
|
|
1220
|
+
await ctx.kernel.waitForWarmup();
|
|
1221
|
+
await ctx.kernel.ensureChangeMetadata(input.changeId);
|
|
1222
|
+
return ctx.kernel.getChangeMetadata(input.changeId);
|
|
1013
1223
|
}),
|
|
1014
1224
|
subscribeChangeMetadata: publicProcedure.input(z.object({ changeId: z.string() })).subscription(({ ctx, input }) => {
|
|
1015
1225
|
return createReactiveSubscription(async () => {
|
|
1016
|
-
|
|
1226
|
+
await ctx.kernel.waitForWarmup();
|
|
1227
|
+
await ctx.kernel.ensureChangeMetadata(input.changeId);
|
|
1228
|
+
return ctx.kernel.getChangeMetadata(input.changeId);
|
|
1017
1229
|
});
|
|
1018
1230
|
}),
|
|
1019
1231
|
readArtifactOutput: publicProcedure.input(z.object({
|
|
1020
1232
|
changeId: z.string(),
|
|
1021
1233
|
outputPath: z.string()
|
|
1022
1234
|
})).query(async ({ ctx, input }) => {
|
|
1023
|
-
|
|
1235
|
+
await ctx.kernel.waitForWarmup();
|
|
1236
|
+
await ctx.kernel.ensureArtifactOutput(input.changeId, input.outputPath);
|
|
1237
|
+
return ctx.kernel.getArtifactOutput(input.changeId, input.outputPath);
|
|
1024
1238
|
}),
|
|
1025
1239
|
subscribeArtifactOutput: publicProcedure.input(z.object({
|
|
1026
1240
|
changeId: z.string(),
|
|
1027
1241
|
outputPath: z.string()
|
|
1028
1242
|
})).subscription(({ ctx, input }) => {
|
|
1029
1243
|
return createReactiveSubscription(async () => {
|
|
1030
|
-
|
|
1244
|
+
await ctx.kernel.waitForWarmup();
|
|
1245
|
+
await ctx.kernel.ensureArtifactOutput(input.changeId, input.outputPath);
|
|
1246
|
+
return ctx.kernel.getArtifactOutput(input.changeId, input.outputPath);
|
|
1031
1247
|
});
|
|
1032
1248
|
}),
|
|
1033
1249
|
readGlobArtifactFiles: publicProcedure.input(z.object({
|
|
1034
1250
|
changeId: z.string(),
|
|
1035
1251
|
outputPath: z.string()
|
|
1036
1252
|
})).query(async ({ ctx, input }) => {
|
|
1037
|
-
|
|
1253
|
+
await ctx.kernel.waitForWarmup();
|
|
1254
|
+
await ctx.kernel.ensureGlobArtifactFiles(input.changeId, input.outputPath);
|
|
1255
|
+
return ctx.kernel.getGlobArtifactFiles(input.changeId, input.outputPath);
|
|
1038
1256
|
}),
|
|
1039
1257
|
subscribeGlobArtifactFiles: publicProcedure.input(z.object({
|
|
1040
1258
|
changeId: z.string(),
|
|
1041
1259
|
outputPath: z.string()
|
|
1042
1260
|
})).subscription(({ ctx, input }) => {
|
|
1043
1261
|
return createReactiveSubscription(async () => {
|
|
1044
|
-
|
|
1262
|
+
await ctx.kernel.waitForWarmup();
|
|
1263
|
+
await ctx.kernel.ensureGlobArtifactFiles(input.changeId, input.outputPath);
|
|
1264
|
+
return ctx.kernel.getGlobArtifactFiles(input.changeId, input.outputPath);
|
|
1045
1265
|
});
|
|
1046
1266
|
}),
|
|
1047
1267
|
writeArtifactOutput: publicProcedure.input(z.object({
|
|
@@ -1086,6 +1306,17 @@ const kvRouter = router({
|
|
|
1086
1306
|
})
|
|
1087
1307
|
});
|
|
1088
1308
|
/**
|
|
1309
|
+
* Search router - unified fulltext search over specs/changes/archives
|
|
1310
|
+
*/
|
|
1311
|
+
const searchRouter = router({
|
|
1312
|
+
query: publicProcedure.input(SearchQuerySchema).query(async ({ ctx, input }) => {
|
|
1313
|
+
return ctx.searchService.query(input);
|
|
1314
|
+
}),
|
|
1315
|
+
subscribe: publicProcedure.input(SearchQuerySchema).subscription(({ ctx, input }) => {
|
|
1316
|
+
return createReactiveSubscriptionWithInput((queryInput) => ctx.searchService.queryReactive(queryInput))(input);
|
|
1317
|
+
})
|
|
1318
|
+
});
|
|
1319
|
+
/**
|
|
1089
1320
|
* Main app router
|
|
1090
1321
|
*/
|
|
1091
1322
|
const appRouter = router({
|
|
@@ -1095,402 +1326,145 @@ const appRouter = router({
|
|
|
1095
1326
|
init: initRouter,
|
|
1096
1327
|
realtime: realtimeRouter,
|
|
1097
1328
|
config: configRouter,
|
|
1098
|
-
cli: cliRouter,
|
|
1099
|
-
opsx: opsxRouter,
|
|
1100
|
-
kv: kvRouter
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
//#
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
function
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
*
|
|
1125
|
-
* @param startPort - The preferred port to start checking from
|
|
1126
|
-
* @param maxAttempts - Maximum number of ports to try (default: 10)
|
|
1127
|
-
* @returns The first available port found
|
|
1128
|
-
* @throws Error if no available port is found in the range
|
|
1129
|
-
*/
|
|
1130
|
-
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
1131
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
1132
|
-
const port = startPort + i;
|
|
1133
|
-
if (await isPortAvailable(port)) return port;
|
|
1134
|
-
}
|
|
1135
|
-
throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
//#endregion
|
|
1139
|
-
//#region src/pty-manager.ts
|
|
1140
|
-
const DEFAULT_SCROLLBACK = 1e3;
|
|
1141
|
-
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
1142
|
-
function detectPtyPlatform() {
|
|
1143
|
-
if (process.platform === "win32") return "windows";
|
|
1144
|
-
if (process.platform === "darwin") return "macos";
|
|
1145
|
-
return "common";
|
|
1146
|
-
}
|
|
1147
|
-
function resolveDefaultShell(platform, env) {
|
|
1148
|
-
if (platform === "windows") return env.ComSpec?.trim() || "cmd.exe";
|
|
1149
|
-
return env.SHELL?.trim() || "/bin/sh";
|
|
1150
|
-
}
|
|
1151
|
-
function resolvePtyCommand(opts) {
|
|
1152
|
-
const command = opts.command?.trim();
|
|
1153
|
-
if (command) return {
|
|
1154
|
-
command,
|
|
1155
|
-
args: opts.args ?? []
|
|
1156
|
-
};
|
|
1157
|
-
return {
|
|
1158
|
-
command: resolveDefaultShell(opts.platform, opts.env),
|
|
1159
|
-
args: []
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
var PtySession = class extends EventEmitter$1 {
|
|
1163
|
-
id;
|
|
1164
|
-
command;
|
|
1165
|
-
args;
|
|
1166
|
-
platform;
|
|
1167
|
-
createdAt;
|
|
1168
|
-
process;
|
|
1169
|
-
titleInterval = null;
|
|
1170
|
-
lastTitle = "";
|
|
1171
|
-
buffer = [];
|
|
1172
|
-
bufferByteLength = 0;
|
|
1173
|
-
maxBufferLines;
|
|
1174
|
-
maxBufferBytes;
|
|
1175
|
-
isExited = false;
|
|
1176
|
-
exitCode = null;
|
|
1177
|
-
constructor(id, opts) {
|
|
1178
|
-
super();
|
|
1179
|
-
this.id = id;
|
|
1180
|
-
this.createdAt = Date.now();
|
|
1181
|
-
const resolvedCommand = resolvePtyCommand({
|
|
1182
|
-
platform: opts.platform,
|
|
1183
|
-
command: opts.command,
|
|
1184
|
-
args: opts.args,
|
|
1185
|
-
env: process.env
|
|
1186
|
-
});
|
|
1187
|
-
this.command = resolvedCommand.command;
|
|
1188
|
-
this.args = resolvedCommand.args;
|
|
1189
|
-
this.platform = opts.platform;
|
|
1190
|
-
this.maxBufferLines = opts.scrollback ?? DEFAULT_SCROLLBACK;
|
|
1191
|
-
this.maxBufferBytes = opts.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
1192
|
-
this.process = pty.spawn(this.command, this.args, {
|
|
1193
|
-
name: "xterm-256color",
|
|
1194
|
-
cols: opts.cols ?? 80,
|
|
1195
|
-
rows: opts.rows ?? 24,
|
|
1196
|
-
cwd: opts.cwd,
|
|
1197
|
-
env: {
|
|
1198
|
-
...process.env,
|
|
1199
|
-
TERM: "xterm-256color"
|
|
1200
|
-
}
|
|
1201
|
-
});
|
|
1202
|
-
this.process.onData((data) => {
|
|
1203
|
-
this.appendBuffer(data);
|
|
1204
|
-
this.emit("data", data);
|
|
1205
|
-
});
|
|
1206
|
-
this.process.onExit(({ exitCode }) => {
|
|
1207
|
-
if (this.titleInterval) {
|
|
1208
|
-
clearInterval(this.titleInterval);
|
|
1209
|
-
this.titleInterval = null;
|
|
1210
|
-
}
|
|
1211
|
-
this.isExited = true;
|
|
1212
|
-
this.exitCode = exitCode;
|
|
1213
|
-
this.emit("exit", exitCode);
|
|
1214
|
-
});
|
|
1215
|
-
this.titleInterval = setInterval(() => {
|
|
1216
|
-
try {
|
|
1217
|
-
const title = this.process.process;
|
|
1218
|
-
if (title && title !== this.lastTitle) {
|
|
1219
|
-
this.lastTitle = title;
|
|
1220
|
-
this.emit("title", title);
|
|
1221
|
-
}
|
|
1222
|
-
} catch {}
|
|
1223
|
-
}, 1e3);
|
|
1224
|
-
}
|
|
1225
|
-
get title() {
|
|
1226
|
-
return this.lastTitle;
|
|
1227
|
-
}
|
|
1228
|
-
appendBuffer(data) {
|
|
1229
|
-
let chunk = data;
|
|
1230
|
-
if (chunk.length > this.maxBufferBytes) chunk = chunk.slice(-this.maxBufferBytes);
|
|
1231
|
-
this.buffer.push(chunk);
|
|
1232
|
-
this.bufferByteLength += chunk.length;
|
|
1233
|
-
while (this.bufferByteLength > this.maxBufferBytes && this.buffer.length > 0) {
|
|
1234
|
-
const removed = this.buffer.shift();
|
|
1235
|
-
this.bufferByteLength -= removed.length;
|
|
1236
|
-
}
|
|
1237
|
-
while (this.buffer.length > this.maxBufferLines) {
|
|
1238
|
-
const removed = this.buffer.shift();
|
|
1239
|
-
this.bufferByteLength -= removed.length;
|
|
1240
|
-
}
|
|
1329
|
+
cli: cliRouter,
|
|
1330
|
+
opsx: opsxRouter,
|
|
1331
|
+
kv: kvRouter,
|
|
1332
|
+
search: searchRouter
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/search-documents.ts
|
|
1337
|
+
function joinParts(parts) {
|
|
1338
|
+
return parts.map((part) => part?.trim() ?? "").filter((part) => part.length > 0).join("\n\n");
|
|
1339
|
+
}
|
|
1340
|
+
async function collectSearchDocuments(adapter) {
|
|
1341
|
+
const docs = [];
|
|
1342
|
+
const specs = await adapter.listSpecsWithMeta();
|
|
1343
|
+
for (const spec of specs) {
|
|
1344
|
+
const raw = await adapter.readSpecRaw(spec.id);
|
|
1345
|
+
if (!raw) continue;
|
|
1346
|
+
docs.push({
|
|
1347
|
+
id: `spec:${spec.id}`,
|
|
1348
|
+
kind: "spec",
|
|
1349
|
+
title: spec.name,
|
|
1350
|
+
href: `/specs/${encodeURIComponent(spec.id)}`,
|
|
1351
|
+
path: `openspec/specs/${spec.id}/spec.md`,
|
|
1352
|
+
content: raw,
|
|
1353
|
+
updatedAt: spec.updatedAt
|
|
1354
|
+
});
|
|
1241
1355
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1356
|
+
const changes = await adapter.listChangesWithMeta();
|
|
1357
|
+
for (const change of changes) {
|
|
1358
|
+
const raw = await adapter.readChangeRaw(change.id);
|
|
1359
|
+
if (!raw) continue;
|
|
1360
|
+
docs.push({
|
|
1361
|
+
id: `change:${change.id}`,
|
|
1362
|
+
kind: "change",
|
|
1363
|
+
title: change.name,
|
|
1364
|
+
href: `/changes/${encodeURIComponent(change.id)}`,
|
|
1365
|
+
path: `openspec/changes/${change.id}`,
|
|
1366
|
+
content: joinParts([
|
|
1367
|
+
raw.proposal,
|
|
1368
|
+
raw.tasks,
|
|
1369
|
+
raw.design,
|
|
1370
|
+
...raw.deltaSpecs.map((deltaSpec) => deltaSpec.content)
|
|
1371
|
+
]),
|
|
1372
|
+
updatedAt: change.updatedAt
|
|
1373
|
+
});
|
|
1244
1374
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1375
|
+
const archives = await adapter.listArchivedChangesWithMeta();
|
|
1376
|
+
for (const archive of archives) {
|
|
1377
|
+
const raw = await adapter.readArchivedChangeRaw(archive.id);
|
|
1378
|
+
if (!raw) continue;
|
|
1379
|
+
docs.push({
|
|
1380
|
+
id: `archive:${archive.id}`,
|
|
1381
|
+
kind: "archive",
|
|
1382
|
+
title: archive.name,
|
|
1383
|
+
href: `/archive/${encodeURIComponent(archive.id)}`,
|
|
1384
|
+
path: `openspec/changes/archive/${archive.id}`,
|
|
1385
|
+
content: joinParts([
|
|
1386
|
+
raw.proposal,
|
|
1387
|
+
raw.tasks,
|
|
1388
|
+
raw.design,
|
|
1389
|
+
...raw.deltaSpecs.map((deltaSpec) => deltaSpec.content)
|
|
1390
|
+
]),
|
|
1391
|
+
updatedAt: archive.updatedAt
|
|
1392
|
+
});
|
|
1247
1393
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1394
|
+
return docs;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/search-service.ts
|
|
1399
|
+
const REBUILD_DEBOUNCE_MS = 250;
|
|
1400
|
+
var SearchService = class {
|
|
1401
|
+
provider;
|
|
1402
|
+
initialized = false;
|
|
1403
|
+
initPromise = null;
|
|
1404
|
+
rebuildPromise = null;
|
|
1405
|
+
rebuildTimer = null;
|
|
1406
|
+
constructor(adapter, watcher, provider = new NodeWorkerSearchProvider()) {
|
|
1407
|
+
this.adapter = adapter;
|
|
1408
|
+
this.provider = provider;
|
|
1409
|
+
watcher?.on("change", () => {
|
|
1410
|
+
this.scheduleRebuild();
|
|
1411
|
+
});
|
|
1250
1412
|
}
|
|
1251
|
-
|
|
1252
|
-
if (this.
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1413
|
+
async init() {
|
|
1414
|
+
if (this.initialized) return;
|
|
1415
|
+
if (this.initPromise) return this.initPromise;
|
|
1416
|
+
this.initPromise = this.rebuildIndex(true);
|
|
1256
1417
|
try {
|
|
1257
|
-
this.
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
toInfo() {
|
|
1262
|
-
return {
|
|
1263
|
-
id: this.id,
|
|
1264
|
-
title: this.lastTitle,
|
|
1265
|
-
command: this.command,
|
|
1266
|
-
args: this.args,
|
|
1267
|
-
platform: this.platform,
|
|
1268
|
-
isExited: this.isExited,
|
|
1269
|
-
exitCode: this.exitCode,
|
|
1270
|
-
createdAt: this.createdAt
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
};
|
|
1274
|
-
var PtyManager = class {
|
|
1275
|
-
sessions = /* @__PURE__ */ new Map();
|
|
1276
|
-
idCounter = 0;
|
|
1277
|
-
platform;
|
|
1278
|
-
constructor(defaultCwd) {
|
|
1279
|
-
this.defaultCwd = defaultCwd;
|
|
1280
|
-
this.platform = detectPtyPlatform();
|
|
1418
|
+
await this.initPromise;
|
|
1419
|
+
} finally {
|
|
1420
|
+
this.initPromise = null;
|
|
1421
|
+
}
|
|
1281
1422
|
}
|
|
1282
|
-
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
rows: opts.rows,
|
|
1287
|
-
command: opts.command,
|
|
1288
|
-
args: opts.args,
|
|
1289
|
-
cwd: this.defaultCwd,
|
|
1290
|
-
scrollback: opts.scrollback,
|
|
1291
|
-
maxBufferBytes: opts.maxBufferBytes,
|
|
1292
|
-
platform: this.platform
|
|
1293
|
-
});
|
|
1294
|
-
this.sessions.set(id, session);
|
|
1295
|
-
return session;
|
|
1423
|
+
async query(input) {
|
|
1424
|
+
const parsed = SearchQuerySchema.parse(input);
|
|
1425
|
+
await this.init();
|
|
1426
|
+
return this.provider.search(parsed);
|
|
1296
1427
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1428
|
+
async queryReactive(input) {
|
|
1429
|
+
const parsed = SearchQuerySchema.parse(input);
|
|
1430
|
+
await this.rebuildIndex();
|
|
1431
|
+
return this.provider.search(parsed);
|
|
1299
1432
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
return result;
|
|
1433
|
+
async dispose() {
|
|
1434
|
+
this.cancelRebuild();
|
|
1435
|
+
await this.provider.dispose();
|
|
1304
1436
|
}
|
|
1305
|
-
|
|
1306
|
-
this.
|
|
1437
|
+
scheduleRebuild() {
|
|
1438
|
+
this.cancelRebuild();
|
|
1439
|
+
this.rebuildTimer = setTimeout(() => {
|
|
1440
|
+
this.rebuildTimer = null;
|
|
1441
|
+
this.rebuildIndex().catch(() => {});
|
|
1442
|
+
}, REBUILD_DEBOUNCE_MS);
|
|
1307
1443
|
}
|
|
1308
|
-
|
|
1309
|
-
this.
|
|
1444
|
+
cancelRebuild() {
|
|
1445
|
+
if (!this.rebuildTimer) return;
|
|
1446
|
+
clearTimeout(this.rebuildTimer);
|
|
1447
|
+
this.rebuildTimer = null;
|
|
1310
1448
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
if (
|
|
1314
|
-
|
|
1315
|
-
this.
|
|
1449
|
+
async rebuildIndex(forceInit = false) {
|
|
1450
|
+
if (!forceInit && !this.initialized) return;
|
|
1451
|
+
if (this.rebuildPromise) return this.rebuildPromise;
|
|
1452
|
+
this.rebuildPromise = (async () => {
|
|
1453
|
+
const docs = await collectSearchDocuments(this.adapter);
|
|
1454
|
+
if (this.initialized) await this.provider.replaceAll(docs);
|
|
1455
|
+
else {
|
|
1456
|
+
await this.provider.init(docs);
|
|
1457
|
+
this.initialized = true;
|
|
1458
|
+
}
|
|
1459
|
+
})();
|
|
1460
|
+
try {
|
|
1461
|
+
await this.rebuildPromise;
|
|
1462
|
+
} finally {
|
|
1463
|
+
this.rebuildPromise = null;
|
|
1316
1464
|
}
|
|
1317
1465
|
}
|
|
1318
|
-
closeAll() {
|
|
1319
|
-
for (const session of this.sessions.values()) session.close();
|
|
1320
|
-
this.sessions.clear();
|
|
1321
|
-
}
|
|
1322
1466
|
};
|
|
1323
1467
|
|
|
1324
|
-
//#endregion
|
|
1325
|
-
//#region src/pty-websocket.ts
|
|
1326
|
-
function createPtyWebSocketHandler(ptyManager) {
|
|
1327
|
-
return (ws) => {
|
|
1328
|
-
const cleanups = /* @__PURE__ */ new Map();
|
|
1329
|
-
const send = (msg) => {
|
|
1330
|
-
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
|
|
1331
|
-
};
|
|
1332
|
-
const sendError = (code, message, opts) => {
|
|
1333
|
-
send({
|
|
1334
|
-
type: "error",
|
|
1335
|
-
code,
|
|
1336
|
-
message,
|
|
1337
|
-
sessionId: opts?.sessionId
|
|
1338
|
-
});
|
|
1339
|
-
};
|
|
1340
|
-
const attachToSession = (session, opts) => {
|
|
1341
|
-
const sessionId = session.id;
|
|
1342
|
-
cleanups.get(sessionId)?.();
|
|
1343
|
-
if (opts?.cols && opts?.rows && !session.isExited) session.resize(opts.cols, opts.rows);
|
|
1344
|
-
const onData = (data) => {
|
|
1345
|
-
send({
|
|
1346
|
-
type: "output",
|
|
1347
|
-
sessionId,
|
|
1348
|
-
data
|
|
1349
|
-
});
|
|
1350
|
-
};
|
|
1351
|
-
const onExit = (exitCode) => {
|
|
1352
|
-
send({
|
|
1353
|
-
type: "exit",
|
|
1354
|
-
sessionId,
|
|
1355
|
-
exitCode
|
|
1356
|
-
});
|
|
1357
|
-
};
|
|
1358
|
-
const onTitle = (title) => {
|
|
1359
|
-
send({
|
|
1360
|
-
type: "title",
|
|
1361
|
-
sessionId,
|
|
1362
|
-
title
|
|
1363
|
-
});
|
|
1364
|
-
};
|
|
1365
|
-
session.on("data", onData);
|
|
1366
|
-
session.on("exit", onExit);
|
|
1367
|
-
session.on("title", onTitle);
|
|
1368
|
-
cleanups.set(sessionId, () => {
|
|
1369
|
-
session.removeListener("data", onData);
|
|
1370
|
-
session.removeListener("exit", onExit);
|
|
1371
|
-
session.removeListener("title", onTitle);
|
|
1372
|
-
cleanups.delete(sessionId);
|
|
1373
|
-
});
|
|
1374
|
-
};
|
|
1375
|
-
ws.on("message", (raw) => {
|
|
1376
|
-
let parsed;
|
|
1377
|
-
try {
|
|
1378
|
-
parsed = JSON.parse(String(raw));
|
|
1379
|
-
} catch {
|
|
1380
|
-
sendError("INVALID_JSON", "Invalid JSON payload");
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
const parsedMessage = PtyClientMessageSchema.safeParse(parsed);
|
|
1384
|
-
if (!parsedMessage.success) {
|
|
1385
|
-
const firstIssue = parsedMessage.error.issues[0]?.message;
|
|
1386
|
-
sendError("INVALID_MESSAGE", firstIssue ?? "Invalid PTY message");
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
const msg = parsedMessage.data;
|
|
1390
|
-
switch (msg.type) {
|
|
1391
|
-
case "create":
|
|
1392
|
-
try {
|
|
1393
|
-
const session = ptyManager.create({
|
|
1394
|
-
cols: msg.cols,
|
|
1395
|
-
rows: msg.rows,
|
|
1396
|
-
command: msg.command,
|
|
1397
|
-
args: msg.args
|
|
1398
|
-
});
|
|
1399
|
-
send({
|
|
1400
|
-
type: "created",
|
|
1401
|
-
requestId: msg.requestId,
|
|
1402
|
-
sessionId: session.id,
|
|
1403
|
-
platform: session.platform
|
|
1404
|
-
});
|
|
1405
|
-
attachToSession(session);
|
|
1406
|
-
} catch (err) {
|
|
1407
|
-
sendError("PTY_CREATE_FAILED", err instanceof Error ? err.message : String(err), { sessionId: msg.requestId });
|
|
1408
|
-
}
|
|
1409
|
-
break;
|
|
1410
|
-
case "attach": {
|
|
1411
|
-
const session = ptyManager.get(msg.sessionId);
|
|
1412
|
-
if (!session) {
|
|
1413
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
1414
|
-
send({
|
|
1415
|
-
type: "exit",
|
|
1416
|
-
sessionId: msg.sessionId,
|
|
1417
|
-
exitCode: -1
|
|
1418
|
-
});
|
|
1419
|
-
break;
|
|
1420
|
-
}
|
|
1421
|
-
attachToSession(session, {
|
|
1422
|
-
cols: msg.cols,
|
|
1423
|
-
rows: msg.rows
|
|
1424
|
-
});
|
|
1425
|
-
const buffer = session.getBuffer();
|
|
1426
|
-
if (buffer) send({
|
|
1427
|
-
type: "buffer",
|
|
1428
|
-
sessionId: session.id,
|
|
1429
|
-
data: buffer
|
|
1430
|
-
});
|
|
1431
|
-
if (session.title) send({
|
|
1432
|
-
type: "title",
|
|
1433
|
-
sessionId: session.id,
|
|
1434
|
-
title: session.title
|
|
1435
|
-
});
|
|
1436
|
-
if (session.isExited) send({
|
|
1437
|
-
type: "exit",
|
|
1438
|
-
sessionId: session.id,
|
|
1439
|
-
exitCode: session.exitCode ?? -1
|
|
1440
|
-
});
|
|
1441
|
-
break;
|
|
1442
|
-
}
|
|
1443
|
-
case "list":
|
|
1444
|
-
send({
|
|
1445
|
-
type: "list",
|
|
1446
|
-
sessions: ptyManager.list().map((s) => ({
|
|
1447
|
-
id: s.id,
|
|
1448
|
-
title: s.title,
|
|
1449
|
-
command: s.command,
|
|
1450
|
-
args: s.args,
|
|
1451
|
-
platform: s.platform,
|
|
1452
|
-
isExited: s.isExited,
|
|
1453
|
-
exitCode: s.exitCode
|
|
1454
|
-
}))
|
|
1455
|
-
});
|
|
1456
|
-
break;
|
|
1457
|
-
case "input": {
|
|
1458
|
-
const session = ptyManager.get(msg.sessionId);
|
|
1459
|
-
if (!session) {
|
|
1460
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
1461
|
-
break;
|
|
1462
|
-
}
|
|
1463
|
-
session.write(msg.data);
|
|
1464
|
-
break;
|
|
1465
|
-
}
|
|
1466
|
-
case "resize": {
|
|
1467
|
-
const session = ptyManager.get(msg.sessionId);
|
|
1468
|
-
if (!session) {
|
|
1469
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
1470
|
-
break;
|
|
1471
|
-
}
|
|
1472
|
-
session.resize(msg.cols, msg.rows);
|
|
1473
|
-
break;
|
|
1474
|
-
}
|
|
1475
|
-
case "close": {
|
|
1476
|
-
const session = ptyManager.get(msg.sessionId);
|
|
1477
|
-
if (!session) {
|
|
1478
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
1479
|
-
break;
|
|
1480
|
-
}
|
|
1481
|
-
cleanups.get(msg.sessionId)?.();
|
|
1482
|
-
ptyManager.close(session.id);
|
|
1483
|
-
break;
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
});
|
|
1487
|
-
ws.on("close", () => {
|
|
1488
|
-
for (const cleanup of cleanups.values()) cleanup();
|
|
1489
|
-
cleanups.clear();
|
|
1490
|
-
});
|
|
1491
|
-
};
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
1468
|
//#endregion
|
|
1495
1469
|
//#region src/server.ts
|
|
1496
1470
|
/**
|
|
@@ -1514,6 +1488,7 @@ function createServer(config) {
|
|
|
1514
1488
|
const cliExecutor = new CliExecutor(configManager, config.projectDir);
|
|
1515
1489
|
const kernel = config.kernel;
|
|
1516
1490
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
1491
|
+
const searchService = new SearchService(adapter, watcher);
|
|
1517
1492
|
const app = new Hono();
|
|
1518
1493
|
const corsOrigins = config.corsOrigins ?? ["http://localhost:5173", "http://localhost:3000"];
|
|
1519
1494
|
app.use("*", cors({
|
|
@@ -1537,6 +1512,7 @@ function createServer(config) {
|
|
|
1537
1512
|
configManager,
|
|
1538
1513
|
cliExecutor,
|
|
1539
1514
|
kernel,
|
|
1515
|
+
searchService,
|
|
1540
1516
|
watcher,
|
|
1541
1517
|
projectDir: config.projectDir
|
|
1542
1518
|
})
|
|
@@ -1547,6 +1523,7 @@ function createServer(config) {
|
|
|
1547
1523
|
configManager,
|
|
1548
1524
|
cliExecutor,
|
|
1549
1525
|
kernel,
|
|
1526
|
+
searchService,
|
|
1550
1527
|
watcher,
|
|
1551
1528
|
projectDir: config.projectDir
|
|
1552
1529
|
});
|
|
@@ -1556,6 +1533,7 @@ function createServer(config) {
|
|
|
1556
1533
|
configManager,
|
|
1557
1534
|
cliExecutor,
|
|
1558
1535
|
kernel,
|
|
1536
|
+
searchService,
|
|
1559
1537
|
watcher,
|
|
1560
1538
|
createContext,
|
|
1561
1539
|
port: config.port ?? 3100
|
|
@@ -1597,6 +1575,7 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
1597
1575
|
ptyWss.close();
|
|
1598
1576
|
wss.close();
|
|
1599
1577
|
server.watcher?.stop();
|
|
1578
|
+
server.searchService.dispose().catch(() => {});
|
|
1600
1579
|
}
|
|
1601
1580
|
};
|
|
1602
1581
|
}
|
|
@@ -1629,6 +1608,9 @@ async function startServer(config, setupApp) {
|
|
|
1629
1608
|
kernel.warmup().catch((err) => {
|
|
1630
1609
|
console.error("Kernel warmup failed:", err);
|
|
1631
1610
|
});
|
|
1611
|
+
server.searchService.init().catch((err) => {
|
|
1612
|
+
console.error("Search service warmup failed:", err);
|
|
1613
|
+
});
|
|
1632
1614
|
return {
|
|
1633
1615
|
url,
|
|
1634
1616
|
port,
|