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.
- package/README.md +2 -2
- package/build/client/assets/__vite-browser-external-2447137e-Bb2UaVfr.js +0 -0
- package/build/client/assets/app-layout-DVRPLoJp.js +1 -0
- package/build/client/assets/app-shell-DnEsSg3r.js +2 -0
- package/build/client/assets/chat-CAKcWygY.js +1 -0
- package/build/client/assets/connect-BDvoix-O.js +1 -0
- package/build/client/assets/{document-title-B6Yxyaur.js → document-title-RYIi5OZD.js} +1 -1
- package/build/client/assets/{entry.client-BlQXdBHx.js → entry.client-BQZeV9JD.js} +1 -1
- package/build/client/assets/ghostty-web-CgkkAFeT.js +1 -0
- package/build/client/assets/home-BjcHgWsO.js +1 -0
- package/build/client/assets/{jsx-runtime-Dafdqr5g.js → jsx-runtime-Cal6WBdn.js} +1 -1
- package/build/client/assets/label-BpA-r6tL.js +1 -0
- package/build/client/assets/{manifest-f96902fd.js → manifest-40da439f.js} +1 -1
- package/build/client/assets/{react-dom-CMuFyqmq.js → react-dom-D4MOkHq2.js} +1 -1
- package/build/client/assets/root-COrr2Ht-.css +2 -0
- package/build/client/assets/{root-bXu6rtsG.js → root-D-p7jx9T.js} +1 -1
- package/build/client/assets/{session-provider-2Ozr-CuV.js → session-provider-QLGSjKmA.js} +1 -1
- package/build/server/index.js +1 -1
- package/package.json +4 -1
- package/prisma/schema.prisma +4 -0
- package/server/index.mjs +3 -0
- package/server/sqlite-setup.mjs +22 -0
- package/server/terminal-socket.mjs +453 -0
- package/build/client/assets/app-layout--vTe8I2j.js +0 -1
- package/build/client/assets/app-shell-ClOglt7r.js +0 -1
- package/build/client/assets/chat-Bf8hrS42.js +0 -1
- package/build/client/assets/connect-CHSFTXwk.js +0 -1
- package/build/client/assets/home-BLKISIGJ.js +0 -1
- package/build/client/assets/label-C7i5Qt-O.js +0 -1
- 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.
|
|
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",
|
package/prisma/schema.prisma
CHANGED
|
@@ -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)
|
package/server/sqlite-setup.mjs
CHANGED
|
@@ -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};
|