xedoc-cli 0.1.6 → 0.1.8

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.
Files changed (30) hide show
  1. package/README.md +2 -2
  2. package/build/client/assets/__vite-browser-external-2447137e-Bb2UaVfr.js +0 -0
  3. package/build/client/assets/app-layout-DVRPLoJp.js +1 -0
  4. package/build/client/assets/app-shell-DnEsSg3r.js +2 -0
  5. package/build/client/assets/chat-CAKcWygY.js +1 -0
  6. package/build/client/assets/connect-BDvoix-O.js +1 -0
  7. package/build/client/assets/{document-title-B6Yxyaur.js → document-title-RYIi5OZD.js} +1 -1
  8. package/build/client/assets/{entry.client-BlQXdBHx.js → entry.client-BQZeV9JD.js} +1 -1
  9. package/build/client/assets/ghostty-web-CgkkAFeT.js +1 -0
  10. package/build/client/assets/home-BjcHgWsO.js +1 -0
  11. package/build/client/assets/{jsx-runtime-Dafdqr5g.js → jsx-runtime-Cal6WBdn.js} +1 -1
  12. package/build/client/assets/label-BpA-r6tL.js +1 -0
  13. package/build/client/assets/{manifest-f96902fd.js → manifest-40da439f.js} +1 -1
  14. package/build/client/assets/{react-dom-CMuFyqmq.js → react-dom-D4MOkHq2.js} +1 -1
  15. package/build/client/assets/root-COrr2Ht-.css +2 -0
  16. package/build/client/assets/{root-bXu6rtsG.js → root-D-p7jx9T.js} +1 -1
  17. package/build/client/assets/{session-provider-2Ozr-CuV.js → session-provider-QLGSjKmA.js} +1 -1
  18. package/build/server/index.js +1 -1
  19. package/package.json +4 -1
  20. package/prisma/schema.prisma +4 -0
  21. package/server/index.mjs +3 -0
  22. package/server/sqlite-setup.mjs +22 -0
  23. package/server/terminal-socket.mjs +453 -0
  24. package/build/client/assets/app-layout--vTe8I2j.js +0 -1
  25. package/build/client/assets/app-shell-ClOglt7r.js +0 -1
  26. package/build/client/assets/chat-Bf8hrS42.js +0 -1
  27. package/build/client/assets/connect-CHSFTXwk.js +0 -1
  28. package/build/client/assets/home-BLKISIGJ.js +0 -1
  29. package/build/client/assets/label-C7i5Qt-O.js +0 -1
  30. package/build/client/assets/root-Cptu-zSj.css +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xedoc-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Local web UI for Codex account, chat, execution, and workspace management.",
5
5
  "author": "Edward Nguyen <monokaijs@gmail.com>",
6
6
  "type": "module",
@@ -19,6 +19,7 @@
19
19
  "build/server/index.js",
20
20
  "prisma/schema.prisma",
21
21
  "server/index.mjs",
22
+ "server/terminal-socket.mjs",
22
23
  "server/sqlite-setup.mjs",
23
24
  "README.md"
24
25
  ],
@@ -44,6 +45,7 @@
44
45
  "dependencies": {
45
46
  "@base-ui/react": "^1.4.1",
46
47
  "@fontsource-variable/geist": "^5.2.9",
48
+ "@lydell/node-pty": "1.1.0",
47
49
  "@openai/codex": "^0.130.0",
48
50
  "@prisma/client": "^6.19.3",
49
51
  "@react-router/node": "^7.15.1",
@@ -52,6 +54,7 @@
52
54
  "clsx": "^2.1.1",
53
55
  "cmdk": "^1.1.1",
54
56
  "dotenv": "16.6.1",
57
+ "ghostty-web": "^0.4.0",
55
58
  "highlight.js": "^11.11.1",
56
59
  "isbot": "^5.1.32",
57
60
  "lucide-react": "^1.16.0",
@@ -74,6 +74,9 @@ model CodexAccount {
74
74
  defaultReasoningEffort String?
75
75
  defaultServiceTier String?
76
76
  lastAuthUrl String?
77
+ lastAuthMode String?
78
+ lastAuthLoginId String?
79
+ lastAuthUserCode String?
77
80
  lastError String?
78
81
  createdAt DateTime @default(now())
79
82
  updatedAt DateTime @updatedAt
@@ -84,6 +87,7 @@ model CodexAccount {
84
87
  model Chat {
85
88
  id String @id @default(uuid())
86
89
  accountId String?
90
+ autoRotateAccount Boolean @default(false)
87
91
  title String
88
92
  workingDirectory String?
89
93
  model String?
package/server/index.mjs CHANGED
@@ -16,6 +16,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"
16
16
  import { PrismaClient } from "@prisma/client"
17
17
  import { createRequestListener } from "@react-router/node"
18
18
  import { Server as SocketServer } from "socket.io"
19
+ import { installTerminalSocketHandlers } from "./terminal-socket.mjs"
19
20
 
20
21
  process.env.NODE_ENV = process.env.NODE_ENV ?? "production"
21
22
 
@@ -147,6 +148,8 @@ function installSocketServer(httpServer) {
147
148
  })
148
149
  })
149
150
 
151
+ installTerminalSocketHandlers(io, { workspaceRoot })
152
+
150
153
  const state = getRealtimeState()
151
154
  const handler = (event) => {
152
155
  io.to(chatRoomName(event.chatId)).emit("chat:event", event)
@@ -70,6 +70,9 @@ async function rebuildCodexAccountTable(prisma) {
70
70
  "defaultReasoningEffort",
71
71
  "defaultServiceTier",
72
72
  "lastAuthUrl",
73
+ "lastAuthMode",
74
+ "lastAuthLoginId",
75
+ "lastAuthUserCode",
73
76
  "lastError",
74
77
  "createdAt",
75
78
  "updatedAt"
@@ -86,6 +89,9 @@ async function rebuildCodexAccountTable(prisma) {
86
89
  ${await selectColumnOrNull(prisma, "CodexAccount", "defaultReasoningEffort")},
87
90
  ${await selectColumnOrNull(prisma, "CodexAccount", "defaultServiceTier")},
88
91
  "lastAuthUrl",
92
+ ${await selectColumnOrNull(prisma, "CodexAccount", "lastAuthMode")},
93
+ ${await selectColumnOrNull(prisma, "CodexAccount", "lastAuthLoginId")},
94
+ ${await selectColumnOrNull(prisma, "CodexAccount", "lastAuthUserCode")},
89
95
  "lastError",
90
96
  "createdAt",
91
97
  "updatedAt"
@@ -104,6 +110,7 @@ async function rebuildChatTable(prisma) {
104
110
  INSERT INTO "Chat_new" (
105
111
  "id",
106
112
  "accountId",
113
+ "autoRotateAccount",
107
114
  "title",
108
115
  "workingDirectory",
109
116
  "model",
@@ -120,6 +127,7 @@ async function rebuildChatTable(prisma) {
120
127
  SELECT
121
128
  "id",
122
129
  "accountId",
130
+ ${await selectColumnOrDefault(prisma, "Chat", "autoRotateAccount", "0")},
123
131
  "title",
124
132
  "workingDirectory",
125
133
  "model",
@@ -144,6 +152,12 @@ async function selectColumnOrNull(prisma, tableName, columnName) {
144
152
  : "NULL"
145
153
  }
146
154
 
155
+ async function selectColumnOrDefault(prisma, tableName, columnName, fallback) {
156
+ return (await tableHasColumn(prisma, tableName, columnName))
157
+ ? `"${columnName}"`
158
+ : fallback
159
+ }
160
+
147
161
  function isDuplicateColumnError(error) {
148
162
  const message = error instanceof Error ? error.message : String(error)
149
163
  return message.toLowerCase().includes("duplicate column name")
@@ -184,6 +198,9 @@ function createCodexAccountTable(tableName) {
184
198
  "defaultReasoningEffort" TEXT,
185
199
  "defaultServiceTier" TEXT,
186
200
  "lastAuthUrl" TEXT,
201
+ "lastAuthMode" TEXT,
202
+ "lastAuthLoginId" TEXT,
203
+ "lastAuthUserCode" TEXT,
187
204
  "lastError" TEXT,
188
205
  "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
189
206
  "updatedAt" DATETIME NOT NULL
@@ -194,6 +211,7 @@ function createChatTable(tableName) {
194
211
  return `CREATE TABLE IF NOT EXISTS "${tableName}" (
195
212
  "id" TEXT NOT NULL PRIMARY KEY,
196
213
  "accountId" TEXT,
214
+ "autoRotateAccount" BOOLEAN NOT NULL DEFAULT false,
197
215
  "title" TEXT NOT NULL,
198
216
  "workingDirectory" TEXT,
199
217
  "model" TEXT,
@@ -259,6 +277,10 @@ const schemaStatements = [
259
277
  'ALTER TABLE "CodexAccount" ADD COLUMN "defaultPermissionMode" TEXT',
260
278
  'ALTER TABLE "CodexAccount" ADD COLUMN "defaultReasoningEffort" TEXT',
261
279
  'ALTER TABLE "CodexAccount" ADD COLUMN "defaultServiceTier" TEXT',
280
+ 'ALTER TABLE "CodexAccount" ADD COLUMN "lastAuthMode" TEXT',
281
+ 'ALTER TABLE "CodexAccount" ADD COLUMN "lastAuthLoginId" TEXT',
282
+ 'ALTER TABLE "CodexAccount" ADD COLUMN "lastAuthUserCode" TEXT',
283
+ 'ALTER TABLE "Chat" ADD COLUMN "autoRotateAccount" BOOLEAN NOT NULL DEFAULT false',
262
284
  'CREATE INDEX IF NOT EXISTS "Chat_updatedAt_idx" ON "Chat"("updatedAt")',
263
285
  'CREATE INDEX IF NOT EXISTS "Chat_lastActivityAt_idx" ON "Chat"("lastActivityAt")',
264
286
  'CREATE INDEX IF NOT EXISTS "Chat_accountId_idx" ON "Chat"("accountId")',
@@ -0,0 +1,453 @@
1
+ import pty from "@lydell/node-pty"
2
+ import { randomUUID } from "node:crypto"
3
+ import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs"
4
+ import { homedir } from "node:os"
5
+ import {
6
+ basename,
7
+ isAbsolute,
8
+ join,
9
+ relative,
10
+ resolve,
11
+ } from "node:path"
12
+
13
+ const REPLAY_LIMIT_BYTES = 1_000_000
14
+ const MAX_TERMINAL_INPUT_BYTES = 1_000_000
15
+ const DEFAULT_COLS = 80
16
+ const DEFAULT_ROWS = 24
17
+
18
+ const installedServers = new WeakSet()
19
+ const terminals = new Map()
20
+
21
+ let exitHandlerInstalled = false
22
+
23
+ export function installTerminalSocketHandlers(io, options = {}) {
24
+ if (installedServers.has(io)) {
25
+ return
26
+ }
27
+ installedServers.add(io)
28
+ installProcessExitHandler()
29
+
30
+ const resolveDirectory = options.resolveDirectory ?? ((path) =>
31
+ resolveWorkspaceDirectory(path, options.workspaceRoot))
32
+
33
+ io.on("connection", (socket) => {
34
+ emitCount(socket)
35
+
36
+ socket.on("terminal:project:join", (payload, ack) => {
37
+ void handleAck(ack, async () => {
38
+ const projectPath = await resolveProjectPath(resolveDirectory, payload)
39
+ joinProjectRoom(socket, projectPath)
40
+ return {
41
+ projectPath,
42
+ terminals: listProjectTerminals(projectPath),
43
+ }
44
+ })
45
+ })
46
+
47
+ socket.on("terminal:project:leave", (payload, ack) => {
48
+ void handleAck(ack, async () => {
49
+ const projectPath = await resolveProjectPath(resolveDirectory, payload)
50
+ socket.leave(projectRoomName(projectPath))
51
+ if (socket.data.terminalProjectPath === projectPath) {
52
+ socket.data.terminalProjectPath = null
53
+ }
54
+ return { projectPath }
55
+ })
56
+ })
57
+
58
+ socket.on("terminal:list", (payload, ack) => {
59
+ void handleAck(ack, async () => {
60
+ const projectPath = await resolveProjectPath(resolveDirectory, payload)
61
+ return {
62
+ projectPath,
63
+ terminals: listProjectTerminals(projectPath),
64
+ }
65
+ })
66
+ })
67
+
68
+ socket.on("terminal:create", (payload, ack) => {
69
+ void handleAck(ack, async () => {
70
+ const projectPath = await resolveProjectPath(resolveDirectory, payload)
71
+ joinProjectRoom(socket, projectPath)
72
+ const terminal = createTerminal(projectPath, payload)
73
+ socket.join(terminalRoomName(terminal.id))
74
+ broadcastProject(io, projectPath)
75
+ broadcastCount(io)
76
+ return {
77
+ projectPath,
78
+ terminal: serializeTerminal(terminal),
79
+ terminals: listProjectTerminals(projectPath),
80
+ }
81
+ })
82
+ })
83
+
84
+ socket.on("terminal:attach", (payload, ack) => {
85
+ void handleAck(ack, async () => {
86
+ const terminal = requireTerminal(readTerminalId(payload))
87
+ socket.join(terminalRoomName(terminal.id))
88
+ return {
89
+ replay: terminal.replay.join(""),
90
+ terminal: serializeTerminal(terminal),
91
+ }
92
+ })
93
+ })
94
+
95
+ socket.on("terminal:detach", (payload, ack) => {
96
+ void handleAck(ack, async () => {
97
+ const terminalId = readTerminalId(payload)
98
+ socket.leave(terminalRoomName(terminalId))
99
+ return { terminalId }
100
+ })
101
+ })
102
+
103
+ socket.on("terminal:input", (payload, ack) => {
104
+ void handleAck(ack, async () => {
105
+ const terminal = requireTerminal(readTerminalId(payload))
106
+ if (terminal.status !== "running" || !terminal.pty) {
107
+ throw new Error("Terminal is not running.")
108
+ }
109
+ const data = readString(payload?.data, "data")
110
+ if (Buffer.byteLength(data, "utf8") > MAX_TERMINAL_INPUT_BYTES) {
111
+ throw new Error("Terminal input is too large.")
112
+ }
113
+ terminal.pty.write(data)
114
+ return { terminalId: terminal.id }
115
+ })
116
+ })
117
+
118
+ socket.on("terminal:resize", (payload, ack) => {
119
+ void handleAck(ack, async () => {
120
+ const terminal = requireTerminal(readTerminalId(payload))
121
+ const cols = normalizeDimension(payload?.cols, DEFAULT_COLS, 8, 500)
122
+ const rows = normalizeDimension(payload?.rows, DEFAULT_ROWS, 4, 300)
123
+ terminal.cols = cols
124
+ terminal.rows = rows
125
+ terminal.updatedAt = new Date()
126
+ if (terminal.status === "running" && terminal.pty) {
127
+ terminal.pty.resize(cols, rows)
128
+ }
129
+ return { cols, rows, terminalId: terminal.id }
130
+ })
131
+ })
132
+
133
+ socket.on("terminal:title", (payload, ack) => {
134
+ void handleAck(ack, async () => {
135
+ const terminal = requireTerminal(readTerminalId(payload))
136
+ const title = normalizeTitle(readString(payload?.title, "title"))
137
+ if (title && title !== terminal.title) {
138
+ terminal.title = title
139
+ terminal.updatedAt = new Date()
140
+ broadcastProject(io, terminal.projectPath)
141
+ }
142
+ return { terminal: serializeTerminal(terminal) }
143
+ })
144
+ })
145
+
146
+ socket.on("terminal:close", (payload, ack) => {
147
+ void handleAck(ack, async () => {
148
+ const terminal = requireTerminal(readTerminalId(payload))
149
+ closeTerminal(terminal)
150
+ broadcastProject(io, terminal.projectPath)
151
+ broadcastCount(io)
152
+ return {
153
+ projectPath: terminal.projectPath,
154
+ terminalId: terminal.id,
155
+ terminals: listProjectTerminals(terminal.projectPath),
156
+ }
157
+ })
158
+ })
159
+ })
160
+ }
161
+
162
+ function createTerminal(projectPath, payload) {
163
+ const id = randomUUID()
164
+ const cols = normalizeDimension(payload?.cols, DEFAULT_COLS, 8, 500)
165
+ const rows = normalizeDimension(payload?.rows, DEFAULT_ROWS, 4, 300)
166
+ const shell = defaultShell()
167
+ const ptyProcess = pty.spawn(shell.command, shell.args, {
168
+ cols,
169
+ cwd: projectPath,
170
+ env: {
171
+ ...process.env,
172
+ COLORTERM: "truecolor",
173
+ PWD: projectPath,
174
+ TERM: "xterm-256color",
175
+ },
176
+ name: "xterm-256color",
177
+ rows,
178
+ })
179
+ const now = new Date()
180
+ const terminal = {
181
+ cols,
182
+ createdAt: now,
183
+ exitCode: null,
184
+ id,
185
+ projectPath,
186
+ pty: ptyProcess,
187
+ replay: [],
188
+ replayBytes: 0,
189
+ rows,
190
+ shell: shell.command,
191
+ status: "running",
192
+ title: defaultTitle(projectPath),
193
+ titleBuffer: "",
194
+ updatedAt: now,
195
+ }
196
+ terminals.set(id, terminal)
197
+
198
+ ptyProcess.onData((data) => {
199
+ appendReplay(terminal, data)
200
+ const title = extractTitle(terminal, data)
201
+ if (title && title !== terminal.title) {
202
+ terminal.title = title
203
+ terminal.updatedAt = new Date()
204
+ broadcastProjectForTerminal(terminal)
205
+ }
206
+ const io = terminal.io
207
+ io?.to(terminalRoomName(id)).emit("terminal:output", {
208
+ data,
209
+ terminalId: id,
210
+ })
211
+ })
212
+
213
+ ptyProcess.onExit(({ exitCode, signal }) => {
214
+ terminal.exitCode = exitCode
215
+ terminal.pty = null
216
+ terminal.status = "exited"
217
+ terminal.updatedAt = new Date()
218
+ terminal.io?.to(terminalRoomName(id)).emit("terminal:exit", {
219
+ exitCode,
220
+ signal,
221
+ terminalId: id,
222
+ })
223
+ broadcastProjectForTerminal(terminal)
224
+ if (terminal.io) {
225
+ broadcastCount(terminal.io)
226
+ }
227
+ })
228
+
229
+ return terminal
230
+ }
231
+
232
+ function closeTerminal(terminal) {
233
+ terminals.delete(terminal.id)
234
+ if (terminal.pty) {
235
+ try {
236
+ terminal.pty.kill()
237
+ } catch {
238
+ // The process may already have exited.
239
+ }
240
+ }
241
+ terminal.pty = null
242
+ terminal.status = "closed"
243
+ terminal.updatedAt = new Date()
244
+ }
245
+
246
+ function joinProjectRoom(socket, projectPath) {
247
+ const previousProjectPath = socket.data.terminalProjectPath
248
+ if (previousProjectPath && previousProjectPath !== projectPath) {
249
+ socket.leave(projectRoomName(previousProjectPath))
250
+ }
251
+ socket.data.terminalProjectPath = projectPath
252
+ socket.join(projectRoomName(projectPath))
253
+ }
254
+
255
+ function listProjectTerminals(projectPath) {
256
+ return [...terminals.values()]
257
+ .filter((terminal) => terminal.projectPath === projectPath && terminal.status !== "closed")
258
+ .sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime())
259
+ .map(serializeTerminal)
260
+ }
261
+
262
+ function serializeTerminal(terminal) {
263
+ return {
264
+ cols: terminal.cols,
265
+ createdAt: terminal.createdAt.toISOString(),
266
+ exitCode: terminal.exitCode,
267
+ id: terminal.id,
268
+ projectPath: terminal.projectPath,
269
+ rows: terminal.rows,
270
+ shell: terminal.shell,
271
+ status: terminal.status,
272
+ title: terminal.title,
273
+ updatedAt: terminal.updatedAt.toISOString(),
274
+ }
275
+ }
276
+
277
+ function appendReplay(terminal, data) {
278
+ terminal.replay.push(data)
279
+ terminal.replayBytes += Buffer.byteLength(data, "utf8")
280
+ while (terminal.replayBytes > REPLAY_LIMIT_BYTES && terminal.replay.length > 1) {
281
+ const removed = terminal.replay.shift()
282
+ terminal.replayBytes -= Buffer.byteLength(removed, "utf8")
283
+ }
284
+ }
285
+
286
+ function extractTitle(terminal, data) {
287
+ terminal.titleBuffer = (terminal.titleBuffer + data).slice(-4096)
288
+ const matches = [
289
+ ...terminal.titleBuffer.matchAll(/\x1b\](?:0|1|2);([^\x07]*)\x07/g),
290
+ ...terminal.titleBuffer.matchAll(/\x1b\](?:0|1|2);([\s\S]*?)\x1b\\/g),
291
+ ]
292
+ const last = matches.at(-1)
293
+ return last ? normalizeTitle(last[1]) : null
294
+ }
295
+
296
+ function normalizeTitle(value) {
297
+ const title = value.replace(/[\x00-\x1f\x7f]/g, "").trim()
298
+ return title ? title.slice(0, 120) : null
299
+ }
300
+
301
+ function defaultTitle(projectPath) {
302
+ return basename(projectPath) || "Shell"
303
+ }
304
+
305
+ function defaultShell() {
306
+ if (process.platform === "win32") {
307
+ return { args: [], command: process.env.COMSPEC || "cmd.exe" }
308
+ }
309
+ return { args: [], command: process.env.SHELL || "/bin/bash" }
310
+ }
311
+
312
+ async function resolveProjectPath(resolveDirectory, payload) {
313
+ const rawProjectPath = readString(payload?.projectPath, "projectPath")
314
+ return resolveDirectory(rawProjectPath)
315
+ }
316
+
317
+ function resolveWorkspaceDirectory(inputPath, configuredRoot) {
318
+ const root = ensureWorkspaceRoot(configuredRoot)
319
+ const requested = inputPath.trim()
320
+ const unresolvedPath = requested
321
+ ? isAbsolute(requested)
322
+ ? requested
323
+ : join(root, requested)
324
+ : root
325
+ let path
326
+ try {
327
+ path = realpathSync(resolve(unresolvedPath))
328
+ } catch {
329
+ throw new Error("Workspace path does not exist.")
330
+ }
331
+ const rootRelativePath = relative(root, path)
332
+ if (rootRelativePath.startsWith("..") || isAbsolute(rootRelativePath)) {
333
+ throw new Error("Path is outside the workspace root.")
334
+ }
335
+ if (!statSync(path).isDirectory()) {
336
+ throw new Error("Workspace path is not a directory.")
337
+ }
338
+ return path
339
+ }
340
+
341
+ function ensureWorkspaceRoot(configuredRoot) {
342
+ const configured = resolveHomePath(configuredRoot?.trim() || process.env.CODEX_WORKSPACE_ROOT?.trim() || "~")
343
+ if (!existsSync(configured)) {
344
+ mkdirSync(configured, { recursive: true })
345
+ }
346
+ return realpathSync(configured)
347
+ }
348
+
349
+ function resolveHomePath(path) {
350
+ if (path === "~") {
351
+ return homedir()
352
+ }
353
+ if (path.startsWith("~/")) {
354
+ return join(homedir(), path.slice(2))
355
+ }
356
+ return resolve(path)
357
+ }
358
+
359
+ function requireTerminal(terminalId) {
360
+ const terminal = terminals.get(terminalId)
361
+ if (!terminal || terminal.status === "closed") {
362
+ throw new Error("Terminal not found.")
363
+ }
364
+ return terminal
365
+ }
366
+
367
+ function readTerminalId(payload) {
368
+ return readString(payload?.terminalId, "terminalId")
369
+ }
370
+
371
+ function readString(value, fieldName) {
372
+ if (typeof value !== "string" || !value.trim()) {
373
+ throw new Error(`${fieldName} is required.`)
374
+ }
375
+ return value
376
+ }
377
+
378
+ function normalizeDimension(value, fallback, min, max) {
379
+ const number = Number.parseInt(String(value ?? ""), 10)
380
+ if (!Number.isFinite(number)) {
381
+ return fallback
382
+ }
383
+ return Math.max(min, Math.min(max, number))
384
+ }
385
+
386
+ async function handleAck(ack, action) {
387
+ try {
388
+ const data = await action()
389
+ ack?.({ ok: true, ...data })
390
+ } catch (error) {
391
+ ack?.({
392
+ message: error instanceof Error ? error.message : "Terminal request failed.",
393
+ ok: false,
394
+ })
395
+ }
396
+ }
397
+
398
+ function projectRoomName(projectPath) {
399
+ return `terminal:project:${Buffer.from(projectPath, "utf8").toString("base64url")}`
400
+ }
401
+
402
+ function terminalRoomName(terminalId) {
403
+ return `terminal:${terminalId}`
404
+ }
405
+
406
+ function runningTerminalCount() {
407
+ return [...terminals.values()].filter((terminal) => terminal.status === "running").length
408
+ }
409
+
410
+ function emitCount(socket) {
411
+ socket.emit("terminal:count", { count: runningTerminalCount() })
412
+ }
413
+
414
+ function broadcastCount(io) {
415
+ io.emit("terminal:count", { count: runningTerminalCount() })
416
+ }
417
+
418
+ function broadcastProject(io, projectPath) {
419
+ for (const terminal of terminals.values()) {
420
+ if (terminal.projectPath === projectPath) {
421
+ terminal.io = io
422
+ }
423
+ }
424
+ io.to(projectRoomName(projectPath)).emit("terminal:project", {
425
+ projectPath,
426
+ terminals: listProjectTerminals(projectPath),
427
+ })
428
+ }
429
+
430
+ function broadcastProjectForTerminal(terminal) {
431
+ if (!terminal.io) {
432
+ return
433
+ }
434
+ broadcastProject(terminal.io, terminal.projectPath)
435
+ }
436
+
437
+ function installProcessExitHandler() {
438
+ if (exitHandlerInstalled) {
439
+ return
440
+ }
441
+ exitHandlerInstalled = true
442
+ process.once("exit", () => {
443
+ for (const terminal of terminals.values()) {
444
+ if (terminal.pty) {
445
+ try {
446
+ terminal.pty.kill()
447
+ } catch {
448
+ // Process exit is already in progress.
449
+ }
450
+ }
451
+ }
452
+ })
453
+ }
@@ -1 +0,0 @@
1
- import{j as t,t as r}from"./jsx-runtime-Dafdqr5g.js";import{t as s}from"./app-shell-ClOglt7r.js";var a=r(),o=t(function(){return(0,a.jsx)(s,{})});export{o as default};