codexapp 0.1.28 → 0.1.30
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-B5wuNFNF.css +1 -0
- package/dist/assets/index-BeRIcrmp.js +1481 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +676 -446
- package/dist-cli/index.js.map +1 -1
- package/package.json +3 -1
- package/dist/assets/index-Bhfj4wvl.js +0 -48
- package/dist/assets/index-CBIbfWDP.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
+
import "dotenv/config";
|
|
4
5
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import {
|
|
6
|
+
import { existsSync as existsSync3 } from "fs";
|
|
6
7
|
import { readFile as readFile2 } from "fs/promises";
|
|
7
|
-
import { homedir as homedir2
|
|
8
|
+
import { homedir as homedir2 } from "os";
|
|
8
9
|
import { join as join3 } from "path";
|
|
9
10
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
10
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
11
12
|
import { dirname as dirname2 } from "path";
|
|
12
|
-
import { get as httpsGet } from "https";
|
|
13
13
|
import { Command } from "commander";
|
|
14
14
|
import qrcode from "qrcode-terminal";
|
|
15
15
|
|
|
16
16
|
// src/server/httpServer.ts
|
|
17
17
|
import { fileURLToPath } from "url";
|
|
18
18
|
import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
|
|
19
|
-
import { existsSync } from "fs";
|
|
20
|
-
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
19
|
+
import { existsSync as existsSync2 } from "fs";
|
|
21
20
|
import express from "express";
|
|
22
21
|
|
|
23
22
|
// src/server/codexAppServerBridge.ts
|
|
23
|
+
import "dotenv/config";
|
|
24
24
|
import { spawn } from "child_process";
|
|
25
|
-
import { randomBytes } from "crypto";
|
|
26
25
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
|
|
26
|
+
import { existsSync } from "fs";
|
|
27
27
|
import { request as httpsRequest } from "https";
|
|
28
28
|
import { homedir } from "os";
|
|
29
29
|
import { tmpdir } from "os";
|
|
30
|
-
import {
|
|
30
|
+
import { isAbsolute, join, resolve } from "path";
|
|
31
31
|
import { writeFile } from "fs/promises";
|
|
32
32
|
function asRecord(value) {
|
|
33
33
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -51,46 +51,6 @@ function setJson(res, statusCode, payload) {
|
|
|
51
51
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
52
52
|
res.end(JSON.stringify(payload));
|
|
53
53
|
}
|
|
54
|
-
function extractThreadMessageText(threadReadPayload) {
|
|
55
|
-
const payload = asRecord(threadReadPayload);
|
|
56
|
-
const thread = asRecord(payload?.thread);
|
|
57
|
-
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
58
|
-
const parts = [];
|
|
59
|
-
for (const turn of turns) {
|
|
60
|
-
const turnRecord = asRecord(turn);
|
|
61
|
-
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
62
|
-
for (const item of items) {
|
|
63
|
-
const itemRecord = asRecord(item);
|
|
64
|
-
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
65
|
-
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
66
|
-
parts.push(itemRecord.text.trim());
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (type === "userMessage") {
|
|
70
|
-
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
71
|
-
for (const block of content) {
|
|
72
|
-
const blockRecord = asRecord(block);
|
|
73
|
-
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
74
|
-
parts.push(blockRecord.text.trim());
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (type === "commandExecution") {
|
|
80
|
-
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
81
|
-
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
82
|
-
if (command) parts.push(command);
|
|
83
|
-
if (output) parts.push(output);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return parts.join("\n").trim();
|
|
88
|
-
}
|
|
89
|
-
function isExactPhraseMatch(query, doc) {
|
|
90
|
-
const q = query.trim().toLowerCase();
|
|
91
|
-
if (!q) return false;
|
|
92
|
-
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
93
|
-
}
|
|
94
54
|
function scoreFileCandidate(path, query) {
|
|
95
55
|
if (!query) return 0;
|
|
96
56
|
const lowerPath = path.toLowerCase();
|
|
@@ -164,30 +124,8 @@ async function runCommand(command, args, options = {}) {
|
|
|
164
124
|
});
|
|
165
125
|
});
|
|
166
126
|
}
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head");
|
|
170
|
-
}
|
|
171
|
-
function isNotGitRepositoryError(error) {
|
|
172
|
-
const message = getErrorMessage(error, "").toLowerCase();
|
|
173
|
-
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
174
|
-
}
|
|
175
|
-
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
176
|
-
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
177
|
-
try {
|
|
178
|
-
await stat(agentsPath);
|
|
179
|
-
} catch {
|
|
180
|
-
await writeFile(agentsPath, "", "utf8");
|
|
181
|
-
}
|
|
182
|
-
await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
183
|
-
await runCommand(
|
|
184
|
-
"git",
|
|
185
|
-
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
186
|
-
{ cwd: repoRoot }
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
async function runCommandCapture(command, args, options = {}) {
|
|
190
|
-
return await new Promise((resolveOutput, reject) => {
|
|
127
|
+
async function runCommandWithOutput(command, args, options = {}) {
|
|
128
|
+
return await new Promise((resolve2, reject) => {
|
|
191
129
|
const proc = spawn(command, args, {
|
|
192
130
|
cwd: options.cwd,
|
|
193
131
|
env: process.env,
|
|
@@ -204,7 +142,7 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
204
142
|
proc.on("error", reject);
|
|
205
143
|
proc.on("close", (code) => {
|
|
206
144
|
if (code === 0) {
|
|
207
|
-
|
|
145
|
+
resolve2(stdout);
|
|
208
146
|
return;
|
|
209
147
|
}
|
|
210
148
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -270,7 +208,7 @@ async function fetchSkillsTree() {
|
|
|
270
208
|
if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
|
|
271
209
|
return skillsTreeCache.entries;
|
|
272
210
|
}
|
|
273
|
-
const resp = await ghFetch(
|
|
211
|
+
const resp = await ghFetch(`https://api.github.com/repos/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/git/trees/main?recursive=1`);
|
|
274
212
|
if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
|
|
275
213
|
const data = await resp.json();
|
|
276
214
|
const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
|
|
@@ -286,7 +224,7 @@ async function fetchSkillsTree() {
|
|
|
286
224
|
entries.push({
|
|
287
225
|
name: skillName,
|
|
288
226
|
owner,
|
|
289
|
-
url: `https://github.com/
|
|
227
|
+
url: `https://github.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/tree/main/skills/${owner}/${skillName}`
|
|
290
228
|
});
|
|
291
229
|
}
|
|
292
230
|
skillsTreeCache = { entries, fetchedAt: Date.now() };
|
|
@@ -298,7 +236,7 @@ async function fetchMetaBatch(entries) {
|
|
|
298
236
|
const batch = toFetch.slice(0, 50);
|
|
299
237
|
const results = await Promise.allSettled(
|
|
300
238
|
batch.map(async (e) => {
|
|
301
|
-
const rawUrl = `https://raw.githubusercontent.com/
|
|
239
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
|
|
302
240
|
const resp = await fetch(rawUrl);
|
|
303
241
|
if (!resp.ok) return;
|
|
304
242
|
const meta = await resp.json();
|
|
@@ -324,6 +262,23 @@ function buildHubEntry(e) {
|
|
|
324
262
|
installed: false
|
|
325
263
|
};
|
|
326
264
|
}
|
|
265
|
+
var GITHUB_DEVICE_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
266
|
+
var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
|
|
267
|
+
var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
|
|
268
|
+
var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
|
|
269
|
+
var SYNC_UPSTREAM_SKILLS_REPO = "skills";
|
|
270
|
+
var HUB_SKILLS_OWNER = "openclaw";
|
|
271
|
+
var HUB_SKILLS_REPO = "skills";
|
|
272
|
+
var startupSkillsSyncInitialized = false;
|
|
273
|
+
var startupSyncStatus = {
|
|
274
|
+
inProgress: false,
|
|
275
|
+
mode: "idle",
|
|
276
|
+
branch: getPreferredSyncBranch(),
|
|
277
|
+
lastAction: "not-started",
|
|
278
|
+
lastRunAtIso: "",
|
|
279
|
+
lastSuccessAtIso: "",
|
|
280
|
+
lastError: ""
|
|
281
|
+
};
|
|
327
282
|
async function scanInstalledSkillsFromDisk() {
|
|
328
283
|
const map = /* @__PURE__ */ new Map();
|
|
329
284
|
const skillsDir = getSkillsInstallDir();
|
|
@@ -342,6 +297,448 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
342
297
|
}
|
|
343
298
|
return map;
|
|
344
299
|
}
|
|
300
|
+
function getSkillsSyncStatePath() {
|
|
301
|
+
return join(getCodexHomeDir(), "skills-sync.json");
|
|
302
|
+
}
|
|
303
|
+
async function readSkillsSyncState() {
|
|
304
|
+
try {
|
|
305
|
+
const raw = await readFile(getSkillsSyncStatePath(), "utf8");
|
|
306
|
+
const parsed = JSON.parse(raw);
|
|
307
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
308
|
+
} catch {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function writeSkillsSyncState(state) {
|
|
313
|
+
await writeFile(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
|
|
314
|
+
}
|
|
315
|
+
async function getGithubJson(url, token, method = "GET", body) {
|
|
316
|
+
const resp = await fetch(url, {
|
|
317
|
+
method,
|
|
318
|
+
headers: {
|
|
319
|
+
Accept: "application/vnd.github+json",
|
|
320
|
+
"Content-Type": "application/json",
|
|
321
|
+
Authorization: `Bearer ${token}`,
|
|
322
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
323
|
+
"User-Agent": "codex-web-local"
|
|
324
|
+
},
|
|
325
|
+
body: body ? JSON.stringify(body) : void 0
|
|
326
|
+
});
|
|
327
|
+
if (!resp.ok) {
|
|
328
|
+
const text = await resp.text();
|
|
329
|
+
throw new Error(`GitHub API ${method} ${url} failed (${resp.status}): ${text}`);
|
|
330
|
+
}
|
|
331
|
+
return await resp.json();
|
|
332
|
+
}
|
|
333
|
+
async function startGithubDeviceLogin() {
|
|
334
|
+
const resp = await fetch("https://github.com/login/device/code", {
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: {
|
|
337
|
+
Accept: "application/json",
|
|
338
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
339
|
+
"User-Agent": "codex-web-local"
|
|
340
|
+
},
|
|
341
|
+
body: new URLSearchParams({
|
|
342
|
+
client_id: GITHUB_DEVICE_CLIENT_ID,
|
|
343
|
+
scope: "repo read:user"
|
|
344
|
+
})
|
|
345
|
+
});
|
|
346
|
+
if (!resp.ok) {
|
|
347
|
+
throw new Error(`GitHub device flow init failed (${resp.status})`);
|
|
348
|
+
}
|
|
349
|
+
return await resp.json();
|
|
350
|
+
}
|
|
351
|
+
async function completeGithubDeviceLogin(deviceCode) {
|
|
352
|
+
const resp = await fetch("https://github.com/login/oauth/access_token", {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: {
|
|
355
|
+
Accept: "application/json",
|
|
356
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
357
|
+
"User-Agent": "codex-web-local"
|
|
358
|
+
},
|
|
359
|
+
body: new URLSearchParams({
|
|
360
|
+
client_id: GITHUB_DEVICE_CLIENT_ID,
|
|
361
|
+
device_code: deviceCode,
|
|
362
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
363
|
+
})
|
|
364
|
+
});
|
|
365
|
+
if (!resp.ok) {
|
|
366
|
+
throw new Error(`GitHub token exchange failed (${resp.status})`);
|
|
367
|
+
}
|
|
368
|
+
const payload = await resp.json();
|
|
369
|
+
if (!payload.access_token) return { token: null, error: payload.error || "unknown_error" };
|
|
370
|
+
return { token: payload.access_token, error: null };
|
|
371
|
+
}
|
|
372
|
+
function isAndroidLikeRuntime() {
|
|
373
|
+
if (process.platform === "android") return true;
|
|
374
|
+
if (existsSync("/data/data/com.termux")) return true;
|
|
375
|
+
if (process.env.TERMUX_VERSION) return true;
|
|
376
|
+
const prefix = process.env.PREFIX?.toLowerCase() ?? "";
|
|
377
|
+
if (prefix.includes("/com.termux/")) return true;
|
|
378
|
+
const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
|
|
379
|
+
if (proot.length > 0) return true;
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
function getPreferredSyncBranch() {
|
|
383
|
+
return isAndroidLikeRuntime() ? "android" : "main";
|
|
384
|
+
}
|
|
385
|
+
function isUpstreamSkillsRepo(repoOwner, repoName) {
|
|
386
|
+
return repoOwner.toLowerCase() === SYNC_UPSTREAM_SKILLS_OWNER.toLowerCase() && repoName.toLowerCase() === SYNC_UPSTREAM_SKILLS_REPO.toLowerCase();
|
|
387
|
+
}
|
|
388
|
+
async function resolveGithubUsername(token) {
|
|
389
|
+
const user = await getGithubJson("https://api.github.com/user", token);
|
|
390
|
+
return user.login;
|
|
391
|
+
}
|
|
392
|
+
async function ensurePrivateForkFromUpstream(token, username, repoName) {
|
|
393
|
+
const repoUrl = `https://api.github.com/repos/${username}/${repoName}`;
|
|
394
|
+
let created = false;
|
|
395
|
+
const existing = await fetch(repoUrl, {
|
|
396
|
+
headers: {
|
|
397
|
+
Accept: "application/vnd.github+json",
|
|
398
|
+
Authorization: `Bearer ${token}`,
|
|
399
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
400
|
+
"User-Agent": "codex-web-local"
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
if (existing.ok) {
|
|
404
|
+
const details = await existing.json();
|
|
405
|
+
if (details.private === true) return;
|
|
406
|
+
await getGithubJson(repoUrl, token, "PATCH", { private: true });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (existing.status !== 404) {
|
|
410
|
+
throw new Error(`Failed to check personal repo existence (${existing.status})`);
|
|
411
|
+
}
|
|
412
|
+
await getGithubJson(
|
|
413
|
+
"https://api.github.com/user/repos",
|
|
414
|
+
token,
|
|
415
|
+
"POST",
|
|
416
|
+
{ name: repoName, private: true, auto_init: false, description: "Codex skills private mirror sync" }
|
|
417
|
+
);
|
|
418
|
+
created = true;
|
|
419
|
+
let ready = false;
|
|
420
|
+
for (let i = 0; i < 20; i++) {
|
|
421
|
+
const check = await fetch(repoUrl, {
|
|
422
|
+
headers: {
|
|
423
|
+
Accept: "application/vnd.github+json",
|
|
424
|
+
Authorization: `Bearer ${token}`,
|
|
425
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
426
|
+
"User-Agent": "codex-web-local"
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
if (check.ok) {
|
|
430
|
+
ready = true;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
434
|
+
}
|
|
435
|
+
if (!ready) throw new Error("Private mirror repo was created but is not available yet");
|
|
436
|
+
if (!created) return;
|
|
437
|
+
const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
|
|
438
|
+
try {
|
|
439
|
+
const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
440
|
+
const branch = getPreferredSyncBranch();
|
|
441
|
+
try {
|
|
442
|
+
await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
|
|
443
|
+
} catch {
|
|
444
|
+
await runCommand("git", ["clone", "--depth", "1", upstreamUrl, tmp]);
|
|
445
|
+
}
|
|
446
|
+
const privateRemote = toGitHubTokenRemote(username, repoName, token);
|
|
447
|
+
await runCommand("git", ["remote", "set-url", "origin", privateRemote], { cwd: tmp });
|
|
448
|
+
try {
|
|
449
|
+
await runCommand("git", ["checkout", "-B", branch], { cwd: tmp });
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
await runCommand("git", ["push", "-u", "origin", `HEAD:${branch}`], { cwd: tmp });
|
|
453
|
+
} finally {
|
|
454
|
+
await rm(tmp, { recursive: true, force: true });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function readRemoteSkillsManifest(token, repoOwner, repoName) {
|
|
458
|
+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
459
|
+
const resp = await fetch(url, {
|
|
460
|
+
headers: {
|
|
461
|
+
Accept: "application/vnd.github+json",
|
|
462
|
+
Authorization: `Bearer ${token}`,
|
|
463
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
464
|
+
"User-Agent": "codex-web-local"
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
if (resp.status === 404) return [];
|
|
468
|
+
if (!resp.ok) throw new Error(`Failed to read remote manifest (${resp.status})`);
|
|
469
|
+
const payload = await resp.json();
|
|
470
|
+
const content = payload.content ? Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8") : "[]";
|
|
471
|
+
const parsed = JSON.parse(content);
|
|
472
|
+
if (!Array.isArray(parsed)) return [];
|
|
473
|
+
const skills = [];
|
|
474
|
+
for (const row of parsed) {
|
|
475
|
+
const item = asRecord(row);
|
|
476
|
+
const owner = typeof item?.owner === "string" ? item.owner : "";
|
|
477
|
+
const name = typeof item?.name === "string" ? item.name : "";
|
|
478
|
+
if (!name) continue;
|
|
479
|
+
skills.push({ ...owner ? { owner } : {}, name, enabled: item?.enabled !== false });
|
|
480
|
+
}
|
|
481
|
+
return skills;
|
|
482
|
+
}
|
|
483
|
+
async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
|
|
484
|
+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
485
|
+
let sha = "";
|
|
486
|
+
const existing = await fetch(url, {
|
|
487
|
+
headers: {
|
|
488
|
+
Accept: "application/vnd.github+json",
|
|
489
|
+
Authorization: `Bearer ${token}`,
|
|
490
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
491
|
+
"User-Agent": "codex-web-local"
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
if (existing.ok) {
|
|
495
|
+
const payload = await existing.json();
|
|
496
|
+
sha = payload.sha ?? "";
|
|
497
|
+
}
|
|
498
|
+
const content = Buffer.from(JSON.stringify(skills, null, 2), "utf8").toString("base64");
|
|
499
|
+
await getGithubJson(url, token, "PUT", {
|
|
500
|
+
message: "Update synced skills manifest",
|
|
501
|
+
content,
|
|
502
|
+
...sha ? { sha } : {}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
function toGitHubTokenRemote(repoOwner, repoName, token) {
|
|
506
|
+
return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
|
|
507
|
+
}
|
|
508
|
+
async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
|
|
509
|
+
const localDir = getSkillsInstallDir();
|
|
510
|
+
await mkdir(localDir, { recursive: true });
|
|
511
|
+
const gitDir = join(localDir, ".git");
|
|
512
|
+
let hasGitDir = false;
|
|
513
|
+
try {
|
|
514
|
+
hasGitDir = (await stat(gitDir)).isDirectory();
|
|
515
|
+
} catch {
|
|
516
|
+
hasGitDir = false;
|
|
517
|
+
}
|
|
518
|
+
if (!hasGitDir) {
|
|
519
|
+
await runCommand("git", ["init"], { cwd: localDir });
|
|
520
|
+
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: localDir });
|
|
521
|
+
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: localDir });
|
|
522
|
+
await runCommand("git", ["add", "-A"], { cwd: localDir });
|
|
523
|
+
try {
|
|
524
|
+
await runCommand("git", ["commit", "-m", "Local skills snapshot before sync"], { cwd: localDir });
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
await runCommand("git", ["branch", "-M", branch], { cwd: localDir });
|
|
528
|
+
try {
|
|
529
|
+
await runCommand("git", ["remote", "add", "origin", repoUrl], { cwd: localDir });
|
|
530
|
+
} catch {
|
|
531
|
+
await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
|
|
532
|
+
}
|
|
533
|
+
await runCommand("git", ["fetch", "origin"], { cwd: localDir });
|
|
534
|
+
try {
|
|
535
|
+
await runCommand("git", ["merge", "--allow-unrelated-histories", "--no-edit", `origin/${branch}`], { cwd: localDir });
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
return localDir;
|
|
539
|
+
}
|
|
540
|
+
await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
|
|
541
|
+
await runCommand("git", ["fetch", "origin"], { cwd: localDir });
|
|
542
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
543
|
+
try {
|
|
544
|
+
await runCommand("git", ["checkout", branch], { cwd: localDir });
|
|
545
|
+
} catch {
|
|
546
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
547
|
+
await runCommand("git", ["checkout", "-B", branch], { cwd: localDir });
|
|
548
|
+
}
|
|
549
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
550
|
+
try {
|
|
551
|
+
await runCommand("git", ["stash", "push", "--include-untracked", "-m", "codex-skills-autostash"], { cwd: localDir });
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
|
|
556
|
+
} catch {
|
|
557
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
await runCommand("git", ["stash", "pop"], { cwd: localDir });
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
return localDir;
|
|
564
|
+
}
|
|
565
|
+
async function resolveMergeConflictsByNewerCommit(repoDir, branch) {
|
|
566
|
+
const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
|
567
|
+
if (unmerged.length === 0) return;
|
|
568
|
+
for (const path of unmerged) {
|
|
569
|
+
const oursTime = await getCommitTime(repoDir, "HEAD", path);
|
|
570
|
+
const theirsTime = await getCommitTime(repoDir, `origin/${branch}`, path);
|
|
571
|
+
if (theirsTime > oursTime) {
|
|
572
|
+
await runCommand("git", ["checkout", "--theirs", "--", path], { cwd: repoDir });
|
|
573
|
+
} else {
|
|
574
|
+
await runCommand("git", ["checkout", "--ours", "--", path], { cwd: repoDir });
|
|
575
|
+
}
|
|
576
|
+
await runCommand("git", ["add", "--", path], { cwd: repoDir });
|
|
577
|
+
}
|
|
578
|
+
const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
|
|
579
|
+
if (mergeHead) {
|
|
580
|
+
await runCommand("git", ["commit", "-m", "Auto-resolve skills merge by newer file"], { cwd: repoDir });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function getCommitTime(repoDir, ref, path) {
|
|
584
|
+
try {
|
|
585
|
+
const output = (await runCommandWithOutput("git", ["log", "-1", "--format=%ct", ref, "--", path], { cwd: repoDir })).trim();
|
|
586
|
+
return output ? Number.parseInt(output, 10) : 0;
|
|
587
|
+
} catch {
|
|
588
|
+
return 0;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
|
|
592
|
+
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
593
|
+
const branch = getPreferredSyncBranch();
|
|
594
|
+
const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
595
|
+
const addPaths = ["."];
|
|
596
|
+
void _installedMap;
|
|
597
|
+
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
|
|
598
|
+
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
|
|
599
|
+
await runCommand("git", ["add", ...addPaths], { cwd: repoDir });
|
|
600
|
+
const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
|
|
601
|
+
if (!status) return;
|
|
602
|
+
await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
|
|
603
|
+
await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
|
|
604
|
+
}
|
|
605
|
+
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName, _localSkillsDir) {
|
|
606
|
+
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
607
|
+
const branch = getPreferredSyncBranch();
|
|
608
|
+
await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
609
|
+
}
|
|
610
|
+
async function bootstrapSkillsFromUpstreamIntoLocal(_localSkillsDir) {
|
|
611
|
+
void _localSkillsDir;
|
|
612
|
+
const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
613
|
+
const branch = getPreferredSyncBranch();
|
|
614
|
+
await ensureSkillsWorkingTreeRepo(repoUrl, branch);
|
|
615
|
+
}
|
|
616
|
+
async function collectLocalSyncedSkills(appServer) {
|
|
617
|
+
const state = await readSkillsSyncState();
|
|
618
|
+
const owners = { ...state.installedOwners ?? {} };
|
|
619
|
+
const tree = await fetchSkillsTree();
|
|
620
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
621
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
622
|
+
for (const entry of tree) {
|
|
623
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
624
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
625
|
+
if (!existingOwner) {
|
|
626
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (existingOwner !== entry.owner) {
|
|
630
|
+
uniqueOwnerByName.delete(entry.name);
|
|
631
|
+
ambiguousNames.add(entry.name);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const skills = await appServer.rpc("skills/list", {});
|
|
635
|
+
const seen = /* @__PURE__ */ new Set();
|
|
636
|
+
const synced = [];
|
|
637
|
+
let ownersChanged = false;
|
|
638
|
+
for (const entry of skills.data ?? []) {
|
|
639
|
+
for (const skill of entry.skills ?? []) {
|
|
640
|
+
const name = typeof skill.name === "string" ? skill.name : "";
|
|
641
|
+
if (!name || seen.has(name)) continue;
|
|
642
|
+
seen.add(name);
|
|
643
|
+
let owner = owners[name];
|
|
644
|
+
if (!owner) {
|
|
645
|
+
owner = uniqueOwnerByName.get(name) ?? "";
|
|
646
|
+
if (owner) {
|
|
647
|
+
owners[name] = owner;
|
|
648
|
+
ownersChanged = true;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
synced.push({ ...owner ? { owner } : {}, name, enabled: skill.enabled !== false });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (ownersChanged) {
|
|
655
|
+
await writeSkillsSyncState({ ...state, installedOwners: owners });
|
|
656
|
+
}
|
|
657
|
+
synced.sort((a, b) => `${a.owner ?? ""}/${a.name}`.localeCompare(`${b.owner ?? ""}/${b.name}`));
|
|
658
|
+
return synced;
|
|
659
|
+
}
|
|
660
|
+
async function autoPushSyncedSkills(appServer) {
|
|
661
|
+
const state = await readSkillsSyncState();
|
|
662
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) return;
|
|
663
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
664
|
+
throw new Error("Refusing to push to upstream skills repository");
|
|
665
|
+
}
|
|
666
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
667
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
668
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
669
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
670
|
+
}
|
|
671
|
+
async function initializeSkillsSyncOnStartup(appServer) {
|
|
672
|
+
if (startupSkillsSyncInitialized) return;
|
|
673
|
+
startupSkillsSyncInitialized = true;
|
|
674
|
+
startupSyncStatus.inProgress = true;
|
|
675
|
+
startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
676
|
+
startupSyncStatus.lastError = "";
|
|
677
|
+
startupSyncStatus.branch = getPreferredSyncBranch();
|
|
678
|
+
try {
|
|
679
|
+
const state = await readSkillsSyncState();
|
|
680
|
+
const localSkillsDir = getSkillsInstallDir();
|
|
681
|
+
if (!state.githubToken) {
|
|
682
|
+
if (!isAndroidLikeRuntime()) {
|
|
683
|
+
startupSyncStatus.mode = "idle";
|
|
684
|
+
startupSyncStatus.lastAction = "skip-upstream-non-android";
|
|
685
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
startupSyncStatus.mode = "unauthenticated-bootstrap";
|
|
689
|
+
startupSyncStatus.lastAction = "pull-upstream";
|
|
690
|
+
await bootstrapSkillsFromUpstreamIntoLocal(localSkillsDir);
|
|
691
|
+
try {
|
|
692
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
696
|
+
startupSyncStatus.lastAction = "pull-upstream-complete";
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
startupSyncStatus.mode = "authenticated-fork-sync";
|
|
700
|
+
startupSyncStatus.lastAction = "ensure-private-fork";
|
|
701
|
+
const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
|
|
702
|
+
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
703
|
+
await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
|
|
704
|
+
const nextState = { ...state, githubUsername: username, repoOwner: username, repoName };
|
|
705
|
+
await writeSkillsSyncState(nextState);
|
|
706
|
+
startupSyncStatus.lastAction = "pull-private-fork";
|
|
707
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName, localSkillsDir);
|
|
708
|
+
try {
|
|
709
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
startupSyncStatus.lastAction = "push-private-fork";
|
|
713
|
+
await autoPushSyncedSkills(appServer);
|
|
714
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
715
|
+
startupSyncStatus.lastAction = "startup-sync-complete";
|
|
716
|
+
} catch (error) {
|
|
717
|
+
startupSyncStatus.lastError = getErrorMessage(error, "startup-sync-failed");
|
|
718
|
+
startupSyncStatus.lastAction = "startup-sync-failed";
|
|
719
|
+
} finally {
|
|
720
|
+
startupSyncStatus.inProgress = false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
724
|
+
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
725
|
+
await ensurePrivateForkFromUpstream(token, username, repoName);
|
|
726
|
+
const current = await readSkillsSyncState();
|
|
727
|
+
await writeSkillsSyncState({
|
|
728
|
+
...current,
|
|
729
|
+
githubToken: token,
|
|
730
|
+
githubUsername: username,
|
|
731
|
+
repoOwner: username,
|
|
732
|
+
repoName
|
|
733
|
+
});
|
|
734
|
+
const localDir = getSkillsInstallDir();
|
|
735
|
+
await pullInstalledSkillsFolderFromRepo(token, username, repoName, localDir);
|
|
736
|
+
try {
|
|
737
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
await autoPushSyncedSkills(appServer);
|
|
741
|
+
}
|
|
345
742
|
async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
346
743
|
const q = query.toLowerCase().trim();
|
|
347
744
|
let filtered = q ? allEntries.filter((s) => {
|
|
@@ -933,82 +1330,9 @@ function getSharedBridgeState() {
|
|
|
933
1330
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
934
1331
|
return created;
|
|
935
1332
|
}
|
|
936
|
-
async function loadAllThreadsForSearch(appServer) {
|
|
937
|
-
const threads = [];
|
|
938
|
-
let cursor = null;
|
|
939
|
-
do {
|
|
940
|
-
const response = asRecord(await appServer.rpc("thread/list", {
|
|
941
|
-
archived: false,
|
|
942
|
-
limit: 100,
|
|
943
|
-
sortKey: "updated_at",
|
|
944
|
-
cursor
|
|
945
|
-
}));
|
|
946
|
-
const data = Array.isArray(response?.data) ? response.data : [];
|
|
947
|
-
for (const row of data) {
|
|
948
|
-
const record = asRecord(row);
|
|
949
|
-
const id = typeof record?.id === "string" ? record.id : "";
|
|
950
|
-
if (!id) continue;
|
|
951
|
-
const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
|
|
952
|
-
const preview = typeof record?.preview === "string" ? record.preview : "";
|
|
953
|
-
threads.push({ id, title, preview });
|
|
954
|
-
}
|
|
955
|
-
cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
|
|
956
|
-
} while (cursor);
|
|
957
|
-
const docs = [];
|
|
958
|
-
const concurrency = 4;
|
|
959
|
-
for (let offset = 0; offset < threads.length; offset += concurrency) {
|
|
960
|
-
const batch = threads.slice(offset, offset + concurrency);
|
|
961
|
-
const loaded = await Promise.all(batch.map(async (thread) => {
|
|
962
|
-
try {
|
|
963
|
-
const readResponse = await appServer.rpc("thread/read", {
|
|
964
|
-
threadId: thread.id,
|
|
965
|
-
includeTurns: true
|
|
966
|
-
});
|
|
967
|
-
const messageText = extractThreadMessageText(readResponse);
|
|
968
|
-
const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
|
|
969
|
-
return {
|
|
970
|
-
id: thread.id,
|
|
971
|
-
title: thread.title,
|
|
972
|
-
preview: thread.preview,
|
|
973
|
-
messageText,
|
|
974
|
-
searchableText
|
|
975
|
-
};
|
|
976
|
-
} catch {
|
|
977
|
-
const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
|
|
978
|
-
return {
|
|
979
|
-
id: thread.id,
|
|
980
|
-
title: thread.title,
|
|
981
|
-
preview: thread.preview,
|
|
982
|
-
messageText: "",
|
|
983
|
-
searchableText
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
}));
|
|
987
|
-
docs.push(...loaded);
|
|
988
|
-
}
|
|
989
|
-
return docs;
|
|
990
|
-
}
|
|
991
|
-
async function buildThreadSearchIndex(appServer) {
|
|
992
|
-
const docs = await loadAllThreadsForSearch(appServer);
|
|
993
|
-
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
994
|
-
return { docsById };
|
|
995
|
-
}
|
|
996
1333
|
function createCodexBridgeMiddleware() {
|
|
997
1334
|
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
998
|
-
|
|
999
|
-
let threadSearchIndexPromise = null;
|
|
1000
|
-
async function getThreadSearchIndex() {
|
|
1001
|
-
if (threadSearchIndex) return threadSearchIndex;
|
|
1002
|
-
if (!threadSearchIndexPromise) {
|
|
1003
|
-
threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
|
|
1004
|
-
threadSearchIndex = index;
|
|
1005
|
-
return index;
|
|
1006
|
-
}).finally(() => {
|
|
1007
|
-
threadSearchIndexPromise = null;
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
return threadSearchIndexPromise;
|
|
1011
|
-
}
|
|
1335
|
+
void initializeSkillsSyncOnStartup(appServer);
|
|
1012
1336
|
const middleware = async (req, res, next) => {
|
|
1013
1337
|
try {
|
|
1014
1338
|
if (!req.url) {
|
|
@@ -1074,76 +1398,6 @@ function createCodexBridgeMiddleware() {
|
|
|
1074
1398
|
setJson(res, 200, { data: { path: homedir() } });
|
|
1075
1399
|
return;
|
|
1076
1400
|
}
|
|
1077
|
-
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1078
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1079
|
-
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1080
|
-
if (!rawSourceCwd) {
|
|
1081
|
-
setJson(res, 400, { error: "Missing sourceCwd" });
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1085
|
-
try {
|
|
1086
|
-
const sourceInfo = await stat(sourceCwd);
|
|
1087
|
-
if (!sourceInfo.isDirectory()) {
|
|
1088
|
-
setJson(res, 400, { error: "sourceCwd is not a directory" });
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
} catch {
|
|
1092
|
-
setJson(res, 404, { error: "sourceCwd does not exist" });
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
try {
|
|
1096
|
-
let gitRoot = "";
|
|
1097
|
-
try {
|
|
1098
|
-
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1099
|
-
} catch (error) {
|
|
1100
|
-
if (!isNotGitRepositoryError(error)) throw error;
|
|
1101
|
-
await runCommand("git", ["init"], { cwd: sourceCwd });
|
|
1102
|
-
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1103
|
-
}
|
|
1104
|
-
const repoName = basename(gitRoot) || "repo";
|
|
1105
|
-
const worktreesRoot = join(getCodexHomeDir(), "worktrees");
|
|
1106
|
-
await mkdir(worktreesRoot, { recursive: true });
|
|
1107
|
-
let worktreeId = "";
|
|
1108
|
-
let worktreeParent = "";
|
|
1109
|
-
let worktreeCwd = "";
|
|
1110
|
-
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1111
|
-
const candidate = randomBytes(2).toString("hex");
|
|
1112
|
-
const parent = join(worktreesRoot, candidate);
|
|
1113
|
-
try {
|
|
1114
|
-
await stat(parent);
|
|
1115
|
-
continue;
|
|
1116
|
-
} catch {
|
|
1117
|
-
worktreeId = candidate;
|
|
1118
|
-
worktreeParent = parent;
|
|
1119
|
-
worktreeCwd = join(parent, repoName);
|
|
1120
|
-
break;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
if (!worktreeId || !worktreeParent || !worktreeCwd) {
|
|
1124
|
-
throw new Error("Failed to allocate a unique worktree id");
|
|
1125
|
-
}
|
|
1126
|
-
const branch = `codex/${worktreeId}`;
|
|
1127
|
-
await mkdir(worktreeParent, { recursive: true });
|
|
1128
|
-
try {
|
|
1129
|
-
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
if (!isMissingHeadError(error)) throw error;
|
|
1132
|
-
await ensureRepoHasInitialCommit(gitRoot);
|
|
1133
|
-
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1134
|
-
}
|
|
1135
|
-
setJson(res, 200, {
|
|
1136
|
-
data: {
|
|
1137
|
-
cwd: worktreeCwd,
|
|
1138
|
-
branch,
|
|
1139
|
-
gitRoot
|
|
1140
|
-
}
|
|
1141
|
-
});
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
|
|
1144
|
-
}
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
1401
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1148
1402
|
const payload = await readJsonBody(req);
|
|
1149
1403
|
const record = asRecord(payload);
|
|
@@ -1269,20 +1523,6 @@ function createCodexBridgeMiddleware() {
|
|
|
1269
1523
|
setJson(res, 200, { data: cache });
|
|
1270
1524
|
return;
|
|
1271
1525
|
}
|
|
1272
|
-
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1273
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1274
|
-
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1275
|
-
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1276
|
-
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1277
|
-
if (!query) {
|
|
1278
|
-
setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
const index = await getThreadSearchIndex();
|
|
1282
|
-
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1283
|
-
setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
1526
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1287
1527
|
const payload = asRecord(await readJsonBody(req));
|
|
1288
1528
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
@@ -1339,6 +1579,182 @@ function createCodexBridgeMiddleware() {
|
|
|
1339
1579
|
}
|
|
1340
1580
|
return;
|
|
1341
1581
|
}
|
|
1582
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
1583
|
+
const state = await readSkillsSyncState();
|
|
1584
|
+
setJson(res, 200, {
|
|
1585
|
+
data: {
|
|
1586
|
+
loggedIn: Boolean(state.githubToken),
|
|
1587
|
+
githubUsername: state.githubUsername ?? "",
|
|
1588
|
+
repoOwner: state.repoOwner ?? "",
|
|
1589
|
+
repoName: state.repoName ?? "",
|
|
1590
|
+
configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
|
|
1591
|
+
startup: {
|
|
1592
|
+
inProgress: startupSyncStatus.inProgress,
|
|
1593
|
+
mode: startupSyncStatus.mode,
|
|
1594
|
+
branch: startupSyncStatus.branch,
|
|
1595
|
+
lastAction: startupSyncStatus.lastAction,
|
|
1596
|
+
lastRunAtIso: startupSyncStatus.lastRunAtIso,
|
|
1597
|
+
lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
|
|
1598
|
+
lastError: startupSyncStatus.lastError
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
|
|
1605
|
+
try {
|
|
1606
|
+
const started = await startGithubDeviceLogin();
|
|
1607
|
+
setJson(res, 200, { data: started });
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
|
|
1610
|
+
}
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
1614
|
+
try {
|
|
1615
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1616
|
+
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
1617
|
+
if (!token) {
|
|
1618
|
+
setJson(res, 400, { error: "Missing GitHub token" });
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const username = await resolveGithubUsername(token);
|
|
1622
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
1623
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
|
|
1626
|
+
}
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
|
|
1630
|
+
try {
|
|
1631
|
+
const state = await readSkillsSyncState();
|
|
1632
|
+
await writeSkillsSyncState({
|
|
1633
|
+
...state,
|
|
1634
|
+
githubToken: void 0,
|
|
1635
|
+
githubUsername: void 0,
|
|
1636
|
+
repoOwner: void 0,
|
|
1637
|
+
repoName: void 0
|
|
1638
|
+
});
|
|
1639
|
+
setJson(res, 200, { ok: true });
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
|
|
1642
|
+
}
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
1646
|
+
try {
|
|
1647
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1648
|
+
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
1649
|
+
if (!deviceCode) {
|
|
1650
|
+
setJson(res, 400, { error: "Missing deviceCode" });
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const result = await completeGithubDeviceLogin(deviceCode);
|
|
1654
|
+
if (!result.token) {
|
|
1655
|
+
setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const token = result.token;
|
|
1659
|
+
const username = await resolveGithubUsername(token);
|
|
1660
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
1661
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
|
|
1664
|
+
}
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
|
|
1668
|
+
try {
|
|
1669
|
+
const state = await readSkillsSyncState();
|
|
1670
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
1671
|
+
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
1675
|
+
setJson(res, 400, { error: "Refusing to push to upstream repository" });
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
1679
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
1680
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
1681
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
1682
|
+
setJson(res, 200, { ok: true, data: { synced: local.length } });
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
|
|
1685
|
+
}
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
|
|
1689
|
+
try {
|
|
1690
|
+
const state = await readSkillsSyncState();
|
|
1691
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
1692
|
+
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
1696
|
+
const tree = await fetchSkillsTree();
|
|
1697
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
1698
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
1699
|
+
for (const entry of tree) {
|
|
1700
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
1701
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
1702
|
+
if (!existingOwner) {
|
|
1703
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
if (existingOwner !== entry.owner) {
|
|
1707
|
+
uniqueOwnerByName.delete(entry.name);
|
|
1708
|
+
ambiguousNames.add(entry.name);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const localDir = await detectUserSkillsDir(appServer);
|
|
1712
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName, localDir);
|
|
1713
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1714
|
+
const localSkills = await scanInstalledSkillsFromDisk();
|
|
1715
|
+
for (const skill of remote) {
|
|
1716
|
+
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
1717
|
+
if (!owner) {
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (!localSkills.has(skill.name)) {
|
|
1721
|
+
await runCommand("python3", [
|
|
1722
|
+
installerScript,
|
|
1723
|
+
"--repo",
|
|
1724
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1725
|
+
"--path",
|
|
1726
|
+
`skills/${owner}/${skill.name}`,
|
|
1727
|
+
"--dest",
|
|
1728
|
+
localDir,
|
|
1729
|
+
"--method",
|
|
1730
|
+
"git"
|
|
1731
|
+
]);
|
|
1732
|
+
}
|
|
1733
|
+
const skillPath = join(localDir, skill.name);
|
|
1734
|
+
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
1735
|
+
}
|
|
1736
|
+
const remoteNames = new Set(remote.map((row) => row.name));
|
|
1737
|
+
for (const [name, localInfo] of localSkills.entries()) {
|
|
1738
|
+
if (!remoteNames.has(name)) {
|
|
1739
|
+
await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const nextOwners = {};
|
|
1743
|
+
for (const item of remote) {
|
|
1744
|
+
const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
|
|
1745
|
+
if (owner) nextOwners[item.name] = owner;
|
|
1746
|
+
}
|
|
1747
|
+
await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
|
|
1748
|
+
try {
|
|
1749
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
1750
|
+
} catch {
|
|
1751
|
+
}
|
|
1752
|
+
setJson(res, 200, { ok: true, data: { synced: remote.length } });
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
|
|
1755
|
+
}
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1342
1758
|
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
1343
1759
|
try {
|
|
1344
1760
|
const owner = url.searchParams.get("owner") || "";
|
|
@@ -1347,7 +1763,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1347
1763
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1348
1764
|
return;
|
|
1349
1765
|
}
|
|
1350
|
-
const rawUrl = `https://raw.githubusercontent.com/
|
|
1766
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1351
1767
|
const resp = await fetch(rawUrl);
|
|
1352
1768
|
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1353
1769
|
const content = await resp.text();
|
|
@@ -1372,7 +1788,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1372
1788
|
await runCommand("python3", [
|
|
1373
1789
|
installerScript,
|
|
1374
1790
|
"--repo",
|
|
1375
|
-
|
|
1791
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1376
1792
|
"--path",
|
|
1377
1793
|
skillPathInRepo,
|
|
1378
1794
|
"--dest",
|
|
@@ -1382,6 +1798,10 @@ function createCodexBridgeMiddleware() {
|
|
|
1382
1798
|
]);
|
|
1383
1799
|
const skillDir = join(installDest, name);
|
|
1384
1800
|
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
1801
|
+
const syncState = await readSkillsSyncState();
|
|
1802
|
+
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
1803
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1804
|
+
await autoPushSyncedSkills(appServer);
|
|
1385
1805
|
setJson(res, 200, { ok: true, path: skillDir });
|
|
1386
1806
|
} catch (error) {
|
|
1387
1807
|
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
@@ -1399,6 +1819,13 @@ function createCodexBridgeMiddleware() {
|
|
|
1399
1819
|
return;
|
|
1400
1820
|
}
|
|
1401
1821
|
await rm(target, { recursive: true, force: true });
|
|
1822
|
+
if (name) {
|
|
1823
|
+
const syncState = await readSkillsSyncState();
|
|
1824
|
+
const nextOwners = { ...syncState.installedOwners ?? {} };
|
|
1825
|
+
delete nextOwners[name];
|
|
1826
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1827
|
+
}
|
|
1828
|
+
await autoPushSyncedSkills(appServer);
|
|
1402
1829
|
try {
|
|
1403
1830
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
1404
1831
|
} catch {
|
|
@@ -1446,7 +1873,6 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1446
1873
|
}
|
|
1447
1874
|
};
|
|
1448
1875
|
middleware.dispose = () => {
|
|
1449
|
-
threadSearchIndex = null;
|
|
1450
1876
|
appServer.dispose();
|
|
1451
1877
|
};
|
|
1452
1878
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -1461,7 +1887,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1461
1887
|
}
|
|
1462
1888
|
|
|
1463
1889
|
// src/server/authMiddleware.ts
|
|
1464
|
-
import { randomBytes
|
|
1890
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1465
1891
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1466
1892
|
function constantTimeCompare(a, b) {
|
|
1467
1893
|
const bufA = Buffer.from(a);
|
|
@@ -1559,7 +1985,7 @@ function createAuthSession(password) {
|
|
|
1559
1985
|
res.status(401).json({ error: "Invalid password" });
|
|
1560
1986
|
return;
|
|
1561
1987
|
}
|
|
1562
|
-
const token =
|
|
1988
|
+
const token = randomBytes(32).toString("hex");
|
|
1563
1989
|
validTokens.add(token);
|
|
1564
1990
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
1565
1991
|
res.json({ ok: true });
|
|
@@ -1605,69 +2031,6 @@ function normalizeLocalImagePath(rawPath) {
|
|
|
1605
2031
|
}
|
|
1606
2032
|
return trimmed;
|
|
1607
2033
|
}
|
|
1608
|
-
function normalizeLocalPath(rawPath) {
|
|
1609
|
-
const trimmed = rawPath.trim();
|
|
1610
|
-
if (!trimmed) return "";
|
|
1611
|
-
if (trimmed.startsWith("file://")) {
|
|
1612
|
-
try {
|
|
1613
|
-
return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
|
|
1614
|
-
} catch {
|
|
1615
|
-
return trimmed.replace(/^file:\/\//u, "");
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return trimmed;
|
|
1619
|
-
}
|
|
1620
|
-
function decodeBrowsePath(rawPath) {
|
|
1621
|
-
if (!rawPath) return "";
|
|
1622
|
-
try {
|
|
1623
|
-
return decodeURIComponent(rawPath);
|
|
1624
|
-
} catch {
|
|
1625
|
-
return rawPath;
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
function escapeHtml(value) {
|
|
1629
|
-
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
1630
|
-
}
|
|
1631
|
-
function toBrowseHref(pathValue) {
|
|
1632
|
-
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
1633
|
-
}
|
|
1634
|
-
async function renderDirectoryListing(res, localPath) {
|
|
1635
|
-
const entries = await readdir2(localPath, { withFileTypes: true });
|
|
1636
|
-
const sorted = entries.slice().sort((a, b) => {
|
|
1637
|
-
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
1638
|
-
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
1639
|
-
return a.name.localeCompare(b.name);
|
|
1640
|
-
});
|
|
1641
|
-
const parentPath = dirname(localPath);
|
|
1642
|
-
const rows = sorted.map((entry) => {
|
|
1643
|
-
const entryPath = join2(localPath, entry.name);
|
|
1644
|
-
const suffix = entry.isDirectory() ? "/" : "";
|
|
1645
|
-
return `<li><a href="${escapeHtml(toBrowseHref(entryPath))}">${escapeHtml(entry.name)}${suffix}</a></li>`;
|
|
1646
|
-
}).join("\n");
|
|
1647
|
-
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
1648
|
-
const html = `<!doctype html>
|
|
1649
|
-
<html lang="en">
|
|
1650
|
-
<head>
|
|
1651
|
-
<meta charset="utf-8" />
|
|
1652
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1653
|
-
<title>Index of ${escapeHtml(localPath)}</title>
|
|
1654
|
-
<style>
|
|
1655
|
-
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 24px; background: #0b1020; color: #dbe6ff; }
|
|
1656
|
-
a { color: #8cc2ff; text-decoration: none; }
|
|
1657
|
-
a:hover { text-decoration: underline; }
|
|
1658
|
-
ul { list-style: none; padding: 0; margin: 12px 0 0; }
|
|
1659
|
-
li { padding: 3px 0; }
|
|
1660
|
-
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
1661
|
-
</style>
|
|
1662
|
-
</head>
|
|
1663
|
-
<body>
|
|
1664
|
-
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
1665
|
-
${parentLink}
|
|
1666
|
-
<ul>${rows}</ul>
|
|
1667
|
-
</body>
|
|
1668
|
-
</html>`;
|
|
1669
|
-
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1670
|
-
}
|
|
1671
2034
|
function createServer(options = {}) {
|
|
1672
2035
|
const app = express();
|
|
1673
2036
|
const bridge = createCodexBridgeMiddleware();
|
|
@@ -1695,43 +2058,7 @@ function createServer(options = {}) {
|
|
|
1695
2058
|
if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
|
|
1696
2059
|
});
|
|
1697
2060
|
});
|
|
1698
|
-
|
|
1699
|
-
const rawPath = typeof req.query.path === "string" ? req.query.path : "";
|
|
1700
|
-
const localPath = normalizeLocalPath(rawPath);
|
|
1701
|
-
if (!localPath || !isAbsolute2(localPath)) {
|
|
1702
|
-
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1703
|
-
return;
|
|
1704
|
-
}
|
|
1705
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
1706
|
-
res.setHeader("Content-Disposition", "inline");
|
|
1707
|
-
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
1708
|
-
if (!error) return;
|
|
1709
|
-
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
1710
|
-
});
|
|
1711
|
-
});
|
|
1712
|
-
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
1713
|
-
const rawPath = typeof req.params.path === "string" ? req.params.path : "";
|
|
1714
|
-
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1715
|
-
if (!localPath || !isAbsolute2(localPath)) {
|
|
1716
|
-
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1717
|
-
return;
|
|
1718
|
-
}
|
|
1719
|
-
try {
|
|
1720
|
-
const fileStat = await stat2(localPath);
|
|
1721
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
1722
|
-
if (fileStat.isDirectory()) {
|
|
1723
|
-
await renderDirectoryListing(res, localPath);
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
1727
|
-
if (!error) return;
|
|
1728
|
-
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
1729
|
-
});
|
|
1730
|
-
} catch {
|
|
1731
|
-
res.status(404).json({ error: "File not found." });
|
|
1732
|
-
}
|
|
1733
|
-
});
|
|
1734
|
-
const hasFrontendAssets = existsSync(spaEntryFile);
|
|
2061
|
+
const hasFrontendAssets = existsSync2(spaEntryFile);
|
|
1735
2062
|
if (hasFrontendAssets) {
|
|
1736
2063
|
app.use(express.static(distDir));
|
|
1737
2064
|
}
|
|
@@ -1838,7 +2165,7 @@ function resolveCodexCommand() {
|
|
|
1838
2165
|
return "codex";
|
|
1839
2166
|
}
|
|
1840
2167
|
const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
|
|
1841
|
-
if (
|
|
2168
|
+
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
1842
2169
|
return userCandidate;
|
|
1843
2170
|
}
|
|
1844
2171
|
const prefix = process.env.PREFIX?.trim();
|
|
@@ -1846,87 +2173,14 @@ function resolveCodexCommand() {
|
|
|
1846
2173
|
return null;
|
|
1847
2174
|
}
|
|
1848
2175
|
const candidate = join3(prefix, "bin", "codex");
|
|
1849
|
-
if (
|
|
2176
|
+
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
1850
2177
|
return candidate;
|
|
1851
2178
|
}
|
|
1852
2179
|
return null;
|
|
1853
2180
|
}
|
|
1854
|
-
function resolveCloudflaredCommand() {
|
|
1855
|
-
if (canRun("cloudflared", ["--version"])) {
|
|
1856
|
-
return "cloudflared";
|
|
1857
|
-
}
|
|
1858
|
-
const localCandidate = join3(homedir2(), ".local", "bin", "cloudflared");
|
|
1859
|
-
if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
1860
|
-
return localCandidate;
|
|
1861
|
-
}
|
|
1862
|
-
return null;
|
|
1863
|
-
}
|
|
1864
|
-
function mapCloudflaredLinuxArch(arch) {
|
|
1865
|
-
if (arch === "x64") {
|
|
1866
|
-
return "amd64";
|
|
1867
|
-
}
|
|
1868
|
-
if (arch === "arm64") {
|
|
1869
|
-
return "arm64";
|
|
1870
|
-
}
|
|
1871
|
-
return null;
|
|
1872
|
-
}
|
|
1873
|
-
function downloadFile(url, destination) {
|
|
1874
|
-
return new Promise((resolve2, reject) => {
|
|
1875
|
-
const request = (currentUrl) => {
|
|
1876
|
-
httpsGet(currentUrl, (response) => {
|
|
1877
|
-
const code = response.statusCode ?? 0;
|
|
1878
|
-
if (code >= 300 && code < 400 && response.headers.location) {
|
|
1879
|
-
response.resume();
|
|
1880
|
-
request(response.headers.location);
|
|
1881
|
-
return;
|
|
1882
|
-
}
|
|
1883
|
-
if (code !== 200) {
|
|
1884
|
-
response.resume();
|
|
1885
|
-
reject(new Error(`Download failed with HTTP status ${String(code)}`));
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
|
-
const file = createWriteStream(destination, { mode: 493 });
|
|
1889
|
-
response.pipe(file);
|
|
1890
|
-
file.on("finish", () => {
|
|
1891
|
-
file.close();
|
|
1892
|
-
resolve2();
|
|
1893
|
-
});
|
|
1894
|
-
file.on("error", reject);
|
|
1895
|
-
}).on("error", reject);
|
|
1896
|
-
};
|
|
1897
|
-
request(url);
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
async function ensureCloudflaredInstalledLinux() {
|
|
1901
|
-
const current = resolveCloudflaredCommand();
|
|
1902
|
-
if (current) {
|
|
1903
|
-
return current;
|
|
1904
|
-
}
|
|
1905
|
-
if (process.platform !== "linux") {
|
|
1906
|
-
return null;
|
|
1907
|
-
}
|
|
1908
|
-
const mappedArch = mapCloudflaredLinuxArch(process.arch);
|
|
1909
|
-
if (!mappedArch) {
|
|
1910
|
-
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
1911
|
-
}
|
|
1912
|
-
const userBinDir = join3(homedir2(), ".local", "bin");
|
|
1913
|
-
mkdirSync(userBinDir, { recursive: true });
|
|
1914
|
-
const destination = join3(userBinDir, "cloudflared");
|
|
1915
|
-
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
1916
|
-
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
1917
|
-
await downloadFile(downloadUrl, destination);
|
|
1918
|
-
chmodSync(destination, 493);
|
|
1919
|
-
process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
|
|
1920
|
-
const installed = resolveCloudflaredCommand();
|
|
1921
|
-
if (!installed) {
|
|
1922
|
-
throw new Error("cloudflared download completed but executable is still not available");
|
|
1923
|
-
}
|
|
1924
|
-
console.log("\ncloudflared installed.\n");
|
|
1925
|
-
return installed;
|
|
1926
|
-
}
|
|
1927
2181
|
function hasCodexAuth() {
|
|
1928
2182
|
const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
|
|
1929
|
-
return
|
|
2183
|
+
return existsSync3(join3(codexHome, "auth.json"));
|
|
1930
2184
|
}
|
|
1931
2185
|
function ensureCodexInstalled() {
|
|
1932
2186
|
let codexCommand = resolveCodexCommand();
|
|
@@ -2005,27 +2259,9 @@ function parseCloudflaredUrl(chunk) {
|
|
|
2005
2259
|
}
|
|
2006
2260
|
return urlMatch[urlMatch.length - 1] ?? null;
|
|
2007
2261
|
}
|
|
2008
|
-
function
|
|
2009
|
-
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
2010
|
-
const interfaces = networkInterfaces();
|
|
2011
|
-
for (const entries of Object.values(interfaces)) {
|
|
2012
|
-
if (!entries) {
|
|
2013
|
-
continue;
|
|
2014
|
-
}
|
|
2015
|
-
for (const entry of entries) {
|
|
2016
|
-
if (entry.internal) {
|
|
2017
|
-
continue;
|
|
2018
|
-
}
|
|
2019
|
-
if (entry.family === "IPv4") {
|
|
2020
|
-
urls.add(`http://${entry.address}:${String(port)}`);
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
return Array.from(urls);
|
|
2025
|
-
}
|
|
2026
|
-
async function startCloudflaredTunnel(command, localPort) {
|
|
2262
|
+
async function startCloudflaredTunnel(localPort) {
|
|
2027
2263
|
return new Promise((resolve2, reject) => {
|
|
2028
|
-
const child = spawn2(
|
|
2264
|
+
const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
2029
2265
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2030
2266
|
});
|
|
2031
2267
|
const timeout = setTimeout(() => {
|
|
@@ -2076,7 +2312,7 @@ function listenWithFallback(server, startPort) {
|
|
|
2076
2312
|
};
|
|
2077
2313
|
server.once("error", onError);
|
|
2078
2314
|
server.once("listening", onListening);
|
|
2079
|
-
server.listen(port
|
|
2315
|
+
server.listen(port);
|
|
2080
2316
|
};
|
|
2081
2317
|
attempt(startPort);
|
|
2082
2318
|
});
|
|
@@ -2098,8 +2334,7 @@ async function startServer(options) {
|
|
|
2098
2334
|
let tunnelUrl = null;
|
|
2099
2335
|
if (options.tunnel) {
|
|
2100
2336
|
try {
|
|
2101
|
-
const
|
|
2102
|
-
const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
|
|
2337
|
+
const tunnel = await startCloudflaredTunnel(port);
|
|
2103
2338
|
tunnelChild = tunnel.process;
|
|
2104
2339
|
tunnelUrl = tunnel.url;
|
|
2105
2340
|
} catch (error) {
|
|
@@ -2114,15 +2349,8 @@ async function startServer(options) {
|
|
|
2114
2349
|
` Version: ${version}`,
|
|
2115
2350
|
" GitHub: https://github.com/friuns2/codexui",
|
|
2116
2351
|
"",
|
|
2117
|
-
`
|
|
2352
|
+
` Local: http://localhost:${String(port)}`
|
|
2118
2353
|
];
|
|
2119
|
-
const accessUrls = getAccessibleUrls(port);
|
|
2120
|
-
if (accessUrls.length > 0) {
|
|
2121
|
-
lines.push(` Local: ${accessUrls[0]}`);
|
|
2122
|
-
for (const accessUrl of accessUrls.slice(1)) {
|
|
2123
|
-
lines.push(` Network: ${accessUrl}`);
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
2354
|
if (port !== requestedPort) {
|
|
2127
2355
|
lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
|
|
2128
2356
|
}
|
|
@@ -2131,7 +2359,9 @@ async function startServer(options) {
|
|
|
2131
2359
|
}
|
|
2132
2360
|
if (tunnelUrl) {
|
|
2133
2361
|
lines.push(` Tunnel: ${tunnelUrl}`);
|
|
2134
|
-
lines.push("
|
|
2362
|
+
lines.push("");
|
|
2363
|
+
lines.push(" Tunnel QR code:");
|
|
2364
|
+
lines.push(` URL: ${tunnelUrl}`);
|
|
2135
2365
|
}
|
|
2136
2366
|
printTermuxKeepAlive(lines);
|
|
2137
2367
|
lines.push("");
|