codexapp 0.1.26 → 0.1.28
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-cli/index.js
CHANGED
|
@@ -2,30 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import { existsSync as existsSync2 } from "fs";
|
|
6
|
-
import { readFile as
|
|
7
|
-
import { homedir as homedir2 } from "os";
|
|
8
|
-
import { join as
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "fs";
|
|
6
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
7
|
+
import { homedir as homedir2, networkInterfaces } from "os";
|
|
8
|
+
import { join as join3 } from "path";
|
|
9
9
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
10
10
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
11
|
-
import { dirname as
|
|
11
|
+
import { dirname as dirname2 } from "path";
|
|
12
|
+
import { get as httpsGet } from "https";
|
|
12
13
|
import { Command } from "commander";
|
|
13
14
|
import qrcode from "qrcode-terminal";
|
|
14
15
|
|
|
15
16
|
// src/server/httpServer.ts
|
|
16
17
|
import { fileURLToPath } from "url";
|
|
17
|
-
import { dirname
|
|
18
|
+
import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
|
|
18
19
|
import { existsSync } from "fs";
|
|
19
|
-
import {
|
|
20
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
20
21
|
import express from "express";
|
|
21
22
|
|
|
22
23
|
// src/server/codexAppServerBridge.ts
|
|
23
24
|
import { spawn } from "child_process";
|
|
25
|
+
import { randomBytes } from "crypto";
|
|
24
26
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
|
|
25
27
|
import { request as httpsRequest } from "https";
|
|
26
28
|
import { homedir } from "os";
|
|
27
29
|
import { tmpdir } from "os";
|
|
28
|
-
import { isAbsolute, join, resolve } from "path";
|
|
30
|
+
import { basename, isAbsolute, join, resolve } from "path";
|
|
29
31
|
import { writeFile } from "fs/promises";
|
|
30
32
|
function asRecord(value) {
|
|
31
33
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -49,6 +51,46 @@ function setJson(res, statusCode, payload) {
|
|
|
49
51
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
50
52
|
res.end(JSON.stringify(payload));
|
|
51
53
|
}
|
|
54
|
+
function extractThreadMessageText(threadReadPayload) {
|
|
55
|
+
const payload = asRecord(threadReadPayload);
|
|
56
|
+
const thread = asRecord(payload?.thread);
|
|
57
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
58
|
+
const parts = [];
|
|
59
|
+
for (const turn of turns) {
|
|
60
|
+
const turnRecord = asRecord(turn);
|
|
61
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const itemRecord = asRecord(item);
|
|
64
|
+
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
65
|
+
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
66
|
+
parts.push(itemRecord.text.trim());
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (type === "userMessage") {
|
|
70
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
71
|
+
for (const block of content) {
|
|
72
|
+
const blockRecord = asRecord(block);
|
|
73
|
+
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
74
|
+
parts.push(blockRecord.text.trim());
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (type === "commandExecution") {
|
|
80
|
+
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
81
|
+
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
82
|
+
if (command) parts.push(command);
|
|
83
|
+
if (output) parts.push(output);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return parts.join("\n").trim();
|
|
88
|
+
}
|
|
89
|
+
function isExactPhraseMatch(query, doc) {
|
|
90
|
+
const q = query.trim().toLowerCase();
|
|
91
|
+
if (!q) return false;
|
|
92
|
+
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
93
|
+
}
|
|
52
94
|
function scoreFileCandidate(path, query) {
|
|
53
95
|
if (!query) return 0;
|
|
54
96
|
const lowerPath = path.toLowerCase();
|
|
@@ -122,6 +164,55 @@ async function runCommand(command, args, options = {}) {
|
|
|
122
164
|
});
|
|
123
165
|
});
|
|
124
166
|
}
|
|
167
|
+
function isMissingHeadError(error) {
|
|
168
|
+
const message = getErrorMessage(error, "").toLowerCase();
|
|
169
|
+
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head");
|
|
170
|
+
}
|
|
171
|
+
function isNotGitRepositoryError(error) {
|
|
172
|
+
const message = getErrorMessage(error, "").toLowerCase();
|
|
173
|
+
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
174
|
+
}
|
|
175
|
+
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
176
|
+
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
177
|
+
try {
|
|
178
|
+
await stat(agentsPath);
|
|
179
|
+
} catch {
|
|
180
|
+
await writeFile(agentsPath, "", "utf8");
|
|
181
|
+
}
|
|
182
|
+
await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
183
|
+
await runCommand(
|
|
184
|
+
"git",
|
|
185
|
+
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
186
|
+
{ cwd: repoRoot }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
async function runCommandCapture(command, args, options = {}) {
|
|
190
|
+
return await new Promise((resolveOutput, reject) => {
|
|
191
|
+
const proc = spawn(command, args, {
|
|
192
|
+
cwd: options.cwd,
|
|
193
|
+
env: process.env,
|
|
194
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
195
|
+
});
|
|
196
|
+
let stdout = "";
|
|
197
|
+
let stderr = "";
|
|
198
|
+
proc.stdout.on("data", (chunk) => {
|
|
199
|
+
stdout += chunk.toString();
|
|
200
|
+
});
|
|
201
|
+
proc.stderr.on("data", (chunk) => {
|
|
202
|
+
stderr += chunk.toString();
|
|
203
|
+
});
|
|
204
|
+
proc.on("error", reject);
|
|
205
|
+
proc.on("close", (code) => {
|
|
206
|
+
if (code === 0) {
|
|
207
|
+
resolveOutput(stdout.trim());
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
211
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
212
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
125
216
|
async function detectUserSkillsDir(appServer) {
|
|
126
217
|
try {
|
|
127
218
|
const result = await appServer.rpc("skills/list", {});
|
|
@@ -842,8 +933,82 @@ function getSharedBridgeState() {
|
|
|
842
933
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
843
934
|
return created;
|
|
844
935
|
}
|
|
936
|
+
async function loadAllThreadsForSearch(appServer) {
|
|
937
|
+
const threads = [];
|
|
938
|
+
let cursor = null;
|
|
939
|
+
do {
|
|
940
|
+
const response = asRecord(await appServer.rpc("thread/list", {
|
|
941
|
+
archived: false,
|
|
942
|
+
limit: 100,
|
|
943
|
+
sortKey: "updated_at",
|
|
944
|
+
cursor
|
|
945
|
+
}));
|
|
946
|
+
const data = Array.isArray(response?.data) ? response.data : [];
|
|
947
|
+
for (const row of data) {
|
|
948
|
+
const record = asRecord(row);
|
|
949
|
+
const id = typeof record?.id === "string" ? record.id : "";
|
|
950
|
+
if (!id) continue;
|
|
951
|
+
const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
|
|
952
|
+
const preview = typeof record?.preview === "string" ? record.preview : "";
|
|
953
|
+
threads.push({ id, title, preview });
|
|
954
|
+
}
|
|
955
|
+
cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
|
|
956
|
+
} while (cursor);
|
|
957
|
+
const docs = [];
|
|
958
|
+
const concurrency = 4;
|
|
959
|
+
for (let offset = 0; offset < threads.length; offset += concurrency) {
|
|
960
|
+
const batch = threads.slice(offset, offset + concurrency);
|
|
961
|
+
const loaded = await Promise.all(batch.map(async (thread) => {
|
|
962
|
+
try {
|
|
963
|
+
const readResponse = await appServer.rpc("thread/read", {
|
|
964
|
+
threadId: thread.id,
|
|
965
|
+
includeTurns: true
|
|
966
|
+
});
|
|
967
|
+
const messageText = extractThreadMessageText(readResponse);
|
|
968
|
+
const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
|
|
969
|
+
return {
|
|
970
|
+
id: thread.id,
|
|
971
|
+
title: thread.title,
|
|
972
|
+
preview: thread.preview,
|
|
973
|
+
messageText,
|
|
974
|
+
searchableText
|
|
975
|
+
};
|
|
976
|
+
} catch {
|
|
977
|
+
const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
|
|
978
|
+
return {
|
|
979
|
+
id: thread.id,
|
|
980
|
+
title: thread.title,
|
|
981
|
+
preview: thread.preview,
|
|
982
|
+
messageText: "",
|
|
983
|
+
searchableText
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
}));
|
|
987
|
+
docs.push(...loaded);
|
|
988
|
+
}
|
|
989
|
+
return docs;
|
|
990
|
+
}
|
|
991
|
+
async function buildThreadSearchIndex(appServer) {
|
|
992
|
+
const docs = await loadAllThreadsForSearch(appServer);
|
|
993
|
+
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
994
|
+
return { docsById };
|
|
995
|
+
}
|
|
845
996
|
function createCodexBridgeMiddleware() {
|
|
846
997
|
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
998
|
+
let threadSearchIndex = null;
|
|
999
|
+
let threadSearchIndexPromise = null;
|
|
1000
|
+
async function getThreadSearchIndex() {
|
|
1001
|
+
if (threadSearchIndex) return threadSearchIndex;
|
|
1002
|
+
if (!threadSearchIndexPromise) {
|
|
1003
|
+
threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
|
|
1004
|
+
threadSearchIndex = index;
|
|
1005
|
+
return index;
|
|
1006
|
+
}).finally(() => {
|
|
1007
|
+
threadSearchIndexPromise = null;
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
return threadSearchIndexPromise;
|
|
1011
|
+
}
|
|
847
1012
|
const middleware = async (req, res, next) => {
|
|
848
1013
|
try {
|
|
849
1014
|
if (!req.url) {
|
|
@@ -909,6 +1074,76 @@ function createCodexBridgeMiddleware() {
|
|
|
909
1074
|
setJson(res, 200, { data: { path: homedir() } });
|
|
910
1075
|
return;
|
|
911
1076
|
}
|
|
1077
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1078
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1079
|
+
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1080
|
+
if (!rawSourceCwd) {
|
|
1081
|
+
setJson(res, 400, { error: "Missing sourceCwd" });
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1085
|
+
try {
|
|
1086
|
+
const sourceInfo = await stat(sourceCwd);
|
|
1087
|
+
if (!sourceInfo.isDirectory()) {
|
|
1088
|
+
setJson(res, 400, { error: "sourceCwd is not a directory" });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
} catch {
|
|
1092
|
+
setJson(res, 404, { error: "sourceCwd does not exist" });
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
try {
|
|
1096
|
+
let gitRoot = "";
|
|
1097
|
+
try {
|
|
1098
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
if (!isNotGitRepositoryError(error)) throw error;
|
|
1101
|
+
await runCommand("git", ["init"], { cwd: sourceCwd });
|
|
1102
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1103
|
+
}
|
|
1104
|
+
const repoName = basename(gitRoot) || "repo";
|
|
1105
|
+
const worktreesRoot = join(getCodexHomeDir(), "worktrees");
|
|
1106
|
+
await mkdir(worktreesRoot, { recursive: true });
|
|
1107
|
+
let worktreeId = "";
|
|
1108
|
+
let worktreeParent = "";
|
|
1109
|
+
let worktreeCwd = "";
|
|
1110
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1111
|
+
const candidate = randomBytes(2).toString("hex");
|
|
1112
|
+
const parent = join(worktreesRoot, candidate);
|
|
1113
|
+
try {
|
|
1114
|
+
await stat(parent);
|
|
1115
|
+
continue;
|
|
1116
|
+
} catch {
|
|
1117
|
+
worktreeId = candidate;
|
|
1118
|
+
worktreeParent = parent;
|
|
1119
|
+
worktreeCwd = join(parent, repoName);
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (!worktreeId || !worktreeParent || !worktreeCwd) {
|
|
1124
|
+
throw new Error("Failed to allocate a unique worktree id");
|
|
1125
|
+
}
|
|
1126
|
+
const branch = `codex/${worktreeId}`;
|
|
1127
|
+
await mkdir(worktreeParent, { recursive: true });
|
|
1128
|
+
try {
|
|
1129
|
+
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
if (!isMissingHeadError(error)) throw error;
|
|
1132
|
+
await ensureRepoHasInitialCommit(gitRoot);
|
|
1133
|
+
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1134
|
+
}
|
|
1135
|
+
setJson(res, 200, {
|
|
1136
|
+
data: {
|
|
1137
|
+
cwd: worktreeCwd,
|
|
1138
|
+
branch,
|
|
1139
|
+
gitRoot
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
|
|
1144
|
+
}
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
912
1147
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
913
1148
|
const payload = await readJsonBody(req);
|
|
914
1149
|
const record = asRecord(payload);
|
|
@@ -1034,6 +1269,20 @@ function createCodexBridgeMiddleware() {
|
|
|
1034
1269
|
setJson(res, 200, { data: cache });
|
|
1035
1270
|
return;
|
|
1036
1271
|
}
|
|
1272
|
+
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1273
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1274
|
+
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1275
|
+
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1276
|
+
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1277
|
+
if (!query) {
|
|
1278
|
+
setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const index = await getThreadSearchIndex();
|
|
1282
|
+
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1283
|
+
setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1037
1286
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1038
1287
|
const payload = asRecord(await readJsonBody(req));
|
|
1039
1288
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
@@ -1197,6 +1446,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1197
1446
|
}
|
|
1198
1447
|
};
|
|
1199
1448
|
middleware.dispose = () => {
|
|
1449
|
+
threadSearchIndex = null;
|
|
1200
1450
|
appServer.dispose();
|
|
1201
1451
|
};
|
|
1202
1452
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -1211,7 +1461,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1211
1461
|
}
|
|
1212
1462
|
|
|
1213
1463
|
// src/server/authMiddleware.ts
|
|
1214
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1464
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1215
1465
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1216
1466
|
function constantTimeCompare(a, b) {
|
|
1217
1467
|
const bufA = Buffer.from(a);
|
|
@@ -1309,7 +1559,7 @@ function createAuthSession(password) {
|
|
|
1309
1559
|
res.status(401).json({ error: "Invalid password" });
|
|
1310
1560
|
return;
|
|
1311
1561
|
}
|
|
1312
|
-
const token =
|
|
1562
|
+
const token = randomBytes2(32).toString("hex");
|
|
1313
1563
|
validTokens.add(token);
|
|
1314
1564
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
1315
1565
|
res.json({ ok: true });
|
|
@@ -1328,74 +1578,32 @@ function createAuthSession(password) {
|
|
|
1328
1578
|
};
|
|
1329
1579
|
}
|
|
1330
1580
|
|
|
1331
|
-
// src/server/
|
|
1332
|
-
import {
|
|
1333
|
-
|
|
1334
|
-
var
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
".
|
|
1338
|
-
".
|
|
1339
|
-
".
|
|
1340
|
-
".
|
|
1341
|
-
".
|
|
1342
|
-
".
|
|
1343
|
-
".
|
|
1344
|
-
".
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
"
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
".ini",
|
|
1356
|
-
".conf",
|
|
1357
|
-
".sql"
|
|
1358
|
-
]);
|
|
1359
|
-
function languageForPath(pathValue) {
|
|
1360
|
-
const extension = extname(pathValue).toLowerCase();
|
|
1361
|
-
switch (extension) {
|
|
1362
|
-
case ".js":
|
|
1363
|
-
return "javascript";
|
|
1364
|
-
case ".ts":
|
|
1365
|
-
return "typescript";
|
|
1366
|
-
case ".jsx":
|
|
1367
|
-
return "javascript";
|
|
1368
|
-
case ".tsx":
|
|
1369
|
-
return "typescript";
|
|
1370
|
-
case ".py":
|
|
1371
|
-
return "python";
|
|
1372
|
-
case ".sh":
|
|
1373
|
-
return "sh";
|
|
1374
|
-
case ".css":
|
|
1375
|
-
case ".scss":
|
|
1376
|
-
return "css";
|
|
1377
|
-
case ".html":
|
|
1378
|
-
case ".htm":
|
|
1379
|
-
return "html";
|
|
1380
|
-
case ".json":
|
|
1381
|
-
return "json";
|
|
1382
|
-
case ".md":
|
|
1383
|
-
return "markdown";
|
|
1384
|
-
case ".yaml":
|
|
1385
|
-
case ".yml":
|
|
1386
|
-
return "yaml";
|
|
1387
|
-
case ".xml":
|
|
1388
|
-
return "xml";
|
|
1389
|
-
case ".sql":
|
|
1390
|
-
return "sql";
|
|
1391
|
-
case ".toml":
|
|
1392
|
-
return "ini";
|
|
1393
|
-
case ".ini":
|
|
1394
|
-
case ".conf":
|
|
1395
|
-
return "ini";
|
|
1396
|
-
default:
|
|
1397
|
-
return "plaintext";
|
|
1581
|
+
// src/server/httpServer.ts
|
|
1582
|
+
import { WebSocketServer } from "ws";
|
|
1583
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1584
|
+
var distDir = join2(__dirname, "..", "dist");
|
|
1585
|
+
var spaEntryFile = join2(distDir, "index.html");
|
|
1586
|
+
var IMAGE_CONTENT_TYPES = {
|
|
1587
|
+
".avif": "image/avif",
|
|
1588
|
+
".bmp": "image/bmp",
|
|
1589
|
+
".gif": "image/gif",
|
|
1590
|
+
".jpeg": "image/jpeg",
|
|
1591
|
+
".jpg": "image/jpeg",
|
|
1592
|
+
".png": "image/png",
|
|
1593
|
+
".svg": "image/svg+xml",
|
|
1594
|
+
".webp": "image/webp"
|
|
1595
|
+
};
|
|
1596
|
+
function normalizeLocalImagePath(rawPath) {
|
|
1597
|
+
const trimmed = rawPath.trim();
|
|
1598
|
+
if (!trimmed) return "";
|
|
1599
|
+
if (trimmed.startsWith("file://")) {
|
|
1600
|
+
try {
|
|
1601
|
+
return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
|
|
1602
|
+
} catch {
|
|
1603
|
+
return trimmed.replace(/^file:\/\//u, "");
|
|
1604
|
+
}
|
|
1398
1605
|
}
|
|
1606
|
+
return trimmed;
|
|
1399
1607
|
}
|
|
1400
1608
|
function normalizeLocalPath(rawPath) {
|
|
1401
1609
|
const trimmed = rawPath.trim();
|
|
@@ -1417,72 +1625,39 @@ function decodeBrowsePath(rawPath) {
|
|
|
1417
1625
|
return rawPath;
|
|
1418
1626
|
}
|
|
1419
1627
|
}
|
|
1420
|
-
function isTextEditablePath(pathValue) {
|
|
1421
|
-
return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
|
|
1422
|
-
}
|
|
1423
1628
|
function escapeHtml(value) {
|
|
1424
1629
|
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
1425
1630
|
}
|
|
1426
1631
|
function toBrowseHref(pathValue) {
|
|
1427
1632
|
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
1428
1633
|
}
|
|
1429
|
-
function
|
|
1430
|
-
return `/codex-local-edit${encodeURI(pathValue)}`;
|
|
1431
|
-
}
|
|
1432
|
-
function escapeForInlineScriptString(value) {
|
|
1433
|
-
return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
|
|
1434
|
-
}
|
|
1435
|
-
async function getDirectoryItems(localPath) {
|
|
1634
|
+
async function renderDirectoryListing(res, localPath) {
|
|
1436
1635
|
const entries = await readdir2(localPath, { withFileTypes: true });
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
return {
|
|
1441
|
-
name: entry.name,
|
|
1442
|
-
path: entryPath,
|
|
1443
|
-
isDirectory: entry.isDirectory(),
|
|
1444
|
-
editable: !entry.isDirectory() && isTextEditablePath(entryPath),
|
|
1445
|
-
mtimeMs: entryStat.mtimeMs
|
|
1446
|
-
};
|
|
1447
|
-
}));
|
|
1448
|
-
return withMeta.sort((a, b) => {
|
|
1449
|
-
if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
|
|
1450
|
-
if (a.isDirectory && !b.isDirectory) return -1;
|
|
1451
|
-
if (!a.isDirectory && b.isDirectory) return 1;
|
|
1636
|
+
const sorted = entries.slice().sort((a, b) => {
|
|
1637
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
1638
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
1452
1639
|
return a.name.localeCompare(b.name);
|
|
1453
1640
|
});
|
|
1454
|
-
}
|
|
1455
|
-
async function createDirectoryListingHtml(localPath) {
|
|
1456
|
-
const items = await getDirectoryItems(localPath);
|
|
1457
1641
|
const parentPath = dirname(localPath);
|
|
1458
|
-
const rows =
|
|
1459
|
-
const
|
|
1460
|
-
const
|
|
1461
|
-
return `<li
|
|
1642
|
+
const rows = sorted.map((entry) => {
|
|
1643
|
+
const entryPath = join2(localPath, entry.name);
|
|
1644
|
+
const suffix = entry.isDirectory() ? "/" : "";
|
|
1645
|
+
return `<li><a href="${escapeHtml(toBrowseHref(entryPath))}">${escapeHtml(entry.name)}${suffix}</a></li>`;
|
|
1462
1646
|
}).join("\n");
|
|
1463
1647
|
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
1464
|
-
|
|
1648
|
+
const html = `<!doctype html>
|
|
1465
1649
|
<html lang="en">
|
|
1466
1650
|
<head>
|
|
1467
1651
|
<meta charset="utf-8" />
|
|
1468
1652
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1469
1653
|
<title>Index of ${escapeHtml(localPath)}</title>
|
|
1470
1654
|
<style>
|
|
1471
|
-
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin:
|
|
1655
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 24px; background: #0b1020; color: #dbe6ff; }
|
|
1472
1656
|
a { color: #8cc2ff; text-decoration: none; }
|
|
1473
1657
|
a:hover { text-decoration: underline; }
|
|
1474
|
-
ul { list-style: none; padding: 0; margin: 12px 0 0;
|
|
1475
|
-
|
|
1476
|
-
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
1477
|
-
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; text-decoration: none; }
|
|
1478
|
-
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
1658
|
+
ul { list-style: none; padding: 0; margin: 12px 0 0; }
|
|
1659
|
+
li { padding: 3px 0; }
|
|
1479
1660
|
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
1480
|
-
@media (max-width: 640px) {
|
|
1481
|
-
body { margin: 12px; }
|
|
1482
|
-
.file-row { gap: 8px; }
|
|
1483
|
-
.file-link { font-size: 15px; padding: 12px; }
|
|
1484
|
-
.icon-btn { width: 44px; height: 44px; }
|
|
1485
|
-
}
|
|
1486
1661
|
</style>
|
|
1487
1662
|
</head>
|
|
1488
1663
|
<body>
|
|
@@ -1491,107 +1666,7 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
1491
1666
|
<ul>${rows}</ul>
|
|
1492
1667
|
</body>
|
|
1493
1668
|
</html>`;
|
|
1494
|
-
|
|
1495
|
-
async function createTextEditorHtml(localPath) {
|
|
1496
|
-
const content = await readFile2(localPath, "utf8");
|
|
1497
|
-
const parentPath = dirname(localPath);
|
|
1498
|
-
const language = languageForPath(localPath);
|
|
1499
|
-
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
1500
|
-
return `<!doctype html>
|
|
1501
|
-
<html lang="en">
|
|
1502
|
-
<head>
|
|
1503
|
-
<meta charset="utf-8" />
|
|
1504
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1505
|
-
<title>Edit ${escapeHtml(localPath)}</title>
|
|
1506
|
-
<style>
|
|
1507
|
-
html, body { width: 100%; height: 100%; margin: 0; }
|
|
1508
|
-
body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
|
|
1509
|
-
.toolbar { position: sticky; top: 0; z-index: 10; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; background: #0b1020; border-bottom: 1px solid #243a5a; }
|
|
1510
|
-
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
1511
|
-
button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
|
|
1512
|
-
button:hover, a:hover { filter: brightness(1.08); }
|
|
1513
|
-
#editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
|
|
1514
|
-
#status { margin-left: 8px; color: #8cc2ff; }
|
|
1515
|
-
.ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
|
|
1516
|
-
.ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
|
|
1517
|
-
.ace_marker-layer .ace_active-line { background: #10213c !important; }
|
|
1518
|
-
.ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
|
|
1519
|
-
.meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
|
|
1520
|
-
</style>
|
|
1521
|
-
</head>
|
|
1522
|
-
<body>
|
|
1523
|
-
<div class="toolbar">
|
|
1524
|
-
<div class="row">
|
|
1525
|
-
<a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
|
|
1526
|
-
<button id="saveBtn" type="button">Save</button>
|
|
1527
|
-
<span id="status"></span>
|
|
1528
|
-
</div>
|
|
1529
|
-
<div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
|
|
1530
|
-
</div>
|
|
1531
|
-
<div id="editor"></div>
|
|
1532
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
|
|
1533
|
-
<script>
|
|
1534
|
-
const saveBtn = document.getElementById('saveBtn');
|
|
1535
|
-
const status = document.getElementById('status');
|
|
1536
|
-
const editor = ace.edit('editor');
|
|
1537
|
-
editor.setTheme('ace/theme/tomorrow_night');
|
|
1538
|
-
editor.session.setMode('ace/mode/${escapeHtml(language)}');
|
|
1539
|
-
editor.setValue(${safeContentLiteral}, -1);
|
|
1540
|
-
editor.setOptions({
|
|
1541
|
-
fontSize: '13px',
|
|
1542
|
-
wrap: true,
|
|
1543
|
-
showPrintMargin: false,
|
|
1544
|
-
useSoftTabs: true,
|
|
1545
|
-
tabSize: 2,
|
|
1546
|
-
behavioursEnabled: true,
|
|
1547
|
-
});
|
|
1548
|
-
editor.resize();
|
|
1549
|
-
|
|
1550
|
-
saveBtn.addEventListener('click', async () => {
|
|
1551
|
-
status.textContent = 'Saving...';
|
|
1552
|
-
const response = await fetch(location.pathname, {
|
|
1553
|
-
method: 'PUT',
|
|
1554
|
-
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
1555
|
-
body: editor.getValue(),
|
|
1556
|
-
});
|
|
1557
|
-
status.textContent = response.ok ? 'Saved' : 'Save failed';
|
|
1558
|
-
});
|
|
1559
|
-
</script>
|
|
1560
|
-
</body>
|
|
1561
|
-
</html>`;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
// src/server/httpServer.ts
|
|
1565
|
-
import { WebSocketServer } from "ws";
|
|
1566
|
-
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1567
|
-
var distDir = join3(__dirname, "..", "dist");
|
|
1568
|
-
var spaEntryFile = join3(distDir, "index.html");
|
|
1569
|
-
var IMAGE_CONTENT_TYPES = {
|
|
1570
|
-
".avif": "image/avif",
|
|
1571
|
-
".bmp": "image/bmp",
|
|
1572
|
-
".gif": "image/gif",
|
|
1573
|
-
".jpeg": "image/jpeg",
|
|
1574
|
-
".jpg": "image/jpeg",
|
|
1575
|
-
".png": "image/png",
|
|
1576
|
-
".svg": "image/svg+xml",
|
|
1577
|
-
".webp": "image/webp"
|
|
1578
|
-
};
|
|
1579
|
-
function normalizeLocalImagePath(rawPath) {
|
|
1580
|
-
const trimmed = rawPath.trim();
|
|
1581
|
-
if (!trimmed) return "";
|
|
1582
|
-
if (trimmed.startsWith("file://")) {
|
|
1583
|
-
try {
|
|
1584
|
-
return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
|
|
1585
|
-
} catch {
|
|
1586
|
-
return trimmed.replace(/^file:\/\//u, "");
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
return trimmed;
|
|
1590
|
-
}
|
|
1591
|
-
function readWildcardPathParam(value) {
|
|
1592
|
-
if (typeof value === "string") return value;
|
|
1593
|
-
if (Array.isArray(value)) return value.join("/");
|
|
1594
|
-
return "";
|
|
1669
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1595
1670
|
}
|
|
1596
1671
|
function createServer(options = {}) {
|
|
1597
1672
|
const app = express();
|
|
@@ -1608,7 +1683,7 @@ function createServer(options = {}) {
|
|
|
1608
1683
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1609
1684
|
return;
|
|
1610
1685
|
}
|
|
1611
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
1686
|
+
const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
|
|
1612
1687
|
if (!contentType) {
|
|
1613
1688
|
res.status(415).json({ error: "Unsupported image type." });
|
|
1614
1689
|
return;
|
|
@@ -1635,18 +1710,17 @@ function createServer(options = {}) {
|
|
|
1635
1710
|
});
|
|
1636
1711
|
});
|
|
1637
1712
|
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
1638
|
-
const rawPath =
|
|
1713
|
+
const rawPath = typeof req.params.path === "string" ? req.params.path : "";
|
|
1639
1714
|
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1640
1715
|
if (!localPath || !isAbsolute2(localPath)) {
|
|
1641
1716
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1642
1717
|
return;
|
|
1643
1718
|
}
|
|
1644
1719
|
try {
|
|
1645
|
-
const fileStat = await
|
|
1720
|
+
const fileStat = await stat2(localPath);
|
|
1646
1721
|
res.setHeader("Cache-Control", "private, no-store");
|
|
1647
1722
|
if (fileStat.isDirectory()) {
|
|
1648
|
-
|
|
1649
|
-
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1723
|
+
await renderDirectoryListing(res, localPath);
|
|
1650
1724
|
return;
|
|
1651
1725
|
}
|
|
1652
1726
|
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
@@ -1657,44 +1731,6 @@ function createServer(options = {}) {
|
|
|
1657
1731
|
res.status(404).json({ error: "File not found." });
|
|
1658
1732
|
}
|
|
1659
1733
|
});
|
|
1660
|
-
app.get("/codex-local-edit/*path", async (req, res) => {
|
|
1661
|
-
const rawPath = readWildcardPathParam(req.params.path);
|
|
1662
|
-
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1663
|
-
if (!localPath || !isAbsolute2(localPath)) {
|
|
1664
|
-
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1665
|
-
return;
|
|
1666
|
-
}
|
|
1667
|
-
try {
|
|
1668
|
-
const fileStat = await stat3(localPath);
|
|
1669
|
-
if (!fileStat.isFile()) {
|
|
1670
|
-
res.status(400).json({ error: "Expected file path." });
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
const html = await createTextEditorHtml(localPath);
|
|
1674
|
-
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1675
|
-
} catch {
|
|
1676
|
-
res.status(404).json({ error: "File not found." });
|
|
1677
|
-
}
|
|
1678
|
-
});
|
|
1679
|
-
app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
|
|
1680
|
-
const rawPath = readWildcardPathParam(req.params.path);
|
|
1681
|
-
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1682
|
-
if (!localPath || !isAbsolute2(localPath)) {
|
|
1683
|
-
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1684
|
-
return;
|
|
1685
|
-
}
|
|
1686
|
-
if (!isTextEditablePath(localPath)) {
|
|
1687
|
-
res.status(415).json({ error: "Only text-like files are editable." });
|
|
1688
|
-
return;
|
|
1689
|
-
}
|
|
1690
|
-
const body = typeof req.body === "string" ? req.body : "";
|
|
1691
|
-
try {
|
|
1692
|
-
await writeFile2(localPath, body, "utf8");
|
|
1693
|
-
res.status(200).json({ ok: true });
|
|
1694
|
-
} catch {
|
|
1695
|
-
res.status(404).json({ error: "File not found." });
|
|
1696
|
-
}
|
|
1697
|
-
});
|
|
1698
1734
|
const hasFrontendAssets = existsSync(spaEntryFile);
|
|
1699
1735
|
if (hasFrontendAssets) {
|
|
1700
1736
|
app.use(express.static(distDir));
|
|
@@ -1766,11 +1802,11 @@ function generatePassword() {
|
|
|
1766
1802
|
|
|
1767
1803
|
// src/cli/index.ts
|
|
1768
1804
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
1769
|
-
var __dirname2 =
|
|
1805
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
1770
1806
|
async function readCliVersion() {
|
|
1771
1807
|
try {
|
|
1772
|
-
const packageJsonPath =
|
|
1773
|
-
const raw = await
|
|
1808
|
+
const packageJsonPath = join3(__dirname2, "..", "package.json");
|
|
1809
|
+
const raw = await readFile2(packageJsonPath, "utf8");
|
|
1774
1810
|
const parsed = JSON.parse(raw);
|
|
1775
1811
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
1776
1812
|
} catch {
|
|
@@ -1795,13 +1831,13 @@ function runWithStatus(command, args) {
|
|
|
1795
1831
|
return result.status ?? -1;
|
|
1796
1832
|
}
|
|
1797
1833
|
function getUserNpmPrefix() {
|
|
1798
|
-
return
|
|
1834
|
+
return join3(homedir2(), ".npm-global");
|
|
1799
1835
|
}
|
|
1800
1836
|
function resolveCodexCommand() {
|
|
1801
1837
|
if (canRun("codex", ["--version"])) {
|
|
1802
1838
|
return "codex";
|
|
1803
1839
|
}
|
|
1804
|
-
const userCandidate =
|
|
1840
|
+
const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
|
|
1805
1841
|
if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
1806
1842
|
return userCandidate;
|
|
1807
1843
|
}
|
|
@@ -1809,15 +1845,88 @@ function resolveCodexCommand() {
|
|
|
1809
1845
|
if (!prefix) {
|
|
1810
1846
|
return null;
|
|
1811
1847
|
}
|
|
1812
|
-
const candidate =
|
|
1848
|
+
const candidate = join3(prefix, "bin", "codex");
|
|
1813
1849
|
if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
|
|
1814
1850
|
return candidate;
|
|
1815
1851
|
}
|
|
1816
1852
|
return null;
|
|
1817
1853
|
}
|
|
1854
|
+
function resolveCloudflaredCommand() {
|
|
1855
|
+
if (canRun("cloudflared", ["--version"])) {
|
|
1856
|
+
return "cloudflared";
|
|
1857
|
+
}
|
|
1858
|
+
const localCandidate = join3(homedir2(), ".local", "bin", "cloudflared");
|
|
1859
|
+
if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
1860
|
+
return localCandidate;
|
|
1861
|
+
}
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1864
|
+
function mapCloudflaredLinuxArch(arch) {
|
|
1865
|
+
if (arch === "x64") {
|
|
1866
|
+
return "amd64";
|
|
1867
|
+
}
|
|
1868
|
+
if (arch === "arm64") {
|
|
1869
|
+
return "arm64";
|
|
1870
|
+
}
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
function downloadFile(url, destination) {
|
|
1874
|
+
return new Promise((resolve2, reject) => {
|
|
1875
|
+
const request = (currentUrl) => {
|
|
1876
|
+
httpsGet(currentUrl, (response) => {
|
|
1877
|
+
const code = response.statusCode ?? 0;
|
|
1878
|
+
if (code >= 300 && code < 400 && response.headers.location) {
|
|
1879
|
+
response.resume();
|
|
1880
|
+
request(response.headers.location);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
if (code !== 200) {
|
|
1884
|
+
response.resume();
|
|
1885
|
+
reject(new Error(`Download failed with HTTP status ${String(code)}`));
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const file = createWriteStream(destination, { mode: 493 });
|
|
1889
|
+
response.pipe(file);
|
|
1890
|
+
file.on("finish", () => {
|
|
1891
|
+
file.close();
|
|
1892
|
+
resolve2();
|
|
1893
|
+
});
|
|
1894
|
+
file.on("error", reject);
|
|
1895
|
+
}).on("error", reject);
|
|
1896
|
+
};
|
|
1897
|
+
request(url);
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
async function ensureCloudflaredInstalledLinux() {
|
|
1901
|
+
const current = resolveCloudflaredCommand();
|
|
1902
|
+
if (current) {
|
|
1903
|
+
return current;
|
|
1904
|
+
}
|
|
1905
|
+
if (process.platform !== "linux") {
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
const mappedArch = mapCloudflaredLinuxArch(process.arch);
|
|
1909
|
+
if (!mappedArch) {
|
|
1910
|
+
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
1911
|
+
}
|
|
1912
|
+
const userBinDir = join3(homedir2(), ".local", "bin");
|
|
1913
|
+
mkdirSync(userBinDir, { recursive: true });
|
|
1914
|
+
const destination = join3(userBinDir, "cloudflared");
|
|
1915
|
+
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
1916
|
+
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
1917
|
+
await downloadFile(downloadUrl, destination);
|
|
1918
|
+
chmodSync(destination, 493);
|
|
1919
|
+
process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
|
|
1920
|
+
const installed = resolveCloudflaredCommand();
|
|
1921
|
+
if (!installed) {
|
|
1922
|
+
throw new Error("cloudflared download completed but executable is still not available");
|
|
1923
|
+
}
|
|
1924
|
+
console.log("\ncloudflared installed.\n");
|
|
1925
|
+
return installed;
|
|
1926
|
+
}
|
|
1818
1927
|
function hasCodexAuth() {
|
|
1819
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
1820
|
-
return existsSync2(
|
|
1928
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
|
|
1929
|
+
return existsSync2(join3(codexHome, "auth.json"));
|
|
1821
1930
|
}
|
|
1822
1931
|
function ensureCodexInstalled() {
|
|
1823
1932
|
let codexCommand = resolveCodexCommand();
|
|
@@ -1835,7 +1944,7 @@ function ensureCodexInstalled() {
|
|
|
1835
1944
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
1836
1945
|
`);
|
|
1837
1946
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
1838
|
-
process.env.PATH = `${
|
|
1947
|
+
process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
1839
1948
|
};
|
|
1840
1949
|
if (isTermuxRuntime()) {
|
|
1841
1950
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -1896,9 +2005,27 @@ function parseCloudflaredUrl(chunk) {
|
|
|
1896
2005
|
}
|
|
1897
2006
|
return urlMatch[urlMatch.length - 1] ?? null;
|
|
1898
2007
|
}
|
|
1899
|
-
|
|
2008
|
+
function getAccessibleUrls(port) {
|
|
2009
|
+
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
2010
|
+
const interfaces = networkInterfaces();
|
|
2011
|
+
for (const entries of Object.values(interfaces)) {
|
|
2012
|
+
if (!entries) {
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
for (const entry of entries) {
|
|
2016
|
+
if (entry.internal) {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
if (entry.family === "IPv4") {
|
|
2020
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return Array.from(urls);
|
|
2025
|
+
}
|
|
2026
|
+
async function startCloudflaredTunnel(command, localPort) {
|
|
1900
2027
|
return new Promise((resolve2, reject) => {
|
|
1901
|
-
const child = spawn2(
|
|
2028
|
+
const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
1902
2029
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1903
2030
|
});
|
|
1904
2031
|
const timeout = setTimeout(() => {
|
|
@@ -1949,7 +2076,7 @@ function listenWithFallback(server, startPort) {
|
|
|
1949
2076
|
};
|
|
1950
2077
|
server.once("error", onError);
|
|
1951
2078
|
server.once("listening", onListening);
|
|
1952
|
-
server.listen(port);
|
|
2079
|
+
server.listen(port, "0.0.0.0");
|
|
1953
2080
|
};
|
|
1954
2081
|
attempt(startPort);
|
|
1955
2082
|
});
|
|
@@ -1971,7 +2098,8 @@ async function startServer(options) {
|
|
|
1971
2098
|
let tunnelUrl = null;
|
|
1972
2099
|
if (options.tunnel) {
|
|
1973
2100
|
try {
|
|
1974
|
-
const
|
|
2101
|
+
const cloudflaredCommand = await ensureCloudflaredInstalledLinux() ?? "cloudflared";
|
|
2102
|
+
const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
|
|
1975
2103
|
tunnelChild = tunnel.process;
|
|
1976
2104
|
tunnelUrl = tunnel.url;
|
|
1977
2105
|
} catch (error) {
|
|
@@ -1986,8 +2114,15 @@ async function startServer(options) {
|
|
|
1986
2114
|
` Version: ${version}`,
|
|
1987
2115
|
" GitHub: https://github.com/friuns2/codexui",
|
|
1988
2116
|
"",
|
|
1989
|
-
`
|
|
2117
|
+
` Bind: http://0.0.0.0:${String(port)}`
|
|
1990
2118
|
];
|
|
2119
|
+
const accessUrls = getAccessibleUrls(port);
|
|
2120
|
+
if (accessUrls.length > 0) {
|
|
2121
|
+
lines.push(` Local: ${accessUrls[0]}`);
|
|
2122
|
+
for (const accessUrl of accessUrls.slice(1)) {
|
|
2123
|
+
lines.push(` Network: ${accessUrl}`);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1991
2126
|
if (port !== requestedPort) {
|
|
1992
2127
|
lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
|
|
1993
2128
|
}
|
|
@@ -1996,9 +2131,7 @@ async function startServer(options) {
|
|
|
1996
2131
|
}
|
|
1997
2132
|
if (tunnelUrl) {
|
|
1998
2133
|
lines.push(` Tunnel: ${tunnelUrl}`);
|
|
1999
|
-
lines.push("");
|
|
2000
|
-
lines.push(" Tunnel QR code:");
|
|
2001
|
-
lines.push(` URL: ${tunnelUrl}`);
|
|
2134
|
+
lines.push(" Tunnel QR code below");
|
|
2002
2135
|
}
|
|
2003
2136
|
printTermuxKeepAlive(lines);
|
|
2004
2137
|
lines.push("");
|