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/assets/index-BwkNEgMe.css +1 -0
- package/dist/assets/index-C0kJJe0e.js +1429 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +462 -128
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/index-B6t8lUoD.css +0 -1
- package/dist/assets/index-CGldz3Gt.js +0 -1428
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
|
|
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
|
|
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 :
|
|
210
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
69
211
|
}
|
|
70
212
|
function getSkillsInstallDir() {
|
|
71
|
-
return
|
|
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",
|
|
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",
|
|
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:
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
720
|
-
const codexAgentsPath =
|
|
721
|
-
await mkdir(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(///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
|
|
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
|
-
|
|
1844
|
-
|
|
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
|
|
2156
|
+
async function findRollbackCommitByExactMessage(cwd, message) {
|
|
1848
2157
|
const normalizedTarget = normalizeCommitMessage(message);
|
|
1849
2158
|
if (!normalizedTarget) return "";
|
|
1850
|
-
const raw = await
|
|
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(
|
|
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
|
|
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
|
|
2846
|
-
const beforeStatus = await
|
|
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
|
|
2852
|
-
const stagedStatus = await
|
|
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
|
|
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
|
|
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
|
|
2889
|
-
const commitSha = await
|
|
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
|
|
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
|
|
3236
|
+
if (await hasRollbackGitWorkingTreeChanges(cwd)) {
|
|
2903
3237
|
const stashMessage = `codex-auto-stash-before-rollback-${Date.now()}`;
|
|
2904
|
-
await
|
|
3238
|
+
await runRollbackGit(cwd, ["stash", "push", "-u", "-m", stashMessage]);
|
|
2905
3239
|
stashed = true;
|
|
2906
3240
|
}
|
|
2907
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
4170
|
+
if (canRunCommand("cloudflared", ["--version"])) {
|
|
3841
4171
|
return "cloudflared";
|
|
3842
4172
|
}
|
|
3843
4173
|
const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
|
|
3844
|
-
if (existsSync5(localCandidate) &&
|
|
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 =
|
|
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 =
|
|
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
|
}
|