codexapp 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cli/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { chmodSync, createWriteStream, existsSync as existsSync4, mkdirSync } from "fs";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync } from "fs";
6
6
  import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
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";
7
+ import { homedir as homedir4, networkInterfaces } from "os";
8
+ import { isAbsolute as isAbsolute3, join as join6, resolve as resolve2 } from "path";
9
+ import { spawn as spawn3 } from "child_process";
10
10
  import { createInterface as createInterface2 } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
12
  import { dirname as dirname3 } from "path";
@@ -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 extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
20
- import { existsSync as existsSync3 } from "fs";
19
+ import { dirname as dirname2, extname as extname3, isAbsolute as isAbsolute2, join as join5 } from "path";
20
+ import { existsSync as existsSync4 } from "fs";
21
21
  import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
22
22
  import express from "express";
23
23
 
@@ -28,9 +28,9 @@ import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as st
28
28
  import { createReadStream } from "fs";
29
29
  import { request as httpRequest } from "http";
30
30
  import { request as httpsRequest } from "https";
31
- import { homedir as homedir2 } from "os";
31
+ import { homedir as homedir3 } from "os";
32
32
  import { tmpdir as tmpdir2 } from "os";
33
- import { basename, isAbsolute, join as join2, resolve } from "path";
33
+ import { basename as basename2, isAbsolute, join as join3, resolve } from "path";
34
34
  import { createInterface } from "readline";
35
35
  import { writeFile as writeFile2 } from "fs/promises";
36
36
 
@@ -1120,6 +1120,84 @@ async function handleSkillsRoutes(req, res, url, context) {
1120
1120
  return false;
1121
1121
  }
1122
1122
 
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
+
1123
1201
  // src/server/codexAppServerBridge.ts
1124
1202
  function asRecord2(value) {
1125
1203
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -1195,6 +1273,53 @@ function scoreFileCandidate(path, query) {
1195
1273
  if (lowerPath.includes(lowerQuery)) return 4;
1196
1274
  return 10;
1197
1275
  }
1276
+ function decodeHtmlEntities(value) {
1277
+ return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/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 hrefMatch = row.match(/href="\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)"/);
1288
+ if (!hrefMatch) continue;
1289
+ const fullName = hrefMatch[1] ?? "";
1290
+ if (!fullName || items.some((item) => item.fullName === fullName)) continue;
1291
+ const descriptionMatch = row.match(/<p[^>]*>([\s\S]*?)<\/p>/);
1292
+ const languageMatch = row.match(/programmingLanguage[^>]*>\s*([\s\S]*?)\s*<\/span>/);
1293
+ const starsMatch = row.match(/href="\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/stargazers"[\s\S]*?>([\s\S]*?)<\/a>/);
1294
+ const starsText = stripHtml(starsMatch?.[1] ?? "").replace(/,/g, "");
1295
+ const stars = Number.parseInt(starsText, 10);
1296
+ items.push({
1297
+ id: seq,
1298
+ fullName,
1299
+ url: `https://github.com/${fullName}`,
1300
+ description: stripHtml(descriptionMatch?.[1] ?? ""),
1301
+ language: stripHtml(languageMatch?.[1] ?? ""),
1302
+ stars: Number.isFinite(stars) ? stars : 0
1303
+ });
1304
+ seq += 1;
1305
+ if (items.length >= limit) break;
1306
+ }
1307
+ return items;
1308
+ }
1309
+ async function fetchGithubTrending(since, limit) {
1310
+ const endpoint = `https://github.com/trending?since=${since}`;
1311
+ const response = await fetch(endpoint, {
1312
+ headers: {
1313
+ "User-Agent": "codex-web-local",
1314
+ Accept: "text/html"
1315
+ }
1316
+ });
1317
+ if (!response.ok) {
1318
+ throw new Error(`GitHub trending fetch failed (${response.status})`);
1319
+ }
1320
+ const html = await response.text();
1321
+ return parseGithubTrendingHtml(html, limit);
1322
+ }
1198
1323
  async function listFilesWithRipgrep(cwd) {
1199
1324
  return await new Promise((resolve3, reject) => {
1200
1325
  const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
@@ -1224,7 +1349,7 @@ async function listFilesWithRipgrep(cwd) {
1224
1349
  }
1225
1350
  function getCodexHomeDir2() {
1226
1351
  const codexHome = process.env.CODEX_HOME?.trim();
1227
- return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
1352
+ return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
1228
1353
  }
1229
1354
  async function runCommand2(command, args, options = {}) {
1230
1355
  await new Promise((resolve3, reject) => {
@@ -1262,7 +1387,7 @@ function isNotGitRepositoryError(error) {
1262
1387
  return message.includes("not a git repository") || message.includes("fatal: not a git repository");
1263
1388
  }
1264
1389
  async function ensureRepoHasInitialCommit(repoRoot) {
1265
- const agentsPath = join2(repoRoot, "AGENTS.md");
1390
+ const agentsPath = join3(repoRoot, "AGENTS.md");
1266
1391
  try {
1267
1392
  await stat2(agentsPath);
1268
1393
  } catch {
@@ -1302,6 +1427,33 @@ async function runCommandCapture(command, args, options = {}) {
1302
1427
  });
1303
1428
  });
1304
1429
  }
1430
+ async function runCommandWithOutput2(command, args, options = {}) {
1431
+ return await new Promise((resolve3, reject) => {
1432
+ const proc = spawn2(command, args, {
1433
+ cwd: options.cwd,
1434
+ env: process.env,
1435
+ stdio: ["ignore", "pipe", "pipe"]
1436
+ });
1437
+ let stdout = "";
1438
+ let stderr = "";
1439
+ proc.stdout.on("data", (chunk) => {
1440
+ stdout += chunk.toString();
1441
+ });
1442
+ proc.stderr.on("data", (chunk) => {
1443
+ stderr += chunk.toString();
1444
+ });
1445
+ proc.on("error", reject);
1446
+ proc.on("close", (code) => {
1447
+ if (code === 0) {
1448
+ resolve3(stdout.trim());
1449
+ return;
1450
+ }
1451
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
1452
+ const suffix = details.length > 0 ? `: ${details}` : "";
1453
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
1454
+ });
1455
+ });
1456
+ }
1305
1457
  function normalizeStringArray(value) {
1306
1458
  if (!Array.isArray(value)) return [];
1307
1459
  const normalized = [];
@@ -1322,8 +1474,32 @@ function normalizeStringRecord(value) {
1322
1474
  }
1323
1475
  return next;
1324
1476
  }
1477
+ function normalizeCommitMessage(value) {
1478
+ if (typeof value !== "string") return "";
1479
+ const normalized = value.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n").trim();
1480
+ return normalized.slice(0, 2e3);
1481
+ }
1482
+ async function hasGitWorkingTreeChanges(cwd) {
1483
+ const status = await runCommandWithOutput2("git", ["status", "--porcelain"], { cwd });
1484
+ return status.trim().length > 0;
1485
+ }
1486
+ async function findCommitByExactMessage(cwd, message) {
1487
+ const normalizedTarget = normalizeCommitMessage(message);
1488
+ if (!normalizedTarget) return "";
1489
+ const raw = await runCommandWithOutput2("git", ["log", "--format=%H%x1f%B%x1e"], { cwd });
1490
+ const entries = raw.split("");
1491
+ for (const entry of entries) {
1492
+ if (!entry.trim()) continue;
1493
+ const [shaRaw, bodyRaw] = entry.split("");
1494
+ const sha = (shaRaw ?? "").trim();
1495
+ const body = normalizeCommitMessage(bodyRaw ?? "");
1496
+ if (!sha) continue;
1497
+ if (body === normalizedTarget) return sha;
1498
+ }
1499
+ return "";
1500
+ }
1325
1501
  function getCodexAuthPath() {
1326
- return join2(getCodexHomeDir2(), "auth.json");
1502
+ return join3(getCodexHomeDir2(), "auth.json");
1327
1503
  }
1328
1504
  async function readCodexAuth() {
1329
1505
  try {
@@ -1337,10 +1513,10 @@ async function readCodexAuth() {
1337
1513
  }
1338
1514
  }
1339
1515
  function getCodexGlobalStatePath() {
1340
- return join2(getCodexHomeDir2(), ".codex-global-state.json");
1516
+ return join3(getCodexHomeDir2(), ".codex-global-state.json");
1341
1517
  }
1342
1518
  function getCodexSessionIndexPath() {
1343
- return join2(getCodexHomeDir2(), "session_index.jsonl");
1519
+ return join3(getCodexHomeDir2(), "session_index.jsonl");
1344
1520
  }
1345
1521
  var MAX_THREAD_TITLES = 500;
1346
1522
  var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
@@ -1599,10 +1775,10 @@ function handleFileUpload(req, res) {
1599
1775
  setJson2(res, 400, { error: "No file in request" });
1600
1776
  return;
1601
1777
  }
1602
- const uploadDir = join2(tmpdir2(), "codex-web-uploads");
1778
+ const uploadDir = join3(tmpdir2(), "codex-web-uploads");
1603
1779
  await mkdir2(uploadDir, { recursive: true });
1604
- const destDir = await mkdtemp2(join2(uploadDir, "f-"));
1605
- const destPath = join2(destDir, fileName);
1780
+ const destDir = await mkdtemp2(join3(uploadDir, "f-"));
1781
+ const destPath = join3(destDir, fileName);
1606
1782
  await writeFile2(destPath, fileData);
1607
1783
  setJson2(res, 200, { path: destPath });
1608
1784
  } catch (err) {
@@ -1709,7 +1885,8 @@ var AppServerProcess = class {
1709
1885
  start() {
1710
1886
  if (this.process) return;
1711
1887
  this.stopping = false;
1712
- const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
1888
+ const invocation = getSpawnInvocation(resolveCodexCommand() ?? "codex", this.appServerArgs);
1889
+ const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
1713
1890
  this.process = proc;
1714
1891
  proc.stdout.setEncoding("utf8");
1715
1892
  proc.stdout.on("data", (chunk) => {
@@ -1935,7 +2112,8 @@ var MethodCatalog = class {
1935
2112
  }
1936
2113
  async runGenerateSchemaCommand(outDir) {
1937
2114
  await new Promise((resolve3, reject) => {
1938
- const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
2115
+ const invocation = getSpawnInvocation(resolveCodexCommand() ?? "codex", ["app-server", "generate-json-schema", "--out", outDir]);
2116
+ const process2 = spawn2(invocation.command, invocation.args, {
1939
2117
  stdio: ["ignore", "ignore", "pipe"]
1940
2118
  });
1941
2119
  let stderr = "";
@@ -1991,9 +2169,9 @@ var MethodCatalog = class {
1991
2169
  if (this.methodCache) {
1992
2170
  return this.methodCache;
1993
2171
  }
1994
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2172
+ const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
1995
2173
  await this.runGenerateSchemaCommand(outDir);
1996
- const clientRequestPath = join2(outDir, "ClientRequest.json");
2174
+ const clientRequestPath = join3(outDir, "ClientRequest.json");
1997
2175
  const raw = await readFile2(clientRequestPath, "utf8");
1998
2176
  const parsed = JSON.parse(raw);
1999
2177
  const methods = this.extractMethodsFromClientRequest(parsed);
@@ -2004,9 +2182,9 @@ var MethodCatalog = class {
2004
2182
  if (this.notificationCache) {
2005
2183
  return this.notificationCache;
2006
2184
  }
2007
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2185
+ const outDir = await mkdtemp2(join3(tmpdir2(), "codex-web-local-schema-"));
2008
2186
  await this.runGenerateSchemaCommand(outDir);
2009
- const serverNotificationPath = join2(outDir, "ServerNotification.json");
2187
+ const serverNotificationPath = join3(outDir, "ServerNotification.json");
2010
2188
  const raw = await readFile2(serverNotificationPath, "utf8");
2011
2189
  const parsed = JSON.parse(raw);
2012
2190
  const methods = this.extractMethodsFromServerNotification(parsed);
@@ -2168,7 +2346,20 @@ function createCodexBridgeMiddleware() {
2168
2346
  return;
2169
2347
  }
2170
2348
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
2171
- setJson2(res, 200, { data: { path: homedir2() } });
2349
+ setJson2(res, 200, { data: { path: homedir3() } });
2350
+ return;
2351
+ }
2352
+ if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
2353
+ const sinceRaw = (url.searchParams.get("since") ?? "").trim().toLowerCase();
2354
+ const since = sinceRaw === "weekly" ? "weekly" : sinceRaw === "monthly" ? "monthly" : "daily";
2355
+ const limitRaw = Number.parseInt((url.searchParams.get("limit") ?? "6").trim(), 10);
2356
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(10, limitRaw)) : 6;
2357
+ try {
2358
+ const data = await fetchGithubTrending(since, limit);
2359
+ setJson2(res, 200, { data });
2360
+ } catch (error) {
2361
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to fetch GitHub trending") });
2362
+ }
2172
2363
  return;
2173
2364
  }
2174
2365
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
@@ -2198,22 +2389,22 @@ function createCodexBridgeMiddleware() {
2198
2389
  await runCommand2("git", ["init"], { cwd: sourceCwd });
2199
2390
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
2200
2391
  }
2201
- const repoName = basename(gitRoot) || "repo";
2202
- const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
2392
+ const repoName = basename2(gitRoot) || "repo";
2393
+ const worktreesRoot = join3(getCodexHomeDir2(), "worktrees");
2203
2394
  await mkdir2(worktreesRoot, { recursive: true });
2204
2395
  let worktreeId = "";
2205
2396
  let worktreeParent = "";
2206
2397
  let worktreeCwd = "";
2207
2398
  for (let attempt = 0; attempt < 12; attempt += 1) {
2208
2399
  const candidate = randomBytes(2).toString("hex");
2209
- const parent = join2(worktreesRoot, candidate);
2400
+ const parent = join3(worktreesRoot, candidate);
2210
2401
  try {
2211
2402
  await stat2(parent);
2212
2403
  continue;
2213
2404
  } catch {
2214
2405
  worktreeId = candidate;
2215
2406
  worktreeParent = parent;
2216
- worktreeCwd = join2(parent, repoName);
2407
+ worktreeCwd = join3(parent, repoName);
2217
2408
  break;
2218
2409
  }
2219
2410
  }
@@ -2241,6 +2432,99 @@ function createCodexBridgeMiddleware() {
2241
2432
  }
2242
2433
  return;
2243
2434
  }
2435
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/auto-commit") {
2436
+ const payload = asRecord2(await readJsonBody(req));
2437
+ const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
2438
+ const commitMessage = normalizeCommitMessage(payload?.message);
2439
+ if (!rawCwd) {
2440
+ setJson2(res, 400, { error: "Missing cwd" });
2441
+ return;
2442
+ }
2443
+ if (!commitMessage) {
2444
+ setJson2(res, 400, { error: "Missing message" });
2445
+ return;
2446
+ }
2447
+ const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
2448
+ try {
2449
+ const cwdInfo = await stat2(cwd);
2450
+ if (!cwdInfo.isDirectory()) {
2451
+ setJson2(res, 400, { error: "cwd is not a directory" });
2452
+ return;
2453
+ }
2454
+ } catch {
2455
+ setJson2(res, 404, { error: "cwd does not exist" });
2456
+ return;
2457
+ }
2458
+ try {
2459
+ await runCommandCapture("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
2460
+ const beforeStatus = await runCommandWithOutput2("git", ["status", "--porcelain"], { cwd });
2461
+ if (!beforeStatus.trim()) {
2462
+ setJson2(res, 200, { data: { committed: false } });
2463
+ return;
2464
+ }
2465
+ await runCommand2("git", ["add", "-A"], { cwd });
2466
+ const stagedStatus = await runCommandWithOutput2("git", ["diff", "--cached", "--name-only"], { cwd });
2467
+ if (!stagedStatus.trim()) {
2468
+ setJson2(res, 200, { data: { committed: false } });
2469
+ return;
2470
+ }
2471
+ await runCommand2("git", ["commit", "-m", commitMessage], { cwd });
2472
+ setJson2(res, 200, { data: { committed: true } });
2473
+ } catch (error) {
2474
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to auto-commit worktree changes") });
2475
+ }
2476
+ return;
2477
+ }
2478
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/rollback-to-message") {
2479
+ const payload = asRecord2(await readJsonBody(req));
2480
+ const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
2481
+ const commitMessage = normalizeCommitMessage(payload?.message);
2482
+ if (!rawCwd) {
2483
+ setJson2(res, 400, { error: "Missing cwd" });
2484
+ return;
2485
+ }
2486
+ if (!commitMessage) {
2487
+ setJson2(res, 400, { error: "Missing message" });
2488
+ return;
2489
+ }
2490
+ const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
2491
+ try {
2492
+ const cwdInfo = await stat2(cwd);
2493
+ if (!cwdInfo.isDirectory()) {
2494
+ setJson2(res, 400, { error: "cwd is not a directory" });
2495
+ return;
2496
+ }
2497
+ } catch {
2498
+ setJson2(res, 404, { error: "cwd does not exist" });
2499
+ return;
2500
+ }
2501
+ try {
2502
+ await runCommandCapture("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
2503
+ const commitSha = await findCommitByExactMessage(cwd, commitMessage);
2504
+ if (!commitSha) {
2505
+ setJson2(res, 404, { error: "No matching commit found for this user message" });
2506
+ return;
2507
+ }
2508
+ let resetTargetSha = "";
2509
+ try {
2510
+ resetTargetSha = await runCommandCapture("git", ["rev-parse", `${commitSha}^`], { cwd });
2511
+ } catch {
2512
+ setJson2(res, 409, { error: "Cannot rollback: matched commit has no parent commit" });
2513
+ return;
2514
+ }
2515
+ let stashed = false;
2516
+ if (await hasGitWorkingTreeChanges(cwd)) {
2517
+ const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
2518
+ await runCommand2("git", ["stash", "push", "-u", "-m", stashMessage], { cwd });
2519
+ stashed = true;
2520
+ }
2521
+ await runCommand2("git", ["reset", "--hard", resetTargetSha], { cwd });
2522
+ setJson2(res, 200, { data: { reset: true, commitSha, resetTargetSha, stashed } });
2523
+ } catch (error) {
2524
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to rollback worktree to user message commit") });
2525
+ }
2526
+ return;
2527
+ }
2244
2528
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
2245
2529
  const payload = await readJsonBody(req);
2246
2530
  const record = asRecord2(payload);
@@ -2318,7 +2602,7 @@ function createCodexBridgeMiddleware() {
2318
2602
  let index = 1;
2319
2603
  while (index < 1e5) {
2320
2604
  const candidateName = `New Project (${String(index)})`;
2321
- const candidatePath = join2(normalizedBasePath, candidateName);
2605
+ const candidatePath = join3(normalizedBasePath, candidateName);
2322
2606
  try {
2323
2607
  await stat2(candidatePath);
2324
2608
  index += 1;
@@ -2564,7 +2848,7 @@ function createAuthSession(password) {
2564
2848
  }
2565
2849
 
2566
2850
  // src/server/localBrowseUi.ts
2567
- import { dirname, extname, join as join3 } from "path";
2851
+ import { dirname, extname as extname2, join as join4 } from "path";
2568
2852
  import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
2569
2853
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2570
2854
  ".txt",
@@ -2595,7 +2879,7 @@ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2595
2879
  ".ps1"
2596
2880
  ]);
2597
2881
  function languageForPath(pathValue) {
2598
- const extension = extname(pathValue).toLowerCase();
2882
+ const extension = extname2(pathValue).toLowerCase();
2599
2883
  switch (extension) {
2600
2884
  case ".js":
2601
2885
  return "javascript";
@@ -2656,7 +2940,7 @@ function decodeBrowsePath(rawPath) {
2656
2940
  }
2657
2941
  }
2658
2942
  function isTextEditablePath(pathValue) {
2659
- return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
2943
+ return TEXT_EDITABLE_EXTENSIONS.has(extname2(pathValue).toLowerCase());
2660
2944
  }
2661
2945
  function looksLikeTextBuffer(buffer) {
2662
2946
  if (buffer.length === 0) return true;
@@ -2702,7 +2986,7 @@ function escapeForInlineScriptString(value) {
2702
2986
  async function getDirectoryItems(localPath) {
2703
2987
  const entries = await readdir3(localPath, { withFileTypes: true });
2704
2988
  const withMeta = await Promise.all(entries.map(async (entry) => {
2705
- const entryPath = join3(localPath, entry.name);
2989
+ const entryPath = join4(localPath, entry.name);
2706
2990
  const entryStat = await stat3(entryPath);
2707
2991
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
2708
2992
  return {
@@ -2889,8 +3173,8 @@ async function createTextEditorHtml(localPath) {
2889
3173
  // src/server/httpServer.ts
2890
3174
  import { WebSocketServer } from "ws";
2891
3175
  var __dirname = dirname2(fileURLToPath(import.meta.url));
2892
- var distDir = join4(__dirname, "..", "dist");
2893
- var spaEntryFile = join4(distDir, "index.html");
3176
+ var distDir = join5(__dirname, "..", "dist");
3177
+ var spaEntryFile = join5(distDir, "index.html");
2894
3178
  var IMAGE_CONTENT_TYPES = {
2895
3179
  ".avif": "image/avif",
2896
3180
  ".bmp": "image/bmp",
@@ -2947,7 +3231,7 @@ function createServer(options = {}) {
2947
3231
  res.status(400).json({ error: "Expected absolute local file path." });
2948
3232
  return;
2949
3233
  }
2950
- const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
3234
+ const contentType = IMAGE_CONTENT_TYPES[extname3(localPath).toLowerCase()];
2951
3235
  if (!contentType) {
2952
3236
  res.status(415).json({ error: "Unsupported image type." });
2953
3237
  return;
@@ -3034,7 +3318,7 @@ function createServer(options = {}) {
3034
3318
  res.status(404).json({ error: "File not found." });
3035
3319
  }
3036
3320
  });
3037
- const hasFrontendAssets = existsSync3(spaEntryFile);
3321
+ const hasFrontendAssets = existsSync4(spaEntryFile);
3038
3322
  if (hasFrontendAssets) {
3039
3323
  app.use(express.static(distDir));
3040
3324
  }
@@ -3105,9 +3389,25 @@ function generatePassword() {
3105
3389
  // src/cli/index.ts
3106
3390
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
3107
3391
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
3392
+ var hasPromptedCloudflaredInstall = false;
3393
+ function getCodexHomePath() {
3394
+ return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
3395
+ }
3396
+ function getCloudflaredPromptMarkerPath() {
3397
+ return join6(getCodexHomePath(), ".cloudflared-install-prompted");
3398
+ }
3399
+ function hasPromptedCloudflaredInstallPersisted() {
3400
+ return existsSync5(getCloudflaredPromptMarkerPath());
3401
+ }
3402
+ async function persistCloudflaredInstallPrompted() {
3403
+ const codexHome = getCodexHomePath();
3404
+ mkdirSync(codexHome, { recursive: true });
3405
+ await writeFile4(getCloudflaredPromptMarkerPath(), `${Date.now()}
3406
+ `, "utf8");
3407
+ }
3108
3408
  async function readCliVersion() {
3109
3409
  try {
3110
- const packageJsonPath = join5(__dirname2, "..", "package.json");
3410
+ const packageJsonPath = join6(__dirname2, "..", "package.json");
3111
3411
  const raw = await readFile4(packageJsonPath, "utf8");
3112
3412
  const parsed = JSON.parse(raw);
3113
3413
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -3119,46 +3419,25 @@ function isTermuxRuntime() {
3119
3419
  return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
3120
3420
  }
3121
3421
  function canRun(command, args = []) {
3122
- const result = spawnSync(command, args, { stdio: "ignore" });
3123
- return result.status === 0;
3422
+ const result = canRunCommand(command, args);
3423
+ return result;
3124
3424
  }
3125
3425
  function runOrFail(command, args, label) {
3126
- const result = spawnSync(command, args, { stdio: "inherit" });
3426
+ const result = spawnSyncCommand(command, args, { stdio: "inherit" });
3127
3427
  if (result.status !== 0) {
3128
3428
  throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
3129
3429
  }
3130
3430
  }
3131
3431
  function runWithStatus(command, args) {
3132
- const result = spawnSync(command, args, { stdio: "inherit" });
3432
+ const result = spawnSyncCommand(command, args, { stdio: "inherit" });
3133
3433
  return result.status ?? -1;
3134
3434
  }
3135
- function getUserNpmPrefix() {
3136
- return join5(homedir3(), ".npm-global");
3137
- }
3138
- function resolveCodexCommand() {
3139
- if (canRun("codex", ["--version"])) {
3140
- return "codex";
3141
- }
3142
- const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
3143
- if (existsSync4(userCandidate) && canRun(userCandidate, ["--version"])) {
3144
- return userCandidate;
3145
- }
3146
- const prefix = process.env.PREFIX?.trim();
3147
- if (!prefix) {
3148
- return null;
3149
- }
3150
- const candidate = join5(prefix, "bin", "codex");
3151
- if (existsSync4(candidate) && canRun(candidate, ["--version"])) {
3152
- return candidate;
3153
- }
3154
- return null;
3155
- }
3156
3435
  function resolveCloudflaredCommand() {
3157
3436
  if (canRun("cloudflared", ["--version"])) {
3158
3437
  return "cloudflared";
3159
3438
  }
3160
- const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
3161
- if (existsSync4(localCandidate) && canRun(localCandidate, ["--version"])) {
3439
+ const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
3440
+ if (existsSync5(localCandidate) && canRun(localCandidate, ["--version"])) {
3162
3441
  return localCandidate;
3163
3442
  }
3164
3443
  return null;
@@ -3211,9 +3490,9 @@ async function ensureCloudflaredInstalledLinux() {
3211
3490
  if (!mappedArch) {
3212
3491
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
3213
3492
  }
3214
- const userBinDir = join5(homedir3(), ".local", "bin");
3493
+ const userBinDir = join6(homedir4(), ".local", "bin");
3215
3494
  mkdirSync(userBinDir, { recursive: true });
3216
- const destination = join5(userBinDir, "cloudflared");
3495
+ const destination = join6(userBinDir, "cloudflared");
3217
3496
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
3218
3497
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
3219
3498
  await downloadFile(downloadUrl, destination);
@@ -3227,6 +3506,14 @@ async function ensureCloudflaredInstalledLinux() {
3227
3506
  return installed;
3228
3507
  }
3229
3508
  async function shouldInstallCloudflaredInteractively() {
3509
+ if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
3510
+ return false;
3511
+ }
3512
+ hasPromptedCloudflaredInstall = true;
3513
+ await persistCloudflaredInstallPrompted();
3514
+ if (process.platform === "win32") {
3515
+ return false;
3516
+ }
3230
3517
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3231
3518
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
3232
3519
  return false;
@@ -3245,6 +3532,9 @@ async function resolveCloudflaredForTunnel() {
3245
3532
  if (current) {
3246
3533
  return current;
3247
3534
  }
3535
+ if (process.platform === "win32") {
3536
+ return null;
3537
+ }
3248
3538
  const installApproved = await shouldInstallCloudflaredInteractively();
3249
3539
  if (!installApproved) {
3250
3540
  return null;
@@ -3252,8 +3542,8 @@ async function resolveCloudflaredForTunnel() {
3252
3542
  return ensureCloudflaredInstalledLinux();
3253
3543
  }
3254
3544
  function hasCodexAuth() {
3255
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3256
- return existsSync4(join5(codexHome, "auth.json"));
3545
+ const codexHome = getCodexHomePath();
3546
+ return existsSync5(join6(codexHome, "auth.json"));
3257
3547
  }
3258
3548
  function ensureCodexInstalled() {
3259
3549
  let codexCommand = resolveCodexCommand();
@@ -3271,7 +3561,7 @@ function ensureCodexInstalled() {
3271
3561
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
3272
3562
  `);
3273
3563
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
3274
- process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
3564
+ process.env.PATH = `${join6(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
3275
3565
  };
3276
3566
  if (isTermuxRuntime()) {
3277
3567
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -3412,8 +3702,8 @@ function listenWithFallback(server, startPort) {
3412
3702
  });
3413
3703
  }
3414
3704
  function getCodexGlobalStatePath2() {
3415
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3416
- return join5(codexHome, ".codex-global-state.json");
3705
+ const codexHome = getCodexHomePath();
3706
+ return join6(codexHome, ".codex-global-state.json");
3417
3707
  }
3418
3708
  function normalizeUniqueStrings(value) {
3419
3709
  if (!Array.isArray(value)) return [];