frogoe 0.5.5 → 0.7.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 +1202 -475
- package/dist/templates/_shared/AGENTS.md +17 -13
- package/dist/templates/_shared/CLAUDE.md +18 -14
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,8 @@ var init_templates = __esm({
|
|
|
25
25
|
].join("\n");
|
|
26
26
|
briefTemplate = `---
|
|
27
27
|
title: My Game
|
|
28
|
-
verb: tap # tap
|
|
28
|
+
verb: tap # tap|hold|steer|aim|swap|place|type|draw|idle \u2014 ONE word, the core action
|
|
29
|
+
session: blitz # blitz (default) | round | toy \u2014 how the run ends
|
|
29
30
|
mood: TODO \u2014 one phrase: "dawn uplift", "arcade cabinet at midnight"...
|
|
30
31
|
palette:
|
|
31
32
|
bg: "#101418" # TODO
|
|
@@ -384,7 +385,7 @@ var init_add2 = __esm({
|
|
|
384
385
|
});
|
|
385
386
|
|
|
386
387
|
// src/fetch-policy.ts
|
|
387
|
-
var DEFAULT_FETCH_POLICY, FetchUnavailableError, FetchNotServedError, sleep, fetchWithPolicy;
|
|
388
|
+
var DEFAULT_FETCH_POLICY, FetchUnavailableError, FetchNotServedError, sleep, fetchWithPolicy, fetchBufferWithPolicy;
|
|
388
389
|
var init_fetch_policy = __esm({
|
|
389
390
|
"src/fetch-policy.ts"() {
|
|
390
391
|
"use strict";
|
|
@@ -444,7 +445,50 @@ var init_fetch_policy = __esm({
|
|
|
444
445
|
} catch (error) {
|
|
445
446
|
const retryable = error instanceof FetchUnavailableError || !(error instanceof FetchNotServedError);
|
|
446
447
|
if (!retryable || attempt === policy.maxAttempts) {
|
|
447
|
-
|
|
448
|
+
if (error instanceof FetchUnavailableError || error instanceof FetchNotServedError) {
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
throw new FetchUnavailableError(url, error);
|
|
452
|
+
}
|
|
453
|
+
await sleep(Math.random() * policy.baseDelayMs * 2 ** (attempt - 1));
|
|
454
|
+
} finally {
|
|
455
|
+
clearTimeout(timer);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
throw new FetchUnavailableError(url, new Error("unreachable"));
|
|
459
|
+
};
|
|
460
|
+
fetchBufferWithPolicy = async (url, options) => {
|
|
461
|
+
const policy = { ...DEFAULT_FETCH_POLICY, ...options?.policy };
|
|
462
|
+
const doFetch = options?.fetchImpl ?? fetch;
|
|
463
|
+
const started = Date.now();
|
|
464
|
+
for (let attempt = 1; attempt <= policy.maxAttempts; attempt += 1) {
|
|
465
|
+
if (Date.now() - started > policy.maxElapsedMs) {
|
|
466
|
+
throw new FetchUnavailableError(url, new Error("wall-clock budget exceeded"));
|
|
467
|
+
}
|
|
468
|
+
const controller = new AbortController();
|
|
469
|
+
const timer = setTimeout(() => {
|
|
470
|
+
controller.abort();
|
|
471
|
+
}, policy.attemptTimeoutMs);
|
|
472
|
+
try {
|
|
473
|
+
const res = await doFetch(url, {
|
|
474
|
+
headers: options?.headers,
|
|
475
|
+
redirect: "follow",
|
|
476
|
+
signal: controller.signal
|
|
477
|
+
});
|
|
478
|
+
if (res.status >= 500) {
|
|
479
|
+
throw new FetchUnavailableError(url, new Error(`upstream ${res.status}`));
|
|
480
|
+
}
|
|
481
|
+
if (res.status >= 400) {
|
|
482
|
+
throw new FetchNotServedError(url, res.status);
|
|
483
|
+
}
|
|
484
|
+
return Buffer.from(await res.arrayBuffer());
|
|
485
|
+
} catch (error) {
|
|
486
|
+
const retryable = error instanceof FetchUnavailableError || !(error instanceof FetchNotServedError);
|
|
487
|
+
if (!retryable || attempt === policy.maxAttempts) {
|
|
488
|
+
if (error instanceof FetchUnavailableError || error instanceof FetchNotServedError) {
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
throw new FetchUnavailableError(url, error);
|
|
448
492
|
}
|
|
449
493
|
await sleep(Math.random() * policy.baseDelayMs * 2 ** (attempt - 1));
|
|
450
494
|
} finally {
|
|
@@ -456,15 +500,109 @@ var init_fetch_policy = __esm({
|
|
|
456
500
|
}
|
|
457
501
|
});
|
|
458
502
|
|
|
459
|
-
// src/
|
|
503
|
+
// src/net/font-proxy.ts
|
|
460
504
|
import { createHash } from "crypto";
|
|
461
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
505
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
462
506
|
import path3 from "path";
|
|
463
|
-
var
|
|
507
|
+
var ALLOWED_FONT_HOSTS, FONT_UA, UPSTREAM_TIMEOUT_MS, FONT_LINK_PATTERN, CSS_URL_PATTERN, proxyPathFor, PROXY_PATH_PREFIX, isLegacyCachedCss, rewriteFontLinks, allowedUrl, decodeProxyToken, rewriteCssFontRefs, typeFor, cacheFileFor, isBinaryEntry, CACHE_MAGIC, readCachedFont, writeCachedFont, proxyFontRequest;
|
|
508
|
+
var init_font_proxy = __esm({
|
|
509
|
+
"src/net/font-proxy.ts"() {
|
|
510
|
+
"use strict";
|
|
511
|
+
ALLOWED_FONT_HOSTS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
|
|
512
|
+
FONT_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36";
|
|
513
|
+
UPSTREAM_TIMEOUT_MS = 4e3;
|
|
514
|
+
FONT_LINK_PATTERN = /href="(https:\/\/(?:fonts\.googleapis\.com|fonts\.gstatic\.com)\/[^"]+)"/gu;
|
|
515
|
+
CSS_URL_PATTERN = /url\((https:\/\/[^)]+)\)/gu;
|
|
516
|
+
proxyPathFor = (url) => `/__frogoe/font/${Buffer.from(url, "utf-8").toString("base64url")}`;
|
|
517
|
+
PROXY_PATH_PREFIX = "/__frogoe/font/";
|
|
518
|
+
isLegacyCachedCss = (css) => css.includes(PROXY_PATH_PREFIX);
|
|
519
|
+
rewriteFontLinks = (html) => html.replaceAll(FONT_LINK_PATTERN, (_match, url) => `href="${proxyPathFor(url)}"`);
|
|
520
|
+
allowedUrl = (url) => {
|
|
521
|
+
try {
|
|
522
|
+
const parsed = new URL(url);
|
|
523
|
+
return parsed.protocol === "https:" && ALLOWED_FONT_HOSTS.has(parsed.hostname);
|
|
524
|
+
} catch {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
decodeProxyToken = (token) => {
|
|
529
|
+
const url = Buffer.from(token, "base64url").toString("utf-8");
|
|
530
|
+
return allowedUrl(url) ? url : null;
|
|
531
|
+
};
|
|
532
|
+
rewriteCssFontRefs = (css) => css.replaceAll(
|
|
533
|
+
CSS_URL_PATTERN,
|
|
534
|
+
(match, url) => allowedUrl(url) ? `url(${proxyPathFor(url)})` : match
|
|
535
|
+
);
|
|
536
|
+
typeFor = (file) => file.endsWith(".css") ? "text/css; charset=utf-8" : "font/woff2";
|
|
537
|
+
cacheFileFor = (cacheDir, url) => {
|
|
538
|
+
const ext = url.includes("css2?") || url.endsWith(".css") ? ".css" : ".woff2";
|
|
539
|
+
return path3.join(cacheDir, createHash("sha256").update(url).digest("hex") + ext);
|
|
540
|
+
};
|
|
541
|
+
isBinaryEntry = (file) => file.endsWith(".woff2");
|
|
542
|
+
CACHE_MAGIC = Buffer.from("FROGOEF1");
|
|
543
|
+
readCachedFont = (url, cacheDir) => {
|
|
544
|
+
const file = cacheFileFor(cacheDir, url);
|
|
545
|
+
if (!existsSync3(file)) return null;
|
|
546
|
+
const raw = readFileSync3(file);
|
|
547
|
+
if (isBinaryEntry(file)) {
|
|
548
|
+
if (raw.length < CACHE_MAGIC.length || !raw.subarray(0, CACHE_MAGIC.length).equals(CACHE_MAGIC)) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
return raw.subarray(CACHE_MAGIC.length);
|
|
552
|
+
}
|
|
553
|
+
return raw;
|
|
554
|
+
};
|
|
555
|
+
writeCachedFont = (url, cacheDir, body) => {
|
|
556
|
+
const file = cacheFileFor(cacheDir, url);
|
|
557
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
558
|
+
writeFileSync3(file, isBinaryEntry(file) ? Buffer.concat([CACHE_MAGIC, Buffer.from(body)]) : body);
|
|
559
|
+
return file;
|
|
560
|
+
};
|
|
561
|
+
proxyFontRequest = async (url, cacheDir, fetchImpl = fetch) => {
|
|
562
|
+
if (!allowedUrl(url)) {
|
|
563
|
+
return { body: "", contentType: "text/plain; charset=utf-8", fromCache: false, status: 403 };
|
|
564
|
+
}
|
|
565
|
+
const cachePath = cacheFileFor(cacheDir, url);
|
|
566
|
+
const isCss = cachePath.endsWith(".css");
|
|
567
|
+
const raw = readCachedFont(url, cacheDir);
|
|
568
|
+
if (raw !== null && !(isCss && isLegacyCachedCss(raw.toString("utf-8")))) {
|
|
569
|
+
const body = isCss ? rewriteCssFontRefs(raw.toString("utf-8")) : raw;
|
|
570
|
+
return { body, cachePath, contentType: typeFor(cachePath), fromCache: true, status: 200 };
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const res = await fetchImpl(url, {
|
|
574
|
+
headers: { "user-agent": FONT_UA },
|
|
575
|
+
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
|
|
576
|
+
});
|
|
577
|
+
if (!res.ok) {
|
|
578
|
+
return {
|
|
579
|
+
body: "",
|
|
580
|
+
contentType: typeFor(cachePath),
|
|
581
|
+
fromCache: false,
|
|
582
|
+
status: 502
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const rawBody = Buffer.from(await res.arrayBuffer());
|
|
586
|
+
writeCachedFont(url, cacheDir, rawBody);
|
|
587
|
+
const body = isCss ? rewriteCssFontRefs(rawBody.toString("utf-8")) : rawBody;
|
|
588
|
+
return { body, cachePath, contentType: typeFor(cachePath), fromCache: false, status: 200 };
|
|
589
|
+
} catch {
|
|
590
|
+
return { body: "", contentType: typeFor(cachePath), fromCache: false, status: 504 };
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// src/bundle.ts
|
|
597
|
+
import { createHash as createHash2 } from "crypto";
|
|
598
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
599
|
+
import path4 from "path";
|
|
600
|
+
var ALLOWED_HOSTS, PIN_EXEMPT_HOSTS, MIME, sha256, dataUri, assertAllowedRemote, cdnPlugin, importMapPlugin, inlineFontCss, localDataUri, LEAK_PATTERN, bundle;
|
|
464
601
|
var init_bundle = __esm({
|
|
465
602
|
"src/bundle.ts"() {
|
|
466
603
|
"use strict";
|
|
467
604
|
init_fetch_policy();
|
|
605
|
+
init_font_proxy();
|
|
468
606
|
ALLOWED_HOSTS = /* @__PURE__ */ new Set([
|
|
469
607
|
"cdn.jsdelivr.net",
|
|
470
608
|
"esm.sh",
|
|
@@ -478,7 +616,6 @@ var init_bundle = __esm({
|
|
|
478
616
|
"fonts.googleapis.test",
|
|
479
617
|
"fonts.gstatic.test"
|
|
480
618
|
]);
|
|
481
|
-
FONT_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36";
|
|
482
619
|
MIME = {
|
|
483
620
|
gif: "image/gif",
|
|
484
621
|
jpeg: "image/jpeg",
|
|
@@ -491,7 +628,7 @@ var init_bundle = __esm({
|
|
|
491
628
|
webp: "image/webp",
|
|
492
629
|
woff2: "font/woff2"
|
|
493
630
|
};
|
|
494
|
-
sha256 = (data) =>
|
|
631
|
+
sha256 = (data) => createHash2("sha256").update(data).digest("hex");
|
|
495
632
|
dataUri = (mime, body) => {
|
|
496
633
|
const base64 = Buffer.from(body, "binary").toString("base64");
|
|
497
634
|
return `data:${mime};base64,${base64}`;
|
|
@@ -551,16 +688,34 @@ var init_bundle = __esm({
|
|
|
551
688
|
if (/^https?:\/\//.test(target)) {
|
|
552
689
|
return { namespace: "frogoe-cdn", path: target };
|
|
553
690
|
}
|
|
554
|
-
return { path:
|
|
691
|
+
return { path: path4.resolve(dir, target) };
|
|
555
692
|
});
|
|
556
693
|
}
|
|
557
694
|
}
|
|
558
695
|
});
|
|
559
696
|
inlineFontCss = async (cssUrl, options, assets) => {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
697
|
+
const cacheDir = path4.join(path4.resolve(options.dir), ".frogoe", "font-cache");
|
|
698
|
+
const fontFetchError = (url, cause) => new Error(
|
|
699
|
+
`bundle/font-unreachable \u2014 ${url} (${cause instanceof Error ? cause.message : String(cause)})
|
|
700
|
+
No local cache for it and the network failed \u2014 retry when the network recovers,
|
|
701
|
+
or run \`frogoe check <game>\` once first: it warms the shared font cache the bundler reads.`,
|
|
702
|
+
{ cause: cause instanceof Error ? cause : void 0 }
|
|
703
|
+
);
|
|
704
|
+
let css;
|
|
705
|
+
const cachedCss = readCachedFont(cssUrl, cacheDir);
|
|
706
|
+
if (cachedCss !== null) {
|
|
707
|
+
css = cachedCss.toString("utf-8");
|
|
708
|
+
} else {
|
|
709
|
+
try {
|
|
710
|
+
css = await fetchWithPolicy(cssUrl, {
|
|
711
|
+
fetchImpl: options.fetchImpl,
|
|
712
|
+
headers: { "user-agent": FONT_UA }
|
|
713
|
+
});
|
|
714
|
+
} catch (error) {
|
|
715
|
+
throw fontFetchError(cssUrl, error);
|
|
716
|
+
}
|
|
717
|
+
writeCachedFont(cssUrl, cacheDir, css);
|
|
718
|
+
}
|
|
564
719
|
assets.push({
|
|
565
720
|
bytes: css.length,
|
|
566
721
|
kind: "css",
|
|
@@ -574,8 +729,21 @@ var init_bundle = __esm({
|
|
|
574
729
|
return cached2;
|
|
575
730
|
}
|
|
576
731
|
assertAllowedRemote(url, options.extraAllowedHosts);
|
|
577
|
-
|
|
578
|
-
const
|
|
732
|
+
let buffer;
|
|
733
|
+
const raw2 = readCachedFont(url, cacheDir);
|
|
734
|
+
if (raw2 !== null) {
|
|
735
|
+
buffer = raw2;
|
|
736
|
+
} else {
|
|
737
|
+
try {
|
|
738
|
+
buffer = await fetchBufferWithPolicy(url, {
|
|
739
|
+
fetchImpl: options.fetchImpl,
|
|
740
|
+
headers: { "user-agent": FONT_UA }
|
|
741
|
+
});
|
|
742
|
+
writeCachedFont(url, cacheDir, buffer);
|
|
743
|
+
} catch (error) {
|
|
744
|
+
throw fontFetchError(url, error);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
579
747
|
const uri = `data:font/woff2;base64,${buffer.toString("base64")}`;
|
|
580
748
|
seen.set(url, uri);
|
|
581
749
|
assets.push({
|
|
@@ -586,6 +754,18 @@ var init_bundle = __esm({
|
|
|
586
754
|
});
|
|
587
755
|
return uri;
|
|
588
756
|
};
|
|
757
|
+
let raw = css;
|
|
758
|
+
for (const match of css.matchAll(/url\((\/__frogoe\/font\/[A-Za-z0-9_-]+)\)/gu)) {
|
|
759
|
+
const ref = match[1] ?? "";
|
|
760
|
+
const decoded = ref === "" ? null : decodeProxyToken(ref.slice(PROXY_PATH_PREFIX.length));
|
|
761
|
+
if (decoded !== null) {
|
|
762
|
+
raw = raw.replaceAll(ref, decoded);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (raw !== css) {
|
|
766
|
+
writeCachedFont(cssUrl, cacheDir, raw);
|
|
767
|
+
css = raw;
|
|
768
|
+
}
|
|
589
769
|
let out = css;
|
|
590
770
|
const urls = [...css.matchAll(/url\((https:\/\/[^)]+)\)/gu)].map((m) => m[1] ?? "");
|
|
591
771
|
for (const url of urls) {
|
|
@@ -596,25 +776,25 @@ var init_bundle = __esm({
|
|
|
596
776
|
return out;
|
|
597
777
|
};
|
|
598
778
|
localDataUri = (dir, href) => {
|
|
599
|
-
const file =
|
|
600
|
-
if (!
|
|
779
|
+
const file = path4.resolve(dir, href.replace(/^\.\//u, ""));
|
|
780
|
+
if (!existsSync4(file)) {
|
|
601
781
|
throw new Error(`bundle/missing-asset: ${href} not found in the game folder`);
|
|
602
782
|
}
|
|
603
|
-
const ext =
|
|
783
|
+
const ext = path4.extname(file).slice(1).toLowerCase();
|
|
604
784
|
const mime = MIME[ext];
|
|
605
785
|
if (!mime) {
|
|
606
786
|
throw new Error(`bundle/unsupported-asset: .${ext} (${href})`);
|
|
607
787
|
}
|
|
608
|
-
return dataUri(mime,
|
|
788
|
+
return dataUri(mime, readFileSync4(file, "binary"));
|
|
609
789
|
};
|
|
610
790
|
LEAK_PATTERN = /(?:src|href)\s*=\s*"(https?:\/\/[^"]+)"/gi;
|
|
611
791
|
bundle = async (options) => {
|
|
612
|
-
const dir =
|
|
613
|
-
const indexPath =
|
|
614
|
-
if (!
|
|
792
|
+
const dir = path4.resolve(options.dir);
|
|
793
|
+
const indexPath = path4.join(dir, "index.html");
|
|
794
|
+
if (!existsSync4(indexPath)) {
|
|
615
795
|
throw new Error("frogoe bundle: no index.html \u2014 run from a game folder");
|
|
616
796
|
}
|
|
617
|
-
const html =
|
|
797
|
+
const html = readFileSync4(indexPath, "utf-8");
|
|
618
798
|
const assets = [];
|
|
619
799
|
const warnings = [];
|
|
620
800
|
let out = html;
|
|
@@ -633,7 +813,7 @@ var init_bundle = __esm({
|
|
|
633
813
|
const { build } = await import("esbuild");
|
|
634
814
|
const result = await build({
|
|
635
815
|
bundle: true,
|
|
636
|
-
entryPoints: [
|
|
816
|
+
entryPoints: [path4.resolve(dir, entryMatch[1])],
|
|
637
817
|
format: "esm",
|
|
638
818
|
logLevel: "silent",
|
|
639
819
|
plugins: [importMapPlugin(dir, imports), cdnPlugin(options, assets)],
|
|
@@ -661,7 +841,7 @@ ${bundled}
|
|
|
661
841
|
${css}
|
|
662
842
|
</style>`;
|
|
663
843
|
} else {
|
|
664
|
-
const css =
|
|
844
|
+
const css = readFileSync4(path4.resolve(dir, href), "utf-8");
|
|
665
845
|
replacement = `<style>
|
|
666
846
|
${css}
|
|
667
847
|
</style>`;
|
|
@@ -676,7 +856,7 @@ ${css}
|
|
|
676
856
|
if (href.endsWith(".js") && entryMatch?.[1] === href) {
|
|
677
857
|
continue;
|
|
678
858
|
}
|
|
679
|
-
if (
|
|
859
|
+
if (existsSync4(path4.resolve(dir, href))) {
|
|
680
860
|
out = out.replaceAll(href, localDataUri(dir, href));
|
|
681
861
|
}
|
|
682
862
|
}
|
|
@@ -708,10 +888,22 @@ ${css}
|
|
|
708
888
|
});
|
|
709
889
|
|
|
710
890
|
// ../lint/src/brief.ts
|
|
711
|
-
var KEY_PATTERN, WS, stripComment, parseLine, parseBrief;
|
|
891
|
+
var VERBS, SESSIONS, KEY_PATTERN, WS, stripComment, parseLine, parseBrief;
|
|
712
892
|
var init_brief = __esm({
|
|
713
893
|
"../lint/src/brief.ts"() {
|
|
714
894
|
"use strict";
|
|
895
|
+
VERBS = [
|
|
896
|
+
"aim",
|
|
897
|
+
"draw",
|
|
898
|
+
"hold",
|
|
899
|
+
"idle",
|
|
900
|
+
"place",
|
|
901
|
+
"steer",
|
|
902
|
+
"swap",
|
|
903
|
+
"tap",
|
|
904
|
+
"type"
|
|
905
|
+
];
|
|
906
|
+
SESSIONS = ["blitz", "round", "toy"];
|
|
715
907
|
KEY_PATTERN = /^[a-z-]+$/u;
|
|
716
908
|
WS = /\s/u;
|
|
717
909
|
stripComment = (value) => {
|
|
@@ -767,8 +959,8 @@ var init_brief = __esm({
|
|
|
767
959
|
});
|
|
768
960
|
|
|
769
961
|
// ../lint/src/art.ts
|
|
770
|
-
import { existsSync as
|
|
771
|
-
import
|
|
962
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
963
|
+
import path5 from "path";
|
|
772
964
|
var SPECS, MIN_DRAW_CALLS, DRAW_CALL, SPRITE_CALL, checkArt, checkScene, hexColorsOf, stripComments;
|
|
773
965
|
var init_art = __esm({
|
|
774
966
|
"../lint/src/art.ts"() {
|
|
@@ -782,16 +974,16 @@ var init_art = __esm({
|
|
|
782
974
|
DRAW_CALL = /(?:fillRect|strokeRect|clearRect|beginPath|closePath|arc|ellipse|moveTo|lineTo|quadraticCurveTo|bezierCurveTo|fillText|strokeText|drawImage|roundRect|fill|stroke)\s*\(/gu;
|
|
783
975
|
SPRITE_CALL = /\bdraw[A-Z]\w*\s*\(/gu;
|
|
784
976
|
checkArt = (dir, findings) => {
|
|
785
|
-
const briefPath =
|
|
786
|
-
const brief =
|
|
977
|
+
const briefPath = path5.join(dir, "BRIEF.md");
|
|
978
|
+
const brief = existsSync5(briefPath) ? parseBrief(readFileSync5(briefPath, "utf-8")) : void 0;
|
|
787
979
|
const briefFonts = (brief?.fonts ?? "").split(",").map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0);
|
|
788
|
-
const gamePath =
|
|
789
|
-
const gameSource =
|
|
980
|
+
const gamePath = path5.join(dir, "game.js");
|
|
981
|
+
const gameSource = existsSync5(gamePath) ? readFileSync5(gamePath, "utf-8") : "";
|
|
790
982
|
const gameColors = hexColorsOf(stripComments(gameSource));
|
|
791
|
-
const htmlSource =
|
|
983
|
+
const htmlSource = existsSync5(path5.join(dir, "index.html")) ? readFileSync5(path5.join(dir, "index.html"), "utf-8") : "";
|
|
792
984
|
for (const spec of SPECS) {
|
|
793
|
-
const scenePath =
|
|
794
|
-
if (!
|
|
985
|
+
const scenePath = path5.join(dir, spec.file);
|
|
986
|
+
if (!existsSync5(scenePath)) {
|
|
795
987
|
findings.push({
|
|
796
988
|
code: "art/missing",
|
|
797
989
|
file: spec.file,
|
|
@@ -802,7 +994,7 @@ var init_art = __esm({
|
|
|
802
994
|
});
|
|
803
995
|
continue;
|
|
804
996
|
}
|
|
805
|
-
const source = stripComments(
|
|
997
|
+
const source = stripComments(readFileSync5(scenePath, "utf-8"));
|
|
806
998
|
checkScene(source, spec, gameColors, briefFonts, htmlSource, findings);
|
|
807
999
|
}
|
|
808
1000
|
};
|
|
@@ -963,19 +1155,47 @@ var init_contrast = __esm({
|
|
|
963
1155
|
});
|
|
964
1156
|
|
|
965
1157
|
// ../lint/src/check.ts
|
|
966
|
-
import { existsSync as
|
|
967
|
-
import
|
|
968
|
-
var
|
|
1158
|
+
import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync6 } from "fs";
|
|
1159
|
+
import path6 from "path";
|
|
1160
|
+
var HANDLER_PATTERNS, VERB_REQUIREMENTS, read, linesOf, findLine, checkBrief, checkFolder, checkPin, checkProject;
|
|
969
1161
|
var init_check = __esm({
|
|
970
1162
|
"../lint/src/check.ts"() {
|
|
971
1163
|
"use strict";
|
|
972
1164
|
init_brief();
|
|
973
1165
|
init_contrast();
|
|
974
1166
|
init_art();
|
|
975
|
-
|
|
1167
|
+
HANDLER_PATTERNS = {
|
|
1168
|
+
down: /input\.on\(\s*["']down["']/u,
|
|
1169
|
+
drag: /input\.on\(\s*["']drag["']/u,
|
|
1170
|
+
up: /input\.on\(\s*["']up["']/u
|
|
1171
|
+
};
|
|
1172
|
+
VERB_REQUIREMENTS = {
|
|
1173
|
+
aim: [
|
|
1174
|
+
{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down },
|
|
1175
|
+
{ label: 'input.on("drag"', pattern: HANDLER_PATTERNS.drag }
|
|
1176
|
+
],
|
|
1177
|
+
draw: [
|
|
1178
|
+
{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down },
|
|
1179
|
+
{ label: 'input.on("drag"', pattern: HANDLER_PATTERNS.drag },
|
|
1180
|
+
{ label: 'input.on("up"', pattern: HANDLER_PATTERNS.up }
|
|
1181
|
+
],
|
|
1182
|
+
hold: [
|
|
1183
|
+
{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down },
|
|
1184
|
+
{ label: 'input.on("up"', pattern: HANDLER_PATTERNS.up }
|
|
1185
|
+
],
|
|
1186
|
+
idle: [{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down }],
|
|
1187
|
+
place: [{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down }],
|
|
1188
|
+
steer: [
|
|
1189
|
+
{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down },
|
|
1190
|
+
{ label: 'input.on("drag"', pattern: HANDLER_PATTERNS.drag }
|
|
1191
|
+
],
|
|
1192
|
+
swap: [{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down }],
|
|
1193
|
+
tap: [{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down }],
|
|
1194
|
+
type: [{ label: 'input.on("down"', pattern: HANDLER_PATTERNS.down }]
|
|
1195
|
+
};
|
|
976
1196
|
read = (file) => {
|
|
977
1197
|
try {
|
|
978
|
-
return
|
|
1198
|
+
return readFileSync6(file, "utf-8");
|
|
979
1199
|
} catch {
|
|
980
1200
|
return "";
|
|
981
1201
|
}
|
|
@@ -986,8 +1206,8 @@ var init_check = __esm({
|
|
|
986
1206
|
return idx === -1 ? void 0 : idx + 1;
|
|
987
1207
|
};
|
|
988
1208
|
checkBrief = (dir, findings) => {
|
|
989
|
-
const file =
|
|
990
|
-
if (!
|
|
1209
|
+
const file = path6.join(dir, "BRIEF.md");
|
|
1210
|
+
if (!existsSync6(file)) {
|
|
991
1211
|
findings.push({
|
|
992
1212
|
code: "brief/missing",
|
|
993
1213
|
file: "BRIEF.md",
|
|
@@ -995,7 +1215,7 @@ var init_check = __esm({
|
|
|
995
1215
|
message: "feed games declare intent before code",
|
|
996
1216
|
severity: "error"
|
|
997
1217
|
});
|
|
998
|
-
return;
|
|
1218
|
+
return null;
|
|
999
1219
|
}
|
|
1000
1220
|
const source = read(file);
|
|
1001
1221
|
const todoLine = findLine(source, /TODO/u);
|
|
@@ -1018,14 +1238,32 @@ var init_check = __esm({
|
|
|
1018
1238
|
message: "no frontmatter block found",
|
|
1019
1239
|
severity: "error"
|
|
1020
1240
|
});
|
|
1021
|
-
return;
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
if (brief.verb !== void 0 && !VERBS.includes(brief.verb)) {
|
|
1244
|
+
findings.push({
|
|
1245
|
+
code: "brief/verb",
|
|
1246
|
+
file: "BRIEF.md",
|
|
1247
|
+
fix: `verb "${brief.verb}" is not in the enum (${VERBS.join("|")}) \u2014 pick the ONE word that names the core action (frogoe-core \u2192 brief-format)`,
|
|
1248
|
+
message: "unknown core verb",
|
|
1249
|
+
severity: "error"
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
if (brief.session !== void 0 && !SESSIONS.includes(brief.session)) {
|
|
1253
|
+
findings.push({
|
|
1254
|
+
code: "brief/session",
|
|
1255
|
+
file: "BRIEF.md",
|
|
1256
|
+
fix: `session "${brief.session}" is not in the enum (${SESSIONS.join("|")}) \u2014 blitz is the short arcade loop (default), round is turn-based, toy never ends (frogoe-core \u2192 brief-format)`,
|
|
1257
|
+
message: "unknown session shape",
|
|
1258
|
+
severity: "error"
|
|
1259
|
+
});
|
|
1022
1260
|
}
|
|
1023
1261
|
const problems = [];
|
|
1024
1262
|
if (!brief.title || brief.title.length < 2 || brief.title.length > 40) {
|
|
1025
1263
|
problems.push("title (2\u201340 chars)");
|
|
1026
1264
|
}
|
|
1027
|
-
if (!brief.verb || !VERBS.
|
|
1028
|
-
problems.push(
|
|
1265
|
+
if (!brief.verb || !VERBS.includes(brief.verb)) {
|
|
1266
|
+
problems.push(`verb (${VERBS.join("|")})`);
|
|
1029
1267
|
}
|
|
1030
1268
|
if (!brief.mood) {
|
|
1031
1269
|
problems.push("mood (one phrase)");
|
|
@@ -1068,9 +1306,10 @@ var init_check = __esm({
|
|
|
1068
1306
|
});
|
|
1069
1307
|
}
|
|
1070
1308
|
}
|
|
1309
|
+
return brief;
|
|
1071
1310
|
};
|
|
1072
|
-
checkFolder = (dir, findings) => {
|
|
1073
|
-
const index = read(
|
|
1311
|
+
checkFolder = (dir, findings, brief) => {
|
|
1312
|
+
const index = read(path6.join(dir, "index.html"));
|
|
1074
1313
|
if (!index) {
|
|
1075
1314
|
findings.push({
|
|
1076
1315
|
code: "folder/index-missing",
|
|
@@ -1140,7 +1379,7 @@ var init_check = __esm({
|
|
|
1140
1379
|
});
|
|
1141
1380
|
}
|
|
1142
1381
|
}
|
|
1143
|
-
const game = read(
|
|
1382
|
+
const game = read(path6.join(dir, "game.js"));
|
|
1144
1383
|
if (!game) {
|
|
1145
1384
|
findings.push({
|
|
1146
1385
|
code: "folder/game-missing",
|
|
@@ -1169,6 +1408,21 @@ var init_check = __esm({
|
|
|
1169
1408
|
severity: "warning"
|
|
1170
1409
|
});
|
|
1171
1410
|
}
|
|
1411
|
+
const requirements = brief?.verb !== void 0 ? VERB_REQUIREMENTS[brief.verb] : void 0;
|
|
1412
|
+
if (requirements !== void 0) {
|
|
1413
|
+
const missing = requirements.filter((req) => !req.pattern.test(game));
|
|
1414
|
+
if (missing.length > 0) {
|
|
1415
|
+
findings.push({
|
|
1416
|
+
code: "input/verb-mismatch",
|
|
1417
|
+
file: "game.js",
|
|
1418
|
+
fix: `verb "${String(brief?.verb)}" requires ${missing.map((m) => m.label).join(" + ")} \u2014 wire it inside defineGame (frogoe-core \u2192 brief-format, verb table)`,
|
|
1419
|
+
line: findLine(game, HANDLER_PATTERNS.down) ?? void 0,
|
|
1420
|
+
message: `declared verb "${String(brief?.verb)}" but its input wiring is missing`,
|
|
1421
|
+
recipe: "frogoe-core \u2192 brief-format (verb \u2192 handler table)",
|
|
1422
|
+
severity: "error"
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1172
1426
|
const gameLine = (pattern) => findLine(game, pattern);
|
|
1173
1427
|
const dragLine = gameLine(/\+=\s*(?:p|pt|pointer)\.(?:dx|dy)\b/u);
|
|
1174
1428
|
if (dragLine !== void 0) {
|
|
@@ -1218,12 +1472,12 @@ var init_check = __esm({
|
|
|
1218
1472
|
severity: "warning"
|
|
1219
1473
|
});
|
|
1220
1474
|
}
|
|
1221
|
-
const blocksDir =
|
|
1475
|
+
const blocksDir = path6.join(dir, "blocks");
|
|
1222
1476
|
let markup = index;
|
|
1223
|
-
if (
|
|
1477
|
+
if (existsSync6(blocksDir)) {
|
|
1224
1478
|
for (const f of readdirSync(blocksDir)) {
|
|
1225
1479
|
if (f.endsWith(".html")) {
|
|
1226
|
-
markup += read(
|
|
1480
|
+
markup += read(path6.join(blocksDir, f));
|
|
1227
1481
|
}
|
|
1228
1482
|
}
|
|
1229
1483
|
}
|
|
@@ -1243,8 +1497,8 @@ var init_check = __esm({
|
|
|
1243
1497
|
}
|
|
1244
1498
|
};
|
|
1245
1499
|
checkPin = (dir, findings) => {
|
|
1246
|
-
const pinFile =
|
|
1247
|
-
if (!
|
|
1500
|
+
const pinFile = path6.join(dir, "frogoe.json");
|
|
1501
|
+
if (!existsSync6(pinFile)) {
|
|
1248
1502
|
findings.push({
|
|
1249
1503
|
code: "folder/contract-pin",
|
|
1250
1504
|
file: "frogoe.json",
|
|
@@ -1259,7 +1513,7 @@ var init_check = __esm({
|
|
|
1259
1513
|
pin = String(JSON.parse(read(pinFile)).contract ?? "");
|
|
1260
1514
|
} catch {
|
|
1261
1515
|
}
|
|
1262
|
-
const contract = read(
|
|
1516
|
+
const contract = read(path6.join(dir, ".frogoe/contract.js"));
|
|
1263
1517
|
const marker = /frogoe contract v([\d.]+)/u.exec(contract.slice(0, 400));
|
|
1264
1518
|
if (!contract) {
|
|
1265
1519
|
findings.push({
|
|
@@ -1282,8 +1536,8 @@ var init_check = __esm({
|
|
|
1282
1536
|
checkProject = (dir) => {
|
|
1283
1537
|
const findings = [];
|
|
1284
1538
|
checkArt(dir, findings);
|
|
1285
|
-
checkBrief(dir, findings);
|
|
1286
|
-
checkFolder(dir, findings);
|
|
1539
|
+
const brief = checkBrief(dir, findings);
|
|
1540
|
+
checkFolder(dir, findings, brief);
|
|
1287
1541
|
checkPin(dir, findings);
|
|
1288
1542
|
findings.sort((a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0));
|
|
1289
1543
|
return {
|
|
@@ -1307,28 +1561,28 @@ var init_src = __esm({
|
|
|
1307
1561
|
});
|
|
1308
1562
|
|
|
1309
1563
|
// src/runtime-source.ts
|
|
1310
|
-
import { readFileSync as
|
|
1311
|
-
import
|
|
1564
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1565
|
+
import path7 from "path";
|
|
1312
1566
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1313
1567
|
var here2, cached, runtimeSource, functions, runtimeFunctions;
|
|
1314
1568
|
var init_runtime_source = __esm({
|
|
1315
1569
|
"src/runtime-source.ts"() {
|
|
1316
1570
|
"use strict";
|
|
1317
|
-
here2 =
|
|
1571
|
+
here2 = path7.dirname(fileURLToPath2(import.meta.url));
|
|
1318
1572
|
cached = null;
|
|
1319
1573
|
runtimeSource = () => {
|
|
1320
1574
|
if (cached !== null) return cached;
|
|
1321
1575
|
const candidates = [
|
|
1322
|
-
|
|
1576
|
+
path7.join(here2, "injected-runtime.js"),
|
|
1323
1577
|
// source mode (src/ dir)
|
|
1324
|
-
|
|
1578
|
+
path7.join(here2, "dist", "injected-runtime.js"),
|
|
1325
1579
|
// dist mode
|
|
1326
|
-
|
|
1580
|
+
path7.join(here2, "..", "dist", "injected-runtime.js")
|
|
1327
1581
|
// dist/cli.js sibling
|
|
1328
1582
|
];
|
|
1329
1583
|
for (const p of candidates) {
|
|
1330
1584
|
try {
|
|
1331
|
-
cached =
|
|
1585
|
+
cached = readFileSync7(p, "utf-8");
|
|
1332
1586
|
return cached;
|
|
1333
1587
|
} catch {
|
|
1334
1588
|
}
|
|
@@ -1586,34 +1840,623 @@ var init_art_verify = __esm({
|
|
|
1586
1840
|
}
|
|
1587
1841
|
});
|
|
1588
1842
|
|
|
1589
|
-
// src/
|
|
1590
|
-
import
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1843
|
+
// src/net/tunnel.ts
|
|
1844
|
+
import { spawn, spawnSync } from "child_process";
|
|
1845
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, chmodSync, writeFileSync as writeFileSync4 } from "fs";
|
|
1846
|
+
import os from "os";
|
|
1847
|
+
import path8 from "path";
|
|
1848
|
+
import { gunzipSync } from "zlib";
|
|
1849
|
+
var URL_PATTERN, parseTunnelUrl, TAG_PATTERN, assertSafeTag, octalAt, extractSingleFile, ENV_PIN, assetName, binaryFileName, cacheBase, resolveLatestTag, mb, downloadWithProgress, binaryEchoes, resolveBinary, startTunnel;
|
|
1850
|
+
var init_tunnel = __esm({
|
|
1851
|
+
"src/net/tunnel.ts"() {
|
|
1594
1852
|
"use strict";
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1853
|
+
URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/iu;
|
|
1854
|
+
parseTunnelUrl = (chunk) => URL_PATTERN.exec(chunk)?.[0];
|
|
1855
|
+
TAG_PATTERN = /^\d{4}\.\d+\.\d+(-[A-Za-z0-9.]+)?$/u;
|
|
1856
|
+
assertSafeTag = (tag) => {
|
|
1857
|
+
if (!TAG_PATTERN.test(tag)) {
|
|
1858
|
+
throw new Error(
|
|
1859
|
+
`cloudflared version "${tag}" is not a valid release tag (expected e.g. 2025.10.1)`
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
return tag;
|
|
1863
|
+
};
|
|
1864
|
+
octalAt = (block, offset, length) => {
|
|
1865
|
+
const raw = block.subarray(offset, offset + length).toString("utf-8");
|
|
1866
|
+
const digits = raw.replace(/[\0 ]/gu, "");
|
|
1867
|
+
return digits.length === 0 ? 0 : Number.parseInt(digits, 8);
|
|
1868
|
+
};
|
|
1869
|
+
extractSingleFile = (tar, wanted) => {
|
|
1870
|
+
for (let offset = 0; offset + 512 <= tar.length; ) {
|
|
1871
|
+
const header = tar.subarray(offset, offset + 512);
|
|
1872
|
+
const name = header.subarray(0, 100).toString("utf-8").replace(/\0.*$/u, "");
|
|
1873
|
+
const size = octalAt(header, 124, 12);
|
|
1874
|
+
const type = header.toString("utf-8").charCodeAt(156);
|
|
1875
|
+
const dataStart = offset + 512;
|
|
1876
|
+
const dataEnd = dataStart + size;
|
|
1877
|
+
if (dataEnd > tar.length) return null;
|
|
1878
|
+
if (name === wanted && (type === 48 || type === 0)) {
|
|
1879
|
+
return tar.subarray(dataStart, dataEnd);
|
|
1880
|
+
}
|
|
1881
|
+
offset = dataStart + Math.ceil(size / 512) * 512;
|
|
1882
|
+
}
|
|
1883
|
+
return null;
|
|
1884
|
+
};
|
|
1885
|
+
ENV_PIN = "FROGOE_CLOUDFLARED_VERSION";
|
|
1886
|
+
assetName = (platform, arch) => {
|
|
1887
|
+
const a = arch === "arm64" ? "arm64" : arch === "x64" ? "amd64" : arch === "ia32" ? "386" : null;
|
|
1888
|
+
if (!a) return null;
|
|
1889
|
+
switch (platform) {
|
|
1890
|
+
case "darwin":
|
|
1891
|
+
return a === "386" ? null : `cloudflared-darwin-${a}.tgz`;
|
|
1892
|
+
case "linux":
|
|
1893
|
+
return `cloudflared-linux-${a}`;
|
|
1894
|
+
// bare binaries
|
|
1895
|
+
case "win32":
|
|
1896
|
+
return a === "arm64" ? null : `cloudflared-windows-${a}.exe`;
|
|
1897
|
+
// bare executables
|
|
1898
|
+
default:
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
binaryFileName = (platform) => platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
1903
|
+
cacheBase = (platform, env, home) => {
|
|
1904
|
+
if (platform === "darwin") return path8.join(home, "Library", "Caches");
|
|
1905
|
+
if (platform === "win32") return env.LOCALAPPDATA ?? path8.join(home, "AppData", "Local");
|
|
1906
|
+
return path8.join(home, ".cache");
|
|
1907
|
+
};
|
|
1908
|
+
resolveLatestTag = async () => {
|
|
1909
|
+
const res = await fetch("https://api.github.com/repos/cloudflare/cloudflared/releases/latest", {
|
|
1910
|
+
headers: { accept: "application/vnd.github+json" }
|
|
1911
|
+
});
|
|
1912
|
+
if (!res.ok) throw new Error(`github api ${res.status}`);
|
|
1913
|
+
const body = await res.json();
|
|
1914
|
+
if (!body.tag_name) throw new Error("github api returned no tag_name");
|
|
1915
|
+
return body.tag_name;
|
|
1916
|
+
};
|
|
1917
|
+
mb = (bytes) => (bytes / (1024 * 1024)).toFixed(0);
|
|
1918
|
+
downloadWithProgress = async (res, onChunk) => {
|
|
1919
|
+
const total = Number(res.headers.get("content-length") ?? 0);
|
|
1920
|
+
const reader = res.body?.getReader();
|
|
1921
|
+
if (!reader) return Buffer.from(await res.arrayBuffer());
|
|
1922
|
+
const chunks = [];
|
|
1923
|
+
let done = 0;
|
|
1924
|
+
let nextReport = 0;
|
|
1925
|
+
for (; ; ) {
|
|
1926
|
+
const step = await reader.read();
|
|
1927
|
+
if (step.done) break;
|
|
1928
|
+
chunks.push(Buffer.from(step.value));
|
|
1929
|
+
done += step.value.byteLength;
|
|
1930
|
+
if (done >= nextReport) {
|
|
1931
|
+
onChunk(done, total);
|
|
1932
|
+
nextReport = done + 5 * 1024 * 1024;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return Buffer.concat(chunks);
|
|
1936
|
+
};
|
|
1937
|
+
binaryEchoes = (bin, tag) => {
|
|
1938
|
+
try {
|
|
1939
|
+
const probe = spawnSync(bin, ["--version"], { encoding: "utf-8", timeout: 1e4 });
|
|
1940
|
+
const out = `${probe.stdout ?? ""}${probe.stderr ?? ""}`;
|
|
1941
|
+
return probe.status === 0 && out.includes(tag);
|
|
1942
|
+
} catch {
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
resolveBinary = async (onProgress) => {
|
|
1947
|
+
const pathProbe = spawnSync("cloudflared", ["--version"], { stdio: "ignore", timeout: 5e3 });
|
|
1948
|
+
if (pathProbe.status === 0) return { downloaded: false, path: "cloudflared" };
|
|
1949
|
+
const platform = process.platform;
|
|
1950
|
+
const asset = assetName(platform, process.arch);
|
|
1951
|
+
if (!asset) {
|
|
1952
|
+
throw new Error(
|
|
1953
|
+
`cloudflared publishes no build for ${platform}/${process.arch} \u2014 install it and put \`cloudflared\` on PATH`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
const tag = process.env[ENV_PIN] ? assertSafeTag(process.env[ENV_PIN]) : assertSafeTag(await resolveLatestTag());
|
|
1957
|
+
const root = path8.join(cacheBase(platform, process.env, os.homedir()), "frogoe", "cloudflared");
|
|
1958
|
+
const dir = path8.join(root, tag);
|
|
1959
|
+
const bin = path8.join(dir, binaryFileName(platform));
|
|
1960
|
+
const rootResolved = path8.resolve(root);
|
|
1961
|
+
const binResolved = path8.resolve(bin);
|
|
1962
|
+
if (!binResolved.startsWith(rootResolved + path8.sep)) {
|
|
1963
|
+
throw new Error("cloudflared binary path escaped the frogoe cache \u2014 refusing to execute");
|
|
1964
|
+
}
|
|
1965
|
+
if (existsSync7(bin) && binaryEchoes(bin, tag)) return { downloaded: false, path: bin };
|
|
1966
|
+
onProgress?.(`downloading cloudflared ${tag} (~25 MB, once)\u2026`);
|
|
1967
|
+
const res = await fetch(
|
|
1968
|
+
`https://github.com/cloudflare/cloudflared/releases/download/${tag}/${asset}`
|
|
1969
|
+
);
|
|
1970
|
+
if (!res.ok) throw new Error(`cloudflared ${tag} download failed (${res.status})`);
|
|
1971
|
+
const raw = await downloadWithProgress(res, (done, total) => {
|
|
1972
|
+
onProgress?.(`downloading cloudflared ${tag} \u2014 ${mb(done)}${total ? `/${mb(total)}` : ""} MB`);
|
|
1973
|
+
});
|
|
1974
|
+
const binary = asset.endsWith(".tgz") ? extractSingleFile(gunzipSync(raw), "cloudflared") : raw;
|
|
1975
|
+
if (!binary) throw new Error("cloudflared archive did not contain the binary");
|
|
1976
|
+
mkdirSync4(dir, { recursive: true });
|
|
1977
|
+
writeFileSync4(bin, binary);
|
|
1978
|
+
if (platform !== "win32") chmodSync(bin, 493);
|
|
1979
|
+
if (!binaryEchoes(bin, tag)) {
|
|
1980
|
+
throw new Error(
|
|
1981
|
+
`cloudflared ${tag} failed its version check \u2014 deleted; set ${ENV_PIN} or install via brew`
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
return { downloaded: true, path: bin };
|
|
1985
|
+
};
|
|
1986
|
+
startTunnel = async (port, options) => {
|
|
1987
|
+
const timeoutMs = options?.timeoutMs ?? 2e4;
|
|
1988
|
+
const bin = await resolveBinary(options?.onProgress);
|
|
1989
|
+
return new Promise((resolve2, reject) => {
|
|
1990
|
+
const child = spawn(
|
|
1991
|
+
bin.path,
|
|
1992
|
+
["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"],
|
|
1993
|
+
{
|
|
1994
|
+
detached: true,
|
|
1995
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1996
|
+
windowsHide: true
|
|
1997
|
+
}
|
|
1998
|
+
);
|
|
1999
|
+
let settled = false;
|
|
2000
|
+
let url;
|
|
2001
|
+
let buffer = "";
|
|
2002
|
+
const exited = new Promise((notify) => {
|
|
2003
|
+
child.once("exit", () => notify());
|
|
2004
|
+
});
|
|
2005
|
+
const killTree = () => {
|
|
2006
|
+
if (process.platform === "win32" || !child.pid) {
|
|
2007
|
+
child.kill("SIGTERM");
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
process.kill(-child.pid, "SIGTERM");
|
|
2012
|
+
} catch {
|
|
2013
|
+
child.kill("SIGTERM");
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
const finish = (error) => {
|
|
2017
|
+
if (settled) return;
|
|
2018
|
+
settled = true;
|
|
2019
|
+
clearTimeout(timer);
|
|
2020
|
+
if (error || !url) {
|
|
2021
|
+
killTree();
|
|
2022
|
+
const tail = buffer.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).slice(-2).join(" | ");
|
|
2023
|
+
const base = error?.message ?? "frogoe tunnel: cloudflared exited before producing a URL";
|
|
2024
|
+
reject(new Error(tail ? `${base} \u2014 ${tail}` : base));
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
resolve2({ exited, stop: killTree, url });
|
|
2028
|
+
};
|
|
2029
|
+
const timer = setTimeout(() => {
|
|
2030
|
+
finish(
|
|
2031
|
+
new Error(
|
|
2032
|
+
`frogoe tunnel: no URL from cloudflared within ${timeoutMs / 1e3}s \u2014 check your internet, or brew install cloudflared`
|
|
2033
|
+
)
|
|
2034
|
+
);
|
|
2035
|
+
}, timeoutMs);
|
|
2036
|
+
child.once("exit", (code) => {
|
|
2037
|
+
if (!settled) {
|
|
2038
|
+
finish(new Error(`frogoe tunnel: cloudflared exited early (code ${code ?? "signal"})`));
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
const watch2 = (stream) => {
|
|
2042
|
+
stream.on("data", (data) => {
|
|
2043
|
+
if (settled && url) return;
|
|
2044
|
+
buffer += data.toString("utf-8");
|
|
2045
|
+
const found = parseTunnelUrl(buffer);
|
|
2046
|
+
if (found && !url) {
|
|
2047
|
+
url = found;
|
|
2048
|
+
finish();
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
2051
|
+
};
|
|
2052
|
+
if (child.stderr && child.stdout) {
|
|
2053
|
+
watch2(child.stderr);
|
|
2054
|
+
watch2(child.stdout);
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
// src/browser/lock.ts
|
|
2062
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, rmSync, statSync, utimesSync } from "fs";
|
|
2063
|
+
import path9 from "path";
|
|
2064
|
+
var DEFAULT_LOCK_TIMINGS, LOCK_DIR_NAME, RECLAIM_DIR_NAME, isErrno, sleep2, tryAcquireDirLock, isDirLockStale, touch, reclaimStaleLock, withInstallLock;
|
|
2065
|
+
var init_lock = __esm({
|
|
2066
|
+
"src/browser/lock.ts"() {
|
|
2067
|
+
"use strict";
|
|
2068
|
+
DEFAULT_LOCK_TIMINGS = {
|
|
2069
|
+
heartbeatMs: 15e3,
|
|
2070
|
+
pollMs: 200,
|
|
2071
|
+
staleMs: 12e4,
|
|
2072
|
+
waitNoticeMs: 1e4
|
|
2073
|
+
};
|
|
2074
|
+
LOCK_DIR_NAME = ".chrome.install.lock";
|
|
2075
|
+
RECLAIM_DIR_NAME = ".chrome.install.reclaim.lock";
|
|
2076
|
+
isErrno = (error, code) => {
|
|
2077
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
2078
|
+
return error.code === code;
|
|
2079
|
+
}
|
|
2080
|
+
return false;
|
|
2081
|
+
};
|
|
2082
|
+
sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2083
|
+
tryAcquireDirLock = (lockDir) => {
|
|
2084
|
+
try {
|
|
2085
|
+
mkdirSync5(lockDir, { recursive: false });
|
|
2086
|
+
return true;
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
if (!isErrno(error, "EEXIST")) throw error;
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
isDirLockStale = (lockDir, staleMs) => {
|
|
2093
|
+
try {
|
|
2094
|
+
return Date.now() - statSync(lockDir).mtimeMs > staleMs;
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
if (isErrno(error, "ENOENT")) return false;
|
|
2097
|
+
throw error;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
touch = (lockDir) => {
|
|
2101
|
+
try {
|
|
2102
|
+
const now = /* @__PURE__ */ new Date();
|
|
2103
|
+
utimesSync(lockDir, now, now);
|
|
2104
|
+
} catch {
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
reclaimStaleLock = (lockDir, reclaimDir, staleMs) => {
|
|
2108
|
+
if (!tryAcquireDirLock(reclaimDir)) return;
|
|
2109
|
+
try {
|
|
2110
|
+
if (isDirLockStale(lockDir, staleMs)) {
|
|
2111
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
2112
|
+
}
|
|
2113
|
+
} finally {
|
|
2114
|
+
rmSync(reclaimDir, { recursive: true, force: true });
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
withInstallLock = async (fn, rootDir, options) => {
|
|
2118
|
+
const timings = options?.timings ?? DEFAULT_LOCK_TIMINGS;
|
|
2119
|
+
const log = options?.log ?? ((line) => console.error(line));
|
|
2120
|
+
const lockDir = path9.join(rootDir, LOCK_DIR_NAME);
|
|
2121
|
+
const reclaimDir = path9.join(rootDir, RECLAIM_DIR_NAME);
|
|
2122
|
+
if (!existsSync8(rootDir)) mkdirSync5(rootDir, { recursive: true });
|
|
2123
|
+
let deadline = Date.now() + timings.staleMs;
|
|
2124
|
+
const waitStart = Date.now();
|
|
2125
|
+
let lastNoticeMs = 0;
|
|
2126
|
+
for (; ; ) {
|
|
2127
|
+
if (existsSync8(reclaimDir)) {
|
|
2128
|
+
if (isDirLockStale(reclaimDir, timings.staleMs)) {
|
|
2129
|
+
rmSync(reclaimDir, { recursive: true, force: true });
|
|
2130
|
+
}
|
|
2131
|
+
await sleep2(timings.pollMs);
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
if (tryAcquireDirLock(lockDir)) {
|
|
2135
|
+
rmSync(reclaimDir, { recursive: true, force: true });
|
|
2136
|
+
break;
|
|
2137
|
+
}
|
|
2138
|
+
const waitedMs = Date.now() - waitStart;
|
|
2139
|
+
if (waitedMs - lastNoticeMs >= timings.waitNoticeMs) {
|
|
2140
|
+
lastNoticeMs = waitedMs;
|
|
2141
|
+
log(
|
|
2142
|
+
` browser: waiting for another frogoe process to finish installing chrome-headless-shell (${Math.round(waitedMs / 1e3)}s elapsed)\u2026`
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
if (isDirLockStale(lockDir, timings.staleMs) || Date.now() > deadline) {
|
|
2146
|
+
reclaimStaleLock(lockDir, reclaimDir, timings.staleMs);
|
|
2147
|
+
deadline = Date.now() + timings.staleMs;
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
await sleep2(timings.pollMs);
|
|
2151
|
+
}
|
|
2152
|
+
const heartbeat = setInterval(() => touch(lockDir), timings.heartbeatMs);
|
|
2153
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
2154
|
+
try {
|
|
2155
|
+
return await fn();
|
|
2156
|
+
} finally {
|
|
2157
|
+
clearInterval(heartbeat);
|
|
2158
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
// src/browser/manager.ts
|
|
2165
|
+
import { cpSync as cpSync2, existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync2, renameSync, rmSync as rmSync2 } from "fs";
|
|
2166
|
+
import { homedir } from "os";
|
|
2167
|
+
import path10 from "path";
|
|
2168
|
+
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;
|
|
2169
|
+
var init_manager = __esm({
|
|
2170
|
+
"src/browser/manager.ts"() {
|
|
2171
|
+
"use strict";
|
|
2172
|
+
init_tunnel();
|
|
2173
|
+
init_lock();
|
|
2174
|
+
CHROME_BUILD = "131.0.6778.204";
|
|
2175
|
+
BROWSER_PATH_ENV = "FROGOE_BROWSER_PATH";
|
|
2176
|
+
CACHE_ROOT = path10.join(cacheBase(process.platform, process.env, homedir()), "frogoe");
|
|
2177
|
+
CACHE_DIR = path10.join(CACHE_ROOT, "chrome");
|
|
2178
|
+
ARCHIVE_CHECKSUMS = {
|
|
2179
|
+
linux: "afaac86e302c4874245991a3d509d529c50ecdd447affff0cdc2b08520906c32",
|
|
2180
|
+
mac: "933463c27a951d3fc153c408c4b8a96c89b9344379963f68a5ff9aa1718bcaf9",
|
|
2181
|
+
mac_arm: "9dae11ffda8d77f92c56f64de84f81fa6781c9d41235a8e9397117d7d004a017",
|
|
2182
|
+
win32: "e1a14441accf82785fd81b78d34d8faf5cc78bbab6b2f90fa7e7e972d93ed77f",
|
|
2183
|
+
win64: "d45834686f461ef6f487c0eacc2063bc311a5de86e92de46600895bf44136714"
|
|
2184
|
+
};
|
|
2185
|
+
messageOf = (error) => {
|
|
2186
|
+
if (error instanceof Error) return error.message;
|
|
2187
|
+
if (typeof error === "string") return error;
|
|
2188
|
+
return "";
|
|
2189
|
+
};
|
|
2190
|
+
positiveIntEnv = (name, raw, fallback) => {
|
|
2191
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
2192
|
+
const trimmed = raw.trim();
|
|
2193
|
+
if (!/^\d+$/u.test(trimmed) || Number.parseInt(trimmed, 10) <= 0) {
|
|
2194
|
+
throw new Error(
|
|
2195
|
+
`frogoe browser: ${name} must be a positive integer of milliseconds (got "${raw}") \u2014 unset it or fix the value, then re-run`
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
return Number.parseInt(trimmed, 10);
|
|
2199
|
+
};
|
|
2200
|
+
isCorruptInstallError = (error) => {
|
|
2201
|
+
const message2 = messageOf(error).toLowerCase();
|
|
2202
|
+
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");
|
|
2203
|
+
};
|
|
2204
|
+
browserPathHintForPlatform = (platform) => {
|
|
2205
|
+
if (platform === "darwin") return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
2206
|
+
if (platform === "win32") return "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
|
|
2207
|
+
return "/usr/bin/google-chrome";
|
|
2208
|
+
};
|
|
2209
|
+
wrapDownloadFailure = (cause) => {
|
|
2210
|
+
const original = messageOf(cause) || String(cause);
|
|
2211
|
+
return new Error(
|
|
2212
|
+
`frogoe browser: failed to download chrome-headless-shell ${CHROME_BUILD} \u2014 ${original}
|
|
2213
|
+
|
|
2214
|
+
Point frogoe at an already-installed Chrome/Chromium instead:
|
|
2215
|
+
|
|
2216
|
+
export ${BROWSER_PATH_ENV}="${browserPathHintForPlatform(process.platform)}"
|
|
2217
|
+
|
|
2218
|
+
Then re-run your command. Any Chrome build works for the sandbox; the pinned
|
|
2219
|
+
headless-shell re-downloads into ${CACHE_DIR} once the network allows.`,
|
|
2220
|
+
{ cause: cause instanceof Error ? cause : void 0 }
|
|
2221
|
+
);
|
|
2222
|
+
};
|
|
2223
|
+
isTimeoutError = (error) => {
|
|
2224
|
+
if (!(error instanceof Error)) return false;
|
|
2225
|
+
return error.name === "TimeoutError" || /timed out \d+ ms/u.test(error.message);
|
|
2226
|
+
};
|
|
2227
|
+
wrapLaunchFailure = (cause, timeoutMs) => {
|
|
2228
|
+
const head = isTimeoutError(cause) ? `frogoe browser: headless chrome did not start within ${String(timeoutMs)} ms.
|
|
2229
|
+
|
|
2230
|
+
Raise the budget or point frogoe at an installed Chrome:
|
|
2231
|
+
|
|
2232
|
+
FROGOE_LAUNCH_TIMEOUT_MS=${String(timeoutMs * 2)} <your command>
|
|
2233
|
+
export ${BROWSER_PATH_ENV}="${browserPathHintForPlatform(process.platform)}"
|
|
2234
|
+
` : `frogoe browser: headless chrome failed to launch \u2014 ${messageOf(cause) || String(cause)}
|
|
2235
|
+
|
|
2236
|
+
If a local Chrome exists, point frogoe at it:
|
|
2237
|
+
|
|
2238
|
+
export ${BROWSER_PATH_ENV}="${browserPathHintForPlatform(process.platform)}"
|
|
2239
|
+
`;
|
|
2240
|
+
return new Error(
|
|
2241
|
+
`${head}
|
|
2242
|
+
Then re-run your command. (The pinned binary lives in ${CACHE_DIR} \u2014
|
|
2243
|
+
see skills/frogoe-cli \u2192 references/live-sandbox.md)`,
|
|
2244
|
+
{ cause: cause instanceof Error ? cause : void 0 }
|
|
2245
|
+
);
|
|
2246
|
+
};
|
|
2247
|
+
findInCache = async (cacheDir, buildId) => {
|
|
2248
|
+
if (!existsSync9(cacheDir)) return {};
|
|
2249
|
+
const { Browser, detectBrowserPlatform, getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
2250
|
+
const platform = detectBrowserPlatform();
|
|
2251
|
+
const match = (await getInstalledBrowsers({ cacheDir })).find(
|
|
2252
|
+
(entry) => entry.browser === Browser.CHROMEHEADLESSSHELL && entry.buildId === buildId && entry.platform === platform
|
|
2253
|
+
);
|
|
2254
|
+
if (!match) return {};
|
|
2255
|
+
if (existsSync9(match.executablePath)) return { executablePath: match.executablePath };
|
|
2256
|
+
return { staleInstallPath: match.path };
|
|
2257
|
+
};
|
|
2258
|
+
legacyCacheDir = (dir) => path10.resolve(dir, "node_modules", ".frogoe-browser");
|
|
2259
|
+
migrateLegacyCache = async (legacyDir, cacheDir, buildId, log) => {
|
|
2260
|
+
if (!existsSync9(legacyDir)) return null;
|
|
2261
|
+
const { Browser, getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
2262
|
+
const legacy = (await getInstalledBrowsers({ cacheDir: legacyDir })).find(
|
|
2263
|
+
(entry) => entry.browser === Browser.CHROMEHEADLESSSHELL && entry.buildId === buildId && existsSync9(entry.executablePath)
|
|
2264
|
+
);
|
|
2265
|
+
if (!legacy) return null;
|
|
2266
|
+
const browserRootName = path10.basename(path10.dirname(legacy.path));
|
|
2267
|
+
const installName = path10.basename(legacy.path);
|
|
2268
|
+
const destination = path10.join(cacheDir, browserRootName, installName);
|
|
2269
|
+
mkdirSync6(path10.dirname(destination), { recursive: true });
|
|
2270
|
+
rmSync2(destination, { recursive: true, force: true });
|
|
2271
|
+
try {
|
|
2272
|
+
try {
|
|
2273
|
+
renameSync(legacy.path, destination);
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
if (!isErrno(error, "EXDEV")) throw error;
|
|
2276
|
+
const staging = `${destination}.migrating-${String(process.pid)}`;
|
|
2277
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
2278
|
+
try {
|
|
2279
|
+
cpSync2(legacy.path, staging, { recursive: true });
|
|
2280
|
+
renameSync(staging, destination);
|
|
2281
|
+
} finally {
|
|
2282
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
2283
|
+
}
|
|
2284
|
+
rmSync2(legacy.path, { recursive: true, force: true });
|
|
2285
|
+
}
|
|
2286
|
+
} catch {
|
|
2287
|
+
return null;
|
|
2288
|
+
}
|
|
2289
|
+
const found = await findInCache(cacheDir, buildId);
|
|
2290
|
+
if (!found.executablePath) return null;
|
|
2291
|
+
rmSync2(path10.dirname(legacy.path), { recursive: true, force: true });
|
|
2292
|
+
rmSync2(legacyDir, { recursive: true, force: true });
|
|
2293
|
+
log?.(` browser: migrated the cached chrome-headless-shell into ${cacheDir} (one-time)`);
|
|
2294
|
+
return found.executablePath;
|
|
2295
|
+
};
|
|
2296
|
+
pruneLegacyCache = (legacyDir, buildId) => {
|
|
2297
|
+
if (!existsSync9(legacyDir)) return false;
|
|
2298
|
+
try {
|
|
2299
|
+
const browserRoot = path10.join(legacyDir, "chrome-headless-shell");
|
|
2300
|
+
if (existsSync9(browserRoot)) {
|
|
2301
|
+
const entries = readdirSync2(browserRoot);
|
|
2302
|
+
if (entries.length > 0 && !entries.some((name) => name.endsWith(`-${buildId}`))) {
|
|
2303
|
+
return false;
|
|
2304
|
+
}
|
|
2305
|
+
} else if (readdirSync2(legacyDir).length > 0) {
|
|
2306
|
+
return false;
|
|
2307
|
+
}
|
|
2308
|
+
rmSync2(legacyDir, { recursive: true, force: true });
|
|
2309
|
+
return true;
|
|
2310
|
+
} catch {
|
|
2311
|
+
return false;
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
progressReporter = () => {
|
|
2315
|
+
const tty = process.stderr.isTTY === true;
|
|
2316
|
+
const mb2 = (bytes) => (bytes / (1024 * 1024)).toFixed(1);
|
|
2317
|
+
let lastPaintMs = 0;
|
|
2318
|
+
let nextMilestone = 0.25;
|
|
2319
|
+
return (downloaded, total) => {
|
|
2320
|
+
if (tty) {
|
|
2321
|
+
const finished = total > 0 && downloaded >= total;
|
|
2322
|
+
if (Date.now() - lastPaintMs < 100 && !finished) return;
|
|
2323
|
+
lastPaintMs = Date.now();
|
|
2324
|
+
process.stderr.write(
|
|
2325
|
+
`\r browser: downloading chrome-headless-shell \u2014 ${mb2(downloaded)}${total > 0 ? `/${mb2(total)}` : ""} MB`
|
|
2326
|
+
);
|
|
2327
|
+
if (finished) process.stderr.write("\n");
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (total <= 0 || downloaded / total < nextMilestone) return;
|
|
2331
|
+
process.stderr.write(
|
|
2332
|
+
` browser: downloading chrome-headless-shell \u2014 ${Math.round(nextMilestone * 100)}% (${mb2(total)} MB)
|
|
2333
|
+
`
|
|
2334
|
+
);
|
|
2335
|
+
nextMilestone += 0.25;
|
|
2336
|
+
};
|
|
2337
|
+
};
|
|
2338
|
+
downloadBrowser = async (log) => {
|
|
2339
|
+
const { Browser, detectBrowserPlatform, install } = await import("@puppeteer/browsers");
|
|
2340
|
+
const platform = detectBrowserPlatform();
|
|
2341
|
+
if (!platform) {
|
|
2342
|
+
throw wrapDownloadFailure(
|
|
2343
|
+
new Error(`no chrome-headless-shell build exists for ${process.platform}/${process.arch}`)
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
const expectedHash = ARCHIVE_CHECKSUMS[platform];
|
|
2347
|
+
if (expectedHash === void 0) {
|
|
2348
|
+
log(
|
|
2349
|
+
` browser: no pinned checksum for ${platform} \u2014 downloading WITHOUT integrity verification`
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
const runInstall = () => install({
|
|
1604
2353
|
browser: Browser.CHROMEHEADLESSSHELL,
|
|
1605
|
-
buildId:
|
|
1606
|
-
cacheDir,
|
|
1607
|
-
unpack: true
|
|
1608
|
-
|
|
1609
|
-
|
|
2354
|
+
buildId: CHROME_BUILD,
|
|
2355
|
+
cacheDir: CACHE_DIR,
|
|
2356
|
+
unpack: true,
|
|
2357
|
+
...expectedHash === void 0 ? {} : { expectedHash },
|
|
2358
|
+
downloadProgressCallback: progressReporter()
|
|
2359
|
+
});
|
|
2360
|
+
try {
|
|
2361
|
+
return (await runInstall()).executablePath;
|
|
2362
|
+
} catch (error) {
|
|
2363
|
+
if (!isCorruptInstallError(error)) throw wrapDownloadFailure(error);
|
|
2364
|
+
log(" browser: cached archive was corrupt \u2014 purging and retrying once\u2026");
|
|
2365
|
+
rmSync2(CACHE_DIR, { recursive: true, force: true });
|
|
2366
|
+
try {
|
|
2367
|
+
return (await runInstall()).executablePath;
|
|
2368
|
+
} catch (retryError) {
|
|
2369
|
+
throw wrapDownloadFailure(retryError);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
ensureBrowser = async (options) => {
|
|
2374
|
+
if (resolved) return resolved;
|
|
2375
|
+
const log = (line) => console.error(line);
|
|
2376
|
+
const fromEnv = process.env[BROWSER_PATH_ENV]?.trim();
|
|
2377
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
2378
|
+
if (!existsSync9(fromEnv)) {
|
|
2379
|
+
throw new Error(
|
|
2380
|
+
`frogoe browser: ${BROWSER_PATH_ENV} is set but "${fromEnv}" does not exist \u2014 fix the path or unset the variable, then re-run`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
resolved = fromEnv;
|
|
2384
|
+
return resolved;
|
|
2385
|
+
}
|
|
2386
|
+
const legacyDirs = options?.legacyDirs ?? [legacyCacheDir(process.cwd())];
|
|
2387
|
+
const pre = await findInCache(CACHE_DIR, CHROME_BUILD);
|
|
2388
|
+
if (pre.executablePath) {
|
|
2389
|
+
resolved = pre.executablePath;
|
|
2390
|
+
} else {
|
|
2391
|
+
resolved = await withInstallLock(
|
|
2392
|
+
async () => {
|
|
2393
|
+
const post = await findInCache(CACHE_DIR, CHROME_BUILD);
|
|
2394
|
+
if (post.executablePath) return post.executablePath;
|
|
2395
|
+
if (post.staleInstallPath) {
|
|
2396
|
+
log(" browser: incomplete install in the cache \u2014 purging before reinstalling\u2026");
|
|
2397
|
+
rmSync2(post.staleInstallPath, { recursive: true, force: true });
|
|
2398
|
+
}
|
|
2399
|
+
for (const legacyDir of legacyDirs) {
|
|
2400
|
+
const migrated = await migrateLegacyCache(legacyDir, CACHE_DIR, CHROME_BUILD, log);
|
|
2401
|
+
if (migrated) return migrated;
|
|
2402
|
+
}
|
|
2403
|
+
return downloadBrowser(log);
|
|
2404
|
+
},
|
|
2405
|
+
CACHE_ROOT,
|
|
2406
|
+
{ log }
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
for (const legacyDir of legacyDirs) {
|
|
2410
|
+
if (pruneLegacyCache(legacyDir, CHROME_BUILD)) {
|
|
2411
|
+
log(` browser: removed the superseded per-project cache at ${legacyDir}`);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
return resolved;
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
|
|
2419
|
+
// src/browser/launch.ts
|
|
2420
|
+
var LAUNCH_TIMEOUT_ENV, PROTOCOL_TIMEOUT_ENV, DEFAULT_LAUNCH_TIMEOUT_MS, DEFAULT_PROTOCOL_TIMEOUT_MS, launchBudgets, launchBrowser;
|
|
2421
|
+
var init_launch = __esm({
|
|
2422
|
+
"src/browser/launch.ts"() {
|
|
2423
|
+
"use strict";
|
|
2424
|
+
init_manager();
|
|
2425
|
+
LAUNCH_TIMEOUT_ENV = "FROGOE_LAUNCH_TIMEOUT_MS";
|
|
2426
|
+
PROTOCOL_TIMEOUT_ENV = "FROGOE_PROTOCOL_TIMEOUT_MS";
|
|
2427
|
+
DEFAULT_LAUNCH_TIMEOUT_MS = 12e4;
|
|
2428
|
+
DEFAULT_PROTOCOL_TIMEOUT_MS = 3e5;
|
|
2429
|
+
launchBudgets = (env = process.env) => ({
|
|
2430
|
+
launchMs: positiveIntEnv(LAUNCH_TIMEOUT_ENV, env[LAUNCH_TIMEOUT_ENV], DEFAULT_LAUNCH_TIMEOUT_MS),
|
|
2431
|
+
protocolMs: positiveIntEnv(
|
|
2432
|
+
PROTOCOL_TIMEOUT_ENV,
|
|
2433
|
+
env[PROTOCOL_TIMEOUT_ENV],
|
|
2434
|
+
DEFAULT_PROTOCOL_TIMEOUT_MS
|
|
2435
|
+
)
|
|
2436
|
+
});
|
|
2437
|
+
launchBrowser = async (options) => {
|
|
2438
|
+
const { default: puppeteer } = await import("puppeteer-core");
|
|
2439
|
+
const budgets = launchBudgets();
|
|
2440
|
+
const executablePath = await ensureBrowser({ legacyDirs: options?.legacyDirs });
|
|
2441
|
+
try {
|
|
2442
|
+
return await puppeteer.launch({
|
|
2443
|
+
args: ["--no-sandbox", "--disable-gpu"],
|
|
2444
|
+
defaultViewport: options?.defaultViewport === void 0 ? null : options.defaultViewport,
|
|
2445
|
+
executablePath,
|
|
2446
|
+
headless: true,
|
|
2447
|
+
protocolTimeout: budgets.protocolMs,
|
|
2448
|
+
timeout: budgets.launchMs
|
|
2449
|
+
});
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
throw wrapLaunchFailure(error, budgets.launchMs);
|
|
2452
|
+
}
|
|
1610
2453
|
};
|
|
1611
2454
|
}
|
|
1612
2455
|
});
|
|
1613
2456
|
|
|
1614
2457
|
// src/net/ip.ts
|
|
1615
2458
|
import { execSync } from "child_process";
|
|
1616
|
-
import
|
|
2459
|
+
import os2 from "os";
|
|
1617
2460
|
var VIRTUAL, isPrivateV4, isLoopback, selfAddresses, pickLanIp, interfaceFromRouteOutput, ROUTE_PROBES, routedInterfaceName, resolveLan;
|
|
1618
2461
|
var init_ip = __esm({
|
|
1619
2462
|
"src/net/ip.ts"() {
|
|
@@ -1713,7 +2556,7 @@ var init_ip = __esm({
|
|
|
1713
2556
|
}
|
|
1714
2557
|
return null;
|
|
1715
2558
|
};
|
|
1716
|
-
resolveLan = () => pickLanIp(
|
|
2559
|
+
resolveLan = () => pickLanIp(os2.networkInterfaces(), routedInterfaceName());
|
|
1717
2560
|
}
|
|
1718
2561
|
});
|
|
1719
2562
|
|
|
@@ -1832,8 +2675,8 @@ var init_records = __esm({
|
|
|
1832
2675
|
});
|
|
1833
2676
|
|
|
1834
2677
|
// src/telemetry/session.ts
|
|
1835
|
-
import { appendFileSync, mkdirSync as
|
|
1836
|
-
import
|
|
2678
|
+
import { appendFileSync, mkdirSync as mkdirSync7, readdirSync as readdirSync3 } from "fs";
|
|
2679
|
+
import path11 from "path";
|
|
1837
2680
|
var pad, sessionStamp, createSessionStore, latestSessionFile;
|
|
1838
2681
|
var init_session = __esm({
|
|
1839
2682
|
"src/telemetry/session.ts"() {
|
|
@@ -1844,25 +2687,25 @@ var init_session = __esm({
|
|
|
1844
2687
|
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
1845
2688
|
};
|
|
1846
2689
|
createSessionStore = (gameDir, startedWall) => {
|
|
1847
|
-
const dir =
|
|
2690
|
+
const dir = path11.join(gameDir, ".frogoe", "sessions");
|
|
1848
2691
|
let file;
|
|
1849
2692
|
return {
|
|
1850
2693
|
file: () => file,
|
|
1851
2694
|
write: (records) => {
|
|
1852
2695
|
if (records.length === 0) return;
|
|
1853
2696
|
if (!file) {
|
|
1854
|
-
|
|
1855
|
-
file =
|
|
2697
|
+
mkdirSync7(dir, { recursive: true });
|
|
2698
|
+
file = path11.join(dir, `${sessionStamp(startedWall)}.jsonl`);
|
|
1856
2699
|
}
|
|
1857
2700
|
appendFileSync(file, records.map((r) => JSON.stringify(r)).join("\n") + "\n", "utf-8");
|
|
1858
2701
|
}
|
|
1859
2702
|
};
|
|
1860
2703
|
};
|
|
1861
2704
|
latestSessionFile = (gameDir) => {
|
|
1862
|
-
const dir =
|
|
2705
|
+
const dir = path11.join(gameDir, ".frogoe", "sessions");
|
|
1863
2706
|
try {
|
|
1864
|
-
const files =
|
|
1865
|
-
return files.length > 0 ?
|
|
2707
|
+
const files = readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
2708
|
+
return files.length > 0 ? path11.join(dir, files[files.length - 1] ?? "") : null;
|
|
1866
2709
|
} catch {
|
|
1867
2710
|
return null;
|
|
1868
2711
|
}
|
|
@@ -1875,9 +2718,9 @@ var run_exports = {};
|
|
|
1875
2718
|
__export(run_exports, {
|
|
1876
2719
|
startServer: () => startServer
|
|
1877
2720
|
});
|
|
1878
|
-
import { existsSync as
|
|
1879
|
-
import
|
|
1880
|
-
import
|
|
2721
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, statSync as statSync2, watch } from "fs";
|
|
2722
|
+
import os3 from "os";
|
|
2723
|
+
import path12 from "path";
|
|
1881
2724
|
import { createAdaptorServer } from "@hono/node-server";
|
|
1882
2725
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
1883
2726
|
import { Hono } from "hono";
|
|
@@ -1886,6 +2729,7 @@ var init_run = __esm({
|
|
|
1886
2729
|
"src/run.ts"() {
|
|
1887
2730
|
"use strict";
|
|
1888
2731
|
init_ip();
|
|
2732
|
+
init_font_proxy();
|
|
1889
2733
|
init_records();
|
|
1890
2734
|
init_session();
|
|
1891
2735
|
buildDevScript = (version) => `<script>(function(){var v="${String(version)}";try{var es=new EventSource("/__frogoe/reload");es.onmessage=function(){location.reload()};es.onerror=function(){es.close()};}catch(e){}setInterval(function(){fetch("/__frogoe/version",{cache:"no-store"}).then(function(r){return r.text()}).then(function(t){if(t!==v)location.reload()}).catch(function(){})},2000);var fps=[],cnt=0,sec=performance.now(),evs=[],up0=performance.now();function up(){return (performance.now()-up0)/1000}function tick(){cnt++;var n=performance.now();if(n-sec>=1000){fps.push(cnt);cnt=0;sec=n}requestAnimationFrame(tick)}requestAnimationFrame(tick);addEventListener("error",function(e){evs.push({type:"error",msg:String(e.message||e).slice(0,200),up:up()})});addEventListener("unhandledrejection",function(e){evs.push({type:"rejection",msg:String(e.reason).slice(0,200),up:up()})});document.addEventListener("visibilitychange",function(){evs.push({type:document.hidden?"hidden":"visible",up:up()})});function flush(beacon){var p={v:1,up:up(),fps:fps.splice(0),events:evs.splice(0)};if(performance.memory)p.mem=Math.round(performance.memory.usedJSHeapSize/1048576);var b=JSON.stringify(p);if(beacon&&navigator.sendBeacon){navigator.sendBeacon("/__frogoe/metrics",new Blob([b],{type:"application/json"}));return}fetch("/__frogoe/metrics",{method:"POST",body:b,headers:{"content-type":"application/json"},keepalive:true}).catch(function(){});}document.addEventListener("visibilitychange",function(){if(document.hidden)flush(true)});addEventListener("pagehide",function(){flush(true)});setInterval(function(){flush(false)},5000);})();</script>`;
|
|
@@ -1905,15 +2749,15 @@ var init_run = __esm({
|
|
|
1905
2749
|
woff2: "font/woff2"
|
|
1906
2750
|
};
|
|
1907
2751
|
startServer = async (dir, requestedPort = 0, telemetry) => {
|
|
1908
|
-
const root =
|
|
1909
|
-
if (!
|
|
2752
|
+
const root = path12.resolve(dir);
|
|
2753
|
+
if (!existsSync10(path12.join(root, "index.html"))) {
|
|
1910
2754
|
throw new Error(`frogoe run: no index.html in ${root} \u2014 is this a game folder?`);
|
|
1911
2755
|
}
|
|
1912
2756
|
const clients = /* @__PURE__ */ new Set();
|
|
1913
2757
|
let version = 0;
|
|
1914
2758
|
let remoteSeen = false;
|
|
1915
2759
|
let lastRemote = "";
|
|
1916
|
-
const own = selfAddresses(
|
|
2760
|
+
const own = selfAddresses(os3.networkInterfaces());
|
|
1917
2761
|
const session = telemetry ? createSessionStore(root, Date.now()) : void 0;
|
|
1918
2762
|
const app = new Hono();
|
|
1919
2763
|
app.use("*", async (c, next) => {
|
|
@@ -1948,6 +2792,22 @@ var init_run = __esm({
|
|
|
1948
2792
|
"/__frogoe/version",
|
|
1949
2793
|
(c) => c.text(String(version), 200, { "cache-control": "no-store" })
|
|
1950
2794
|
);
|
|
2795
|
+
const fontCacheDir = path12.join(root, ".frogoe", "font-cache");
|
|
2796
|
+
app.get("/__frogoe/font/:token", async (c) => {
|
|
2797
|
+
const url = decodeProxyToken(c.req.param("token"));
|
|
2798
|
+
if (url === null) {
|
|
2799
|
+
return c.text("forbidden", 403);
|
|
2800
|
+
}
|
|
2801
|
+
const result = await proxyFontRequest(url, fontCacheDir);
|
|
2802
|
+
return new Response(result.body, {
|
|
2803
|
+
headers: {
|
|
2804
|
+
"cache-control": "no-store",
|
|
2805
|
+
"content-type": result.contentType,
|
|
2806
|
+
"x-frogoe-cache": result.fromCache ? "hit" : "miss"
|
|
2807
|
+
},
|
|
2808
|
+
status: result.status
|
|
2809
|
+
});
|
|
2810
|
+
});
|
|
1951
2811
|
app.post("/__frogoe/metrics", async (c) => {
|
|
1952
2812
|
try {
|
|
1953
2813
|
const payload = JSON.parse(await c.req.text());
|
|
@@ -1964,23 +2824,24 @@ var init_run = __esm({
|
|
|
1964
2824
|
});
|
|
1965
2825
|
app.get("*", (c) => {
|
|
1966
2826
|
const raw = decodeURIComponent(new URL(c.req.url).pathname);
|
|
1967
|
-
const safe =
|
|
1968
|
-
let file =
|
|
2827
|
+
const safe = path12.normalize(raw).replaceAll("\\", "/");
|
|
2828
|
+
let file = path12.join(root, safe === "/" ? "index.html" : safe);
|
|
1969
2829
|
if (!file.startsWith(root)) {
|
|
1970
2830
|
return c.text("forbidden", 403);
|
|
1971
2831
|
}
|
|
1972
|
-
if (
|
|
1973
|
-
file =
|
|
2832
|
+
if (existsSync10(file) && statSync2(file).isDirectory()) {
|
|
2833
|
+
file = path12.join(file, "index.html");
|
|
1974
2834
|
}
|
|
1975
|
-
if (!
|
|
2835
|
+
if (!existsSync10(file)) {
|
|
1976
2836
|
return c.text(`frogoe run: not found: ${raw}`, 404);
|
|
1977
2837
|
}
|
|
1978
|
-
const body =
|
|
1979
|
-
const ext =
|
|
2838
|
+
const body = readFileSync8(file);
|
|
2839
|
+
const ext = path12.extname(file).slice(1).toLowerCase();
|
|
1980
2840
|
const type = MIME2[ext] ?? "application/octet-stream";
|
|
1981
2841
|
if (ext === "html" || ext === "htm") {
|
|
1982
2842
|
const html = body.toString("utf-8");
|
|
1983
|
-
const
|
|
2843
|
+
const proxied = rewriteFontLinks(html);
|
|
2844
|
+
const injected = /<\/body>/iu.test(proxied) ? proxied.replace(/<\/body>/iu, `${buildDevScript(version)}</body>`) : proxied + buildDevScript(version);
|
|
1984
2845
|
return c.body(injected, 200, {
|
|
1985
2846
|
"cache-control": "no-store",
|
|
1986
2847
|
"content-type": type
|
|
@@ -2005,7 +2866,7 @@ var init_run = __esm({
|
|
|
2005
2866
|
const lan = lanInfo.ip ? `http://${lanInfo.ip}:${port}` : void 0;
|
|
2006
2867
|
let timer;
|
|
2007
2868
|
const watcher = watch(root, { recursive: true }, (_event, file) => {
|
|
2008
|
-
const first = file?.split(
|
|
2869
|
+
const first = file?.split(path12.sep)[0];
|
|
2009
2870
|
if (first === "snapshots" || first === ".frogoe" || first === "dist") {
|
|
2010
2871
|
return;
|
|
2011
2872
|
}
|
|
@@ -2039,9 +2900,9 @@ var init_run = __esm({
|
|
|
2039
2900
|
});
|
|
2040
2901
|
|
|
2041
2902
|
// src/raster.ts
|
|
2042
|
-
import { existsSync as
|
|
2043
|
-
import
|
|
2044
|
-
var SCENES,
|
|
2903
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
2904
|
+
import path13 from "path";
|
|
2905
|
+
var SCENES, sleep3, rasterScriptFor, rasterizeArt;
|
|
2045
2906
|
var init_raster = __esm({
|
|
2046
2907
|
"src/raster.ts"() {
|
|
2047
2908
|
"use strict";
|
|
@@ -2049,32 +2910,33 @@ var init_raster = __esm({
|
|
|
2049
2910
|
init_runtime_source();
|
|
2050
2911
|
init_art_verify();
|
|
2051
2912
|
init_runtime_source();
|
|
2052
|
-
|
|
2913
|
+
init_launch();
|
|
2914
|
+
init_manager();
|
|
2053
2915
|
SCENES = [
|
|
2054
2916
|
{
|
|
2055
2917
|
draw: "drawPoster",
|
|
2056
2918
|
height: 1920,
|
|
2057
|
-
out:
|
|
2919
|
+
out: path13.join("dist", "assets", "poster.png"),
|
|
2058
2920
|
size: "w, h",
|
|
2059
|
-
source:
|
|
2921
|
+
source: path13.join("assets", "poster.js"),
|
|
2060
2922
|
width: 1080
|
|
2061
2923
|
},
|
|
2062
2924
|
{
|
|
2063
2925
|
draw: "drawIcon",
|
|
2064
2926
|
height: 1024,
|
|
2065
|
-
out:
|
|
2927
|
+
out: path13.join("dist", "assets", "icon.png"),
|
|
2066
2928
|
size: "size",
|
|
2067
|
-
source:
|
|
2929
|
+
source: path13.join("assets", "icon.js"),
|
|
2068
2930
|
width: 1024
|
|
2069
2931
|
}
|
|
2070
2932
|
];
|
|
2071
|
-
|
|
2933
|
+
sleep3 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2072
2934
|
rasterScriptFor = (scene, palette) => {
|
|
2073
2935
|
const sizeArg = scene.draw === "drawIcon" ? String(scene.width) : `${String(scene.width)}, ${String(scene.height)}`;
|
|
2074
2936
|
const analyze = scene.draw === "drawIcon" ? "analyzeIconCorners" : "analyzeTitleBand";
|
|
2075
2937
|
return `(async () => {
|
|
2076
2938
|
${runtimeSource()}
|
|
2077
|
-
const mod = await import("./assets/${
|
|
2939
|
+
const mod = await import("./assets/${path13.basename(scene.source)}");
|
|
2078
2940
|
const canvas = document.createElement("canvas");
|
|
2079
2941
|
canvas.id = "__frogoe_raster";
|
|
2080
2942
|
canvas.width = ${String(scene.width)};
|
|
@@ -2101,8 +2963,8 @@ var init_raster = __esm({
|
|
|
2101
2963
|
})()`;
|
|
2102
2964
|
};
|
|
2103
2965
|
rasterizeArt = async (options) => {
|
|
2104
|
-
const dir =
|
|
2105
|
-
const missing = SCENES.filter((scene) => !
|
|
2966
|
+
const dir = path13.resolve(options.dir);
|
|
2967
|
+
const missing = SCENES.filter((scene) => !existsSync11(path13.join(dir, scene.source)));
|
|
2106
2968
|
if (missing.length > 0) {
|
|
2107
2969
|
throw new Error(
|
|
2108
2970
|
`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 +2972,10 @@ var init_raster = __esm({
|
|
|
2110
2972
|
}
|
|
2111
2973
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
2112
2974
|
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
|
-
});
|
|
2975
|
+
const browser = await launchBrowser({ legacyDirs: [legacyCacheDir(dir)] });
|
|
2121
2976
|
try {
|
|
2122
2977
|
const base = server.urls.local.replace(/\/$/u, "");
|
|
2123
|
-
const brief = parseBrief(
|
|
2978
|
+
const brief = parseBrief(readFileSync9(path13.join(dir, "BRIEF.md"), "utf-8"));
|
|
2124
2979
|
const palette = {
|
|
2125
2980
|
accent: brief?.accent ?? "#ff3b3b",
|
|
2126
2981
|
bg: brief?.bg ?? "#101418",
|
|
@@ -2166,7 +3021,7 @@ var init_raster = __esm({
|
|
|
2166
3021
|
}
|
|
2167
3022
|
} else {
|
|
2168
3023
|
const report = await page.evaluate("window.__frogoeArtReport");
|
|
2169
|
-
const gameSource =
|
|
3024
|
+
const gameSource = readFileSync9(path13.join(dir, "game.js"), "utf-8");
|
|
2170
3025
|
const verdict = verifyIconFullbleed(report, [...hexColorsOf(gameSource)]);
|
|
2171
3026
|
if (verdict !== null) throw new Error(verdict);
|
|
2172
3027
|
if (metrics.coverage < 0.08) {
|
|
@@ -2175,13 +3030,13 @@ var init_raster = __esm({
|
|
|
2175
3030
|
);
|
|
2176
3031
|
}
|
|
2177
3032
|
}
|
|
2178
|
-
await
|
|
3033
|
+
await sleep3(80);
|
|
2179
3034
|
const canvas = await page.$("#__frogoe_raster");
|
|
2180
3035
|
if (canvas === null) throw new Error(`bundle/art-crash \u2014 ${scene.source}: no canvas`);
|
|
2181
|
-
const outPath =
|
|
2182
|
-
|
|
3036
|
+
const outPath = path13.join(dir, scene.out);
|
|
3037
|
+
mkdirSync8(path13.dirname(outPath), { recursive: true });
|
|
2183
3038
|
await canvas.screenshot({ omitBackground: true, path: outPath, type: "png" });
|
|
2184
|
-
files.push({ bytes:
|
|
3039
|
+
files.push({ bytes: statSync3(outPath).size, file: scene.out });
|
|
2185
3040
|
} finally {
|
|
2186
3041
|
await page.close();
|
|
2187
3042
|
}
|
|
@@ -2201,8 +3056,8 @@ __export(bundle_exports, {
|
|
|
2201
3056
|
command: () => command2
|
|
2202
3057
|
});
|
|
2203
3058
|
import { defineCommand as defineCommand2 } from "citty";
|
|
2204
|
-
import { mkdirSync as
|
|
2205
|
-
import
|
|
3059
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
3060
|
+
import path14 from "path";
|
|
2206
3061
|
var command2;
|
|
2207
3062
|
var init_bundle2 = __esm({
|
|
2208
3063
|
"src/commands/bundle.ts"() {
|
|
@@ -2218,9 +3073,9 @@ var init_bundle2 = __esm({
|
|
|
2218
3073
|
async run({ args }) {
|
|
2219
3074
|
const dir = args.dir ? String(args.dir) : process.cwd();
|
|
2220
3075
|
const report = await bundle({ dir });
|
|
2221
|
-
const outPath = args.out ?
|
|
2222
|
-
|
|
2223
|
-
|
|
3076
|
+
const outPath = args.out ? path14.resolve(String(args.out)) : path14.join(dir, "dist", "index.html");
|
|
3077
|
+
mkdirSync9(path14.dirname(outPath), { recursive: true });
|
|
3078
|
+
writeFileSync5(outPath, report.artifact, "utf-8");
|
|
2224
3079
|
const art = await rasterizeArt({ dir });
|
|
2225
3080
|
for (const warning of [...report.warnings, ...art.warnings]) {
|
|
2226
3081
|
console.log(` \u26A0 ${warning}`);
|
|
@@ -2479,6 +3334,9 @@ var init_driver = __esm({
|
|
|
2479
3334
|
});
|
|
2480
3335
|
await page.mouse.up();
|
|
2481
3336
|
},
|
|
3337
|
+
async type(text) {
|
|
3338
|
+
await page.keyboard.type(text, { delay: 30 });
|
|
3339
|
+
},
|
|
2482
3340
|
async clickRetryAwaitReload(timeoutMs) {
|
|
2483
3341
|
const interactable = `(() => {
|
|
2484
3342
|
const b = document.querySelector("[data-block-retry]");
|
|
@@ -2535,7 +3393,7 @@ var init_types = __esm({
|
|
|
2535
3393
|
});
|
|
2536
3394
|
|
|
2537
3395
|
// src/live/decisions.ts
|
|
2538
|
-
var FPS_FLOOR2, FPS_SUSTAINED_WINDOW, FROZEN_STREAK, outlineFinding, collapseFinding, pageErrorFinding, consoleErrorFinding, canvasMissingFinding, canvasUnpaintedFinding, contractMissingFinding, stateStuckFinding, earlyDeathFinding, fpsFinding, fpsSustainedFinding, frozenFrameFinding, pausedFinding, stateCorruptFinding, playabilityFinding, finishEventFinding, neverEndsFinding, audioLockedFinding, noGameoverCardFinding, THROTTLE_RATE, THROTTLED_FPS_FLOOR, fpsThrottledFinding, noRetryFinding, retryDeadFinding, rebootFinding;
|
|
3396
|
+
var FPS_FLOOR2, FPS_SUSTAINED_WINDOW, FROZEN_STREAK, END_BUDGET_MS, ROUND_END_GRACE_MS, endBudgetMs, warnsWhenItNeverEnds, outlineFinding, collapseFinding, pageErrorFinding, consoleErrorFinding, canvasMissingFinding, canvasUnpaintedFinding, contractMissingFinding, stateStuckFinding, earlyDeathFinding, fpsFinding, fpsSustainedFinding, frozenFrameFinding, pausedFinding, stateCorruptFinding, playabilityFinding, finishEventFinding, neverEndsFinding, audioLockedFinding, noGameoverCardFinding, THROTTLE_RATE, THROTTLED_FPS_FLOOR, fpsThrottledFinding, noRetryFinding, retryDeadFinding, rebootFinding;
|
|
2539
3397
|
var init_decisions = __esm({
|
|
2540
3398
|
"src/live/decisions.ts"() {
|
|
2541
3399
|
"use strict";
|
|
@@ -2543,6 +3401,10 @@ var init_decisions = __esm({
|
|
|
2543
3401
|
FPS_FLOOR2 = 30;
|
|
2544
3402
|
FPS_SUSTAINED_WINDOW = 3;
|
|
2545
3403
|
FROZEN_STREAK = 3;
|
|
3404
|
+
END_BUDGET_MS = 45e3;
|
|
3405
|
+
ROUND_END_GRACE_MS = 8e3;
|
|
3406
|
+
endBudgetMs = (session) => session === "round" ? ROUND_END_GRACE_MS : session === "toy" ? 0 : END_BUDGET_MS;
|
|
3407
|
+
warnsWhenItNeverEnds = (session) => session === "blitz";
|
|
2546
3408
|
outlineFinding = (measures) => {
|
|
2547
3409
|
const bare = measures.filter((m) => !m.hasOutline);
|
|
2548
3410
|
if (bare.length === 0 || !bare[0]) {
|
|
@@ -2758,7 +3620,7 @@ var init_decisions = __esm({
|
|
|
2758
3620
|
neverEndsFinding = (budgetMs) => finding({
|
|
2759
3621
|
code: "live/never-ends",
|
|
2760
3622
|
file: "game.js",
|
|
2761
|
-
fix: `no death within ${Math.round(budgetMs / 1e3)}s of passive play \u2014
|
|
3623
|
+
fix: `no death within ${Math.round(budgetMs / 1e3)}s of passive play \u2014 blitz feed games are short loops; declare session: round in BRIEF.md for turn-based games or session: toy for endless toys, and the gate stops waiting`,
|
|
2762
3624
|
message: "game never reached the over state",
|
|
2763
3625
|
phase: "end",
|
|
2764
3626
|
severity: "warning"
|
|
@@ -2835,12 +3697,11 @@ var init_decisions = __esm({
|
|
|
2835
3697
|
});
|
|
2836
3698
|
|
|
2837
3699
|
// src/live/phases.ts
|
|
2838
|
-
var
|
|
3700
|
+
var 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, ladderFor, execStep, hasError, runBootChecks, runDesktopPass, overShotName, retryShotName, waitForOver, runStartBurst, verifyDeath, runLifecycle;
|
|
2839
3701
|
var init_phases = __esm({
|
|
2840
3702
|
"src/live/phases.ts"() {
|
|
2841
3703
|
"use strict";
|
|
2842
3704
|
init_decisions();
|
|
2843
|
-
END_BUDGET_MS = 45e3;
|
|
2844
3705
|
RETRY_NAV_MS = 8e3;
|
|
2845
3706
|
PLAY_STEPS = 7;
|
|
2846
3707
|
PLAY_STEP_MS = 850;
|
|
@@ -2853,11 +3714,98 @@ var init_phases = __esm({
|
|
|
2853
3714
|
STABILITY_CYCLES = 2;
|
|
2854
3715
|
START_BURST_TAPS = 3;
|
|
2855
3716
|
DESKTOP_FPS_MS = 2e3;
|
|
2856
|
-
|
|
3717
|
+
sleep4 = (ms) => new Promise((resolve2) => {
|
|
2857
3718
|
setTimeout(resolve2, ms);
|
|
2858
3719
|
});
|
|
2859
3720
|
jitterX = (step) => step * 37 % 121 - 60;
|
|
2860
3721
|
jitterY = (step) => step * 53 % 181 - 90;
|
|
3722
|
+
ladderFor = (verb, viewport) => {
|
|
3723
|
+
const cx = Math.round(viewport.width / 2);
|
|
3724
|
+
const cy = Math.round(viewport.height / 2);
|
|
3725
|
+
const tap = (step) => ({
|
|
3726
|
+
kind: "tap",
|
|
3727
|
+
x: Math.round(cx + jitterX(step)),
|
|
3728
|
+
y: Math.round(cy + jitterY(step))
|
|
3729
|
+
});
|
|
3730
|
+
const sweep = (step) => {
|
|
3731
|
+
const x = Math.round(cx + jitterX(step));
|
|
3732
|
+
const y = Math.round(cy + jitterY(step));
|
|
3733
|
+
return { kind: "drag", x1: x - DRAG_SPAN, x2: x + DRAG_SPAN, y1: y, y2: y };
|
|
3734
|
+
};
|
|
3735
|
+
const base = () => Array.from({ length: PLAY_STEPS }, (_, step) => {
|
|
3736
|
+
if (step === HOLD_STEP_INDEX) {
|
|
3737
|
+
return { kind: "hold", ms: HOLD_MS, x: tap(step).x, y: tap(step).y };
|
|
3738
|
+
}
|
|
3739
|
+
if (step === DRAG_STEP_INDEX) return sweep(step);
|
|
3740
|
+
return tap(step);
|
|
3741
|
+
});
|
|
3742
|
+
switch (verb) {
|
|
3743
|
+
case "swap": {
|
|
3744
|
+
const gap = 34;
|
|
3745
|
+
return [
|
|
3746
|
+
{ kind: "tap", x: cx - gap, y: Math.round(cy + jitterY(0)) },
|
|
3747
|
+
{ kind: "tap", x: cx + gap, y: Math.round(cy + jitterY(0)) },
|
|
3748
|
+
{ kind: "tap", x: Math.round(cx + jitterX(2)), y: cy - gap },
|
|
3749
|
+
{ kind: "tap", x: Math.round(cx + jitterX(2)), y: cy + gap },
|
|
3750
|
+
{ kind: "tap", x: cx - gap, y: Math.round(cy + jitterY(4)) },
|
|
3751
|
+
{ kind: "tap", x: cx + gap, y: Math.round(cy + jitterY(4)) },
|
|
3752
|
+
sweep(0),
|
|
3753
|
+
{ kind: "drag", x1: cx, x2: cx, y1: cy + DRAG_SPAN, y2: cy - DRAG_SPAN }
|
|
3754
|
+
];
|
|
3755
|
+
}
|
|
3756
|
+
case "place": {
|
|
3757
|
+
return [
|
|
3758
|
+
{ kind: "tap", x: cx - 80, y: cy - 60 },
|
|
3759
|
+
{ kind: "tap", x: cx + 40, y: cy + 20 },
|
|
3760
|
+
{ kind: "tap", x: cx + 80, y: cy - 40 },
|
|
3761
|
+
{ kind: "tap", x: cx - 30, y: cy + 70 },
|
|
3762
|
+
{ kind: "tap", x: Math.round(cx + jitterX(4)), y: cy },
|
|
3763
|
+
{ kind: "tap", x: cx - 70, y: cy + 80 },
|
|
3764
|
+
{ kind: "drag", x1: cx - 60, x2: cx + 60, y1: cy, y2: cy }
|
|
3765
|
+
];
|
|
3766
|
+
}
|
|
3767
|
+
case "type": {
|
|
3768
|
+
return [
|
|
3769
|
+
{ kind: "type", text: "frogoe" },
|
|
3770
|
+
tap(0),
|
|
3771
|
+
{ kind: "type", text: "glow" },
|
|
3772
|
+
tap(2),
|
|
3773
|
+
{ kind: "hold", ms: HOLD_MS, x: tap(3).x, y: tap(3).y },
|
|
3774
|
+
sweep(5),
|
|
3775
|
+
{ kind: "type", text: "game" },
|
|
3776
|
+
tap(6)
|
|
3777
|
+
];
|
|
3778
|
+
}
|
|
3779
|
+
case "draw": {
|
|
3780
|
+
return [
|
|
3781
|
+
{ kind: "drag", x1: cx - 90, x2: cx + 90, y1: cy - 50, y2: cy + 30 },
|
|
3782
|
+
{ kind: "tap", x: cx + 60, y: cy - 60 },
|
|
3783
|
+
{ kind: "drag", x1: cx + 80, x2: cx - 70, y1: cy - 20, y2: cy + 70 },
|
|
3784
|
+
{ kind: "hold", ms: HOLD_MS, x: cx, y: cy },
|
|
3785
|
+
{ kind: "drag", x1: cx - 40, x2: cx + 70, y1: cy + 90, y2: cy - 90 },
|
|
3786
|
+
{ kind: "tap", x: cx - 60, y: cy + 20 }
|
|
3787
|
+
];
|
|
3788
|
+
}
|
|
3789
|
+
default:
|
|
3790
|
+
return base();
|
|
3791
|
+
}
|
|
3792
|
+
};
|
|
3793
|
+
execStep = async (driver, step) => {
|
|
3794
|
+
switch (step.kind) {
|
|
3795
|
+
case "tap":
|
|
3796
|
+
await driver.tap(step.x, step.y);
|
|
3797
|
+
break;
|
|
3798
|
+
case "hold":
|
|
3799
|
+
await driver.hold(step.x, step.y, step.ms);
|
|
3800
|
+
break;
|
|
3801
|
+
case "drag":
|
|
3802
|
+
await driver.drag(step.x1, step.y1, step.x2, step.y2);
|
|
3803
|
+
break;
|
|
3804
|
+
case "type":
|
|
3805
|
+
await driver.type(step.text);
|
|
3806
|
+
break;
|
|
3807
|
+
}
|
|
3808
|
+
};
|
|
2861
3809
|
hasError = (findings) => findings.some((f) => f.severity === "error");
|
|
2862
3810
|
runBootChecks = async (driver, ctx) => {
|
|
2863
3811
|
const findings = [];
|
|
@@ -2900,7 +3848,7 @@ var init_phases = __esm({
|
|
|
2900
3848
|
return findings;
|
|
2901
3849
|
};
|
|
2902
3850
|
runDesktopPass = async (driver, ctx) => {
|
|
2903
|
-
const doSleep = ctx.sleep ??
|
|
3851
|
+
const doSleep = ctx.sleep ?? sleep4;
|
|
2904
3852
|
const findings = await runBootChecks(driver, ctx);
|
|
2905
3853
|
if (hasError(findings)) {
|
|
2906
3854
|
return { findings };
|
|
@@ -2921,8 +3869,8 @@ var init_phases = __esm({
|
|
|
2921
3869
|
};
|
|
2922
3870
|
overShotName = (cycle) => cycle === 0 ? "live-mobile-over.png" : `live-mobile-over-${cycle + 1}.png`;
|
|
2923
3871
|
retryShotName = (cycle) => cycle === 0 ? "live-mobile-retry.png" : `live-mobile-retry-${cycle + 1}.png`;
|
|
2924
|
-
waitForOver = async (driver, doSleep) => {
|
|
2925
|
-
for (let waited = 0; waited <
|
|
3872
|
+
waitForOver = async (driver, doSleep, budgetMs) => {
|
|
3873
|
+
for (let waited = 0; waited < budgetMs; waited += POLL_MS) {
|
|
2926
3874
|
await doSleep(POLL_MS);
|
|
2927
3875
|
if (await driver.gameState() === "over") {
|
|
2928
3876
|
return true;
|
|
@@ -2931,7 +3879,7 @@ var init_phases = __esm({
|
|
|
2931
3879
|
return false;
|
|
2932
3880
|
};
|
|
2933
3881
|
runStartBurst = async (driver, ctx) => {
|
|
2934
|
-
const doSleep = ctx.sleep ??
|
|
3882
|
+
const doSleep = ctx.sleep ?? sleep4;
|
|
2935
3883
|
for (let step = 0; step < START_BURST_TAPS; step++) {
|
|
2936
3884
|
const x = Math.round(ctx.viewport.width / 2 + jitterX(step));
|
|
2937
3885
|
const y = Math.round(ctx.viewport.height / 2 + jitterY(step));
|
|
@@ -2940,7 +3888,7 @@ var init_phases = __esm({
|
|
|
2940
3888
|
}
|
|
2941
3889
|
};
|
|
2942
3890
|
verifyDeath = async (driver, ctx, findings, cycle) => {
|
|
2943
|
-
const doSleep = ctx.sleep ??
|
|
3891
|
+
const doSleep = ctx.sleep ?? sleep4;
|
|
2944
3892
|
let events = await driver.finishEvents();
|
|
2945
3893
|
if (events.length === 0) {
|
|
2946
3894
|
await doSleep(GRACE_MS);
|
|
@@ -2961,7 +3909,7 @@ var init_phases = __esm({
|
|
|
2961
3909
|
return presence.retry;
|
|
2962
3910
|
};
|
|
2963
3911
|
runLifecycle = async (driver, ctx) => {
|
|
2964
|
-
const doSleep = ctx.sleep ??
|
|
3912
|
+
const doSleep = ctx.sleep ?? sleep4;
|
|
2965
3913
|
const settle = ctx.settleMs ?? 2e3;
|
|
2966
3914
|
const name = ctx.viewport.name;
|
|
2967
3915
|
const findings = [];
|
|
@@ -2971,6 +3919,7 @@ var init_phases = __esm({
|
|
|
2971
3919
|
if (hasError(boot)) {
|
|
2972
3920
|
return { findings, lifecycle: { ends: false, retryReloads: 0 }, playability: "no-input" };
|
|
2973
3921
|
}
|
|
3922
|
+
const ladder = ladderFor(ctx.verb ?? "tap", ctx.viewport);
|
|
2974
3923
|
const mark = await driver.fpsMark();
|
|
2975
3924
|
const hashes = [];
|
|
2976
3925
|
let streak = 0;
|
|
@@ -2979,16 +3928,8 @@ var init_phases = __esm({
|
|
|
2979
3928
|
let sawPaused = false;
|
|
2980
3929
|
let sawStuck = false;
|
|
2981
3930
|
let corrupt = null;
|
|
2982
|
-
for (
|
|
2983
|
-
|
|
2984
|
-
const y = Math.round(ctx.viewport.height / 2 + jitterY(step));
|
|
2985
|
-
if (step === HOLD_STEP_INDEX) {
|
|
2986
|
-
await driver.hold(x, y, HOLD_MS);
|
|
2987
|
-
} else if (step === DRAG_STEP_INDEX) {
|
|
2988
|
-
await driver.drag(x - DRAG_SPAN, y, x + DRAG_SPAN, y);
|
|
2989
|
-
} else {
|
|
2990
|
-
await driver.tap(x, y);
|
|
2991
|
-
}
|
|
3931
|
+
for (const step of ladder) {
|
|
3932
|
+
await execStep(driver, step);
|
|
2992
3933
|
await doSleep(PLAY_STEP_MS);
|
|
2993
3934
|
const state = await driver.gameState();
|
|
2994
3935
|
if (state === "over") {
|
|
@@ -3056,13 +3997,17 @@ var init_phases = __esm({
|
|
|
3056
3997
|
findings.push(audioFinding);
|
|
3057
3998
|
}
|
|
3058
3999
|
}
|
|
4000
|
+
const session = ctx.session ?? "blitz";
|
|
4001
|
+
const budget = endBudgetMs(session);
|
|
3059
4002
|
let ends = false;
|
|
3060
4003
|
let retryReloads = 0;
|
|
3061
|
-
if (!sawOver) {
|
|
3062
|
-
sawOver = await waitForOver(driver, doSleep);
|
|
4004
|
+
if (!sawOver && budget > 0) {
|
|
4005
|
+
sawOver = await waitForOver(driver, doSleep, budget);
|
|
3063
4006
|
}
|
|
3064
4007
|
if (!sawOver) {
|
|
3065
|
-
|
|
4008
|
+
if (warnsWhenItNeverEnds(session)) {
|
|
4009
|
+
findings.push(neverEndsFinding(END_BUDGET_MS));
|
|
4010
|
+
}
|
|
3066
4011
|
} else {
|
|
3067
4012
|
ends = true;
|
|
3068
4013
|
let canRetry = await verifyDeath(driver, ctx, findings, 0);
|
|
@@ -3089,7 +4034,7 @@ var init_phases = __esm({
|
|
|
3089
4034
|
await ctx.shot?.(retryShotName(cycle));
|
|
3090
4035
|
if (cycle < STABILITY_CYCLES - 1) {
|
|
3091
4036
|
await runStartBurst(driver, ctx);
|
|
3092
|
-
const overAgain = await waitForOver(driver, doSleep);
|
|
4037
|
+
const overAgain = await waitForOver(driver, doSleep, END_BUDGET_MS);
|
|
3093
4038
|
if (!overAgain) {
|
|
3094
4039
|
findings.push(neverEndsFinding(END_BUDGET_MS));
|
|
3095
4040
|
break;
|
|
@@ -3101,16 +4046,8 @@ var init_phases = __esm({
|
|
|
3101
4046
|
const throttleMark = await driver.fpsMark();
|
|
3102
4047
|
await driver.setCpuThrottling(THROTTLE_RATE);
|
|
3103
4048
|
await runStartBurst(driver, ctx);
|
|
3104
|
-
for (
|
|
3105
|
-
|
|
3106
|
-
const y = Math.round(ctx.viewport.height / 2 + jitterY(step));
|
|
3107
|
-
if (step === HOLD_STEP_INDEX) {
|
|
3108
|
-
await driver.hold(x, y, HOLD_MS);
|
|
3109
|
-
} else if (step === DRAG_STEP_INDEX) {
|
|
3110
|
-
await driver.drag(x - DRAG_SPAN, y, x + DRAG_SPAN, y);
|
|
3111
|
-
} else {
|
|
3112
|
-
await driver.tap(x, y);
|
|
3113
|
-
}
|
|
4049
|
+
for (const step of ladder) {
|
|
4050
|
+
await execStep(driver, step);
|
|
3114
4051
|
await doSleep(PLAY_STEP_MS);
|
|
3115
4052
|
}
|
|
3116
4053
|
const throttledBuckets = await driver.fpsSince(throttleMark);
|
|
@@ -3134,19 +4071,32 @@ var live_exports = {};
|
|
|
3134
4071
|
__export(live_exports, {
|
|
3135
4072
|
collectLive: () => collectLive
|
|
3136
4073
|
});
|
|
3137
|
-
import { mkdirSync as
|
|
3138
|
-
import
|
|
3139
|
-
var VIEWPORTS, waitForServer, collectLive;
|
|
4074
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
|
|
4075
|
+
import path15 from "path";
|
|
4076
|
+
var VIEWPORTS, declaredIntent, waitForServer, collectLive;
|
|
3140
4077
|
var init_live = __esm({
|
|
3141
4078
|
"src/live/index.ts"() {
|
|
3142
4079
|
"use strict";
|
|
3143
|
-
|
|
4080
|
+
init_src();
|
|
4081
|
+
init_launch();
|
|
4082
|
+
init_manager();
|
|
3144
4083
|
init_driver();
|
|
3145
4084
|
init_phases();
|
|
3146
4085
|
VIEWPORTS = [
|
|
3147
4086
|
{ height: 844, name: "mobile", width: 390 },
|
|
3148
4087
|
{ height: 800, name: "desktop", width: 1280 }
|
|
3149
4088
|
];
|
|
4089
|
+
declaredIntent = (dir) => {
|
|
4090
|
+
try {
|
|
4091
|
+
const brief = parseBrief(readFileSync10(path15.join(dir, "BRIEF.md"), "utf-8"));
|
|
4092
|
+
return {
|
|
4093
|
+
session: brief?.session !== void 0 && SESSIONS.includes(brief.session) ? brief.session : "blitz",
|
|
4094
|
+
verb: brief?.verb !== void 0 && VERBS.includes(brief.verb) ? brief.verb : "tap"
|
|
4095
|
+
};
|
|
4096
|
+
} catch {
|
|
4097
|
+
return { session: "blitz", verb: "tap" };
|
|
4098
|
+
}
|
|
4099
|
+
};
|
|
3150
4100
|
waitForServer = async (url) => {
|
|
3151
4101
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
3152
4102
|
try {
|
|
@@ -3156,11 +4106,11 @@ var init_live = __esm({
|
|
|
3156
4106
|
}
|
|
3157
4107
|
} catch {
|
|
3158
4108
|
}
|
|
3159
|
-
await
|
|
4109
|
+
await sleep4(500);
|
|
3160
4110
|
}
|
|
3161
4111
|
};
|
|
3162
4112
|
collectLive = async (options) => {
|
|
3163
|
-
const dir =
|
|
4113
|
+
const dir = path15.resolve(options.dir);
|
|
3164
4114
|
const settle = options.settleMs ?? 2e3;
|
|
3165
4115
|
const findings = [];
|
|
3166
4116
|
const screenshots = [];
|
|
@@ -3170,16 +4120,10 @@ var init_live = __esm({
|
|
|
3170
4120
|
};
|
|
3171
4121
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
3172
4122
|
const server = await startServer2(dir);
|
|
3173
|
-
const snapshotDir =
|
|
3174
|
-
|
|
3175
|
-
const
|
|
3176
|
-
const
|
|
3177
|
-
const browser = await puppeteer.launch({
|
|
3178
|
-
args: ["--no-sandbox", "--disable-gpu"],
|
|
3179
|
-
defaultViewport: null,
|
|
3180
|
-
executablePath,
|
|
3181
|
-
headless: true
|
|
3182
|
-
});
|
|
4123
|
+
const snapshotDir = path15.join(dir, "snapshots");
|
|
4124
|
+
mkdirSync10(snapshotDir, { recursive: true });
|
|
4125
|
+
const intent = declaredIntent(dir);
|
|
4126
|
+
const browser = await launchBrowser({ legacyDirs: [legacyCacheDir(dir)] });
|
|
3183
4127
|
try {
|
|
3184
4128
|
let serverReady = false;
|
|
3185
4129
|
for (const viewport of VIEWPORTS) {
|
|
@@ -3191,19 +4135,21 @@ var init_live = __esm({
|
|
|
3191
4135
|
size: { height: viewport2.height, width: viewport2.width }
|
|
3192
4136
|
});
|
|
3193
4137
|
const shot = async (name) => {
|
|
3194
|
-
|
|
3195
|
-
screenshots.push(
|
|
4138
|
+
writeFileSync6(path15.join(snapshotDir, name), await driver.screenshot());
|
|
4139
|
+
screenshots.push(path15.join("snapshots", name));
|
|
3196
4140
|
};
|
|
3197
4141
|
if (!serverReady) {
|
|
3198
4142
|
await waitForServer(server.urls.local);
|
|
3199
4143
|
serverReady = true;
|
|
3200
4144
|
}
|
|
3201
4145
|
await page.goto(server.urls.local, { timeout: 15e3, waitUntil: "domcontentloaded" });
|
|
3202
|
-
await
|
|
4146
|
+
await sleep4(settle);
|
|
3203
4147
|
if (viewport2.name === "mobile") {
|
|
3204
4148
|
const outcome = await runLifecycle(driver, {
|
|
3205
4149
|
settleMs: settle,
|
|
3206
4150
|
shot,
|
|
4151
|
+
verb: intent.verb,
|
|
4152
|
+
session: intent.session,
|
|
3207
4153
|
viewport: viewport2
|
|
3208
4154
|
});
|
|
3209
4155
|
findings.push(...outcome.findings);
|
|
@@ -3627,9 +4573,9 @@ var init_compose = __esm({
|
|
|
3627
4573
|
});
|
|
3628
4574
|
|
|
3629
4575
|
// src/manifest.ts
|
|
3630
|
-
import { createHash as
|
|
3631
|
-
import { existsSync as
|
|
3632
|
-
import
|
|
4576
|
+
import { createHash as createHash3 } from "crypto";
|
|
4577
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
4578
|
+
import path16 from "path";
|
|
3633
4579
|
var DESCRIPTION_CAP, descriptionFrom, buildManifest;
|
|
3634
4580
|
var init_manifest = __esm({
|
|
3635
4581
|
"src/manifest.ts"() {
|
|
@@ -3648,16 +4594,16 @@ var init_manifest = __esm({
|
|
|
3648
4594
|
return null;
|
|
3649
4595
|
};
|
|
3650
4596
|
buildManifest = (options) => {
|
|
3651
|
-
const dir =
|
|
3652
|
-
const briefSource =
|
|
4597
|
+
const dir = path16.resolve(options.dir);
|
|
4598
|
+
const briefSource = readFileSync11(path16.join(dir, "BRIEF.md"), "utf-8");
|
|
3653
4599
|
const brief = parseBrief(briefSource);
|
|
3654
4600
|
if (!brief?.title) return null;
|
|
3655
|
-
const pin =
|
|
3656
|
-
const artifact = options.artifactHtml ??
|
|
3657
|
-
const sha2562 =
|
|
4601
|
+
const pin = existsSync12(path16.join(dir, "frogoe.json")) ? JSON.parse(readFileSync11(path16.join(dir, "frogoe.json"), "utf-8")).contract ?? "0.1.0" : "0.1.0";
|
|
4602
|
+
const artifact = options.artifactHtml ?? readFileSync11(path16.join(dir, "dist", "index.html"), "utf-8");
|
|
4603
|
+
const sha2562 = createHash3("sha256").update(artifact, "utf-8").digest("hex");
|
|
3658
4604
|
const mediaSha = (file) => {
|
|
3659
|
-
const full =
|
|
3660
|
-
return
|
|
4605
|
+
const full = path16.join(dir, "dist", "assets", file);
|
|
4606
|
+
return existsSync12(full) ? createHash3("sha256").update(readFileSync11(full)).digest("hex") : null;
|
|
3661
4607
|
};
|
|
3662
4608
|
return {
|
|
3663
4609
|
artifact: "index.html",
|
|
@@ -3666,7 +4612,7 @@ var init_manifest = __esm({
|
|
|
3666
4612
|
description: descriptionFrom(briefSource),
|
|
3667
4613
|
entry: "index.html",
|
|
3668
4614
|
fonts: brief.fonts ?? null,
|
|
3669
|
-
icon:
|
|
4615
|
+
icon: existsSync12(path16.join(dir, "dist", "assets", "icon.png")) ? "assets/icon.png" : null,
|
|
3670
4616
|
iconSha256: mediaSha("icon.png"),
|
|
3671
4617
|
mood: brief.mood ?? null,
|
|
3672
4618
|
palette: {
|
|
@@ -3675,8 +4621,9 @@ var init_manifest = __esm({
|
|
|
3675
4621
|
fg: brief.fg ?? "",
|
|
3676
4622
|
...brief.outline ? { outline: brief.outline } : {}
|
|
3677
4623
|
},
|
|
3678
|
-
poster:
|
|
4624
|
+
poster: existsSync12(path16.join(dir, "dist", "assets", "poster.png")) ? "assets/poster.png" : null,
|
|
3679
4625
|
posterSha256: mediaSha("poster.png"),
|
|
4626
|
+
session: brief.session !== void 0 && SESSIONS.includes(brief.session) ? brief.session : "blitz",
|
|
3680
4627
|
title: brief.title,
|
|
3681
4628
|
verb: brief.verb ?? "tap"
|
|
3682
4629
|
};
|
|
@@ -3689,8 +4636,8 @@ var embed_exports = {};
|
|
|
3689
4636
|
__export(embed_exports, {
|
|
3690
4637
|
command: () => command4
|
|
3691
4638
|
});
|
|
3692
|
-
import { existsSync as
|
|
3693
|
-
import
|
|
4639
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
4640
|
+
import path17 from "path";
|
|
3694
4641
|
import { defineCommand as defineCommand4 } from "citty";
|
|
3695
4642
|
var dataUri2, command4;
|
|
3696
4643
|
var init_embed = __esm({
|
|
@@ -3700,15 +4647,15 @@ var init_embed = __esm({
|
|
|
3700
4647
|
init_card();
|
|
3701
4648
|
init_compose();
|
|
3702
4649
|
init_manifest();
|
|
3703
|
-
dataUri2 = (file) =>
|
|
4650
|
+
dataUri2 = (file) => existsSync13(file) ? `data:image/png;base64,${readFileSync12(file).toString("base64")}` : null;
|
|
3704
4651
|
command4 = defineCommand4({
|
|
3705
4652
|
args: {
|
|
3706
4653
|
dir: { type: "positional", required: false, description: "game folder (default: cwd)" }
|
|
3707
4654
|
},
|
|
3708
4655
|
async run({ args }) {
|
|
3709
4656
|
const dir = args.dir ? String(args.dir) : process.cwd();
|
|
3710
|
-
const artifactPath =
|
|
3711
|
-
if (!
|
|
4657
|
+
const artifactPath = path17.join(dir, "dist", "index.html");
|
|
4658
|
+
if (!existsSync13(artifactPath)) {
|
|
3712
4659
|
throw new Error(
|
|
3713
4660
|
"frogoe embed: no dist/index.html \u2014 run `frogoe bundle` first (and `frogoe check` before it: check \u2192 bundle \u2192 embed)"
|
|
3714
4661
|
);
|
|
@@ -3724,20 +4671,20 @@ var init_embed = __esm({
|
|
|
3724
4671
|
if (!manifest) {
|
|
3725
4672
|
throw new Error("frogoe embed: could not build the manifest from BRIEF.md");
|
|
3726
4673
|
}
|
|
3727
|
-
const posterDataUri = dataUri2(
|
|
3728
|
-
const iconDataUri = dataUri2(
|
|
3729
|
-
const gameHtml =
|
|
4674
|
+
const posterDataUri = dataUri2(path17.join(dir, "dist", "assets", "poster.png"));
|
|
4675
|
+
const iconDataUri = dataUri2(path17.join(dir, "dist", "assets", "icon.png"));
|
|
4676
|
+
const gameHtml = readFileSync12(artifactPath, "utf-8");
|
|
3730
4677
|
const { payloadB64 } = composePayload(gameHtml);
|
|
3731
4678
|
const html = composeEmbedHtml({ iconDataUri, manifest, payloadB64, posterDataUri });
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
4679
|
+
mkdirSync11(path17.join(dir, "dist"), { recursive: true });
|
|
4680
|
+
writeFileSync7(
|
|
4681
|
+
path17.join(dir, "dist", "manifest.json"),
|
|
3735
4682
|
`${JSON.stringify(manifest, null, 2)}
|
|
3736
4683
|
`,
|
|
3737
4684
|
"utf-8"
|
|
3738
4685
|
);
|
|
3739
|
-
const outPath =
|
|
3740
|
-
|
|
4686
|
+
const outPath = path17.join(dir, "dist", "embed.html");
|
|
4687
|
+
writeFileSync7(outPath, html, "utf-8");
|
|
3741
4688
|
console.log(` frogoe embed \u2192 ${outPath}`);
|
|
3742
4689
|
console.log(
|
|
3743
4690
|
` ${html.length.toLocaleString("en-US")} bytes \xB7 sandbox allow-scripts \xB7 sha256 ${manifest.artifactSha256.slice(0, 12)}`
|
|
@@ -3864,17 +4811,18 @@ var vision_exports = {};
|
|
|
3864
4811
|
__export(vision_exports, {
|
|
3865
4812
|
command: () => command5
|
|
3866
4813
|
});
|
|
3867
|
-
import { existsSync as
|
|
3868
|
-
import
|
|
4814
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
4815
|
+
import path18 from "path";
|
|
3869
4816
|
import { defineCommand as defineCommand5 } from "citty";
|
|
3870
4817
|
var HELPERS, PAGE_SCRIPT, show, metricLine, command5;
|
|
3871
4818
|
var init_vision = __esm({
|
|
3872
4819
|
"src/commands/vision.ts"() {
|
|
3873
4820
|
"use strict";
|
|
3874
4821
|
init_src();
|
|
4822
|
+
init_launch();
|
|
4823
|
+
init_manager();
|
|
3875
4824
|
init_art_eyes();
|
|
3876
4825
|
init_art_verify();
|
|
3877
|
-
init_browser();
|
|
3878
4826
|
HELPERS = [
|
|
3879
4827
|
luminance,
|
|
3880
4828
|
contrastRatio2,
|
|
@@ -3984,14 +4932,14 @@ var init_vision = __esm({
|
|
|
3984
4932
|
objects: { type: "boolean", description: "SPRITES objects only" }
|
|
3985
4933
|
},
|
|
3986
4934
|
async run({ args }) {
|
|
3987
|
-
const dir =
|
|
3988
|
-
const briefPath =
|
|
3989
|
-
if (!
|
|
4935
|
+
const dir = path18.resolve(args.dir ? String(args.dir) : process.cwd());
|
|
4936
|
+
const briefPath = path18.join(dir, "BRIEF.md");
|
|
4937
|
+
if (!existsSync14(briefPath)) {
|
|
3990
4938
|
throw new Error(
|
|
3991
4939
|
"frogoe vision: no BRIEF.md in this folder \u2014 run this from a game (frogoe init)"
|
|
3992
4940
|
);
|
|
3993
4941
|
}
|
|
3994
|
-
const brief = parseBrief(
|
|
4942
|
+
const brief = parseBrief(readFileSync13(briefPath, "utf-8"));
|
|
3995
4943
|
if (!brief) {
|
|
3996
4944
|
throw new Error("frogoe vision: BRIEF.md is present but unparsable \u2014 fill the frontmatter");
|
|
3997
4945
|
}
|
|
@@ -4011,12 +4959,9 @@ var init_vision = __esm({
|
|
|
4011
4959
|
const compact = !wantAll;
|
|
4012
4960
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
4013
4961
|
const server = await startServer2(dir);
|
|
4014
|
-
const
|
|
4015
|
-
const browser = await puppeteer.launch({
|
|
4016
|
-
args: ["--no-sandbox", "--disable-gpu"],
|
|
4962
|
+
const browser = await launchBrowser({
|
|
4017
4963
|
defaultViewport: { height: 844, width: 390 },
|
|
4018
|
-
|
|
4019
|
-
headless: true
|
|
4964
|
+
legacyDirs: [legacyCacheDir(dir)]
|
|
4020
4965
|
});
|
|
4021
4966
|
let report;
|
|
4022
4967
|
try {
|
|
@@ -4101,7 +5046,7 @@ var init_vision = __esm({
|
|
|
4101
5046
|
console.log(show(icon, pretty));
|
|
4102
5047
|
}
|
|
4103
5048
|
}
|
|
4104
|
-
if (!targeted && !
|
|
5049
|
+
if (!targeted && !existsSync14(path18.join(dir, "assets", "poster.js"))) {
|
|
4105
5050
|
console.log("\n(note: run `frogoe check` first \u2014 vision only looks, it never gates)");
|
|
4106
5051
|
}
|
|
4107
5052
|
},
|
|
@@ -4183,7 +5128,7 @@ var report_exports = {};
|
|
|
4183
5128
|
__export(report_exports, {
|
|
4184
5129
|
command: () => command8
|
|
4185
5130
|
});
|
|
4186
|
-
import { readFileSync as
|
|
5131
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
4187
5132
|
import { defineCommand as defineCommand8 } from "citty";
|
|
4188
5133
|
var command8;
|
|
4189
5134
|
var init_report = __esm({
|
|
@@ -4202,7 +5147,7 @@ var init_report = __esm({
|
|
|
4202
5147
|
console.log(`frogoe report: no sessions in ${dir} \u2014 play a run under \`frogoe run\` first`);
|
|
4203
5148
|
return;
|
|
4204
5149
|
}
|
|
4205
|
-
const records =
|
|
5150
|
+
const records = readFileSync14(file, "utf-8").split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
4206
5151
|
const s = summarizeRecords(records);
|
|
4207
5152
|
console.log(`
|
|
4208
5153
|
frogoe report \u2014 ${file}`);
|
|
@@ -4240,8 +5185,8 @@ var init_firewall = __esm({
|
|
|
4240
5185
|
return null;
|
|
4241
5186
|
}
|
|
4242
5187
|
};
|
|
4243
|
-
binaryBlocked = (
|
|
4244
|
-
const out = run(`--getappblocked "${
|
|
5188
|
+
binaryBlocked = (path19) => {
|
|
5189
|
+
const out = run(`--getappblocked "${path19}"`);
|
|
4245
5190
|
if (out === null) return null;
|
|
4246
5191
|
if (/blocked/iu.test(out)) return true;
|
|
4247
5192
|
if (/allowed/iu.test(out)) return false;
|
|
@@ -4297,224 +5242,6 @@ var init_plan = __esm({
|
|
|
4297
5242
|
}
|
|
4298
5243
|
});
|
|
4299
5244
|
|
|
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
5245
|
// src/commands/run.ts
|
|
4519
5246
|
var run_exports2 = {};
|
|
4520
5247
|
__export(run_exports2, {
|
|
@@ -4643,9 +5370,9 @@ var init_run2 = __esm({
|
|
|
4643
5370
|
|
|
4644
5371
|
// src/utils/skillsManifest.ts
|
|
4645
5372
|
import { execFile } from "child_process";
|
|
4646
|
-
import { createHash as
|
|
4647
|
-
import { existsSync as
|
|
4648
|
-
import { homedir } from "os";
|
|
5373
|
+
import { createHash as createHash4 } from "crypto";
|
|
5374
|
+
import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
|
|
5375
|
+
import { homedir as homedir2 } from "os";
|
|
4649
5376
|
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
4650
5377
|
import { promisify } from "util";
|
|
4651
5378
|
function isCoreSkill(name) {
|
|
@@ -4654,10 +5381,10 @@ function isCoreSkill(name) {
|
|
|
4654
5381
|
function listFilesSorted(dir) {
|
|
4655
5382
|
const out = [];
|
|
4656
5383
|
const walk = (d) => {
|
|
4657
|
-
for (const name of
|
|
5384
|
+
for (const name of readdirSync4(d)) {
|
|
4658
5385
|
if (name === ".DS_Store") continue;
|
|
4659
5386
|
const p = join(d, name);
|
|
4660
|
-
if (
|
|
5387
|
+
if (statSync4(p).isDirectory()) walk(p);
|
|
4661
5388
|
else out.push(p);
|
|
4662
5389
|
}
|
|
4663
5390
|
};
|
|
@@ -4666,13 +5393,13 @@ function listFilesSorted(dir) {
|
|
|
4666
5393
|
}
|
|
4667
5394
|
function hashSkillBundle(skillDir) {
|
|
4668
5395
|
const files = listFilesSorted(skillDir);
|
|
4669
|
-
const h =
|
|
5396
|
+
const h = createHash4("sha256");
|
|
4670
5397
|
for (const f of files) {
|
|
4671
5398
|
const rel = relative(skillDir, f).split(sep).join("/");
|
|
4672
5399
|
h.update(rel);
|
|
4673
5400
|
h.update("\0");
|
|
4674
5401
|
const ext = rel.slice(rel.lastIndexOf("."));
|
|
4675
|
-
const buf =
|
|
5402
|
+
const buf = readFileSync15(f);
|
|
4676
5403
|
if (TEXT_EXT.has(ext)) h.update(buf.toString("utf8").replace(/\r\n/g, "\n"), "utf8");
|
|
4677
5404
|
else h.update(buf);
|
|
4678
5405
|
h.update("\0");
|
|
@@ -4680,7 +5407,7 @@ function hashSkillBundle(skillDir) {
|
|
|
4680
5407
|
return { hash: h.digest("hex").slice(0, 16), files: files.length };
|
|
4681
5408
|
}
|
|
4682
5409
|
function buildManifest2(skillsRoot, meta) {
|
|
4683
|
-
const names =
|
|
5410
|
+
const names = readdirSync4(skillsRoot).filter((n) => existsSync15(join(skillsRoot, n, "SKILL.md"))).sort();
|
|
4684
5411
|
const skills = {};
|
|
4685
5412
|
for (const name of names) skills[name] = hashSkillBundle(join(skillsRoot, name));
|
|
4686
5413
|
return { source: meta.source, skills };
|
|
@@ -4696,7 +5423,7 @@ function agentFromDir(dir) {
|
|
|
4696
5423
|
}
|
|
4697
5424
|
function listSubdirs(dir) {
|
|
4698
5425
|
try {
|
|
4699
|
-
return
|
|
5426
|
+
return readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
4700
5427
|
} catch {
|
|
4701
5428
|
return [];
|
|
4702
5429
|
}
|
|
@@ -4705,7 +5432,7 @@ function discoverSkillRoots(base, scope) {
|
|
|
4705
5432
|
const candidates = [];
|
|
4706
5433
|
const add = (hostBase, host) => {
|
|
4707
5434
|
const dir = join(hostBase, host, "skills");
|
|
4708
|
-
if (
|
|
5435
|
+
if (existsSync15(dir) && statSync4(dir).isDirectory())
|
|
4709
5436
|
candidates.push({ dir, agent: agentLabel(host), scope });
|
|
4710
5437
|
};
|
|
4711
5438
|
for (const host of listSubdirs(base)) add(base, host);
|
|
@@ -4732,18 +5459,18 @@ function scopeForDir(dir, home, cwd) {
|
|
|
4732
5459
|
}
|
|
4733
5460
|
function locateInstall(skillNames, opts = {}) {
|
|
4734
5461
|
if (opts.dir) {
|
|
4735
|
-
return
|
|
5462
|
+
return existsSync15(opts.dir) ? {
|
|
4736
5463
|
dir: opts.dir,
|
|
4737
5464
|
agent: agentFromDir(opts.dir),
|
|
4738
|
-
scope: scopeForDir(opts.dir, opts.home ??
|
|
5465
|
+
scope: scopeForDir(opts.dir, opts.home ?? homedir2(), opts.cwd ?? process.cwd())
|
|
4739
5466
|
} : null;
|
|
4740
5467
|
}
|
|
4741
5468
|
const roots = [
|
|
4742
|
-
...discoverSkillRoots(opts.home ??
|
|
5469
|
+
...discoverSkillRoots(opts.home ?? homedir2(), "global"),
|
|
4743
5470
|
...discoverSkillRoots(opts.cwd ?? process.cwd(), "project")
|
|
4744
5471
|
];
|
|
4745
5472
|
for (const root of roots) {
|
|
4746
|
-
if (skillNames.some((n) =>
|
|
5473
|
+
if (skillNames.some((n) => existsSync15(join(root.dir, n, "SKILL.md")))) return root;
|
|
4747
5474
|
}
|
|
4748
5475
|
return null;
|
|
4749
5476
|
}
|
|
@@ -4751,7 +5478,7 @@ function hashInstalled(root, skillNames) {
|
|
|
4751
5478
|
const out = {};
|
|
4752
5479
|
for (const name of skillNames) {
|
|
4753
5480
|
const skillDir = join(root.dir, name);
|
|
4754
|
-
if (
|
|
5481
|
+
if (existsSync15(join(skillDir, "SKILL.md"))) out[name] = hashSkillBundle(skillDir);
|
|
4755
5482
|
}
|
|
4756
5483
|
return out;
|
|
4757
5484
|
}
|
|
@@ -4788,7 +5515,7 @@ function findRepoManifest(cwd = process.cwd()) {
|
|
|
4788
5515
|
let dir = cwd;
|
|
4789
5516
|
for (let i = 0; i < 16; i++) {
|
|
4790
5517
|
const p = join(dir, MANIFEST_FILE);
|
|
4791
|
-
if (
|
|
5518
|
+
if (existsSync15(p)) return p;
|
|
4792
5519
|
const parent = join(dir, "..");
|
|
4793
5520
|
if (parent === dir) break;
|
|
4794
5521
|
dir = parent;
|
|
@@ -4828,9 +5555,9 @@ async function remoteHeadSha(repoSlug) {
|
|
|
4828
5555
|
}
|
|
4829
5556
|
function resolveLocalManifest(source) {
|
|
4830
5557
|
const direct = source.endsWith(".json") ? source : join(source, MANIFEST_FILE);
|
|
4831
|
-
if (
|
|
5558
|
+
if (existsSync15(direct)) return JSON.parse(readFileSync15(direct, "utf8"));
|
|
4832
5559
|
const skillsRoot = source.endsWith("skills") ? source : join(source, "skills");
|
|
4833
|
-
if (
|
|
5560
|
+
if (existsSync15(skillsRoot)) return buildManifest2(skillsRoot, { source: skillsRoot });
|
|
4834
5561
|
throw new Error(`No skills manifest found at: ${source}`);
|
|
4835
5562
|
}
|
|
4836
5563
|
async function fetchRemoteManifest(source) {
|
|
@@ -4853,7 +5580,7 @@ async function resolveLatestManifest(source, cwd = process.cwd(), opts = {}) {
|
|
|
4853
5580
|
}
|
|
4854
5581
|
if (!source && !opts.canonical) {
|
|
4855
5582
|
const repoManifest = findRepoManifest(cwd);
|
|
4856
|
-
if (repoManifest) return JSON.parse(
|
|
5583
|
+
if (repoManifest) return JSON.parse(readFileSync15(repoManifest, "utf8"));
|
|
4857
5584
|
}
|
|
4858
5585
|
return fetchRemoteManifest(source);
|
|
4859
5586
|
}
|
|
@@ -5120,7 +5847,7 @@ import { defineCommand as defineCommand11, runMain } from "citty";
|
|
|
5120
5847
|
// package.json
|
|
5121
5848
|
var package_default = {
|
|
5122
5849
|
name: "frogoe",
|
|
5123
|
-
version: "0.
|
|
5850
|
+
version: "0.7.0",
|
|
5124
5851
|
description: "froge CLI \u2014 the agent's hands: init, add, run, check, bundle",
|
|
5125
5852
|
homepage: "https://github.com/frogoe/engine#readme",
|
|
5126
5853
|
bugs: "https://github.com/frogoe/engine/issues",
|