codexapp 0.1.52 → 0.1.56

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
@@ -9,14 +9,156 @@ import { isAbsolute as isAbsolute3, join as join6, resolve as resolve2 } from "p
9
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
- import { dirname as dirname3 } from "path";
12
+ import { dirname as dirname4 } from "path";
13
13
  import { get as httpsGet } from "https";
14
14
  import { Command } from "commander";
15
15
  import qrcode from "qrcode-terminal";
16
16
 
17
+ // src/commandResolution.ts
18
+ import { spawnSync } from "child_process";
19
+ import { existsSync } from "fs";
20
+ import { homedir } from "os";
21
+ import { delimiter, join } from "path";
22
+ function uniqueStrings(values) {
23
+ const unique = [];
24
+ for (const value of values) {
25
+ const normalized = value?.trim();
26
+ if (!normalized || unique.includes(normalized)) continue;
27
+ unique.push(normalized);
28
+ }
29
+ return unique;
30
+ }
31
+ function isPathLike(command) {
32
+ return command.includes("/") || command.includes("\\") || /^[a-zA-Z]:/.test(command);
33
+ }
34
+ function isRunnableCommand(command, args = []) {
35
+ if (isPathLike(command) && !existsSync(command)) {
36
+ return false;
37
+ }
38
+ return canRunCommand(command, args);
39
+ }
40
+ function getWindowsAppDataNpmPrefix() {
41
+ const appData = process.env.APPDATA?.trim();
42
+ return appData ? join(appData, "npm") : null;
43
+ }
44
+ function getPotentialNpmPrefixes() {
45
+ return uniqueStrings([
46
+ process.env.npm_config_prefix,
47
+ process.env.PREFIX,
48
+ getUserNpmPrefix(),
49
+ process.platform === "win32" ? getWindowsAppDataNpmPrefix() : null
50
+ ]);
51
+ }
52
+ function getPotentialCodexPackageDirs(prefix) {
53
+ const dirs = [join(prefix, "node_modules", "@openai", "codex")];
54
+ if (process.platform !== "win32") {
55
+ dirs.push(join(prefix, "lib", "node_modules", "@openai", "codex"));
56
+ }
57
+ return dirs;
58
+ }
59
+ function getPotentialCodexExecutables(prefix) {
60
+ return getPotentialCodexPackageDirs(prefix).map((packageDir) => process.platform === "win32" ? join(
61
+ packageDir,
62
+ "node_modules",
63
+ "@openai",
64
+ "codex-win32-x64",
65
+ "vendor",
66
+ "x86_64-pc-windows-msvc",
67
+ "codex",
68
+ "codex.exe"
69
+ ) : join(packageDir, "bin", "codex"));
70
+ }
71
+ function getPotentialRipgrepExecutables(prefix) {
72
+ return getPotentialCodexPackageDirs(prefix).map((packageDir) => process.platform === "win32" ? join(
73
+ packageDir,
74
+ "node_modules",
75
+ "@openai",
76
+ "codex-win32-x64",
77
+ "vendor",
78
+ "x86_64-pc-windows-msvc",
79
+ "path",
80
+ "rg.exe"
81
+ ) : join(packageDir, "bin", "rg"));
82
+ }
83
+ function canRunCommand(command, args = []) {
84
+ const result = spawnSync(command, args, {
85
+ stdio: "ignore",
86
+ windowsHide: true
87
+ });
88
+ return !result.error && result.status === 0;
89
+ }
90
+ function getUserNpmPrefix() {
91
+ return join(homedir(), ".npm-global");
92
+ }
93
+ function getNpmGlobalBinDir(prefix) {
94
+ return process.platform === "win32" ? prefix : join(prefix, "bin");
95
+ }
96
+ function prependPathEntry(existingPath, entry) {
97
+ const normalizedEntry = entry.trim();
98
+ if (!normalizedEntry) return existingPath;
99
+ const parts = existingPath.split(delimiter).map((value) => value.trim()).filter(Boolean);
100
+ if (parts.includes(normalizedEntry)) {
101
+ return existingPath;
102
+ }
103
+ return existingPath ? `${normalizedEntry}${delimiter}${existingPath}` : normalizedEntry;
104
+ }
105
+ function resolveCodexCommand() {
106
+ const explicit = process.env.CODEXUI_CODEX_COMMAND?.trim();
107
+ const packageCandidates = getPotentialNpmPrefixes().flatMap(getPotentialCodexExecutables);
108
+ const fallbackCandidates = process.platform === "win32" ? [...packageCandidates, "codex"] : ["codex", ...packageCandidates];
109
+ for (const candidate of uniqueStrings([explicit, ...fallbackCandidates])) {
110
+ if (isRunnableCommand(candidate, ["--version"])) {
111
+ return candidate;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ function resolveRipgrepCommand() {
117
+ const explicit = process.env.CODEXUI_RG_COMMAND?.trim();
118
+ const packageCandidates = getPotentialNpmPrefixes().flatMap(getPotentialRipgrepExecutables);
119
+ const fallbackCandidates = process.platform === "win32" ? [...packageCandidates, "rg"] : ["rg", ...packageCandidates];
120
+ for (const candidate of uniqueStrings([explicit, ...fallbackCandidates])) {
121
+ if (isRunnableCommand(candidate, ["--version"])) {
122
+ return candidate;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ function resolvePythonCommand() {
128
+ const candidates = process.platform === "win32" ? [
129
+ { command: "python", args: [] },
130
+ { command: "py", args: ["-3"] },
131
+ { command: "python3", args: [] }
132
+ ] : [
133
+ { command: "python3", args: [] },
134
+ { command: "python", args: [] }
135
+ ];
136
+ for (const candidate of candidates) {
137
+ if (isRunnableCommand(candidate.command, [...candidate.args, "--version"])) {
138
+ return candidate;
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ function resolveSkillInstallerScriptPath(codexHome) {
144
+ const normalizedCodexHome = codexHome?.trim();
145
+ const candidates = uniqueStrings([
146
+ normalizedCodexHome ? join(normalizedCodexHome, "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py") : null,
147
+ process.env.CODEX_HOME?.trim() ? join(process.env.CODEX_HOME.trim(), "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py") : null,
148
+ join(homedir(), ".codex", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py"),
149
+ join(homedir(), ".cursor", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py")
150
+ ]);
151
+ for (const candidate of candidates) {
152
+ if (existsSync(candidate)) {
153
+ return candidate;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
17
159
  // src/server/httpServer.ts
18
160
  import { fileURLToPath } from "url";
19
- import { dirname as dirname2, extname as extname3, isAbsolute as isAbsolute2, join as join5 } from "path";
161
+ import { dirname as dirname3, extname as extname3, isAbsolute as isAbsolute2, join as join5 } from "path";
20
162
  import { existsSync as existsSync4 } from "fs";
21
163
  import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
22
164
  import express from "express";
@@ -30,16 +172,16 @@ import { request as httpRequest } from "http";
30
172
  import { request as httpsRequest } from "https";
31
173
  import { homedir as homedir3 } from "os";
32
174
  import { tmpdir as tmpdir2 } from "os";
33
- import { basename as basename3, isAbsolute, join as join3, resolve } from "path";
175
+ import { basename as basename3, dirname, isAbsolute, join as join3, resolve } from "path";
34
176
  import { createInterface } from "readline";
35
177
  import { writeFile as writeFile2 } from "fs/promises";
36
178
 
37
179
  // src/server/skillsRoutes.ts
38
180
  import { spawn } from "child_process";
39
181
  import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
40
- import { existsSync } from "fs";
41
- import { homedir, tmpdir } from "os";
42
- import { join } from "path";
182
+ import { existsSync as existsSync2 } from "fs";
183
+ import { homedir as homedir2, tmpdir } from "os";
184
+ import { join as join2 } from "path";
43
185
  import { writeFile } from "fs/promises";
44
186
  function asRecord(value) {
45
187
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -65,39 +207,45 @@ function setJson(res, statusCode, payload) {
65
207
  }
66
208
  function getCodexHomeDir() {
67
209
  const codexHome = process.env.CODEX_HOME?.trim();
68
- return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
210
+ return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
69
211
  }
70
212
  function getSkillsInstallDir() {
71
- return join(getCodexHomeDir(), "skills");
72
- }
73
- function resolveSkillInstallerScriptPath() {
74
- const candidates = [
75
- join(getCodexHomeDir(), "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py"),
76
- join(homedir(), ".codex", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py"),
77
- join(homedir(), ".cursor", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py")
78
- ];
79
- for (const candidate of candidates) {
80
- if (existsSync(candidate)) return candidate;
81
- }
82
- throw new Error(`Skill installer script not found. Checked: ${candidates.join(", ")}`);
213
+ return join2(getCodexHomeDir(), "skills");
83
214
  }
215
+ var DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
84
216
  async function runCommand(command, args, options = {}) {
217
+ const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
85
218
  await new Promise((resolve3, reject) => {
86
219
  const proc = spawn(command, args, {
87
220
  cwd: options.cwd,
88
221
  env: process.env,
89
222
  stdio: ["ignore", "pipe", "pipe"]
90
223
  });
224
+ let settled = false;
91
225
  let stdout = "";
92
226
  let stderr = "";
227
+ const timer = setTimeout(() => {
228
+ if (settled) return;
229
+ settled = true;
230
+ proc.kill("SIGKILL");
231
+ reject(new Error(`Command timed out after ${timeout}ms (${command} ${args.join(" ")})`));
232
+ }, timeout);
93
233
  proc.stdout.on("data", (chunk) => {
94
234
  stdout += chunk.toString();
95
235
  });
96
236
  proc.stderr.on("data", (chunk) => {
97
237
  stderr += chunk.toString();
98
238
  });
99
- proc.on("error", reject);
239
+ proc.on("error", (err) => {
240
+ if (settled) return;
241
+ settled = true;
242
+ clearTimeout(timer);
243
+ reject(err);
244
+ });
100
245
  proc.on("close", (code) => {
246
+ if (settled) return;
247
+ settled = true;
248
+ clearTimeout(timer);
101
249
  if (code === 0) {
102
250
  resolve3();
103
251
  return;
@@ -109,22 +257,38 @@ async function runCommand(command, args, options = {}) {
109
257
  });
110
258
  }
111
259
  async function runCommandWithOutput(command, args, options = {}) {
260
+ const timeout = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
112
261
  return await new Promise((resolve3, reject) => {
113
262
  const proc = spawn(command, args, {
114
263
  cwd: options.cwd,
115
264
  env: process.env,
116
265
  stdio: ["ignore", "pipe", "pipe"]
117
266
  });
267
+ let settled = false;
118
268
  let stdout = "";
119
269
  let stderr = "";
270
+ const timer = setTimeout(() => {
271
+ if (settled) return;
272
+ settled = true;
273
+ proc.kill("SIGKILL");
274
+ reject(new Error(`Command timed out after ${timeout}ms (${command} ${args.join(" ")})`));
275
+ }, timeout);
120
276
  proc.stdout.on("data", (chunk) => {
121
277
  stdout += chunk.toString();
122
278
  });
123
279
  proc.stderr.on("data", (chunk) => {
124
280
  stderr += chunk.toString();
125
281
  });
126
- proc.on("error", reject);
282
+ proc.on("error", (err) => {
283
+ if (settled) return;
284
+ settled = true;
285
+ clearTimeout(timer);
286
+ reject(err);
287
+ });
127
288
  proc.on("close", (code) => {
289
+ if (settled) return;
290
+ settled = true;
291
+ clearTimeout(timer);
128
292
  if (code === 0) {
129
293
  resolve3(stdout.trim());
130
294
  return;
@@ -135,6 +299,21 @@ async function runCommandWithOutput(command, args, options = {}) {
135
299
  });
136
300
  });
137
301
  }
302
+ function withTimeout(promise, ms, label) {
303
+ return new Promise((resolve3, reject) => {
304
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
305
+ promise.then(
306
+ (val) => {
307
+ clearTimeout(timer);
308
+ resolve3(val);
309
+ },
310
+ (err) => {
311
+ clearTimeout(timer);
312
+ reject(err);
313
+ }
314
+ );
315
+ });
316
+ }
138
317
  async function detectUserSkillsDir(appServer) {
139
318
  try {
140
319
  const result = await appServer.rpc("skills/list", {});
@@ -250,13 +429,14 @@ var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
250
429
  var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
251
430
  var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
252
431
  var SYNC_UPSTREAM_SKILLS_REPO = "skills";
432
+ var PRIVATE_SYNC_BRANCH = "main";
253
433
  var HUB_SKILLS_OWNER = "openclaw";
254
434
  var HUB_SKILLS_REPO = "skills";
255
435
  var startupSkillsSyncInitialized = false;
256
436
  var startupSyncStatus = {
257
437
  inProgress: false,
258
438
  mode: "idle",
259
- branch: getPreferredSyncBranch(),
439
+ branch: PRIVATE_SYNC_BRANCH,
260
440
  lastAction: "not-started",
261
441
  lastRunAtIso: "",
262
442
  lastSuccessAtIso: "",
@@ -269,7 +449,7 @@ async function scanInstalledSkillsFromDisk() {
269
449
  const entries = await readdir(skillsDir, { withFileTypes: true });
270
450
  for (const entry of entries) {
271
451
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
272
- const skillMd = join(skillsDir, entry.name, "SKILL.md");
452
+ const skillMd = join2(skillsDir, entry.name, "SKILL.md");
273
453
  try {
274
454
  await stat(skillMd);
275
455
  map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
@@ -298,7 +478,7 @@ function extractSkillDescriptionFromMarkdown(markdown) {
298
478
  return "";
299
479
  }
300
480
  function getSkillsSyncStatePath() {
301
- return join(getCodexHomeDir(), "skills-sync.json");
481
+ return join2(getCodexHomeDir(), "skills-sync.json");
302
482
  }
303
483
  async function readSkillsSyncState() {
304
484
  try {
@@ -371,14 +551,14 @@ async function completeGithubDeviceLogin(deviceCode) {
371
551
  }
372
552
  function isAndroidLikeRuntime() {
373
553
  if (process.platform === "android") return true;
374
- if (existsSync("/data/data/com.termux")) return true;
554
+ if (existsSync2("/data/data/com.termux")) return true;
375
555
  if (process.env.TERMUX_VERSION) return true;
376
556
  const prefix = process.env.PREFIX?.toLowerCase() ?? "";
377
557
  if (prefix.includes("/com.termux/")) return true;
378
558
  const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
379
559
  return proot.length > 0;
380
560
  }
381
- function getPreferredSyncBranch() {
561
+ function getPreferredPublicUpstreamBranch() {
382
562
  return isAndroidLikeRuntime() ? "android" : "main";
383
563
  }
384
564
  function isUpstreamSkillsRepo(repoOwner, repoName) {
@@ -433,10 +613,10 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
433
613
  }
434
614
  if (!ready) throw new Error("Private mirror repo was created but is not available yet");
435
615
  if (!created) return;
436
- const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
616
+ const tmp = await mkdtemp(join2(tmpdir(), "codex-skills-seed-"));
437
617
  try {
438
618
  const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
439
- const branch = getPreferredSyncBranch();
619
+ const branch = PRIVATE_SYNC_BRANCH;
440
620
  try {
441
621
  await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
442
622
  } catch {
@@ -507,7 +687,7 @@ function toGitHubTokenRemote(repoOwner, repoName, token) {
507
687
  async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
508
688
  const localDir = getSkillsInstallDir();
509
689
  await mkdir(localDir, { recursive: true });
510
- const gitDir = join(localDir, ".git");
690
+ const gitDir = join2(localDir, ".git");
511
691
  let hasGitDir = false;
512
692
  try {
513
693
  hasGitDir = (await stat(gitDir)).isDirectory();
@@ -553,7 +733,7 @@ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
553
733
  }
554
734
  let pulledMtimes = /* @__PURE__ */ new Map();
555
735
  try {
556
- await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
736
+ await runCommand("git", ["pull", "--no-rebase", "--no-ff", "origin", branch], { cwd: localDir });
557
737
  pulledMtimes = await snapshotFileMtimes(localDir);
558
738
  } catch {
559
739
  await resolveMergeConflictsByNewerCommit(localDir, branch);
@@ -622,7 +802,7 @@ async function walkFileMtimes(rootDir, currentDir, out) {
622
802
  for (const entry of entries) {
623
803
  const entryName = String(entry.name);
624
804
  if (entryName === ".git") continue;
625
- const absolutePath = join(currentDir, entryName);
805
+ const absolutePath = join2(currentDir, entryName);
626
806
  const relativePath = absolutePath.slice(rootDir.length + 1);
627
807
  if (entry.isDirectory()) {
628
808
  await walkFileMtimes(rootDir, absolutePath, out);
@@ -637,8 +817,37 @@ async function walkFileMtimes(rootDir, currentDir, out) {
637
817
  }
638
818
  }
639
819
  async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
820
+ function isNonFastForwardPushError(error) {
821
+ const text = getErrorMessage(error, "").toLowerCase();
822
+ return text.includes("non-fast-forward") || text.includes("fetch first") || text.includes("rejected") && text.includes("push");
823
+ }
824
+ async function pushWithNonFastForwardRetry(repoDir2, branch2) {
825
+ const maxAttempts = 3;
826
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
827
+ try {
828
+ await runCommand("git", ["push", "origin", `HEAD:${branch2}`], { cwd: repoDir2 });
829
+ return;
830
+ } catch (error) {
831
+ if (!isNonFastForwardPushError(error) || attempt >= maxAttempts) {
832
+ throw error;
833
+ }
834
+ }
835
+ await runCommand("git", ["fetch", "origin"], { cwd: repoDir2 });
836
+ try {
837
+ await runCommand("git", ["pull", "--no-rebase", "--no-ff", "origin", branch2], { cwd: repoDir2 });
838
+ } catch {
839
+ await resolveMergeConflictsByNewerCommit(repoDir2, branch2);
840
+ }
841
+ await runCommand("git", ["add", "."], { cwd: repoDir2 });
842
+ const statusAfterReconcile = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir2 })).trim();
843
+ if (statusAfterReconcile) {
844
+ await runCommand("git", ["commit", "-m", "Reconcile skills sync before push retry"], { cwd: repoDir2 });
845
+ }
846
+ }
847
+ throw new Error("Failed to push after non-fast-forward retries");
848
+ }
640
849
  const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
641
- const branch = getPreferredSyncBranch();
850
+ const branch = PRIVATE_SYNC_BRANCH;
642
851
  const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
643
852
  void _installedMap;
644
853
  await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
@@ -647,16 +856,16 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
647
856
  const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
648
857
  if (!status) return;
649
858
  await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
650
- await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
859
+ await pushWithNonFastForwardRetry(repoDir, branch);
651
860
  }
652
861
  async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
653
862
  const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
654
- const branch = getPreferredSyncBranch();
863
+ const branch = PRIVATE_SYNC_BRANCH;
655
864
  await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
656
865
  }
657
866
  async function bootstrapSkillsFromUpstreamIntoLocal() {
658
867
  const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
659
- const branch = getPreferredSyncBranch();
868
+ const branch = getPreferredPublicUpstreamBranch();
660
869
  await ensureSkillsWorkingTreeRepo(repoUrl, branch);
661
870
  }
662
871
  async function collectLocalSyncedSkills(appServer) {
@@ -716,9 +925,9 @@ async function autoPushSyncedSkills(appServer) {
716
925
  }
717
926
  async function ensureCodexAgentsSymlinkToSkillsAgents() {
718
927
  const codexHomeDir = getCodexHomeDir();
719
- const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
720
- const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
721
- await mkdir(join(codexHomeDir, "skills"), { recursive: true });
928
+ const skillsAgentsPath = join2(codexHomeDir, "skills", "AGENTS.md");
929
+ const codexAgentsPath = join2(codexHomeDir, "AGENTS.md");
930
+ await mkdir(join2(codexHomeDir, "skills"), { recursive: true });
722
931
  let copiedFromCodex = false;
723
932
  try {
724
933
  const codexAgentsStat = await lstat(codexAgentsPath);
@@ -742,7 +951,7 @@ async function ensureCodexAgentsSymlinkToSkillsAgents() {
742
951
  await writeFile(skillsAgentsPath, "", "utf8");
743
952
  }
744
953
  }
745
- const relativeTarget = join("skills", "AGENTS.md");
954
+ const relativeTarget = join2("skills", "AGENTS.md");
746
955
  try {
747
956
  const current = await lstat(codexAgentsPath);
748
957
  if (current.isSymbolicLink()) {
@@ -760,7 +969,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
760
969
  startupSyncStatus.inProgress = true;
761
970
  startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
762
971
  startupSyncStatus.lastError = "";
763
- startupSyncStatus.branch = getPreferredSyncBranch();
972
+ startupSyncStatus.branch = PRIVATE_SYNC_BRANCH;
764
973
  try {
765
974
  const state = await readSkillsSyncState();
766
975
  if (!state.githubToken) {
@@ -772,6 +981,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
772
981
  return;
773
982
  }
774
983
  startupSyncStatus.mode = "unauthenticated-bootstrap";
984
+ startupSyncStatus.branch = getPreferredPublicUpstreamBranch();
775
985
  startupSyncStatus.lastAction = "pull-upstream";
776
986
  await bootstrapSkillsFromUpstreamIntoLocal();
777
987
  try {
@@ -783,6 +993,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
783
993
  return;
784
994
  }
785
995
  startupSyncStatus.mode = "authenticated-fork-sync";
996
+ startupSyncStatus.branch = PRIVATE_SYNC_BRANCH;
786
997
  startupSyncStatus.lastAction = "ensure-private-fork";
787
998
  const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
788
999
  const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
@@ -1022,13 +1233,21 @@ async function handleSkillsRoutes(req, res, url, context) {
1022
1233
  }
1023
1234
  const localDir = await detectUserSkillsDir(appServer);
1024
1235
  await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
1025
- const installerScript = resolveSkillInstallerScriptPath();
1236
+ const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir());
1237
+ if (!installerScript) {
1238
+ throw new Error("Skill installer script not found");
1239
+ }
1240
+ const pythonCommand = resolvePythonCommand();
1241
+ if (!pythonCommand) {
1242
+ throw new Error("Python 3 is required to install skills");
1243
+ }
1026
1244
  const localSkills = await scanInstalledSkillsFromDisk();
1027
1245
  for (const skill of remote) {
1028
1246
  const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
1029
1247
  if (!owner) continue;
1030
1248
  if (!localSkills.has(skill.name)) {
1031
- await runCommand("python3", [
1249
+ await runCommand(pythonCommand.command, [
1250
+ ...pythonCommand.args,
1032
1251
  installerScript,
1033
1252
  "--repo",
1034
1253
  `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
@@ -1040,7 +1259,7 @@ async function handleSkillsRoutes(req, res, url, context) {
1040
1259
  "git"
1041
1260
  ]);
1042
1261
  }
1043
- const skillPath = join(localDir, skill.name);
1262
+ const skillPath = join2(localDir, skill.name);
1044
1263
  await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
1045
1264
  }
1046
1265
  const remoteNames = new Set(remote.map((row) => row.name));
@@ -1106,9 +1325,25 @@ async function handleSkillsRoutes(req, res, url, context) {
1106
1325
  setJson(res, 400, { error: "Missing owner or name" });
1107
1326
  return true;
1108
1327
  }
1109
- const installerScript = resolveSkillInstallerScriptPath();
1110
- const installDest = await detectUserSkillsDir(appServer);
1111
- await runCommand("python3", [
1328
+ const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir());
1329
+ if (!installerScript) {
1330
+ throw new Error("Skill installer script not found");
1331
+ }
1332
+ const pythonCommand = resolvePythonCommand();
1333
+ if (!pythonCommand) {
1334
+ throw new Error("Python 3 is required to install skills");
1335
+ }
1336
+ const installDest = await withTimeout(
1337
+ detectUserSkillsDir(appServer),
1338
+ 1e4,
1339
+ "detectUserSkillsDir"
1340
+ ).catch(() => getSkillsInstallDir());
1341
+ const skillDir = join2(installDest, name);
1342
+ if (existsSync2(skillDir)) {
1343
+ await rm(skillDir, { recursive: true, force: true });
1344
+ }
1345
+ await runCommand(pythonCommand.command, [
1346
+ ...pythonCommand.args,
1112
1347
  installerScript,
1113
1348
  "--repo",
1114
1349
  `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
@@ -1118,13 +1353,16 @@ async function handleSkillsRoutes(req, res, url, context) {
1118
1353
  installDest,
1119
1354
  "--method",
1120
1355
  "git"
1121
- ]);
1122
- const skillDir = join(installDest, name);
1123
- await ensureInstalledSkillIsValid(appServer, skillDir);
1356
+ ], { timeoutMs: 9e4 });
1357
+ try {
1358
+ await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
1359
+ } catch {
1360
+ }
1124
1361
  const syncState = await readSkillsSyncState();
1125
1362
  const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
1126
1363
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1127
- await autoPushSyncedSkills(appServer);
1364
+ autoPushSyncedSkills(appServer).catch(() => {
1365
+ });
1128
1366
  setJson(res, 200, { ok: true, path: skillDir });
1129
1367
  } catch (error) {
1130
1368
  setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
@@ -1136,7 +1374,7 @@ async function handleSkillsRoutes(req, res, url, context) {
1136
1374
  const payload = asRecord(await readJsonBody2(req));
1137
1375
  const name = typeof payload?.name === "string" ? payload.name : "";
1138
1376
  const path = typeof payload?.path === "string" ? payload.path : "";
1139
- const target = path || (name ? join(getSkillsInstallDir(), name) : "");
1377
+ const target = path || (name ? join2(getSkillsInstallDir(), name) : "");
1140
1378
  if (!target) {
1141
1379
  setJson(res, 400, { error: "Missing name or path" });
1142
1380
  return true;
@@ -1148,9 +1386,10 @@ async function handleSkillsRoutes(req, res, url, context) {
1148
1386
  delete nextOwners[name];
1149
1387
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1150
1388
  }
1151
- await autoPushSyncedSkills(appServer);
1389
+ autoPushSyncedSkills(appServer).catch(() => {
1390
+ });
1152
1391
  try {
1153
- await appServer.rpc("skills/list", { forceReload: true });
1392
+ await withTimeout(appServer.rpc("skills/list", { forceReload: true }), 1e4, "skills/list reload");
1154
1393
  } catch {
1155
1394
  }
1156
1395
  setJson(res, 200, { ok: true, deletedPath: target });
@@ -1529,10 +1768,8 @@ ${summary}`;
1529
1768
  };
1530
1769
 
1531
1770
  // src/utils/commandInvocation.ts
1532
- import { spawnSync } from "child_process";
1533
- import { existsSync as existsSync2 } from "fs";
1534
- import { homedir as homedir2 } from "os";
1535
- import { basename as basename2, extname, join as join2 } from "path";
1771
+ import { spawnSync as spawnSync2 } from "child_process";
1772
+ import { basename as basename2, extname } from "path";
1536
1773
  var WINDOWS_CMD_NAMES = /* @__PURE__ */ new Set(["codex", "npm", "npx"]);
1537
1774
  function quoteCmdExeArg(value) {
1538
1775
  const normalized = value.replace(/"/g, '""');
@@ -1566,44 +1803,7 @@ function getSpawnInvocation(command, args = []) {
1566
1803
  }
1567
1804
  function spawnSyncCommand(command, args = [], options = {}) {
1568
1805
  const invocation = getSpawnInvocation(command, args);
1569
- return spawnSync(invocation.command, invocation.args, options);
1570
- }
1571
- function canRunCommand(command, args = []) {
1572
- const result = spawnSyncCommand(command, args, { stdio: "ignore" });
1573
- return result.status === 0;
1574
- }
1575
- function getUserNpmPrefix() {
1576
- return join2(homedir2(), ".npm-global");
1577
- }
1578
- function resolveCodexCommand() {
1579
- if (canRunCommand("codex", ["--version"])) {
1580
- return "codex";
1581
- }
1582
- if (process.platform === "win32") {
1583
- const windowsCandidates = [
1584
- process.env.APPDATA ? join2(process.env.APPDATA, "npm", "codex.cmd") : "",
1585
- join2(homedir2(), ".local", "bin", "codex.cmd"),
1586
- join2(getUserNpmPrefix(), "bin", "codex.cmd")
1587
- ].filter(Boolean);
1588
- for (const candidate2 of windowsCandidates) {
1589
- if (existsSync2(candidate2) && canRunCommand(candidate2, ["--version"])) {
1590
- return candidate2;
1591
- }
1592
- }
1593
- }
1594
- const userCandidate = join2(getUserNpmPrefix(), "bin", "codex");
1595
- if (existsSync2(userCandidate) && canRunCommand(userCandidate, ["--version"])) {
1596
- return userCandidate;
1597
- }
1598
- const prefix = process.env.PREFIX?.trim();
1599
- if (!prefix) {
1600
- return null;
1601
- }
1602
- const candidate = join2(prefix, "bin", "codex");
1603
- if (existsSync2(candidate) && canRunCommand(candidate, ["--version"])) {
1604
- return candidate;
1605
- }
1606
- return null;
1806
+ return spawnSync2(invocation.command, invocation.args, options);
1607
1807
  }
1608
1808
 
1609
1809
  // src/server/codexAppServerBridge.ts
@@ -1681,9 +1881,62 @@ function scoreFileCandidate(path, query) {
1681
1881
  if (lowerPath.includes(lowerQuery)) return 4;
1682
1882
  return 10;
1683
1883
  }
1884
+ function decodeHtmlEntities(value) {
1885
+ return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/gi, "/");
1886
+ }
1887
+ function stripHtml(value) {
1888
+ return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
1889
+ }
1890
+ function parseGithubTrendingHtml(html, limit) {
1891
+ const rows = html.match(/<article[\s\S]*?<\/article>/g) ?? [];
1892
+ const items = [];
1893
+ let seq = Date.now();
1894
+ for (const row of rows) {
1895
+ const repoBlockMatch = row.match(/<h2[\s\S]*?<\/h2>/);
1896
+ const hrefMatch = repoBlockMatch?.[0]?.match(/href="\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)"/);
1897
+ if (!hrefMatch) continue;
1898
+ const fullName = hrefMatch[1] ?? "";
1899
+ if (!fullName || items.some((item) => item.fullName === fullName)) continue;
1900
+ const descriptionMatch = row.match(/<p[^>]*class="[^"]*col-9[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*>([\s\S]*?)<\/p>/);
1901
+ const languageMatch = row.match(/programmingLanguage[^>]*>\s*([\s\S]*?)\s*<\/span>/);
1902
+ const starsMatch = row.match(/href="\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/stargazers"[\s\S]*?>([\s\S]*?)<\/a>/);
1903
+ const starsText = stripHtml(starsMatch?.[1] ?? "").replace(/,/g, "");
1904
+ const stars = Number.parseInt(starsText, 10);
1905
+ items.push({
1906
+ id: seq,
1907
+ fullName,
1908
+ url: `https://github.com/${fullName}`,
1909
+ description: stripHtml(descriptionMatch?.[1] ?? ""),
1910
+ language: stripHtml(languageMatch?.[1] ?? ""),
1911
+ stars: Number.isFinite(stars) ? stars : 0
1912
+ });
1913
+ seq += 1;
1914
+ if (items.length >= limit) break;
1915
+ }
1916
+ return items;
1917
+ }
1918
+ async function fetchGithubTrending(since, limit) {
1919
+ const endpoint = `https://github.com/trending?since=${since}`;
1920
+ const response = await fetch(endpoint, {
1921
+ headers: {
1922
+ "User-Agent": "codex-web-local",
1923
+ Accept: "text/html"
1924
+ }
1925
+ });
1926
+ if (!response.ok) {
1927
+ throw new Error(`GitHub trending fetch failed (${response.status})`);
1928
+ }
1929
+ const html = await response.text();
1930
+ return parseGithubTrendingHtml(html, limit);
1931
+ }
1684
1932
  async function listFilesWithRipgrep(cwd) {
1685
1933
  return await new Promise((resolve3, reject) => {
1686
- const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1934
+ const ripgrepCommand = resolveRipgrepCommand();
1935
+ if (!ripgrepCommand) {
1936
+ reject(new Error("ripgrep (rg) is not available"));
1937
+ return;
1938
+ }
1939
+ const proc = spawn2(ripgrepCommand, ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1687
1940
  cwd,
1688
1941
  env: process.env,
1689
1942
  stdio: ["ignore", "pipe", "pipe"]
@@ -1840,14 +2093,70 @@ function normalizeCommitMessage(value) {
1840
2093
  const normalized = value.replace(/\r\n?/gu, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n").trim();
1841
2094
  return normalized.slice(0, 2e3);
1842
2095
  }
1843
- async function hasGitWorkingTreeChanges(cwd) {
1844
- const status = await runCommandWithOutput2("git", ["status", "--porcelain"], { cwd });
2096
+ function getRollbackGitDirForCwd(cwd) {
2097
+ return join3(cwd, ".codex", "rollbacks", ".git");
2098
+ }
2099
+ async function ensureLocalCodexGitignoreHasRollbacks(cwd) {
2100
+ const localCodexDir = join3(cwd, ".codex");
2101
+ const gitignorePath = join3(localCodexDir, ".gitignore");
2102
+ await mkdir2(localCodexDir, { recursive: true });
2103
+ let current = "";
2104
+ try {
2105
+ current = await readFile2(gitignorePath, "utf8");
2106
+ } catch {
2107
+ current = "";
2108
+ }
2109
+ const rows = current.split(/\r?\n/).map((line) => line.trim());
2110
+ if (rows.includes("rollbacks/")) return;
2111
+ const prefix = current.length > 0 && !current.endsWith("\n") ? `${current}
2112
+ ` : current;
2113
+ await writeFile2(gitignorePath, `${prefix}rollbacks/
2114
+ `, "utf8");
2115
+ }
2116
+ async function ensureRollbackGitRepo(cwd) {
2117
+ const gitDir = getRollbackGitDirForCwd(cwd);
2118
+ try {
2119
+ const headInfo = await stat2(join3(gitDir, "HEAD"));
2120
+ if (!headInfo.isFile()) {
2121
+ throw new Error("Invalid rollback git repository");
2122
+ }
2123
+ } catch {
2124
+ await mkdir2(dirname(gitDir), { recursive: true });
2125
+ await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, "init"]);
2126
+ }
2127
+ await runCommand2("git", ["--git-dir", gitDir, "config", "user.email", "codex@local"]);
2128
+ await runCommand2("git", ["--git-dir", gitDir, "config", "user.name", "Codex Rollback"]);
2129
+ try {
2130
+ await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, "rev-parse", "--verify", "HEAD"]);
2131
+ } catch {
2132
+ await runCommand2(
2133
+ "git",
2134
+ ["--git-dir", gitDir, "--work-tree", cwd, "commit", "--allow-empty", "-m", "Initialize rollback history"]
2135
+ );
2136
+ }
2137
+ await ensureLocalCodexGitignoreHasRollbacks(cwd);
2138
+ return gitDir;
2139
+ }
2140
+ async function runRollbackGit(cwd, args) {
2141
+ const gitDir = await ensureRollbackGitRepo(cwd);
2142
+ await runCommand2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2143
+ }
2144
+ async function runRollbackGitCapture(cwd, args) {
2145
+ const gitDir = await ensureRollbackGitRepo(cwd);
2146
+ return await runCommandCapture("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2147
+ }
2148
+ async function runRollbackGitWithOutput(cwd, args) {
2149
+ const gitDir = await ensureRollbackGitRepo(cwd);
2150
+ return await runCommandWithOutput2("git", ["--git-dir", gitDir, "--work-tree", cwd, ...args]);
2151
+ }
2152
+ async function hasRollbackGitWorkingTreeChanges(cwd) {
2153
+ const status = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
1845
2154
  return status.trim().length > 0;
1846
2155
  }
1847
- async function findCommitByExactMessage(cwd, message) {
2156
+ async function findRollbackCommitByExactMessage(cwd, message) {
1848
2157
  const normalizedTarget = normalizeCommitMessage(message);
1849
2158
  if (!normalizedTarget) return "";
1850
- const raw = await runCommandWithOutput2("git", ["log", "--format=%H%x1f%B%x1e"], { cwd });
2159
+ const raw = await runRollbackGitWithOutput(cwd, ["log", "--format=%H%x1f%B%x1e"]);
1851
2160
  const entries = raw.split("");
1852
2161
  for (const entry of entries) {
1853
2162
  if (!entry.trim()) continue;
@@ -2273,10 +2582,17 @@ var AppServerProcess = class {
2273
2582
  'sandbox_mode="danger-full-access"'
2274
2583
  ];
2275
2584
  }
2585
+ getCodexCommand() {
2586
+ const codexCommand = resolveCodexCommand();
2587
+ if (!codexCommand) {
2588
+ throw new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND.");
2589
+ }
2590
+ return codexCommand;
2591
+ }
2276
2592
  start() {
2277
2593
  if (this.process) return;
2278
2594
  this.stopping = false;
2279
- const invocation = getSpawnInvocation(resolveCodexCommand() ?? "codex", this.appServerArgs);
2595
+ const invocation = getSpawnInvocation(this.getCodexCommand(), this.appServerArgs);
2280
2596
  const proc = spawn2(invocation.command, invocation.args, { stdio: ["pipe", "pipe", "pipe"] });
2281
2597
  this.process = proc;
2282
2598
  proc.stdout.setEncoding("utf8");
@@ -2503,7 +2819,12 @@ var MethodCatalog = class {
2503
2819
  }
2504
2820
  async runGenerateSchemaCommand(outDir) {
2505
2821
  await new Promise((resolve3, reject) => {
2506
- const invocation = getSpawnInvocation(resolveCodexCommand() ?? "codex", ["app-server", "generate-json-schema", "--out", outDir]);
2822
+ const codexCommand = resolveCodexCommand();
2823
+ if (!codexCommand) {
2824
+ reject(new Error("Codex CLI is not available. Install @openai/codex or set CODEXUI_CODEX_COMMAND."));
2825
+ return;
2826
+ }
2827
+ const invocation = getSpawnInvocation(codexCommand, ["app-server", "generate-json-schema", "--out", outDir]);
2507
2828
  const process2 = spawn2(invocation.command, invocation.args, {
2508
2829
  stdio: ["ignore", "ignore", "pipe"]
2509
2830
  });
@@ -2748,6 +3069,19 @@ function createCodexBridgeMiddleware() {
2748
3069
  setJson2(res, 200, { data: { path: homedir3() } });
2749
3070
  return;
2750
3071
  }
3072
+ if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
3073
+ const sinceRaw = (url.searchParams.get("since") ?? "").trim().toLowerCase();
3074
+ const since = sinceRaw === "weekly" ? "weekly" : sinceRaw === "monthly" ? "monthly" : "daily";
3075
+ const limitRaw = Number.parseInt((url.searchParams.get("limit") ?? "6").trim(), 10);
3076
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(10, limitRaw)) : 6;
3077
+ try {
3078
+ const data = await fetchGithubTrending(since, limit);
3079
+ setJson2(res, 200, { data });
3080
+ } catch (error) {
3081
+ setJson2(res, 502, { error: getErrorMessage3(error, "Failed to fetch GitHub trending") });
3082
+ }
3083
+ return;
3084
+ }
2751
3085
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
2752
3086
  const payload = asRecord3(await readJsonBody(req));
2753
3087
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
@@ -2842,22 +3176,22 @@ function createCodexBridgeMiddleware() {
2842
3176
  return;
2843
3177
  }
2844
3178
  try {
2845
- await runCommandCapture("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
2846
- const beforeStatus = await runCommandWithOutput2("git", ["status", "--porcelain"], { cwd });
3179
+ await ensureRollbackGitRepo(cwd);
3180
+ const beforeStatus = await runRollbackGitWithOutput(cwd, ["status", "--porcelain"]);
2847
3181
  if (!beforeStatus.trim()) {
2848
3182
  setJson2(res, 200, { data: { committed: false } });
2849
3183
  return;
2850
3184
  }
2851
- await runCommand2("git", ["add", "-A"], { cwd });
2852
- const stagedStatus = await runCommandWithOutput2("git", ["diff", "--cached", "--name-only"], { cwd });
3185
+ await runRollbackGit(cwd, ["add", "-A"]);
3186
+ const stagedStatus = await runRollbackGitWithOutput(cwd, ["diff", "--cached", "--name-only"]);
2853
3187
  if (!stagedStatus.trim()) {
2854
3188
  setJson2(res, 200, { data: { committed: false } });
2855
3189
  return;
2856
3190
  }
2857
- await runCommand2("git", ["commit", "-m", commitMessage], { cwd });
3191
+ await runRollbackGit(cwd, ["commit", "-m", commitMessage]);
2858
3192
  setJson2(res, 200, { data: { committed: true } });
2859
3193
  } catch (error) {
2860
- setJson2(res, 500, { error: getErrorMessage3(error, "Failed to auto-commit worktree changes") });
3194
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to auto-commit rollback changes") });
2861
3195
  }
2862
3196
  return;
2863
3197
  }
@@ -2885,29 +3219,29 @@ function createCodexBridgeMiddleware() {
2885
3219
  return;
2886
3220
  }
2887
3221
  try {
2888
- await runCommandCapture("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
2889
- const commitSha = await findCommitByExactMessage(cwd, commitMessage);
3222
+ await ensureRollbackGitRepo(cwd);
3223
+ const commitSha = await findRollbackCommitByExactMessage(cwd, commitMessage);
2890
3224
  if (!commitSha) {
2891
3225
  setJson2(res, 404, { error: "No matching commit found for this user message" });
2892
3226
  return;
2893
3227
  }
2894
3228
  let resetTargetSha = "";
2895
3229
  try {
2896
- resetTargetSha = await runCommandCapture("git", ["rev-parse", `${commitSha}^`], { cwd });
3230
+ resetTargetSha = await runRollbackGitCapture(cwd, ["rev-parse", `${commitSha}^`]);
2897
3231
  } catch {
2898
3232
  setJson2(res, 409, { error: "Cannot rollback: matched commit has no parent commit" });
2899
3233
  return;
2900
3234
  }
2901
3235
  let stashed = false;
2902
- if (await hasGitWorkingTreeChanges(cwd)) {
3236
+ if (await hasRollbackGitWorkingTreeChanges(cwd)) {
2903
3237
  const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
2904
- await runCommand2("git", ["stash", "push", "-u", "-m", stashMessage], { cwd });
3238
+ await runRollbackGit(cwd, ["stash", "push", "-u", "-m", stashMessage]);
2905
3239
  stashed = true;
2906
3240
  }
2907
- await runCommand2("git", ["reset", "--hard", resetTargetSha], { cwd });
3241
+ await runRollbackGit(cwd, ["reset", "--hard", resetTargetSha]);
2908
3242
  setJson2(res, 200, { data: { reset: true, commitSha, resetTargetSha, stashed } });
2909
3243
  } catch (error) {
2910
- setJson2(res, 500, { error: getErrorMessage3(error, "Failed to rollback worktree to user message commit") });
3244
+ setJson2(res, 500, { error: getErrorMessage3(error, "Failed to rollback project to user message commit") });
2911
3245
  }
2912
3246
  return;
2913
3247
  }
@@ -3252,7 +3586,7 @@ function createAuthSession(password) {
3252
3586
  }
3253
3587
 
3254
3588
  // src/server/localBrowseUi.ts
3255
- import { dirname, extname as extname2, join as join4 } from "path";
3589
+ import { dirname as dirname2, extname as extname2, join as join4 } from "path";
3256
3590
  import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
3257
3591
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
3258
3592
  ".txt",
@@ -3410,7 +3744,7 @@ async function getDirectoryItems(localPath) {
3410
3744
  }
3411
3745
  async function createDirectoryListingHtml(localPath) {
3412
3746
  const items = await getDirectoryItems(localPath);
3413
- const parentPath = dirname(localPath);
3747
+ const parentPath = dirname2(localPath);
3414
3748
  const rows = items.map((item) => {
3415
3749
  const suffix = item.isDirectory ? "/" : "";
3416
3750
  const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
@@ -3507,7 +3841,7 @@ async function createDirectoryListingHtml(localPath) {
3507
3841
  }
3508
3842
  async function createTextEditorHtml(localPath) {
3509
3843
  const content = await readFile3(localPath, "utf8");
3510
- const parentPath = dirname(localPath);
3844
+ const parentPath = dirname2(localPath);
3511
3845
  const language = languageForPath(localPath);
3512
3846
  const safeContentLiteral = escapeForInlineScriptString(content);
3513
3847
  return `<!doctype html>
@@ -3576,7 +3910,7 @@ async function createTextEditorHtml(localPath) {
3576
3910
 
3577
3911
  // src/server/httpServer.ts
3578
3912
  import { WebSocketServer } from "ws";
3579
- var __dirname = dirname2(fileURLToPath(import.meta.url));
3913
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
3580
3914
  var distDir = join5(__dirname, "..", "dist");
3581
3915
  var spaEntryFile = join5(distDir, "index.html");
3582
3916
  var IMAGE_CONTENT_TYPES = {
@@ -3792,7 +4126,7 @@ function generatePassword() {
3792
4126
 
3793
4127
  // src/cli/index.ts
3794
4128
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
3795
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
4129
+ var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
3796
4130
  var hasPromptedCloudflaredInstall = false;
3797
4131
  function getCodexHomePath() {
3798
4132
  return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
@@ -3822,10 +4156,6 @@ async function readCliVersion() {
3822
4156
  function isTermuxRuntime() {
3823
4157
  return Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("/com.termux/"));
3824
4158
  }
3825
- function canRun(command, args = []) {
3826
- const result = canRunCommand(command, args);
3827
- return result;
3828
- }
3829
4159
  function runOrFail(command, args, label) {
3830
4160
  const result = spawnSyncCommand(command, args, { stdio: "inherit" });
3831
4161
  if (result.status !== 0) {
@@ -3837,11 +4167,11 @@ function runWithStatus(command, args) {
3837
4167
  return result.status ?? -1;
3838
4168
  }
3839
4169
  function resolveCloudflaredCommand() {
3840
- if (canRun("cloudflared", ["--version"])) {
4170
+ if (canRunCommand("cloudflared", ["--version"])) {
3841
4171
  return "cloudflared";
3842
4172
  }
3843
4173
  const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
3844
- if (existsSync5(localCandidate) && canRun(localCandidate, ["--version"])) {
4174
+ if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
3845
4175
  return localCandidate;
3846
4176
  }
3847
4177
  return null;
@@ -3901,7 +4231,7 @@ async function ensureCloudflaredInstalledLinux() {
3901
4231
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
3902
4232
  await downloadFile(downloadUrl, destination);
3903
4233
  chmodSync(destination, 493);
3904
- process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
4234
+ process.env.PATH = prependPathEntry(process.env.PATH ?? "", userBinDir);
3905
4235
  const installed = resolveCloudflaredCommand();
3906
4236
  if (!installed) {
3907
4237
  throw new Error("cloudflared download completed but executable is still not available");
@@ -3965,7 +4295,7 @@ function ensureCodexInstalled() {
3965
4295
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
3966
4296
  `);
3967
4297
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
3968
- process.env.PATH = `${join6(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
4298
+ process.env.PATH = prependPathEntry(process.env.PATH ?? "", getNpmGlobalBinDir(userPrefix));
3969
4299
  };
3970
4300
  if (isTermuxRuntime()) {
3971
4301
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -4172,6 +4502,9 @@ async function startServer(options) {
4172
4502
  }
4173
4503
  }
4174
4504
  const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
4505
+ if (codexCommand) {
4506
+ process.env.CODEXUI_CODEX_COMMAND = codexCommand;
4507
+ }
4175
4508
  if (!hasCodexAuth() && codexCommand) {
4176
4509
  console.log("\nCodex is not logged in. Starting `codex login`...\n");
4177
4510
  runOrFail(codexCommand, ["login"], "Codex login");
@@ -4251,6 +4584,7 @@ async function startServer(options) {
4251
4584
  }
4252
4585
  async function runLogin() {
4253
4586
  const codexCommand = ensureCodexInstalled() ?? "codex";
4587
+ process.env.CODEXUI_CODEX_COMMAND = codexCommand;
4254
4588
  console.log("\nStarting `codex login`...\n");
4255
4589
  runOrFail(codexCommand, ["login"], "Codex login");
4256
4590
  }