frogoe 0.5.5 → 0.6.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +724 -367
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1586,34 +1586,623 @@ var init_art_verify = __esm({
1586
1586
  }
1587
1587
  });
1588
1588
 
1589
- // src/browser.ts
1589
+ // src/net/tunnel.ts
1590
+ import { spawn, spawnSync } from "child_process";
1591
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, chmodSync, writeFileSync as writeFileSync3 } from "fs";
1592
+ import os from "os";
1590
1593
  import path7 from "path";
1591
- var browserPath, ensureBrowser;
1592
- var init_browser = __esm({
1593
- "src/browser.ts"() {
1594
+ import { gunzipSync } from "zlib";
1595
+ var URL_PATTERN, parseTunnelUrl, TAG_PATTERN, assertSafeTag, octalAt, extractSingleFile, ENV_PIN, assetName, binaryFileName, cacheBase, resolveLatestTag, mb, downloadWithProgress, binaryEchoes, resolveBinary, startTunnel;
1596
+ var init_tunnel = __esm({
1597
+ "src/net/tunnel.ts"() {
1598
+ "use strict";
1599
+ URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/iu;
1600
+ parseTunnelUrl = (chunk) => URL_PATTERN.exec(chunk)?.[0];
1601
+ TAG_PATTERN = /^\d{4}\.\d+\.\d+(-[A-Za-z0-9.]+)?$/u;
1602
+ assertSafeTag = (tag) => {
1603
+ if (!TAG_PATTERN.test(tag)) {
1604
+ throw new Error(
1605
+ `cloudflared version "${tag}" is not a valid release tag (expected e.g. 2025.10.1)`
1606
+ );
1607
+ }
1608
+ return tag;
1609
+ };
1610
+ octalAt = (block, offset, length) => {
1611
+ const raw = block.subarray(offset, offset + length).toString("utf-8");
1612
+ const digits = raw.replace(/[\0 ]/gu, "");
1613
+ return digits.length === 0 ? 0 : Number.parseInt(digits, 8);
1614
+ };
1615
+ extractSingleFile = (tar, wanted) => {
1616
+ for (let offset = 0; offset + 512 <= tar.length; ) {
1617
+ const header = tar.subarray(offset, offset + 512);
1618
+ const name = header.subarray(0, 100).toString("utf-8").replace(/\0.*$/u, "");
1619
+ const size = octalAt(header, 124, 12);
1620
+ const type = header.toString("utf-8").charCodeAt(156);
1621
+ const dataStart = offset + 512;
1622
+ const dataEnd = dataStart + size;
1623
+ if (dataEnd > tar.length) return null;
1624
+ if (name === wanted && (type === 48 || type === 0)) {
1625
+ return tar.subarray(dataStart, dataEnd);
1626
+ }
1627
+ offset = dataStart + Math.ceil(size / 512) * 512;
1628
+ }
1629
+ return null;
1630
+ };
1631
+ ENV_PIN = "FROGOE_CLOUDFLARED_VERSION";
1632
+ assetName = (platform, arch) => {
1633
+ const a = arch === "arm64" ? "arm64" : arch === "x64" ? "amd64" : arch === "ia32" ? "386" : null;
1634
+ if (!a) return null;
1635
+ switch (platform) {
1636
+ case "darwin":
1637
+ return a === "386" ? null : `cloudflared-darwin-${a}.tgz`;
1638
+ case "linux":
1639
+ return `cloudflared-linux-${a}`;
1640
+ // bare binaries
1641
+ case "win32":
1642
+ return a === "arm64" ? null : `cloudflared-windows-${a}.exe`;
1643
+ // bare executables
1644
+ default:
1645
+ return null;
1646
+ }
1647
+ };
1648
+ binaryFileName = (platform) => platform === "win32" ? "cloudflared.exe" : "cloudflared";
1649
+ cacheBase = (platform, env, home) => {
1650
+ if (platform === "darwin") return path7.join(home, "Library", "Caches");
1651
+ if (platform === "win32") return env.LOCALAPPDATA ?? path7.join(home, "AppData", "Local");
1652
+ return path7.join(home, ".cache");
1653
+ };
1654
+ resolveLatestTag = async () => {
1655
+ const res = await fetch("https://api.github.com/repos/cloudflare/cloudflared/releases/latest", {
1656
+ headers: { accept: "application/vnd.github+json" }
1657
+ });
1658
+ if (!res.ok) throw new Error(`github api ${res.status}`);
1659
+ const body = await res.json();
1660
+ if (!body.tag_name) throw new Error("github api returned no tag_name");
1661
+ return body.tag_name;
1662
+ };
1663
+ mb = (bytes) => (bytes / (1024 * 1024)).toFixed(0);
1664
+ downloadWithProgress = async (res, onChunk) => {
1665
+ const total = Number(res.headers.get("content-length") ?? 0);
1666
+ const reader = res.body?.getReader();
1667
+ if (!reader) return Buffer.from(await res.arrayBuffer());
1668
+ const chunks = [];
1669
+ let done = 0;
1670
+ let nextReport = 0;
1671
+ for (; ; ) {
1672
+ const step = await reader.read();
1673
+ if (step.done) break;
1674
+ chunks.push(Buffer.from(step.value));
1675
+ done += step.value.byteLength;
1676
+ if (done >= nextReport) {
1677
+ onChunk(done, total);
1678
+ nextReport = done + 5 * 1024 * 1024;
1679
+ }
1680
+ }
1681
+ return Buffer.concat(chunks);
1682
+ };
1683
+ binaryEchoes = (bin, tag) => {
1684
+ try {
1685
+ const probe = spawnSync(bin, ["--version"], { encoding: "utf-8", timeout: 1e4 });
1686
+ const out = `${probe.stdout ?? ""}${probe.stderr ?? ""}`;
1687
+ return probe.status === 0 && out.includes(tag);
1688
+ } catch {
1689
+ return false;
1690
+ }
1691
+ };
1692
+ resolveBinary = async (onProgress) => {
1693
+ const pathProbe = spawnSync("cloudflared", ["--version"], { stdio: "ignore", timeout: 5e3 });
1694
+ if (pathProbe.status === 0) return { downloaded: false, path: "cloudflared" };
1695
+ const platform = process.platform;
1696
+ const asset = assetName(platform, process.arch);
1697
+ if (!asset) {
1698
+ throw new Error(
1699
+ `cloudflared publishes no build for ${platform}/${process.arch} \u2014 install it and put \`cloudflared\` on PATH`
1700
+ );
1701
+ }
1702
+ const tag = process.env[ENV_PIN] ? assertSafeTag(process.env[ENV_PIN]) : assertSafeTag(await resolveLatestTag());
1703
+ const root = path7.join(cacheBase(platform, process.env, os.homedir()), "frogoe", "cloudflared");
1704
+ const dir = path7.join(root, tag);
1705
+ const bin = path7.join(dir, binaryFileName(platform));
1706
+ const rootResolved = path7.resolve(root);
1707
+ const binResolved = path7.resolve(bin);
1708
+ if (!binResolved.startsWith(rootResolved + path7.sep)) {
1709
+ throw new Error("cloudflared binary path escaped the frogoe cache \u2014 refusing to execute");
1710
+ }
1711
+ if (existsSync6(bin) && binaryEchoes(bin, tag)) return { downloaded: false, path: bin };
1712
+ onProgress?.(`downloading cloudflared ${tag} (~25 MB, once)\u2026`);
1713
+ const res = await fetch(
1714
+ `https://github.com/cloudflare/cloudflared/releases/download/${tag}/${asset}`
1715
+ );
1716
+ if (!res.ok) throw new Error(`cloudflared ${tag} download failed (${res.status})`);
1717
+ const raw = await downloadWithProgress(res, (done, total) => {
1718
+ onProgress?.(`downloading cloudflared ${tag} \u2014 ${mb(done)}${total ? `/${mb(total)}` : ""} MB`);
1719
+ });
1720
+ const binary = asset.endsWith(".tgz") ? extractSingleFile(gunzipSync(raw), "cloudflared") : raw;
1721
+ if (!binary) throw new Error("cloudflared archive did not contain the binary");
1722
+ mkdirSync3(dir, { recursive: true });
1723
+ writeFileSync3(bin, binary);
1724
+ if (platform !== "win32") chmodSync(bin, 493);
1725
+ if (!binaryEchoes(bin, tag)) {
1726
+ throw new Error(
1727
+ `cloudflared ${tag} failed its version check \u2014 deleted; set ${ENV_PIN} or install via brew`
1728
+ );
1729
+ }
1730
+ return { downloaded: true, path: bin };
1731
+ };
1732
+ startTunnel = async (port, options) => {
1733
+ const timeoutMs = options?.timeoutMs ?? 2e4;
1734
+ const bin = await resolveBinary(options?.onProgress);
1735
+ return new Promise((resolve2, reject) => {
1736
+ const child = spawn(
1737
+ bin.path,
1738
+ ["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"],
1739
+ {
1740
+ detached: true,
1741
+ stdio: ["ignore", "pipe", "pipe"],
1742
+ windowsHide: true
1743
+ }
1744
+ );
1745
+ let settled = false;
1746
+ let url;
1747
+ let buffer = "";
1748
+ const exited = new Promise((notify) => {
1749
+ child.once("exit", () => notify());
1750
+ });
1751
+ const killTree = () => {
1752
+ if (process.platform === "win32" || !child.pid) {
1753
+ child.kill("SIGTERM");
1754
+ return;
1755
+ }
1756
+ try {
1757
+ process.kill(-child.pid, "SIGTERM");
1758
+ } catch {
1759
+ child.kill("SIGTERM");
1760
+ }
1761
+ };
1762
+ const finish = (error) => {
1763
+ if (settled) return;
1764
+ settled = true;
1765
+ clearTimeout(timer);
1766
+ if (error || !url) {
1767
+ killTree();
1768
+ const tail = buffer.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).slice(-2).join(" | ");
1769
+ const base = error?.message ?? "frogoe tunnel: cloudflared exited before producing a URL";
1770
+ reject(new Error(tail ? `${base} \u2014 ${tail}` : base));
1771
+ return;
1772
+ }
1773
+ resolve2({ exited, stop: killTree, url });
1774
+ };
1775
+ const timer = setTimeout(() => {
1776
+ finish(
1777
+ new Error(
1778
+ `frogoe tunnel: no URL from cloudflared within ${timeoutMs / 1e3}s \u2014 check your internet, or brew install cloudflared`
1779
+ )
1780
+ );
1781
+ }, timeoutMs);
1782
+ child.once("exit", (code) => {
1783
+ if (!settled) {
1784
+ finish(new Error(`frogoe tunnel: cloudflared exited early (code ${code ?? "signal"})`));
1785
+ }
1786
+ });
1787
+ const watch2 = (stream) => {
1788
+ stream.on("data", (data) => {
1789
+ if (settled && url) return;
1790
+ buffer += data.toString("utf-8");
1791
+ const found = parseTunnelUrl(buffer);
1792
+ if (found && !url) {
1793
+ url = found;
1794
+ finish();
1795
+ }
1796
+ });
1797
+ };
1798
+ if (child.stderr && child.stdout) {
1799
+ watch2(child.stderr);
1800
+ watch2(child.stdout);
1801
+ }
1802
+ });
1803
+ };
1804
+ }
1805
+ });
1806
+
1807
+ // src/browser/lock.ts
1808
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, rmSync, statSync, utimesSync } from "fs";
1809
+ import path8 from "path";
1810
+ var DEFAULT_LOCK_TIMINGS, LOCK_DIR_NAME, RECLAIM_DIR_NAME, isErrno, sleep2, tryAcquireDirLock, isDirLockStale, touch, reclaimStaleLock, withInstallLock;
1811
+ var init_lock = __esm({
1812
+ "src/browser/lock.ts"() {
1813
+ "use strict";
1814
+ DEFAULT_LOCK_TIMINGS = {
1815
+ heartbeatMs: 15e3,
1816
+ pollMs: 200,
1817
+ staleMs: 12e4,
1818
+ waitNoticeMs: 1e4
1819
+ };
1820
+ LOCK_DIR_NAME = ".chrome.install.lock";
1821
+ RECLAIM_DIR_NAME = ".chrome.install.reclaim.lock";
1822
+ isErrno = (error, code) => {
1823
+ if (typeof error === "object" && error !== null && "code" in error) {
1824
+ return error.code === code;
1825
+ }
1826
+ return false;
1827
+ };
1828
+ sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1829
+ tryAcquireDirLock = (lockDir) => {
1830
+ try {
1831
+ mkdirSync4(lockDir, { recursive: false });
1832
+ return true;
1833
+ } catch (error) {
1834
+ if (!isErrno(error, "EEXIST")) throw error;
1835
+ return false;
1836
+ }
1837
+ };
1838
+ isDirLockStale = (lockDir, staleMs) => {
1839
+ try {
1840
+ return Date.now() - statSync(lockDir).mtimeMs > staleMs;
1841
+ } catch (error) {
1842
+ if (isErrno(error, "ENOENT")) return false;
1843
+ throw error;
1844
+ }
1845
+ };
1846
+ touch = (lockDir) => {
1847
+ try {
1848
+ const now = /* @__PURE__ */ new Date();
1849
+ utimesSync(lockDir, now, now);
1850
+ } catch {
1851
+ }
1852
+ };
1853
+ reclaimStaleLock = (lockDir, reclaimDir, staleMs) => {
1854
+ if (!tryAcquireDirLock(reclaimDir)) return;
1855
+ try {
1856
+ if (isDirLockStale(lockDir, staleMs)) {
1857
+ rmSync(lockDir, { recursive: true, force: true });
1858
+ }
1859
+ } finally {
1860
+ rmSync(reclaimDir, { recursive: true, force: true });
1861
+ }
1862
+ };
1863
+ withInstallLock = async (fn, rootDir, options) => {
1864
+ const timings = options?.timings ?? DEFAULT_LOCK_TIMINGS;
1865
+ const log = options?.log ?? ((line) => console.error(line));
1866
+ const lockDir = path8.join(rootDir, LOCK_DIR_NAME);
1867
+ const reclaimDir = path8.join(rootDir, RECLAIM_DIR_NAME);
1868
+ if (!existsSync7(rootDir)) mkdirSync4(rootDir, { recursive: true });
1869
+ let deadline = Date.now() + timings.staleMs;
1870
+ const waitStart = Date.now();
1871
+ let lastNoticeMs = 0;
1872
+ for (; ; ) {
1873
+ if (existsSync7(reclaimDir)) {
1874
+ if (isDirLockStale(reclaimDir, timings.staleMs)) {
1875
+ rmSync(reclaimDir, { recursive: true, force: true });
1876
+ }
1877
+ await sleep2(timings.pollMs);
1878
+ continue;
1879
+ }
1880
+ if (tryAcquireDirLock(lockDir)) {
1881
+ rmSync(reclaimDir, { recursive: true, force: true });
1882
+ break;
1883
+ }
1884
+ const waitedMs = Date.now() - waitStart;
1885
+ if (waitedMs - lastNoticeMs >= timings.waitNoticeMs) {
1886
+ lastNoticeMs = waitedMs;
1887
+ log(
1888
+ ` browser: waiting for another frogoe process to finish installing chrome-headless-shell (${Math.round(waitedMs / 1e3)}s elapsed)\u2026`
1889
+ );
1890
+ }
1891
+ if (isDirLockStale(lockDir, timings.staleMs) || Date.now() > deadline) {
1892
+ reclaimStaleLock(lockDir, reclaimDir, timings.staleMs);
1893
+ deadline = Date.now() + timings.staleMs;
1894
+ continue;
1895
+ }
1896
+ await sleep2(timings.pollMs);
1897
+ }
1898
+ const heartbeat = setInterval(() => touch(lockDir), timings.heartbeatMs);
1899
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
1900
+ try {
1901
+ return await fn();
1902
+ } finally {
1903
+ clearInterval(heartbeat);
1904
+ rmSync(lockDir, { recursive: true, force: true });
1905
+ }
1906
+ };
1907
+ }
1908
+ });
1909
+
1910
+ // src/browser/manager.ts
1911
+ import { cpSync as cpSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, renameSync, rmSync as rmSync2 } from "fs";
1912
+ import { homedir } from "os";
1913
+ import path9 from "path";
1914
+ var CHROME_BUILD, BROWSER_PATH_ENV, CACHE_ROOT, CACHE_DIR, ARCHIVE_CHECKSUMS, messageOf, positiveIntEnv, isCorruptInstallError, browserPathHintForPlatform, wrapDownloadFailure, isTimeoutError, wrapLaunchFailure, findInCache, legacyCacheDir, migrateLegacyCache, pruneLegacyCache, progressReporter, downloadBrowser, resolved, ensureBrowser;
1915
+ var init_manager = __esm({
1916
+ "src/browser/manager.ts"() {
1917
+ "use strict";
1918
+ init_tunnel();
1919
+ init_lock();
1920
+ CHROME_BUILD = "131.0.6778.204";
1921
+ BROWSER_PATH_ENV = "FROGOE_BROWSER_PATH";
1922
+ CACHE_ROOT = path9.join(cacheBase(process.platform, process.env, homedir()), "frogoe");
1923
+ CACHE_DIR = path9.join(CACHE_ROOT, "chrome");
1924
+ ARCHIVE_CHECKSUMS = {
1925
+ linux: "afaac86e302c4874245991a3d509d529c50ecdd447affff0cdc2b08520906c32",
1926
+ mac: "933463c27a951d3fc153c408c4b8a96c89b9344379963f68a5ff9aa1718bcaf9",
1927
+ mac_arm: "9dae11ffda8d77f92c56f64de84f81fa6781c9d41235a8e9397117d7d004a017",
1928
+ win32: "e1a14441accf82785fd81b78d34d8faf5cc78bbab6b2f90fa7e7e972d93ed77f",
1929
+ win64: "d45834686f461ef6f487c0eacc2063bc311a5de86e92de46600895bf44136714"
1930
+ };
1931
+ messageOf = (error) => {
1932
+ if (error instanceof Error) return error.message;
1933
+ if (typeof error === "string") return error;
1934
+ return "";
1935
+ };
1936
+ positiveIntEnv = (name, raw, fallback) => {
1937
+ if (raw === void 0 || raw.trim() === "") return fallback;
1938
+ const trimmed = raw.trim();
1939
+ if (!/^\d+$/u.test(trimmed) || Number.parseInt(trimmed, 10) <= 0) {
1940
+ throw new Error(
1941
+ `frogoe browser: ${name} must be a positive integer of milliseconds (got "${raw}") \u2014 unset it or fix the value, then re-run`
1942
+ );
1943
+ }
1944
+ return Number.parseInt(trimmed, 10);
1945
+ };
1946
+ isCorruptInstallError = (error) => {
1947
+ const message2 = messageOf(error).toLowerCase();
1948
+ return message2.includes("end of central directory") || message2.includes("end-of-central-directory") || message2.includes("invalid or corrupt") || message2.includes("corrupt zip") || message2.includes("corrupted") || message2.includes("not a zip") || message2.includes("unexpected end of") || message2.includes("integrity check failed") || message2.includes("exists but the executable");
1949
+ };
1950
+ browserPathHintForPlatform = (platform) => {
1951
+ if (platform === "darwin") return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
1952
+ if (platform === "win32") return "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
1953
+ return "/usr/bin/google-chrome";
1954
+ };
1955
+ wrapDownloadFailure = (cause) => {
1956
+ const original = messageOf(cause) || String(cause);
1957
+ return new Error(
1958
+ `frogoe browser: failed to download chrome-headless-shell ${CHROME_BUILD} \u2014 ${original}
1959
+
1960
+ Point frogoe at an already-installed Chrome/Chromium instead:
1961
+
1962
+ export ${BROWSER_PATH_ENV}="${browserPathHintForPlatform(process.platform)}"
1963
+
1964
+ Then re-run your command. Any Chrome build works for the sandbox; the pinned
1965
+ headless-shell re-downloads into ${CACHE_DIR} once the network allows.`,
1966
+ { cause: cause instanceof Error ? cause : void 0 }
1967
+ );
1968
+ };
1969
+ isTimeoutError = (error) => {
1970
+ if (!(error instanceof Error)) return false;
1971
+ return error.name === "TimeoutError" || /timed out \d+ ms/u.test(error.message);
1972
+ };
1973
+ wrapLaunchFailure = (cause, timeoutMs) => {
1974
+ const head = isTimeoutError(cause) ? `frogoe browser: headless chrome did not start within ${String(timeoutMs)} ms.
1975
+
1976
+ Raise the budget or point frogoe at an installed Chrome:
1977
+
1978
+ FROGOE_LAUNCH_TIMEOUT_MS=${String(timeoutMs * 2)} <your command>
1979
+ export ${BROWSER_PATH_ENV}="${browserPathHintForPlatform(process.platform)}"
1980
+ ` : `frogoe browser: headless chrome failed to launch \u2014 ${messageOf(cause) || String(cause)}
1981
+
1982
+ If a local Chrome exists, point frogoe at it:
1983
+
1984
+ export ${BROWSER_PATH_ENV}="${browserPathHintForPlatform(process.platform)}"
1985
+ `;
1986
+ return new Error(
1987
+ `${head}
1988
+ Then re-run your command. (The pinned binary lives in ${CACHE_DIR} \u2014
1989
+ see skills/frogoe-cli \u2192 references/live-sandbox.md)`,
1990
+ { cause: cause instanceof Error ? cause : void 0 }
1991
+ );
1992
+ };
1993
+ findInCache = async (cacheDir, buildId) => {
1994
+ if (!existsSync8(cacheDir)) return {};
1995
+ const { Browser, detectBrowserPlatform, getInstalledBrowsers } = await import("@puppeteer/browsers");
1996
+ const platform = detectBrowserPlatform();
1997
+ const match = (await getInstalledBrowsers({ cacheDir })).find(
1998
+ (entry) => entry.browser === Browser.CHROMEHEADLESSSHELL && entry.buildId === buildId && entry.platform === platform
1999
+ );
2000
+ if (!match) return {};
2001
+ if (existsSync8(match.executablePath)) return { executablePath: match.executablePath };
2002
+ return { staleInstallPath: match.path };
2003
+ };
2004
+ legacyCacheDir = (dir) => path9.resolve(dir, "node_modules", ".frogoe-browser");
2005
+ migrateLegacyCache = async (legacyDir, cacheDir, buildId, log) => {
2006
+ if (!existsSync8(legacyDir)) return null;
2007
+ const { Browser, getInstalledBrowsers } = await import("@puppeteer/browsers");
2008
+ const legacy = (await getInstalledBrowsers({ cacheDir: legacyDir })).find(
2009
+ (entry) => entry.browser === Browser.CHROMEHEADLESSSHELL && entry.buildId === buildId && existsSync8(entry.executablePath)
2010
+ );
2011
+ if (!legacy) return null;
2012
+ const browserRootName = path9.basename(path9.dirname(legacy.path));
2013
+ const installName = path9.basename(legacy.path);
2014
+ const destination = path9.join(cacheDir, browserRootName, installName);
2015
+ mkdirSync5(path9.dirname(destination), { recursive: true });
2016
+ rmSync2(destination, { recursive: true, force: true });
2017
+ try {
2018
+ try {
2019
+ renameSync(legacy.path, destination);
2020
+ } catch (error) {
2021
+ if (!isErrno(error, "EXDEV")) throw error;
2022
+ const staging = `${destination}.migrating-${String(process.pid)}`;
2023
+ rmSync2(staging, { recursive: true, force: true });
2024
+ try {
2025
+ cpSync2(legacy.path, staging, { recursive: true });
2026
+ renameSync(staging, destination);
2027
+ } finally {
2028
+ rmSync2(staging, { recursive: true, force: true });
2029
+ }
2030
+ rmSync2(legacy.path, { recursive: true, force: true });
2031
+ }
2032
+ } catch {
2033
+ return null;
2034
+ }
2035
+ const found = await findInCache(cacheDir, buildId);
2036
+ if (!found.executablePath) return null;
2037
+ rmSync2(path9.dirname(legacy.path), { recursive: true, force: true });
2038
+ rmSync2(legacyDir, { recursive: true, force: true });
2039
+ log?.(` browser: migrated the cached chrome-headless-shell into ${cacheDir} (one-time)`);
2040
+ return found.executablePath;
2041
+ };
2042
+ pruneLegacyCache = (legacyDir, buildId) => {
2043
+ if (!existsSync8(legacyDir)) return false;
2044
+ try {
2045
+ const browserRoot = path9.join(legacyDir, "chrome-headless-shell");
2046
+ if (existsSync8(browserRoot)) {
2047
+ const entries = readdirSync2(browserRoot);
2048
+ if (entries.length > 0 && !entries.some((name) => name.endsWith(`-${buildId}`))) {
2049
+ return false;
2050
+ }
2051
+ } else if (readdirSync2(legacyDir).length > 0) {
2052
+ return false;
2053
+ }
2054
+ rmSync2(legacyDir, { recursive: true, force: true });
2055
+ return true;
2056
+ } catch {
2057
+ return false;
2058
+ }
2059
+ };
2060
+ progressReporter = () => {
2061
+ const tty = process.stderr.isTTY === true;
2062
+ const mb2 = (bytes) => (bytes / (1024 * 1024)).toFixed(1);
2063
+ let lastPaintMs = 0;
2064
+ let nextMilestone = 0.25;
2065
+ return (downloaded, total) => {
2066
+ if (tty) {
2067
+ const finished = total > 0 && downloaded >= total;
2068
+ if (Date.now() - lastPaintMs < 100 && !finished) return;
2069
+ lastPaintMs = Date.now();
2070
+ process.stderr.write(
2071
+ `\r browser: downloading chrome-headless-shell \u2014 ${mb2(downloaded)}${total > 0 ? `/${mb2(total)}` : ""} MB`
2072
+ );
2073
+ if (finished) process.stderr.write("\n");
2074
+ return;
2075
+ }
2076
+ if (total <= 0 || downloaded / total < nextMilestone) return;
2077
+ process.stderr.write(
2078
+ ` browser: downloading chrome-headless-shell \u2014 ${Math.round(nextMilestone * 100)}% (${mb2(total)} MB)
2079
+ `
2080
+ );
2081
+ nextMilestone += 0.25;
2082
+ };
2083
+ };
2084
+ downloadBrowser = async (log) => {
2085
+ const { Browser, detectBrowserPlatform, install } = await import("@puppeteer/browsers");
2086
+ const platform = detectBrowserPlatform();
2087
+ if (!platform) {
2088
+ throw wrapDownloadFailure(
2089
+ new Error(`no chrome-headless-shell build exists for ${process.platform}/${process.arch}`)
2090
+ );
2091
+ }
2092
+ const expectedHash = ARCHIVE_CHECKSUMS[platform];
2093
+ if (expectedHash === void 0) {
2094
+ log(
2095
+ ` browser: no pinned checksum for ${platform} \u2014 downloading WITHOUT integrity verification`
2096
+ );
2097
+ }
2098
+ const runInstall = () => install({
2099
+ browser: Browser.CHROMEHEADLESSSHELL,
2100
+ buildId: CHROME_BUILD,
2101
+ cacheDir: CACHE_DIR,
2102
+ unpack: true,
2103
+ ...expectedHash === void 0 ? {} : { expectedHash },
2104
+ downloadProgressCallback: progressReporter()
2105
+ });
2106
+ try {
2107
+ return (await runInstall()).executablePath;
2108
+ } catch (error) {
2109
+ if (!isCorruptInstallError(error)) throw wrapDownloadFailure(error);
2110
+ log(" browser: cached archive was corrupt \u2014 purging and retrying once\u2026");
2111
+ rmSync2(CACHE_DIR, { recursive: true, force: true });
2112
+ try {
2113
+ return (await runInstall()).executablePath;
2114
+ } catch (retryError) {
2115
+ throw wrapDownloadFailure(retryError);
2116
+ }
2117
+ }
2118
+ };
2119
+ ensureBrowser = async (options) => {
2120
+ if (resolved) return resolved;
2121
+ const log = (line) => console.error(line);
2122
+ const fromEnv = process.env[BROWSER_PATH_ENV]?.trim();
2123
+ if (fromEnv !== void 0 && fromEnv !== "") {
2124
+ if (!existsSync8(fromEnv)) {
2125
+ throw new Error(
2126
+ `frogoe browser: ${BROWSER_PATH_ENV} is set but "${fromEnv}" does not exist \u2014 fix the path or unset the variable, then re-run`
2127
+ );
2128
+ }
2129
+ resolved = fromEnv;
2130
+ return resolved;
2131
+ }
2132
+ const legacyDirs = options?.legacyDirs ?? [legacyCacheDir(process.cwd())];
2133
+ const pre = await findInCache(CACHE_DIR, CHROME_BUILD);
2134
+ if (pre.executablePath) {
2135
+ resolved = pre.executablePath;
2136
+ } else {
2137
+ resolved = await withInstallLock(
2138
+ async () => {
2139
+ const post = await findInCache(CACHE_DIR, CHROME_BUILD);
2140
+ if (post.executablePath) return post.executablePath;
2141
+ if (post.staleInstallPath) {
2142
+ log(" browser: incomplete install in the cache \u2014 purging before reinstalling\u2026");
2143
+ rmSync2(post.staleInstallPath, { recursive: true, force: true });
2144
+ }
2145
+ for (const legacyDir of legacyDirs) {
2146
+ const migrated = await migrateLegacyCache(legacyDir, CACHE_DIR, CHROME_BUILD, log);
2147
+ if (migrated) return migrated;
2148
+ }
2149
+ return downloadBrowser(log);
2150
+ },
2151
+ CACHE_ROOT,
2152
+ { log }
2153
+ );
2154
+ }
2155
+ for (const legacyDir of legacyDirs) {
2156
+ if (pruneLegacyCache(legacyDir, CHROME_BUILD)) {
2157
+ log(` browser: removed the superseded per-project cache at ${legacyDir}`);
2158
+ }
2159
+ }
2160
+ return resolved;
2161
+ };
2162
+ }
2163
+ });
2164
+
2165
+ // src/browser/launch.ts
2166
+ var LAUNCH_TIMEOUT_ENV, PROTOCOL_TIMEOUT_ENV, DEFAULT_LAUNCH_TIMEOUT_MS, DEFAULT_PROTOCOL_TIMEOUT_MS, launchBudgets, launchBrowser;
2167
+ var init_launch = __esm({
2168
+ "src/browser/launch.ts"() {
1594
2169
  "use strict";
1595
- ensureBrowser = async () => {
1596
- if (browserPath) {
1597
- return browserPath;
1598
- }
1599
- const { Browser, getInstalledBrowsers, install } = await import("@puppeteer/browsers");
1600
- const cacheDir = path7.resolve(process.cwd(), "node_modules/.frogoe-browser");
1601
- const installed = await getInstalledBrowsers({ cacheDir });
1602
- const existing = installed.find((b) => b.browser === Browser.CHROMEHEADLESSSHELL);
1603
- browserPath = existing?.executablePath ?? (await install({
1604
- browser: Browser.CHROMEHEADLESSSHELL,
1605
- buildId: "131.0.6778.204",
1606
- cacheDir,
1607
- unpack: true
1608
- })).executablePath;
1609
- return browserPath;
2170
+ init_manager();
2171
+ LAUNCH_TIMEOUT_ENV = "FROGOE_LAUNCH_TIMEOUT_MS";
2172
+ PROTOCOL_TIMEOUT_ENV = "FROGOE_PROTOCOL_TIMEOUT_MS";
2173
+ DEFAULT_LAUNCH_TIMEOUT_MS = 12e4;
2174
+ DEFAULT_PROTOCOL_TIMEOUT_MS = 3e5;
2175
+ launchBudgets = (env = process.env) => ({
2176
+ launchMs: positiveIntEnv(LAUNCH_TIMEOUT_ENV, env[LAUNCH_TIMEOUT_ENV], DEFAULT_LAUNCH_TIMEOUT_MS),
2177
+ protocolMs: positiveIntEnv(
2178
+ PROTOCOL_TIMEOUT_ENV,
2179
+ env[PROTOCOL_TIMEOUT_ENV],
2180
+ DEFAULT_PROTOCOL_TIMEOUT_MS
2181
+ )
2182
+ });
2183
+ launchBrowser = async (options) => {
2184
+ const { default: puppeteer } = await import("puppeteer-core");
2185
+ const budgets = launchBudgets();
2186
+ const executablePath = await ensureBrowser({ legacyDirs: options?.legacyDirs });
2187
+ try {
2188
+ return await puppeteer.launch({
2189
+ args: ["--no-sandbox", "--disable-gpu"],
2190
+ defaultViewport: options?.defaultViewport === void 0 ? null : options.defaultViewport,
2191
+ executablePath,
2192
+ headless: true,
2193
+ protocolTimeout: budgets.protocolMs,
2194
+ timeout: budgets.launchMs
2195
+ });
2196
+ } catch (error) {
2197
+ throw wrapLaunchFailure(error, budgets.launchMs);
2198
+ }
1610
2199
  };
1611
2200
  }
1612
2201
  });
1613
2202
 
1614
2203
  // src/net/ip.ts
1615
2204
  import { execSync } from "child_process";
1616
- import os from "os";
2205
+ import os2 from "os";
1617
2206
  var VIRTUAL, isPrivateV4, isLoopback, selfAddresses, pickLanIp, interfaceFromRouteOutput, ROUTE_PROBES, routedInterfaceName, resolveLan;
1618
2207
  var init_ip = __esm({
1619
2208
  "src/net/ip.ts"() {
@@ -1713,7 +2302,7 @@ var init_ip = __esm({
1713
2302
  }
1714
2303
  return null;
1715
2304
  };
1716
- resolveLan = () => pickLanIp(os.networkInterfaces(), routedInterfaceName());
2305
+ resolveLan = () => pickLanIp(os2.networkInterfaces(), routedInterfaceName());
1717
2306
  }
1718
2307
  });
1719
2308
 
@@ -1832,8 +2421,8 @@ var init_records = __esm({
1832
2421
  });
1833
2422
 
1834
2423
  // src/telemetry/session.ts
1835
- import { appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
1836
- import path8 from "path";
2424
+ import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3 } from "fs";
2425
+ import path10 from "path";
1837
2426
  var pad, sessionStamp, createSessionStore, latestSessionFile;
1838
2427
  var init_session = __esm({
1839
2428
  "src/telemetry/session.ts"() {
@@ -1844,25 +2433,25 @@ var init_session = __esm({
1844
2433
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
1845
2434
  };
1846
2435
  createSessionStore = (gameDir, startedWall) => {
1847
- const dir = path8.join(gameDir, ".frogoe", "sessions");
2436
+ const dir = path10.join(gameDir, ".frogoe", "sessions");
1848
2437
  let file;
1849
2438
  return {
1850
2439
  file: () => file,
1851
2440
  write: (records) => {
1852
2441
  if (records.length === 0) return;
1853
2442
  if (!file) {
1854
- mkdirSync3(dir, { recursive: true });
1855
- file = path8.join(dir, `${sessionStamp(startedWall)}.jsonl`);
2443
+ mkdirSync6(dir, { recursive: true });
2444
+ file = path10.join(dir, `${sessionStamp(startedWall)}.jsonl`);
1856
2445
  }
1857
2446
  appendFileSync(file, records.map((r) => JSON.stringify(r)).join("\n") + "\n", "utf-8");
1858
2447
  }
1859
2448
  };
1860
2449
  };
1861
2450
  latestSessionFile = (gameDir) => {
1862
- const dir = path8.join(gameDir, ".frogoe", "sessions");
2451
+ const dir = path10.join(gameDir, ".frogoe", "sessions");
1863
2452
  try {
1864
- const files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).sort();
1865
- return files.length > 0 ? path8.join(dir, files[files.length - 1] ?? "") : null;
2453
+ const files = readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).sort();
2454
+ return files.length > 0 ? path10.join(dir, files[files.length - 1] ?? "") : null;
1866
2455
  } catch {
1867
2456
  return null;
1868
2457
  }
@@ -1875,9 +2464,9 @@ var run_exports = {};
1875
2464
  __export(run_exports, {
1876
2465
  startServer: () => startServer
1877
2466
  });
1878
- import { existsSync as existsSync6, readFileSync as readFileSync7, statSync, watch } from "fs";
1879
- import os2 from "os";
1880
- import path9 from "path";
2467
+ import { existsSync as existsSync9, readFileSync as readFileSync7, statSync as statSync2, watch } from "fs";
2468
+ import os3 from "os";
2469
+ import path11 from "path";
1881
2470
  import { createAdaptorServer } from "@hono/node-server";
1882
2471
  import { getConnInfo } from "@hono/node-server/conninfo";
1883
2472
  import { Hono } from "hono";
@@ -1905,15 +2494,15 @@ var init_run = __esm({
1905
2494
  woff2: "font/woff2"
1906
2495
  };
1907
2496
  startServer = async (dir, requestedPort = 0, telemetry) => {
1908
- const root = path9.resolve(dir);
1909
- if (!existsSync6(path9.join(root, "index.html"))) {
2497
+ const root = path11.resolve(dir);
2498
+ if (!existsSync9(path11.join(root, "index.html"))) {
1910
2499
  throw new Error(`frogoe run: no index.html in ${root} \u2014 is this a game folder?`);
1911
2500
  }
1912
2501
  const clients = /* @__PURE__ */ new Set();
1913
2502
  let version = 0;
1914
2503
  let remoteSeen = false;
1915
2504
  let lastRemote = "";
1916
- const own = selfAddresses(os2.networkInterfaces());
2505
+ const own = selfAddresses(os3.networkInterfaces());
1917
2506
  const session = telemetry ? createSessionStore(root, Date.now()) : void 0;
1918
2507
  const app = new Hono();
1919
2508
  app.use("*", async (c, next) => {
@@ -1964,19 +2553,19 @@ var init_run = __esm({
1964
2553
  });
1965
2554
  app.get("*", (c) => {
1966
2555
  const raw = decodeURIComponent(new URL(c.req.url).pathname);
1967
- const safe = path9.normalize(raw).replaceAll("\\", "/");
1968
- let file = path9.join(root, safe === "/" ? "index.html" : safe);
2556
+ const safe = path11.normalize(raw).replaceAll("\\", "/");
2557
+ let file = path11.join(root, safe === "/" ? "index.html" : safe);
1969
2558
  if (!file.startsWith(root)) {
1970
2559
  return c.text("forbidden", 403);
1971
2560
  }
1972
- if (existsSync6(file) && statSync(file).isDirectory()) {
1973
- file = path9.join(file, "index.html");
2561
+ if (existsSync9(file) && statSync2(file).isDirectory()) {
2562
+ file = path11.join(file, "index.html");
1974
2563
  }
1975
- if (!existsSync6(file)) {
2564
+ if (!existsSync9(file)) {
1976
2565
  return c.text(`frogoe run: not found: ${raw}`, 404);
1977
2566
  }
1978
2567
  const body = readFileSync7(file);
1979
- const ext = path9.extname(file).slice(1).toLowerCase();
2568
+ const ext = path11.extname(file).slice(1).toLowerCase();
1980
2569
  const type = MIME2[ext] ?? "application/octet-stream";
1981
2570
  if (ext === "html" || ext === "htm") {
1982
2571
  const html = body.toString("utf-8");
@@ -2005,7 +2594,7 @@ var init_run = __esm({
2005
2594
  const lan = lanInfo.ip ? `http://${lanInfo.ip}:${port}` : void 0;
2006
2595
  let timer;
2007
2596
  const watcher = watch(root, { recursive: true }, (_event, file) => {
2008
- const first = file?.split(path9.sep)[0];
2597
+ const first = file?.split(path11.sep)[0];
2009
2598
  if (first === "snapshots" || first === ".frogoe" || first === "dist") {
2010
2599
  return;
2011
2600
  }
@@ -2039,9 +2628,9 @@ var init_run = __esm({
2039
2628
  });
2040
2629
 
2041
2630
  // src/raster.ts
2042
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
2043
- import path10 from "path";
2044
- var SCENES, sleep2, rasterScriptFor, rasterizeArt;
2631
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
2632
+ import path12 from "path";
2633
+ var SCENES, sleep3, rasterScriptFor, rasterizeArt;
2045
2634
  var init_raster = __esm({
2046
2635
  "src/raster.ts"() {
2047
2636
  "use strict";
@@ -2049,32 +2638,33 @@ var init_raster = __esm({
2049
2638
  init_runtime_source();
2050
2639
  init_art_verify();
2051
2640
  init_runtime_source();
2052
- init_browser();
2641
+ init_launch();
2642
+ init_manager();
2053
2643
  SCENES = [
2054
2644
  {
2055
2645
  draw: "drawPoster",
2056
2646
  height: 1920,
2057
- out: path10.join("dist", "assets", "poster.png"),
2647
+ out: path12.join("dist", "assets", "poster.png"),
2058
2648
  size: "w, h",
2059
- source: path10.join("assets", "poster.js"),
2649
+ source: path12.join("assets", "poster.js"),
2060
2650
  width: 1080
2061
2651
  },
2062
2652
  {
2063
2653
  draw: "drawIcon",
2064
2654
  height: 1024,
2065
- out: path10.join("dist", "assets", "icon.png"),
2655
+ out: path12.join("dist", "assets", "icon.png"),
2066
2656
  size: "size",
2067
- source: path10.join("assets", "icon.js"),
2657
+ source: path12.join("assets", "icon.js"),
2068
2658
  width: 1024
2069
2659
  }
2070
2660
  ];
2071
- sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
2661
+ sleep3 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
2072
2662
  rasterScriptFor = (scene, palette) => {
2073
2663
  const sizeArg = scene.draw === "drawIcon" ? String(scene.width) : `${String(scene.width)}, ${String(scene.height)}`;
2074
2664
  const analyze = scene.draw === "drawIcon" ? "analyzeIconCorners" : "analyzeTitleBand";
2075
2665
  return `(async () => {
2076
2666
  ${runtimeSource()}
2077
- const mod = await import("./assets/${path10.basename(scene.source)}");
2667
+ const mod = await import("./assets/${path12.basename(scene.source)}");
2078
2668
  const canvas = document.createElement("canvas");
2079
2669
  canvas.id = "__frogoe_raster";
2080
2670
  canvas.width = ${String(scene.width)};
@@ -2101,8 +2691,8 @@ var init_raster = __esm({
2101
2691
  })()`;
2102
2692
  };
2103
2693
  rasterizeArt = async (options) => {
2104
- const dir = path10.resolve(options.dir);
2105
- const missing = SCENES.filter((scene) => !existsSync7(path10.join(dir, scene.source)));
2694
+ const dir = path12.resolve(options.dir);
2695
+ const missing = SCENES.filter((scene) => !existsSync10(path12.join(dir, scene.source)));
2106
2696
  if (missing.length > 0) {
2107
2697
  throw new Error(
2108
2698
  `bundle/art-missing \u2014 author the identity scenes first (${missing.map((scene) => scene.source).join(", ")}); run \`frogoe check\` and see frogoe-creative \u2192 references/art.md`
@@ -2110,17 +2700,10 @@ var init_raster = __esm({
2110
2700
  }
2111
2701
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_run(), run_exports));
2112
2702
  const server = await startServer2(dir);
2113
- const { default: puppeteer } = await import("puppeteer-core");
2114
- const executablePath = await ensureBrowser();
2115
- const browser = await puppeteer.launch({
2116
- args: ["--no-sandbox", "--disable-gpu"],
2117
- defaultViewport: null,
2118
- executablePath,
2119
- headless: true
2120
- });
2703
+ const browser = await launchBrowser({ legacyDirs: [legacyCacheDir(dir)] });
2121
2704
  try {
2122
2705
  const base = server.urls.local.replace(/\/$/u, "");
2123
- const brief = parseBrief(readFileSync8(path10.join(dir, "BRIEF.md"), "utf-8"));
2706
+ const brief = parseBrief(readFileSync8(path12.join(dir, "BRIEF.md"), "utf-8"));
2124
2707
  const palette = {
2125
2708
  accent: brief?.accent ?? "#ff3b3b",
2126
2709
  bg: brief?.bg ?? "#101418",
@@ -2166,7 +2749,7 @@ var init_raster = __esm({
2166
2749
  }
2167
2750
  } else {
2168
2751
  const report = await page.evaluate("window.__frogoeArtReport");
2169
- const gameSource = readFileSync8(path10.join(dir, "game.js"), "utf-8");
2752
+ const gameSource = readFileSync8(path12.join(dir, "game.js"), "utf-8");
2170
2753
  const verdict = verifyIconFullbleed(report, [...hexColorsOf(gameSource)]);
2171
2754
  if (verdict !== null) throw new Error(verdict);
2172
2755
  if (metrics.coverage < 0.08) {
@@ -2175,13 +2758,13 @@ var init_raster = __esm({
2175
2758
  );
2176
2759
  }
2177
2760
  }
2178
- await sleep2(80);
2761
+ await sleep3(80);
2179
2762
  const canvas = await page.$("#__frogoe_raster");
2180
2763
  if (canvas === null) throw new Error(`bundle/art-crash \u2014 ${scene.source}: no canvas`);
2181
- const outPath = path10.join(dir, scene.out);
2182
- mkdirSync4(path10.dirname(outPath), { recursive: true });
2764
+ const outPath = path12.join(dir, scene.out);
2765
+ mkdirSync7(path12.dirname(outPath), { recursive: true });
2183
2766
  await canvas.screenshot({ omitBackground: true, path: outPath, type: "png" });
2184
- files.push({ bytes: statSync2(outPath).size, file: scene.out });
2767
+ files.push({ bytes: statSync3(outPath).size, file: scene.out });
2185
2768
  } finally {
2186
2769
  await page.close();
2187
2770
  }
@@ -2201,8 +2784,8 @@ __export(bundle_exports, {
2201
2784
  command: () => command2
2202
2785
  });
2203
2786
  import { defineCommand as defineCommand2 } from "citty";
2204
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
2205
- import path11 from "path";
2787
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync4 } from "fs";
2788
+ import path13 from "path";
2206
2789
  var command2;
2207
2790
  var init_bundle2 = __esm({
2208
2791
  "src/commands/bundle.ts"() {
@@ -2218,9 +2801,9 @@ var init_bundle2 = __esm({
2218
2801
  async run({ args }) {
2219
2802
  const dir = args.dir ? String(args.dir) : process.cwd();
2220
2803
  const report = await bundle({ dir });
2221
- const outPath = args.out ? path11.resolve(String(args.out)) : path11.join(dir, "dist", "index.html");
2222
- mkdirSync5(path11.dirname(outPath), { recursive: true });
2223
- writeFileSync3(outPath, report.artifact, "utf-8");
2804
+ const outPath = args.out ? path13.resolve(String(args.out)) : path13.join(dir, "dist", "index.html");
2805
+ mkdirSync8(path13.dirname(outPath), { recursive: true });
2806
+ writeFileSync4(outPath, report.artifact, "utf-8");
2224
2807
  const art = await rasterizeArt({ dir });
2225
2808
  for (const warning of [...report.warnings, ...art.warnings]) {
2226
2809
  console.log(` \u26A0 ${warning}`);
@@ -2835,7 +3418,7 @@ var init_decisions = __esm({
2835
3418
  });
2836
3419
 
2837
3420
  // src/live/phases.ts
2838
- var END_BUDGET_MS, RETRY_NAV_MS, PLAY_STEPS, PLAY_STEP_MS, HOLD_STEP_INDEX, DRAG_STEP_INDEX, DRAG_SPAN, HOLD_MS, POLL_MS, GRACE_MS, STABILITY_CYCLES, START_BURST_TAPS, DESKTOP_FPS_MS, sleep3, jitterX, jitterY, hasError, runBootChecks, runDesktopPass, overShotName, retryShotName, waitForOver, runStartBurst, verifyDeath, runLifecycle;
3421
+ var END_BUDGET_MS, RETRY_NAV_MS, PLAY_STEPS, PLAY_STEP_MS, HOLD_STEP_INDEX, DRAG_STEP_INDEX, DRAG_SPAN, HOLD_MS, POLL_MS, GRACE_MS, STABILITY_CYCLES, START_BURST_TAPS, DESKTOP_FPS_MS, sleep4, jitterX, jitterY, hasError, runBootChecks, runDesktopPass, overShotName, retryShotName, waitForOver, runStartBurst, verifyDeath, runLifecycle;
2839
3422
  var init_phases = __esm({
2840
3423
  "src/live/phases.ts"() {
2841
3424
  "use strict";
@@ -2853,7 +3436,7 @@ var init_phases = __esm({
2853
3436
  STABILITY_CYCLES = 2;
2854
3437
  START_BURST_TAPS = 3;
2855
3438
  DESKTOP_FPS_MS = 2e3;
2856
- sleep3 = (ms) => new Promise((resolve2) => {
3439
+ sleep4 = (ms) => new Promise((resolve2) => {
2857
3440
  setTimeout(resolve2, ms);
2858
3441
  });
2859
3442
  jitterX = (step) => step * 37 % 121 - 60;
@@ -2900,7 +3483,7 @@ var init_phases = __esm({
2900
3483
  return findings;
2901
3484
  };
2902
3485
  runDesktopPass = async (driver, ctx) => {
2903
- const doSleep = ctx.sleep ?? sleep3;
3486
+ const doSleep = ctx.sleep ?? sleep4;
2904
3487
  const findings = await runBootChecks(driver, ctx);
2905
3488
  if (hasError(findings)) {
2906
3489
  return { findings };
@@ -2931,7 +3514,7 @@ var init_phases = __esm({
2931
3514
  return false;
2932
3515
  };
2933
3516
  runStartBurst = async (driver, ctx) => {
2934
- const doSleep = ctx.sleep ?? sleep3;
3517
+ const doSleep = ctx.sleep ?? sleep4;
2935
3518
  for (let step = 0; step < START_BURST_TAPS; step++) {
2936
3519
  const x = Math.round(ctx.viewport.width / 2 + jitterX(step));
2937
3520
  const y = Math.round(ctx.viewport.height / 2 + jitterY(step));
@@ -2940,7 +3523,7 @@ var init_phases = __esm({
2940
3523
  }
2941
3524
  };
2942
3525
  verifyDeath = async (driver, ctx, findings, cycle) => {
2943
- const doSleep = ctx.sleep ?? sleep3;
3526
+ const doSleep = ctx.sleep ?? sleep4;
2944
3527
  let events = await driver.finishEvents();
2945
3528
  if (events.length === 0) {
2946
3529
  await doSleep(GRACE_MS);
@@ -2961,7 +3544,7 @@ var init_phases = __esm({
2961
3544
  return presence.retry;
2962
3545
  };
2963
3546
  runLifecycle = async (driver, ctx) => {
2964
- const doSleep = ctx.sleep ?? sleep3;
3547
+ const doSleep = ctx.sleep ?? sleep4;
2965
3548
  const settle = ctx.settleMs ?? 2e3;
2966
3549
  const name = ctx.viewport.name;
2967
3550
  const findings = [];
@@ -3134,13 +3717,14 @@ var live_exports = {};
3134
3717
  __export(live_exports, {
3135
3718
  collectLive: () => collectLive
3136
3719
  });
3137
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
3138
- import path12 from "path";
3720
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
3721
+ import path14 from "path";
3139
3722
  var VIEWPORTS, waitForServer, collectLive;
3140
3723
  var init_live = __esm({
3141
3724
  "src/live/index.ts"() {
3142
3725
  "use strict";
3143
- init_browser();
3726
+ init_launch();
3727
+ init_manager();
3144
3728
  init_driver();
3145
3729
  init_phases();
3146
3730
  VIEWPORTS = [
@@ -3156,11 +3740,11 @@ var init_live = __esm({
3156
3740
  }
3157
3741
  } catch {
3158
3742
  }
3159
- await sleep3(500);
3743
+ await sleep4(500);
3160
3744
  }
3161
3745
  };
3162
3746
  collectLive = async (options) => {
3163
- const dir = path12.resolve(options.dir);
3747
+ const dir = path14.resolve(options.dir);
3164
3748
  const settle = options.settleMs ?? 2e3;
3165
3749
  const findings = [];
3166
3750
  const screenshots = [];
@@ -3170,16 +3754,9 @@ var init_live = __esm({
3170
3754
  };
3171
3755
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_run(), run_exports));
3172
3756
  const server = await startServer2(dir);
3173
- const snapshotDir = path12.join(dir, "snapshots");
3174
- mkdirSync6(snapshotDir, { recursive: true });
3175
- const { default: puppeteer } = await import("puppeteer-core");
3176
- const executablePath = await ensureBrowser();
3177
- const browser = await puppeteer.launch({
3178
- args: ["--no-sandbox", "--disable-gpu"],
3179
- defaultViewport: null,
3180
- executablePath,
3181
- headless: true
3182
- });
3757
+ const snapshotDir = path14.join(dir, "snapshots");
3758
+ mkdirSync9(snapshotDir, { recursive: true });
3759
+ const browser = await launchBrowser({ legacyDirs: [legacyCacheDir(dir)] });
3183
3760
  try {
3184
3761
  let serverReady = false;
3185
3762
  for (const viewport of VIEWPORTS) {
@@ -3191,15 +3768,15 @@ var init_live = __esm({
3191
3768
  size: { height: viewport2.height, width: viewport2.width }
3192
3769
  });
3193
3770
  const shot = async (name) => {
3194
- writeFileSync4(path12.join(snapshotDir, name), await driver.screenshot());
3195
- screenshots.push(path12.join("snapshots", name));
3771
+ writeFileSync5(path14.join(snapshotDir, name), await driver.screenshot());
3772
+ screenshots.push(path14.join("snapshots", name));
3196
3773
  };
3197
3774
  if (!serverReady) {
3198
3775
  await waitForServer(server.urls.local);
3199
3776
  serverReady = true;
3200
3777
  }
3201
3778
  await page.goto(server.urls.local, { timeout: 15e3, waitUntil: "domcontentloaded" });
3202
- await sleep3(settle);
3779
+ await sleep4(settle);
3203
3780
  if (viewport2.name === "mobile") {
3204
3781
  const outcome = await runLifecycle(driver, {
3205
3782
  settleMs: settle,
@@ -3628,8 +4205,8 @@ var init_compose = __esm({
3628
4205
 
3629
4206
  // src/manifest.ts
3630
4207
  import { createHash as createHash2 } from "crypto";
3631
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
3632
- import path13 from "path";
4208
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
4209
+ import path15 from "path";
3633
4210
  var DESCRIPTION_CAP, descriptionFrom, buildManifest;
3634
4211
  var init_manifest = __esm({
3635
4212
  "src/manifest.ts"() {
@@ -3648,16 +4225,16 @@ var init_manifest = __esm({
3648
4225
  return null;
3649
4226
  };
3650
4227
  buildManifest = (options) => {
3651
- const dir = path13.resolve(options.dir);
3652
- const briefSource = readFileSync9(path13.join(dir, "BRIEF.md"), "utf-8");
4228
+ const dir = path15.resolve(options.dir);
4229
+ const briefSource = readFileSync9(path15.join(dir, "BRIEF.md"), "utf-8");
3653
4230
  const brief = parseBrief(briefSource);
3654
4231
  if (!brief?.title) return null;
3655
- const pin = existsSync8(path13.join(dir, "frogoe.json")) ? JSON.parse(readFileSync9(path13.join(dir, "frogoe.json"), "utf-8")).contract ?? "0.1.0" : "0.1.0";
3656
- const artifact = options.artifactHtml ?? readFileSync9(path13.join(dir, "dist", "index.html"), "utf-8");
4232
+ const pin = existsSync11(path15.join(dir, "frogoe.json")) ? JSON.parse(readFileSync9(path15.join(dir, "frogoe.json"), "utf-8")).contract ?? "0.1.0" : "0.1.0";
4233
+ const artifact = options.artifactHtml ?? readFileSync9(path15.join(dir, "dist", "index.html"), "utf-8");
3657
4234
  const sha2562 = createHash2("sha256").update(artifact, "utf-8").digest("hex");
3658
4235
  const mediaSha = (file) => {
3659
- const full = path13.join(dir, "dist", "assets", file);
3660
- return existsSync8(full) ? createHash2("sha256").update(readFileSync9(full)).digest("hex") : null;
4236
+ const full = path15.join(dir, "dist", "assets", file);
4237
+ return existsSync11(full) ? createHash2("sha256").update(readFileSync9(full)).digest("hex") : null;
3661
4238
  };
3662
4239
  return {
3663
4240
  artifact: "index.html",
@@ -3666,7 +4243,7 @@ var init_manifest = __esm({
3666
4243
  description: descriptionFrom(briefSource),
3667
4244
  entry: "index.html",
3668
4245
  fonts: brief.fonts ?? null,
3669
- icon: existsSync8(path13.join(dir, "dist", "assets", "icon.png")) ? "assets/icon.png" : null,
4246
+ icon: existsSync11(path15.join(dir, "dist", "assets", "icon.png")) ? "assets/icon.png" : null,
3670
4247
  iconSha256: mediaSha("icon.png"),
3671
4248
  mood: brief.mood ?? null,
3672
4249
  palette: {
@@ -3675,7 +4252,7 @@ var init_manifest = __esm({
3675
4252
  fg: brief.fg ?? "",
3676
4253
  ...brief.outline ? { outline: brief.outline } : {}
3677
4254
  },
3678
- poster: existsSync8(path13.join(dir, "dist", "assets", "poster.png")) ? "assets/poster.png" : null,
4255
+ poster: existsSync11(path15.join(dir, "dist", "assets", "poster.png")) ? "assets/poster.png" : null,
3679
4256
  posterSha256: mediaSha("poster.png"),
3680
4257
  title: brief.title,
3681
4258
  verb: brief.verb ?? "tap"
@@ -3689,8 +4266,8 @@ var embed_exports = {};
3689
4266
  __export(embed_exports, {
3690
4267
  command: () => command4
3691
4268
  });
3692
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
3693
- import path14 from "path";
4269
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
4270
+ import path16 from "path";
3694
4271
  import { defineCommand as defineCommand4 } from "citty";
3695
4272
  var dataUri2, command4;
3696
4273
  var init_embed = __esm({
@@ -3700,15 +4277,15 @@ var init_embed = __esm({
3700
4277
  init_card();
3701
4278
  init_compose();
3702
4279
  init_manifest();
3703
- dataUri2 = (file) => existsSync9(file) ? `data:image/png;base64,${readFileSync10(file).toString("base64")}` : null;
4280
+ dataUri2 = (file) => existsSync12(file) ? `data:image/png;base64,${readFileSync10(file).toString("base64")}` : null;
3704
4281
  command4 = defineCommand4({
3705
4282
  args: {
3706
4283
  dir: { type: "positional", required: false, description: "game folder (default: cwd)" }
3707
4284
  },
3708
4285
  async run({ args }) {
3709
4286
  const dir = args.dir ? String(args.dir) : process.cwd();
3710
- const artifactPath = path14.join(dir, "dist", "index.html");
3711
- if (!existsSync9(artifactPath)) {
4287
+ const artifactPath = path16.join(dir, "dist", "index.html");
4288
+ if (!existsSync12(artifactPath)) {
3712
4289
  throw new Error(
3713
4290
  "frogoe embed: no dist/index.html \u2014 run `frogoe bundle` first (and `frogoe check` before it: check \u2192 bundle \u2192 embed)"
3714
4291
  );
@@ -3724,20 +4301,20 @@ var init_embed = __esm({
3724
4301
  if (!manifest) {
3725
4302
  throw new Error("frogoe embed: could not build the manifest from BRIEF.md");
3726
4303
  }
3727
- const posterDataUri = dataUri2(path14.join(dir, "dist", "assets", "poster.png"));
3728
- const iconDataUri = dataUri2(path14.join(dir, "dist", "assets", "icon.png"));
4304
+ const posterDataUri = dataUri2(path16.join(dir, "dist", "assets", "poster.png"));
4305
+ const iconDataUri = dataUri2(path16.join(dir, "dist", "assets", "icon.png"));
3729
4306
  const gameHtml = readFileSync10(artifactPath, "utf-8");
3730
4307
  const { payloadB64 } = composePayload(gameHtml);
3731
4308
  const html = composeEmbedHtml({ iconDataUri, manifest, payloadB64, posterDataUri });
3732
- mkdirSync7(path14.join(dir, "dist"), { recursive: true });
3733
- writeFileSync5(
3734
- path14.join(dir, "dist", "manifest.json"),
4309
+ mkdirSync10(path16.join(dir, "dist"), { recursive: true });
4310
+ writeFileSync6(
4311
+ path16.join(dir, "dist", "manifest.json"),
3735
4312
  `${JSON.stringify(manifest, null, 2)}
3736
4313
  `,
3737
4314
  "utf-8"
3738
4315
  );
3739
- const outPath = path14.join(dir, "dist", "embed.html");
3740
- writeFileSync5(outPath, html, "utf-8");
4316
+ const outPath = path16.join(dir, "dist", "embed.html");
4317
+ writeFileSync6(outPath, html, "utf-8");
3741
4318
  console.log(` frogoe embed \u2192 ${outPath}`);
3742
4319
  console.log(
3743
4320
  ` ${html.length.toLocaleString("en-US")} bytes \xB7 sandbox allow-scripts \xB7 sha256 ${manifest.artifactSha256.slice(0, 12)}`
@@ -3864,17 +4441,18 @@ var vision_exports = {};
3864
4441
  __export(vision_exports, {
3865
4442
  command: () => command5
3866
4443
  });
3867
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
3868
- import path15 from "path";
4444
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
4445
+ import path17 from "path";
3869
4446
  import { defineCommand as defineCommand5 } from "citty";
3870
4447
  var HELPERS, PAGE_SCRIPT, show, metricLine, command5;
3871
4448
  var init_vision = __esm({
3872
4449
  "src/commands/vision.ts"() {
3873
4450
  "use strict";
3874
4451
  init_src();
4452
+ init_launch();
4453
+ init_manager();
3875
4454
  init_art_eyes();
3876
4455
  init_art_verify();
3877
- init_browser();
3878
4456
  HELPERS = [
3879
4457
  luminance,
3880
4458
  contrastRatio2,
@@ -3984,9 +4562,9 @@ var init_vision = __esm({
3984
4562
  objects: { type: "boolean", description: "SPRITES objects only" }
3985
4563
  },
3986
4564
  async run({ args }) {
3987
- const dir = path15.resolve(args.dir ? String(args.dir) : process.cwd());
3988
- const briefPath = path15.join(dir, "BRIEF.md");
3989
- if (!existsSync10(briefPath)) {
4565
+ const dir = path17.resolve(args.dir ? String(args.dir) : process.cwd());
4566
+ const briefPath = path17.join(dir, "BRIEF.md");
4567
+ if (!existsSync13(briefPath)) {
3990
4568
  throw new Error(
3991
4569
  "frogoe vision: no BRIEF.md in this folder \u2014 run this from a game (frogoe init)"
3992
4570
  );
@@ -4011,12 +4589,9 @@ var init_vision = __esm({
4011
4589
  const compact = !wantAll;
4012
4590
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_run(), run_exports));
4013
4591
  const server = await startServer2(dir);
4014
- const { default: puppeteer } = await import("puppeteer-core");
4015
- const browser = await puppeteer.launch({
4016
- args: ["--no-sandbox", "--disable-gpu"],
4592
+ const browser = await launchBrowser({
4017
4593
  defaultViewport: { height: 844, width: 390 },
4018
- executablePath: await ensureBrowser(),
4019
- headless: true
4594
+ legacyDirs: [legacyCacheDir(dir)]
4020
4595
  });
4021
4596
  let report;
4022
4597
  try {
@@ -4101,7 +4676,7 @@ var init_vision = __esm({
4101
4676
  console.log(show(icon, pretty));
4102
4677
  }
4103
4678
  }
4104
- if (!targeted && !existsSync10(path15.join(dir, "assets", "poster.js"))) {
4679
+ if (!targeted && !existsSync13(path17.join(dir, "assets", "poster.js"))) {
4105
4680
  console.log("\n(note: run `frogoe check` first \u2014 vision only looks, it never gates)");
4106
4681
  }
4107
4682
  },
@@ -4240,8 +4815,8 @@ var init_firewall = __esm({
4240
4815
  return null;
4241
4816
  }
4242
4817
  };
4243
- binaryBlocked = (path17) => {
4244
- const out = run(`--getappblocked "${path17}"`);
4818
+ binaryBlocked = (path18) => {
4819
+ const out = run(`--getappblocked "${path18}"`);
4245
4820
  if (out === null) return null;
4246
4821
  if (/blocked/iu.test(out)) return true;
4247
4822
  if (/allowed/iu.test(out)) return false;
@@ -4297,224 +4872,6 @@ var init_plan = __esm({
4297
4872
  }
4298
4873
  });
4299
4874
 
4300
- // src/net/tunnel.ts
4301
- import { spawn, spawnSync } from "child_process";
4302
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, chmodSync, writeFileSync as writeFileSync6 } from "fs";
4303
- import os3 from "os";
4304
- import path16 from "path";
4305
- import { gunzipSync } from "zlib";
4306
- var URL_PATTERN, parseTunnelUrl, TAG_PATTERN, assertSafeTag, octalAt, extractSingleFile, ENV_PIN, assetName, binaryFileName, cacheBase, resolveLatestTag, mb, downloadWithProgress, binaryEchoes, resolveBinary, startTunnel;
4307
- var init_tunnel = __esm({
4308
- "src/net/tunnel.ts"() {
4309
- "use strict";
4310
- URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/iu;
4311
- parseTunnelUrl = (chunk) => URL_PATTERN.exec(chunk)?.[0];
4312
- TAG_PATTERN = /^\d{4}\.\d+\.\d+(-[A-Za-z0-9.]+)?$/u;
4313
- assertSafeTag = (tag) => {
4314
- if (!TAG_PATTERN.test(tag)) {
4315
- throw new Error(
4316
- `cloudflared version "${tag}" is not a valid release tag (expected e.g. 2025.10.1)`
4317
- );
4318
- }
4319
- return tag;
4320
- };
4321
- octalAt = (block, offset, length) => {
4322
- const raw = block.subarray(offset, offset + length).toString("utf-8");
4323
- const digits = raw.replace(/[\0 ]/gu, "");
4324
- return digits.length === 0 ? 0 : Number.parseInt(digits, 8);
4325
- };
4326
- extractSingleFile = (tar, wanted) => {
4327
- for (let offset = 0; offset + 512 <= tar.length; ) {
4328
- const header = tar.subarray(offset, offset + 512);
4329
- const name = header.subarray(0, 100).toString("utf-8").replace(/\0.*$/u, "");
4330
- const size = octalAt(header, 124, 12);
4331
- const type = header.toString("utf-8").charCodeAt(156);
4332
- const dataStart = offset + 512;
4333
- const dataEnd = dataStart + size;
4334
- if (dataEnd > tar.length) return null;
4335
- if (name === wanted && (type === 48 || type === 0)) {
4336
- return tar.subarray(dataStart, dataEnd);
4337
- }
4338
- offset = dataStart + Math.ceil(size / 512) * 512;
4339
- }
4340
- return null;
4341
- };
4342
- ENV_PIN = "FROGOE_CLOUDFLARED_VERSION";
4343
- assetName = (platform, arch) => {
4344
- const a = arch === "arm64" ? "arm64" : arch === "x64" ? "amd64" : arch === "ia32" ? "386" : null;
4345
- if (!a) return null;
4346
- switch (platform) {
4347
- case "darwin":
4348
- return a === "386" ? null : `cloudflared-darwin-${a}.tgz`;
4349
- case "linux":
4350
- return `cloudflared-linux-${a}`;
4351
- // bare binaries
4352
- case "win32":
4353
- return a === "arm64" ? null : `cloudflared-windows-${a}.exe`;
4354
- // bare executables
4355
- default:
4356
- return null;
4357
- }
4358
- };
4359
- binaryFileName = (platform) => platform === "win32" ? "cloudflared.exe" : "cloudflared";
4360
- cacheBase = (platform, env, home) => {
4361
- if (platform === "darwin") return path16.join(home, "Library", "Caches");
4362
- if (platform === "win32") return env.LOCALAPPDATA ?? path16.join(home, "AppData", "Local");
4363
- return path16.join(home, ".cache");
4364
- };
4365
- resolveLatestTag = async () => {
4366
- const res = await fetch("https://api.github.com/repos/cloudflare/cloudflared/releases/latest", {
4367
- headers: { accept: "application/vnd.github+json" }
4368
- });
4369
- if (!res.ok) throw new Error(`github api ${res.status}`);
4370
- const body = await res.json();
4371
- if (!body.tag_name) throw new Error("github api returned no tag_name");
4372
- return body.tag_name;
4373
- };
4374
- mb = (bytes) => (bytes / (1024 * 1024)).toFixed(0);
4375
- downloadWithProgress = async (res, onChunk) => {
4376
- const total = Number(res.headers.get("content-length") ?? 0);
4377
- const reader = res.body?.getReader();
4378
- if (!reader) return Buffer.from(await res.arrayBuffer());
4379
- const chunks = [];
4380
- let done = 0;
4381
- let nextReport = 0;
4382
- for (; ; ) {
4383
- const step = await reader.read();
4384
- if (step.done) break;
4385
- chunks.push(Buffer.from(step.value));
4386
- done += step.value.byteLength;
4387
- if (done >= nextReport) {
4388
- onChunk(done, total);
4389
- nextReport = done + 5 * 1024 * 1024;
4390
- }
4391
- }
4392
- return Buffer.concat(chunks);
4393
- };
4394
- binaryEchoes = (bin, tag) => {
4395
- try {
4396
- const probe = spawnSync(bin, ["--version"], { encoding: "utf-8", timeout: 1e4 });
4397
- const out = `${probe.stdout ?? ""}${probe.stderr ?? ""}`;
4398
- return probe.status === 0 && out.includes(tag);
4399
- } catch {
4400
- return false;
4401
- }
4402
- };
4403
- resolveBinary = async (onProgress) => {
4404
- const pathProbe = spawnSync("cloudflared", ["--version"], { stdio: "ignore", timeout: 5e3 });
4405
- if (pathProbe.status === 0) return { downloaded: false, path: "cloudflared" };
4406
- const platform = process.platform;
4407
- const asset = assetName(platform, process.arch);
4408
- if (!asset) {
4409
- throw new Error(
4410
- `cloudflared publishes no build for ${platform}/${process.arch} \u2014 install it and put \`cloudflared\` on PATH`
4411
- );
4412
- }
4413
- const tag = process.env[ENV_PIN] ? assertSafeTag(process.env[ENV_PIN]) : assertSafeTag(await resolveLatestTag());
4414
- const root = path16.join(cacheBase(platform, process.env, os3.homedir()), "frogoe", "cloudflared");
4415
- const dir = path16.join(root, tag);
4416
- const bin = path16.join(dir, binaryFileName(platform));
4417
- const rootResolved = path16.resolve(root);
4418
- const binResolved = path16.resolve(bin);
4419
- if (!binResolved.startsWith(rootResolved + path16.sep)) {
4420
- throw new Error("cloudflared binary path escaped the frogoe cache \u2014 refusing to execute");
4421
- }
4422
- if (existsSync11(bin) && binaryEchoes(bin, tag)) return { downloaded: false, path: bin };
4423
- onProgress?.(`downloading cloudflared ${tag} (~25 MB, once)\u2026`);
4424
- const res = await fetch(
4425
- `https://github.com/cloudflare/cloudflared/releases/download/${tag}/${asset}`
4426
- );
4427
- if (!res.ok) throw new Error(`cloudflared ${tag} download failed (${res.status})`);
4428
- const raw = await downloadWithProgress(res, (done, total) => {
4429
- onProgress?.(`downloading cloudflared ${tag} \u2014 ${mb(done)}${total ? `/${mb(total)}` : ""} MB`);
4430
- });
4431
- const binary = asset.endsWith(".tgz") ? extractSingleFile(gunzipSync(raw), "cloudflared") : raw;
4432
- if (!binary) throw new Error("cloudflared archive did not contain the binary");
4433
- mkdirSync8(dir, { recursive: true });
4434
- writeFileSync6(bin, binary);
4435
- if (platform !== "win32") chmodSync(bin, 493);
4436
- if (!binaryEchoes(bin, tag)) {
4437
- throw new Error(
4438
- `cloudflared ${tag} failed its version check \u2014 deleted; set ${ENV_PIN} or install via brew`
4439
- );
4440
- }
4441
- return { downloaded: true, path: bin };
4442
- };
4443
- startTunnel = async (port, options) => {
4444
- const timeoutMs = options?.timeoutMs ?? 2e4;
4445
- const bin = await resolveBinary(options?.onProgress);
4446
- return new Promise((resolve2, reject) => {
4447
- const child = spawn(
4448
- bin.path,
4449
- ["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"],
4450
- {
4451
- detached: true,
4452
- stdio: ["ignore", "pipe", "pipe"],
4453
- windowsHide: true
4454
- }
4455
- );
4456
- let settled = false;
4457
- let url;
4458
- let buffer = "";
4459
- const exited = new Promise((notify) => {
4460
- child.once("exit", () => notify());
4461
- });
4462
- const killTree = () => {
4463
- if (process.platform === "win32" || !child.pid) {
4464
- child.kill("SIGTERM");
4465
- return;
4466
- }
4467
- try {
4468
- process.kill(-child.pid, "SIGTERM");
4469
- } catch {
4470
- child.kill("SIGTERM");
4471
- }
4472
- };
4473
- const finish = (error) => {
4474
- if (settled) return;
4475
- settled = true;
4476
- clearTimeout(timer);
4477
- if (error || !url) {
4478
- killTree();
4479
- const tail = buffer.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).slice(-2).join(" | ");
4480
- const base = error?.message ?? "frogoe tunnel: cloudflared exited before producing a URL";
4481
- reject(new Error(tail ? `${base} \u2014 ${tail}` : base));
4482
- return;
4483
- }
4484
- resolve2({ exited, stop: killTree, url });
4485
- };
4486
- const timer = setTimeout(() => {
4487
- finish(
4488
- new Error(
4489
- `frogoe tunnel: no URL from cloudflared within ${timeoutMs / 1e3}s \u2014 check your internet, or brew install cloudflared`
4490
- )
4491
- );
4492
- }, timeoutMs);
4493
- child.once("exit", (code) => {
4494
- if (!settled) {
4495
- finish(new Error(`frogoe tunnel: cloudflared exited early (code ${code ?? "signal"})`));
4496
- }
4497
- });
4498
- const watch2 = (stream) => {
4499
- stream.on("data", (data) => {
4500
- if (settled && url) return;
4501
- buffer += data.toString("utf-8");
4502
- const found = parseTunnelUrl(buffer);
4503
- if (found && !url) {
4504
- url = found;
4505
- finish();
4506
- }
4507
- });
4508
- };
4509
- if (child.stderr && child.stdout) {
4510
- watch2(child.stderr);
4511
- watch2(child.stdout);
4512
- }
4513
- });
4514
- };
4515
- }
4516
- });
4517
-
4518
4875
  // src/commands/run.ts
4519
4876
  var run_exports2 = {};
4520
4877
  __export(run_exports2, {
@@ -4644,8 +5001,8 @@ var init_run2 = __esm({
4644
5001
  // src/utils/skillsManifest.ts
4645
5002
  import { execFile } from "child_process";
4646
5003
  import { createHash as createHash3 } from "crypto";
4647
- import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
4648
- import { homedir } from "os";
5004
+ import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
5005
+ import { homedir as homedir2 } from "os";
4649
5006
  import { isAbsolute, join, relative, resolve, sep } from "path";
4650
5007
  import { promisify } from "util";
4651
5008
  function isCoreSkill(name) {
@@ -4654,10 +5011,10 @@ function isCoreSkill(name) {
4654
5011
  function listFilesSorted(dir) {
4655
5012
  const out = [];
4656
5013
  const walk = (d) => {
4657
- for (const name of readdirSync3(d)) {
5014
+ for (const name of readdirSync4(d)) {
4658
5015
  if (name === ".DS_Store") continue;
4659
5016
  const p = join(d, name);
4660
- if (statSync3(p).isDirectory()) walk(p);
5017
+ if (statSync4(p).isDirectory()) walk(p);
4661
5018
  else out.push(p);
4662
5019
  }
4663
5020
  };
@@ -4680,7 +5037,7 @@ function hashSkillBundle(skillDir) {
4680
5037
  return { hash: h.digest("hex").slice(0, 16), files: files.length };
4681
5038
  }
4682
5039
  function buildManifest2(skillsRoot, meta) {
4683
- const names = readdirSync3(skillsRoot).filter((n) => existsSync12(join(skillsRoot, n, "SKILL.md"))).sort();
5040
+ const names = readdirSync4(skillsRoot).filter((n) => existsSync14(join(skillsRoot, n, "SKILL.md"))).sort();
4684
5041
  const skills = {};
4685
5042
  for (const name of names) skills[name] = hashSkillBundle(join(skillsRoot, name));
4686
5043
  return { source: meta.source, skills };
@@ -4696,7 +5053,7 @@ function agentFromDir(dir) {
4696
5053
  }
4697
5054
  function listSubdirs(dir) {
4698
5055
  try {
4699
- return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
5056
+ return readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
4700
5057
  } catch {
4701
5058
  return [];
4702
5059
  }
@@ -4705,7 +5062,7 @@ function discoverSkillRoots(base, scope) {
4705
5062
  const candidates = [];
4706
5063
  const add = (hostBase, host) => {
4707
5064
  const dir = join(hostBase, host, "skills");
4708
- if (existsSync12(dir) && statSync3(dir).isDirectory())
5065
+ if (existsSync14(dir) && statSync4(dir).isDirectory())
4709
5066
  candidates.push({ dir, agent: agentLabel(host), scope });
4710
5067
  };
4711
5068
  for (const host of listSubdirs(base)) add(base, host);
@@ -4732,18 +5089,18 @@ function scopeForDir(dir, home, cwd) {
4732
5089
  }
4733
5090
  function locateInstall(skillNames, opts = {}) {
4734
5091
  if (opts.dir) {
4735
- return existsSync12(opts.dir) ? {
5092
+ return existsSync14(opts.dir) ? {
4736
5093
  dir: opts.dir,
4737
5094
  agent: agentFromDir(opts.dir),
4738
- scope: scopeForDir(opts.dir, opts.home ?? homedir(), opts.cwd ?? process.cwd())
5095
+ scope: scopeForDir(opts.dir, opts.home ?? homedir2(), opts.cwd ?? process.cwd())
4739
5096
  } : null;
4740
5097
  }
4741
5098
  const roots = [
4742
- ...discoverSkillRoots(opts.home ?? homedir(), "global"),
5099
+ ...discoverSkillRoots(opts.home ?? homedir2(), "global"),
4743
5100
  ...discoverSkillRoots(opts.cwd ?? process.cwd(), "project")
4744
5101
  ];
4745
5102
  for (const root of roots) {
4746
- if (skillNames.some((n) => existsSync12(join(root.dir, n, "SKILL.md")))) return root;
5103
+ if (skillNames.some((n) => existsSync14(join(root.dir, n, "SKILL.md")))) return root;
4747
5104
  }
4748
5105
  return null;
4749
5106
  }
@@ -4751,7 +5108,7 @@ function hashInstalled(root, skillNames) {
4751
5108
  const out = {};
4752
5109
  for (const name of skillNames) {
4753
5110
  const skillDir = join(root.dir, name);
4754
- if (existsSync12(join(skillDir, "SKILL.md"))) out[name] = hashSkillBundle(skillDir);
5111
+ if (existsSync14(join(skillDir, "SKILL.md"))) out[name] = hashSkillBundle(skillDir);
4755
5112
  }
4756
5113
  return out;
4757
5114
  }
@@ -4788,7 +5145,7 @@ function findRepoManifest(cwd = process.cwd()) {
4788
5145
  let dir = cwd;
4789
5146
  for (let i = 0; i < 16; i++) {
4790
5147
  const p = join(dir, MANIFEST_FILE);
4791
- if (existsSync12(p)) return p;
5148
+ if (existsSync14(p)) return p;
4792
5149
  const parent = join(dir, "..");
4793
5150
  if (parent === dir) break;
4794
5151
  dir = parent;
@@ -4828,9 +5185,9 @@ async function remoteHeadSha(repoSlug) {
4828
5185
  }
4829
5186
  function resolveLocalManifest(source) {
4830
5187
  const direct = source.endsWith(".json") ? source : join(source, MANIFEST_FILE);
4831
- if (existsSync12(direct)) return JSON.parse(readFileSync13(direct, "utf8"));
5188
+ if (existsSync14(direct)) return JSON.parse(readFileSync13(direct, "utf8"));
4832
5189
  const skillsRoot = source.endsWith("skills") ? source : join(source, "skills");
4833
- if (existsSync12(skillsRoot)) return buildManifest2(skillsRoot, { source: skillsRoot });
5190
+ if (existsSync14(skillsRoot)) return buildManifest2(skillsRoot, { source: skillsRoot });
4834
5191
  throw new Error(`No skills manifest found at: ${source}`);
4835
5192
  }
4836
5193
  async function fetchRemoteManifest(source) {
@@ -5120,7 +5477,7 @@ import { defineCommand as defineCommand11, runMain } from "citty";
5120
5477
  // package.json
5121
5478
  var package_default = {
5122
5479
  name: "frogoe",
5123
- version: "0.5.5",
5480
+ version: "0.6.0",
5124
5481
  description: "froge CLI \u2014 the agent's hands: init, add, run, check, bundle",
5125
5482
  homepage: "https://github.com/frogoe/engine#readme",
5126
5483
  bugs: "https://github.com/frogoe/engine/issues",