frogoe 0.6.0 → 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 +601 -231
- 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
|
}
|
|
@@ -1588,9 +1842,9 @@ var init_art_verify = __esm({
|
|
|
1588
1842
|
|
|
1589
1843
|
// src/net/tunnel.ts
|
|
1590
1844
|
import { spawn, spawnSync } from "child_process";
|
|
1591
|
-
import { existsSync as
|
|
1845
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, chmodSync, writeFileSync as writeFileSync4 } from "fs";
|
|
1592
1846
|
import os from "os";
|
|
1593
|
-
import
|
|
1847
|
+
import path8 from "path";
|
|
1594
1848
|
import { gunzipSync } from "zlib";
|
|
1595
1849
|
var URL_PATTERN, parseTunnelUrl, TAG_PATTERN, assertSafeTag, octalAt, extractSingleFile, ENV_PIN, assetName, binaryFileName, cacheBase, resolveLatestTag, mb, downloadWithProgress, binaryEchoes, resolveBinary, startTunnel;
|
|
1596
1850
|
var init_tunnel = __esm({
|
|
@@ -1647,9 +1901,9 @@ var init_tunnel = __esm({
|
|
|
1647
1901
|
};
|
|
1648
1902
|
binaryFileName = (platform) => platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
1649
1903
|
cacheBase = (platform, env, home) => {
|
|
1650
|
-
if (platform === "darwin") return
|
|
1651
|
-
if (platform === "win32") return env.LOCALAPPDATA ??
|
|
1652
|
-
return
|
|
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");
|
|
1653
1907
|
};
|
|
1654
1908
|
resolveLatestTag = async () => {
|
|
1655
1909
|
const res = await fetch("https://api.github.com/repos/cloudflare/cloudflared/releases/latest", {
|
|
@@ -1700,15 +1954,15 @@ var init_tunnel = __esm({
|
|
|
1700
1954
|
);
|
|
1701
1955
|
}
|
|
1702
1956
|
const tag = process.env[ENV_PIN] ? assertSafeTag(process.env[ENV_PIN]) : assertSafeTag(await resolveLatestTag());
|
|
1703
|
-
const root =
|
|
1704
|
-
const dir =
|
|
1705
|
-
const bin =
|
|
1706
|
-
const rootResolved =
|
|
1707
|
-
const binResolved =
|
|
1708
|
-
if (!binResolved.startsWith(rootResolved +
|
|
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)) {
|
|
1709
1963
|
throw new Error("cloudflared binary path escaped the frogoe cache \u2014 refusing to execute");
|
|
1710
1964
|
}
|
|
1711
|
-
if (
|
|
1965
|
+
if (existsSync7(bin) && binaryEchoes(bin, tag)) return { downloaded: false, path: bin };
|
|
1712
1966
|
onProgress?.(`downloading cloudflared ${tag} (~25 MB, once)\u2026`);
|
|
1713
1967
|
const res = await fetch(
|
|
1714
1968
|
`https://github.com/cloudflare/cloudflared/releases/download/${tag}/${asset}`
|
|
@@ -1719,8 +1973,8 @@ var init_tunnel = __esm({
|
|
|
1719
1973
|
});
|
|
1720
1974
|
const binary = asset.endsWith(".tgz") ? extractSingleFile(gunzipSync(raw), "cloudflared") : raw;
|
|
1721
1975
|
if (!binary) throw new Error("cloudflared archive did not contain the binary");
|
|
1722
|
-
|
|
1723
|
-
|
|
1976
|
+
mkdirSync4(dir, { recursive: true });
|
|
1977
|
+
writeFileSync4(bin, binary);
|
|
1724
1978
|
if (platform !== "win32") chmodSync(bin, 493);
|
|
1725
1979
|
if (!binaryEchoes(bin, tag)) {
|
|
1726
1980
|
throw new Error(
|
|
@@ -1805,8 +2059,8 @@ var init_tunnel = __esm({
|
|
|
1805
2059
|
});
|
|
1806
2060
|
|
|
1807
2061
|
// src/browser/lock.ts
|
|
1808
|
-
import { existsSync as
|
|
1809
|
-
import
|
|
2062
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, rmSync, statSync, utimesSync } from "fs";
|
|
2063
|
+
import path9 from "path";
|
|
1810
2064
|
var DEFAULT_LOCK_TIMINGS, LOCK_DIR_NAME, RECLAIM_DIR_NAME, isErrno, sleep2, tryAcquireDirLock, isDirLockStale, touch, reclaimStaleLock, withInstallLock;
|
|
1811
2065
|
var init_lock = __esm({
|
|
1812
2066
|
"src/browser/lock.ts"() {
|
|
@@ -1828,7 +2082,7 @@ var init_lock = __esm({
|
|
|
1828
2082
|
sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1829
2083
|
tryAcquireDirLock = (lockDir) => {
|
|
1830
2084
|
try {
|
|
1831
|
-
|
|
2085
|
+
mkdirSync5(lockDir, { recursive: false });
|
|
1832
2086
|
return true;
|
|
1833
2087
|
} catch (error) {
|
|
1834
2088
|
if (!isErrno(error, "EEXIST")) throw error;
|
|
@@ -1863,14 +2117,14 @@ var init_lock = __esm({
|
|
|
1863
2117
|
withInstallLock = async (fn, rootDir, options) => {
|
|
1864
2118
|
const timings = options?.timings ?? DEFAULT_LOCK_TIMINGS;
|
|
1865
2119
|
const log = options?.log ?? ((line) => console.error(line));
|
|
1866
|
-
const lockDir =
|
|
1867
|
-
const reclaimDir =
|
|
1868
|
-
if (!
|
|
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 });
|
|
1869
2123
|
let deadline = Date.now() + timings.staleMs;
|
|
1870
2124
|
const waitStart = Date.now();
|
|
1871
2125
|
let lastNoticeMs = 0;
|
|
1872
2126
|
for (; ; ) {
|
|
1873
|
-
if (
|
|
2127
|
+
if (existsSync8(reclaimDir)) {
|
|
1874
2128
|
if (isDirLockStale(reclaimDir, timings.staleMs)) {
|
|
1875
2129
|
rmSync(reclaimDir, { recursive: true, force: true });
|
|
1876
2130
|
}
|
|
@@ -1908,9 +2162,9 @@ var init_lock = __esm({
|
|
|
1908
2162
|
});
|
|
1909
2163
|
|
|
1910
2164
|
// src/browser/manager.ts
|
|
1911
|
-
import { cpSync as cpSync2, existsSync as
|
|
2165
|
+
import { cpSync as cpSync2, existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync2, renameSync, rmSync as rmSync2 } from "fs";
|
|
1912
2166
|
import { homedir } from "os";
|
|
1913
|
-
import
|
|
2167
|
+
import path10 from "path";
|
|
1914
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;
|
|
1915
2169
|
var init_manager = __esm({
|
|
1916
2170
|
"src/browser/manager.ts"() {
|
|
@@ -1919,8 +2173,8 @@ var init_manager = __esm({
|
|
|
1919
2173
|
init_lock();
|
|
1920
2174
|
CHROME_BUILD = "131.0.6778.204";
|
|
1921
2175
|
BROWSER_PATH_ENV = "FROGOE_BROWSER_PATH";
|
|
1922
|
-
CACHE_ROOT =
|
|
1923
|
-
CACHE_DIR =
|
|
2176
|
+
CACHE_ROOT = path10.join(cacheBase(process.platform, process.env, homedir()), "frogoe");
|
|
2177
|
+
CACHE_DIR = path10.join(CACHE_ROOT, "chrome");
|
|
1924
2178
|
ARCHIVE_CHECKSUMS = {
|
|
1925
2179
|
linux: "afaac86e302c4874245991a3d509d529c50ecdd447affff0cdc2b08520906c32",
|
|
1926
2180
|
mac: "933463c27a951d3fc153c408c4b8a96c89b9344379963f68a5ff9aa1718bcaf9",
|
|
@@ -1991,28 +2245,28 @@ see skills/frogoe-cli \u2192 references/live-sandbox.md)`,
|
|
|
1991
2245
|
);
|
|
1992
2246
|
};
|
|
1993
2247
|
findInCache = async (cacheDir, buildId) => {
|
|
1994
|
-
if (!
|
|
2248
|
+
if (!existsSync9(cacheDir)) return {};
|
|
1995
2249
|
const { Browser, detectBrowserPlatform, getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
1996
2250
|
const platform = detectBrowserPlatform();
|
|
1997
2251
|
const match = (await getInstalledBrowsers({ cacheDir })).find(
|
|
1998
2252
|
(entry) => entry.browser === Browser.CHROMEHEADLESSSHELL && entry.buildId === buildId && entry.platform === platform
|
|
1999
2253
|
);
|
|
2000
2254
|
if (!match) return {};
|
|
2001
|
-
if (
|
|
2255
|
+
if (existsSync9(match.executablePath)) return { executablePath: match.executablePath };
|
|
2002
2256
|
return { staleInstallPath: match.path };
|
|
2003
2257
|
};
|
|
2004
|
-
legacyCacheDir = (dir) =>
|
|
2258
|
+
legacyCacheDir = (dir) => path10.resolve(dir, "node_modules", ".frogoe-browser");
|
|
2005
2259
|
migrateLegacyCache = async (legacyDir, cacheDir, buildId, log) => {
|
|
2006
|
-
if (!
|
|
2260
|
+
if (!existsSync9(legacyDir)) return null;
|
|
2007
2261
|
const { Browser, getInstalledBrowsers } = await import("@puppeteer/browsers");
|
|
2008
2262
|
const legacy = (await getInstalledBrowsers({ cacheDir: legacyDir })).find(
|
|
2009
|
-
(entry) => entry.browser === Browser.CHROMEHEADLESSSHELL && entry.buildId === buildId &&
|
|
2263
|
+
(entry) => entry.browser === Browser.CHROMEHEADLESSSHELL && entry.buildId === buildId && existsSync9(entry.executablePath)
|
|
2010
2264
|
);
|
|
2011
2265
|
if (!legacy) return null;
|
|
2012
|
-
const browserRootName =
|
|
2013
|
-
const installName =
|
|
2014
|
-
const destination =
|
|
2015
|
-
|
|
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 });
|
|
2016
2270
|
rmSync2(destination, { recursive: true, force: true });
|
|
2017
2271
|
try {
|
|
2018
2272
|
try {
|
|
@@ -2034,16 +2288,16 @@ see skills/frogoe-cli \u2192 references/live-sandbox.md)`,
|
|
|
2034
2288
|
}
|
|
2035
2289
|
const found = await findInCache(cacheDir, buildId);
|
|
2036
2290
|
if (!found.executablePath) return null;
|
|
2037
|
-
rmSync2(
|
|
2291
|
+
rmSync2(path10.dirname(legacy.path), { recursive: true, force: true });
|
|
2038
2292
|
rmSync2(legacyDir, { recursive: true, force: true });
|
|
2039
2293
|
log?.(` browser: migrated the cached chrome-headless-shell into ${cacheDir} (one-time)`);
|
|
2040
2294
|
return found.executablePath;
|
|
2041
2295
|
};
|
|
2042
2296
|
pruneLegacyCache = (legacyDir, buildId) => {
|
|
2043
|
-
if (!
|
|
2297
|
+
if (!existsSync9(legacyDir)) return false;
|
|
2044
2298
|
try {
|
|
2045
|
-
const browserRoot =
|
|
2046
|
-
if (
|
|
2299
|
+
const browserRoot = path10.join(legacyDir, "chrome-headless-shell");
|
|
2300
|
+
if (existsSync9(browserRoot)) {
|
|
2047
2301
|
const entries = readdirSync2(browserRoot);
|
|
2048
2302
|
if (entries.length > 0 && !entries.some((name) => name.endsWith(`-${buildId}`))) {
|
|
2049
2303
|
return false;
|
|
@@ -2121,7 +2375,7 @@ see skills/frogoe-cli \u2192 references/live-sandbox.md)`,
|
|
|
2121
2375
|
const log = (line) => console.error(line);
|
|
2122
2376
|
const fromEnv = process.env[BROWSER_PATH_ENV]?.trim();
|
|
2123
2377
|
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
2124
|
-
if (!
|
|
2378
|
+
if (!existsSync9(fromEnv)) {
|
|
2125
2379
|
throw new Error(
|
|
2126
2380
|
`frogoe browser: ${BROWSER_PATH_ENV} is set but "${fromEnv}" does not exist \u2014 fix the path or unset the variable, then re-run`
|
|
2127
2381
|
);
|
|
@@ -2421,8 +2675,8 @@ var init_records = __esm({
|
|
|
2421
2675
|
});
|
|
2422
2676
|
|
|
2423
2677
|
// src/telemetry/session.ts
|
|
2424
|
-
import { appendFileSync, mkdirSync as
|
|
2425
|
-
import
|
|
2678
|
+
import { appendFileSync, mkdirSync as mkdirSync7, readdirSync as readdirSync3 } from "fs";
|
|
2679
|
+
import path11 from "path";
|
|
2426
2680
|
var pad, sessionStamp, createSessionStore, latestSessionFile;
|
|
2427
2681
|
var init_session = __esm({
|
|
2428
2682
|
"src/telemetry/session.ts"() {
|
|
@@ -2433,25 +2687,25 @@ var init_session = __esm({
|
|
|
2433
2687
|
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
2434
2688
|
};
|
|
2435
2689
|
createSessionStore = (gameDir, startedWall) => {
|
|
2436
|
-
const dir =
|
|
2690
|
+
const dir = path11.join(gameDir, ".frogoe", "sessions");
|
|
2437
2691
|
let file;
|
|
2438
2692
|
return {
|
|
2439
2693
|
file: () => file,
|
|
2440
2694
|
write: (records) => {
|
|
2441
2695
|
if (records.length === 0) return;
|
|
2442
2696
|
if (!file) {
|
|
2443
|
-
|
|
2444
|
-
file =
|
|
2697
|
+
mkdirSync7(dir, { recursive: true });
|
|
2698
|
+
file = path11.join(dir, `${sessionStamp(startedWall)}.jsonl`);
|
|
2445
2699
|
}
|
|
2446
2700
|
appendFileSync(file, records.map((r) => JSON.stringify(r)).join("\n") + "\n", "utf-8");
|
|
2447
2701
|
}
|
|
2448
2702
|
};
|
|
2449
2703
|
};
|
|
2450
2704
|
latestSessionFile = (gameDir) => {
|
|
2451
|
-
const dir =
|
|
2705
|
+
const dir = path11.join(gameDir, ".frogoe", "sessions");
|
|
2452
2706
|
try {
|
|
2453
2707
|
const files = readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
2454
|
-
return files.length > 0 ?
|
|
2708
|
+
return files.length > 0 ? path11.join(dir, files[files.length - 1] ?? "") : null;
|
|
2455
2709
|
} catch {
|
|
2456
2710
|
return null;
|
|
2457
2711
|
}
|
|
@@ -2464,9 +2718,9 @@ var run_exports = {};
|
|
|
2464
2718
|
__export(run_exports, {
|
|
2465
2719
|
startServer: () => startServer
|
|
2466
2720
|
});
|
|
2467
|
-
import { existsSync as
|
|
2721
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, statSync as statSync2, watch } from "fs";
|
|
2468
2722
|
import os3 from "os";
|
|
2469
|
-
import
|
|
2723
|
+
import path12 from "path";
|
|
2470
2724
|
import { createAdaptorServer } from "@hono/node-server";
|
|
2471
2725
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
2472
2726
|
import { Hono } from "hono";
|
|
@@ -2475,6 +2729,7 @@ var init_run = __esm({
|
|
|
2475
2729
|
"src/run.ts"() {
|
|
2476
2730
|
"use strict";
|
|
2477
2731
|
init_ip();
|
|
2732
|
+
init_font_proxy();
|
|
2478
2733
|
init_records();
|
|
2479
2734
|
init_session();
|
|
2480
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>`;
|
|
@@ -2494,8 +2749,8 @@ var init_run = __esm({
|
|
|
2494
2749
|
woff2: "font/woff2"
|
|
2495
2750
|
};
|
|
2496
2751
|
startServer = async (dir, requestedPort = 0, telemetry) => {
|
|
2497
|
-
const root =
|
|
2498
|
-
if (!
|
|
2752
|
+
const root = path12.resolve(dir);
|
|
2753
|
+
if (!existsSync10(path12.join(root, "index.html"))) {
|
|
2499
2754
|
throw new Error(`frogoe run: no index.html in ${root} \u2014 is this a game folder?`);
|
|
2500
2755
|
}
|
|
2501
2756
|
const clients = /* @__PURE__ */ new Set();
|
|
@@ -2537,6 +2792,22 @@ var init_run = __esm({
|
|
|
2537
2792
|
"/__frogoe/version",
|
|
2538
2793
|
(c) => c.text(String(version), 200, { "cache-control": "no-store" })
|
|
2539
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
|
+
});
|
|
2540
2811
|
app.post("/__frogoe/metrics", async (c) => {
|
|
2541
2812
|
try {
|
|
2542
2813
|
const payload = JSON.parse(await c.req.text());
|
|
@@ -2553,23 +2824,24 @@ var init_run = __esm({
|
|
|
2553
2824
|
});
|
|
2554
2825
|
app.get("*", (c) => {
|
|
2555
2826
|
const raw = decodeURIComponent(new URL(c.req.url).pathname);
|
|
2556
|
-
const safe =
|
|
2557
|
-
let file =
|
|
2827
|
+
const safe = path12.normalize(raw).replaceAll("\\", "/");
|
|
2828
|
+
let file = path12.join(root, safe === "/" ? "index.html" : safe);
|
|
2558
2829
|
if (!file.startsWith(root)) {
|
|
2559
2830
|
return c.text("forbidden", 403);
|
|
2560
2831
|
}
|
|
2561
|
-
if (
|
|
2562
|
-
file =
|
|
2832
|
+
if (existsSync10(file) && statSync2(file).isDirectory()) {
|
|
2833
|
+
file = path12.join(file, "index.html");
|
|
2563
2834
|
}
|
|
2564
|
-
if (!
|
|
2835
|
+
if (!existsSync10(file)) {
|
|
2565
2836
|
return c.text(`frogoe run: not found: ${raw}`, 404);
|
|
2566
2837
|
}
|
|
2567
|
-
const body =
|
|
2568
|
-
const ext =
|
|
2838
|
+
const body = readFileSync8(file);
|
|
2839
|
+
const ext = path12.extname(file).slice(1).toLowerCase();
|
|
2569
2840
|
const type = MIME2[ext] ?? "application/octet-stream";
|
|
2570
2841
|
if (ext === "html" || ext === "htm") {
|
|
2571
2842
|
const html = body.toString("utf-8");
|
|
2572
|
-
const
|
|
2843
|
+
const proxied = rewriteFontLinks(html);
|
|
2844
|
+
const injected = /<\/body>/iu.test(proxied) ? proxied.replace(/<\/body>/iu, `${buildDevScript(version)}</body>`) : proxied + buildDevScript(version);
|
|
2573
2845
|
return c.body(injected, 200, {
|
|
2574
2846
|
"cache-control": "no-store",
|
|
2575
2847
|
"content-type": type
|
|
@@ -2594,7 +2866,7 @@ var init_run = __esm({
|
|
|
2594
2866
|
const lan = lanInfo.ip ? `http://${lanInfo.ip}:${port}` : void 0;
|
|
2595
2867
|
let timer;
|
|
2596
2868
|
const watcher = watch(root, { recursive: true }, (_event, file) => {
|
|
2597
|
-
const first = file?.split(
|
|
2869
|
+
const first = file?.split(path12.sep)[0];
|
|
2598
2870
|
if (first === "snapshots" || first === ".frogoe" || first === "dist") {
|
|
2599
2871
|
return;
|
|
2600
2872
|
}
|
|
@@ -2628,8 +2900,8 @@ var init_run = __esm({
|
|
|
2628
2900
|
});
|
|
2629
2901
|
|
|
2630
2902
|
// src/raster.ts
|
|
2631
|
-
import { existsSync as
|
|
2632
|
-
import
|
|
2903
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
2904
|
+
import path13 from "path";
|
|
2633
2905
|
var SCENES, sleep3, rasterScriptFor, rasterizeArt;
|
|
2634
2906
|
var init_raster = __esm({
|
|
2635
2907
|
"src/raster.ts"() {
|
|
@@ -2644,17 +2916,17 @@ var init_raster = __esm({
|
|
|
2644
2916
|
{
|
|
2645
2917
|
draw: "drawPoster",
|
|
2646
2918
|
height: 1920,
|
|
2647
|
-
out:
|
|
2919
|
+
out: path13.join("dist", "assets", "poster.png"),
|
|
2648
2920
|
size: "w, h",
|
|
2649
|
-
source:
|
|
2921
|
+
source: path13.join("assets", "poster.js"),
|
|
2650
2922
|
width: 1080
|
|
2651
2923
|
},
|
|
2652
2924
|
{
|
|
2653
2925
|
draw: "drawIcon",
|
|
2654
2926
|
height: 1024,
|
|
2655
|
-
out:
|
|
2927
|
+
out: path13.join("dist", "assets", "icon.png"),
|
|
2656
2928
|
size: "size",
|
|
2657
|
-
source:
|
|
2929
|
+
source: path13.join("assets", "icon.js"),
|
|
2658
2930
|
width: 1024
|
|
2659
2931
|
}
|
|
2660
2932
|
];
|
|
@@ -2664,7 +2936,7 @@ var init_raster = __esm({
|
|
|
2664
2936
|
const analyze = scene.draw === "drawIcon" ? "analyzeIconCorners" : "analyzeTitleBand";
|
|
2665
2937
|
return `(async () => {
|
|
2666
2938
|
${runtimeSource()}
|
|
2667
|
-
const mod = await import("./assets/${
|
|
2939
|
+
const mod = await import("./assets/${path13.basename(scene.source)}");
|
|
2668
2940
|
const canvas = document.createElement("canvas");
|
|
2669
2941
|
canvas.id = "__frogoe_raster";
|
|
2670
2942
|
canvas.width = ${String(scene.width)};
|
|
@@ -2691,8 +2963,8 @@ var init_raster = __esm({
|
|
|
2691
2963
|
})()`;
|
|
2692
2964
|
};
|
|
2693
2965
|
rasterizeArt = async (options) => {
|
|
2694
|
-
const dir =
|
|
2695
|
-
const missing = SCENES.filter((scene) => !
|
|
2966
|
+
const dir = path13.resolve(options.dir);
|
|
2967
|
+
const missing = SCENES.filter((scene) => !existsSync11(path13.join(dir, scene.source)));
|
|
2696
2968
|
if (missing.length > 0) {
|
|
2697
2969
|
throw new Error(
|
|
2698
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`
|
|
@@ -2703,7 +2975,7 @@ var init_raster = __esm({
|
|
|
2703
2975
|
const browser = await launchBrowser({ legacyDirs: [legacyCacheDir(dir)] });
|
|
2704
2976
|
try {
|
|
2705
2977
|
const base = server.urls.local.replace(/\/$/u, "");
|
|
2706
|
-
const brief = parseBrief(
|
|
2978
|
+
const brief = parseBrief(readFileSync9(path13.join(dir, "BRIEF.md"), "utf-8"));
|
|
2707
2979
|
const palette = {
|
|
2708
2980
|
accent: brief?.accent ?? "#ff3b3b",
|
|
2709
2981
|
bg: brief?.bg ?? "#101418",
|
|
@@ -2749,7 +3021,7 @@ var init_raster = __esm({
|
|
|
2749
3021
|
}
|
|
2750
3022
|
} else {
|
|
2751
3023
|
const report = await page.evaluate("window.__frogoeArtReport");
|
|
2752
|
-
const gameSource =
|
|
3024
|
+
const gameSource = readFileSync9(path13.join(dir, "game.js"), "utf-8");
|
|
2753
3025
|
const verdict = verifyIconFullbleed(report, [...hexColorsOf(gameSource)]);
|
|
2754
3026
|
if (verdict !== null) throw new Error(verdict);
|
|
2755
3027
|
if (metrics.coverage < 0.08) {
|
|
@@ -2761,8 +3033,8 @@ var init_raster = __esm({
|
|
|
2761
3033
|
await sleep3(80);
|
|
2762
3034
|
const canvas = await page.$("#__frogoe_raster");
|
|
2763
3035
|
if (canvas === null) throw new Error(`bundle/art-crash \u2014 ${scene.source}: no canvas`);
|
|
2764
|
-
const outPath =
|
|
2765
|
-
|
|
3036
|
+
const outPath = path13.join(dir, scene.out);
|
|
3037
|
+
mkdirSync8(path13.dirname(outPath), { recursive: true });
|
|
2766
3038
|
await canvas.screenshot({ omitBackground: true, path: outPath, type: "png" });
|
|
2767
3039
|
files.push({ bytes: statSync3(outPath).size, file: scene.out });
|
|
2768
3040
|
} finally {
|
|
@@ -2784,8 +3056,8 @@ __export(bundle_exports, {
|
|
|
2784
3056
|
command: () => command2
|
|
2785
3057
|
});
|
|
2786
3058
|
import { defineCommand as defineCommand2 } from "citty";
|
|
2787
|
-
import { mkdirSync as
|
|
2788
|
-
import
|
|
3059
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
3060
|
+
import path14 from "path";
|
|
2789
3061
|
var command2;
|
|
2790
3062
|
var init_bundle2 = __esm({
|
|
2791
3063
|
"src/commands/bundle.ts"() {
|
|
@@ -2801,9 +3073,9 @@ var init_bundle2 = __esm({
|
|
|
2801
3073
|
async run({ args }) {
|
|
2802
3074
|
const dir = args.dir ? String(args.dir) : process.cwd();
|
|
2803
3075
|
const report = await bundle({ dir });
|
|
2804
|
-
const outPath = args.out ?
|
|
2805
|
-
|
|
2806
|
-
|
|
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");
|
|
2807
3079
|
const art = await rasterizeArt({ dir });
|
|
2808
3080
|
for (const warning of [...report.warnings, ...art.warnings]) {
|
|
2809
3081
|
console.log(` \u26A0 ${warning}`);
|
|
@@ -3062,6 +3334,9 @@ var init_driver = __esm({
|
|
|
3062
3334
|
});
|
|
3063
3335
|
await page.mouse.up();
|
|
3064
3336
|
},
|
|
3337
|
+
async type(text) {
|
|
3338
|
+
await page.keyboard.type(text, { delay: 30 });
|
|
3339
|
+
},
|
|
3065
3340
|
async clickRetryAwaitReload(timeoutMs) {
|
|
3066
3341
|
const interactable = `(() => {
|
|
3067
3342
|
const b = document.querySelector("[data-block-retry]");
|
|
@@ -3118,7 +3393,7 @@ var init_types = __esm({
|
|
|
3118
3393
|
});
|
|
3119
3394
|
|
|
3120
3395
|
// src/live/decisions.ts
|
|
3121
|
-
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;
|
|
3122
3397
|
var init_decisions = __esm({
|
|
3123
3398
|
"src/live/decisions.ts"() {
|
|
3124
3399
|
"use strict";
|
|
@@ -3126,6 +3401,10 @@ var init_decisions = __esm({
|
|
|
3126
3401
|
FPS_FLOOR2 = 30;
|
|
3127
3402
|
FPS_SUSTAINED_WINDOW = 3;
|
|
3128
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";
|
|
3129
3408
|
outlineFinding = (measures) => {
|
|
3130
3409
|
const bare = measures.filter((m) => !m.hasOutline);
|
|
3131
3410
|
if (bare.length === 0 || !bare[0]) {
|
|
@@ -3341,7 +3620,7 @@ var init_decisions = __esm({
|
|
|
3341
3620
|
neverEndsFinding = (budgetMs) => finding({
|
|
3342
3621
|
code: "live/never-ends",
|
|
3343
3622
|
file: "game.js",
|
|
3344
|
-
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`,
|
|
3345
3624
|
message: "game never reached the over state",
|
|
3346
3625
|
phase: "end",
|
|
3347
3626
|
severity: "warning"
|
|
@@ -3418,12 +3697,11 @@ var init_decisions = __esm({
|
|
|
3418
3697
|
});
|
|
3419
3698
|
|
|
3420
3699
|
// src/live/phases.ts
|
|
3421
|
-
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;
|
|
3422
3701
|
var init_phases = __esm({
|
|
3423
3702
|
"src/live/phases.ts"() {
|
|
3424
3703
|
"use strict";
|
|
3425
3704
|
init_decisions();
|
|
3426
|
-
END_BUDGET_MS = 45e3;
|
|
3427
3705
|
RETRY_NAV_MS = 8e3;
|
|
3428
3706
|
PLAY_STEPS = 7;
|
|
3429
3707
|
PLAY_STEP_MS = 850;
|
|
@@ -3441,6 +3719,93 @@ var init_phases = __esm({
|
|
|
3441
3719
|
});
|
|
3442
3720
|
jitterX = (step) => step * 37 % 121 - 60;
|
|
3443
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
|
+
};
|
|
3444
3809
|
hasError = (findings) => findings.some((f) => f.severity === "error");
|
|
3445
3810
|
runBootChecks = async (driver, ctx) => {
|
|
3446
3811
|
const findings = [];
|
|
@@ -3504,8 +3869,8 @@ var init_phases = __esm({
|
|
|
3504
3869
|
};
|
|
3505
3870
|
overShotName = (cycle) => cycle === 0 ? "live-mobile-over.png" : `live-mobile-over-${cycle + 1}.png`;
|
|
3506
3871
|
retryShotName = (cycle) => cycle === 0 ? "live-mobile-retry.png" : `live-mobile-retry-${cycle + 1}.png`;
|
|
3507
|
-
waitForOver = async (driver, doSleep) => {
|
|
3508
|
-
for (let waited = 0; waited <
|
|
3872
|
+
waitForOver = async (driver, doSleep, budgetMs) => {
|
|
3873
|
+
for (let waited = 0; waited < budgetMs; waited += POLL_MS) {
|
|
3509
3874
|
await doSleep(POLL_MS);
|
|
3510
3875
|
if (await driver.gameState() === "over") {
|
|
3511
3876
|
return true;
|
|
@@ -3554,6 +3919,7 @@ var init_phases = __esm({
|
|
|
3554
3919
|
if (hasError(boot)) {
|
|
3555
3920
|
return { findings, lifecycle: { ends: false, retryReloads: 0 }, playability: "no-input" };
|
|
3556
3921
|
}
|
|
3922
|
+
const ladder = ladderFor(ctx.verb ?? "tap", ctx.viewport);
|
|
3557
3923
|
const mark = await driver.fpsMark();
|
|
3558
3924
|
const hashes = [];
|
|
3559
3925
|
let streak = 0;
|
|
@@ -3562,16 +3928,8 @@ var init_phases = __esm({
|
|
|
3562
3928
|
let sawPaused = false;
|
|
3563
3929
|
let sawStuck = false;
|
|
3564
3930
|
let corrupt = null;
|
|
3565
|
-
for (
|
|
3566
|
-
|
|
3567
|
-
const y = Math.round(ctx.viewport.height / 2 + jitterY(step));
|
|
3568
|
-
if (step === HOLD_STEP_INDEX) {
|
|
3569
|
-
await driver.hold(x, y, HOLD_MS);
|
|
3570
|
-
} else if (step === DRAG_STEP_INDEX) {
|
|
3571
|
-
await driver.drag(x - DRAG_SPAN, y, x + DRAG_SPAN, y);
|
|
3572
|
-
} else {
|
|
3573
|
-
await driver.tap(x, y);
|
|
3574
|
-
}
|
|
3931
|
+
for (const step of ladder) {
|
|
3932
|
+
await execStep(driver, step);
|
|
3575
3933
|
await doSleep(PLAY_STEP_MS);
|
|
3576
3934
|
const state = await driver.gameState();
|
|
3577
3935
|
if (state === "over") {
|
|
@@ -3639,13 +3997,17 @@ var init_phases = __esm({
|
|
|
3639
3997
|
findings.push(audioFinding);
|
|
3640
3998
|
}
|
|
3641
3999
|
}
|
|
4000
|
+
const session = ctx.session ?? "blitz";
|
|
4001
|
+
const budget = endBudgetMs(session);
|
|
3642
4002
|
let ends = false;
|
|
3643
4003
|
let retryReloads = 0;
|
|
3644
|
-
if (!sawOver) {
|
|
3645
|
-
sawOver = await waitForOver(driver, doSleep);
|
|
4004
|
+
if (!sawOver && budget > 0) {
|
|
4005
|
+
sawOver = await waitForOver(driver, doSleep, budget);
|
|
3646
4006
|
}
|
|
3647
4007
|
if (!sawOver) {
|
|
3648
|
-
|
|
4008
|
+
if (warnsWhenItNeverEnds(session)) {
|
|
4009
|
+
findings.push(neverEndsFinding(END_BUDGET_MS));
|
|
4010
|
+
}
|
|
3649
4011
|
} else {
|
|
3650
4012
|
ends = true;
|
|
3651
4013
|
let canRetry = await verifyDeath(driver, ctx, findings, 0);
|
|
@@ -3672,7 +4034,7 @@ var init_phases = __esm({
|
|
|
3672
4034
|
await ctx.shot?.(retryShotName(cycle));
|
|
3673
4035
|
if (cycle < STABILITY_CYCLES - 1) {
|
|
3674
4036
|
await runStartBurst(driver, ctx);
|
|
3675
|
-
const overAgain = await waitForOver(driver, doSleep);
|
|
4037
|
+
const overAgain = await waitForOver(driver, doSleep, END_BUDGET_MS);
|
|
3676
4038
|
if (!overAgain) {
|
|
3677
4039
|
findings.push(neverEndsFinding(END_BUDGET_MS));
|
|
3678
4040
|
break;
|
|
@@ -3684,16 +4046,8 @@ var init_phases = __esm({
|
|
|
3684
4046
|
const throttleMark = await driver.fpsMark();
|
|
3685
4047
|
await driver.setCpuThrottling(THROTTLE_RATE);
|
|
3686
4048
|
await runStartBurst(driver, ctx);
|
|
3687
|
-
for (
|
|
3688
|
-
|
|
3689
|
-
const y = Math.round(ctx.viewport.height / 2 + jitterY(step));
|
|
3690
|
-
if (step === HOLD_STEP_INDEX) {
|
|
3691
|
-
await driver.hold(x, y, HOLD_MS);
|
|
3692
|
-
} else if (step === DRAG_STEP_INDEX) {
|
|
3693
|
-
await driver.drag(x - DRAG_SPAN, y, x + DRAG_SPAN, y);
|
|
3694
|
-
} else {
|
|
3695
|
-
await driver.tap(x, y);
|
|
3696
|
-
}
|
|
4049
|
+
for (const step of ladder) {
|
|
4050
|
+
await execStep(driver, step);
|
|
3697
4051
|
await doSleep(PLAY_STEP_MS);
|
|
3698
4052
|
}
|
|
3699
4053
|
const throttledBuckets = await driver.fpsSince(throttleMark);
|
|
@@ -3717,12 +4071,13 @@ var live_exports = {};
|
|
|
3717
4071
|
__export(live_exports, {
|
|
3718
4072
|
collectLive: () => collectLive
|
|
3719
4073
|
});
|
|
3720
|
-
import { mkdirSync as
|
|
3721
|
-
import
|
|
3722
|
-
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;
|
|
3723
4077
|
var init_live = __esm({
|
|
3724
4078
|
"src/live/index.ts"() {
|
|
3725
4079
|
"use strict";
|
|
4080
|
+
init_src();
|
|
3726
4081
|
init_launch();
|
|
3727
4082
|
init_manager();
|
|
3728
4083
|
init_driver();
|
|
@@ -3731,6 +4086,17 @@ var init_live = __esm({
|
|
|
3731
4086
|
{ height: 844, name: "mobile", width: 390 },
|
|
3732
4087
|
{ height: 800, name: "desktop", width: 1280 }
|
|
3733
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
|
+
};
|
|
3734
4100
|
waitForServer = async (url) => {
|
|
3735
4101
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
3736
4102
|
try {
|
|
@@ -3744,7 +4110,7 @@ var init_live = __esm({
|
|
|
3744
4110
|
}
|
|
3745
4111
|
};
|
|
3746
4112
|
collectLive = async (options) => {
|
|
3747
|
-
const dir =
|
|
4113
|
+
const dir = path15.resolve(options.dir);
|
|
3748
4114
|
const settle = options.settleMs ?? 2e3;
|
|
3749
4115
|
const findings = [];
|
|
3750
4116
|
const screenshots = [];
|
|
@@ -3754,8 +4120,9 @@ var init_live = __esm({
|
|
|
3754
4120
|
};
|
|
3755
4121
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
3756
4122
|
const server = await startServer2(dir);
|
|
3757
|
-
const snapshotDir =
|
|
3758
|
-
|
|
4123
|
+
const snapshotDir = path15.join(dir, "snapshots");
|
|
4124
|
+
mkdirSync10(snapshotDir, { recursive: true });
|
|
4125
|
+
const intent = declaredIntent(dir);
|
|
3759
4126
|
const browser = await launchBrowser({ legacyDirs: [legacyCacheDir(dir)] });
|
|
3760
4127
|
try {
|
|
3761
4128
|
let serverReady = false;
|
|
@@ -3768,8 +4135,8 @@ var init_live = __esm({
|
|
|
3768
4135
|
size: { height: viewport2.height, width: viewport2.width }
|
|
3769
4136
|
});
|
|
3770
4137
|
const shot = async (name) => {
|
|
3771
|
-
|
|
3772
|
-
screenshots.push(
|
|
4138
|
+
writeFileSync6(path15.join(snapshotDir, name), await driver.screenshot());
|
|
4139
|
+
screenshots.push(path15.join("snapshots", name));
|
|
3773
4140
|
};
|
|
3774
4141
|
if (!serverReady) {
|
|
3775
4142
|
await waitForServer(server.urls.local);
|
|
@@ -3781,6 +4148,8 @@ var init_live = __esm({
|
|
|
3781
4148
|
const outcome = await runLifecycle(driver, {
|
|
3782
4149
|
settleMs: settle,
|
|
3783
4150
|
shot,
|
|
4151
|
+
verb: intent.verb,
|
|
4152
|
+
session: intent.session,
|
|
3784
4153
|
viewport: viewport2
|
|
3785
4154
|
});
|
|
3786
4155
|
findings.push(...outcome.findings);
|
|
@@ -4204,9 +4573,9 @@ var init_compose = __esm({
|
|
|
4204
4573
|
});
|
|
4205
4574
|
|
|
4206
4575
|
// src/manifest.ts
|
|
4207
|
-
import { createHash as
|
|
4208
|
-
import { existsSync as
|
|
4209
|
-
import
|
|
4576
|
+
import { createHash as createHash3 } from "crypto";
|
|
4577
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
4578
|
+
import path16 from "path";
|
|
4210
4579
|
var DESCRIPTION_CAP, descriptionFrom, buildManifest;
|
|
4211
4580
|
var init_manifest = __esm({
|
|
4212
4581
|
"src/manifest.ts"() {
|
|
@@ -4225,16 +4594,16 @@ var init_manifest = __esm({
|
|
|
4225
4594
|
return null;
|
|
4226
4595
|
};
|
|
4227
4596
|
buildManifest = (options) => {
|
|
4228
|
-
const dir =
|
|
4229
|
-
const briefSource =
|
|
4597
|
+
const dir = path16.resolve(options.dir);
|
|
4598
|
+
const briefSource = readFileSync11(path16.join(dir, "BRIEF.md"), "utf-8");
|
|
4230
4599
|
const brief = parseBrief(briefSource);
|
|
4231
4600
|
if (!brief?.title) return null;
|
|
4232
|
-
const pin =
|
|
4233
|
-
const artifact = options.artifactHtml ??
|
|
4234
|
-
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");
|
|
4235
4604
|
const mediaSha = (file) => {
|
|
4236
|
-
const full =
|
|
4237
|
-
return
|
|
4605
|
+
const full = path16.join(dir, "dist", "assets", file);
|
|
4606
|
+
return existsSync12(full) ? createHash3("sha256").update(readFileSync11(full)).digest("hex") : null;
|
|
4238
4607
|
};
|
|
4239
4608
|
return {
|
|
4240
4609
|
artifact: "index.html",
|
|
@@ -4243,7 +4612,7 @@ var init_manifest = __esm({
|
|
|
4243
4612
|
description: descriptionFrom(briefSource),
|
|
4244
4613
|
entry: "index.html",
|
|
4245
4614
|
fonts: brief.fonts ?? null,
|
|
4246
|
-
icon:
|
|
4615
|
+
icon: existsSync12(path16.join(dir, "dist", "assets", "icon.png")) ? "assets/icon.png" : null,
|
|
4247
4616
|
iconSha256: mediaSha("icon.png"),
|
|
4248
4617
|
mood: brief.mood ?? null,
|
|
4249
4618
|
palette: {
|
|
@@ -4252,8 +4621,9 @@ var init_manifest = __esm({
|
|
|
4252
4621
|
fg: brief.fg ?? "",
|
|
4253
4622
|
...brief.outline ? { outline: brief.outline } : {}
|
|
4254
4623
|
},
|
|
4255
|
-
poster:
|
|
4624
|
+
poster: existsSync12(path16.join(dir, "dist", "assets", "poster.png")) ? "assets/poster.png" : null,
|
|
4256
4625
|
posterSha256: mediaSha("poster.png"),
|
|
4626
|
+
session: brief.session !== void 0 && SESSIONS.includes(brief.session) ? brief.session : "blitz",
|
|
4257
4627
|
title: brief.title,
|
|
4258
4628
|
verb: brief.verb ?? "tap"
|
|
4259
4629
|
};
|
|
@@ -4266,8 +4636,8 @@ var embed_exports = {};
|
|
|
4266
4636
|
__export(embed_exports, {
|
|
4267
4637
|
command: () => command4
|
|
4268
4638
|
});
|
|
4269
|
-
import { existsSync as
|
|
4270
|
-
import
|
|
4639
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
4640
|
+
import path17 from "path";
|
|
4271
4641
|
import { defineCommand as defineCommand4 } from "citty";
|
|
4272
4642
|
var dataUri2, command4;
|
|
4273
4643
|
var init_embed = __esm({
|
|
@@ -4277,15 +4647,15 @@ var init_embed = __esm({
|
|
|
4277
4647
|
init_card();
|
|
4278
4648
|
init_compose();
|
|
4279
4649
|
init_manifest();
|
|
4280
|
-
dataUri2 = (file) =>
|
|
4650
|
+
dataUri2 = (file) => existsSync13(file) ? `data:image/png;base64,${readFileSync12(file).toString("base64")}` : null;
|
|
4281
4651
|
command4 = defineCommand4({
|
|
4282
4652
|
args: {
|
|
4283
4653
|
dir: { type: "positional", required: false, description: "game folder (default: cwd)" }
|
|
4284
4654
|
},
|
|
4285
4655
|
async run({ args }) {
|
|
4286
4656
|
const dir = args.dir ? String(args.dir) : process.cwd();
|
|
4287
|
-
const artifactPath =
|
|
4288
|
-
if (!
|
|
4657
|
+
const artifactPath = path17.join(dir, "dist", "index.html");
|
|
4658
|
+
if (!existsSync13(artifactPath)) {
|
|
4289
4659
|
throw new Error(
|
|
4290
4660
|
"frogoe embed: no dist/index.html \u2014 run `frogoe bundle` first (and `frogoe check` before it: check \u2192 bundle \u2192 embed)"
|
|
4291
4661
|
);
|
|
@@ -4301,20 +4671,20 @@ var init_embed = __esm({
|
|
|
4301
4671
|
if (!manifest) {
|
|
4302
4672
|
throw new Error("frogoe embed: could not build the manifest from BRIEF.md");
|
|
4303
4673
|
}
|
|
4304
|
-
const posterDataUri = dataUri2(
|
|
4305
|
-
const iconDataUri = dataUri2(
|
|
4306
|
-
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");
|
|
4307
4677
|
const { payloadB64 } = composePayload(gameHtml);
|
|
4308
4678
|
const html = composeEmbedHtml({ iconDataUri, manifest, payloadB64, posterDataUri });
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4679
|
+
mkdirSync11(path17.join(dir, "dist"), { recursive: true });
|
|
4680
|
+
writeFileSync7(
|
|
4681
|
+
path17.join(dir, "dist", "manifest.json"),
|
|
4312
4682
|
`${JSON.stringify(manifest, null, 2)}
|
|
4313
4683
|
`,
|
|
4314
4684
|
"utf-8"
|
|
4315
4685
|
);
|
|
4316
|
-
const outPath =
|
|
4317
|
-
|
|
4686
|
+
const outPath = path17.join(dir, "dist", "embed.html");
|
|
4687
|
+
writeFileSync7(outPath, html, "utf-8");
|
|
4318
4688
|
console.log(` frogoe embed \u2192 ${outPath}`);
|
|
4319
4689
|
console.log(
|
|
4320
4690
|
` ${html.length.toLocaleString("en-US")} bytes \xB7 sandbox allow-scripts \xB7 sha256 ${manifest.artifactSha256.slice(0, 12)}`
|
|
@@ -4441,8 +4811,8 @@ var vision_exports = {};
|
|
|
4441
4811
|
__export(vision_exports, {
|
|
4442
4812
|
command: () => command5
|
|
4443
4813
|
});
|
|
4444
|
-
import { existsSync as
|
|
4445
|
-
import
|
|
4814
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
4815
|
+
import path18 from "path";
|
|
4446
4816
|
import { defineCommand as defineCommand5 } from "citty";
|
|
4447
4817
|
var HELPERS, PAGE_SCRIPT, show, metricLine, command5;
|
|
4448
4818
|
var init_vision = __esm({
|
|
@@ -4562,14 +4932,14 @@ var init_vision = __esm({
|
|
|
4562
4932
|
objects: { type: "boolean", description: "SPRITES objects only" }
|
|
4563
4933
|
},
|
|
4564
4934
|
async run({ args }) {
|
|
4565
|
-
const dir =
|
|
4566
|
-
const briefPath =
|
|
4567
|
-
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)) {
|
|
4568
4938
|
throw new Error(
|
|
4569
4939
|
"frogoe vision: no BRIEF.md in this folder \u2014 run this from a game (frogoe init)"
|
|
4570
4940
|
);
|
|
4571
4941
|
}
|
|
4572
|
-
const brief = parseBrief(
|
|
4942
|
+
const brief = parseBrief(readFileSync13(briefPath, "utf-8"));
|
|
4573
4943
|
if (!brief) {
|
|
4574
4944
|
throw new Error("frogoe vision: BRIEF.md is present but unparsable \u2014 fill the frontmatter");
|
|
4575
4945
|
}
|
|
@@ -4676,7 +5046,7 @@ var init_vision = __esm({
|
|
|
4676
5046
|
console.log(show(icon, pretty));
|
|
4677
5047
|
}
|
|
4678
5048
|
}
|
|
4679
|
-
if (!targeted && !
|
|
5049
|
+
if (!targeted && !existsSync14(path18.join(dir, "assets", "poster.js"))) {
|
|
4680
5050
|
console.log("\n(note: run `frogoe check` first \u2014 vision only looks, it never gates)");
|
|
4681
5051
|
}
|
|
4682
5052
|
},
|
|
@@ -4758,7 +5128,7 @@ var report_exports = {};
|
|
|
4758
5128
|
__export(report_exports, {
|
|
4759
5129
|
command: () => command8
|
|
4760
5130
|
});
|
|
4761
|
-
import { readFileSync as
|
|
5131
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
4762
5132
|
import { defineCommand as defineCommand8 } from "citty";
|
|
4763
5133
|
var command8;
|
|
4764
5134
|
var init_report = __esm({
|
|
@@ -4777,7 +5147,7 @@ var init_report = __esm({
|
|
|
4777
5147
|
console.log(`frogoe report: no sessions in ${dir} \u2014 play a run under \`frogoe run\` first`);
|
|
4778
5148
|
return;
|
|
4779
5149
|
}
|
|
4780
|
-
const records =
|
|
5150
|
+
const records = readFileSync14(file, "utf-8").split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
4781
5151
|
const s = summarizeRecords(records);
|
|
4782
5152
|
console.log(`
|
|
4783
5153
|
frogoe report \u2014 ${file}`);
|
|
@@ -4815,8 +5185,8 @@ var init_firewall = __esm({
|
|
|
4815
5185
|
return null;
|
|
4816
5186
|
}
|
|
4817
5187
|
};
|
|
4818
|
-
binaryBlocked = (
|
|
4819
|
-
const out = run(`--getappblocked "${
|
|
5188
|
+
binaryBlocked = (path19) => {
|
|
5189
|
+
const out = run(`--getappblocked "${path19}"`);
|
|
4820
5190
|
if (out === null) return null;
|
|
4821
5191
|
if (/blocked/iu.test(out)) return true;
|
|
4822
5192
|
if (/allowed/iu.test(out)) return false;
|
|
@@ -5000,8 +5370,8 @@ var init_run2 = __esm({
|
|
|
5000
5370
|
|
|
5001
5371
|
// src/utils/skillsManifest.ts
|
|
5002
5372
|
import { execFile } from "child_process";
|
|
5003
|
-
import { createHash as
|
|
5004
|
-
import { existsSync as
|
|
5373
|
+
import { createHash as createHash4 } from "crypto";
|
|
5374
|
+
import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
|
|
5005
5375
|
import { homedir as homedir2 } from "os";
|
|
5006
5376
|
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
5007
5377
|
import { promisify } from "util";
|
|
@@ -5023,13 +5393,13 @@ function listFilesSorted(dir) {
|
|
|
5023
5393
|
}
|
|
5024
5394
|
function hashSkillBundle(skillDir) {
|
|
5025
5395
|
const files = listFilesSorted(skillDir);
|
|
5026
|
-
const h =
|
|
5396
|
+
const h = createHash4("sha256");
|
|
5027
5397
|
for (const f of files) {
|
|
5028
5398
|
const rel = relative(skillDir, f).split(sep).join("/");
|
|
5029
5399
|
h.update(rel);
|
|
5030
5400
|
h.update("\0");
|
|
5031
5401
|
const ext = rel.slice(rel.lastIndexOf("."));
|
|
5032
|
-
const buf =
|
|
5402
|
+
const buf = readFileSync15(f);
|
|
5033
5403
|
if (TEXT_EXT.has(ext)) h.update(buf.toString("utf8").replace(/\r\n/g, "\n"), "utf8");
|
|
5034
5404
|
else h.update(buf);
|
|
5035
5405
|
h.update("\0");
|
|
@@ -5037,7 +5407,7 @@ function hashSkillBundle(skillDir) {
|
|
|
5037
5407
|
return { hash: h.digest("hex").slice(0, 16), files: files.length };
|
|
5038
5408
|
}
|
|
5039
5409
|
function buildManifest2(skillsRoot, meta) {
|
|
5040
|
-
const names = readdirSync4(skillsRoot).filter((n) =>
|
|
5410
|
+
const names = readdirSync4(skillsRoot).filter((n) => existsSync15(join(skillsRoot, n, "SKILL.md"))).sort();
|
|
5041
5411
|
const skills = {};
|
|
5042
5412
|
for (const name of names) skills[name] = hashSkillBundle(join(skillsRoot, name));
|
|
5043
5413
|
return { source: meta.source, skills };
|
|
@@ -5062,7 +5432,7 @@ function discoverSkillRoots(base, scope) {
|
|
|
5062
5432
|
const candidates = [];
|
|
5063
5433
|
const add = (hostBase, host) => {
|
|
5064
5434
|
const dir = join(hostBase, host, "skills");
|
|
5065
|
-
if (
|
|
5435
|
+
if (existsSync15(dir) && statSync4(dir).isDirectory())
|
|
5066
5436
|
candidates.push({ dir, agent: agentLabel(host), scope });
|
|
5067
5437
|
};
|
|
5068
5438
|
for (const host of listSubdirs(base)) add(base, host);
|
|
@@ -5089,7 +5459,7 @@ function scopeForDir(dir, home, cwd) {
|
|
|
5089
5459
|
}
|
|
5090
5460
|
function locateInstall(skillNames, opts = {}) {
|
|
5091
5461
|
if (opts.dir) {
|
|
5092
|
-
return
|
|
5462
|
+
return existsSync15(opts.dir) ? {
|
|
5093
5463
|
dir: opts.dir,
|
|
5094
5464
|
agent: agentFromDir(opts.dir),
|
|
5095
5465
|
scope: scopeForDir(opts.dir, opts.home ?? homedir2(), opts.cwd ?? process.cwd())
|
|
@@ -5100,7 +5470,7 @@ function locateInstall(skillNames, opts = {}) {
|
|
|
5100
5470
|
...discoverSkillRoots(opts.cwd ?? process.cwd(), "project")
|
|
5101
5471
|
];
|
|
5102
5472
|
for (const root of roots) {
|
|
5103
|
-
if (skillNames.some((n) =>
|
|
5473
|
+
if (skillNames.some((n) => existsSync15(join(root.dir, n, "SKILL.md")))) return root;
|
|
5104
5474
|
}
|
|
5105
5475
|
return null;
|
|
5106
5476
|
}
|
|
@@ -5108,7 +5478,7 @@ function hashInstalled(root, skillNames) {
|
|
|
5108
5478
|
const out = {};
|
|
5109
5479
|
for (const name of skillNames) {
|
|
5110
5480
|
const skillDir = join(root.dir, name);
|
|
5111
|
-
if (
|
|
5481
|
+
if (existsSync15(join(skillDir, "SKILL.md"))) out[name] = hashSkillBundle(skillDir);
|
|
5112
5482
|
}
|
|
5113
5483
|
return out;
|
|
5114
5484
|
}
|
|
@@ -5145,7 +5515,7 @@ function findRepoManifest(cwd = process.cwd()) {
|
|
|
5145
5515
|
let dir = cwd;
|
|
5146
5516
|
for (let i = 0; i < 16; i++) {
|
|
5147
5517
|
const p = join(dir, MANIFEST_FILE);
|
|
5148
|
-
if (
|
|
5518
|
+
if (existsSync15(p)) return p;
|
|
5149
5519
|
const parent = join(dir, "..");
|
|
5150
5520
|
if (parent === dir) break;
|
|
5151
5521
|
dir = parent;
|
|
@@ -5185,9 +5555,9 @@ async function remoteHeadSha(repoSlug) {
|
|
|
5185
5555
|
}
|
|
5186
5556
|
function resolveLocalManifest(source) {
|
|
5187
5557
|
const direct = source.endsWith(".json") ? source : join(source, MANIFEST_FILE);
|
|
5188
|
-
if (
|
|
5558
|
+
if (existsSync15(direct)) return JSON.parse(readFileSync15(direct, "utf8"));
|
|
5189
5559
|
const skillsRoot = source.endsWith("skills") ? source : join(source, "skills");
|
|
5190
|
-
if (
|
|
5560
|
+
if (existsSync15(skillsRoot)) return buildManifest2(skillsRoot, { source: skillsRoot });
|
|
5191
5561
|
throw new Error(`No skills manifest found at: ${source}`);
|
|
5192
5562
|
}
|
|
5193
5563
|
async function fetchRemoteManifest(source) {
|
|
@@ -5210,7 +5580,7 @@ async function resolveLatestManifest(source, cwd = process.cwd(), opts = {}) {
|
|
|
5210
5580
|
}
|
|
5211
5581
|
if (!source && !opts.canonical) {
|
|
5212
5582
|
const repoManifest = findRepoManifest(cwd);
|
|
5213
|
-
if (repoManifest) return JSON.parse(
|
|
5583
|
+
if (repoManifest) return JSON.parse(readFileSync15(repoManifest, "utf8"));
|
|
5214
5584
|
}
|
|
5215
5585
|
return fetchRemoteManifest(source);
|
|
5216
5586
|
}
|
|
@@ -5477,7 +5847,7 @@ import { defineCommand as defineCommand11, runMain } from "citty";
|
|
|
5477
5847
|
// package.json
|
|
5478
5848
|
var package_default = {
|
|
5479
5849
|
name: "frogoe",
|
|
5480
|
-
version: "0.
|
|
5850
|
+
version: "0.7.0",
|
|
5481
5851
|
description: "froge CLI \u2014 the agent's hands: init, add, run, check, bundle",
|
|
5482
5852
|
homepage: "https://github.com/frogoe/engine#readme",
|
|
5483
5853
|
bugs: "https://github.com/frogoe/engine/issues",
|