codexapp 0.1.25 → 0.1.26
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,32 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import {
|
|
6
|
-
import { readFile as
|
|
5
|
+
import { existsSync as existsSync2 } from "fs";
|
|
6
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
7
7
|
import { homedir as homedir2 } from "os";
|
|
8
|
-
import { join as
|
|
8
|
+
import { join as join4 } from "path";
|
|
9
9
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
10
10
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
11
|
-
import { dirname as
|
|
12
|
-
import { get as httpsGet } from "https";
|
|
11
|
+
import { dirname as dirname3 } from "path";
|
|
13
12
|
import { Command } from "commander";
|
|
14
13
|
import qrcode from "qrcode-terminal";
|
|
15
14
|
|
|
16
15
|
// src/server/httpServer.ts
|
|
17
16
|
import { fileURLToPath } from "url";
|
|
18
|
-
import { dirname, extname, isAbsolute as isAbsolute2, join as
|
|
17
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
19
18
|
import { existsSync } from "fs";
|
|
20
|
-
import {
|
|
19
|
+
import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
|
|
21
20
|
import express from "express";
|
|
22
21
|
|
|
23
22
|
// src/server/codexAppServerBridge.ts
|
|
24
23
|
import { spawn } from "child_process";
|
|
25
|
-
import { randomBytes } from "crypto";
|
|
26
24
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
|
|
27
25
|
import { request as httpsRequest } from "https";
|
|
28
26
|
import { homedir } from "os";
|
|
29
27
|
import { tmpdir } from "os";
|
|
30
|
-
import {
|
|
28
|
+
import { isAbsolute, join, resolve } from "path";
|
|
31
29
|
import { writeFile } from "fs/promises";
|
|
32
30
|
function asRecord(value) {
|
|
33
31
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -51,46 +49,6 @@ function setJson(res, statusCode, payload) {
|
|
|
51
49
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
52
50
|
res.end(JSON.stringify(payload));
|
|
53
51
|
}
|
|
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
|
-
}
|
|
94
52
|
function scoreFileCandidate(path, query) {
|
|
95
53
|
if (!query) return 0;
|
|
96
54
|
const lowerPath = path.toLowerCase();
|
|
@@ -164,55 +122,6 @@ async function runCommand(command, args, options = {}) {
|
|
|
164
122
|
});
|
|
165
123
|
});
|
|
166
124
|
}
|
|
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
|
-
}
|
|
216
125
|
async function detectUserSkillsDir(appServer) {
|
|
217
126
|
try {
|
|
218
127
|
const result = await appServer.rpc("skills/list", {});
|
|
@@ -933,82 +842,8 @@ function getSharedBridgeState() {
|
|
|
933
842
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
934
843
|
return created;
|
|
935
844
|
}
|
|
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
|
-
}
|
|
996
845
|
function createCodexBridgeMiddleware() {
|
|
997
846
|
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
|
-
}
|
|
1012
847
|
const middleware = async (req, res, next) => {
|
|
1013
848
|
try {
|
|
1014
849
|
if (!req.url) {
|
|
@@ -1074,76 +909,6 @@ function createCodexBridgeMiddleware() {
|
|
|
1074
909
|
setJson(res, 200, { data: { path: homedir() } });
|
|
1075
910
|
return;
|
|
1076
911
|
}
|
|
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
|
-
}
|
|
1147
912
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1148
913
|
const payload = await readJsonBody(req);
|
|
1149
914
|
const record = asRecord(payload);
|
|
@@ -1269,20 +1034,6 @@ function createCodexBridgeMiddleware() {
|
|
|
1269
1034
|
setJson(res, 200, { data: cache });
|
|
1270
1035
|
return;
|
|
1271
1036
|
}
|
|
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
|
-
}
|
|
1286
1037
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1287
1038
|
const payload = asRecord(await readJsonBody(req));
|
|
1288
1039
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
@@ -1446,7 +1197,6 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1446
1197
|
}
|
|
1447
1198
|
};
|
|
1448
1199
|
middleware.dispose = () => {
|
|
1449
|
-
threadSearchIndex = null;
|
|
1450
1200
|
appServer.dispose();
|
|
1451
1201
|
};
|
|
1452
1202
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -1461,7 +1211,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1461
1211
|
}
|
|
1462
1212
|
|
|
1463
1213
|
// src/server/authMiddleware.ts
|
|
1464
|
-
import { randomBytes
|
|
1214
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1465
1215
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1466
1216
|
function constantTimeCompare(a, b) {
|
|
1467
1217
|
const bufA = Buffer.from(a);
|
|
@@ -1559,7 +1309,7 @@ function createAuthSession(password) {
|
|
|
1559
1309
|
res.status(401).json({ error: "Invalid password" });
|
|
1560
1310
|
return;
|
|
1561
1311
|
}
|
|
1562
|
-
const token =
|
|
1312
|
+
const token = randomBytes(32).toString("hex");
|
|
1563
1313
|
validTokens.add(token);
|
|
1564
1314
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
1565
1315
|
res.json({ ok: true });
|
|
@@ -1578,32 +1328,74 @@ function createAuthSession(password) {
|
|
|
1578
1328
|
};
|
|
1579
1329
|
}
|
|
1580
1330
|
|
|
1581
|
-
// src/server/
|
|
1582
|
-
import {
|
|
1583
|
-
|
|
1584
|
-
var
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
".
|
|
1588
|
-
".
|
|
1589
|
-
".
|
|
1590
|
-
".
|
|
1591
|
-
".
|
|
1592
|
-
".
|
|
1593
|
-
".
|
|
1594
|
-
".
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1331
|
+
// src/server/localBrowseUi.ts
|
|
1332
|
+
import { dirname, extname, join as join2 } from "path";
|
|
1333
|
+
import { readFile as readFile2, readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
1334
|
+
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1335
|
+
".txt",
|
|
1336
|
+
".md",
|
|
1337
|
+
".json",
|
|
1338
|
+
".js",
|
|
1339
|
+
".ts",
|
|
1340
|
+
".tsx",
|
|
1341
|
+
".jsx",
|
|
1342
|
+
".css",
|
|
1343
|
+
".scss",
|
|
1344
|
+
".html",
|
|
1345
|
+
".htm",
|
|
1346
|
+
".xml",
|
|
1347
|
+
".yml",
|
|
1348
|
+
".yaml",
|
|
1349
|
+
".log",
|
|
1350
|
+
".csv",
|
|
1351
|
+
".env",
|
|
1352
|
+
".py",
|
|
1353
|
+
".sh",
|
|
1354
|
+
".toml",
|
|
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";
|
|
1605
1398
|
}
|
|
1606
|
-
return trimmed;
|
|
1607
1399
|
}
|
|
1608
1400
|
function normalizeLocalPath(rawPath) {
|
|
1609
1401
|
const trimmed = rawPath.trim();
|
|
@@ -1625,39 +1417,72 @@ function decodeBrowsePath(rawPath) {
|
|
|
1625
1417
|
return rawPath;
|
|
1626
1418
|
}
|
|
1627
1419
|
}
|
|
1420
|
+
function isTextEditablePath(pathValue) {
|
|
1421
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
|
|
1422
|
+
}
|
|
1628
1423
|
function escapeHtml(value) {
|
|
1629
1424
|
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
1630
1425
|
}
|
|
1631
1426
|
function toBrowseHref(pathValue) {
|
|
1632
1427
|
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
1633
1428
|
}
|
|
1634
|
-
|
|
1429
|
+
function toEditHref(pathValue) {
|
|
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) {
|
|
1635
1436
|
const entries = await readdir2(localPath, { withFileTypes: true });
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1437
|
+
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
1438
|
+
const entryPath = join2(localPath, entry.name);
|
|
1439
|
+
const entryStat = await stat2(entryPath);
|
|
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;
|
|
1639
1452
|
return a.name.localeCompare(b.name);
|
|
1640
1453
|
});
|
|
1454
|
+
}
|
|
1455
|
+
async function createDirectoryListingHtml(localPath) {
|
|
1456
|
+
const items = await getDirectoryItems(localPath);
|
|
1641
1457
|
const parentPath = dirname(localPath);
|
|
1642
|
-
const rows =
|
|
1643
|
-
const
|
|
1644
|
-
const
|
|
1645
|
-
return `<li><a href="${escapeHtml(toBrowseHref(
|
|
1458
|
+
const rows = items.map((item) => {
|
|
1459
|
+
const suffix = item.isDirectory ? "/" : "";
|
|
1460
|
+
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
1461
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
1646
1462
|
}).join("\n");
|
|
1647
1463
|
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
1648
|
-
|
|
1464
|
+
return `<!doctype html>
|
|
1649
1465
|
<html lang="en">
|
|
1650
1466
|
<head>
|
|
1651
1467
|
<meta charset="utf-8" />
|
|
1652
1468
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1653
1469
|
<title>Index of ${escapeHtml(localPath)}</title>
|
|
1654
1470
|
<style>
|
|
1655
|
-
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin:
|
|
1471
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
|
|
1656
1472
|
a { color: #8cc2ff; text-decoration: none; }
|
|
1657
1473
|
a:hover { text-decoration: underline; }
|
|
1658
|
-
ul { list-style: none; padding: 0; margin: 12px 0 0; }
|
|
1659
|
-
|
|
1474
|
+
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
1475
|
+
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
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; }
|
|
1660
1479
|
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
|
+
}
|
|
1661
1486
|
</style>
|
|
1662
1487
|
</head>
|
|
1663
1488
|
<body>
|
|
@@ -1666,7 +1491,107 @@ async function renderDirectoryListing(res, localPath) {
|
|
|
1666
1491
|
<ul>${rows}</ul>
|
|
1667
1492
|
</body>
|
|
1668
1493
|
</html>`;
|
|
1669
|
-
|
|
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 "";
|
|
1670
1595
|
}
|
|
1671
1596
|
function createServer(options = {}) {
|
|
1672
1597
|
const app = express();
|
|
@@ -1683,7 +1608,7 @@ function createServer(options = {}) {
|
|
|
1683
1608
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1684
1609
|
return;
|
|
1685
1610
|
}
|
|
1686
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
1611
|
+
const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
|
|
1687
1612
|
if (!contentType) {
|
|
1688
1613
|
res.status(415).json({ error: "Unsupported image type." });
|
|
1689
1614
|
return;
|
|
@@ -1710,17 +1635,18 @@ function createServer(options = {}) {
|
|
|
1710
1635
|
});
|
|
1711
1636
|
});
|
|
1712
1637
|
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
1713
|
-
const rawPath =
|
|
1638
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
1714
1639
|
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1715
1640
|
if (!localPath || !isAbsolute2(localPath)) {
|
|
1716
1641
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1717
1642
|
return;
|
|
1718
1643
|
}
|
|
1719
1644
|
try {
|
|
1720
|
-
const fileStat = await
|
|
1645
|
+
const fileStat = await stat3(localPath);
|
|
1721
1646
|
res.setHeader("Cache-Control", "private, no-store");
|
|
1722
1647
|
if (fileStat.isDirectory()) {
|
|
1723
|
-
await
|
|
1648
|
+
const html = await createDirectoryListingHtml(localPath);
|
|
1649
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1724
1650
|
return;
|
|
1725
1651
|
}
|
|
1726
1652
|
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
@@ -1731,6 +1657,44 @@ function createServer(options = {}) {
|
|
|
1731
1657
|
res.status(404).json({ error: "File not found." });
|
|
1732
1658
|
}
|
|
1733
1659
|
});
|
|
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
|
+
});
|
|
1734
1698
|
const hasFrontendAssets = existsSync(spaEntryFile);
|
|
1735
1699
|
if (hasFrontendAssets) {
|
|
1736
1700
|
app.use(express.static(distDir));
|
|
@@ -1802,11 +1766,11 @@ function generatePassword() {
|
|
|
1802
1766
|
|
|
1803
1767
|
// src/cli/index.ts
|
|
1804
1768
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
1805
|
-
var __dirname2 =
|
|
1769
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
1806
1770
|
async function readCliVersion() {
|
|
1807
1771
|
try {
|
|
1808
|
-
const packageJsonPath =
|
|
1809
|
-
const raw = await
|
|
1772
|
+
const packageJsonPath = join4(__dirname2, "..", "package.json");
|
|
1773
|
+
const raw = await readFile3(packageJsonPath, "utf8");
|
|
1810
1774
|
const parsed = JSON.parse(raw);
|
|
1811
1775
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
1812
1776
|
} catch {
|
|
@@ -1831,13 +1795,13 @@ function runWithStatus(command, args) {
|
|
|
1831
1795
|
return result.status ?? -1;
|
|
1832
1796
|
}
|
|
1833
1797
|
function getUserNpmPrefix() {
|
|
1834
|
-
return
|
|
1798
|
+
return join4(homedir2(), ".npm-global");
|
|
1835
1799
|
}
|
|
1836
1800
|
function resolveCodexCommand() {
|
|
1837
1801
|
if (canRun("codex", ["--version"])) {
|
|
1838
1802
|
return "codex";
|
|
1839
1803
|
}
|
|
1840
|
-
const userCandidate =
|
|
1804
|
+
const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
|
|
1841
1805
|
if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
1842
1806
|
return userCandidate;
|
|
1843
1807
|
}
|
|
@@ -1845,88 +1809,15 @@ function resolveCodexCommand() {
|
|
|
1845
1809
|
if (!prefix) {
|
|
1846
1810
|
return null;
|
|
1847
1811
|
}
|
|
1848
|
-
const candidate =
|
|
1812
|
+
const candidate = join4(prefix, "bin", "codex");
|
|
1849
1813
|
if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
|
|
1850
1814
|
return candidate;
|
|
1851
1815
|
}
|
|
1852
1816
|
return null;
|
|
1853
1817
|
}
|
|
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
|
-
}
|
|
1927
1818
|
function hasCodexAuth() {
|
|
1928
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
1929
|
-
return existsSync2(
|
|
1819
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
|
|
1820
|
+
return existsSync2(join4(codexHome, "auth.json"));
|
|
1930
1821
|
}
|
|
1931
1822
|
function ensureCodexInstalled() {
|
|
1932
1823
|
let codexCommand = resolveCodexCommand();
|
|
@@ -1944,7 +1835,7 @@ function ensureCodexInstalled() {
|
|
|
1944
1835
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
1945
1836
|
`);
|
|
1946
1837
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
1947
|
-
process.env.PATH = `${
|
|
1838
|
+
process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
1948
1839
|
};
|
|
1949
1840
|
if (isTermuxRuntime()) {
|
|
1950
1841
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -2005,9 +1896,9 @@ function parseCloudflaredUrl(chunk) {
|
|
|
2005
1896
|
}
|
|
2006
1897
|
return urlMatch[urlMatch.length - 1] ?? null;
|
|
2007
1898
|
}
|
|
2008
|
-
async function startCloudflaredTunnel(
|
|
1899
|
+
async function startCloudflaredTunnel(localPort) {
|
|
2009
1900
|
return new Promise((resolve2, reject) => {
|
|
2010
|
-
const child = spawn2(
|
|
1901
|
+
const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
2011
1902
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2012
1903
|
});
|
|
2013
1904
|
const timeout = setTimeout(() => {
|
|
@@ -2080,8 +1971,7 @@ async function startServer(options) {
|
|
|
2080
1971
|
let tunnelUrl = null;
|
|
2081
1972
|
if (options.tunnel) {
|
|
2082
1973
|
try {
|
|
2083
|
-
const
|
|
2084
|
-
const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
|
|
1974
|
+
const tunnel = await startCloudflaredTunnel(port);
|
|
2085
1975
|
tunnelChild = tunnel.process;
|
|
2086
1976
|
tunnelUrl = tunnel.url;
|
|
2087
1977
|
} catch (error) {
|