codexapp 0.1.51 → 0.1.53
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 +0 -34
- package/dist/assets/index-B_hsCD9g.js +1425 -0
- package/dist/assets/index-Dpf9bMnE.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +108 -621
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/index-8tP34EwU.js +0 -1429
- package/dist/assets/index-B8I4NE8D.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import { chmodSync, createWriteStream, existsSync as
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
|
|
6
6
|
import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { isAbsolute as isAbsolute3, join as
|
|
9
|
-
import { spawn as spawn3 } from "child_process";
|
|
10
|
-
import { createInterface
|
|
7
|
+
import { homedir as homedir3, networkInterfaces } from "os";
|
|
8
|
+
import { isAbsolute as isAbsolute3, join as join5, resolve as resolve2 } from "path";
|
|
9
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
10
|
+
import { createInterface } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
12
|
import { dirname as dirname3 } from "path";
|
|
13
13
|
import { get as httpsGet } from "https";
|
|
@@ -16,8 +16,8 @@ import qrcode from "qrcode-terminal";
|
|
|
16
16
|
|
|
17
17
|
// src/server/httpServer.ts
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
|
-
import { dirname as dirname2, extname as
|
|
20
|
-
import { existsSync as
|
|
19
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
20
|
+
import { existsSync as existsSync2 } from "fs";
|
|
21
21
|
import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
|
|
22
22
|
import express from "express";
|
|
23
23
|
|
|
@@ -25,13 +25,10 @@ import express from "express";
|
|
|
25
25
|
import { spawn as spawn2 } from "child_process";
|
|
26
26
|
import { randomBytes } from "crypto";
|
|
27
27
|
import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
28
|
-
import { createReadStream } from "fs";
|
|
29
|
-
import { request as httpRequest } from "http";
|
|
30
28
|
import { request as httpsRequest } from "https";
|
|
31
|
-
import { homedir as
|
|
29
|
+
import { homedir as homedir2 } from "os";
|
|
32
30
|
import { tmpdir as tmpdir2 } from "os";
|
|
33
|
-
import { basename
|
|
34
|
-
import { createInterface } from "readline";
|
|
31
|
+
import { basename, isAbsolute, join as join2, resolve } from "path";
|
|
35
32
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
36
33
|
|
|
37
34
|
// src/server/skillsRoutes.ts
|
|
@@ -1120,84 +1117,6 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1120
1117
|
return false;
|
|
1121
1118
|
}
|
|
1122
1119
|
|
|
1123
|
-
// src/utils/commandInvocation.ts
|
|
1124
|
-
import { spawnSync } from "child_process";
|
|
1125
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1126
|
-
import { homedir as homedir2 } from "os";
|
|
1127
|
-
import { basename, extname, join as join2 } from "path";
|
|
1128
|
-
var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
|
|
1129
|
-
function quoteCmdExeArg(value) {
|
|
1130
|
-
const normalized = value.replace(/"/g, '""');
|
|
1131
|
-
if (!/[\s"]/u.test(normalized)) {
|
|
1132
|
-
return normalized;
|
|
1133
|
-
}
|
|
1134
|
-
return `"${normalized}"`;
|
|
1135
|
-
}
|
|
1136
|
-
function needsCmdExeWrapper(command) {
|
|
1137
|
-
if (process.platform !== "win32") {
|
|
1138
|
-
return false;
|
|
1139
|
-
}
|
|
1140
|
-
const lowerCommand = command.toLowerCase();
|
|
1141
|
-
const baseName = basename(lowerCommand);
|
|
1142
|
-
if (/\.(cmd|bat)$/i.test(baseName)) {
|
|
1143
|
-
return true;
|
|
1144
|
-
}
|
|
1145
|
-
if (extname(baseName)) {
|
|
1146
|
-
return false;
|
|
1147
|
-
}
|
|
1148
|
-
return WINDOWS_CMD_NAMES.has(baseName);
|
|
1149
|
-
}
|
|
1150
|
-
function getSpawnInvocation(command, args = []) {
|
|
1151
|
-
if (needsCmdExeWrapper(command)) {
|
|
1152
|
-
return {
|
|
1153
|
-
command: "cmd.exe",
|
|
1154
|
-
args: ["/d", "/s", "/c", [quoteCmdExeArg(command), ...args.map((arg) => quoteCmdExeArg(arg))].join(" ")]
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
return { command, args };
|
|
1158
|
-
}
|
|
1159
|
-
function spawnSyncCommand(command, args = [], options = {}) {
|
|
1160
|
-
const invocation = getSpawnInvocation(command, args);
|
|
1161
|
-
return spawnSync(invocation.command, invocation.args, options);
|
|
1162
|
-
}
|
|
1163
|
-
function canRunCommand(command, args = []) {
|
|
1164
|
-
const result = spawnSyncCommand(command, args, { stdio: "ignore" });
|
|
1165
|
-
return result.status === 0;
|
|
1166
|
-
}
|
|
1167
|
-
function getUserNpmPrefix() {
|
|
1168
|
-
return join2(homedir2(), ".npm-global");
|
|
1169
|
-
}
|
|
1170
|
-
function resolveCodexCommand() {
|
|
1171
|
-
if (canRunCommand("codex", ["--version"])) {
|
|
1172
|
-
return "codex";
|
|
1173
|
-
}
|
|
1174
|
-
if (process.platform === "win32") {
|
|
1175
|
-
const windowsCandidates = [
|
|
1176
|
-
process.env.APPDATA ? join2(process.env.APPDATA, "npm", "codex.cmd") : "",
|
|
1177
|
-
join2(homedir2(), ".local", "bin", "codex.cmd"),
|
|
1178
|
-
join2(getUserNpmPrefix(), "bin", "codex.cmd")
|
|
1179
|
-
].filter(Boolean);
|
|
1180
|
-
for (const candidate2 of windowsCandidates) {
|
|
1181
|
-
if (existsSync2(candidate2) && canRunCommand(candidate2, ["--version"])) {
|
|
1182
|
-
return candidate2;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
const userCandidate = join2(getUserNpmPrefix(), "bin", "codex");
|
|
1187
|
-
if (existsSync2(userCandidate) && canRunCommand(userCandidate, ["--version"])) {
|
|
1188
|
-
return userCandidate;
|
|
1189
|
-
}
|
|
1190
|
-
const prefix = process.env.PREFIX?.trim();
|
|
1191
|
-
if (!prefix) {
|
|
1192
|
-
return null;
|
|
1193
|
-
}
|
|
1194
|
-
const candidate = join2(prefix, "bin", "codex");
|
|
1195
|
-
if (existsSync2(candidate) && canRunCommand(candidate, ["--version"])) {
|
|
1196
|
-
return candidate;
|
|
1197
|
-
}
|
|
1198
|
-
return null;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
1120
|
// src/server/codexAppServerBridge.ts
|
|
1202
1121
|
function asRecord2(value) {
|
|
1203
1122
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -1273,54 +1192,6 @@ function scoreFileCandidate(path, query) {
|
|
|
1273
1192
|
if (lowerPath.includes(lowerQuery)) return 4;
|
|
1274
1193
|
return 10;
|
|
1275
1194
|
}
|
|
1276
|
-
function decodeHtmlEntities(value) {
|
|
1277
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(///gi, "/");
|
|
1278
|
-
}
|
|
1279
|
-
function stripHtml(value) {
|
|
1280
|
-
return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
|
|
1281
|
-
}
|
|
1282
|
-
function parseGithubTrendingHtml(html, limit) {
|
|
1283
|
-
const rows = html.match(/<article[\s\S]*?<\/article>/g) ?? [];
|
|
1284
|
-
const items = [];
|
|
1285
|
-
let seq = Date.now();
|
|
1286
|
-
for (const row of rows) {
|
|
1287
|
-
const repoBlockMatch = row.match(/<h2[\s\S]*?<\/h2>/);
|
|
1288
|
-
const hrefMatch = repoBlockMatch?.[0]?.match(/href="\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)"/);
|
|
1289
|
-
if (!hrefMatch) continue;
|
|
1290
|
-
const fullName = hrefMatch[1] ?? "";
|
|
1291
|
-
if (!fullName || items.some((item) => item.fullName === fullName)) continue;
|
|
1292
|
-
const descriptionMatch = row.match(/<p[^>]*class="[^"]*col-9[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*>([\s\S]*?)<\/p>/);
|
|
1293
|
-
const languageMatch = row.match(/programmingLanguage[^>]*>\s*([\s\S]*?)\s*<\/span>/);
|
|
1294
|
-
const starsMatch = row.match(/href="\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/stargazers"[\s\S]*?>([\s\S]*?)<\/a>/);
|
|
1295
|
-
const starsText = stripHtml(starsMatch?.[1] ?? "").replace(/,/g, "");
|
|
1296
|
-
const stars = Number.parseInt(starsText, 10);
|
|
1297
|
-
items.push({
|
|
1298
|
-
id: seq,
|
|
1299
|
-
fullName,
|
|
1300
|
-
url: `https://github.com/${fullName}`,
|
|
1301
|
-
description: stripHtml(descriptionMatch?.[1] ?? ""),
|
|
1302
|
-
language: stripHtml(languageMatch?.[1] ?? ""),
|
|
1303
|
-
stars: Number.isFinite(stars) ? stars : 0
|
|
1304
|
-
});
|
|
1305
|
-
seq += 1;
|
|
1306
|
-
if (items.length >= limit) break;
|
|
1307
|
-
}
|
|
1308
|
-
return items;
|
|
1309
|
-
}
|
|
1310
|
-
async function fetchGithubTrending(since, limit) {
|
|
1311
|
-
const endpoint = `https://github.com/trending?since=${since}`;
|
|
1312
|
-
const response = await fetch(endpoint, {
|
|
1313
|
-
headers: {
|
|
1314
|
-
"User-Agent": "codex-web-local",
|
|
1315
|
-
Accept: "text/html"
|
|
1316
|
-
}
|
|
1317
|
-
});
|
|
1318
|
-
if (!response.ok) {
|
|
1319
|
-
throw new Error(`GitHub trending fetch failed (${response.status})`);
|
|
1320
|
-
}
|
|
1321
|
-
const html = await response.text();
|
|
1322
|
-
return parseGithubTrendingHtml(html, limit);
|
|
1323
|
-
}
|
|
1324
1195
|
async function listFilesWithRipgrep(cwd) {
|
|
1325
1196
|
return await new Promise((resolve3, reject) => {
|
|
1326
1197
|
const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
@@ -1350,7 +1221,7 @@ async function listFilesWithRipgrep(cwd) {
|
|
|
1350
1221
|
}
|
|
1351
1222
|
function getCodexHomeDir2() {
|
|
1352
1223
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
1353
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
1224
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
1354
1225
|
}
|
|
1355
1226
|
async function runCommand2(command, args, options = {}) {
|
|
1356
1227
|
await new Promise((resolve3, reject) => {
|
|
@@ -1388,7 +1259,7 @@ function isNotGitRepositoryError(error) {
|
|
|
1388
1259
|
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
1389
1260
|
}
|
|
1390
1261
|
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
1391
|
-
const agentsPath =
|
|
1262
|
+
const agentsPath = join2(repoRoot, "AGENTS.md");
|
|
1392
1263
|
try {
|
|
1393
1264
|
await stat2(agentsPath);
|
|
1394
1265
|
} catch {
|
|
@@ -1428,33 +1299,6 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
1428
1299
|
});
|
|
1429
1300
|
});
|
|
1430
1301
|
}
|
|
1431
|
-
async function runCommandWithOutput2(command, args, options = {}) {
|
|
1432
|
-
return await new Promise((resolve3, reject) => {
|
|
1433
|
-
const proc = spawn2(command, args, {
|
|
1434
|
-
cwd: options.cwd,
|
|
1435
|
-
env: process.env,
|
|
1436
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1437
|
-
});
|
|
1438
|
-
let stdout = "";
|
|
1439
|
-
let stderr = "";
|
|
1440
|
-
proc.stdout.on("data", (chunk) => {
|
|
1441
|
-
stdout += chunk.toString();
|
|
1442
|
-
});
|
|
1443
|
-
proc.stderr.on("data", (chunk) => {
|
|
1444
|
-
stderr += chunk.toString();
|
|
1445
|
-
});
|
|
1446
|
-
proc.on("error", reject);
|
|
1447
|
-
proc.on("close", (code) => {
|
|
1448
|
-
if (code === 0) {
|
|
1449
|
-
resolve3(stdout.trim());
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
1453
|
-
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
1454
|
-
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
1455
|
-
});
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
1302
|
function normalizeStringArray(value) {
|
|
1459
1303
|
if (!Array.isArray(value)) return [];
|
|
1460
1304
|
const normalized = [];
|
|
@@ -1475,32 +1319,8 @@ function normalizeStringRecord(value) {
|
|
|
1475
1319
|
}
|
|
1476
1320
|
return next;
|
|
1477
1321
|
}
|
|
1478
|
-
function normalizeCommitMessage(value) {
|
|
1479
|
-
if (typeof value !== "string") return "";
|
|
1480
|
-
const normalized = value.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n").trim();
|
|
1481
|
-
return normalized.slice(0, 2e3);
|
|
1482
|
-
}
|
|
1483
|
-
async function hasGitWorkingTreeChanges(cwd) {
|
|
1484
|
-
const status = await runCommandWithOutput2("git", ["status", "--porcelain"], { cwd });
|
|
1485
|
-
return status.trim().length > 0;
|
|
1486
|
-
}
|
|
1487
|
-
async function findCommitByExactMessage(cwd, message) {
|
|
1488
|
-
const normalizedTarget = normalizeCommitMessage(message);
|
|
1489
|
-
if (!normalizedTarget) return "";
|
|
1490
|
-
const raw = await runCommandWithOutput2("git", ["log", "--format=%H%x1f%B%x1e"], { cwd });
|
|
1491
|
-
const entries = raw.split("");
|
|
1492
|
-
for (const entry of entries) {
|
|
1493
|
-
if (!entry.trim()) continue;
|
|
1494
|
-
const [shaRaw, bodyRaw] = entry.split("");
|
|
1495
|
-
const sha = (shaRaw ?? "").trim();
|
|
1496
|
-
const body = normalizeCommitMessage(bodyRaw ?? "");
|
|
1497
|
-
if (!sha) continue;
|
|
1498
|
-
if (body === normalizedTarget) return sha;
|
|
1499
|
-
}
|
|
1500
|
-
return "";
|
|
1501
|
-
}
|
|
1502
1322
|
function getCodexAuthPath() {
|
|
1503
|
-
return
|
|
1323
|
+
return join2(getCodexHomeDir2(), "auth.json");
|
|
1504
1324
|
}
|
|
1505
1325
|
async function readCodexAuth() {
|
|
1506
1326
|
try {
|
|
@@ -1514,20 +1334,12 @@ async function readCodexAuth() {
|
|
|
1514
1334
|
}
|
|
1515
1335
|
}
|
|
1516
1336
|
function getCodexGlobalStatePath() {
|
|
1517
|
-
return
|
|
1518
|
-
}
|
|
1519
|
-
function getCodexSessionIndexPath() {
|
|
1520
|
-
return join3(getCodexHomeDir2(), "session_index.jsonl");
|
|
1337
|
+
return join2(getCodexHomeDir2(), ".codex-global-state.json");
|
|
1521
1338
|
}
|
|
1522
1339
|
var MAX_THREAD_TITLES = 500;
|
|
1523
|
-
var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
|
|
1524
|
-
var sessionIndexThreadTitleCacheState = {
|
|
1525
|
-
fileSignature: null,
|
|
1526
|
-
cache: EMPTY_THREAD_TITLE_CACHE
|
|
1527
|
-
};
|
|
1528
1340
|
function normalizeThreadTitleCache(value) {
|
|
1529
1341
|
const record = asRecord2(value);
|
|
1530
|
-
if (!record) return
|
|
1342
|
+
if (!record) return { titles: {}, order: [] };
|
|
1531
1343
|
const rawTitles = asRecord2(record.titles);
|
|
1532
1344
|
const titles = {};
|
|
1533
1345
|
if (rawTitles) {
|
|
@@ -1551,47 +1363,6 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
1551
1363
|
const { [id]: _, ...titles } = cache.titles;
|
|
1552
1364
|
return { titles, order: cache.order.filter((o) => o !== id) };
|
|
1553
1365
|
}
|
|
1554
|
-
function normalizeSessionIndexThreadTitle(value) {
|
|
1555
|
-
const record = asRecord2(value);
|
|
1556
|
-
if (!record) return null;
|
|
1557
|
-
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
1558
|
-
const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
|
|
1559
|
-
const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
|
|
1560
|
-
const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
|
|
1561
|
-
if (!id || !title) return null;
|
|
1562
|
-
return {
|
|
1563
|
-
id,
|
|
1564
|
-
title,
|
|
1565
|
-
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
|
-
function trimThreadTitleCache(cache) {
|
|
1569
|
-
const titles = { ...cache.titles };
|
|
1570
|
-
const order = cache.order.filter((id) => {
|
|
1571
|
-
if (!titles[id]) return false;
|
|
1572
|
-
return true;
|
|
1573
|
-
}).slice(0, MAX_THREAD_TITLES);
|
|
1574
|
-
for (const id of Object.keys(titles)) {
|
|
1575
|
-
if (!order.includes(id)) {
|
|
1576
|
-
delete titles[id];
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
return { titles, order };
|
|
1580
|
-
}
|
|
1581
|
-
function mergeThreadTitleCaches(base, overlay) {
|
|
1582
|
-
const titles = { ...base.titles, ...overlay.titles };
|
|
1583
|
-
const order = [];
|
|
1584
|
-
for (const id of [...overlay.order, ...base.order]) {
|
|
1585
|
-
if (!titles[id] || order.includes(id)) continue;
|
|
1586
|
-
order.push(id);
|
|
1587
|
-
}
|
|
1588
|
-
for (const id of Object.keys(titles)) {
|
|
1589
|
-
if (!order.includes(id)) {
|
|
1590
|
-
order.push(id);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
return trimThreadTitleCache({ titles, order });
|
|
1594
|
-
}
|
|
1595
1366
|
async function readThreadTitleCache() {
|
|
1596
1367
|
const statePath = getCodexGlobalStatePath();
|
|
1597
1368
|
try {
|
|
@@ -1599,7 +1370,7 @@ async function readThreadTitleCache() {
|
|
|
1599
1370
|
const payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1600
1371
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
1601
1372
|
} catch {
|
|
1602
|
-
return
|
|
1373
|
+
return { titles: {}, order: [] };
|
|
1603
1374
|
}
|
|
1604
1375
|
}
|
|
1605
1376
|
async function writeThreadTitleCache(cache) {
|
|
@@ -1614,69 +1385,6 @@ async function writeThreadTitleCache(cache) {
|
|
|
1614
1385
|
payload["thread-titles"] = cache;
|
|
1615
1386
|
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1616
1387
|
}
|
|
1617
|
-
function getSessionIndexFileSignature(stats) {
|
|
1618
|
-
return `${String(stats.mtimeMs)}:${String(stats.size)}`;
|
|
1619
|
-
}
|
|
1620
|
-
async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
|
|
1621
|
-
const latestById = /* @__PURE__ */ new Map();
|
|
1622
|
-
const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
|
|
1623
|
-
const lines = createInterface({
|
|
1624
|
-
input,
|
|
1625
|
-
crlfDelay: Infinity
|
|
1626
|
-
});
|
|
1627
|
-
try {
|
|
1628
|
-
for await (const line of lines) {
|
|
1629
|
-
const trimmed = line.trim();
|
|
1630
|
-
if (!trimmed) continue;
|
|
1631
|
-
try {
|
|
1632
|
-
const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
|
|
1633
|
-
if (!entry) continue;
|
|
1634
|
-
const previous = latestById.get(entry.id);
|
|
1635
|
-
if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
|
|
1636
|
-
latestById.set(entry.id, entry);
|
|
1637
|
-
}
|
|
1638
|
-
} catch {
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
} finally {
|
|
1642
|
-
lines.close();
|
|
1643
|
-
input.close();
|
|
1644
|
-
}
|
|
1645
|
-
const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
|
|
1646
|
-
const titles = {};
|
|
1647
|
-
const order = [];
|
|
1648
|
-
for (const entry of entries) {
|
|
1649
|
-
titles[entry.id] = entry.title;
|
|
1650
|
-
order.push(entry.id);
|
|
1651
|
-
}
|
|
1652
|
-
return trimThreadTitleCache({ titles, order });
|
|
1653
|
-
}
|
|
1654
|
-
async function readThreadTitlesFromSessionIndex() {
|
|
1655
|
-
const sessionIndexPath = getCodexSessionIndexPath();
|
|
1656
|
-
try {
|
|
1657
|
-
const stats = await stat2(sessionIndexPath);
|
|
1658
|
-
const fileSignature = getSessionIndexFileSignature(stats);
|
|
1659
|
-
if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
|
|
1660
|
-
return sessionIndexThreadTitleCacheState.cache;
|
|
1661
|
-
}
|
|
1662
|
-
const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
|
|
1663
|
-
sessionIndexThreadTitleCacheState = { fileSignature, cache };
|
|
1664
|
-
return cache;
|
|
1665
|
-
} catch {
|
|
1666
|
-
sessionIndexThreadTitleCacheState = {
|
|
1667
|
-
fileSignature: "missing",
|
|
1668
|
-
cache: EMPTY_THREAD_TITLE_CACHE
|
|
1669
|
-
};
|
|
1670
|
-
return sessionIndexThreadTitleCacheState.cache;
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
async function readMergedThreadTitleCache() {
|
|
1674
|
-
const [sessionIndexCache, persistedCache] = await Promise.all([
|
|
1675
|
-
readThreadTitlesFromSessionIndex(),
|
|
1676
|
-
readThreadTitleCache()
|
|
1677
|
-
]);
|
|
1678
|
-
return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
|
|
1679
|
-
}
|
|
1680
1388
|
async function readWorkspaceRootsState() {
|
|
1681
1389
|
const statePath = getCodexGlobalStatePath();
|
|
1682
1390
|
let payload = {};
|
|
@@ -1776,10 +1484,10 @@ function handleFileUpload(req, res) {
|
|
|
1776
1484
|
setJson2(res, 400, { error: "No file in request" });
|
|
1777
1485
|
return;
|
|
1778
1486
|
}
|
|
1779
|
-
const uploadDir =
|
|
1487
|
+
const uploadDir = join2(tmpdir2(), "codex-web-uploads");
|
|
1780
1488
|
await mkdir2(uploadDir, { recursive: true });
|
|
1781
|
-
const destDir = await mkdtemp2(
|
|
1782
|
-
const destPath =
|
|
1489
|
+
const destDir = await mkdtemp2(join2(uploadDir, "f-"));
|
|
1490
|
+
const destPath = join2(destDir, fileName);
|
|
1783
1491
|
await writeFile2(destPath, fileData);
|
|
1784
1492
|
setJson2(res, 200, { path: destPath });
|
|
1785
1493
|
} catch (err) {
|
|
@@ -1790,79 +1498,32 @@ function handleFileUpload(req, res) {
|
|
|
1790
1498
|
setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
|
|
1791
1499
|
});
|
|
1792
1500
|
}
|
|
1793
|
-
function httpPost(url, headers, body) {
|
|
1794
|
-
const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
|
|
1795
|
-
return new Promise((resolve3, reject) => {
|
|
1796
|
-
const req = doRequest(url, { method: "POST", headers }, (res) => {
|
|
1797
|
-
const chunks = [];
|
|
1798
|
-
res.on("data", (c) => chunks.push(c));
|
|
1799
|
-
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
1800
|
-
res.on("error", reject);
|
|
1801
|
-
});
|
|
1802
|
-
req.on("error", reject);
|
|
1803
|
-
req.write(body);
|
|
1804
|
-
req.end();
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
var curlImpersonateAvailable = null;
|
|
1808
|
-
function curlImpersonatePost(url, headers, body) {
|
|
1809
|
-
return new Promise((resolve3, reject) => {
|
|
1810
|
-
const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
|
|
1811
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
1812
|
-
if (k.toLowerCase() === "content-length") continue;
|
|
1813
|
-
args.push("-H", `${k}: ${String(v)}`);
|
|
1814
|
-
}
|
|
1815
|
-
args.push("--data-binary", "@-");
|
|
1816
|
-
const proc = spawn2("curl-impersonate-chrome", args, {
|
|
1817
|
-
env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
|
|
1818
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1819
|
-
});
|
|
1820
|
-
const chunks = [];
|
|
1821
|
-
proc.stdout.on("data", (c) => chunks.push(c));
|
|
1822
|
-
proc.on("error", (e) => {
|
|
1823
|
-
curlImpersonateAvailable = false;
|
|
1824
|
-
reject(e);
|
|
1825
|
-
});
|
|
1826
|
-
proc.on("close", (code) => {
|
|
1827
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1828
|
-
const lastNewline = raw.lastIndexOf("\n");
|
|
1829
|
-
const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
|
|
1830
|
-
const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
|
|
1831
|
-
const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
|
|
1832
|
-
curlImpersonateAvailable = true;
|
|
1833
|
-
resolve3({ status, body: responseBody });
|
|
1834
|
-
});
|
|
1835
|
-
proc.stdin.write(body);
|
|
1836
|
-
proc.stdin.end();
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
1501
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
1840
|
-
const
|
|
1502
|
+
const headers = {
|
|
1841
1503
|
"Content-Type": contentType,
|
|
1842
1504
|
"Content-Length": body.length,
|
|
1843
1505
|
Authorization: `Bearer ${authToken}`,
|
|
1844
1506
|
originator: "Codex Desktop",
|
|
1845
1507
|
"User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
|
|
1846
1508
|
};
|
|
1847
|
-
if (accountId)
|
|
1848
|
-
|
|
1849
|
-
let result;
|
|
1850
|
-
try {
|
|
1851
|
-
result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1852
|
-
} catch {
|
|
1853
|
-
result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1854
|
-
}
|
|
1855
|
-
if (result.status === 403 && result.body.includes("cf_chl")) {
|
|
1856
|
-
if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
|
|
1857
|
-
try {
|
|
1858
|
-
const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1859
|
-
if (ciResult.status !== 403) return ciResult;
|
|
1860
|
-
} catch {
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
|
|
1509
|
+
if (accountId) {
|
|
1510
|
+
headers["ChatGPT-Account-Id"] = accountId;
|
|
1864
1511
|
}
|
|
1865
|
-
return
|
|
1512
|
+
return new Promise((resolve3, reject) => {
|
|
1513
|
+
const req = httpsRequest(
|
|
1514
|
+
"https://chatgpt.com/backend-api/transcribe",
|
|
1515
|
+
{ method: "POST", headers },
|
|
1516
|
+
(res) => {
|
|
1517
|
+
const chunks = [];
|
|
1518
|
+
res.on("data", (c) => chunks.push(c));
|
|
1519
|
+
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
1520
|
+
res.on("error", reject);
|
|
1521
|
+
}
|
|
1522
|
+
);
|
|
1523
|
+
req.on("error", reject);
|
|
1524
|
+
req.write(body);
|
|
1525
|
+
req.end();
|
|
1526
|
+
});
|
|
1866
1527
|
}
|
|
1867
1528
|
var AppServerProcess = class {
|
|
1868
1529
|
constructor() {
|
|
@@ -1886,8 +1547,7 @@ var AppServerProcess = class {
|
|
|
1886
1547
|
start() {
|
|
1887
1548
|
if (this.process) return;
|
|
1888
1549
|
this.stopping = false;
|
|
1889
|
-
const
|
|
1890
|
-
const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1550
|
+
const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1891
1551
|
this.process = proc;
|
|
1892
1552
|
proc.stdout.setEncoding("utf8");
|
|
1893
1553
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -2113,8 +1773,7 @@ var MethodCatalog = class {
|
|
|
2113
1773
|
}
|
|
2114
1774
|
async runGenerateSchemaCommand(outDir) {
|
|
2115
1775
|
await new Promise((resolve3, reject) => {
|
|
2116
|
-
const
|
|
2117
|
-
const process2 = spawn2(invocation.command, invocation.args, {
|
|
1776
|
+
const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
2118
1777
|
stdio: ["ignore", "ignore", "pipe"]
|
|
2119
1778
|
});
|
|
2120
1779
|
let stderr = "";
|
|
@@ -2170,9 +1829,9 @@ var MethodCatalog = class {
|
|
|
2170
1829
|
if (this.methodCache) {
|
|
2171
1830
|
return this.methodCache;
|
|
2172
1831
|
}
|
|
2173
|
-
const outDir = await mkdtemp2(
|
|
1832
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
2174
1833
|
await this.runGenerateSchemaCommand(outDir);
|
|
2175
|
-
const clientRequestPath =
|
|
1834
|
+
const clientRequestPath = join2(outDir, "ClientRequest.json");
|
|
2176
1835
|
const raw = await readFile2(clientRequestPath, "utf8");
|
|
2177
1836
|
const parsed = JSON.parse(raw);
|
|
2178
1837
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
@@ -2183,9 +1842,9 @@ var MethodCatalog = class {
|
|
|
2183
1842
|
if (this.notificationCache) {
|
|
2184
1843
|
return this.notificationCache;
|
|
2185
1844
|
}
|
|
2186
|
-
const outDir = await mkdtemp2(
|
|
1845
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
2187
1846
|
await this.runGenerateSchemaCommand(outDir);
|
|
2188
|
-
const serverNotificationPath =
|
|
1847
|
+
const serverNotificationPath = join2(outDir, "ServerNotification.json");
|
|
2189
1848
|
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
2190
1849
|
const parsed = JSON.parse(raw);
|
|
2191
1850
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
@@ -2347,20 +2006,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2347
2006
|
return;
|
|
2348
2007
|
}
|
|
2349
2008
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
2350
|
-
setJson2(res, 200, { data: { path:
|
|
2351
|
-
return;
|
|
2352
|
-
}
|
|
2353
|
-
if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
|
|
2354
|
-
const sinceRaw = (url.searchParams.get("since") ?? "").trim().toLowerCase();
|
|
2355
|
-
const since = sinceRaw === "weekly" ? "weekly" : sinceRaw === "monthly" ? "monthly" : "daily";
|
|
2356
|
-
const limitRaw = Number.parseInt((url.searchParams.get("limit") ?? "6").trim(), 10);
|
|
2357
|
-
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(10, limitRaw)) : 6;
|
|
2358
|
-
try {
|
|
2359
|
-
const data = await fetchGithubTrending(since, limit);
|
|
2360
|
-
setJson2(res, 200, { data });
|
|
2361
|
-
} catch (error) {
|
|
2362
|
-
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to fetch GitHub trending") });
|
|
2363
|
-
}
|
|
2009
|
+
setJson2(res, 200, { data: { path: homedir2() } });
|
|
2364
2010
|
return;
|
|
2365
2011
|
}
|
|
2366
2012
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
@@ -2390,22 +2036,22 @@ function createCodexBridgeMiddleware() {
|
|
|
2390
2036
|
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
2391
2037
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
2392
2038
|
}
|
|
2393
|
-
const repoName =
|
|
2394
|
-
const worktreesRoot =
|
|
2039
|
+
const repoName = basename(gitRoot) || "repo";
|
|
2040
|
+
const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
|
|
2395
2041
|
await mkdir2(worktreesRoot, { recursive: true });
|
|
2396
2042
|
let worktreeId = "";
|
|
2397
2043
|
let worktreeParent = "";
|
|
2398
2044
|
let worktreeCwd = "";
|
|
2399
2045
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
2400
2046
|
const candidate = randomBytes(2).toString("hex");
|
|
2401
|
-
const parent =
|
|
2047
|
+
const parent = join2(worktreesRoot, candidate);
|
|
2402
2048
|
try {
|
|
2403
2049
|
await stat2(parent);
|
|
2404
2050
|
continue;
|
|
2405
2051
|
} catch {
|
|
2406
2052
|
worktreeId = candidate;
|
|
2407
2053
|
worktreeParent = parent;
|
|
2408
|
-
worktreeCwd =
|
|
2054
|
+
worktreeCwd = join2(parent, repoName);
|
|
2409
2055
|
break;
|
|
2410
2056
|
}
|
|
2411
2057
|
}
|
|
@@ -2433,99 +2079,6 @@ function createCodexBridgeMiddleware() {
|
|
|
2433
2079
|
}
|
|
2434
2080
|
return;
|
|
2435
2081
|
}
|
|
2436
|
-
if (req.method === "POST" && url.pathname === "/codex-api/worktree/auto-commit") {
|
|
2437
|
-
const payload = asRecord2(await readJsonBody(req));
|
|
2438
|
-
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
2439
|
-
const commitMessage = normalizeCommitMessage(payload?.message);
|
|
2440
|
-
if (!rawCwd) {
|
|
2441
|
-
setJson2(res, 400, { error: "Missing cwd" });
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
|
-
if (!commitMessage) {
|
|
2445
|
-
setJson2(res, 400, { error: "Missing message" });
|
|
2446
|
-
return;
|
|
2447
|
-
}
|
|
2448
|
-
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
2449
|
-
try {
|
|
2450
|
-
const cwdInfo = await stat2(cwd);
|
|
2451
|
-
if (!cwdInfo.isDirectory()) {
|
|
2452
|
-
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
2453
|
-
return;
|
|
2454
|
-
}
|
|
2455
|
-
} catch {
|
|
2456
|
-
setJson2(res, 404, { error: "cwd does not exist" });
|
|
2457
|
-
return;
|
|
2458
|
-
}
|
|
2459
|
-
try {
|
|
2460
|
-
await runCommandCapture("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
2461
|
-
const beforeStatus = await runCommandWithOutput2("git", ["status", "--porcelain"], { cwd });
|
|
2462
|
-
if (!beforeStatus.trim()) {
|
|
2463
|
-
setJson2(res, 200, { data: { committed: false } });
|
|
2464
|
-
return;
|
|
2465
|
-
}
|
|
2466
|
-
await runCommand2("git", ["add", "-A"], { cwd });
|
|
2467
|
-
const stagedStatus = await runCommandWithOutput2("git", ["diff", "--cached", "--name-only"], { cwd });
|
|
2468
|
-
if (!stagedStatus.trim()) {
|
|
2469
|
-
setJson2(res, 200, { data: { committed: false } });
|
|
2470
|
-
return;
|
|
2471
|
-
}
|
|
2472
|
-
await runCommand2("git", ["commit", "-m", commitMessage], { cwd });
|
|
2473
|
-
setJson2(res, 200, { data: { committed: true } });
|
|
2474
|
-
} catch (error) {
|
|
2475
|
-
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to auto-commit worktree changes") });
|
|
2476
|
-
}
|
|
2477
|
-
return;
|
|
2478
|
-
}
|
|
2479
|
-
if (req.method === "POST" && url.pathname === "/codex-api/worktree/rollback-to-message") {
|
|
2480
|
-
const payload = asRecord2(await readJsonBody(req));
|
|
2481
|
-
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
2482
|
-
const commitMessage = normalizeCommitMessage(payload?.message);
|
|
2483
|
-
if (!rawCwd) {
|
|
2484
|
-
setJson2(res, 400, { error: "Missing cwd" });
|
|
2485
|
-
return;
|
|
2486
|
-
}
|
|
2487
|
-
if (!commitMessage) {
|
|
2488
|
-
setJson2(res, 400, { error: "Missing message" });
|
|
2489
|
-
return;
|
|
2490
|
-
}
|
|
2491
|
-
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
2492
|
-
try {
|
|
2493
|
-
const cwdInfo = await stat2(cwd);
|
|
2494
|
-
if (!cwdInfo.isDirectory()) {
|
|
2495
|
-
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
2496
|
-
return;
|
|
2497
|
-
}
|
|
2498
|
-
} catch {
|
|
2499
|
-
setJson2(res, 404, { error: "cwd does not exist" });
|
|
2500
|
-
return;
|
|
2501
|
-
}
|
|
2502
|
-
try {
|
|
2503
|
-
await runCommandCapture("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
2504
|
-
const commitSha = await findCommitByExactMessage(cwd, commitMessage);
|
|
2505
|
-
if (!commitSha) {
|
|
2506
|
-
setJson2(res, 404, { error: "No matching commit found for this user message" });
|
|
2507
|
-
return;
|
|
2508
|
-
}
|
|
2509
|
-
let resetTargetSha = "";
|
|
2510
|
-
try {
|
|
2511
|
-
resetTargetSha = await runCommandCapture("git", ["rev-parse", `${commitSha}^`], { cwd });
|
|
2512
|
-
} catch {
|
|
2513
|
-
setJson2(res, 409, { error: "Cannot rollback: matched commit has no parent commit" });
|
|
2514
|
-
return;
|
|
2515
|
-
}
|
|
2516
|
-
let stashed = false;
|
|
2517
|
-
if (await hasGitWorkingTreeChanges(cwd)) {
|
|
2518
|
-
const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
|
|
2519
|
-
await runCommand2("git", ["stash", "push", "-u", "-m", stashMessage], { cwd });
|
|
2520
|
-
stashed = true;
|
|
2521
|
-
}
|
|
2522
|
-
await runCommand2("git", ["reset", "--hard", resetTargetSha], { cwd });
|
|
2523
|
-
setJson2(res, 200, { data: { reset: true, commitSha, resetTargetSha, stashed } });
|
|
2524
|
-
} catch (error) {
|
|
2525
|
-
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to rollback worktree to user message commit") });
|
|
2526
|
-
}
|
|
2527
|
-
return;
|
|
2528
|
-
}
|
|
2529
2082
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
2530
2083
|
const payload = await readJsonBody(req);
|
|
2531
2084
|
const record = asRecord2(payload);
|
|
@@ -2603,7 +2156,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2603
2156
|
let index = 1;
|
|
2604
2157
|
while (index < 1e5) {
|
|
2605
2158
|
const candidateName = `New Project (${String(index)})`;
|
|
2606
|
-
const candidatePath =
|
|
2159
|
+
const candidatePath = join2(normalizedBasePath, candidateName);
|
|
2607
2160
|
try {
|
|
2608
2161
|
await stat2(candidatePath);
|
|
2609
2162
|
index += 1;
|
|
@@ -2647,7 +2200,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2647
2200
|
return;
|
|
2648
2201
|
}
|
|
2649
2202
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
2650
|
-
const cache = await
|
|
2203
|
+
const cache = await readThreadTitleCache();
|
|
2651
2204
|
setJson2(res, 200, { data: cache });
|
|
2652
2205
|
return;
|
|
2653
2206
|
}
|
|
@@ -2849,7 +2402,7 @@ function createAuthSession(password) {
|
|
|
2849
2402
|
}
|
|
2850
2403
|
|
|
2851
2404
|
// src/server/localBrowseUi.ts
|
|
2852
|
-
import { dirname, extname
|
|
2405
|
+
import { dirname, extname, join as join3 } from "path";
|
|
2853
2406
|
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
2854
2407
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2855
2408
|
".txt",
|
|
@@ -2880,7 +2433,7 @@ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2880
2433
|
".ps1"
|
|
2881
2434
|
]);
|
|
2882
2435
|
function languageForPath(pathValue) {
|
|
2883
|
-
const extension =
|
|
2436
|
+
const extension = extname(pathValue).toLowerCase();
|
|
2884
2437
|
switch (extension) {
|
|
2885
2438
|
case ".js":
|
|
2886
2439
|
return "javascript";
|
|
@@ -2941,7 +2494,7 @@ function decodeBrowsePath(rawPath) {
|
|
|
2941
2494
|
}
|
|
2942
2495
|
}
|
|
2943
2496
|
function isTextEditablePath(pathValue) {
|
|
2944
|
-
return TEXT_EDITABLE_EXTENSIONS.has(
|
|
2497
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
|
|
2945
2498
|
}
|
|
2946
2499
|
function looksLikeTextBuffer(buffer) {
|
|
2947
2500
|
if (buffer.length === 0) return true;
|
|
@@ -2987,7 +2540,7 @@ function escapeForInlineScriptString(value) {
|
|
|
2987
2540
|
async function getDirectoryItems(localPath) {
|
|
2988
2541
|
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
2989
2542
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
2990
|
-
const entryPath =
|
|
2543
|
+
const entryPath = join3(localPath, entry.name);
|
|
2991
2544
|
const entryStat = await stat3(entryPath);
|
|
2992
2545
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
2993
2546
|
return {
|
|
@@ -3011,9 +2564,9 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3011
2564
|
const rows = items.map((item) => {
|
|
3012
2565
|
const suffix = item.isDirectory ? "/" : "";
|
|
3013
2566
|
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
3014
|
-
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a
|
|
2567
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
3015
2568
|
}).join("\n");
|
|
3016
|
-
const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
|
|
2569
|
+
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
3017
2570
|
return `<!doctype html>
|
|
3018
2571
|
<html lang="en">
|
|
3019
2572
|
<head>
|
|
@@ -3027,27 +2580,8 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3027
2580
|
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
3028
2581
|
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
3029
2582
|
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
3030
|
-
.
|
|
3031
|
-
.header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
|
|
3032
|
-
.header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
|
|
3033
|
-
.header-open-btn {
|
|
3034
|
-
height: 42px;
|
|
3035
|
-
padding: 0 14px;
|
|
3036
|
-
border: 1px solid #4f8de0;
|
|
3037
|
-
border-radius: 10px;
|
|
3038
|
-
background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
|
|
3039
|
-
color: #eef6ff;
|
|
3040
|
-
font-weight: 700;
|
|
3041
|
-
letter-spacing: 0.01em;
|
|
3042
|
-
cursor: pointer;
|
|
3043
|
-
box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
|
|
3044
|
-
}
|
|
3045
|
-
.header-open-btn:hover { filter: brightness(1.08); }
|
|
3046
|
-
.header-open-btn:disabled { opacity: 0.6; cursor: default; }
|
|
3047
|
-
.row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
|
|
3048
|
-
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
|
|
2583
|
+
.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; }
|
|
3049
2584
|
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
3050
|
-
.status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
|
|
3051
2585
|
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
3052
2586
|
@media (max-width: 640px) {
|
|
3053
2587
|
body { margin: 12px; }
|
|
@@ -3059,46 +2593,8 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3059
2593
|
</head>
|
|
3060
2594
|
<body>
|
|
3061
2595
|
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
3062
|
-
|
|
3063
|
-
${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
|
|
3064
|
-
<button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
|
|
3065
|
-
</div>
|
|
3066
|
-
<p id="status" class="status"></p>
|
|
2596
|
+
${parentLink}
|
|
3067
2597
|
<ul>${rows}</ul>
|
|
3068
|
-
<script>
|
|
3069
|
-
const status = document.getElementById('status');
|
|
3070
|
-
document.addEventListener('click', async (event) => {
|
|
3071
|
-
const target = event.target;
|
|
3072
|
-
if (!(target instanceof Element)) return;
|
|
3073
|
-
const button = target.closest('.open-folder-btn');
|
|
3074
|
-
if (!(button instanceof HTMLButtonElement)) return;
|
|
3075
|
-
|
|
3076
|
-
const path = button.getAttribute('data-path') || '';
|
|
3077
|
-
if (!path) return;
|
|
3078
|
-
button.disabled = true;
|
|
3079
|
-
status.textContent = 'Opening folder in Codex...';
|
|
3080
|
-
try {
|
|
3081
|
-
const response = await fetch('/codex-api/project-root', {
|
|
3082
|
-
method: 'POST',
|
|
3083
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3084
|
-
body: JSON.stringify({
|
|
3085
|
-
path,
|
|
3086
|
-
createIfMissing: false,
|
|
3087
|
-
label: '',
|
|
3088
|
-
}),
|
|
3089
|
-
});
|
|
3090
|
-
if (!response.ok) {
|
|
3091
|
-
status.textContent = 'Failed to open folder.';
|
|
3092
|
-
button.disabled = false;
|
|
3093
|
-
return;
|
|
3094
|
-
}
|
|
3095
|
-
window.location.assign('/#/');
|
|
3096
|
-
} catch {
|
|
3097
|
-
status.textContent = 'Failed to open folder.';
|
|
3098
|
-
button.disabled = false;
|
|
3099
|
-
}
|
|
3100
|
-
});
|
|
3101
|
-
</script>
|
|
3102
2598
|
</body>
|
|
3103
2599
|
</html>`;
|
|
3104
2600
|
}
|
|
@@ -3174,8 +2670,8 @@ async function createTextEditorHtml(localPath) {
|
|
|
3174
2670
|
// src/server/httpServer.ts
|
|
3175
2671
|
import { WebSocketServer } from "ws";
|
|
3176
2672
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
3177
|
-
var distDir =
|
|
3178
|
-
var spaEntryFile =
|
|
2673
|
+
var distDir = join4(__dirname, "..", "dist");
|
|
2674
|
+
var spaEntryFile = join4(distDir, "index.html");
|
|
3179
2675
|
var IMAGE_CONTENT_TYPES = {
|
|
3180
2676
|
".avif": "image/avif",
|
|
3181
2677
|
".bmp": "image/bmp",
|
|
@@ -3232,7 +2728,7 @@ function createServer(options = {}) {
|
|
|
3232
2728
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
3233
2729
|
return;
|
|
3234
2730
|
}
|
|
3235
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
2731
|
+
const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
|
|
3236
2732
|
if (!contentType) {
|
|
3237
2733
|
res.status(415).json({ error: "Unsupported image type." });
|
|
3238
2734
|
return;
|
|
@@ -3319,7 +2815,7 @@ function createServer(options = {}) {
|
|
|
3319
2815
|
res.status(404).json({ error: "File not found." });
|
|
3320
2816
|
}
|
|
3321
2817
|
});
|
|
3322
|
-
const hasFrontendAssets =
|
|
2818
|
+
const hasFrontendAssets = existsSync2(spaEntryFile);
|
|
3323
2819
|
if (hasFrontendAssets) {
|
|
3324
2820
|
app.use(express.static(distDir));
|
|
3325
2821
|
}
|
|
@@ -3390,25 +2886,9 @@ function generatePassword() {
|
|
|
3390
2886
|
// src/cli/index.ts
|
|
3391
2887
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
3392
2888
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
3393
|
-
var hasPromptedCloudflaredInstall = false;
|
|
3394
|
-
function getCodexHomePath() {
|
|
3395
|
-
return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
|
|
3396
|
-
}
|
|
3397
|
-
function getCloudflaredPromptMarkerPath() {
|
|
3398
|
-
return join6(getCodexHomePath(), ".cloudflared-install-prompted");
|
|
3399
|
-
}
|
|
3400
|
-
function hasPromptedCloudflaredInstallPersisted() {
|
|
3401
|
-
return existsSync5(getCloudflaredPromptMarkerPath());
|
|
3402
|
-
}
|
|
3403
|
-
async function persistCloudflaredInstallPrompted() {
|
|
3404
|
-
const codexHome = getCodexHomePath();
|
|
3405
|
-
mkdirSync(codexHome, { recursive: true });
|
|
3406
|
-
await writeFile4(getCloudflaredPromptMarkerPath(), `${Date.now()}
|
|
3407
|
-
`, "utf8");
|
|
3408
|
-
}
|
|
3409
2889
|
async function readCliVersion() {
|
|
3410
2890
|
try {
|
|
3411
|
-
const packageJsonPath =
|
|
2891
|
+
const packageJsonPath = join5(__dirname2, "..", "package.json");
|
|
3412
2892
|
const raw = await readFile4(packageJsonPath, "utf8");
|
|
3413
2893
|
const parsed = JSON.parse(raw);
|
|
3414
2894
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
@@ -3420,25 +2900,46 @@ function isTermuxRuntime() {
|
|
|
3420
2900
|
return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
|
|
3421
2901
|
}
|
|
3422
2902
|
function canRun(command, args = []) {
|
|
3423
|
-
const result =
|
|
3424
|
-
return result;
|
|
2903
|
+
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
2904
|
+
return result.status === 0;
|
|
3425
2905
|
}
|
|
3426
2906
|
function runOrFail(command, args, label) {
|
|
3427
|
-
const result =
|
|
2907
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
3428
2908
|
if (result.status !== 0) {
|
|
3429
2909
|
throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
|
|
3430
2910
|
}
|
|
3431
2911
|
}
|
|
3432
2912
|
function runWithStatus(command, args) {
|
|
3433
|
-
const result =
|
|
2913
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
3434
2914
|
return result.status ?? -1;
|
|
3435
2915
|
}
|
|
2916
|
+
function getUserNpmPrefix() {
|
|
2917
|
+
return join5(homedir3(), ".npm-global");
|
|
2918
|
+
}
|
|
2919
|
+
function resolveCodexCommand() {
|
|
2920
|
+
if (canRun("codex", ["--version"])) {
|
|
2921
|
+
return "codex";
|
|
2922
|
+
}
|
|
2923
|
+
const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
|
|
2924
|
+
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2925
|
+
return userCandidate;
|
|
2926
|
+
}
|
|
2927
|
+
const prefix = process.env.PREFIX?.trim();
|
|
2928
|
+
if (!prefix) {
|
|
2929
|
+
return null;
|
|
2930
|
+
}
|
|
2931
|
+
const candidate = join5(prefix, "bin", "codex");
|
|
2932
|
+
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2933
|
+
return candidate;
|
|
2934
|
+
}
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
3436
2937
|
function resolveCloudflaredCommand() {
|
|
3437
2938
|
if (canRun("cloudflared", ["--version"])) {
|
|
3438
2939
|
return "cloudflared";
|
|
3439
2940
|
}
|
|
3440
|
-
const localCandidate =
|
|
3441
|
-
if (
|
|
2941
|
+
const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
|
|
2942
|
+
if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
3442
2943
|
return localCandidate;
|
|
3443
2944
|
}
|
|
3444
2945
|
return null;
|
|
@@ -3491,9 +2992,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
3491
2992
|
if (!mappedArch) {
|
|
3492
2993
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
3493
2994
|
}
|
|
3494
|
-
const userBinDir =
|
|
2995
|
+
const userBinDir = join5(homedir3(), ".local", "bin");
|
|
3495
2996
|
mkdirSync(userBinDir, { recursive: true });
|
|
3496
|
-
const destination =
|
|
2997
|
+
const destination = join5(userBinDir, "cloudflared");
|
|
3497
2998
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
3498
2999
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
3499
3000
|
await downloadFile(downloadUrl, destination);
|
|
@@ -3507,19 +3008,11 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
3507
3008
|
return installed;
|
|
3508
3009
|
}
|
|
3509
3010
|
async function shouldInstallCloudflaredInteractively() {
|
|
3510
|
-
if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
|
|
3511
|
-
return false;
|
|
3512
|
-
}
|
|
3513
|
-
hasPromptedCloudflaredInstall = true;
|
|
3514
|
-
await persistCloudflaredInstallPrompted();
|
|
3515
|
-
if (process.platform === "win32") {
|
|
3516
|
-
return false;
|
|
3517
|
-
}
|
|
3518
3011
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3519
3012
|
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
3520
3013
|
return false;
|
|
3521
3014
|
}
|
|
3522
|
-
const prompt =
|
|
3015
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
3523
3016
|
try {
|
|
3524
3017
|
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
3525
3018
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -3533,9 +3026,6 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3533
3026
|
if (current) {
|
|
3534
3027
|
return current;
|
|
3535
3028
|
}
|
|
3536
|
-
if (process.platform === "win32") {
|
|
3537
|
-
return null;
|
|
3538
|
-
}
|
|
3539
3029
|
const installApproved = await shouldInstallCloudflaredInteractively();
|
|
3540
3030
|
if (!installApproved) {
|
|
3541
3031
|
return null;
|
|
@@ -3543,8 +3033,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3543
3033
|
return ensureCloudflaredInstalledLinux();
|
|
3544
3034
|
}
|
|
3545
3035
|
function hasCodexAuth() {
|
|
3546
|
-
const codexHome =
|
|
3547
|
-
return
|
|
3036
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3037
|
+
return existsSync3(join5(codexHome, "auth.json"));
|
|
3548
3038
|
}
|
|
3549
3039
|
function ensureCodexInstalled() {
|
|
3550
3040
|
let codexCommand = resolveCodexCommand();
|
|
@@ -3562,7 +3052,7 @@ function ensureCodexInstalled() {
|
|
|
3562
3052
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
3563
3053
|
`);
|
|
3564
3054
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
3565
|
-
process.env.PATH = `${
|
|
3055
|
+
process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
3566
3056
|
};
|
|
3567
3057
|
if (isTermuxRuntime()) {
|
|
3568
3058
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -3625,22 +3115,19 @@ function parseCloudflaredUrl(chunk) {
|
|
|
3625
3115
|
}
|
|
3626
3116
|
function getAccessibleUrls(port) {
|
|
3627
3117
|
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3118
|
+
const interfaces = networkInterfaces();
|
|
3119
|
+
for (const entries of Object.values(interfaces)) {
|
|
3120
|
+
if (!entries) {
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
for (const entry of entries) {
|
|
3124
|
+
if (entry.internal) {
|
|
3632
3125
|
continue;
|
|
3633
3126
|
}
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
continue;
|
|
3637
|
-
}
|
|
3638
|
-
if (entry.family === "IPv4") {
|
|
3639
|
-
urls.add(`http://${entry.address}:${String(port)}`);
|
|
3640
|
-
}
|
|
3127
|
+
if (entry.family === "IPv4") {
|
|
3128
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
3641
3129
|
}
|
|
3642
3130
|
}
|
|
3643
|
-
} catch {
|
|
3644
3131
|
}
|
|
3645
3132
|
return Array.from(urls);
|
|
3646
3133
|
}
|
|
@@ -3703,8 +3190,8 @@ function listenWithFallback(server, startPort) {
|
|
|
3703
3190
|
});
|
|
3704
3191
|
}
|
|
3705
3192
|
function getCodexGlobalStatePath2() {
|
|
3706
|
-
const codexHome =
|
|
3707
|
-
return
|
|
3193
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3194
|
+
return join5(codexHome, ".codex-global-state.json");
|
|
3708
3195
|
}
|
|
3709
3196
|
function normalizeUniqueStrings(value) {
|
|
3710
3197
|
if (!Array.isArray(value)) return [];
|
|
@@ -3828,7 +3315,7 @@ async function startServer(options) {
|
|
|
3828
3315
|
qrcode.generate(tunnelUrl, { small: true });
|
|
3829
3316
|
console.log("");
|
|
3830
3317
|
}
|
|
3831
|
-
|
|
3318
|
+
openBrowser(`http://localhost:${String(port)}`);
|
|
3832
3319
|
function shutdown() {
|
|
3833
3320
|
console.log("\nShutting down...");
|
|
3834
3321
|
if (tunnelChild && !tunnelChild.killed) {
|
|
@@ -3851,7 +3338,7 @@ async function runLogin() {
|
|
|
3851
3338
|
console.log("\nStarting `codex login`...\n");
|
|
3852
3339
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
3853
3340
|
}
|
|
3854
|
-
program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").
|
|
3341
|
+
program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (projectPath, opts) => {
|
|
3855
3342
|
const rawArgv = process.argv.slice(2);
|
|
3856
3343
|
const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
|
|
3857
3344
|
let openProjectOnly = (opts.openProject ?? "").trim();
|