codexapp 0.1.21 → 0.1.23
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/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Codex Web Local</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-oKlr-X4X.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BNrLRqWJ.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body class="bg-slate-950">
|
|
11
11
|
<div id="app"></div>
|
package/dist-cli/index.js
CHANGED
|
@@ -16,15 +16,17 @@ import qrcode from "qrcode-terminal";
|
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
17
|
import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
|
|
18
18
|
import { existsSync } from "fs";
|
|
19
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
19
20
|
import express from "express";
|
|
20
21
|
|
|
21
22
|
// src/server/codexAppServerBridge.ts
|
|
22
23
|
import { spawn } from "child_process";
|
|
24
|
+
import { randomBytes } from "crypto";
|
|
23
25
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
|
|
24
26
|
import { request as httpsRequest } from "https";
|
|
25
27
|
import { homedir } from "os";
|
|
26
28
|
import { tmpdir } from "os";
|
|
27
|
-
import { isAbsolute, join, resolve } from "path";
|
|
29
|
+
import { basename, isAbsolute, join, resolve } from "path";
|
|
28
30
|
import { writeFile } from "fs/promises";
|
|
29
31
|
function asRecord(value) {
|
|
30
32
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -121,6 +123,55 @@ async function runCommand(command, args, options = {}) {
|
|
|
121
123
|
});
|
|
122
124
|
});
|
|
123
125
|
}
|
|
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
|
+
}
|
|
124
175
|
async function detectUserSkillsDir(appServer) {
|
|
125
176
|
try {
|
|
126
177
|
const result = await appServer.rpc("skills/list", {});
|
|
@@ -908,6 +959,76 @@ function createCodexBridgeMiddleware() {
|
|
|
908
959
|
setJson(res, 200, { data: { path: homedir() } });
|
|
909
960
|
return;
|
|
910
961
|
}
|
|
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
|
+
}
|
|
911
1032
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
912
1033
|
const payload = await readJsonBody(req);
|
|
913
1034
|
const record = asRecord(payload);
|
|
@@ -1210,7 +1331,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1210
1331
|
}
|
|
1211
1332
|
|
|
1212
1333
|
// src/server/authMiddleware.ts
|
|
1213
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1334
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1214
1335
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1215
1336
|
function constantTimeCompare(a, b) {
|
|
1216
1337
|
const bufA = Buffer.from(a);
|
|
@@ -1308,7 +1429,7 @@ function createAuthSession(password) {
|
|
|
1308
1429
|
res.status(401).json({ error: "Invalid password" });
|
|
1309
1430
|
return;
|
|
1310
1431
|
}
|
|
1311
|
-
const token =
|
|
1432
|
+
const token = randomBytes2(32).toString("hex");
|
|
1312
1433
|
validTokens.add(token);
|
|
1313
1434
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
1314
1435
|
res.json({ ok: true });
|
|
@@ -1354,6 +1475,69 @@ function normalizeLocalImagePath(rawPath) {
|
|
|
1354
1475
|
}
|
|
1355
1476
|
return trimmed;
|
|
1356
1477
|
}
|
|
1478
|
+
function normalizeLocalPath(rawPath) {
|
|
1479
|
+
const trimmed = rawPath.trim();
|
|
1480
|
+
if (!trimmed) return "";
|
|
1481
|
+
if (trimmed.startsWith("file://")) {
|
|
1482
|
+
try {
|
|
1483
|
+
return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
|
|
1484
|
+
} catch {
|
|
1485
|
+
return trimmed.replace(/^file:\/\//u, "");
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return trimmed;
|
|
1489
|
+
}
|
|
1490
|
+
function decodeBrowsePath(rawPath) {
|
|
1491
|
+
if (!rawPath) return "";
|
|
1492
|
+
try {
|
|
1493
|
+
return decodeURIComponent(rawPath);
|
|
1494
|
+
} catch {
|
|
1495
|
+
return rawPath;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
function escapeHtml(value) {
|
|
1499
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
1500
|
+
}
|
|
1501
|
+
function toBrowseHref(pathValue) {
|
|
1502
|
+
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
1503
|
+
}
|
|
1504
|
+
async function renderDirectoryListing(res, localPath) {
|
|
1505
|
+
const entries = await readdir2(localPath, { withFileTypes: true });
|
|
1506
|
+
const sorted = entries.slice().sort((a, b) => {
|
|
1507
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
1508
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
1509
|
+
return a.name.localeCompare(b.name);
|
|
1510
|
+
});
|
|
1511
|
+
const parentPath = dirname(localPath);
|
|
1512
|
+
const rows = sorted.map((entry) => {
|
|
1513
|
+
const entryPath = join2(localPath, entry.name);
|
|
1514
|
+
const suffix = entry.isDirectory() ? "/" : "";
|
|
1515
|
+
return `<li><a href="${escapeHtml(toBrowseHref(entryPath))}">${escapeHtml(entry.name)}${suffix}</a></li>`;
|
|
1516
|
+
}).join("\n");
|
|
1517
|
+
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
1518
|
+
const html = `<!doctype html>
|
|
1519
|
+
<html lang="en">
|
|
1520
|
+
<head>
|
|
1521
|
+
<meta charset="utf-8" />
|
|
1522
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1523
|
+
<title>Index of ${escapeHtml(localPath)}</title>
|
|
1524
|
+
<style>
|
|
1525
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 24px; background: #0b1020; color: #dbe6ff; }
|
|
1526
|
+
a { color: #8cc2ff; text-decoration: none; }
|
|
1527
|
+
a:hover { text-decoration: underline; }
|
|
1528
|
+
ul { list-style: none; padding: 0; margin: 12px 0 0; }
|
|
1529
|
+
li { padding: 3px 0; }
|
|
1530
|
+
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
1531
|
+
</style>
|
|
1532
|
+
</head>
|
|
1533
|
+
<body>
|
|
1534
|
+
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
1535
|
+
${parentLink}
|
|
1536
|
+
<ul>${rows}</ul>
|
|
1537
|
+
</body>
|
|
1538
|
+
</html>`;
|
|
1539
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1540
|
+
}
|
|
1357
1541
|
function createServer(options = {}) {
|
|
1358
1542
|
const app = express();
|
|
1359
1543
|
const bridge = createCodexBridgeMiddleware();
|
|
@@ -1381,6 +1565,42 @@ function createServer(options = {}) {
|
|
|
1381
1565
|
if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
|
|
1382
1566
|
});
|
|
1383
1567
|
});
|
|
1568
|
+
app.get("/codex-local-file", (req, res) => {
|
|
1569
|
+
const rawPath = typeof req.query.path === "string" ? req.query.path : "";
|
|
1570
|
+
const localPath = normalizeLocalPath(rawPath);
|
|
1571
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
1572
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
1576
|
+
res.setHeader("Content-Disposition", "inline");
|
|
1577
|
+
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
1578
|
+
if (!error) return;
|
|
1579
|
+
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
1583
|
+
const rawPath = typeof req.params.path === "string" ? req.params.path : "";
|
|
1584
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1585
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
1586
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
const fileStat = await stat2(localPath);
|
|
1591
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
1592
|
+
if (fileStat.isDirectory()) {
|
|
1593
|
+
await renderDirectoryListing(res, localPath);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
1597
|
+
if (!error) return;
|
|
1598
|
+
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
1599
|
+
});
|
|
1600
|
+
} catch {
|
|
1601
|
+
res.status(404).json({ error: "File not found." });
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1384
1604
|
const hasFrontendAssets = existsSync(spaEntryFile);
|
|
1385
1605
|
if (hasFrontendAssets) {
|
|
1386
1606
|
app.use(express.static(distDir));
|