codexapp 0.1.24 → 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/assets/index-Bmlv0aWl.js +48 -0
- package/dist/assets/index-lTEk-dqp.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +273 -179
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/index-BNrLRqWJ.css +0 -1
- package/dist/assets/index-oKlr-X4X.js +0 -48
package/dist-cli/index.js
CHANGED
|
@@ -3,30 +3,29 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
5
|
import { existsSync as existsSync2 } from "fs";
|
|
6
|
-
import { readFile as
|
|
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
|
|
11
|
+
import { dirname as dirname3 } from "path";
|
|
12
12
|
import { Command } from "commander";
|
|
13
13
|
import qrcode from "qrcode-terminal";
|
|
14
14
|
|
|
15
15
|
// src/server/httpServer.ts
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
|
-
import { dirname, extname, isAbsolute as isAbsolute2, join as
|
|
17
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
18
18
|
import { existsSync } from "fs";
|
|
19
|
-
import {
|
|
19
|
+
import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
|
|
20
20
|
import express from "express";
|
|
21
21
|
|
|
22
22
|
// src/server/codexAppServerBridge.ts
|
|
23
23
|
import { spawn } from "child_process";
|
|
24
|
-
import { randomBytes } from "crypto";
|
|
25
24
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
|
|
26
25
|
import { request as httpsRequest } from "https";
|
|
27
26
|
import { homedir } from "os";
|
|
28
27
|
import { tmpdir } from "os";
|
|
29
|
-
import {
|
|
28
|
+
import { isAbsolute, join, resolve } from "path";
|
|
30
29
|
import { writeFile } from "fs/promises";
|
|
31
30
|
function asRecord(value) {
|
|
32
31
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -123,55 +122,6 @@ async function runCommand(command, args, options = {}) {
|
|
|
123
122
|
});
|
|
124
123
|
});
|
|
125
124
|
}
|
|
126
|
-
function isMissingHeadError(error) {
|
|
127
|
-
const message = getErrorMessage(error, "").toLowerCase();
|
|
128
|
-
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head");
|
|
129
|
-
}
|
|
130
|
-
function isNotGitRepositoryError(error) {
|
|
131
|
-
const message = getErrorMessage(error, "").toLowerCase();
|
|
132
|
-
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
133
|
-
}
|
|
134
|
-
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
135
|
-
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
136
|
-
try {
|
|
137
|
-
await stat(agentsPath);
|
|
138
|
-
} catch {
|
|
139
|
-
await writeFile(agentsPath, "", "utf8");
|
|
140
|
-
}
|
|
141
|
-
await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
142
|
-
await runCommand(
|
|
143
|
-
"git",
|
|
144
|
-
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
145
|
-
{ cwd: repoRoot }
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
async function runCommandCapture(command, args, options = {}) {
|
|
149
|
-
return await new Promise((resolveOutput, reject) => {
|
|
150
|
-
const proc = spawn(command, args, {
|
|
151
|
-
cwd: options.cwd,
|
|
152
|
-
env: process.env,
|
|
153
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
154
|
-
});
|
|
155
|
-
let stdout = "";
|
|
156
|
-
let stderr = "";
|
|
157
|
-
proc.stdout.on("data", (chunk) => {
|
|
158
|
-
stdout += chunk.toString();
|
|
159
|
-
});
|
|
160
|
-
proc.stderr.on("data", (chunk) => {
|
|
161
|
-
stderr += chunk.toString();
|
|
162
|
-
});
|
|
163
|
-
proc.on("error", reject);
|
|
164
|
-
proc.on("close", (code) => {
|
|
165
|
-
if (code === 0) {
|
|
166
|
-
resolveOutput(stdout.trim());
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
170
|
-
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
171
|
-
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
125
|
async function detectUserSkillsDir(appServer) {
|
|
176
126
|
try {
|
|
177
127
|
const result = await appServer.rpc("skills/list", {});
|
|
@@ -959,76 +909,6 @@ function createCodexBridgeMiddleware() {
|
|
|
959
909
|
setJson(res, 200, { data: { path: homedir() } });
|
|
960
910
|
return;
|
|
961
911
|
}
|
|
962
|
-
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
963
|
-
const payload = asRecord(await readJsonBody(req));
|
|
964
|
-
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
965
|
-
if (!rawSourceCwd) {
|
|
966
|
-
setJson(res, 400, { error: "Missing sourceCwd" });
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
970
|
-
try {
|
|
971
|
-
const sourceInfo = await stat(sourceCwd);
|
|
972
|
-
if (!sourceInfo.isDirectory()) {
|
|
973
|
-
setJson(res, 400, { error: "sourceCwd is not a directory" });
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
} catch {
|
|
977
|
-
setJson(res, 404, { error: "sourceCwd does not exist" });
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
try {
|
|
981
|
-
let gitRoot = "";
|
|
982
|
-
try {
|
|
983
|
-
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
984
|
-
} catch (error) {
|
|
985
|
-
if (!isNotGitRepositoryError(error)) throw error;
|
|
986
|
-
await runCommand("git", ["init"], { cwd: sourceCwd });
|
|
987
|
-
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
988
|
-
}
|
|
989
|
-
const repoName = basename(gitRoot) || "repo";
|
|
990
|
-
const worktreesRoot = join(getCodexHomeDir(), "worktrees");
|
|
991
|
-
await mkdir(worktreesRoot, { recursive: true });
|
|
992
|
-
let worktreeId = "";
|
|
993
|
-
let worktreeParent = "";
|
|
994
|
-
let worktreeCwd = "";
|
|
995
|
-
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
996
|
-
const candidate = randomBytes(2).toString("hex");
|
|
997
|
-
const parent = join(worktreesRoot, candidate);
|
|
998
|
-
try {
|
|
999
|
-
await stat(parent);
|
|
1000
|
-
continue;
|
|
1001
|
-
} catch {
|
|
1002
|
-
worktreeId = candidate;
|
|
1003
|
-
worktreeParent = parent;
|
|
1004
|
-
worktreeCwd = join(parent, repoName);
|
|
1005
|
-
break;
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
if (!worktreeId || !worktreeParent || !worktreeCwd) {
|
|
1009
|
-
throw new Error("Failed to allocate a unique worktree id");
|
|
1010
|
-
}
|
|
1011
|
-
const branch = `codex/${worktreeId}`;
|
|
1012
|
-
await mkdir(worktreeParent, { recursive: true });
|
|
1013
|
-
try {
|
|
1014
|
-
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1015
|
-
} catch (error) {
|
|
1016
|
-
if (!isMissingHeadError(error)) throw error;
|
|
1017
|
-
await ensureRepoHasInitialCommit(gitRoot);
|
|
1018
|
-
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1019
|
-
}
|
|
1020
|
-
setJson(res, 200, {
|
|
1021
|
-
data: {
|
|
1022
|
-
cwd: worktreeCwd,
|
|
1023
|
-
branch,
|
|
1024
|
-
gitRoot
|
|
1025
|
-
}
|
|
1026
|
-
});
|
|
1027
|
-
} catch (error) {
|
|
1028
|
-
setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
|
|
1029
|
-
}
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
912
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1033
913
|
const payload = await readJsonBody(req);
|
|
1034
914
|
const record = asRecord(payload);
|
|
@@ -1331,7 +1211,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1331
1211
|
}
|
|
1332
1212
|
|
|
1333
1213
|
// src/server/authMiddleware.ts
|
|
1334
|
-
import { randomBytes
|
|
1214
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1335
1215
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1336
1216
|
function constantTimeCompare(a, b) {
|
|
1337
1217
|
const bufA = Buffer.from(a);
|
|
@@ -1429,7 +1309,7 @@ function createAuthSession(password) {
|
|
|
1429
1309
|
res.status(401).json({ error: "Invalid password" });
|
|
1430
1310
|
return;
|
|
1431
1311
|
}
|
|
1432
|
-
const token =
|
|
1312
|
+
const token = randomBytes(32).toString("hex");
|
|
1433
1313
|
validTokens.add(token);
|
|
1434
1314
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
1435
1315
|
res.json({ ok: true });
|
|
@@ -1448,32 +1328,74 @@ function createAuthSession(password) {
|
|
|
1448
1328
|
};
|
|
1449
1329
|
}
|
|
1450
1330
|
|
|
1451
|
-
// src/server/
|
|
1452
|
-
import {
|
|
1453
|
-
|
|
1454
|
-
var
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
".
|
|
1458
|
-
".
|
|
1459
|
-
".
|
|
1460
|
-
".
|
|
1461
|
-
".
|
|
1462
|
-
".
|
|
1463
|
-
".
|
|
1464
|
-
".
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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";
|
|
1475
1398
|
}
|
|
1476
|
-
return trimmed;
|
|
1477
1399
|
}
|
|
1478
1400
|
function normalizeLocalPath(rawPath) {
|
|
1479
1401
|
const trimmed = rawPath.trim();
|
|
@@ -1495,39 +1417,72 @@ function decodeBrowsePath(rawPath) {
|
|
|
1495
1417
|
return rawPath;
|
|
1496
1418
|
}
|
|
1497
1419
|
}
|
|
1420
|
+
function isTextEditablePath(pathValue) {
|
|
1421
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
|
|
1422
|
+
}
|
|
1498
1423
|
function escapeHtml(value) {
|
|
1499
1424
|
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
1500
1425
|
}
|
|
1501
1426
|
function toBrowseHref(pathValue) {
|
|
1502
1427
|
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
1503
1428
|
}
|
|
1504
|
-
|
|
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) {
|
|
1505
1436
|
const entries = await readdir2(localPath, { withFileTypes: true });
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
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;
|
|
1509
1452
|
return a.name.localeCompare(b.name);
|
|
1510
1453
|
});
|
|
1454
|
+
}
|
|
1455
|
+
async function createDirectoryListingHtml(localPath) {
|
|
1456
|
+
const items = await getDirectoryItems(localPath);
|
|
1511
1457
|
const parentPath = dirname(localPath);
|
|
1512
|
-
const rows =
|
|
1513
|
-
const
|
|
1514
|
-
const
|
|
1515
|
-
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>`;
|
|
1516
1462
|
}).join("\n");
|
|
1517
1463
|
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
1518
|
-
|
|
1464
|
+
return `<!doctype html>
|
|
1519
1465
|
<html lang="en">
|
|
1520
1466
|
<head>
|
|
1521
1467
|
<meta charset="utf-8" />
|
|
1522
1468
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1523
1469
|
<title>Index of ${escapeHtml(localPath)}</title>
|
|
1524
1470
|
<style>
|
|
1525
|
-
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin:
|
|
1471
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
|
|
1526
1472
|
a { color: #8cc2ff; text-decoration: none; }
|
|
1527
1473
|
a:hover { text-decoration: underline; }
|
|
1528
|
-
ul { list-style: none; padding: 0; margin: 12px 0 0; }
|
|
1529
|
-
|
|
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; }
|
|
1530
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
|
+
}
|
|
1531
1486
|
</style>
|
|
1532
1487
|
</head>
|
|
1533
1488
|
<body>
|
|
@@ -1536,7 +1491,107 @@ async function renderDirectoryListing(res, localPath) {
|
|
|
1536
1491
|
<ul>${rows}</ul>
|
|
1537
1492
|
</body>
|
|
1538
1493
|
</html>`;
|
|
1539
|
-
|
|
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 "";
|
|
1540
1595
|
}
|
|
1541
1596
|
function createServer(options = {}) {
|
|
1542
1597
|
const app = express();
|
|
@@ -1553,7 +1608,7 @@ function createServer(options = {}) {
|
|
|
1553
1608
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1554
1609
|
return;
|
|
1555
1610
|
}
|
|
1556
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
1611
|
+
const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
|
|
1557
1612
|
if (!contentType) {
|
|
1558
1613
|
res.status(415).json({ error: "Unsupported image type." });
|
|
1559
1614
|
return;
|
|
@@ -1580,17 +1635,18 @@ function createServer(options = {}) {
|
|
|
1580
1635
|
});
|
|
1581
1636
|
});
|
|
1582
1637
|
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
1583
|
-
const rawPath =
|
|
1638
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
1584
1639
|
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1585
1640
|
if (!localPath || !isAbsolute2(localPath)) {
|
|
1586
1641
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1587
1642
|
return;
|
|
1588
1643
|
}
|
|
1589
1644
|
try {
|
|
1590
|
-
const fileStat = await
|
|
1645
|
+
const fileStat = await stat3(localPath);
|
|
1591
1646
|
res.setHeader("Cache-Control", "private, no-store");
|
|
1592
1647
|
if (fileStat.isDirectory()) {
|
|
1593
|
-
await
|
|
1648
|
+
const html = await createDirectoryListingHtml(localPath);
|
|
1649
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1594
1650
|
return;
|
|
1595
1651
|
}
|
|
1596
1652
|
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
@@ -1601,6 +1657,44 @@ function createServer(options = {}) {
|
|
|
1601
1657
|
res.status(404).json({ error: "File not found." });
|
|
1602
1658
|
}
|
|
1603
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
|
+
});
|
|
1604
1698
|
const hasFrontendAssets = existsSync(spaEntryFile);
|
|
1605
1699
|
if (hasFrontendAssets) {
|
|
1606
1700
|
app.use(express.static(distDir));
|
|
@@ -1672,11 +1766,11 @@ function generatePassword() {
|
|
|
1672
1766
|
|
|
1673
1767
|
// src/cli/index.ts
|
|
1674
1768
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
1675
|
-
var __dirname2 =
|
|
1769
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
1676
1770
|
async function readCliVersion() {
|
|
1677
1771
|
try {
|
|
1678
|
-
const packageJsonPath =
|
|
1679
|
-
const raw = await
|
|
1772
|
+
const packageJsonPath = join4(__dirname2, "..", "package.json");
|
|
1773
|
+
const raw = await readFile3(packageJsonPath, "utf8");
|
|
1680
1774
|
const parsed = JSON.parse(raw);
|
|
1681
1775
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
1682
1776
|
} catch {
|
|
@@ -1701,13 +1795,13 @@ function runWithStatus(command, args) {
|
|
|
1701
1795
|
return result.status ?? -1;
|
|
1702
1796
|
}
|
|
1703
1797
|
function getUserNpmPrefix() {
|
|
1704
|
-
return
|
|
1798
|
+
return join4(homedir2(), ".npm-global");
|
|
1705
1799
|
}
|
|
1706
1800
|
function resolveCodexCommand() {
|
|
1707
1801
|
if (canRun("codex", ["--version"])) {
|
|
1708
1802
|
return "codex";
|
|
1709
1803
|
}
|
|
1710
|
-
const userCandidate =
|
|
1804
|
+
const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
|
|
1711
1805
|
if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
1712
1806
|
return userCandidate;
|
|
1713
1807
|
}
|
|
@@ -1715,15 +1809,15 @@ function resolveCodexCommand() {
|
|
|
1715
1809
|
if (!prefix) {
|
|
1716
1810
|
return null;
|
|
1717
1811
|
}
|
|
1718
|
-
const candidate =
|
|
1812
|
+
const candidate = join4(prefix, "bin", "codex");
|
|
1719
1813
|
if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
|
|
1720
1814
|
return candidate;
|
|
1721
1815
|
}
|
|
1722
1816
|
return null;
|
|
1723
1817
|
}
|
|
1724
1818
|
function hasCodexAuth() {
|
|
1725
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
1726
|
-
return existsSync2(
|
|
1819
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
|
|
1820
|
+
return existsSync2(join4(codexHome, "auth.json"));
|
|
1727
1821
|
}
|
|
1728
1822
|
function ensureCodexInstalled() {
|
|
1729
1823
|
let codexCommand = resolveCodexCommand();
|
|
@@ -1741,7 +1835,7 @@ function ensureCodexInstalled() {
|
|
|
1741
1835
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
1742
1836
|
`);
|
|
1743
1837
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
1744
|
-
process.env.PATH = `${
|
|
1838
|
+
process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
1745
1839
|
};
|
|
1746
1840
|
if (isTermuxRuntime()) {
|
|
1747
1841
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|