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.
- package/dist/cli.js +724 -367
- 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/
|
|
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
|
-
|
|
1592
|
-
var
|
|
1593
|
-
|
|
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
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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
|
|
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(
|
|
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
|
|
1836
|
-
import
|
|
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 =
|
|
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
|
-
|
|
1855
|
-
file =
|
|
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 =
|
|
2451
|
+
const dir = path10.join(gameDir, ".frogoe", "sessions");
|
|
1863
2452
|
try {
|
|
1864
|
-
const files =
|
|
1865
|
-
return files.length > 0 ?
|
|
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
|
|
1879
|
-
import
|
|
1880
|
-
import
|
|
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 =
|
|
1909
|
-
if (!
|
|
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(
|
|
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 =
|
|
1968
|
-
let file =
|
|
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 (
|
|
1973
|
-
file =
|
|
2561
|
+
if (existsSync9(file) && statSync2(file).isDirectory()) {
|
|
2562
|
+
file = path11.join(file, "index.html");
|
|
1974
2563
|
}
|
|
1975
|
-
if (!
|
|
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 =
|
|
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(
|
|
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
|
|
2043
|
-
import
|
|
2044
|
-
var SCENES,
|
|
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
|
-
|
|
2641
|
+
init_launch();
|
|
2642
|
+
init_manager();
|
|
2053
2643
|
SCENES = [
|
|
2054
2644
|
{
|
|
2055
2645
|
draw: "drawPoster",
|
|
2056
2646
|
height: 1920,
|
|
2057
|
-
out:
|
|
2647
|
+
out: path12.join("dist", "assets", "poster.png"),
|
|
2058
2648
|
size: "w, h",
|
|
2059
|
-
source:
|
|
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:
|
|
2655
|
+
out: path12.join("dist", "assets", "icon.png"),
|
|
2066
2656
|
size: "size",
|
|
2067
|
-
source:
|
|
2657
|
+
source: path12.join("assets", "icon.js"),
|
|
2068
2658
|
width: 1024
|
|
2069
2659
|
}
|
|
2070
2660
|
];
|
|
2071
|
-
|
|
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/${
|
|
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 =
|
|
2105
|
-
const missing = SCENES.filter((scene) => !
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
2182
|
-
|
|
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:
|
|
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
|
|
2205
|
-
import
|
|
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 ?
|
|
2222
|
-
|
|
2223
|
-
|
|
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,
|
|
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
|
-
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
|
3138
|
-
import
|
|
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
|
-
|
|
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
|
|
3743
|
+
await sleep4(500);
|
|
3160
3744
|
}
|
|
3161
3745
|
};
|
|
3162
3746
|
collectLive = async (options) => {
|
|
3163
|
-
const 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 =
|
|
3174
|
-
|
|
3175
|
-
const
|
|
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
|
-
|
|
3195
|
-
screenshots.push(
|
|
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
|
|
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
|
|
3632
|
-
import
|
|
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 =
|
|
3652
|
-
const briefSource = readFileSync9(
|
|
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 =
|
|
3656
|
-
const artifact = options.artifactHtml ?? readFileSync9(
|
|
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 =
|
|
3660
|
-
return
|
|
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:
|
|
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:
|
|
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
|
|
3693
|
-
import
|
|
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) =>
|
|
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 =
|
|
3711
|
-
if (!
|
|
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(
|
|
3728
|
-
const iconDataUri = dataUri2(
|
|
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
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
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 =
|
|
3740
|
-
|
|
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
|
|
3868
|
-
import
|
|
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 =
|
|
3988
|
-
const briefPath =
|
|
3989
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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 && !
|
|
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 = (
|
|
4244
|
-
const out = run(`--getappblocked "${
|
|
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
|
|
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
|
|
5014
|
+
for (const name of readdirSync4(d)) {
|
|
4658
5015
|
if (name === ".DS_Store") continue;
|
|
4659
5016
|
const p = join(d, name);
|
|
4660
|
-
if (
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
|
5092
|
+
return existsSync14(opts.dir) ? {
|
|
4736
5093
|
dir: opts.dir,
|
|
4737
5094
|
agent: agentFromDir(opts.dir),
|
|
4738
|
-
scope: scopeForDir(opts.dir, opts.home ??
|
|
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 ??
|
|
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) =>
|
|
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 (
|
|
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 (
|
|
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 (
|
|
5188
|
+
if (existsSync14(direct)) return JSON.parse(readFileSync13(direct, "utf8"));
|
|
4832
5189
|
const skillsRoot = source.endsWith("skills") ? source : join(source, "skills");
|
|
4833
|
-
if (
|
|
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.
|
|
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",
|