codexapp 0.1.33 → 0.1.34
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-5eZebBiF.js +1442 -0
- package/dist/assets/index-zaSJxL5w.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +782 -37
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -2
- package/dist/assets/index-CJZ_Yq9m.js +0 -1442
- package/dist/assets/index-PpE_h85K.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
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 existsSync3 } from "fs";
|
|
7
|
-
import { readFile as
|
|
8
|
-
import { homedir as homedir2 } from "os";
|
|
9
|
-
import { join as
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync3, 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 { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
19
20
|
import { existsSync as existsSync2 } 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, lstat, readlink, symlink } from "fs/promises";
|
|
26
28
|
import { existsSync } from "fs";
|
|
27
29
|
import { request as httpsRequest } from "https";
|
|
28
30
|
import { homedir } from "os";
|
|
29
31
|
import { tmpdir } from "os";
|
|
30
|
-
import { isAbsolute, join, resolve } from "path";
|
|
32
|
+
import { basename, isAbsolute, join, resolve } from "path";
|
|
31
33
|
import { writeFile } from "fs/promises";
|
|
32
34
|
function asRecord(value) {
|
|
33
35
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -51,6 +53,46 @@ function setJson(res, statusCode, payload) {
|
|
|
51
53
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
52
54
|
res.end(JSON.stringify(payload));
|
|
53
55
|
}
|
|
56
|
+
function extractThreadMessageText(threadReadPayload) {
|
|
57
|
+
const payload = asRecord(threadReadPayload);
|
|
58
|
+
const thread = asRecord(payload?.thread);
|
|
59
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
60
|
+
const parts = [];
|
|
61
|
+
for (const turn of turns) {
|
|
62
|
+
const turnRecord = asRecord(turn);
|
|
63
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const itemRecord = asRecord(item);
|
|
66
|
+
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
67
|
+
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
68
|
+
parts.push(itemRecord.text.trim());
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (type === "userMessage") {
|
|
72
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
73
|
+
for (const block of content) {
|
|
74
|
+
const blockRecord = asRecord(block);
|
|
75
|
+
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
76
|
+
parts.push(blockRecord.text.trim());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (type === "commandExecution") {
|
|
82
|
+
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
83
|
+
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
84
|
+
if (command) parts.push(command);
|
|
85
|
+
if (output) parts.push(output);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return parts.join("\n").trim();
|
|
90
|
+
}
|
|
91
|
+
function isExactPhraseMatch(query, doc) {
|
|
92
|
+
const q = query.trim().toLowerCase();
|
|
93
|
+
if (!q) return false;
|
|
94
|
+
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
95
|
+
}
|
|
54
96
|
function scoreFileCandidate(path, query) {
|
|
55
97
|
if (!query) return 0;
|
|
56
98
|
const lowerPath = path.toLowerCase();
|
|
@@ -124,6 +166,55 @@ async function runCommand(command, args, options = {}) {
|
|
|
124
166
|
});
|
|
125
167
|
});
|
|
126
168
|
}
|
|
169
|
+
function isMissingHeadError(error) {
|
|
170
|
+
const message = getErrorMessage(error, "").toLowerCase();
|
|
171
|
+
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
172
|
+
}
|
|
173
|
+
function isNotGitRepositoryError(error) {
|
|
174
|
+
const message = getErrorMessage(error, "").toLowerCase();
|
|
175
|
+
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
176
|
+
}
|
|
177
|
+
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
178
|
+
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
179
|
+
try {
|
|
180
|
+
await stat(agentsPath);
|
|
181
|
+
} catch {
|
|
182
|
+
await writeFile(agentsPath, "", "utf8");
|
|
183
|
+
}
|
|
184
|
+
await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
185
|
+
await runCommand(
|
|
186
|
+
"git",
|
|
187
|
+
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
188
|
+
{ cwd: repoRoot }
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
async function runCommandCapture(command, args, options = {}) {
|
|
192
|
+
return await new Promise((resolve2, reject) => {
|
|
193
|
+
const proc = spawn(command, args, {
|
|
194
|
+
cwd: options.cwd,
|
|
195
|
+
env: process.env,
|
|
196
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
197
|
+
});
|
|
198
|
+
let stdout = "";
|
|
199
|
+
let stderr = "";
|
|
200
|
+
proc.stdout.on("data", (chunk) => {
|
|
201
|
+
stdout += chunk.toString();
|
|
202
|
+
});
|
|
203
|
+
proc.stderr.on("data", (chunk) => {
|
|
204
|
+
stderr += chunk.toString();
|
|
205
|
+
});
|
|
206
|
+
proc.on("error", reject);
|
|
207
|
+
proc.on("close", (code) => {
|
|
208
|
+
if (code === 0) {
|
|
209
|
+
resolve2(stdout.trim());
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
213
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
214
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
127
218
|
async function runCommandWithOutput(command, args, options = {}) {
|
|
128
219
|
return await new Promise((resolve2, reject) => {
|
|
129
220
|
const proc = spawn(command, args, {
|
|
@@ -142,7 +233,7 @@ async function runCommandWithOutput(command, args, options = {}) {
|
|
|
142
233
|
proc.on("error", reject);
|
|
143
234
|
proc.on("close", (code) => {
|
|
144
235
|
if (code === 0) {
|
|
145
|
-
resolve2(stdout);
|
|
236
|
+
resolve2(stdout.trim());
|
|
146
237
|
return;
|
|
147
238
|
}
|
|
148
239
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -721,11 +812,29 @@ async function ensureCodexAgentsSymlinkToSkillsAgents() {
|
|
|
721
812
|
const codexHomeDir = getCodexHomeDir();
|
|
722
813
|
const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
|
|
723
814
|
const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
|
|
815
|
+
await mkdir(join(codexHomeDir, "skills"), { recursive: true });
|
|
816
|
+
let copiedFromCodex = false;
|
|
724
817
|
try {
|
|
725
|
-
const
|
|
726
|
-
if (
|
|
818
|
+
const codexAgentsStat = await lstat(codexAgentsPath);
|
|
819
|
+
if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
|
|
820
|
+
const content = await readFile(codexAgentsPath, "utf8");
|
|
821
|
+
await writeFile(skillsAgentsPath, content, "utf8");
|
|
822
|
+
copiedFromCodex = true;
|
|
823
|
+
} else {
|
|
824
|
+
await rm(codexAgentsPath, { force: true, recursive: true });
|
|
825
|
+
}
|
|
727
826
|
} catch {
|
|
728
|
-
|
|
827
|
+
}
|
|
828
|
+
if (!copiedFromCodex) {
|
|
829
|
+
try {
|
|
830
|
+
const skillsAgentsStat = await stat(skillsAgentsPath);
|
|
831
|
+
if (!skillsAgentsStat.isFile()) {
|
|
832
|
+
await rm(skillsAgentsPath, { force: true, recursive: true });
|
|
833
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
834
|
+
}
|
|
835
|
+
} catch {
|
|
836
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
837
|
+
}
|
|
729
838
|
}
|
|
730
839
|
const relativeTarget = join("skills", "AGENTS.md");
|
|
731
840
|
try {
|
|
@@ -1402,8 +1511,82 @@ function getSharedBridgeState() {
|
|
|
1402
1511
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
1403
1512
|
return created;
|
|
1404
1513
|
}
|
|
1514
|
+
async function loadAllThreadsForSearch(appServer) {
|
|
1515
|
+
const threads = [];
|
|
1516
|
+
let cursor = null;
|
|
1517
|
+
do {
|
|
1518
|
+
const response = asRecord(await appServer.rpc("thread/list", {
|
|
1519
|
+
archived: false,
|
|
1520
|
+
limit: 100,
|
|
1521
|
+
sortKey: "updated_at",
|
|
1522
|
+
cursor
|
|
1523
|
+
}));
|
|
1524
|
+
const data = Array.isArray(response?.data) ? response.data : [];
|
|
1525
|
+
for (const row of data) {
|
|
1526
|
+
const record = asRecord(row);
|
|
1527
|
+
const id = typeof record?.id === "string" ? record.id : "";
|
|
1528
|
+
if (!id) continue;
|
|
1529
|
+
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";
|
|
1530
|
+
const preview = typeof record?.preview === "string" ? record.preview : "";
|
|
1531
|
+
threads.push({ id, title, preview });
|
|
1532
|
+
}
|
|
1533
|
+
cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
|
|
1534
|
+
} while (cursor);
|
|
1535
|
+
const docs = [];
|
|
1536
|
+
const concurrency = 4;
|
|
1537
|
+
for (let offset = 0; offset < threads.length; offset += concurrency) {
|
|
1538
|
+
const batch = threads.slice(offset, offset + concurrency);
|
|
1539
|
+
const loaded = await Promise.all(batch.map(async (thread) => {
|
|
1540
|
+
try {
|
|
1541
|
+
const readResponse = await appServer.rpc("thread/read", {
|
|
1542
|
+
threadId: thread.id,
|
|
1543
|
+
includeTurns: true
|
|
1544
|
+
});
|
|
1545
|
+
const messageText = extractThreadMessageText(readResponse);
|
|
1546
|
+
const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
|
|
1547
|
+
return {
|
|
1548
|
+
id: thread.id,
|
|
1549
|
+
title: thread.title,
|
|
1550
|
+
preview: thread.preview,
|
|
1551
|
+
messageText,
|
|
1552
|
+
searchableText
|
|
1553
|
+
};
|
|
1554
|
+
} catch {
|
|
1555
|
+
const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
|
|
1556
|
+
return {
|
|
1557
|
+
id: thread.id,
|
|
1558
|
+
title: thread.title,
|
|
1559
|
+
preview: thread.preview,
|
|
1560
|
+
messageText: "",
|
|
1561
|
+
searchableText
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
}));
|
|
1565
|
+
docs.push(...loaded);
|
|
1566
|
+
}
|
|
1567
|
+
return docs;
|
|
1568
|
+
}
|
|
1569
|
+
async function buildThreadSearchIndex(appServer) {
|
|
1570
|
+
const docs = await loadAllThreadsForSearch(appServer);
|
|
1571
|
+
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
1572
|
+
return { docsById };
|
|
1573
|
+
}
|
|
1405
1574
|
function createCodexBridgeMiddleware() {
|
|
1406
1575
|
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
1576
|
+
let threadSearchIndex = null;
|
|
1577
|
+
let threadSearchIndexPromise = null;
|
|
1578
|
+
async function getThreadSearchIndex() {
|
|
1579
|
+
if (threadSearchIndex) return threadSearchIndex;
|
|
1580
|
+
if (!threadSearchIndexPromise) {
|
|
1581
|
+
threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
|
|
1582
|
+
threadSearchIndex = index;
|
|
1583
|
+
return index;
|
|
1584
|
+
}).finally(() => {
|
|
1585
|
+
threadSearchIndexPromise = null;
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
return threadSearchIndexPromise;
|
|
1589
|
+
}
|
|
1407
1590
|
void initializeSkillsSyncOnStartup(appServer);
|
|
1408
1591
|
const middleware = async (req, res, next) => {
|
|
1409
1592
|
try {
|
|
@@ -1470,6 +1653,76 @@ function createCodexBridgeMiddleware() {
|
|
|
1470
1653
|
setJson(res, 200, { data: { path: homedir() } });
|
|
1471
1654
|
return;
|
|
1472
1655
|
}
|
|
1656
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1657
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1658
|
+
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1659
|
+
if (!rawSourceCwd) {
|
|
1660
|
+
setJson(res, 400, { error: "Missing sourceCwd" });
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1664
|
+
try {
|
|
1665
|
+
const sourceInfo = await stat(sourceCwd);
|
|
1666
|
+
if (!sourceInfo.isDirectory()) {
|
|
1667
|
+
setJson(res, 400, { error: "sourceCwd is not a directory" });
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
} catch {
|
|
1671
|
+
setJson(res, 404, { error: "sourceCwd does not exist" });
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
let gitRoot = "";
|
|
1676
|
+
try {
|
|
1677
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
if (!isNotGitRepositoryError(error)) throw error;
|
|
1680
|
+
await runCommand("git", ["init"], { cwd: sourceCwd });
|
|
1681
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1682
|
+
}
|
|
1683
|
+
const repoName = basename(gitRoot) || "repo";
|
|
1684
|
+
const worktreesRoot = join(getCodexHomeDir(), "worktrees");
|
|
1685
|
+
await mkdir(worktreesRoot, { recursive: true });
|
|
1686
|
+
let worktreeId = "";
|
|
1687
|
+
let worktreeParent = "";
|
|
1688
|
+
let worktreeCwd = "";
|
|
1689
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1690
|
+
const candidate = randomBytes(2).toString("hex");
|
|
1691
|
+
const parent = join(worktreesRoot, candidate);
|
|
1692
|
+
try {
|
|
1693
|
+
await stat(parent);
|
|
1694
|
+
continue;
|
|
1695
|
+
} catch {
|
|
1696
|
+
worktreeId = candidate;
|
|
1697
|
+
worktreeParent = parent;
|
|
1698
|
+
worktreeCwd = join(parent, repoName);
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (!worktreeId || !worktreeParent || !worktreeCwd) {
|
|
1703
|
+
throw new Error("Failed to allocate a unique worktree id");
|
|
1704
|
+
}
|
|
1705
|
+
const branch = `codex/${worktreeId}`;
|
|
1706
|
+
await mkdir(worktreeParent, { recursive: true });
|
|
1707
|
+
try {
|
|
1708
|
+
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
if (!isMissingHeadError(error)) throw error;
|
|
1711
|
+
await ensureRepoHasInitialCommit(gitRoot);
|
|
1712
|
+
await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1713
|
+
}
|
|
1714
|
+
setJson(res, 200, {
|
|
1715
|
+
data: {
|
|
1716
|
+
cwd: worktreeCwd,
|
|
1717
|
+
branch,
|
|
1718
|
+
gitRoot
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
|
|
1723
|
+
}
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1473
1726
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1474
1727
|
const payload = await readJsonBody(req);
|
|
1475
1728
|
const record = asRecord(payload);
|
|
@@ -1595,6 +1848,20 @@ function createCodexBridgeMiddleware() {
|
|
|
1595
1848
|
setJson(res, 200, { data: cache });
|
|
1596
1849
|
return;
|
|
1597
1850
|
}
|
|
1851
|
+
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1852
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1853
|
+
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1854
|
+
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1855
|
+
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1856
|
+
if (!query) {
|
|
1857
|
+
setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const index = await getThreadSearchIndex();
|
|
1861
|
+
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1862
|
+
setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1598
1865
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1599
1866
|
const payload = asRecord(await readJsonBody(req));
|
|
1600
1867
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
@@ -1761,7 +2028,13 @@ function createCodexBridgeMiddleware() {
|
|
|
1761
2028
|
try {
|
|
1762
2029
|
const state = await readSkillsSyncState();
|
|
1763
2030
|
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
1764
|
-
|
|
2031
|
+
const localDir2 = await detectUserSkillsDir(appServer);
|
|
2032
|
+
await bootstrapSkillsFromUpstreamIntoLocal(localDir2);
|
|
2033
|
+
try {
|
|
2034
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
|
|
1765
2038
|
return;
|
|
1766
2039
|
}
|
|
1767
2040
|
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
@@ -1945,6 +2218,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1945
2218
|
}
|
|
1946
2219
|
};
|
|
1947
2220
|
middleware.dispose = () => {
|
|
2221
|
+
threadSearchIndex = null;
|
|
1948
2222
|
appServer.dispose();
|
|
1949
2223
|
};
|
|
1950
2224
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -1959,7 +2233,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1959
2233
|
}
|
|
1960
2234
|
|
|
1961
2235
|
// src/server/authMiddleware.ts
|
|
1962
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
2236
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1963
2237
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1964
2238
|
function constantTimeCompare(a, b) {
|
|
1965
2239
|
const bufA = Buffer.from(a);
|
|
@@ -2057,7 +2331,7 @@ function createAuthSession(password) {
|
|
|
2057
2331
|
res.status(401).json({ error: "Invalid password" });
|
|
2058
2332
|
return;
|
|
2059
2333
|
}
|
|
2060
|
-
const token =
|
|
2334
|
+
const token = randomBytes2(32).toString("hex");
|
|
2061
2335
|
validTokens.add(token);
|
|
2062
2336
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
2063
2337
|
res.json({ ok: true });
|
|
@@ -2076,11 +2350,277 @@ function createAuthSession(password) {
|
|
|
2076
2350
|
};
|
|
2077
2351
|
}
|
|
2078
2352
|
|
|
2353
|
+
// src/server/localBrowseUi.ts
|
|
2354
|
+
import { dirname, extname, join as join2 } from "path";
|
|
2355
|
+
import { open, readFile as readFile2, readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
2356
|
+
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2357
|
+
".txt",
|
|
2358
|
+
".md",
|
|
2359
|
+
".json",
|
|
2360
|
+
".js",
|
|
2361
|
+
".ts",
|
|
2362
|
+
".tsx",
|
|
2363
|
+
".jsx",
|
|
2364
|
+
".css",
|
|
2365
|
+
".scss",
|
|
2366
|
+
".html",
|
|
2367
|
+
".htm",
|
|
2368
|
+
".xml",
|
|
2369
|
+
".yml",
|
|
2370
|
+
".yaml",
|
|
2371
|
+
".log",
|
|
2372
|
+
".csv",
|
|
2373
|
+
".env",
|
|
2374
|
+
".py",
|
|
2375
|
+
".sh",
|
|
2376
|
+
".toml",
|
|
2377
|
+
".ini",
|
|
2378
|
+
".conf",
|
|
2379
|
+
".sql",
|
|
2380
|
+
".bat",
|
|
2381
|
+
".cmd",
|
|
2382
|
+
".ps1"
|
|
2383
|
+
]);
|
|
2384
|
+
function languageForPath(pathValue) {
|
|
2385
|
+
const extension = extname(pathValue).toLowerCase();
|
|
2386
|
+
switch (extension) {
|
|
2387
|
+
case ".js":
|
|
2388
|
+
return "javascript";
|
|
2389
|
+
case ".ts":
|
|
2390
|
+
return "typescript";
|
|
2391
|
+
case ".jsx":
|
|
2392
|
+
return "javascript";
|
|
2393
|
+
case ".tsx":
|
|
2394
|
+
return "typescript";
|
|
2395
|
+
case ".py":
|
|
2396
|
+
return "python";
|
|
2397
|
+
case ".sh":
|
|
2398
|
+
return "sh";
|
|
2399
|
+
case ".css":
|
|
2400
|
+
case ".scss":
|
|
2401
|
+
return "css";
|
|
2402
|
+
case ".html":
|
|
2403
|
+
case ".htm":
|
|
2404
|
+
return "html";
|
|
2405
|
+
case ".json":
|
|
2406
|
+
return "json";
|
|
2407
|
+
case ".md":
|
|
2408
|
+
return "markdown";
|
|
2409
|
+
case ".yaml":
|
|
2410
|
+
case ".yml":
|
|
2411
|
+
return "yaml";
|
|
2412
|
+
case ".xml":
|
|
2413
|
+
return "xml";
|
|
2414
|
+
case ".sql":
|
|
2415
|
+
return "sql";
|
|
2416
|
+
case ".toml":
|
|
2417
|
+
return "ini";
|
|
2418
|
+
case ".ini":
|
|
2419
|
+
case ".conf":
|
|
2420
|
+
return "ini";
|
|
2421
|
+
default:
|
|
2422
|
+
return "plaintext";
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
function normalizeLocalPath(rawPath) {
|
|
2426
|
+
const trimmed = rawPath.trim();
|
|
2427
|
+
if (!trimmed) return "";
|
|
2428
|
+
if (trimmed.startsWith("file://")) {
|
|
2429
|
+
try {
|
|
2430
|
+
return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
|
|
2431
|
+
} catch {
|
|
2432
|
+
return trimmed.replace(/^file:\/\//u, "");
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
return trimmed;
|
|
2436
|
+
}
|
|
2437
|
+
function decodeBrowsePath(rawPath) {
|
|
2438
|
+
if (!rawPath) return "";
|
|
2439
|
+
try {
|
|
2440
|
+
return decodeURIComponent(rawPath);
|
|
2441
|
+
} catch {
|
|
2442
|
+
return rawPath;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
function isTextEditablePath(pathValue) {
|
|
2446
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
|
|
2447
|
+
}
|
|
2448
|
+
function looksLikeTextBuffer(buffer) {
|
|
2449
|
+
if (buffer.length === 0) return true;
|
|
2450
|
+
for (const byte of buffer) {
|
|
2451
|
+
if (byte === 0) return false;
|
|
2452
|
+
}
|
|
2453
|
+
const decoded = buffer.toString("utf8");
|
|
2454
|
+
const replacementCount = (decoded.match(/\uFFFD/gu) ?? []).length;
|
|
2455
|
+
return replacementCount / decoded.length < 0.05;
|
|
2456
|
+
}
|
|
2457
|
+
async function probeFileIsText(localPath) {
|
|
2458
|
+
const handle = await open(localPath, "r");
|
|
2459
|
+
try {
|
|
2460
|
+
const sample = Buffer.allocUnsafe(4096);
|
|
2461
|
+
const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
|
|
2462
|
+
return looksLikeTextBuffer(sample.subarray(0, bytesRead));
|
|
2463
|
+
} finally {
|
|
2464
|
+
await handle.close();
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
async function isTextEditableFile(localPath) {
|
|
2468
|
+
if (isTextEditablePath(localPath)) return true;
|
|
2469
|
+
try {
|
|
2470
|
+
const fileStat = await stat2(localPath);
|
|
2471
|
+
if (!fileStat.isFile()) return false;
|
|
2472
|
+
return await probeFileIsText(localPath);
|
|
2473
|
+
} catch {
|
|
2474
|
+
return false;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
function escapeHtml(value) {
|
|
2478
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
2479
|
+
}
|
|
2480
|
+
function toBrowseHref(pathValue) {
|
|
2481
|
+
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
2482
|
+
}
|
|
2483
|
+
function toEditHref(pathValue) {
|
|
2484
|
+
return `/codex-local-edit${encodeURI(pathValue)}`;
|
|
2485
|
+
}
|
|
2486
|
+
function escapeForInlineScriptString(value) {
|
|
2487
|
+
return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
|
|
2488
|
+
}
|
|
2489
|
+
async function getDirectoryItems(localPath) {
|
|
2490
|
+
const entries = await readdir2(localPath, { withFileTypes: true });
|
|
2491
|
+
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
2492
|
+
const entryPath = join2(localPath, entry.name);
|
|
2493
|
+
const entryStat = await stat2(entryPath);
|
|
2494
|
+
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
2495
|
+
return {
|
|
2496
|
+
name: entry.name,
|
|
2497
|
+
path: entryPath,
|
|
2498
|
+
isDirectory: entry.isDirectory(),
|
|
2499
|
+
editable,
|
|
2500
|
+
mtimeMs: entryStat.mtimeMs
|
|
2501
|
+
};
|
|
2502
|
+
}));
|
|
2503
|
+
return withMeta.sort((a, b) => {
|
|
2504
|
+
if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
|
|
2505
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
2506
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
2507
|
+
return a.name.localeCompare(b.name);
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
async function createDirectoryListingHtml(localPath) {
|
|
2511
|
+
const items = await getDirectoryItems(localPath);
|
|
2512
|
+
const parentPath = dirname(localPath);
|
|
2513
|
+
const rows = items.map((item) => {
|
|
2514
|
+
const suffix = item.isDirectory ? "/" : "";
|
|
2515
|
+
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
2516
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
2517
|
+
}).join("\n");
|
|
2518
|
+
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
2519
|
+
return `<!doctype html>
|
|
2520
|
+
<html lang="en">
|
|
2521
|
+
<head>
|
|
2522
|
+
<meta charset="utf-8" />
|
|
2523
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2524
|
+
<title>Index of ${escapeHtml(localPath)}</title>
|
|
2525
|
+
<style>
|
|
2526
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
|
|
2527
|
+
a { color: #8cc2ff; text-decoration: none; }
|
|
2528
|
+
a:hover { text-decoration: underline; }
|
|
2529
|
+
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
2530
|
+
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
2531
|
+
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
2532
|
+
.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; }
|
|
2533
|
+
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
2534
|
+
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
2535
|
+
@media (max-width: 640px) {
|
|
2536
|
+
body { margin: 12px; }
|
|
2537
|
+
.file-row { gap: 8px; }
|
|
2538
|
+
.file-link { font-size: 15px; padding: 12px; }
|
|
2539
|
+
.icon-btn { width: 44px; height: 44px; }
|
|
2540
|
+
}
|
|
2541
|
+
</style>
|
|
2542
|
+
</head>
|
|
2543
|
+
<body>
|
|
2544
|
+
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
2545
|
+
${parentLink}
|
|
2546
|
+
<ul>${rows}</ul>
|
|
2547
|
+
</body>
|
|
2548
|
+
</html>`;
|
|
2549
|
+
}
|
|
2550
|
+
async function createTextEditorHtml(localPath) {
|
|
2551
|
+
const content = await readFile2(localPath, "utf8");
|
|
2552
|
+
const parentPath = dirname(localPath);
|
|
2553
|
+
const language = languageForPath(localPath);
|
|
2554
|
+
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
2555
|
+
return `<!doctype html>
|
|
2556
|
+
<html lang="en">
|
|
2557
|
+
<head>
|
|
2558
|
+
<meta charset="utf-8" />
|
|
2559
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2560
|
+
<title>Edit ${escapeHtml(localPath)}</title>
|
|
2561
|
+
<style>
|
|
2562
|
+
html, body { width: 100%; height: 100%; margin: 0; }
|
|
2563
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
|
|
2564
|
+
.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; }
|
|
2565
|
+
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
2566
|
+
button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
|
|
2567
|
+
button:hover, a:hover { filter: brightness(1.08); }
|
|
2568
|
+
#editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
|
|
2569
|
+
#status { margin-left: 8px; color: #8cc2ff; }
|
|
2570
|
+
.ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
|
|
2571
|
+
.ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
|
|
2572
|
+
.ace_marker-layer .ace_active-line { background: #10213c !important; }
|
|
2573
|
+
.ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
|
|
2574
|
+
.meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
|
|
2575
|
+
</style>
|
|
2576
|
+
</head>
|
|
2577
|
+
<body>
|
|
2578
|
+
<div class="toolbar">
|
|
2579
|
+
<div class="row">
|
|
2580
|
+
<a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
|
|
2581
|
+
<button id="saveBtn" type="button">Save</button>
|
|
2582
|
+
<span id="status"></span>
|
|
2583
|
+
</div>
|
|
2584
|
+
<div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
|
|
2585
|
+
</div>
|
|
2586
|
+
<div id="editor"></div>
|
|
2587
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
|
|
2588
|
+
<script>
|
|
2589
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
2590
|
+
const status = document.getElementById('status');
|
|
2591
|
+
const editor = ace.edit('editor');
|
|
2592
|
+
editor.setTheme('ace/theme/tomorrow_night');
|
|
2593
|
+
editor.session.setMode('ace/mode/${escapeHtml(language)}');
|
|
2594
|
+
editor.setValue(${safeContentLiteral}, -1);
|
|
2595
|
+
editor.setOptions({
|
|
2596
|
+
fontSize: '13px',
|
|
2597
|
+
wrap: true,
|
|
2598
|
+
showPrintMargin: false,
|
|
2599
|
+
useSoftTabs: true,
|
|
2600
|
+
tabSize: 2,
|
|
2601
|
+
behavioursEnabled: true,
|
|
2602
|
+
});
|
|
2603
|
+
editor.resize();
|
|
2604
|
+
|
|
2605
|
+
saveBtn.addEventListener('click', async () => {
|
|
2606
|
+
status.textContent = 'Saving...';
|
|
2607
|
+
const response = await fetch(location.pathname, {
|
|
2608
|
+
method: 'PUT',
|
|
2609
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
2610
|
+
body: editor.getValue(),
|
|
2611
|
+
});
|
|
2612
|
+
status.textContent = response.ok ? 'Saved' : 'Save failed';
|
|
2613
|
+
});
|
|
2614
|
+
</script>
|
|
2615
|
+
</body>
|
|
2616
|
+
</html>`;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2079
2619
|
// src/server/httpServer.ts
|
|
2080
2620
|
import { WebSocketServer } from "ws";
|
|
2081
|
-
var __dirname =
|
|
2082
|
-
var distDir =
|
|
2083
|
-
var spaEntryFile =
|
|
2621
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
2622
|
+
var distDir = join3(__dirname, "..", "dist");
|
|
2623
|
+
var spaEntryFile = join3(distDir, "index.html");
|
|
2084
2624
|
var IMAGE_CONTENT_TYPES = {
|
|
2085
2625
|
".avif": "image/avif",
|
|
2086
2626
|
".bmp": "image/bmp",
|
|
@@ -2103,6 +2643,11 @@ function normalizeLocalImagePath(rawPath) {
|
|
|
2103
2643
|
}
|
|
2104
2644
|
return trimmed;
|
|
2105
2645
|
}
|
|
2646
|
+
function readWildcardPathParam(value) {
|
|
2647
|
+
if (typeof value === "string") return value;
|
|
2648
|
+
if (Array.isArray(value)) return value.join("/");
|
|
2649
|
+
return "";
|
|
2650
|
+
}
|
|
2106
2651
|
function createServer(options = {}) {
|
|
2107
2652
|
const app = express();
|
|
2108
2653
|
const bridge = createCodexBridgeMiddleware();
|
|
@@ -2118,7 +2663,7 @@ function createServer(options = {}) {
|
|
|
2118
2663
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2119
2664
|
return;
|
|
2120
2665
|
}
|
|
2121
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
2666
|
+
const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
|
|
2122
2667
|
if (!contentType) {
|
|
2123
2668
|
res.status(415).json({ error: "Unsupported image type." });
|
|
2124
2669
|
return;
|
|
@@ -2130,6 +2675,81 @@ function createServer(options = {}) {
|
|
|
2130
2675
|
if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
|
|
2131
2676
|
});
|
|
2132
2677
|
});
|
|
2678
|
+
app.get("/codex-local-file", (req, res) => {
|
|
2679
|
+
const rawPath = typeof req.query.path === "string" ? req.query.path : "";
|
|
2680
|
+
const localPath = normalizeLocalPath(rawPath);
|
|
2681
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
2682
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
2686
|
+
res.setHeader("Content-Disposition", "inline");
|
|
2687
|
+
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
2688
|
+
if (!error) return;
|
|
2689
|
+
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
2690
|
+
});
|
|
2691
|
+
});
|
|
2692
|
+
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
2693
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
2694
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
2695
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
2696
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
try {
|
|
2700
|
+
const fileStat = await stat3(localPath);
|
|
2701
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
2702
|
+
if (fileStat.isDirectory()) {
|
|
2703
|
+
const html = await createDirectoryListingHtml(localPath);
|
|
2704
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
2708
|
+
if (!error) return;
|
|
2709
|
+
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
2710
|
+
});
|
|
2711
|
+
} catch {
|
|
2712
|
+
res.status(404).json({ error: "File not found." });
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
app.get("/codex-local-edit/*path", async (req, res) => {
|
|
2716
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
2717
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
2718
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
2719
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
try {
|
|
2723
|
+
const fileStat = await stat3(localPath);
|
|
2724
|
+
if (!fileStat.isFile()) {
|
|
2725
|
+
res.status(400).json({ error: "Expected file path." });
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
const html = await createTextEditorHtml(localPath);
|
|
2729
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
2730
|
+
} catch {
|
|
2731
|
+
res.status(404).json({ error: "File not found." });
|
|
2732
|
+
}
|
|
2733
|
+
});
|
|
2734
|
+
app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
|
|
2735
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
2736
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
2737
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
2738
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (!await isTextEditableFile(localPath)) {
|
|
2742
|
+
res.status(415).json({ error: "Only text-like files are editable." });
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
const body = typeof req.body === "string" ? req.body : "";
|
|
2746
|
+
try {
|
|
2747
|
+
await writeFile2(localPath, body, "utf8");
|
|
2748
|
+
res.status(200).json({ ok: true });
|
|
2749
|
+
} catch {
|
|
2750
|
+
res.status(404).json({ error: "File not found." });
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2133
2753
|
const hasFrontendAssets = existsSync2(spaEntryFile);
|
|
2134
2754
|
if (hasFrontendAssets) {
|
|
2135
2755
|
app.use(express.static(distDir));
|
|
@@ -2201,11 +2821,11 @@ function generatePassword() {
|
|
|
2201
2821
|
|
|
2202
2822
|
// src/cli/index.ts
|
|
2203
2823
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
2204
|
-
var __dirname2 =
|
|
2824
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2205
2825
|
async function readCliVersion() {
|
|
2206
2826
|
try {
|
|
2207
|
-
const packageJsonPath =
|
|
2208
|
-
const raw = await
|
|
2827
|
+
const packageJsonPath = join4(__dirname2, "..", "package.json");
|
|
2828
|
+
const raw = await readFile3(packageJsonPath, "utf8");
|
|
2209
2829
|
const parsed = JSON.parse(raw);
|
|
2210
2830
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2211
2831
|
} catch {
|
|
@@ -2230,13 +2850,13 @@ function runWithStatus(command, args) {
|
|
|
2230
2850
|
return result.status ?? -1;
|
|
2231
2851
|
}
|
|
2232
2852
|
function getUserNpmPrefix() {
|
|
2233
|
-
return
|
|
2853
|
+
return join4(homedir2(), ".npm-global");
|
|
2234
2854
|
}
|
|
2235
2855
|
function resolveCodexCommand() {
|
|
2236
2856
|
if (canRun("codex", ["--version"])) {
|
|
2237
2857
|
return "codex";
|
|
2238
2858
|
}
|
|
2239
|
-
const userCandidate =
|
|
2859
|
+
const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
|
|
2240
2860
|
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2241
2861
|
return userCandidate;
|
|
2242
2862
|
}
|
|
@@ -2244,15 +2864,113 @@ function resolveCodexCommand() {
|
|
|
2244
2864
|
if (!prefix) {
|
|
2245
2865
|
return null;
|
|
2246
2866
|
}
|
|
2247
|
-
const candidate =
|
|
2867
|
+
const candidate = join4(prefix, "bin", "codex");
|
|
2248
2868
|
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2249
2869
|
return candidate;
|
|
2250
2870
|
}
|
|
2251
2871
|
return null;
|
|
2252
2872
|
}
|
|
2873
|
+
function resolveCloudflaredCommand() {
|
|
2874
|
+
if (canRun("cloudflared", ["--version"])) {
|
|
2875
|
+
return "cloudflared";
|
|
2876
|
+
}
|
|
2877
|
+
const localCandidate = join4(homedir2(), ".local", "bin", "cloudflared");
|
|
2878
|
+
if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2879
|
+
return localCandidate;
|
|
2880
|
+
}
|
|
2881
|
+
return null;
|
|
2882
|
+
}
|
|
2883
|
+
function mapCloudflaredLinuxArch(arch) {
|
|
2884
|
+
if (arch === "x64") {
|
|
2885
|
+
return "amd64";
|
|
2886
|
+
}
|
|
2887
|
+
if (arch === "arm64") {
|
|
2888
|
+
return "arm64";
|
|
2889
|
+
}
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2892
|
+
function downloadFile(url, destination) {
|
|
2893
|
+
return new Promise((resolve2, reject) => {
|
|
2894
|
+
const request = (currentUrl) => {
|
|
2895
|
+
httpsGet(currentUrl, (response) => {
|
|
2896
|
+
const code = response.statusCode ?? 0;
|
|
2897
|
+
if (code >= 300 && code < 400 && response.headers.location) {
|
|
2898
|
+
response.resume();
|
|
2899
|
+
request(response.headers.location);
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
if (code !== 200) {
|
|
2903
|
+
response.resume();
|
|
2904
|
+
reject(new Error(`Download failed with HTTP status ${String(code)}`));
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
const file = createWriteStream(destination, { mode: 493 });
|
|
2908
|
+
response.pipe(file);
|
|
2909
|
+
file.on("finish", () => {
|
|
2910
|
+
file.close();
|
|
2911
|
+
resolve2();
|
|
2912
|
+
});
|
|
2913
|
+
file.on("error", reject);
|
|
2914
|
+
}).on("error", reject);
|
|
2915
|
+
};
|
|
2916
|
+
request(url);
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
async function ensureCloudflaredInstalledLinux() {
|
|
2920
|
+
const current = resolveCloudflaredCommand();
|
|
2921
|
+
if (current) {
|
|
2922
|
+
return current;
|
|
2923
|
+
}
|
|
2924
|
+
if (process.platform !== "linux") {
|
|
2925
|
+
return null;
|
|
2926
|
+
}
|
|
2927
|
+
const mappedArch = mapCloudflaredLinuxArch(process.arch);
|
|
2928
|
+
if (!mappedArch) {
|
|
2929
|
+
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2930
|
+
}
|
|
2931
|
+
const userBinDir = join4(homedir2(), ".local", "bin");
|
|
2932
|
+
mkdirSync(userBinDir, { recursive: true });
|
|
2933
|
+
const destination = join4(userBinDir, "cloudflared");
|
|
2934
|
+
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2935
|
+
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
2936
|
+
await downloadFile(downloadUrl, destination);
|
|
2937
|
+
chmodSync(destination, 493);
|
|
2938
|
+
process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
|
|
2939
|
+
const installed = resolveCloudflaredCommand();
|
|
2940
|
+
if (!installed) {
|
|
2941
|
+
throw new Error("cloudflared download completed but executable is still not available");
|
|
2942
|
+
}
|
|
2943
|
+
console.log("\ncloudflared installed.\n");
|
|
2944
|
+
return installed;
|
|
2945
|
+
}
|
|
2946
|
+
async function shouldInstallCloudflaredInteractively() {
|
|
2947
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2948
|
+
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
2949
|
+
return false;
|
|
2950
|
+
}
|
|
2951
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
2952
|
+
try {
|
|
2953
|
+
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
2954
|
+
const normalized = answer.trim().toLowerCase();
|
|
2955
|
+
return normalized === "y" || normalized === "yes";
|
|
2956
|
+
} finally {
|
|
2957
|
+
prompt.close();
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
async function resolveCloudflaredForTunnel() {
|
|
2961
|
+
const current = resolveCloudflaredCommand();
|
|
2962
|
+
if (current) {
|
|
2963
|
+
return current;
|
|
2964
|
+
}
|
|
2965
|
+
const installApproved = await shouldInstallCloudflaredInteractively();
|
|
2966
|
+
if (!installApproved) {
|
|
2967
|
+
return null;
|
|
2968
|
+
}
|
|
2969
|
+
return ensureCloudflaredInstalledLinux();
|
|
2970
|
+
}
|
|
2253
2971
|
function hasCodexAuth() {
|
|
2254
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
2255
|
-
return existsSync3(
|
|
2972
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
|
|
2973
|
+
return existsSync3(join4(codexHome, "auth.json"));
|
|
2256
2974
|
}
|
|
2257
2975
|
function ensureCodexInstalled() {
|
|
2258
2976
|
let codexCommand = resolveCodexCommand();
|
|
@@ -2270,7 +2988,7 @@ function ensureCodexInstalled() {
|
|
|
2270
2988
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
2271
2989
|
`);
|
|
2272
2990
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
2273
|
-
process.env.PATH = `${
|
|
2991
|
+
process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
2274
2992
|
};
|
|
2275
2993
|
if (isTermuxRuntime()) {
|
|
2276
2994
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -2331,9 +3049,27 @@ function parseCloudflaredUrl(chunk) {
|
|
|
2331
3049
|
}
|
|
2332
3050
|
return urlMatch[urlMatch.length - 1] ?? null;
|
|
2333
3051
|
}
|
|
2334
|
-
|
|
3052
|
+
function getAccessibleUrls(port) {
|
|
3053
|
+
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
3054
|
+
const interfaces = networkInterfaces();
|
|
3055
|
+
for (const entries of Object.values(interfaces)) {
|
|
3056
|
+
if (!entries) {
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
for (const entry of entries) {
|
|
3060
|
+
if (entry.internal) {
|
|
3061
|
+
continue;
|
|
3062
|
+
}
|
|
3063
|
+
if (entry.family === "IPv4") {
|
|
3064
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
return Array.from(urls);
|
|
3069
|
+
}
|
|
3070
|
+
async function startCloudflaredTunnel(command, localPort) {
|
|
2335
3071
|
return new Promise((resolve2, reject) => {
|
|
2336
|
-
const child = spawn2(
|
|
3072
|
+
const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
2337
3073
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2338
3074
|
});
|
|
2339
3075
|
const timeout = setTimeout(() => {
|
|
@@ -2384,7 +3120,7 @@ function listenWithFallback(server, startPort) {
|
|
|
2384
3120
|
};
|
|
2385
3121
|
server.once("error", onError);
|
|
2386
3122
|
server.once("listening", onListening);
|
|
2387
|
-
server.listen(port);
|
|
3123
|
+
server.listen(port, "0.0.0.0");
|
|
2388
3124
|
};
|
|
2389
3125
|
attempt(startPort);
|
|
2390
3126
|
});
|
|
@@ -2406,7 +3142,11 @@ async function startServer(options) {
|
|
|
2406
3142
|
let tunnelUrl = null;
|
|
2407
3143
|
if (options.tunnel) {
|
|
2408
3144
|
try {
|
|
2409
|
-
const
|
|
3145
|
+
const cloudflaredCommand = await resolveCloudflaredForTunnel();
|
|
3146
|
+
if (!cloudflaredCommand) {
|
|
3147
|
+
throw new Error("cloudflared is not installed");
|
|
3148
|
+
}
|
|
3149
|
+
const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
|
|
2410
3150
|
tunnelChild = tunnel.process;
|
|
2411
3151
|
tunnelUrl = tunnel.url;
|
|
2412
3152
|
} catch (error) {
|
|
@@ -2421,8 +3161,15 @@ async function startServer(options) {
|
|
|
2421
3161
|
` Version: ${version}`,
|
|
2422
3162
|
" GitHub: https://github.com/friuns2/codexui",
|
|
2423
3163
|
"",
|
|
2424
|
-
`
|
|
3164
|
+
` Bind: http://0.0.0.0:${String(port)}`
|
|
2425
3165
|
];
|
|
3166
|
+
const accessUrls = getAccessibleUrls(port);
|
|
3167
|
+
if (accessUrls.length > 0) {
|
|
3168
|
+
lines.push(` Local: ${accessUrls[0]}`);
|
|
3169
|
+
for (const accessUrl of accessUrls.slice(1)) {
|
|
3170
|
+
lines.push(` Network: ${accessUrl}`);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
2426
3173
|
if (port !== requestedPort) {
|
|
2427
3174
|
lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
|
|
2428
3175
|
}
|
|
@@ -2431,9 +3178,7 @@ async function startServer(options) {
|
|
|
2431
3178
|
}
|
|
2432
3179
|
if (tunnelUrl) {
|
|
2433
3180
|
lines.push(` Tunnel: ${tunnelUrl}`);
|
|
2434
|
-
lines.push("");
|
|
2435
|
-
lines.push(" Tunnel QR code:");
|
|
2436
|
-
lines.push(` URL: ${tunnelUrl}`);
|
|
3181
|
+
lines.push(" Tunnel QR code below");
|
|
2437
3182
|
}
|
|
2438
3183
|
printTermuxKeepAlive(lines);
|
|
2439
3184
|
lines.push("");
|