codexapp 0.1.28 → 0.1.29
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-cSjdFqX4.js +48 -0
- package/dist/assets/index-sxLu4gFb.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +336 -60
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/index-Bhfj4wvl.js +0 -48
- package/dist/assets/index-CBIbfWDP.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -3,21 +3,22 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
5
|
import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "fs";
|
|
6
|
-
import { readFile as
|
|
6
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
7
7
|
import { homedir as homedir2, networkInterfaces } from "os";
|
|
8
|
-
import { join as
|
|
8
|
+
import { join as join4 } from "path";
|
|
9
9
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
10
|
+
import { createInterface } from "readline/promises";
|
|
10
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
11
|
-
import { dirname as
|
|
12
|
+
import { dirname as dirname3 } from "path";
|
|
12
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 } from "fs";
|
|
20
|
-
import {
|
|
21
|
+
import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
|
|
21
22
|
import express from "express";
|
|
22
23
|
|
|
23
24
|
// src/server/codexAppServerBridge.ts
|
|
@@ -1578,32 +1579,77 @@ function createAuthSession(password) {
|
|
|
1578
1579
|
};
|
|
1579
1580
|
}
|
|
1580
1581
|
|
|
1581
|
-
// src/server/
|
|
1582
|
-
import {
|
|
1583
|
-
|
|
1584
|
-
var
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
".
|
|
1588
|
-
".
|
|
1589
|
-
".
|
|
1590
|
-
".
|
|
1591
|
-
".
|
|
1592
|
-
".
|
|
1593
|
-
".
|
|
1594
|
-
".
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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";
|
|
1605
1652
|
}
|
|
1606
|
-
return trimmed;
|
|
1607
1653
|
}
|
|
1608
1654
|
function normalizeLocalPath(rawPath) {
|
|
1609
1655
|
const trimmed = rawPath.trim();
|
|
@@ -1625,39 +1671,102 @@ function decodeBrowsePath(rawPath) {
|
|
|
1625
1671
|
return rawPath;
|
|
1626
1672
|
}
|
|
1627
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
|
+
}
|
|
1628
1706
|
function escapeHtml(value) {
|
|
1629
1707
|
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
1630
1708
|
}
|
|
1631
1709
|
function toBrowseHref(pathValue) {
|
|
1632
1710
|
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
1633
1711
|
}
|
|
1634
|
-
|
|
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) {
|
|
1635
1719
|
const entries = await readdir2(localPath, { withFileTypes: true });
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
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;
|
|
1639
1736
|
return a.name.localeCompare(b.name);
|
|
1640
1737
|
});
|
|
1738
|
+
}
|
|
1739
|
+
async function createDirectoryListingHtml(localPath) {
|
|
1740
|
+
const items = await getDirectoryItems(localPath);
|
|
1641
1741
|
const parentPath = dirname(localPath);
|
|
1642
|
-
const rows =
|
|
1643
|
-
const
|
|
1644
|
-
const
|
|
1645
|
-
return `<li><a href="${escapeHtml(toBrowseHref(
|
|
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>`;
|
|
1646
1746
|
}).join("\n");
|
|
1647
1747
|
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
1648
|
-
|
|
1748
|
+
return `<!doctype html>
|
|
1649
1749
|
<html lang="en">
|
|
1650
1750
|
<head>
|
|
1651
1751
|
<meta charset="utf-8" />
|
|
1652
1752
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1653
1753
|
<title>Index of ${escapeHtml(localPath)}</title>
|
|
1654
1754
|
<style>
|
|
1655
|
-
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin:
|
|
1755
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
|
|
1656
1756
|
a { color: #8cc2ff; text-decoration: none; }
|
|
1657
1757
|
a:hover { text-decoration: underline; }
|
|
1658
|
-
ul { list-style: none; padding: 0; margin: 12px 0 0; }
|
|
1659
|
-
|
|
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; }
|
|
1660
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
|
+
}
|
|
1661
1770
|
</style>
|
|
1662
1771
|
</head>
|
|
1663
1772
|
<body>
|
|
@@ -1666,7 +1775,107 @@ async function renderDirectoryListing(res, localPath) {
|
|
|
1666
1775
|
<ul>${rows}</ul>
|
|
1667
1776
|
</body>
|
|
1668
1777
|
</html>`;
|
|
1669
|
-
|
|
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
|
+
|
|
1848
|
+
// src/server/httpServer.ts
|
|
1849
|
+
import { WebSocketServer } from "ws";
|
|
1850
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1851
|
+
var distDir = join3(__dirname, "..", "dist");
|
|
1852
|
+
var spaEntryFile = join3(distDir, "index.html");
|
|
1853
|
+
var IMAGE_CONTENT_TYPES = {
|
|
1854
|
+
".avif": "image/avif",
|
|
1855
|
+
".bmp": "image/bmp",
|
|
1856
|
+
".gif": "image/gif",
|
|
1857
|
+
".jpeg": "image/jpeg",
|
|
1858
|
+
".jpg": "image/jpeg",
|
|
1859
|
+
".png": "image/png",
|
|
1860
|
+
".svg": "image/svg+xml",
|
|
1861
|
+
".webp": "image/webp"
|
|
1862
|
+
};
|
|
1863
|
+
function normalizeLocalImagePath(rawPath) {
|
|
1864
|
+
const trimmed = rawPath.trim();
|
|
1865
|
+
if (!trimmed) return "";
|
|
1866
|
+
if (trimmed.startsWith("file://")) {
|
|
1867
|
+
try {
|
|
1868
|
+
return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
|
|
1869
|
+
} catch {
|
|
1870
|
+
return trimmed.replace(/^file:\/\//u, "");
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return trimmed;
|
|
1874
|
+
}
|
|
1875
|
+
function readWildcardPathParam(value) {
|
|
1876
|
+
if (typeof value === "string") return value;
|
|
1877
|
+
if (Array.isArray(value)) return value.join("/");
|
|
1878
|
+
return "";
|
|
1670
1879
|
}
|
|
1671
1880
|
function createServer(options = {}) {
|
|
1672
1881
|
const app = express();
|
|
@@ -1683,7 +1892,7 @@ function createServer(options = {}) {
|
|
|
1683
1892
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1684
1893
|
return;
|
|
1685
1894
|
}
|
|
1686
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
1895
|
+
const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
|
|
1687
1896
|
if (!contentType) {
|
|
1688
1897
|
res.status(415).json({ error: "Unsupported image type." });
|
|
1689
1898
|
return;
|
|
@@ -1710,17 +1919,18 @@ function createServer(options = {}) {
|
|
|
1710
1919
|
});
|
|
1711
1920
|
});
|
|
1712
1921
|
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
1713
|
-
const rawPath =
|
|
1922
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
1714
1923
|
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
1715
1924
|
if (!localPath || !isAbsolute2(localPath)) {
|
|
1716
1925
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
1717
1926
|
return;
|
|
1718
1927
|
}
|
|
1719
1928
|
try {
|
|
1720
|
-
const fileStat = await
|
|
1929
|
+
const fileStat = await stat3(localPath);
|
|
1721
1930
|
res.setHeader("Cache-Control", "private, no-store");
|
|
1722
1931
|
if (fileStat.isDirectory()) {
|
|
1723
|
-
await
|
|
1932
|
+
const html = await createDirectoryListingHtml(localPath);
|
|
1933
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
1724
1934
|
return;
|
|
1725
1935
|
}
|
|
1726
1936
|
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
@@ -1731,6 +1941,44 @@ function createServer(options = {}) {
|
|
|
1731
1941
|
res.status(404).json({ error: "File not found." });
|
|
1732
1942
|
}
|
|
1733
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
|
+
});
|
|
1734
1982
|
const hasFrontendAssets = existsSync(spaEntryFile);
|
|
1735
1983
|
if (hasFrontendAssets) {
|
|
1736
1984
|
app.use(express.static(distDir));
|
|
@@ -1802,11 +2050,11 @@ function generatePassword() {
|
|
|
1802
2050
|
|
|
1803
2051
|
// src/cli/index.ts
|
|
1804
2052
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
1805
|
-
var __dirname2 =
|
|
2053
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
1806
2054
|
async function readCliVersion() {
|
|
1807
2055
|
try {
|
|
1808
|
-
const packageJsonPath =
|
|
1809
|
-
const raw = await
|
|
2056
|
+
const packageJsonPath = join4(__dirname2, "..", "package.json");
|
|
2057
|
+
const raw = await readFile3(packageJsonPath, "utf8");
|
|
1810
2058
|
const parsed = JSON.parse(raw);
|
|
1811
2059
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
1812
2060
|
} catch {
|
|
@@ -1831,13 +2079,13 @@ function runWithStatus(command, args) {
|
|
|
1831
2079
|
return result.status ?? -1;
|
|
1832
2080
|
}
|
|
1833
2081
|
function getUserNpmPrefix() {
|
|
1834
|
-
return
|
|
2082
|
+
return join4(homedir2(), ".npm-global");
|
|
1835
2083
|
}
|
|
1836
2084
|
function resolveCodexCommand() {
|
|
1837
2085
|
if (canRun("codex", ["--version"])) {
|
|
1838
2086
|
return "codex";
|
|
1839
2087
|
}
|
|
1840
|
-
const userCandidate =
|
|
2088
|
+
const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
|
|
1841
2089
|
if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
1842
2090
|
return userCandidate;
|
|
1843
2091
|
}
|
|
@@ -1845,7 +2093,7 @@ function resolveCodexCommand() {
|
|
|
1845
2093
|
if (!prefix) {
|
|
1846
2094
|
return null;
|
|
1847
2095
|
}
|
|
1848
|
-
const candidate =
|
|
2096
|
+
const candidate = join4(prefix, "bin", "codex");
|
|
1849
2097
|
if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
|
|
1850
2098
|
return candidate;
|
|
1851
2099
|
}
|
|
@@ -1855,7 +2103,7 @@ function resolveCloudflaredCommand() {
|
|
|
1855
2103
|
if (canRun("cloudflared", ["--version"])) {
|
|
1856
2104
|
return "cloudflared";
|
|
1857
2105
|
}
|
|
1858
|
-
const localCandidate =
|
|
2106
|
+
const localCandidate = join4(homedir2(), ".local", "bin", "cloudflared");
|
|
1859
2107
|
if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
1860
2108
|
return localCandidate;
|
|
1861
2109
|
}
|
|
@@ -1909,9 +2157,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
1909
2157
|
if (!mappedArch) {
|
|
1910
2158
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
1911
2159
|
}
|
|
1912
|
-
const userBinDir =
|
|
2160
|
+
const userBinDir = join4(homedir2(), ".local", "bin");
|
|
1913
2161
|
mkdirSync(userBinDir, { recursive: true });
|
|
1914
|
-
const destination =
|
|
2162
|
+
const destination = join4(userBinDir, "cloudflared");
|
|
1915
2163
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
1916
2164
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
1917
2165
|
await downloadFile(downloadUrl, destination);
|
|
@@ -1924,9 +2172,34 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
1924
2172
|
console.log("\ncloudflared installed.\n");
|
|
1925
2173
|
return installed;
|
|
1926
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
|
+
}
|
|
1927
2200
|
function hasCodexAuth() {
|
|
1928
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
1929
|
-
return existsSync2(
|
|
2201
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
|
|
2202
|
+
return existsSync2(join4(codexHome, "auth.json"));
|
|
1930
2203
|
}
|
|
1931
2204
|
function ensureCodexInstalled() {
|
|
1932
2205
|
let codexCommand = resolveCodexCommand();
|
|
@@ -1944,7 +2217,7 @@ function ensureCodexInstalled() {
|
|
|
1944
2217
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
1945
2218
|
`);
|
|
1946
2219
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
1947
|
-
process.env.PATH = `${
|
|
2220
|
+
process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
1948
2221
|
};
|
|
1949
2222
|
if (isTermuxRuntime()) {
|
|
1950
2223
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -2098,7 +2371,10 @@ async function startServer(options) {
|
|
|
2098
2371
|
let tunnelUrl = null;
|
|
2099
2372
|
if (options.tunnel) {
|
|
2100
2373
|
try {
|
|
2101
|
-
const cloudflaredCommand = await
|
|
2374
|
+
const cloudflaredCommand = await resolveCloudflaredForTunnel();
|
|
2375
|
+
if (!cloudflaredCommand) {
|
|
2376
|
+
throw new Error("cloudflared is not installed");
|
|
2377
|
+
}
|
|
2102
2378
|
const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
|
|
2103
2379
|
tunnelChild = tunnel.process;
|
|
2104
2380
|
tunnelUrl = tunnel.url;
|