@openspecui/server 1.0.4 → 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 +544 -389
- 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
|
-
|
|
17
|
-
|
|
18
|
+
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
19
|
+
import { NodeWorkerSearchProvider } from "@openspecui/search/node";
|
|
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
|
+
}
|
|
18
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,6 +1479,17 @@ const kvRouter = router({
|
|
|
1086
1479
|
})
|
|
1087
1480
|
});
|
|
1088
1481
|
/**
|
|
1482
|
+
* Search router - unified fulltext search over specs/changes/archives
|
|
1483
|
+
*/
|
|
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
|
+
/**
|
|
1089
1493
|
* Main app router
|
|
1090
1494
|
*/
|
|
1091
1495
|
const appRouter = router({
|
|
@@ -1095,402 +1499,145 @@ const appRouter = router({
|
|
|
1095
1499
|
init: initRouter,
|
|
1096
1500
|
realtime: realtimeRouter,
|
|
1097
1501
|
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
|
-
}
|
|
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
|
+
});
|
|
1241
1528
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
+
});
|
|
1244
1547
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
+
});
|
|
1247
1566
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
+
});
|
|
1250
1585
|
}
|
|
1251
|
-
|
|
1252
|
-
if (this.
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1586
|
+
async init() {
|
|
1587
|
+
if (this.initialized) return;
|
|
1588
|
+
if (this.initPromise) return this.initPromise;
|
|
1589
|
+
this.initPromise = this.rebuildIndex(true);
|
|
1256
1590
|
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();
|
|
1591
|
+
await this.initPromise;
|
|
1592
|
+
} finally {
|
|
1593
|
+
this.initPromise = null;
|
|
1594
|
+
}
|
|
1281
1595
|
}
|
|
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;
|
|
1596
|
+
async query(input) {
|
|
1597
|
+
const parsed = SearchQuerySchema.parse(input);
|
|
1598
|
+
await this.init();
|
|
1599
|
+
return this.provider.search(parsed);
|
|
1296
1600
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1601
|
+
async queryReactive(input) {
|
|
1602
|
+
const parsed = SearchQuerySchema.parse(input);
|
|
1603
|
+
await this.rebuildIndex();
|
|
1604
|
+
return this.provider.search(parsed);
|
|
1299
1605
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
return result;
|
|
1606
|
+
async dispose() {
|
|
1607
|
+
this.cancelRebuild();
|
|
1608
|
+
await this.provider.dispose();
|
|
1304
1609
|
}
|
|
1305
|
-
|
|
1306
|
-
this.
|
|
1610
|
+
scheduleRebuild() {
|
|
1611
|
+
this.cancelRebuild();
|
|
1612
|
+
this.rebuildTimer = setTimeout(() => {
|
|
1613
|
+
this.rebuildTimer = null;
|
|
1614
|
+
this.rebuildIndex().catch(() => {});
|
|
1615
|
+
}, REBUILD_DEBOUNCE_MS);
|
|
1307
1616
|
}
|
|
1308
|
-
|
|
1309
|
-
this.
|
|
1617
|
+
cancelRebuild() {
|
|
1618
|
+
if (!this.rebuildTimer) return;
|
|
1619
|
+
clearTimeout(this.rebuildTimer);
|
|
1620
|
+
this.rebuildTimer = null;
|
|
1310
1621
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
if (
|
|
1314
|
-
|
|
1315
|
-
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;
|
|
1316
1637
|
}
|
|
1317
1638
|
}
|
|
1318
|
-
closeAll() {
|
|
1319
|
-
for (const session of this.sessions.values()) session.close();
|
|
1320
|
-
this.sessions.clear();
|
|
1321
|
-
}
|
|
1322
1639
|
};
|
|
1323
1640
|
|
|
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
1641
|
//#endregion
|
|
1495
1642
|
//#region src/server.ts
|
|
1496
1643
|
/**
|
|
@@ -1514,6 +1661,7 @@ function createServer(config) {
|
|
|
1514
1661
|
const cliExecutor = new CliExecutor(configManager, config.projectDir);
|
|
1515
1662
|
const kernel = config.kernel;
|
|
1516
1663
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
1664
|
+
const searchService = new SearchService(adapter, watcher);
|
|
1517
1665
|
const app = new Hono();
|
|
1518
1666
|
const corsOrigins = config.corsOrigins ?? ["http://localhost:5173", "http://localhost:3000"];
|
|
1519
1667
|
app.use("*", cors({
|
|
@@ -1537,6 +1685,7 @@ function createServer(config) {
|
|
|
1537
1685
|
configManager,
|
|
1538
1686
|
cliExecutor,
|
|
1539
1687
|
kernel,
|
|
1688
|
+
searchService,
|
|
1540
1689
|
watcher,
|
|
1541
1690
|
projectDir: config.projectDir
|
|
1542
1691
|
})
|
|
@@ -1547,6 +1696,7 @@ function createServer(config) {
|
|
|
1547
1696
|
configManager,
|
|
1548
1697
|
cliExecutor,
|
|
1549
1698
|
kernel,
|
|
1699
|
+
searchService,
|
|
1550
1700
|
watcher,
|
|
1551
1701
|
projectDir: config.projectDir
|
|
1552
1702
|
});
|
|
@@ -1556,6 +1706,7 @@ function createServer(config) {
|
|
|
1556
1706
|
configManager,
|
|
1557
1707
|
cliExecutor,
|
|
1558
1708
|
kernel,
|
|
1709
|
+
searchService,
|
|
1559
1710
|
watcher,
|
|
1560
1711
|
createContext,
|
|
1561
1712
|
port: config.port ?? 3100
|
|
@@ -1597,6 +1748,7 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
1597
1748
|
ptyWss.close();
|
|
1598
1749
|
wss.close();
|
|
1599
1750
|
server.watcher?.stop();
|
|
1751
|
+
server.searchService.dispose().catch(() => {});
|
|
1600
1752
|
}
|
|
1601
1753
|
};
|
|
1602
1754
|
}
|
|
@@ -1629,6 +1781,9 @@ async function startServer(config, setupApp) {
|
|
|
1629
1781
|
kernel.warmup().catch((err) => {
|
|
1630
1782
|
console.error("Kernel warmup failed:", err);
|
|
1631
1783
|
});
|
|
1784
|
+
server.searchService.init().catch((err) => {
|
|
1785
|
+
console.error("Search service warmup failed:", err);
|
|
1786
|
+
});
|
|
1632
1787
|
return {
|
|
1633
1788
|
url,
|
|
1634
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
|
}
|