codexapp 0.1.30 → 0.1.31
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-A4ZTH1ru.css +1 -0
- package/dist/assets/index-CcJFYgVf.js +48 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +738 -692
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -3
- package/dist/assets/index-B5wuNFNF.css +0 -1
- package/dist/assets/index-BeRIcrmp.js +0 -1481
package/dist-cli/index.js
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import "dotenv/config";
|
|
5
4
|
import { createServer as createServer2 } from "http";
|
|
6
|
-
import { existsSync as
|
|
7
|
-
import { readFile as
|
|
8
|
-
import { homedir as homedir2 } from "os";
|
|
9
|
-
import { join as
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "fs";
|
|
6
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
7
|
+
import { homedir as homedir2, networkInterfaces } from "os";
|
|
8
|
+
import { join as join4 } from "path";
|
|
10
9
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
10
|
+
import { createInterface } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
|
-
import { dirname as
|
|
12
|
+
import { dirname as dirname3 } from "path";
|
|
13
|
+
import { get as httpsGet } from "https";
|
|
13
14
|
import { Command } from "commander";
|
|
14
15
|
import qrcode from "qrcode-terminal";
|
|
15
16
|
|
|
16
17
|
// src/server/httpServer.ts
|
|
17
18
|
import { fileURLToPath } from "url";
|
|
18
|
-
import { dirname, extname, isAbsolute as isAbsolute2, join as
|
|
19
|
-
import { existsSync
|
|
19
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
20
|
+
import { existsSync } from "fs";
|
|
21
|
+
import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
|
|
20
22
|
import express from "express";
|
|
21
23
|
|
|
22
24
|
// src/server/codexAppServerBridge.ts
|
|
23
|
-
import "dotenv/config";
|
|
24
25
|
import { spawn } from "child_process";
|
|
26
|
+
import { randomBytes } from "crypto";
|
|
25
27
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
|
|
26
|
-
import { existsSync } from "fs";
|
|
27
28
|
import { request as httpsRequest } from "https";
|
|
28
29
|
import { homedir } from "os";
|
|
29
30
|
import { tmpdir } from "os";
|
|
30
|
-
import { isAbsolute, join, resolve } from "path";
|
|
31
|
+
import { basename, isAbsolute, join, resolve } from "path";
|
|
31
32
|
import { writeFile } from "fs/promises";
|
|
32
33
|
function asRecord(value) {
|
|
33
34
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -51,6 +52,46 @@ function setJson(res, statusCode, payload) {
|
|
|
51
52
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
52
53
|
res.end(JSON.stringify(payload));
|
|
53
54
|
}
|
|
55
|
+
function extractThreadMessageText(threadReadPayload) {
|
|
56
|
+
const payload = asRecord(threadReadPayload);
|
|
57
|
+
const thread = asRecord(payload?.thread);
|
|
58
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
59
|
+
const parts = [];
|
|
60
|
+
for (const turn of turns) {
|
|
61
|
+
const turnRecord = asRecord(turn);
|
|
62
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
63
|
+
for (const item of items) {
|
|
64
|
+
const itemRecord = asRecord(item);
|
|
65
|
+
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
66
|
+
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
67
|
+
parts.push(itemRecord.text.trim());
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (type === "userMessage") {
|
|
71
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
72
|
+
for (const block of content) {
|
|
73
|
+
const blockRecord = asRecord(block);
|
|
74
|
+
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
75
|
+
parts.push(blockRecord.text.trim());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (type === "commandExecution") {
|
|
81
|
+
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
82
|
+
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
83
|
+
if (command) parts.push(command);
|
|
84
|
+
if (output) parts.push(output);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return parts.join("\n").trim();
|
|
89
|
+
}
|
|
90
|
+
function isExactPhraseMatch(query, doc) {
|
|
91
|
+
const q = query.trim().toLowerCase();
|
|
92
|
+
if (!q) return false;
|
|
93
|
+
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
94
|
+
}
|
|
54
95
|
function scoreFileCandidate(path, query) {
|
|
55
96
|
if (!query) return 0;
|
|
56
97
|
const lowerPath = path.toLowerCase();
|
|
@@ -124,8 +165,30 @@ async function runCommand(command, args, options = {}) {
|
|
|
124
165
|
});
|
|
125
166
|
});
|
|
126
167
|
}
|
|
127
|
-
|
|
128
|
-
|
|
168
|
+
function isMissingHeadError(error) {
|
|
169
|
+
const message = getErrorMessage(error, "").toLowerCase();
|
|
170
|
+
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
171
|
+
}
|
|
172
|
+
function isNotGitRepositoryError(error) {
|
|
173
|
+
const message = getErrorMessage(error, "").toLowerCase();
|
|
174
|
+
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
175
|
+
}
|
|
176
|
+
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
177
|
+
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
178
|
+
try {
|
|
179
|
+
await stat(agentsPath);
|
|
180
|
+
} catch {
|
|
181
|
+
await writeFile(agentsPath, "", "utf8");
|
|
182
|
+
}
|
|
183
|
+
await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
184
|
+
await runCommand(
|
|
185
|
+
"git",
|
|
186
|
+
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
187
|
+
{ cwd: repoRoot }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
async function runCommandCapture(command, args, options = {}) {
|
|
191
|
+
return await new Promise((resolveOutput, reject) => {
|
|
129
192
|
const proc = spawn(command, args, {
|
|
130
193
|
cwd: options.cwd,
|
|
131
194
|
env: process.env,
|
|
@@ -142,7 +205,7 @@ async function runCommandWithOutput(command, args, options = {}) {
|
|
|
142
205
|
proc.on("error", reject);
|
|
143
206
|
proc.on("close", (code) => {
|
|
144
207
|
if (code === 0) {
|
|
145
|
-
|
|
208
|
+
resolveOutput(stdout.trim());
|
|
146
209
|
return;
|
|
147
210
|
}
|
|
148
211
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -208,7 +271,7 @@ async function fetchSkillsTree() {
|
|
|
208
271
|
if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
|
|
209
272
|
return skillsTreeCache.entries;
|
|
210
273
|
}
|
|
211
|
-
const resp = await ghFetch(
|
|
274
|
+
const resp = await ghFetch("https://api.github.com/repos/openclaw/skills/git/trees/main?recursive=1");
|
|
212
275
|
if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
|
|
213
276
|
const data = await resp.json();
|
|
214
277
|
const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
|
|
@@ -224,7 +287,7 @@ async function fetchSkillsTree() {
|
|
|
224
287
|
entries.push({
|
|
225
288
|
name: skillName,
|
|
226
289
|
owner,
|
|
227
|
-
url: `https://github.com
|
|
290
|
+
url: `https://github.com/openclaw/skills/tree/main/skills/${owner}/${skillName}`
|
|
228
291
|
});
|
|
229
292
|
}
|
|
230
293
|
skillsTreeCache = { entries, fetchedAt: Date.now() };
|
|
@@ -236,7 +299,7 @@ async function fetchMetaBatch(entries) {
|
|
|
236
299
|
const batch = toFetch.slice(0, 50);
|
|
237
300
|
const results = await Promise.allSettled(
|
|
238
301
|
batch.map(async (e) => {
|
|
239
|
-
const rawUrl = `https://raw.githubusercontent.com
|
|
302
|
+
const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${e.owner}/${e.name}/_meta.json`;
|
|
240
303
|
const resp = await fetch(rawUrl);
|
|
241
304
|
if (!resp.ok) return;
|
|
242
305
|
const meta = await resp.json();
|
|
@@ -262,23 +325,6 @@ function buildHubEntry(e) {
|
|
|
262
325
|
installed: false
|
|
263
326
|
};
|
|
264
327
|
}
|
|
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
|
-
};
|
|
282
328
|
async function scanInstalledSkillsFromDisk() {
|
|
283
329
|
const map = /* @__PURE__ */ new Map();
|
|
284
330
|
const skillsDir = getSkillsInstallDir();
|
|
@@ -297,448 +343,6 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
297
343
|
}
|
|
298
344
|
return map;
|
|
299
345
|
}
|
|
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
|
-
}
|
|
742
346
|
async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
743
347
|
const q = query.toLowerCase().trim();
|
|
744
348
|
let filtered = q ? allEntries.filter((s) => {
|
|
@@ -1330,9 +934,82 @@ function getSharedBridgeState() {
|
|
|
1330
934
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
1331
935
|
return created;
|
|
1332
936
|
}
|
|
937
|
+
async function loadAllThreadsForSearch(appServer) {
|
|
938
|
+
const threads = [];
|
|
939
|
+
let cursor = null;
|
|
940
|
+
do {
|
|
941
|
+
const response = asRecord(await appServer.rpc("thread/list", {
|
|
942
|
+
archived: false,
|
|
943
|
+
limit: 100,
|
|
944
|
+
sortKey: "updated_at",
|
|
945
|
+
cursor
|
|
946
|
+
}));
|
|
947
|
+
const data = Array.isArray(response?.data) ? response.data : [];
|
|
948
|
+
for (const row of data) {
|
|
949
|
+
const record = asRecord(row);
|
|
950
|
+
const id = typeof record?.id === "string" ? record.id : "";
|
|
951
|
+
if (!id) continue;
|
|
952
|
+
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";
|
|
953
|
+
const preview = typeof record?.preview === "string" ? record.preview : "";
|
|
954
|
+
threads.push({ id, title, preview });
|
|
955
|
+
}
|
|
956
|
+
cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
|
|
957
|
+
} while (cursor);
|
|
958
|
+
const docs = [];
|
|
959
|
+
const concurrency = 4;
|
|
960
|
+
for (let offset = 0; offset < threads.length; offset += concurrency) {
|
|
961
|
+
const batch = threads.slice(offset, offset + concurrency);
|
|
962
|
+
const loaded = await Promise.all(batch.map(async (thread) => {
|
|
963
|
+
try {
|
|
964
|
+
const readResponse = await appServer.rpc("thread/read", {
|
|
965
|
+
threadId: thread.id,
|
|
966
|
+
includeTurns: true
|
|
967
|
+
});
|
|
968
|
+
const messageText = extractThreadMessageText(readResponse);
|
|
969
|
+
const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
|
|
970
|
+
return {
|
|
971
|
+
id: thread.id,
|
|
972
|
+
title: thread.title,
|
|
973
|
+
preview: thread.preview,
|
|
974
|
+
messageText,
|
|
975
|
+
searchableText
|
|
976
|
+
};
|
|
977
|
+
} catch {
|
|
978
|
+
const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
|
|
979
|
+
return {
|
|
980
|
+
id: thread.id,
|
|
981
|
+
title: thread.title,
|
|
982
|
+
preview: thread.preview,
|
|
983
|
+
messageText: "",
|
|
984
|
+
searchableText
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
}));
|
|
988
|
+
docs.push(...loaded);
|
|
989
|
+
}
|
|
990
|
+
return docs;
|
|
991
|
+
}
|
|
992
|
+
async function buildThreadSearchIndex(appServer) {
|
|
993
|
+
const docs = await loadAllThreadsForSearch(appServer);
|
|
994
|
+
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
995
|
+
return { docsById };
|
|
996
|
+
}
|
|
1333
997
|
function createCodexBridgeMiddleware() {
|
|
1334
998
|
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
1335
|
-
|
|
999
|
+
let threadSearchIndex = null;
|
|
1000
|
+
let threadSearchIndexPromise = null;
|
|
1001
|
+
async function getThreadSearchIndex() {
|
|
1002
|
+
if (threadSearchIndex) return threadSearchIndex;
|
|
1003
|
+
if (!threadSearchIndexPromise) {
|
|
1004
|
+
threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
|
|
1005
|
+
threadSearchIndex = index;
|
|
1006
|
+
return index;
|
|
1007
|
+
}).finally(() => {
|
|
1008
|
+
threadSearchIndexPromise = null;
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
return threadSearchIndexPromise;
|
|
1012
|
+
}
|
|
1336
1013
|
const middleware = async (req, res, next) => {
|
|
1337
1014
|
try {
|
|
1338
1015
|
if (!req.url) {
|
|
@@ -1398,6 +1075,76 @@ function createCodexBridgeMiddleware() {
|
|
|
1398
1075
|
setJson(res, 200, { data: { path: homedir() } });
|
|
1399
1076
|
return;
|
|
1400
1077
|
}
|
|
1078
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1079
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1080
|
+
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1081
|
+
if (!rawSourceCwd) {
|
|
1082
|
+
setJson(res, 400, { error: "Missing sourceCwd" });
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1086
|
+
try {
|
|
1087
|
+
const sourceInfo = await stat(sourceCwd);
|
|
1088
|
+
if (!sourceInfo.isDirectory()) {
|
|
1089
|
+
setJson(res, 400, { error: "sourceCwd is not a directory" });
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
setJson(res, 404, { error: "sourceCwd does not exist" });
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
let gitRoot = "";
|
|
1098
|
+
try {
|
|
1099
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (!isNotGitRepositoryError(error)) throw error;
|
|
1102
|
+
await runCommand("git", ["init"], { cwd: sourceCwd });
|
|
1103
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1104
|
+
}
|
|
1105
|
+
const repoName = basename(gitRoot) || "repo";
|
|
1106
|
+
const worktreesRoot = join(getCodexHomeDir(), "worktrees");
|
|
1107
|
+
await mkdir(worktreesRoot, { recursive: true });
|
|
1108
|
+
let worktreeId = "";
|
|
1109
|
+
let worktreeParent = "";
|
|
1110
|
+
let worktreeCwd = "";
|
|
1111
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1112
|
+
const candidate = randomBytes(2).toString("hex");
|
|
1113
|
+
const parent = join(worktreesRoot, candidate);
|
|
1114
|
+
try {
|
|
1115
|
+
await stat(parent);
|
|
1116
|
+
continue;
|
|
1117
|
+
} catch {
|
|
1118
|
+
worktreeId = candidate;
|
|
1119
|
+
worktreeParent = parent;
|
|
1120
|
+
worktreeCwd = join(parent, repoName);
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (!worktreeId || !worktreeParent || !worktreeCwd) {
|
|
1125
|
+
throw new Error("Failed to allocate a unique worktree id");
|
|
1126
|
+
}
|
|
1127
|
+
const branch = `codex/${worktreeId}`;
|
|
1128
|
+
await mkdir(worktreeParent, { recursive: true });
|
|
1129
|
+
try {
|
|
1130
|
+
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
if (!isMissingHeadError(error)) throw error;
|
|
1133
|
+
await ensureRepoHasInitialCommit(gitRoot);
|
|
1134
|
+
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1135
|
+
}
|
|
1136
|
+
setJson(res, 200, {
|
|
1137
|
+
data: {
|
|
1138
|
+
cwd: worktreeCwd,
|
|
1139
|
+
branch,
|
|
1140
|
+
gitRoot
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
|
|
1145
|
+
}
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1401
1148
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1402
1149
|
const payload = await readJsonBody(req);
|
|
1403
1150
|
const record = asRecord(payload);
|
|
@@ -1523,6 +1270,20 @@ function createCodexBridgeMiddleware() {
|
|
|
1523
1270
|
setJson(res, 200, { data: cache });
|
|
1524
1271
|
return;
|
|
1525
1272
|
}
|
|
1273
|
+
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1274
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1275
|
+
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1276
|
+
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1277
|
+
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1278
|
+
if (!query) {
|
|
1279
|
+
setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const index = await getThreadSearchIndex();
|
|
1283
|
+
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1284
|
+
setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1526
1287
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1527
1288
|
const payload = asRecord(await readJsonBody(req));
|
|
1528
1289
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
@@ -1579,182 +1340,6 @@ function createCodexBridgeMiddleware() {
|
|
|
1579
1340
|
}
|
|
1580
1341
|
return;
|
|
1581
1342
|
}
|
|
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
|
-
}
|
|
1758
1343
|
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
1759
1344
|
try {
|
|
1760
1345
|
const owner = url.searchParams.get("owner") || "";
|
|
@@ -1763,7 +1348,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1763
1348
|
setJson(res, 400, { error: "Missing owner or name" });
|
|
1764
1349
|
return;
|
|
1765
1350
|
}
|
|
1766
|
-
const rawUrl = `https://raw.githubusercontent.com
|
|
1351
|
+
const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${owner}/${name}/SKILL.md`;
|
|
1767
1352
|
const resp = await fetch(rawUrl);
|
|
1768
1353
|
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1769
1354
|
const content = await resp.text();
|
|
@@ -1788,7 +1373,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1788
1373
|
await runCommand("python3", [
|
|
1789
1374
|
installerScript,
|
|
1790
1375
|
"--repo",
|
|
1791
|
-
|
|
1376
|
+
"openclaw/skills",
|
|
1792
1377
|
"--path",
|
|
1793
1378
|
skillPathInRepo,
|
|
1794
1379
|
"--dest",
|
|
@@ -1798,10 +1383,6 @@ function createCodexBridgeMiddleware() {
|
|
|
1798
1383
|
]);
|
|
1799
1384
|
const skillDir = join(installDest, name);
|
|
1800
1385
|
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);
|
|
1805
1386
|
setJson(res, 200, { ok: true, path: skillDir });
|
|
1806
1387
|
} catch (error) {
|
|
1807
1388
|
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
@@ -1819,13 +1400,6 @@ function createCodexBridgeMiddleware() {
|
|
|
1819
1400
|
return;
|
|
1820
1401
|
}
|
|
1821
1402
|
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);
|
|
1829
1403
|
try {
|
|
1830
1404
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
1831
1405
|
} catch {
|
|
@@ -1873,6 +1447,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1873
1447
|
}
|
|
1874
1448
|
};
|
|
1875
1449
|
middleware.dispose = () => {
|
|
1450
|
+
threadSearchIndex = null;
|
|
1876
1451
|
appServer.dispose();
|
|
1877
1452
|
};
|
|
1878
1453
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -1887,7 +1462,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1887
1462
|
}
|
|
1888
1463
|
|
|
1889
1464
|
// src/server/authMiddleware.ts
|
|
1890
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1465
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1891
1466
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1892
1467
|
function constantTimeCompare(a, b) {
|
|
1893
1468
|
const bufA = Buffer.from(a);
|
|
@@ -1985,7 +1560,7 @@ function createAuthSession(password) {
|
|
|
1985
1560
|
res.status(401).json({ error: "Invalid password" });
|
|
1986
1561
|
return;
|
|
1987
1562
|
}
|
|
1988
|
-
const token =
|
|
1563
|
+
const token = randomBytes2(32).toString("hex");
|
|
1989
1564
|
validTokens.add(token);
|
|
1990
1565
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
1991
1566
|
res.json({ ok: true });
|
|
@@ -2004,11 +1579,277 @@ function createAuthSession(password) {
|
|
|
2004
1579
|
};
|
|
2005
1580
|
}
|
|
2006
1581
|
|
|
1582
|
+
// src/server/localBrowseUi.ts
|
|
1583
|
+
import { dirname, extname, join as join2 } from "path";
|
|
1584
|
+
import { open, readFile as readFile2, readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
1585
|
+
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1586
|
+
".txt",
|
|
1587
|
+
".md",
|
|
1588
|
+
".json",
|
|
1589
|
+
".js",
|
|
1590
|
+
".ts",
|
|
1591
|
+
".tsx",
|
|
1592
|
+
".jsx",
|
|
1593
|
+
".css",
|
|
1594
|
+
".scss",
|
|
1595
|
+
".html",
|
|
1596
|
+
".htm",
|
|
1597
|
+
".xml",
|
|
1598
|
+
".yml",
|
|
1599
|
+
".yaml",
|
|
1600
|
+
".log",
|
|
1601
|
+
".csv",
|
|
1602
|
+
".env",
|
|
1603
|
+
".py",
|
|
1604
|
+
".sh",
|
|
1605
|
+
".toml",
|
|
1606
|
+
".ini",
|
|
1607
|
+
".conf",
|
|
1608
|
+
".sql",
|
|
1609
|
+
".bat",
|
|
1610
|
+
".cmd",
|
|
1611
|
+
".ps1"
|
|
1612
|
+
]);
|
|
1613
|
+
function languageForPath(pathValue) {
|
|
1614
|
+
const extension = extname(pathValue).toLowerCase();
|
|
1615
|
+
switch (extension) {
|
|
1616
|
+
case ".js":
|
|
1617
|
+
return "javascript";
|
|
1618
|
+
case ".ts":
|
|
1619
|
+
return "typescript";
|
|
1620
|
+
case ".jsx":
|
|
1621
|
+
return "javascript";
|
|
1622
|
+
case ".tsx":
|
|
1623
|
+
return "typescript";
|
|
1624
|
+
case ".py":
|
|
1625
|
+
return "python";
|
|
1626
|
+
case ".sh":
|
|
1627
|
+
return "sh";
|
|
1628
|
+
case ".css":
|
|
1629
|
+
case ".scss":
|
|
1630
|
+
return "css";
|
|
1631
|
+
case ".html":
|
|
1632
|
+
case ".htm":
|
|
1633
|
+
return "html";
|
|
1634
|
+
case ".json":
|
|
1635
|
+
return "json";
|
|
1636
|
+
case ".md":
|
|
1637
|
+
return "markdown";
|
|
1638
|
+
case ".yaml":
|
|
1639
|
+
case ".yml":
|
|
1640
|
+
return "yaml";
|
|
1641
|
+
case ".xml":
|
|
1642
|
+
return "xml";
|
|
1643
|
+
case ".sql":
|
|
1644
|
+
return "sql";
|
|
1645
|
+
case ".toml":
|
|
1646
|
+
return "ini";
|
|
1647
|
+
case ".ini":
|
|
1648
|
+
case ".conf":
|
|
1649
|
+
return "ini";
|
|
1650
|
+
default:
|
|
1651
|
+
return "plaintext";
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function normalizeLocalPath(rawPath) {
|
|
1655
|
+
const trimmed = rawPath.trim();
|
|
1656
|
+
if (!trimmed) return "";
|
|
1657
|
+
if (trimmed.startsWith("file://")) {
|
|
1658
|
+
try {
|
|
1659
|
+
return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
|
|
1660
|
+
} catch {
|
|
1661
|
+
return trimmed.replace(/^file:\/\//u, "");
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return trimmed;
|
|
1665
|
+
}
|
|
1666
|
+
function decodeBrowsePath(rawPath) {
|
|
1667
|
+
if (!rawPath) return "";
|
|
1668
|
+
try {
|
|
1669
|
+
return decodeURIComponent(rawPath);
|
|
1670
|
+
} catch {
|
|
1671
|
+
return rawPath;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function isTextEditablePath(pathValue) {
|
|
1675
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
|
|
1676
|
+
}
|
|
1677
|
+
function looksLikeTextBuffer(buffer) {
|
|
1678
|
+
if (buffer.length === 0) return true;
|
|
1679
|
+
for (const byte of buffer) {
|
|
1680
|
+
if (byte === 0) return false;
|
|
1681
|
+
}
|
|
1682
|
+
const decoded = buffer.toString("utf8");
|
|
1683
|
+
const replacementCount = (decoded.match(/\uFFFD/gu) ?? []).length;
|
|
1684
|
+
return replacementCount / decoded.length < 0.05;
|
|
1685
|
+
}
|
|
1686
|
+
async function probeFileIsText(localPath) {
|
|
1687
|
+
const handle = await open(localPath, "r");
|
|
1688
|
+
try {
|
|
1689
|
+
const sample = Buffer.allocUnsafe(4096);
|
|
1690
|
+
const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
|
|
1691
|
+
return looksLikeTextBuffer(sample.subarray(0, bytesRead));
|
|
1692
|
+
} finally {
|
|
1693
|
+
await handle.close();
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
async function isTextEditableFile(localPath) {
|
|
1697
|
+
if (isTextEditablePath(localPath)) return true;
|
|
1698
|
+
try {
|
|
1699
|
+
const fileStat = await stat2(localPath);
|
|
1700
|
+
if (!fileStat.isFile()) return false;
|
|
1701
|
+
return await probeFileIsText(localPath);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function escapeHtml(value) {
|
|
1707
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
1708
|
+
}
|
|
1709
|
+
function toBrowseHref(pathValue) {
|
|
1710
|
+
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
1711
|
+
}
|
|
1712
|
+
function toEditHref(pathValue) {
|
|
1713
|
+
return `/codex-local-edit${encodeURI(pathValue)}`;
|
|
1714
|
+
}
|
|
1715
|
+
function escapeForInlineScriptString(value) {
|
|
1716
|
+
return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
|
|
1717
|
+
}
|
|
1718
|
+
async function getDirectoryItems(localPath) {
|
|
1719
|
+
const entries = await readdir2(localPath, { withFileTypes: true });
|
|
1720
|
+
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
1721
|
+
const entryPath = join2(localPath, entry.name);
|
|
1722
|
+
const entryStat = await stat2(entryPath);
|
|
1723
|
+
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
1724
|
+
return {
|
|
1725
|
+
name: entry.name,
|
|
1726
|
+
path: entryPath,
|
|
1727
|
+
isDirectory: entry.isDirectory(),
|
|
1728
|
+
editable,
|
|
1729
|
+
mtimeMs: entryStat.mtimeMs
|
|
1730
|
+
};
|
|
1731
|
+
}));
|
|
1732
|
+
return withMeta.sort((a, b) => {
|
|
1733
|
+
if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
|
|
1734
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
1735
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
1736
|
+
return a.name.localeCompare(b.name);
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
async function createDirectoryListingHtml(localPath) {
|
|
1740
|
+
const items = await getDirectoryItems(localPath);
|
|
1741
|
+
const parentPath = dirname(localPath);
|
|
1742
|
+
const rows = items.map((item) => {
|
|
1743
|
+
const suffix = item.isDirectory ? "/" : "";
|
|
1744
|
+
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
1745
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
1746
|
+
}).join("\n");
|
|
1747
|
+
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
1748
|
+
return `<!doctype html>
|
|
1749
|
+
<html lang="en">
|
|
1750
|
+
<head>
|
|
1751
|
+
<meta charset="utf-8" />
|
|
1752
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1753
|
+
<title>Index of ${escapeHtml(localPath)}</title>
|
|
1754
|
+
<style>
|
|
1755
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
|
|
1756
|
+
a { color: #8cc2ff; text-decoration: none; }
|
|
1757
|
+
a:hover { text-decoration: underline; }
|
|
1758
|
+
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
1759
|
+
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
1760
|
+
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
1761
|
+
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; text-decoration: none; }
|
|
1762
|
+
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
1763
|
+
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
1764
|
+
@media (max-width: 640px) {
|
|
1765
|
+
body { margin: 12px; }
|
|
1766
|
+
.file-row { gap: 8px; }
|
|
1767
|
+
.file-link { font-size: 15px; padding: 12px; }
|
|
1768
|
+
.icon-btn { width: 44px; height: 44px; }
|
|
1769
|
+
}
|
|
1770
|
+
</style>
|
|
1771
|
+
</head>
|
|
1772
|
+
<body>
|
|
1773
|
+
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
1774
|
+
${parentLink}
|
|
1775
|
+
<ul>${rows}</ul>
|
|
1776
|
+
</body>
|
|
1777
|
+
</html>`;
|
|
1778
|
+
}
|
|
1779
|
+
async function createTextEditorHtml(localPath) {
|
|
1780
|
+
const content = await readFile2(localPath, "utf8");
|
|
1781
|
+
const parentPath = dirname(localPath);
|
|
1782
|
+
const language = languageForPath(localPath);
|
|
1783
|
+
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
1784
|
+
return `<!doctype html>
|
|
1785
|
+
<html lang="en">
|
|
1786
|
+
<head>
|
|
1787
|
+
<meta charset="utf-8" />
|
|
1788
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1789
|
+
<title>Edit ${escapeHtml(localPath)}</title>
|
|
1790
|
+
<style>
|
|
1791
|
+
html, body { width: 100%; height: 100%; margin: 0; }
|
|
1792
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
|
|
1793
|
+
.toolbar { position: sticky; top: 0; z-index: 10; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; background: #0b1020; border-bottom: 1px solid #243a5a; }
|
|
1794
|
+
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
1795
|
+
button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
|
|
1796
|
+
button:hover, a:hover { filter: brightness(1.08); }
|
|
1797
|
+
#editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
|
|
1798
|
+
#status { margin-left: 8px; color: #8cc2ff; }
|
|
1799
|
+
.ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
|
|
1800
|
+
.ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
|
|
1801
|
+
.ace_marker-layer .ace_active-line { background: #10213c !important; }
|
|
1802
|
+
.ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
|
|
1803
|
+
.meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
|
|
1804
|
+
</style>
|
|
1805
|
+
</head>
|
|
1806
|
+
<body>
|
|
1807
|
+
<div class="toolbar">
|
|
1808
|
+
<div class="row">
|
|
1809
|
+
<a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
|
|
1810
|
+
<button id="saveBtn" type="button">Save</button>
|
|
1811
|
+
<span id="status"></span>
|
|
1812
|
+
</div>
|
|
1813
|
+
<div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
|
|
1814
|
+
</div>
|
|
1815
|
+
<div id="editor"></div>
|
|
1816
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
|
|
1817
|
+
<script>
|
|
1818
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
1819
|
+
const status = document.getElementById('status');
|
|
1820
|
+
const editor = ace.edit('editor');
|
|
1821
|
+
editor.setTheme('ace/theme/tomorrow_night');
|
|
1822
|
+
editor.session.setMode('ace/mode/${escapeHtml(language)}');
|
|
1823
|
+
editor.setValue(${safeContentLiteral}, -1);
|
|
1824
|
+
editor.setOptions({
|
|
1825
|
+
fontSize: '13px',
|
|
1826
|
+
wrap: true,
|
|
1827
|
+
showPrintMargin: false,
|
|
1828
|
+
useSoftTabs: true,
|
|
1829
|
+
tabSize: 2,
|
|
1830
|
+
behavioursEnabled: true,
|
|
1831
|
+
});
|
|
1832
|
+
editor.resize();
|
|
1833
|
+
|
|
1834
|
+
saveBtn.addEventListener('click', async () => {
|
|
1835
|
+
status.textContent = 'Saving...';
|
|
1836
|
+
const response = await fetch(location.pathname, {
|
|
1837
|
+
method: 'PUT',
|
|
1838
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
1839
|
+
body: editor.getValue(),
|
|
1840
|
+
});
|
|
1841
|
+
status.textContent = response.ok ? 'Saved' : 'Save failed';
|
|
1842
|
+
});
|
|
1843
|
+
</script>
|
|
1844
|
+
</body>
|
|
1845
|
+
</html>`;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
2007
1848
|
// src/server/httpServer.ts
|
|
2008
1849
|
import { WebSocketServer } from "ws";
|
|
2009
|
-
var __dirname =
|
|
2010
|
-
var distDir =
|
|
2011
|
-
var spaEntryFile =
|
|
1850
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1851
|
+
var distDir = join3(__dirname, "..", "dist");
|
|
1852
|
+
var spaEntryFile = join3(distDir, "index.html");
|
|
2012
1853
|
var IMAGE_CONTENT_TYPES = {
|
|
2013
1854
|
".avif": "image/avif",
|
|
2014
1855
|
".bmp": "image/bmp",
|
|
@@ -2031,6 +1872,11 @@ function normalizeLocalImagePath(rawPath) {
|
|
|
2031
1872
|
}
|
|
2032
1873
|
return trimmed;
|
|
2033
1874
|
}
|
|
1875
|
+
function readWildcardPathParam(value) {
|
|
1876
|
+
if (typeof value === "string") return value;
|
|
1877
|
+
if (Array.isArray(value)) return value.join("/");
|
|
1878
|
+
return "";
|
|
1879
|
+
}
|
|
2034
1880
|
function createServer(options = {}) {
|
|
2035
1881
|
const app = express();
|
|
2036
1882
|
const bridge = createCodexBridgeMiddleware();
|
|
@@ -2046,7 +1892,7 @@ function createServer(options = {}) {
|
|
|
2046
1892
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2047
1893
|
return;
|
|
2048
1894
|
}
|
|
2049
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
1895
|
+
const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
|
|
2050
1896
|
if (!contentType) {
|
|
2051
1897
|
res.status(415).json({ error: "Unsupported image type." });
|
|
2052
1898
|
return;
|
|
@@ -2058,7 +1904,82 @@ function createServer(options = {}) {
|
|
|
2058
1904
|
if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
|
|
2059
1905
|
});
|
|
2060
1906
|
});
|
|
2061
|
-
|
|
1907
|
+
app.get("/codex-local-file", (req, res) => {
|
|
1908
|
+
const rawPath = typeof req.query.path === "string" ? req.query.path : "";
|
|
1909
|
+
const localPath = normalizeLocalPath(rawPath);
|
|
1910
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
1911
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
1915
|
+
res.setHeader("Content-Disposition", "inline");
|
|
1916
|
+
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
1917
|
+
if (!error) return;
|
|
1918
|
+
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
1919
|
+
});
|
|
1920
|
+
});
|
|
1921
|
+
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
1922
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
1923
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1924
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
1925
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
try {
|
|
1929
|
+
const fileStat = await stat3(localPath);
|
|
1930
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
1931
|
+
if (fileStat.isDirectory()) {
|
|
1932
|
+
const html = await createDirectoryListingHtml(localPath);
|
|
1933
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
1937
|
+
if (!error) return;
|
|
1938
|
+
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
1939
|
+
});
|
|
1940
|
+
} catch {
|
|
1941
|
+
res.status(404).json({ error: "File not found." });
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
app.get("/codex-local-edit/*path", async (req, res) => {
|
|
1945
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
1946
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1947
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
1948
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
try {
|
|
1952
|
+
const fileStat = await stat3(localPath);
|
|
1953
|
+
if (!fileStat.isFile()) {
|
|
1954
|
+
res.status(400).json({ error: "Expected file path." });
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const html = await createTextEditorHtml(localPath);
|
|
1958
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1959
|
+
} catch {
|
|
1960
|
+
res.status(404).json({ error: "File not found." });
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
|
|
1964
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
1965
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1966
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
1967
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (!await isTextEditableFile(localPath)) {
|
|
1971
|
+
res.status(415).json({ error: "Only text-like files are editable." });
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const body = typeof req.body === "string" ? req.body : "";
|
|
1975
|
+
try {
|
|
1976
|
+
await writeFile2(localPath, body, "utf8");
|
|
1977
|
+
res.status(200).json({ ok: true });
|
|
1978
|
+
} catch {
|
|
1979
|
+
res.status(404).json({ error: "File not found." });
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
const hasFrontendAssets = existsSync(spaEntryFile);
|
|
2062
1983
|
if (hasFrontendAssets) {
|
|
2063
1984
|
app.use(express.static(distDir));
|
|
2064
1985
|
}
|
|
@@ -2129,11 +2050,11 @@ function generatePassword() {
|
|
|
2129
2050
|
|
|
2130
2051
|
// src/cli/index.ts
|
|
2131
2052
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
2132
|
-
var __dirname2 =
|
|
2053
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2133
2054
|
async function readCliVersion() {
|
|
2134
2055
|
try {
|
|
2135
|
-
const packageJsonPath =
|
|
2136
|
-
const raw = await
|
|
2056
|
+
const packageJsonPath = join4(__dirname2, "..", "package.json");
|
|
2057
|
+
const raw = await readFile3(packageJsonPath, "utf8");
|
|
2137
2058
|
const parsed = JSON.parse(raw);
|
|
2138
2059
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2139
2060
|
} catch {
|
|
@@ -2158,29 +2079,127 @@ function runWithStatus(command, args) {
|
|
|
2158
2079
|
return result.status ?? -1;
|
|
2159
2080
|
}
|
|
2160
2081
|
function getUserNpmPrefix() {
|
|
2161
|
-
return
|
|
2082
|
+
return join4(homedir2(), ".npm-global");
|
|
2162
2083
|
}
|
|
2163
2084
|
function resolveCodexCommand() {
|
|
2164
2085
|
if (canRun("codex", ["--version"])) {
|
|
2165
2086
|
return "codex";
|
|
2166
2087
|
}
|
|
2167
|
-
const userCandidate =
|
|
2168
|
-
if (
|
|
2088
|
+
const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
|
|
2089
|
+
if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2169
2090
|
return userCandidate;
|
|
2170
2091
|
}
|
|
2171
2092
|
const prefix = process.env.PREFIX?.trim();
|
|
2172
2093
|
if (!prefix) {
|
|
2173
2094
|
return null;
|
|
2174
2095
|
}
|
|
2175
|
-
const candidate =
|
|
2176
|
-
if (
|
|
2096
|
+
const candidate = join4(prefix, "bin", "codex");
|
|
2097
|
+
if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
|
|
2177
2098
|
return candidate;
|
|
2178
2099
|
}
|
|
2179
2100
|
return null;
|
|
2180
2101
|
}
|
|
2102
|
+
function resolveCloudflaredCommand() {
|
|
2103
|
+
if (canRun("cloudflared", ["--version"])) {
|
|
2104
|
+
return "cloudflared";
|
|
2105
|
+
}
|
|
2106
|
+
const localCandidate = join4(homedir2(), ".local", "bin", "cloudflared");
|
|
2107
|
+
if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2108
|
+
return localCandidate;
|
|
2109
|
+
}
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
function mapCloudflaredLinuxArch(arch) {
|
|
2113
|
+
if (arch === "x64") {
|
|
2114
|
+
return "amd64";
|
|
2115
|
+
}
|
|
2116
|
+
if (arch === "arm64") {
|
|
2117
|
+
return "arm64";
|
|
2118
|
+
}
|
|
2119
|
+
return null;
|
|
2120
|
+
}
|
|
2121
|
+
function downloadFile(url, destination) {
|
|
2122
|
+
return new Promise((resolve2, reject) => {
|
|
2123
|
+
const request = (currentUrl) => {
|
|
2124
|
+
httpsGet(currentUrl, (response) => {
|
|
2125
|
+
const code = response.statusCode ?? 0;
|
|
2126
|
+
if (code >= 300 && code < 400 && response.headers.location) {
|
|
2127
|
+
response.resume();
|
|
2128
|
+
request(response.headers.location);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
if (code !== 200) {
|
|
2132
|
+
response.resume();
|
|
2133
|
+
reject(new Error(`Download failed with HTTP status ${String(code)}`));
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const file = createWriteStream(destination, { mode: 493 });
|
|
2137
|
+
response.pipe(file);
|
|
2138
|
+
file.on("finish", () => {
|
|
2139
|
+
file.close();
|
|
2140
|
+
resolve2();
|
|
2141
|
+
});
|
|
2142
|
+
file.on("error", reject);
|
|
2143
|
+
}).on("error", reject);
|
|
2144
|
+
};
|
|
2145
|
+
request(url);
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
async function ensureCloudflaredInstalledLinux() {
|
|
2149
|
+
const current = resolveCloudflaredCommand();
|
|
2150
|
+
if (current) {
|
|
2151
|
+
return current;
|
|
2152
|
+
}
|
|
2153
|
+
if (process.platform !== "linux") {
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
const mappedArch = mapCloudflaredLinuxArch(process.arch);
|
|
2157
|
+
if (!mappedArch) {
|
|
2158
|
+
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2159
|
+
}
|
|
2160
|
+
const userBinDir = join4(homedir2(), ".local", "bin");
|
|
2161
|
+
mkdirSync(userBinDir, { recursive: true });
|
|
2162
|
+
const destination = join4(userBinDir, "cloudflared");
|
|
2163
|
+
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2164
|
+
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
2165
|
+
await downloadFile(downloadUrl, destination);
|
|
2166
|
+
chmodSync(destination, 493);
|
|
2167
|
+
process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
|
|
2168
|
+
const installed = resolveCloudflaredCommand();
|
|
2169
|
+
if (!installed) {
|
|
2170
|
+
throw new Error("cloudflared download completed but executable is still not available");
|
|
2171
|
+
}
|
|
2172
|
+
console.log("\ncloudflared installed.\n");
|
|
2173
|
+
return installed;
|
|
2174
|
+
}
|
|
2175
|
+
async function shouldInstallCloudflaredInteractively() {
|
|
2176
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2177
|
+
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
2178
|
+
return false;
|
|
2179
|
+
}
|
|
2180
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
2181
|
+
try {
|
|
2182
|
+
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
2183
|
+
const normalized = answer.trim().toLowerCase();
|
|
2184
|
+
return normalized === "y" || normalized === "yes";
|
|
2185
|
+
} finally {
|
|
2186
|
+
prompt.close();
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
async function resolveCloudflaredForTunnel() {
|
|
2190
|
+
const current = resolveCloudflaredCommand();
|
|
2191
|
+
if (current) {
|
|
2192
|
+
return current;
|
|
2193
|
+
}
|
|
2194
|
+
const installApproved = await shouldInstallCloudflaredInteractively();
|
|
2195
|
+
if (!installApproved) {
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
return ensureCloudflaredInstalledLinux();
|
|
2199
|
+
}
|
|
2181
2200
|
function hasCodexAuth() {
|
|
2182
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
2183
|
-
return
|
|
2201
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
|
|
2202
|
+
return existsSync2(join4(codexHome, "auth.json"));
|
|
2184
2203
|
}
|
|
2185
2204
|
function ensureCodexInstalled() {
|
|
2186
2205
|
let codexCommand = resolveCodexCommand();
|
|
@@ -2198,7 +2217,7 @@ function ensureCodexInstalled() {
|
|
|
2198
2217
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
2199
2218
|
`);
|
|
2200
2219
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
2201
|
-
process.env.PATH = `${
|
|
2220
|
+
process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
2202
2221
|
};
|
|
2203
2222
|
if (isTermuxRuntime()) {
|
|
2204
2223
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -2259,9 +2278,27 @@ function parseCloudflaredUrl(chunk) {
|
|
|
2259
2278
|
}
|
|
2260
2279
|
return urlMatch[urlMatch.length - 1] ?? null;
|
|
2261
2280
|
}
|
|
2262
|
-
|
|
2281
|
+
function getAccessibleUrls(port) {
|
|
2282
|
+
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
2283
|
+
const interfaces = networkInterfaces();
|
|
2284
|
+
for (const entries of Object.values(interfaces)) {
|
|
2285
|
+
if (!entries) {
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
for (const entry of entries) {
|
|
2289
|
+
if (entry.internal) {
|
|
2290
|
+
continue;
|
|
2291
|
+
}
|
|
2292
|
+
if (entry.family === "IPv4") {
|
|
2293
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
return Array.from(urls);
|
|
2298
|
+
}
|
|
2299
|
+
async function startCloudflaredTunnel(command, localPort) {
|
|
2263
2300
|
return new Promise((resolve2, reject) => {
|
|
2264
|
-
const child = spawn2(
|
|
2301
|
+
const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
2265
2302
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2266
2303
|
});
|
|
2267
2304
|
const timeout = setTimeout(() => {
|
|
@@ -2312,7 +2349,7 @@ function listenWithFallback(server, startPort) {
|
|
|
2312
2349
|
};
|
|
2313
2350
|
server.once("error", onError);
|
|
2314
2351
|
server.once("listening", onListening);
|
|
2315
|
-
server.listen(port);
|
|
2352
|
+
server.listen(port, "0.0.0.0");
|
|
2316
2353
|
};
|
|
2317
2354
|
attempt(startPort);
|
|
2318
2355
|
});
|
|
@@ -2334,7 +2371,11 @@ async function startServer(options) {
|
|
|
2334
2371
|
let tunnelUrl = null;
|
|
2335
2372
|
if (options.tunnel) {
|
|
2336
2373
|
try {
|
|
2337
|
-
const
|
|
2374
|
+
const cloudflaredCommand = await resolveCloudflaredForTunnel();
|
|
2375
|
+
if (!cloudflaredCommand) {
|
|
2376
|
+
throw new Error("cloudflared is not installed");
|
|
2377
|
+
}
|
|
2378
|
+
const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
|
|
2338
2379
|
tunnelChild = tunnel.process;
|
|
2339
2380
|
tunnelUrl = tunnel.url;
|
|
2340
2381
|
} catch (error) {
|
|
@@ -2349,8 +2390,15 @@ async function startServer(options) {
|
|
|
2349
2390
|
` Version: ${version}`,
|
|
2350
2391
|
" GitHub: https://github.com/friuns2/codexui",
|
|
2351
2392
|
"",
|
|
2352
|
-
`
|
|
2393
|
+
` Bind: http://0.0.0.0:${String(port)}`
|
|
2353
2394
|
];
|
|
2395
|
+
const accessUrls = getAccessibleUrls(port);
|
|
2396
|
+
if (accessUrls.length > 0) {
|
|
2397
|
+
lines.push(` Local: ${accessUrls[0]}`);
|
|
2398
|
+
for (const accessUrl of accessUrls.slice(1)) {
|
|
2399
|
+
lines.push(` Network: ${accessUrl}`);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2354
2402
|
if (port !== requestedPort) {
|
|
2355
2403
|
lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
|
|
2356
2404
|
}
|
|
@@ -2359,9 +2407,7 @@ async function startServer(options) {
|
|
|
2359
2407
|
}
|
|
2360
2408
|
if (tunnelUrl) {
|
|
2361
2409
|
lines.push(` Tunnel: ${tunnelUrl}`);
|
|
2362
|
-
lines.push("");
|
|
2363
|
-
lines.push(" Tunnel QR code:");
|
|
2364
|
-
lines.push(` URL: ${tunnelUrl}`);
|
|
2410
|
+
lines.push(" Tunnel QR code below");
|
|
2365
2411
|
}
|
|
2366
2412
|
printTermuxKeepAlive(lines);
|
|
2367
2413
|
lines.push("");
|