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-DuLxR4eK.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CB76aP9Z.css">
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 = randomBytes(32).toString("hex");
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, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
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));