codexapp 0.1.27 → 0.1.28

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-cli/index.js CHANGED
@@ -2,21 +2,22 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { existsSync as existsSync2 } from "fs";
6
- import { readFile as readFile3 } from "fs/promises";
7
- import { homedir as homedir2 } from "os";
8
- import { join as join4 } from "path";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "fs";
6
+ import { readFile as readFile2 } from "fs/promises";
7
+ import { homedir as homedir2, networkInterfaces } from "os";
8
+ import { join as join3 } from "path";
9
9
  import { spawn as spawn2, spawnSync } from "child_process";
10
10
  import { fileURLToPath as fileURLToPath2 } from "url";
11
- import { dirname as dirname3 } from "path";
11
+ import { dirname as dirname2 } from "path";
12
+ import { get as httpsGet } from "https";
12
13
  import { Command } from "commander";
13
14
  import qrcode from "qrcode-terminal";
14
15
 
15
16
  // src/server/httpServer.ts
16
17
  import { fileURLToPath } from "url";
17
- import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join3 } from "path";
18
+ import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
18
19
  import { existsSync } from "fs";
19
- import { writeFile as writeFile2, stat as stat3 } from "fs/promises";
20
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
20
21
  import express from "express";
21
22
 
22
23
  // src/server/codexAppServerBridge.ts
@@ -1577,74 +1578,32 @@ function createAuthSession(password) {
1577
1578
  };
1578
1579
  }
1579
1580
 
1580
- // src/server/localBrowseUi.ts
1581
- import { dirname, extname, join as join2 } from "path";
1582
- import { readFile as readFile2, readdir as readdir2, stat as stat2 } from "fs/promises";
1583
- var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1584
- ".txt",
1585
- ".md",
1586
- ".json",
1587
- ".js",
1588
- ".ts",
1589
- ".tsx",
1590
- ".jsx",
1591
- ".css",
1592
- ".scss",
1593
- ".html",
1594
- ".htm",
1595
- ".xml",
1596
- ".yml",
1597
- ".yaml",
1598
- ".log",
1599
- ".csv",
1600
- ".env",
1601
- ".py",
1602
- ".sh",
1603
- ".toml",
1604
- ".ini",
1605
- ".conf",
1606
- ".sql"
1607
- ]);
1608
- function languageForPath(pathValue) {
1609
- const extension = extname(pathValue).toLowerCase();
1610
- switch (extension) {
1611
- case ".js":
1612
- return "javascript";
1613
- case ".ts":
1614
- return "typescript";
1615
- case ".jsx":
1616
- return "javascript";
1617
- case ".tsx":
1618
- return "typescript";
1619
- case ".py":
1620
- return "python";
1621
- case ".sh":
1622
- return "sh";
1623
- case ".css":
1624
- case ".scss":
1625
- return "css";
1626
- case ".html":
1627
- case ".htm":
1628
- return "html";
1629
- case ".json":
1630
- return "json";
1631
- case ".md":
1632
- return "markdown";
1633
- case ".yaml":
1634
- case ".yml":
1635
- return "yaml";
1636
- case ".xml":
1637
- return "xml";
1638
- case ".sql":
1639
- return "sql";
1640
- case ".toml":
1641
- return "ini";
1642
- case ".ini":
1643
- case ".conf":
1644
- return "ini";
1645
- default:
1646
- return "plaintext";
1581
+ // src/server/httpServer.ts
1582
+ import { WebSocketServer } from "ws";
1583
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1584
+ var distDir = join2(__dirname, "..", "dist");
1585
+ var spaEntryFile = join2(distDir, "index.html");
1586
+ var IMAGE_CONTENT_TYPES = {
1587
+ ".avif": "image/avif",
1588
+ ".bmp": "image/bmp",
1589
+ ".gif": "image/gif",
1590
+ ".jpeg": "image/jpeg",
1591
+ ".jpg": "image/jpeg",
1592
+ ".png": "image/png",
1593
+ ".svg": "image/svg+xml",
1594
+ ".webp": "image/webp"
1595
+ };
1596
+ function normalizeLocalImagePath(rawPath) {
1597
+ const trimmed = rawPath.trim();
1598
+ if (!trimmed) return "";
1599
+ if (trimmed.startsWith("file://")) {
1600
+ try {
1601
+ return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
1602
+ } catch {
1603
+ return trimmed.replace(/^file:\/\//u, "");
1604
+ }
1647
1605
  }
1606
+ return trimmed;
1648
1607
  }
1649
1608
  function normalizeLocalPath(rawPath) {
1650
1609
  const trimmed = rawPath.trim();
@@ -1666,72 +1625,39 @@ function decodeBrowsePath(rawPath) {
1666
1625
  return rawPath;
1667
1626
  }
1668
1627
  }
1669
- function isTextEditablePath(pathValue) {
1670
- return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
1671
- }
1672
1628
  function escapeHtml(value) {
1673
1629
  return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
1674
1630
  }
1675
1631
  function toBrowseHref(pathValue) {
1676
1632
  return `/codex-local-browse${encodeURI(pathValue)}`;
1677
1633
  }
1678
- function toEditHref(pathValue) {
1679
- return `/codex-local-edit${encodeURI(pathValue)}`;
1680
- }
1681
- function escapeForInlineScriptString(value) {
1682
- return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
1683
- }
1684
- async function getDirectoryItems(localPath) {
1634
+ async function renderDirectoryListing(res, localPath) {
1685
1635
  const entries = await readdir2(localPath, { withFileTypes: true });
1686
- const withMeta = await Promise.all(entries.map(async (entry) => {
1687
- const entryPath = join2(localPath, entry.name);
1688
- const entryStat = await stat2(entryPath);
1689
- return {
1690
- name: entry.name,
1691
- path: entryPath,
1692
- isDirectory: entry.isDirectory(),
1693
- editable: !entry.isDirectory() && isTextEditablePath(entryPath),
1694
- mtimeMs: entryStat.mtimeMs
1695
- };
1696
- }));
1697
- return withMeta.sort((a, b) => {
1698
- if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
1699
- if (a.isDirectory && !b.isDirectory) return -1;
1700
- if (!a.isDirectory && b.isDirectory) return 1;
1636
+ const sorted = entries.slice().sort((a, b) => {
1637
+ if (a.isDirectory() && !b.isDirectory()) return -1;
1638
+ if (!a.isDirectory() && b.isDirectory()) return 1;
1701
1639
  return a.name.localeCompare(b.name);
1702
1640
  });
1703
- }
1704
- async function createDirectoryListingHtml(localPath) {
1705
- const items = await getDirectoryItems(localPath);
1706
1641
  const parentPath = dirname(localPath);
1707
- const rows = items.map((item) => {
1708
- const suffix = item.isDirectory ? "/" : "";
1709
- const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
1710
- return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
1642
+ const rows = sorted.map((entry) => {
1643
+ const entryPath = join2(localPath, entry.name);
1644
+ const suffix = entry.isDirectory() ? "/" : "";
1645
+ return `<li><a href="${escapeHtml(toBrowseHref(entryPath))}">${escapeHtml(entry.name)}${suffix}</a></li>`;
1711
1646
  }).join("\n");
1712
1647
  const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
1713
- return `<!doctype html>
1648
+ const html = `<!doctype html>
1714
1649
  <html lang="en">
1715
1650
  <head>
1716
1651
  <meta charset="utf-8" />
1717
1652
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1718
1653
  <title>Index of ${escapeHtml(localPath)}</title>
1719
1654
  <style>
1720
- body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
1655
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 24px; background: #0b1020; color: #dbe6ff; }
1721
1656
  a { color: #8cc2ff; text-decoration: none; }
1722
1657
  a:hover { text-decoration: underline; }
1723
- ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
1724
- .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
1725
- .file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
1726
- .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; }
1727
- .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
1658
+ ul { list-style: none; padding: 0; margin: 12px 0 0; }
1659
+ li { padding: 3px 0; }
1728
1660
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
1729
- @media (max-width: 640px) {
1730
- body { margin: 12px; }
1731
- .file-row { gap: 8px; }
1732
- .file-link { font-size: 15px; padding: 12px; }
1733
- .icon-btn { width: 44px; height: 44px; }
1734
- }
1735
1661
  </style>
1736
1662
  </head>
1737
1663
  <body>
@@ -1740,107 +1666,7 @@ async function createDirectoryListingHtml(localPath) {
1740
1666
  <ul>${rows}</ul>
1741
1667
  </body>
1742
1668
  </html>`;
1743
- }
1744
- async function createTextEditorHtml(localPath) {
1745
- const content = await readFile2(localPath, "utf8");
1746
- const parentPath = dirname(localPath);
1747
- const language = languageForPath(localPath);
1748
- const safeContentLiteral = escapeForInlineScriptString(content);
1749
- return `<!doctype html>
1750
- <html lang="en">
1751
- <head>
1752
- <meta charset="utf-8" />
1753
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1754
- <title>Edit ${escapeHtml(localPath)}</title>
1755
- <style>
1756
- html, body { width: 100%; height: 100%; margin: 0; }
1757
- body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
1758
- .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; }
1759
- .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
1760
- button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
1761
- button:hover, a:hover { filter: brightness(1.08); }
1762
- #editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
1763
- #status { margin-left: 8px; color: #8cc2ff; }
1764
- .ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
1765
- .ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
1766
- .ace_marker-layer .ace_active-line { background: #10213c !important; }
1767
- .ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
1768
- .meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
1769
- </style>
1770
- </head>
1771
- <body>
1772
- <div class="toolbar">
1773
- <div class="row">
1774
- <a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
1775
- <button id="saveBtn" type="button">Save</button>
1776
- <span id="status"></span>
1777
- </div>
1778
- <div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
1779
- </div>
1780
- <div id="editor"></div>
1781
- <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
1782
- <script>
1783
- const saveBtn = document.getElementById('saveBtn');
1784
- const status = document.getElementById('status');
1785
- const editor = ace.edit('editor');
1786
- editor.setTheme('ace/theme/tomorrow_night');
1787
- editor.session.setMode('ace/mode/${escapeHtml(language)}');
1788
- editor.setValue(${safeContentLiteral}, -1);
1789
- editor.setOptions({
1790
- fontSize: '13px',
1791
- wrap: true,
1792
- showPrintMargin: false,
1793
- useSoftTabs: true,
1794
- tabSize: 2,
1795
- behavioursEnabled: true,
1796
- });
1797
- editor.resize();
1798
-
1799
- saveBtn.addEventListener('click', async () => {
1800
- status.textContent = 'Saving...';
1801
- const response = await fetch(location.pathname, {
1802
- method: 'PUT',
1803
- headers: { 'Content-Type': 'text/plain; charset=utf-8' },
1804
- body: editor.getValue(),
1805
- });
1806
- status.textContent = response.ok ? 'Saved' : 'Save failed';
1807
- });
1808
- </script>
1809
- </body>
1810
- </html>`;
1811
- }
1812
-
1813
- // src/server/httpServer.ts
1814
- import { WebSocketServer } from "ws";
1815
- var __dirname = dirname2(fileURLToPath(import.meta.url));
1816
- var distDir = join3(__dirname, "..", "dist");
1817
- var spaEntryFile = join3(distDir, "index.html");
1818
- var IMAGE_CONTENT_TYPES = {
1819
- ".avif": "image/avif",
1820
- ".bmp": "image/bmp",
1821
- ".gif": "image/gif",
1822
- ".jpeg": "image/jpeg",
1823
- ".jpg": "image/jpeg",
1824
- ".png": "image/png",
1825
- ".svg": "image/svg+xml",
1826
- ".webp": "image/webp"
1827
- };
1828
- function normalizeLocalImagePath(rawPath) {
1829
- const trimmed = rawPath.trim();
1830
- if (!trimmed) return "";
1831
- if (trimmed.startsWith("file://")) {
1832
- try {
1833
- return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
1834
- } catch {
1835
- return trimmed.replace(/^file:\/\//u, "");
1836
- }
1837
- }
1838
- return trimmed;
1839
- }
1840
- function readWildcardPathParam(value) {
1841
- if (typeof value === "string") return value;
1842
- if (Array.isArray(value)) return value.join("/");
1843
- return "";
1669
+ res.status(200).type("text/html; charset=utf-8").send(html);
1844
1670
  }
1845
1671
  function createServer(options = {}) {
1846
1672
  const app = express();
@@ -1857,7 +1683,7 @@ function createServer(options = {}) {
1857
1683
  res.status(400).json({ error: "Expected absolute local file path." });
1858
1684
  return;
1859
1685
  }
1860
- const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
1686
+ const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
1861
1687
  if (!contentType) {
1862
1688
  res.status(415).json({ error: "Unsupported image type." });
1863
1689
  return;
@@ -1884,18 +1710,17 @@ function createServer(options = {}) {
1884
1710
  });
1885
1711
  });
1886
1712
  app.get("/codex-local-browse/*path", async (req, res) => {
1887
- const rawPath = readWildcardPathParam(req.params.path);
1713
+ const rawPath = typeof req.params.path === "string" ? req.params.path : "";
1888
1714
  const localPath = decodeBrowsePath(`/${rawPath}`);
1889
1715
  if (!localPath || !isAbsolute2(localPath)) {
1890
1716
  res.status(400).json({ error: "Expected absolute local file path." });
1891
1717
  return;
1892
1718
  }
1893
1719
  try {
1894
- const fileStat = await stat3(localPath);
1720
+ const fileStat = await stat2(localPath);
1895
1721
  res.setHeader("Cache-Control", "private, no-store");
1896
1722
  if (fileStat.isDirectory()) {
1897
- const html = await createDirectoryListingHtml(localPath);
1898
- res.status(200).type("text/html; charset=utf-8").send(html);
1723
+ await renderDirectoryListing(res, localPath);
1899
1724
  return;
1900
1725
  }
1901
1726
  res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
@@ -1906,44 +1731,6 @@ function createServer(options = {}) {
1906
1731
  res.status(404).json({ error: "File not found." });
1907
1732
  }
1908
1733
  });
1909
- app.get("/codex-local-edit/*path", async (req, res) => {
1910
- const rawPath = readWildcardPathParam(req.params.path);
1911
- const localPath = decodeBrowsePath(`/${rawPath}`);
1912
- if (!localPath || !isAbsolute2(localPath)) {
1913
- res.status(400).json({ error: "Expected absolute local file path." });
1914
- return;
1915
- }
1916
- try {
1917
- const fileStat = await stat3(localPath);
1918
- if (!fileStat.isFile()) {
1919
- res.status(400).json({ error: "Expected file path." });
1920
- return;
1921
- }
1922
- const html = await createTextEditorHtml(localPath);
1923
- res.status(200).type("text/html; charset=utf-8").send(html);
1924
- } catch {
1925
- res.status(404).json({ error: "File not found." });
1926
- }
1927
- });
1928
- app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
1929
- const rawPath = readWildcardPathParam(req.params.path);
1930
- const localPath = decodeBrowsePath(`/${rawPath}`);
1931
- if (!localPath || !isAbsolute2(localPath)) {
1932
- res.status(400).json({ error: "Expected absolute local file path." });
1933
- return;
1934
- }
1935
- if (!isTextEditablePath(localPath)) {
1936
- res.status(415).json({ error: "Only text-like files are editable." });
1937
- return;
1938
- }
1939
- const body = typeof req.body === "string" ? req.body : "";
1940
- try {
1941
- await writeFile2(localPath, body, "utf8");
1942
- res.status(200).json({ ok: true });
1943
- } catch {
1944
- res.status(404).json({ error: "File not found." });
1945
- }
1946
- });
1947
1734
  const hasFrontendAssets = existsSync(spaEntryFile);
1948
1735
  if (hasFrontendAssets) {
1949
1736
  app.use(express.static(distDir));
@@ -2015,11 +1802,11 @@ function generatePassword() {
2015
1802
 
2016
1803
  // src/cli/index.ts
2017
1804
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
2018
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
1805
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2019
1806
  async function readCliVersion() {
2020
1807
  try {
2021
- const packageJsonPath = join4(__dirname2, "..", "package.json");
2022
- const raw = await readFile3(packageJsonPath, "utf8");
1808
+ const packageJsonPath = join3(__dirname2, "..", "package.json");
1809
+ const raw = await readFile2(packageJsonPath, "utf8");
2023
1810
  const parsed = JSON.parse(raw);
2024
1811
  return typeof parsed.version === "string" ? parsed.version : "unknown";
2025
1812
  } catch {
@@ -2044,13 +1831,13 @@ function runWithStatus(command, args) {
2044
1831
  return result.status ?? -1;
2045
1832
  }
2046
1833
  function getUserNpmPrefix() {
2047
- return join4(homedir2(), ".npm-global");
1834
+ return join3(homedir2(), ".npm-global");
2048
1835
  }
2049
1836
  function resolveCodexCommand() {
2050
1837
  if (canRun("codex", ["--version"])) {
2051
1838
  return "codex";
2052
1839
  }
2053
- const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
1840
+ const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
2054
1841
  if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
2055
1842
  return userCandidate;
2056
1843
  }
@@ -2058,15 +1845,88 @@ function resolveCodexCommand() {
2058
1845
  if (!prefix) {
2059
1846
  return null;
2060
1847
  }
2061
- const candidate = join4(prefix, "bin", "codex");
1848
+ const candidate = join3(prefix, "bin", "codex");
2062
1849
  if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
2063
1850
  return candidate;
2064
1851
  }
2065
1852
  return null;
2066
1853
  }
1854
+ function resolveCloudflaredCommand() {
1855
+ if (canRun("cloudflared", ["--version"])) {
1856
+ return "cloudflared";
1857
+ }
1858
+ const localCandidate = join3(homedir2(), ".local", "bin", "cloudflared");
1859
+ if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
1860
+ return localCandidate;
1861
+ }
1862
+ return null;
1863
+ }
1864
+ function mapCloudflaredLinuxArch(arch) {
1865
+ if (arch === "x64") {
1866
+ return "amd64";
1867
+ }
1868
+ if (arch === "arm64") {
1869
+ return "arm64";
1870
+ }
1871
+ return null;
1872
+ }
1873
+ function downloadFile(url, destination) {
1874
+ return new Promise((resolve2, reject) => {
1875
+ const request = (currentUrl) => {
1876
+ httpsGet(currentUrl, (response) => {
1877
+ const code = response.statusCode ?? 0;
1878
+ if (code >= 300 && code < 400 && response.headers.location) {
1879
+ response.resume();
1880
+ request(response.headers.location);
1881
+ return;
1882
+ }
1883
+ if (code !== 200) {
1884
+ response.resume();
1885
+ reject(new Error(`Download failed with HTTP status ${String(code)}`));
1886
+ return;
1887
+ }
1888
+ const file = createWriteStream(destination, { mode: 493 });
1889
+ response.pipe(file);
1890
+ file.on("finish", () => {
1891
+ file.close();
1892
+ resolve2();
1893
+ });
1894
+ file.on("error", reject);
1895
+ }).on("error", reject);
1896
+ };
1897
+ request(url);
1898
+ });
1899
+ }
1900
+ async function ensureCloudflaredInstalledLinux() {
1901
+ const current = resolveCloudflaredCommand();
1902
+ if (current) {
1903
+ return current;
1904
+ }
1905
+ if (process.platform !== "linux") {
1906
+ return null;
1907
+ }
1908
+ const mappedArch = mapCloudflaredLinuxArch(process.arch);
1909
+ if (!mappedArch) {
1910
+ throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
1911
+ }
1912
+ const userBinDir = join3(homedir2(), ".local", "bin");
1913
+ mkdirSync(userBinDir, { recursive: true });
1914
+ const destination = join3(userBinDir, "cloudflared");
1915
+ const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
1916
+ console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
1917
+ await downloadFile(downloadUrl, destination);
1918
+ chmodSync(destination, 493);
1919
+ process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
1920
+ const installed = resolveCloudflaredCommand();
1921
+ if (!installed) {
1922
+ throw new Error("cloudflared download completed but executable is still not available");
1923
+ }
1924
+ console.log("\ncloudflared installed.\n");
1925
+ return installed;
1926
+ }
2067
1927
  function hasCodexAuth() {
2068
- const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
2069
- return existsSync2(join4(codexHome, "auth.json"));
1928
+ const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
1929
+ return existsSync2(join3(codexHome, "auth.json"));
2070
1930
  }
2071
1931
  function ensureCodexInstalled() {
2072
1932
  let codexCommand = resolveCodexCommand();
@@ -2084,7 +1944,7 @@ function ensureCodexInstalled() {
2084
1944
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
2085
1945
  `);
2086
1946
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
2087
- process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1947
+ process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
2088
1948
  };
2089
1949
  if (isTermuxRuntime()) {
2090
1950
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -2145,9 +2005,27 @@ function parseCloudflaredUrl(chunk) {
2145
2005
  }
2146
2006
  return urlMatch[urlMatch.length - 1] ?? null;
2147
2007
  }
2148
- async function startCloudflaredTunnel(localPort) {
2008
+ function getAccessibleUrls(port) {
2009
+ const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
2010
+ const interfaces = networkInterfaces();
2011
+ for (const entries of Object.values(interfaces)) {
2012
+ if (!entries) {
2013
+ continue;
2014
+ }
2015
+ for (const entry of entries) {
2016
+ if (entry.internal) {
2017
+ continue;
2018
+ }
2019
+ if (entry.family === "IPv4") {
2020
+ urls.add(`http://${entry.address}:${String(port)}`);
2021
+ }
2022
+ }
2023
+ }
2024
+ return Array.from(urls);
2025
+ }
2026
+ async function startCloudflaredTunnel(command, localPort) {
2149
2027
  return new Promise((resolve2, reject) => {
2150
- const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2028
+ const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2151
2029
  stdio: ["ignore", "pipe", "pipe"]
2152
2030
  });
2153
2031
  const timeout = setTimeout(() => {
@@ -2198,7 +2076,7 @@ function listenWithFallback(server, startPort) {
2198
2076
  };
2199
2077
  server.once("error", onError);
2200
2078
  server.once("listening", onListening);
2201
- server.listen(port);
2079
+ server.listen(port, "0.0.0.0");
2202
2080
  };
2203
2081
  attempt(startPort);
2204
2082
  });
@@ -2220,7 +2098,8 @@ async function startServer(options) {
2220
2098
  let tunnelUrl = null;
2221
2099
  if (options.tunnel) {
2222
2100
  try {
2223
- const tunnel = await startCloudflaredTunnel(port);
2101
+ const cloudflaredCommand = await ensureCloudflaredInstalledLinux() ?? "cloudflared";
2102
+ const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
2224
2103
  tunnelChild = tunnel.process;
2225
2104
  tunnelUrl = tunnel.url;
2226
2105
  } catch (error) {
@@ -2235,8 +2114,15 @@ async function startServer(options) {
2235
2114
  ` Version: ${version}`,
2236
2115
  " GitHub: https://github.com/friuns2/codexui",
2237
2116
  "",
2238
- ` Local: http://localhost:${String(port)}`
2117
+ ` Bind: http://0.0.0.0:${String(port)}`
2239
2118
  ];
2119
+ const accessUrls = getAccessibleUrls(port);
2120
+ if (accessUrls.length > 0) {
2121
+ lines.push(` Local: ${accessUrls[0]}`);
2122
+ for (const accessUrl of accessUrls.slice(1)) {
2123
+ lines.push(` Network: ${accessUrl}`);
2124
+ }
2125
+ }
2240
2126
  if (port !== requestedPort) {
2241
2127
  lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
2242
2128
  }
@@ -2245,9 +2131,7 @@ async function startServer(options) {
2245
2131
  }
2246
2132
  if (tunnelUrl) {
2247
2133
  lines.push(` Tunnel: ${tunnelUrl}`);
2248
- lines.push("");
2249
- lines.push(" Tunnel QR code:");
2250
- lines.push(` URL: ${tunnelUrl}`);
2134
+ lines.push(" Tunnel QR code below");
2251
2135
  }
2252
2136
  printTermuxKeepAlive(lines);
2253
2137
  lines.push("");