codexapp 0.1.48 → 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
@@ -1273,6 +1273,53 @@ function scoreFileCandidate(path, query) {
1273
1273
  if (lowerPath.includes(lowerQuery)) return 4;
1274
1274
  return 10;
1275
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
+ }
1276
1323
  async function listFilesWithRipgrep(cwd) {
1277
1324
  return await new Promise((resolve3, reject) => {
1278
1325
  const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
@@ -1380,6 +1427,33 @@ async function runCommandCapture(command, args, options = {}) {
1380
1427
  });
1381
1428
  });
1382
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
+ }
1383
1457
  function normalizeStringArray(value) {
1384
1458
  if (!Array.isArray(value)) return [];
1385
1459
  const normalized = [];
@@ -1400,6 +1474,30 @@ function normalizeStringRecord(value) {
1400
1474
  }
1401
1475
  return next;
1402
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
+ }
1403
1501
  function getCodexAuthPath() {
1404
1502
  return join3(getCodexHomeDir2(), "auth.json");
1405
1503
  }
@@ -2251,6 +2349,19 @@ function createCodexBridgeMiddleware() {
2251
2349
  setJson2(res, 200, { data: { path: homedir3() } });
2252
2350
  return;
2253
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
+ }
2363
+ return;
2364
+ }
2254
2365
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
2255
2366
  const payload = asRecord2(await readJsonBody(req));
2256
2367
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
@@ -2321,6 +2432,99 @@ function createCodexBridgeMiddleware() {
2321
2432
  }
2322
2433
  return;
2323
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
+ }
2324
2528
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
2325
2529
  const payload = await readJsonBody(req);
2326
2530
  const record = asRecord2(payload);
@@ -3185,6 +3389,22 @@ function generatePassword() {
3185
3389
  // src/cli/index.ts
3186
3390
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
3187
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
+ }
3188
3408
  async function readCliVersion() {
3189
3409
  try {
3190
3410
  const packageJsonPath = join6(__dirname2, "..", "package.json");
@@ -3286,6 +3506,14 @@ async function ensureCloudflaredInstalledLinux() {
3286
3506
  return installed;
3287
3507
  }
3288
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
+ }
3289
3517
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3290
3518
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
3291
3519
  return false;
@@ -3304,6 +3532,9 @@ async function resolveCloudflaredForTunnel() {
3304
3532
  if (current) {
3305
3533
  return current;
3306
3534
  }
3535
+ if (process.platform === "win32") {
3536
+ return null;
3537
+ }
3307
3538
  const installApproved = await shouldInstallCloudflaredInteractively();
3308
3539
  if (!installApproved) {
3309
3540
  return null;
@@ -3311,7 +3542,7 @@ async function resolveCloudflaredForTunnel() {
3311
3542
  return ensureCloudflaredInstalledLinux();
3312
3543
  }
3313
3544
  function hasCodexAuth() {
3314
- const codexHome = process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
3545
+ const codexHome = getCodexHomePath();
3315
3546
  return existsSync5(join6(codexHome, "auth.json"));
3316
3547
  }
3317
3548
  function ensureCodexInstalled() {
@@ -3471,7 +3702,7 @@ function listenWithFallback(server, startPort) {
3471
3702
  });
3472
3703
  }
3473
3704
  function getCodexGlobalStatePath2() {
3474
- const codexHome = process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
3705
+ const codexHome = getCodexHomePath();
3475
3706
  return join6(codexHome, ".codex-global-state.json");
3476
3707
  }
3477
3708
  function normalizeUniqueStrings(value) {