@openspecui/server 1.0.3 → 1.1.0
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 +552 -375
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,414 @@
|
|
|
1
1
|
import { serve } from "@hono/node-server";
|
|
2
|
-
import {
|
|
3
|
-
import { cors } from "hono/cors";
|
|
2
|
+
import { ApplyInstructionsSchema, ArtifactInstructionsSchema, ChangeStatusSchema, CliExecutor, ConfigManager, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, TemplatesSchema, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, initWatcherPool, isWatcherPoolInitialized, reactiveReadDir, reactiveReadFile, reactiveStat, 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
15
|
import { dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
12
16
|
import { z } from "zod";
|
|
13
17
|
import { parse } from "yaml";
|
|
14
|
-
import { EventEmitter } from "node:events";
|
|
15
|
-
import {
|
|
16
|
-
import * as pty from "@lydell/node-pty";
|
|
17
|
-
import { EventEmitter as EventEmitter$1 } from "events";
|
|
18
|
+
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
19
|
+
import { NodeWorkerSearchProvider } from "@openspecui/search/node";
|
|
18
20
|
|
|
21
|
+
//#region src/port-utils.ts
|
|
22
|
+
/**
|
|
23
|
+
* Check if a port is available by trying to listen on it.
|
|
24
|
+
* Uses default binding (both IPv4 and IPv6) to detect conflicts.
|
|
25
|
+
*/
|
|
26
|
+
function isPortAvailable(port) {
|
|
27
|
+
return new Promise((resolve$1) => {
|
|
28
|
+
const server = createServer$1();
|
|
29
|
+
server.once("error", () => {
|
|
30
|
+
resolve$1(false);
|
|
31
|
+
});
|
|
32
|
+
server.once("listening", () => {
|
|
33
|
+
server.close(() => resolve$1(true));
|
|
34
|
+
});
|
|
35
|
+
server.listen(port);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Find an available port starting from the given port.
|
|
40
|
+
* Will try up to maxAttempts ports sequentially.
|
|
41
|
+
*
|
|
42
|
+
* @param startPort - The preferred port to start checking from
|
|
43
|
+
* @param maxAttempts - Maximum number of ports to try (default: 10)
|
|
44
|
+
* @returns The first available port found
|
|
45
|
+
* @throws Error if no available port is found in the range
|
|
46
|
+
*/
|
|
47
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
48
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
49
|
+
const port = startPort + i;
|
|
50
|
+
if (await isPortAvailable(port)) return port;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/pty-manager.ts
|
|
57
|
+
const DEFAULT_SCROLLBACK = 1e3;
|
|
58
|
+
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
59
|
+
function detectPtyPlatform() {
|
|
60
|
+
if (process.platform === "win32") return "windows";
|
|
61
|
+
if (process.platform === "darwin") return "macos";
|
|
62
|
+
return "common";
|
|
63
|
+
}
|
|
64
|
+
function resolveDefaultShell(platform, env) {
|
|
65
|
+
if (platform === "windows") return env.ComSpec?.trim() || "cmd.exe";
|
|
66
|
+
return env.SHELL?.trim() || "/bin/sh";
|
|
67
|
+
}
|
|
68
|
+
function resolvePtyCommand(opts) {
|
|
69
|
+
const command = opts.command?.trim();
|
|
70
|
+
if (command) return {
|
|
71
|
+
command,
|
|
72
|
+
args: opts.args ?? []
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
command: resolveDefaultShell(opts.platform, opts.env),
|
|
76
|
+
args: []
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
var PtySession = class extends EventEmitter {
|
|
80
|
+
id;
|
|
81
|
+
command;
|
|
82
|
+
args;
|
|
83
|
+
platform;
|
|
84
|
+
createdAt;
|
|
85
|
+
process;
|
|
86
|
+
titleInterval = null;
|
|
87
|
+
lastTitle = "";
|
|
88
|
+
buffer = [];
|
|
89
|
+
bufferByteLength = 0;
|
|
90
|
+
maxBufferLines;
|
|
91
|
+
maxBufferBytes;
|
|
92
|
+
isExited = false;
|
|
93
|
+
exitCode = null;
|
|
94
|
+
constructor(id, opts) {
|
|
95
|
+
super();
|
|
96
|
+
this.id = id;
|
|
97
|
+
this.createdAt = Date.now();
|
|
98
|
+
const resolvedCommand = resolvePtyCommand({
|
|
99
|
+
platform: opts.platform,
|
|
100
|
+
command: opts.command,
|
|
101
|
+
args: opts.args,
|
|
102
|
+
env: process.env
|
|
103
|
+
});
|
|
104
|
+
this.command = resolvedCommand.command;
|
|
105
|
+
this.args = resolvedCommand.args;
|
|
106
|
+
this.platform = opts.platform;
|
|
107
|
+
this.maxBufferLines = opts.scrollback ?? DEFAULT_SCROLLBACK;
|
|
108
|
+
this.maxBufferBytes = opts.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
109
|
+
this.process = pty.spawn(this.command, this.args, {
|
|
110
|
+
name: "xterm-256color",
|
|
111
|
+
cols: opts.cols ?? 80,
|
|
112
|
+
rows: opts.rows ?? 24,
|
|
113
|
+
cwd: opts.cwd,
|
|
114
|
+
env: {
|
|
115
|
+
...process.env,
|
|
116
|
+
TERM: "xterm-256color"
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
this.process.onData((data) => {
|
|
120
|
+
this.appendBuffer(data);
|
|
121
|
+
this.emit("data", data);
|
|
122
|
+
});
|
|
123
|
+
this.process.onExit(({ exitCode }) => {
|
|
124
|
+
if (this.titleInterval) {
|
|
125
|
+
clearInterval(this.titleInterval);
|
|
126
|
+
this.titleInterval = null;
|
|
127
|
+
}
|
|
128
|
+
this.isExited = true;
|
|
129
|
+
this.exitCode = exitCode;
|
|
130
|
+
this.emit("exit", exitCode);
|
|
131
|
+
});
|
|
132
|
+
this.titleInterval = setInterval(() => {
|
|
133
|
+
try {
|
|
134
|
+
const title = this.process.process;
|
|
135
|
+
if (title && title !== this.lastTitle) {
|
|
136
|
+
this.lastTitle = title;
|
|
137
|
+
this.emit("title", title);
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
}, 1e3);
|
|
141
|
+
}
|
|
142
|
+
get title() {
|
|
143
|
+
return this.lastTitle;
|
|
144
|
+
}
|
|
145
|
+
appendBuffer(data) {
|
|
146
|
+
let chunk = data;
|
|
147
|
+
if (chunk.length > this.maxBufferBytes) chunk = chunk.slice(-this.maxBufferBytes);
|
|
148
|
+
this.buffer.push(chunk);
|
|
149
|
+
this.bufferByteLength += chunk.length;
|
|
150
|
+
while (this.bufferByteLength > this.maxBufferBytes && this.buffer.length > 0) {
|
|
151
|
+
const removed = this.buffer.shift();
|
|
152
|
+
this.bufferByteLength -= removed.length;
|
|
153
|
+
}
|
|
154
|
+
while (this.buffer.length > this.maxBufferLines) {
|
|
155
|
+
const removed = this.buffer.shift();
|
|
156
|
+
this.bufferByteLength -= removed.length;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
getBuffer() {
|
|
160
|
+
return this.buffer.join("");
|
|
161
|
+
}
|
|
162
|
+
write(data) {
|
|
163
|
+
if (!this.isExited) this.process.write(data);
|
|
164
|
+
}
|
|
165
|
+
resize(cols, rows) {
|
|
166
|
+
if (!this.isExited) this.process.resize(cols, rows);
|
|
167
|
+
}
|
|
168
|
+
close() {
|
|
169
|
+
if (this.titleInterval) {
|
|
170
|
+
clearInterval(this.titleInterval);
|
|
171
|
+
this.titleInterval = null;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
this.process.kill();
|
|
175
|
+
} catch {}
|
|
176
|
+
this.removeAllListeners();
|
|
177
|
+
}
|
|
178
|
+
toInfo() {
|
|
179
|
+
return {
|
|
180
|
+
id: this.id,
|
|
181
|
+
title: this.lastTitle,
|
|
182
|
+
command: this.command,
|
|
183
|
+
args: this.args,
|
|
184
|
+
platform: this.platform,
|
|
185
|
+
isExited: this.isExited,
|
|
186
|
+
exitCode: this.exitCode,
|
|
187
|
+
createdAt: this.createdAt
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var PtyManager = class {
|
|
192
|
+
sessions = /* @__PURE__ */ new Map();
|
|
193
|
+
idCounter = 0;
|
|
194
|
+
platform;
|
|
195
|
+
constructor(defaultCwd) {
|
|
196
|
+
this.defaultCwd = defaultCwd;
|
|
197
|
+
this.platform = detectPtyPlatform();
|
|
198
|
+
}
|
|
199
|
+
create(opts) {
|
|
200
|
+
const id = `pty-${++this.idCounter}`;
|
|
201
|
+
const session = new PtySession(id, {
|
|
202
|
+
cols: opts.cols,
|
|
203
|
+
rows: opts.rows,
|
|
204
|
+
command: opts.command,
|
|
205
|
+
args: opts.args,
|
|
206
|
+
cwd: this.defaultCwd,
|
|
207
|
+
scrollback: opts.scrollback,
|
|
208
|
+
maxBufferBytes: opts.maxBufferBytes,
|
|
209
|
+
platform: this.platform
|
|
210
|
+
});
|
|
211
|
+
this.sessions.set(id, session);
|
|
212
|
+
return session;
|
|
213
|
+
}
|
|
214
|
+
get(id) {
|
|
215
|
+
return this.sessions.get(id);
|
|
216
|
+
}
|
|
217
|
+
list() {
|
|
218
|
+
const result = [];
|
|
219
|
+
for (const session of this.sessions.values()) result.push(session.toInfo());
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
write(id, data) {
|
|
223
|
+
this.sessions.get(id)?.write(data);
|
|
224
|
+
}
|
|
225
|
+
resize(id, cols, rows) {
|
|
226
|
+
this.sessions.get(id)?.resize(cols, rows);
|
|
227
|
+
}
|
|
228
|
+
close(id) {
|
|
229
|
+
const session = this.sessions.get(id);
|
|
230
|
+
if (session) {
|
|
231
|
+
session.close();
|
|
232
|
+
this.sessions.delete(id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
closeAll() {
|
|
236
|
+
for (const session of this.sessions.values()) session.close();
|
|
237
|
+
this.sessions.clear();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/pty-websocket.ts
|
|
243
|
+
function createPtyWebSocketHandler(ptyManager) {
|
|
244
|
+
return (ws) => {
|
|
245
|
+
const cleanups = /* @__PURE__ */ new Map();
|
|
246
|
+
const send = (msg) => {
|
|
247
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
|
|
248
|
+
};
|
|
249
|
+
const sendError = (code, message, opts) => {
|
|
250
|
+
send({
|
|
251
|
+
type: "error",
|
|
252
|
+
code,
|
|
253
|
+
message,
|
|
254
|
+
sessionId: opts?.sessionId
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
const attachToSession = (session, opts) => {
|
|
258
|
+
const sessionId = session.id;
|
|
259
|
+
cleanups.get(sessionId)?.();
|
|
260
|
+
if (opts?.cols && opts?.rows && !session.isExited) session.resize(opts.cols, opts.rows);
|
|
261
|
+
const onData = (data) => {
|
|
262
|
+
send({
|
|
263
|
+
type: "output",
|
|
264
|
+
sessionId,
|
|
265
|
+
data
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
const onExit = (exitCode) => {
|
|
269
|
+
send({
|
|
270
|
+
type: "exit",
|
|
271
|
+
sessionId,
|
|
272
|
+
exitCode
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
const onTitle = (title) => {
|
|
276
|
+
send({
|
|
277
|
+
type: "title",
|
|
278
|
+
sessionId,
|
|
279
|
+
title
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
session.on("data", onData);
|
|
283
|
+
session.on("exit", onExit);
|
|
284
|
+
session.on("title", onTitle);
|
|
285
|
+
cleanups.set(sessionId, () => {
|
|
286
|
+
session.removeListener("data", onData);
|
|
287
|
+
session.removeListener("exit", onExit);
|
|
288
|
+
session.removeListener("title", onTitle);
|
|
289
|
+
cleanups.delete(sessionId);
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
ws.on("message", (raw) => {
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(String(raw));
|
|
296
|
+
} catch {
|
|
297
|
+
sendError("INVALID_JSON", "Invalid JSON payload");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const parsedMessage = PtyClientMessageSchema.safeParse(parsed);
|
|
301
|
+
if (!parsedMessage.success) {
|
|
302
|
+
const firstIssue = parsedMessage.error.issues[0]?.message;
|
|
303
|
+
sendError("INVALID_MESSAGE", firstIssue ?? "Invalid PTY message");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const msg = parsedMessage.data;
|
|
307
|
+
switch (msg.type) {
|
|
308
|
+
case "create":
|
|
309
|
+
try {
|
|
310
|
+
const session = ptyManager.create({
|
|
311
|
+
cols: msg.cols,
|
|
312
|
+
rows: msg.rows,
|
|
313
|
+
command: msg.command,
|
|
314
|
+
args: msg.args
|
|
315
|
+
});
|
|
316
|
+
send({
|
|
317
|
+
type: "created",
|
|
318
|
+
requestId: msg.requestId,
|
|
319
|
+
sessionId: session.id,
|
|
320
|
+
platform: session.platform
|
|
321
|
+
});
|
|
322
|
+
attachToSession(session);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
sendError("PTY_CREATE_FAILED", err instanceof Error ? err.message : String(err), { sessionId: msg.requestId });
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
case "attach": {
|
|
328
|
+
const session = ptyManager.get(msg.sessionId);
|
|
329
|
+
if (!session) {
|
|
330
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
331
|
+
send({
|
|
332
|
+
type: "exit",
|
|
333
|
+
sessionId: msg.sessionId,
|
|
334
|
+
exitCode: -1
|
|
335
|
+
});
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
attachToSession(session, {
|
|
339
|
+
cols: msg.cols,
|
|
340
|
+
rows: msg.rows
|
|
341
|
+
});
|
|
342
|
+
const buffer = session.getBuffer();
|
|
343
|
+
if (buffer) send({
|
|
344
|
+
type: "buffer",
|
|
345
|
+
sessionId: session.id,
|
|
346
|
+
data: buffer
|
|
347
|
+
});
|
|
348
|
+
if (session.title) send({
|
|
349
|
+
type: "title",
|
|
350
|
+
sessionId: session.id,
|
|
351
|
+
title: session.title
|
|
352
|
+
});
|
|
353
|
+
if (session.isExited) send({
|
|
354
|
+
type: "exit",
|
|
355
|
+
sessionId: session.id,
|
|
356
|
+
exitCode: session.exitCode ?? -1
|
|
357
|
+
});
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case "list":
|
|
361
|
+
send({
|
|
362
|
+
type: "list",
|
|
363
|
+
sessions: ptyManager.list().map((s) => ({
|
|
364
|
+
id: s.id,
|
|
365
|
+
title: s.title,
|
|
366
|
+
command: s.command,
|
|
367
|
+
args: s.args,
|
|
368
|
+
platform: s.platform,
|
|
369
|
+
isExited: s.isExited,
|
|
370
|
+
exitCode: s.exitCode
|
|
371
|
+
}))
|
|
372
|
+
});
|
|
373
|
+
break;
|
|
374
|
+
case "input": {
|
|
375
|
+
const session = ptyManager.get(msg.sessionId);
|
|
376
|
+
if (!session) {
|
|
377
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
session.write(msg.data);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case "resize": {
|
|
384
|
+
const session = ptyManager.get(msg.sessionId);
|
|
385
|
+
if (!session) {
|
|
386
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
session.resize(msg.cols, msg.rows);
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case "close": {
|
|
393
|
+
const session = ptyManager.get(msg.sessionId);
|
|
394
|
+
if (!session) {
|
|
395
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
cleanups.get(msg.sessionId)?.();
|
|
399
|
+
ptyManager.close(session.id);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
ws.on("close", () => {
|
|
405
|
+
for (const cleanup of cleanups.values()) cleanup();
|
|
406
|
+
cleanups.clear();
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
//#endregion
|
|
19
412
|
//#region src/cli-stream-observable.ts
|
|
20
413
|
/**
|
|
21
414
|
* 创建安全的 CLI 流式 observable
|
|
@@ -131,7 +524,7 @@ function parseSchemaYaml(content) {
|
|
|
131
524
|
*/
|
|
132
525
|
var ReactiveKV = class {
|
|
133
526
|
store = /* @__PURE__ */ new Map();
|
|
134
|
-
emitter = new EventEmitter();
|
|
527
|
+
emitter = new EventEmitter$1();
|
|
135
528
|
constructor() {
|
|
136
529
|
this.emitter.setMaxListeners(200);
|
|
137
530
|
}
|
|
@@ -1086,389 +1479,165 @@ const kvRouter = router({
|
|
|
1086
1479
|
})
|
|
1087
1480
|
});
|
|
1088
1481
|
/**
|
|
1089
|
-
*
|
|
1090
|
-
*/
|
|
1091
|
-
const appRouter = router({
|
|
1092
|
-
spec: specRouter,
|
|
1093
|
-
change: changeRouter,
|
|
1094
|
-
archive: archiveRouter,
|
|
1095
|
-
init: initRouter,
|
|
1096
|
-
realtime: realtimeRouter,
|
|
1097
|
-
config: configRouter,
|
|
1098
|
-
cli: cliRouter,
|
|
1099
|
-
opsx: opsxRouter,
|
|
1100
|
-
kv: kvRouter
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
//#endregion
|
|
1104
|
-
//#region src/port-utils.ts
|
|
1105
|
-
/**
|
|
1106
|
-
* Check if a port is available by trying to listen on it.
|
|
1107
|
-
* Uses default binding (both IPv4 and IPv6) to detect conflicts.
|
|
1108
|
-
*/
|
|
1109
|
-
function isPortAvailable(port) {
|
|
1110
|
-
return new Promise((resolve$1) => {
|
|
1111
|
-
const server = createServer$1();
|
|
1112
|
-
server.once("error", () => {
|
|
1113
|
-
resolve$1(false);
|
|
1114
|
-
});
|
|
1115
|
-
server.once("listening", () => {
|
|
1116
|
-
server.close(() => resolve$1(true));
|
|
1117
|
-
});
|
|
1118
|
-
server.listen(port);
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* Find an available port starting from the given port.
|
|
1123
|
-
* Will try up to maxAttempts ports sequentially.
|
|
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
|
|
1482
|
+
* Search router - unified fulltext search over specs/changes/archives
|
|
1129
1483
|
*/
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
name: "xterm-256color",
|
|
1175
|
-
cols: opts.cols ?? 80,
|
|
1176
|
-
rows: opts.rows ?? 24,
|
|
1177
|
-
cwd: opts.cwd,
|
|
1178
|
-
env: {
|
|
1179
|
-
...process.env,
|
|
1180
|
-
TERM: "xterm-256color"
|
|
1181
|
-
}
|
|
1182
|
-
});
|
|
1183
|
-
this.process.onData((data) => {
|
|
1184
|
-
this.appendBuffer(data);
|
|
1185
|
-
this.emit("data", data);
|
|
1186
|
-
});
|
|
1187
|
-
this.process.onExit(({ exitCode }) => {
|
|
1188
|
-
if (this.titleInterval) {
|
|
1189
|
-
clearInterval(this.titleInterval);
|
|
1190
|
-
this.titleInterval = null;
|
|
1191
|
-
}
|
|
1192
|
-
this.isExited = true;
|
|
1193
|
-
this.exitCode = exitCode;
|
|
1194
|
-
this.emit("exit", exitCode);
|
|
1195
|
-
});
|
|
1196
|
-
this.titleInterval = setInterval(() => {
|
|
1197
|
-
try {
|
|
1198
|
-
const title = this.process.process;
|
|
1199
|
-
if (title && title !== this.lastTitle) {
|
|
1200
|
-
this.lastTitle = title;
|
|
1201
|
-
this.emit("title", title);
|
|
1202
|
-
}
|
|
1203
|
-
} catch {}
|
|
1204
|
-
}, 1e3);
|
|
1205
|
-
}
|
|
1206
|
-
get title() {
|
|
1207
|
-
return this.lastTitle;
|
|
1208
|
-
}
|
|
1209
|
-
appendBuffer(data) {
|
|
1210
|
-
let chunk = data;
|
|
1211
|
-
if (chunk.length > this.maxBufferBytes) chunk = chunk.slice(-this.maxBufferBytes);
|
|
1212
|
-
this.buffer.push(chunk);
|
|
1213
|
-
this.bufferByteLength += chunk.length;
|
|
1214
|
-
while (this.bufferByteLength > this.maxBufferBytes && this.buffer.length > 0) {
|
|
1215
|
-
const removed = this.buffer.shift();
|
|
1216
|
-
this.bufferByteLength -= removed.length;
|
|
1217
|
-
}
|
|
1218
|
-
while (this.buffer.length > this.maxBufferLines) {
|
|
1219
|
-
const removed = this.buffer.shift();
|
|
1220
|
-
this.bufferByteLength -= removed.length;
|
|
1221
|
-
}
|
|
1484
|
+
const searchRouter = router({
|
|
1485
|
+
query: publicProcedure.input(SearchQuerySchema).query(async ({ ctx, input }) => {
|
|
1486
|
+
return ctx.searchService.query(input);
|
|
1487
|
+
}),
|
|
1488
|
+
subscribe: publicProcedure.input(SearchQuerySchema).subscription(({ ctx, input }) => {
|
|
1489
|
+
return createReactiveSubscriptionWithInput((queryInput) => ctx.searchService.queryReactive(queryInput))(input);
|
|
1490
|
+
})
|
|
1491
|
+
});
|
|
1492
|
+
/**
|
|
1493
|
+
* Main app router
|
|
1494
|
+
*/
|
|
1495
|
+
const appRouter = router({
|
|
1496
|
+
spec: specRouter,
|
|
1497
|
+
change: changeRouter,
|
|
1498
|
+
archive: archiveRouter,
|
|
1499
|
+
init: initRouter,
|
|
1500
|
+
realtime: realtimeRouter,
|
|
1501
|
+
config: configRouter,
|
|
1502
|
+
cli: cliRouter,
|
|
1503
|
+
opsx: opsxRouter,
|
|
1504
|
+
kv: kvRouter,
|
|
1505
|
+
search: searchRouter
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
//#endregion
|
|
1509
|
+
//#region src/search-documents.ts
|
|
1510
|
+
function joinParts(parts) {
|
|
1511
|
+
return parts.map((part) => part?.trim() ?? "").filter((part) => part.length > 0).join("\n\n");
|
|
1512
|
+
}
|
|
1513
|
+
async function collectSearchDocuments(adapter) {
|
|
1514
|
+
const docs = [];
|
|
1515
|
+
const specs = await adapter.listSpecsWithMeta();
|
|
1516
|
+
for (const spec of specs) {
|
|
1517
|
+
const raw = await adapter.readSpecRaw(spec.id);
|
|
1518
|
+
if (!raw) continue;
|
|
1519
|
+
docs.push({
|
|
1520
|
+
id: `spec:${spec.id}`,
|
|
1521
|
+
kind: "spec",
|
|
1522
|
+
title: spec.name,
|
|
1523
|
+
href: `/specs/${encodeURIComponent(spec.id)}`,
|
|
1524
|
+
path: `openspec/specs/${spec.id}/spec.md`,
|
|
1525
|
+
content: raw,
|
|
1526
|
+
updatedAt: spec.updatedAt
|
|
1527
|
+
});
|
|
1222
1528
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1529
|
+
const changes = await adapter.listChangesWithMeta();
|
|
1530
|
+
for (const change of changes) {
|
|
1531
|
+
const raw = await adapter.readChangeRaw(change.id);
|
|
1532
|
+
if (!raw) continue;
|
|
1533
|
+
docs.push({
|
|
1534
|
+
id: `change:${change.id}`,
|
|
1535
|
+
kind: "change",
|
|
1536
|
+
title: change.name,
|
|
1537
|
+
href: `/changes/${encodeURIComponent(change.id)}`,
|
|
1538
|
+
path: `openspec/changes/${change.id}`,
|
|
1539
|
+
content: joinParts([
|
|
1540
|
+
raw.proposal,
|
|
1541
|
+
raw.tasks,
|
|
1542
|
+
raw.design,
|
|
1543
|
+
...raw.deltaSpecs.map((deltaSpec) => deltaSpec.content)
|
|
1544
|
+
]),
|
|
1545
|
+
updatedAt: change.updatedAt
|
|
1546
|
+
});
|
|
1225
1547
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1548
|
+
const archives = await adapter.listArchivedChangesWithMeta();
|
|
1549
|
+
for (const archive of archives) {
|
|
1550
|
+
const raw = await adapter.readArchivedChangeRaw(archive.id);
|
|
1551
|
+
if (!raw) continue;
|
|
1552
|
+
docs.push({
|
|
1553
|
+
id: `archive:${archive.id}`,
|
|
1554
|
+
kind: "archive",
|
|
1555
|
+
title: archive.name,
|
|
1556
|
+
href: `/archive/${encodeURIComponent(archive.id)}`,
|
|
1557
|
+
path: `openspec/changes/archive/${archive.id}`,
|
|
1558
|
+
content: joinParts([
|
|
1559
|
+
raw.proposal,
|
|
1560
|
+
raw.tasks,
|
|
1561
|
+
raw.design,
|
|
1562
|
+
...raw.deltaSpecs.map((deltaSpec) => deltaSpec.content)
|
|
1563
|
+
]),
|
|
1564
|
+
updatedAt: archive.updatedAt
|
|
1565
|
+
});
|
|
1228
1566
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1567
|
+
return docs;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
//#endregion
|
|
1571
|
+
//#region src/search-service.ts
|
|
1572
|
+
const REBUILD_DEBOUNCE_MS = 250;
|
|
1573
|
+
var SearchService = class {
|
|
1574
|
+
provider;
|
|
1575
|
+
initialized = false;
|
|
1576
|
+
initPromise = null;
|
|
1577
|
+
rebuildPromise = null;
|
|
1578
|
+
rebuildTimer = null;
|
|
1579
|
+
constructor(adapter, watcher, provider = new NodeWorkerSearchProvider()) {
|
|
1580
|
+
this.adapter = adapter;
|
|
1581
|
+
this.provider = provider;
|
|
1582
|
+
watcher?.on("change", () => {
|
|
1583
|
+
this.scheduleRebuild();
|
|
1584
|
+
});
|
|
1231
1585
|
}
|
|
1232
|
-
|
|
1233
|
-
if (this.
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1586
|
+
async init() {
|
|
1587
|
+
if (this.initialized) return;
|
|
1588
|
+
if (this.initPromise) return this.initPromise;
|
|
1589
|
+
this.initPromise = this.rebuildIndex(true);
|
|
1237
1590
|
try {
|
|
1238
|
-
this.
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
toInfo() {
|
|
1243
|
-
return {
|
|
1244
|
-
id: this.id,
|
|
1245
|
-
title: this.lastTitle,
|
|
1246
|
-
command: this.command,
|
|
1247
|
-
args: this.args,
|
|
1248
|
-
platform: this.platform,
|
|
1249
|
-
isExited: this.isExited,
|
|
1250
|
-
exitCode: this.exitCode,
|
|
1251
|
-
createdAt: this.createdAt
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
};
|
|
1255
|
-
var PtyManager = class {
|
|
1256
|
-
sessions = /* @__PURE__ */ new Map();
|
|
1257
|
-
idCounter = 0;
|
|
1258
|
-
platform;
|
|
1259
|
-
constructor(defaultCwd) {
|
|
1260
|
-
this.defaultCwd = defaultCwd;
|
|
1261
|
-
this.platform = detectPtyPlatform();
|
|
1591
|
+
await this.initPromise;
|
|
1592
|
+
} finally {
|
|
1593
|
+
this.initPromise = null;
|
|
1594
|
+
}
|
|
1262
1595
|
}
|
|
1263
|
-
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
rows: opts.rows,
|
|
1268
|
-
command: opts.command,
|
|
1269
|
-
args: opts.args,
|
|
1270
|
-
cwd: this.defaultCwd,
|
|
1271
|
-
scrollback: opts.scrollback,
|
|
1272
|
-
maxBufferBytes: opts.maxBufferBytes,
|
|
1273
|
-
platform: this.platform
|
|
1274
|
-
});
|
|
1275
|
-
this.sessions.set(id, session);
|
|
1276
|
-
return session;
|
|
1596
|
+
async query(input) {
|
|
1597
|
+
const parsed = SearchQuerySchema.parse(input);
|
|
1598
|
+
await this.init();
|
|
1599
|
+
return this.provider.search(parsed);
|
|
1277
1600
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1601
|
+
async queryReactive(input) {
|
|
1602
|
+
const parsed = SearchQuerySchema.parse(input);
|
|
1603
|
+
await this.rebuildIndex();
|
|
1604
|
+
return this.provider.search(parsed);
|
|
1280
1605
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
return result;
|
|
1606
|
+
async dispose() {
|
|
1607
|
+
this.cancelRebuild();
|
|
1608
|
+
await this.provider.dispose();
|
|
1285
1609
|
}
|
|
1286
|
-
|
|
1287
|
-
this.
|
|
1610
|
+
scheduleRebuild() {
|
|
1611
|
+
this.cancelRebuild();
|
|
1612
|
+
this.rebuildTimer = setTimeout(() => {
|
|
1613
|
+
this.rebuildTimer = null;
|
|
1614
|
+
this.rebuildIndex().catch(() => {});
|
|
1615
|
+
}, REBUILD_DEBOUNCE_MS);
|
|
1288
1616
|
}
|
|
1289
|
-
|
|
1290
|
-
this.
|
|
1617
|
+
cancelRebuild() {
|
|
1618
|
+
if (!this.rebuildTimer) return;
|
|
1619
|
+
clearTimeout(this.rebuildTimer);
|
|
1620
|
+
this.rebuildTimer = null;
|
|
1291
1621
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
if (
|
|
1295
|
-
|
|
1296
|
-
this.
|
|
1622
|
+
async rebuildIndex(forceInit = false) {
|
|
1623
|
+
if (!forceInit && !this.initialized) return;
|
|
1624
|
+
if (this.rebuildPromise) return this.rebuildPromise;
|
|
1625
|
+
this.rebuildPromise = (async () => {
|
|
1626
|
+
const docs = await collectSearchDocuments(this.adapter);
|
|
1627
|
+
if (this.initialized) await this.provider.replaceAll(docs);
|
|
1628
|
+
else {
|
|
1629
|
+
await this.provider.init(docs);
|
|
1630
|
+
this.initialized = true;
|
|
1631
|
+
}
|
|
1632
|
+
})();
|
|
1633
|
+
try {
|
|
1634
|
+
await this.rebuildPromise;
|
|
1635
|
+
} finally {
|
|
1636
|
+
this.rebuildPromise = null;
|
|
1297
1637
|
}
|
|
1298
1638
|
}
|
|
1299
|
-
closeAll() {
|
|
1300
|
-
for (const session of this.sessions.values()) session.close();
|
|
1301
|
-
this.sessions.clear();
|
|
1302
|
-
}
|
|
1303
1639
|
};
|
|
1304
1640
|
|
|
1305
|
-
//#endregion
|
|
1306
|
-
//#region src/pty-websocket.ts
|
|
1307
|
-
function createPtyWebSocketHandler(ptyManager) {
|
|
1308
|
-
return (ws) => {
|
|
1309
|
-
const cleanups = /* @__PURE__ */ new Map();
|
|
1310
|
-
const send = (msg) => {
|
|
1311
|
-
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
|
|
1312
|
-
};
|
|
1313
|
-
const sendError = (code, message, opts) => {
|
|
1314
|
-
send({
|
|
1315
|
-
type: "error",
|
|
1316
|
-
code,
|
|
1317
|
-
message,
|
|
1318
|
-
sessionId: opts?.sessionId
|
|
1319
|
-
});
|
|
1320
|
-
};
|
|
1321
|
-
const attachToSession = (session, opts) => {
|
|
1322
|
-
const sessionId = session.id;
|
|
1323
|
-
cleanups.get(sessionId)?.();
|
|
1324
|
-
if (opts?.cols && opts?.rows && !session.isExited) session.resize(opts.cols, opts.rows);
|
|
1325
|
-
const onData = (data) => {
|
|
1326
|
-
send({
|
|
1327
|
-
type: "output",
|
|
1328
|
-
sessionId,
|
|
1329
|
-
data
|
|
1330
|
-
});
|
|
1331
|
-
};
|
|
1332
|
-
const onExit = (exitCode) => {
|
|
1333
|
-
send({
|
|
1334
|
-
type: "exit",
|
|
1335
|
-
sessionId,
|
|
1336
|
-
exitCode
|
|
1337
|
-
});
|
|
1338
|
-
};
|
|
1339
|
-
const onTitle = (title) => {
|
|
1340
|
-
send({
|
|
1341
|
-
type: "title",
|
|
1342
|
-
sessionId,
|
|
1343
|
-
title
|
|
1344
|
-
});
|
|
1345
|
-
};
|
|
1346
|
-
session.on("data", onData);
|
|
1347
|
-
session.on("exit", onExit);
|
|
1348
|
-
session.on("title", onTitle);
|
|
1349
|
-
cleanups.set(sessionId, () => {
|
|
1350
|
-
session.removeListener("data", onData);
|
|
1351
|
-
session.removeListener("exit", onExit);
|
|
1352
|
-
session.removeListener("title", onTitle);
|
|
1353
|
-
cleanups.delete(sessionId);
|
|
1354
|
-
});
|
|
1355
|
-
};
|
|
1356
|
-
ws.on("message", (raw) => {
|
|
1357
|
-
let parsed;
|
|
1358
|
-
try {
|
|
1359
|
-
parsed = JSON.parse(String(raw));
|
|
1360
|
-
} catch {
|
|
1361
|
-
sendError("INVALID_JSON", "Invalid JSON payload");
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
const parsedMessage = PtyClientMessageSchema.safeParse(parsed);
|
|
1365
|
-
if (!parsedMessage.success) {
|
|
1366
|
-
const firstIssue = parsedMessage.error.issues[0]?.message;
|
|
1367
|
-
sendError("INVALID_MESSAGE", firstIssue ?? "Invalid PTY message");
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
const msg = parsedMessage.data;
|
|
1371
|
-
switch (msg.type) {
|
|
1372
|
-
case "create": {
|
|
1373
|
-
const session = ptyManager.create({
|
|
1374
|
-
cols: msg.cols,
|
|
1375
|
-
rows: msg.rows,
|
|
1376
|
-
command: msg.command,
|
|
1377
|
-
args: msg.args
|
|
1378
|
-
});
|
|
1379
|
-
send({
|
|
1380
|
-
type: "created",
|
|
1381
|
-
requestId: msg.requestId,
|
|
1382
|
-
sessionId: session.id,
|
|
1383
|
-
platform: session.platform
|
|
1384
|
-
});
|
|
1385
|
-
attachToSession(session);
|
|
1386
|
-
break;
|
|
1387
|
-
}
|
|
1388
|
-
case "attach": {
|
|
1389
|
-
const session = ptyManager.get(msg.sessionId);
|
|
1390
|
-
if (!session) {
|
|
1391
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
1392
|
-
send({
|
|
1393
|
-
type: "exit",
|
|
1394
|
-
sessionId: msg.sessionId,
|
|
1395
|
-
exitCode: -1
|
|
1396
|
-
});
|
|
1397
|
-
break;
|
|
1398
|
-
}
|
|
1399
|
-
attachToSession(session, {
|
|
1400
|
-
cols: msg.cols,
|
|
1401
|
-
rows: msg.rows
|
|
1402
|
-
});
|
|
1403
|
-
const buffer = session.getBuffer();
|
|
1404
|
-
if (buffer) send({
|
|
1405
|
-
type: "buffer",
|
|
1406
|
-
sessionId: session.id,
|
|
1407
|
-
data: buffer
|
|
1408
|
-
});
|
|
1409
|
-
if (session.title) send({
|
|
1410
|
-
type: "title",
|
|
1411
|
-
sessionId: session.id,
|
|
1412
|
-
title: session.title
|
|
1413
|
-
});
|
|
1414
|
-
if (session.isExited) send({
|
|
1415
|
-
type: "exit",
|
|
1416
|
-
sessionId: session.id,
|
|
1417
|
-
exitCode: session.exitCode ?? -1
|
|
1418
|
-
});
|
|
1419
|
-
break;
|
|
1420
|
-
}
|
|
1421
|
-
case "list":
|
|
1422
|
-
send({
|
|
1423
|
-
type: "list",
|
|
1424
|
-
sessions: ptyManager.list().map((s) => ({
|
|
1425
|
-
id: s.id,
|
|
1426
|
-
title: s.title,
|
|
1427
|
-
command: s.command,
|
|
1428
|
-
args: s.args,
|
|
1429
|
-
platform: s.platform,
|
|
1430
|
-
isExited: s.isExited,
|
|
1431
|
-
exitCode: s.exitCode
|
|
1432
|
-
}))
|
|
1433
|
-
});
|
|
1434
|
-
break;
|
|
1435
|
-
case "input": {
|
|
1436
|
-
const session = ptyManager.get(msg.sessionId);
|
|
1437
|
-
if (!session) {
|
|
1438
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
1439
|
-
break;
|
|
1440
|
-
}
|
|
1441
|
-
session.write(msg.data);
|
|
1442
|
-
break;
|
|
1443
|
-
}
|
|
1444
|
-
case "resize": {
|
|
1445
|
-
const session = ptyManager.get(msg.sessionId);
|
|
1446
|
-
if (!session) {
|
|
1447
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
1448
|
-
break;
|
|
1449
|
-
}
|
|
1450
|
-
session.resize(msg.cols, msg.rows);
|
|
1451
|
-
break;
|
|
1452
|
-
}
|
|
1453
|
-
case "close": {
|
|
1454
|
-
const session = ptyManager.get(msg.sessionId);
|
|
1455
|
-
if (!session) {
|
|
1456
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
1457
|
-
break;
|
|
1458
|
-
}
|
|
1459
|
-
cleanups.get(msg.sessionId)?.();
|
|
1460
|
-
ptyManager.close(session.id);
|
|
1461
|
-
break;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
});
|
|
1465
|
-
ws.on("close", () => {
|
|
1466
|
-
for (const cleanup of cleanups.values()) cleanup();
|
|
1467
|
-
cleanups.clear();
|
|
1468
|
-
});
|
|
1469
|
-
};
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
1641
|
//#endregion
|
|
1473
1642
|
//#region src/server.ts
|
|
1474
1643
|
/**
|
|
@@ -1492,6 +1661,7 @@ function createServer(config) {
|
|
|
1492
1661
|
const cliExecutor = new CliExecutor(configManager, config.projectDir);
|
|
1493
1662
|
const kernel = config.kernel;
|
|
1494
1663
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
1664
|
+
const searchService = new SearchService(adapter, watcher);
|
|
1495
1665
|
const app = new Hono();
|
|
1496
1666
|
const corsOrigins = config.corsOrigins ?? ["http://localhost:5173", "http://localhost:3000"];
|
|
1497
1667
|
app.use("*", cors({
|
|
@@ -1515,6 +1685,7 @@ function createServer(config) {
|
|
|
1515
1685
|
configManager,
|
|
1516
1686
|
cliExecutor,
|
|
1517
1687
|
kernel,
|
|
1688
|
+
searchService,
|
|
1518
1689
|
watcher,
|
|
1519
1690
|
projectDir: config.projectDir
|
|
1520
1691
|
})
|
|
@@ -1525,6 +1696,7 @@ function createServer(config) {
|
|
|
1525
1696
|
configManager,
|
|
1526
1697
|
cliExecutor,
|
|
1527
1698
|
kernel,
|
|
1699
|
+
searchService,
|
|
1528
1700
|
watcher,
|
|
1529
1701
|
projectDir: config.projectDir
|
|
1530
1702
|
});
|
|
@@ -1534,6 +1706,7 @@ function createServer(config) {
|
|
|
1534
1706
|
configManager,
|
|
1535
1707
|
cliExecutor,
|
|
1536
1708
|
kernel,
|
|
1709
|
+
searchService,
|
|
1537
1710
|
watcher,
|
|
1538
1711
|
createContext,
|
|
1539
1712
|
port: config.port ?? 3100
|
|
@@ -1575,6 +1748,7 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
1575
1748
|
ptyWss.close();
|
|
1576
1749
|
wss.close();
|
|
1577
1750
|
server.watcher?.stop();
|
|
1751
|
+
server.searchService.dispose().catch(() => {});
|
|
1578
1752
|
}
|
|
1579
1753
|
};
|
|
1580
1754
|
}
|
|
@@ -1607,6 +1781,9 @@ async function startServer(config, setupApp) {
|
|
|
1607
1781
|
kernel.warmup().catch((err) => {
|
|
1608
1782
|
console.error("Kernel warmup failed:", err);
|
|
1609
1783
|
});
|
|
1784
|
+
server.searchService.init().catch((err) => {
|
|
1785
|
+
console.error("Search service warmup failed:", err);
|
|
1786
|
+
});
|
|
1610
1787
|
return {
|
|
1611
1788
|
url,
|
|
1612
1789
|
port,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openspecui/server",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"yaml": "^2.8.0",
|
|
21
21
|
"yargs": "^18.0.0",
|
|
22
22
|
"zod": "^3.24.1",
|
|
23
|
-
"@openspecui/
|
|
23
|
+
"@openspecui/search": "1.1.0",
|
|
24
|
+
"@openspecui/core": "1.1.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@types/node": "^22.10.2",
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"scripts": {
|
|
35
36
|
"build": "tsdown src/index.ts --format esm --no-dts",
|
|
36
37
|
"typecheck": "tsc --noEmit",
|
|
37
|
-
"dev": "tsx watch --include '../core/dist/**' src/standalone.ts",
|
|
38
|
+
"dev": "tsx watch --include '../core/dist/**' --include '../search/dist/**' src/standalone.ts",
|
|
38
39
|
"test": "vitest run",
|
|
39
40
|
"test:watch": "vitest"
|
|
40
41
|
}
|