frogoe 0.2.2 → 0.3.1
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/README.md +84 -0
- package/dist/cli.js +618 -66
- package/dist/templates/_shared/AGENTS.md +14 -13
- package/dist/templates/_shared/CLAUDE.md +14 -13
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frogoe
|
|
2
|
+
|
|
3
|
+
**Write a closure. Ship a game.** A tiny game framework built for agents — one
|
|
4
|
+
`defineGame` closure with four nouns, and a CLI that scaffolds, serves, gates,
|
|
5
|
+
and bundles feed-ready games as single self-contained HTML files.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx frogoe init my-game
|
|
11
|
+
cd my-game
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or install globally:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g frogoe
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## The 60-second loop
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
frogoe init my-game # runnable folder: living stub game, BRIEF, pinned contract
|
|
24
|
+
cd my-game
|
|
25
|
+
frogoe run # live reload + phone QR (try --tunnel: works on any network)
|
|
26
|
+
frogoe add score-card # themeable HUD block, injected + idempotent
|
|
27
|
+
frogoe lint # fast static contract lint (stable finding codes, --json)
|
|
28
|
+
frogoe check # full gate: lint + headless Chrome — full lifecycle, fps,
|
|
29
|
+
# audio recovery, 4x phone-class throttle, screenshots
|
|
30
|
+
frogoe report # last playtest: fps dips, errors, wall-clock
|
|
31
|
+
frogoe bundle # ONE self-contained HTML — externals dissolved, zero
|
|
32
|
+
# runtime requests (only after check passes)
|
|
33
|
+
frogoe skills check # skill freshness (hash = per-bundle SHA16)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Built for agents
|
|
37
|
+
|
|
38
|
+
The real docs are [agent skills](https://github.com/frogoe/engine/tree/main/skills) —
|
|
39
|
+
the contract, creative direction, CLI loop, and HUD registry, written for the
|
|
40
|
+
AI that writes the game:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx skills add frogoe/engine
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Playtests under `frogoe run` are telemetered: fps dips, page errors and
|
|
47
|
+
lock-screens print live in your terminal and persist to
|
|
48
|
+
`.frogoe/sessions/*.jsonl`. `frogoe report` replays the last session — dips
|
|
49
|
+
below 30fps with their wall-clock moments.
|
|
50
|
+
|
|
51
|
+
## The contract
|
|
52
|
+
|
|
53
|
+
A game is one closure. The platform gives four nouns — everything visible is
|
|
54
|
+
yours:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { defineGame } from "frogoe";
|
|
58
|
+
|
|
59
|
+
defineGame(({ stage, input, loop, finish }) => {
|
|
60
|
+
let y = stage.height / 2;
|
|
61
|
+
let vy = 0;
|
|
62
|
+
|
|
63
|
+
input.on("down", () => {
|
|
64
|
+
vy = -300;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
loop.update = (dt) => {
|
|
68
|
+
vy += 900 * dt;
|
|
69
|
+
y += vy * dt;
|
|
70
|
+
};
|
|
71
|
+
loop.render = (ctx) => {
|
|
72
|
+
ctx.fillStyle = "#fff";
|
|
73
|
+
ctx.fillRect(stage.play.center - 12, y, 24, 24);
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The platform draws nothing — zero taste by construction. Games run from a
|
|
79
|
+
single pinned runtime (`frogoe.json` → `.frogoe/contract.js`), so every game
|
|
80
|
+
is an immutable artifact of exactly the contract it was built on.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
Apache-2.0
|
package/dist/cli.js
CHANGED
|
@@ -183,19 +183,18 @@ var init_init = __esm({
|
|
|
183
183
|
// src/add.ts
|
|
184
184
|
import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
185
185
|
import path2 from "path";
|
|
186
|
-
var
|
|
186
|
+
var parseBlock, blockMarker, injectIntoHtml, addBlock;
|
|
187
187
|
var init_add = __esm({
|
|
188
188
|
"src/add.ts"() {
|
|
189
189
|
"use strict";
|
|
190
190
|
init_init();
|
|
191
|
-
STYLE_PATTERN = /<style>([\s\S]*?)<\/style>/u;
|
|
192
|
-
MARKUP_PATTERN = /<\/style>\s*([\s\S]*)$/u;
|
|
193
191
|
parseBlock = (source) => {
|
|
194
|
-
const
|
|
195
|
-
const
|
|
192
|
+
const styleOpen = source.indexOf("<style>");
|
|
193
|
+
const styleClose = source.indexOf("</style>");
|
|
194
|
+
const css = styleOpen === -1 || styleClose === -1 || styleClose < styleOpen ? null : source.slice(styleOpen + "<style>".length, styleClose);
|
|
196
195
|
return {
|
|
197
|
-
css:
|
|
198
|
-
markup: (
|
|
196
|
+
css: css?.trim() ?? null,
|
|
197
|
+
markup: (styleClose === -1 ? "" : source.slice(styleClose + "</style>".length)).trim()
|
|
199
198
|
};
|
|
200
199
|
};
|
|
201
200
|
blockMarker = (name) => `<!-- frogoe:block:${name} -->`;
|
|
@@ -371,8 +370,8 @@ var init_fetch_policy = __esm({
|
|
|
371
370
|
url;
|
|
372
371
|
status;
|
|
373
372
|
};
|
|
374
|
-
sleep = (ms) => new Promise((
|
|
375
|
-
setTimeout(
|
|
373
|
+
sleep = (ms) => new Promise((resolve2) => {
|
|
374
|
+
setTimeout(resolve2, ms);
|
|
376
375
|
});
|
|
377
376
|
fetchWithPolicy = async (url, options) => {
|
|
378
377
|
const policy = { ...DEFAULT_FETCH_POLICY, ...options?.policy };
|
|
@@ -723,11 +722,32 @@ var init_bundle2 = __esm({
|
|
|
723
722
|
});
|
|
724
723
|
|
|
725
724
|
// ../lint/src/brief.ts
|
|
726
|
-
var stripComment, parseBrief;
|
|
725
|
+
var KEY_PATTERN, WS, stripComment, parseLine, parseBrief;
|
|
727
726
|
var init_brief = __esm({
|
|
728
727
|
"../lint/src/brief.ts"() {
|
|
729
728
|
"use strict";
|
|
730
|
-
|
|
729
|
+
KEY_PATTERN = /^[a-z-]+$/u;
|
|
730
|
+
WS = /\s/u;
|
|
731
|
+
stripComment = (value) => {
|
|
732
|
+
let cut = -1;
|
|
733
|
+
for (let i = 1; i < value.length; i += 1) {
|
|
734
|
+
if (value[i] === "#" && WS.test(value[i - 1] ?? "")) {
|
|
735
|
+
cut = i;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return (cut === -1 ? value : value.slice(0, cut)).trim().replace(/^["']|["']$/gu, "");
|
|
740
|
+
};
|
|
741
|
+
parseLine = (line) => {
|
|
742
|
+
let indent = 0;
|
|
743
|
+
while (indent < line.length && line[indent] === " ") indent += 1;
|
|
744
|
+
const body = line.slice(indent);
|
|
745
|
+
const colon = body.indexOf(":");
|
|
746
|
+
if (colon === -1) return null;
|
|
747
|
+
const key = body.slice(0, colon);
|
|
748
|
+
if (!KEY_PATTERN.test(key)) return null;
|
|
749
|
+
return { indent, key, rest: body.slice(colon + 1) };
|
|
750
|
+
};
|
|
731
751
|
parseBrief = (source) => {
|
|
732
752
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(source);
|
|
733
753
|
if (!match) {
|
|
@@ -739,20 +759,20 @@ var init_brief = __esm({
|
|
|
739
759
|
if (rawLine.trim() === "") {
|
|
740
760
|
continue;
|
|
741
761
|
}
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
if (nested && section === "palette") {
|
|
745
|
-
const key = nested[1];
|
|
746
|
-
brief[key] = stripComment(nested[2] ?? "");
|
|
762
|
+
const parsed = parseLine(rawLine);
|
|
763
|
+
if (!parsed) {
|
|
747
764
|
continue;
|
|
748
765
|
}
|
|
749
|
-
if (
|
|
750
|
-
|
|
766
|
+
if (parsed.indent >= 2 && section === "palette") {
|
|
767
|
+
brief[parsed.key] = stripComment(parsed.rest);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (parsed.indent === 0) {
|
|
771
|
+
section = parsed.key;
|
|
751
772
|
if (section === "palette") {
|
|
752
773
|
continue;
|
|
753
774
|
}
|
|
754
|
-
|
|
755
|
-
brief[key] = stripComment(top[2] ?? "");
|
|
775
|
+
brief[parsed.key] = stripComment(parsed.rest);
|
|
756
776
|
}
|
|
757
777
|
}
|
|
758
778
|
return brief;
|
|
@@ -1333,8 +1353,8 @@ var init_driver = __esm({
|
|
|
1333
1353
|
async hold(x, y, ms) {
|
|
1334
1354
|
await page.mouse.move(x, y);
|
|
1335
1355
|
await page.mouse.down();
|
|
1336
|
-
await new Promise((
|
|
1337
|
-
setTimeout(
|
|
1356
|
+
await new Promise((resolve2) => {
|
|
1357
|
+
setTimeout(resolve2, ms);
|
|
1338
1358
|
});
|
|
1339
1359
|
await page.mouse.up();
|
|
1340
1360
|
},
|
|
@@ -1342,8 +1362,8 @@ var init_driver = __esm({
|
|
|
1342
1362
|
await page.mouse.move(x1, y1);
|
|
1343
1363
|
await page.mouse.down();
|
|
1344
1364
|
await page.mouse.move(x2, y2, { steps: 6 });
|
|
1345
|
-
await new Promise((
|
|
1346
|
-
setTimeout(
|
|
1365
|
+
await new Promise((resolve2) => {
|
|
1366
|
+
setTimeout(resolve2, 80);
|
|
1347
1367
|
});
|
|
1348
1368
|
await page.mouse.up();
|
|
1349
1369
|
},
|
|
@@ -1369,8 +1389,8 @@ var init_driver = __esm({
|
|
|
1369
1389
|
if (Date.now() - started > grace) {
|
|
1370
1390
|
return false;
|
|
1371
1391
|
}
|
|
1372
|
-
await new Promise((
|
|
1373
|
-
setTimeout(
|
|
1392
|
+
await new Promise((resolve2) => {
|
|
1393
|
+
setTimeout(resolve2, 100);
|
|
1374
1394
|
});
|
|
1375
1395
|
}
|
|
1376
1396
|
const navigated = page.waitForNavigation({ timeout: timeoutMs, waitUntil: "domcontentloaded" }).then(() => true).catch(() => false);
|
|
@@ -1721,8 +1741,8 @@ var init_phases = __esm({
|
|
|
1721
1741
|
STABILITY_CYCLES = 2;
|
|
1722
1742
|
START_BURST_TAPS = 3;
|
|
1723
1743
|
DESKTOP_FPS_MS = 2e3;
|
|
1724
|
-
sleep2 = (ms) => new Promise((
|
|
1725
|
-
setTimeout(
|
|
1744
|
+
sleep2 = (ms) => new Promise((resolve2) => {
|
|
1745
|
+
setTimeout(resolve2, ms);
|
|
1726
1746
|
});
|
|
1727
1747
|
jitterX = (step) => step * 37 % 121 - 60;
|
|
1728
1748
|
jitterY = (step) => step * 53 % 181 - 90;
|
|
@@ -2375,9 +2395,9 @@ var init_run = __esm({
|
|
|
2375
2395
|
return c.body(body, 200, { "content-type": type });
|
|
2376
2396
|
});
|
|
2377
2397
|
const server = createAdaptorServer({ fetch: app.fetch });
|
|
2378
|
-
await new Promise((
|
|
2398
|
+
await new Promise((resolve2, reject) => {
|
|
2379
2399
|
server.once("error", reject);
|
|
2380
|
-
server.listen(requestedPort, "0.0.0.0", () =>
|
|
2400
|
+
server.listen(requestedPort, "0.0.0.0", () => resolve2());
|
|
2381
2401
|
});
|
|
2382
2402
|
const address = server.address();
|
|
2383
2403
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
@@ -2568,23 +2588,27 @@ var init_check3 = __esm({
|
|
|
2568
2588
|
args: {
|
|
2569
2589
|
dir: { type: "positional", required: false, description: "game folder (default: cwd)" },
|
|
2570
2590
|
json: { type: "boolean", description: "machine-readable findings" },
|
|
2571
|
-
live: {
|
|
2591
|
+
live: {
|
|
2592
|
+
type: "boolean",
|
|
2593
|
+
description: "deprecated no-op \u2014 the live sandbox always runs now"
|
|
2594
|
+
}
|
|
2572
2595
|
},
|
|
2573
2596
|
async run({ args }) {
|
|
2597
|
+
if (args.live) {
|
|
2598
|
+
console.error(" note: --live is deprecated \u2014 the live sandbox always runs now");
|
|
2599
|
+
}
|
|
2574
2600
|
const dir = args.dir ? String(args.dir) : process.cwd();
|
|
2575
2601
|
const result = checkProject(dir);
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
console.log(` snapshots: ${live.screenshots.join(", ")}`);
|
|
2587
|
-
}
|
|
2602
|
+
const { collectLive: collectLive2 } = await Promise.resolve().then(() => (init_live(), live_exports));
|
|
2603
|
+
console.log(" live pass: boot \u2192 play \u2192 end \u2192 retry (headless chrome)\u2026");
|
|
2604
|
+
const live = await collectLive2({ dir });
|
|
2605
|
+
result.findings = [...result.findings, ...live.findings].sort(
|
|
2606
|
+
(a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0)
|
|
2607
|
+
);
|
|
2608
|
+
result.errors = result.findings.filter((f) => f.severity === "error").length;
|
|
2609
|
+
result.warnings = result.findings.filter((f) => f.severity === "warning").length;
|
|
2610
|
+
if (live.screenshots.length > 0) {
|
|
2611
|
+
console.log(` snapshots: ${live.screenshots.join(", ")}`);
|
|
2588
2612
|
}
|
|
2589
2613
|
if (args.json) {
|
|
2590
2614
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -2597,7 +2621,9 @@ var init_check3 = __esm({
|
|
|
2597
2621
|
process.exitCode = 1;
|
|
2598
2622
|
}
|
|
2599
2623
|
},
|
|
2600
|
-
meta: {
|
|
2624
|
+
meta: {
|
|
2625
|
+
description: "full gate: contract lint + live browser sandbox (FPS, playability, HUD outline, audio recovery, screenshots)"
|
|
2626
|
+
}
|
|
2601
2627
|
});
|
|
2602
2628
|
}
|
|
2603
2629
|
});
|
|
@@ -2633,20 +2659,55 @@ var init_init2 = __esm({
|
|
|
2633
2659
|
}
|
|
2634
2660
|
});
|
|
2635
2661
|
|
|
2662
|
+
// src/commands/lint.ts
|
|
2663
|
+
var lint_exports = {};
|
|
2664
|
+
__export(lint_exports, {
|
|
2665
|
+
command: () => command5
|
|
2666
|
+
});
|
|
2667
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
2668
|
+
var command5;
|
|
2669
|
+
var init_lint = __esm({
|
|
2670
|
+
"src/commands/lint.ts"() {
|
|
2671
|
+
"use strict";
|
|
2672
|
+
init_check2();
|
|
2673
|
+
command5 = defineCommand5({
|
|
2674
|
+
args: {
|
|
2675
|
+
dir: { type: "positional", required: false, description: "game folder (default: cwd)" },
|
|
2676
|
+
json: { type: "boolean", description: "machine-readable findings" }
|
|
2677
|
+
},
|
|
2678
|
+
async run({ args }) {
|
|
2679
|
+
const dir = args.dir ? String(args.dir) : process.cwd();
|
|
2680
|
+
const result = checkProject(dir);
|
|
2681
|
+
if (args.json) {
|
|
2682
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2683
|
+
} else {
|
|
2684
|
+
console.log(formatFindings(result));
|
|
2685
|
+
console.log(`
|
|
2686
|
+
${result.errors} error(s), ${result.warnings} warning(s)`);
|
|
2687
|
+
}
|
|
2688
|
+
if (result.errors > 0) {
|
|
2689
|
+
process.exitCode = 1;
|
|
2690
|
+
}
|
|
2691
|
+
},
|
|
2692
|
+
meta: { description: "static contract lint only \u2014 fast iteration (check is the full gate)" }
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2636
2697
|
// src/commands/report.ts
|
|
2637
2698
|
var report_exports = {};
|
|
2638
2699
|
__export(report_exports, {
|
|
2639
|
-
command: () =>
|
|
2700
|
+
command: () => command6
|
|
2640
2701
|
});
|
|
2641
2702
|
import { readFileSync as readFileSync6 } from "fs";
|
|
2642
|
-
import { defineCommand as
|
|
2643
|
-
var
|
|
2703
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
2704
|
+
var command6;
|
|
2644
2705
|
var init_report = __esm({
|
|
2645
2706
|
"src/commands/report.ts"() {
|
|
2646
2707
|
"use strict";
|
|
2647
2708
|
init_records();
|
|
2648
2709
|
init_session();
|
|
2649
|
-
|
|
2710
|
+
command6 = defineCommand6({
|
|
2650
2711
|
args: {
|
|
2651
2712
|
dir: { type: "positional", required: false, description: "game folder (default: cwd)" }
|
|
2652
2713
|
},
|
|
@@ -2758,12 +2819,21 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync6, chmodSync, writeFil
|
|
|
2758
2819
|
import os3 from "os";
|
|
2759
2820
|
import path9 from "path";
|
|
2760
2821
|
import { gunzipSync } from "zlib";
|
|
2761
|
-
var URL_PATTERN, parseTunnelUrl, octalAt, extractSingleFile, ENV_PIN, assetName, binaryFileName, cacheBase, resolveLatestTag, mb, downloadWithProgress, binaryEchoes, resolveBinary, startTunnel;
|
|
2822
|
+
var URL_PATTERN, parseTunnelUrl, TAG_PATTERN, assertSafeTag, octalAt, extractSingleFile, ENV_PIN, assetName, binaryFileName, cacheBase, resolveLatestTag, mb, downloadWithProgress, binaryEchoes, resolveBinary, startTunnel;
|
|
2762
2823
|
var init_tunnel = __esm({
|
|
2763
2824
|
"src/net/tunnel.ts"() {
|
|
2764
2825
|
"use strict";
|
|
2765
2826
|
URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/iu;
|
|
2766
2827
|
parseTunnelUrl = (chunk) => URL_PATTERN.exec(chunk)?.[0];
|
|
2828
|
+
TAG_PATTERN = /^\d{4}\.\d+\.\d+(-[A-Za-z0-9.]+)?$/u;
|
|
2829
|
+
assertSafeTag = (tag) => {
|
|
2830
|
+
if (!TAG_PATTERN.test(tag)) {
|
|
2831
|
+
throw new Error(
|
|
2832
|
+
`cloudflared version "${tag}" is not a valid release tag (expected e.g. 2025.10.1)`
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
return tag;
|
|
2836
|
+
};
|
|
2767
2837
|
octalAt = (block, offset, length) => {
|
|
2768
2838
|
const raw = block.subarray(offset, offset + length).toString("utf-8");
|
|
2769
2839
|
const digits = raw.replace(/[\0 ]/gu, "");
|
|
@@ -2856,10 +2926,15 @@ var init_tunnel = __esm({
|
|
|
2856
2926
|
`cloudflared publishes no build for ${platform}/${process.arch} \u2014 install it and put \`cloudflared\` on PATH`
|
|
2857
2927
|
);
|
|
2858
2928
|
}
|
|
2859
|
-
const tag = process.env[ENV_PIN]
|
|
2929
|
+
const tag = process.env[ENV_PIN] ? assertSafeTag(process.env[ENV_PIN]) : assertSafeTag(await resolveLatestTag());
|
|
2860
2930
|
const root = path9.join(cacheBase(platform, process.env, os3.homedir()), "frogoe", "cloudflared");
|
|
2861
2931
|
const dir = path9.join(root, tag);
|
|
2862
2932
|
const bin = path9.join(dir, binaryFileName(platform));
|
|
2933
|
+
const rootResolved = path9.resolve(root);
|
|
2934
|
+
const binResolved = path9.resolve(bin);
|
|
2935
|
+
if (!binResolved.startsWith(rootResolved + path9.sep)) {
|
|
2936
|
+
throw new Error("cloudflared binary path escaped the frogoe cache \u2014 refusing to execute");
|
|
2937
|
+
}
|
|
2863
2938
|
if (existsSync6(bin) && binaryEchoes(bin, tag)) return { downloaded: false, path: bin };
|
|
2864
2939
|
onProgress?.(`downloading cloudflared ${tag} (~25 MB, once)\u2026`);
|
|
2865
2940
|
const res = await fetch(
|
|
@@ -2884,7 +2959,7 @@ var init_tunnel = __esm({
|
|
|
2884
2959
|
startTunnel = async (port, options) => {
|
|
2885
2960
|
const timeoutMs = options?.timeoutMs ?? 2e4;
|
|
2886
2961
|
const bin = await resolveBinary(options?.onProgress);
|
|
2887
|
-
return new Promise((
|
|
2962
|
+
return new Promise((resolve2, reject) => {
|
|
2888
2963
|
const child = spawn(
|
|
2889
2964
|
bin.path,
|
|
2890
2965
|
["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"],
|
|
@@ -2922,7 +2997,7 @@ var init_tunnel = __esm({
|
|
|
2922
2997
|
reject(new Error(tail ? `${base} \u2014 ${tail}` : base));
|
|
2923
2998
|
return;
|
|
2924
2999
|
}
|
|
2925
|
-
|
|
3000
|
+
resolve2({ exited, stop: killTree, url });
|
|
2926
3001
|
};
|
|
2927
3002
|
const timer = setTimeout(() => {
|
|
2928
3003
|
finish(
|
|
@@ -2959,10 +3034,10 @@ var init_tunnel = __esm({
|
|
|
2959
3034
|
// src/commands/run.ts
|
|
2960
3035
|
var run_exports2 = {};
|
|
2961
3036
|
__export(run_exports2, {
|
|
2962
|
-
command: () =>
|
|
3037
|
+
command: () => command7
|
|
2963
3038
|
});
|
|
2964
|
-
import { defineCommand as
|
|
2965
|
-
var NUDGE_MS, printQr, message,
|
|
3039
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
3040
|
+
var NUDGE_MS, printQr, message, command7;
|
|
2966
3041
|
var init_run2 = __esm({
|
|
2967
3042
|
"src/commands/run.ts"() {
|
|
2968
3043
|
"use strict";
|
|
@@ -2981,7 +3056,7 @@ var init_run2 = __esm({
|
|
|
2981
3056
|
});
|
|
2982
3057
|
};
|
|
2983
3058
|
message = (error) => error instanceof Error ? error.message : String(error);
|
|
2984
|
-
|
|
3059
|
+
command7 = defineCommand7({
|
|
2985
3060
|
args: {
|
|
2986
3061
|
dir: { type: "positional", required: false, description: "game folder (default: cwd)" },
|
|
2987
3062
|
port: { type: "string", description: "port (default: random free)" },
|
|
@@ -3081,13 +3156,486 @@ var init_run2 = __esm({
|
|
|
3081
3156
|
}
|
|
3082
3157
|
});
|
|
3083
3158
|
|
|
3159
|
+
// src/utils/skillsManifest.ts
|
|
3160
|
+
import { execFile } from "child_process";
|
|
3161
|
+
import { createHash as createHash2 } from "crypto";
|
|
3162
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
3163
|
+
import { homedir } from "os";
|
|
3164
|
+
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
3165
|
+
import { promisify } from "util";
|
|
3166
|
+
function isCoreSkill(name) {
|
|
3167
|
+
return name === ENTRY_SKILL || name.startsWith("frogoe-");
|
|
3168
|
+
}
|
|
3169
|
+
function listFilesSorted(dir) {
|
|
3170
|
+
const out = [];
|
|
3171
|
+
const walk = (d) => {
|
|
3172
|
+
for (const name of readdirSync3(d)) {
|
|
3173
|
+
if (name === ".DS_Store") continue;
|
|
3174
|
+
const p = join(d, name);
|
|
3175
|
+
if (statSync2(p).isDirectory()) walk(p);
|
|
3176
|
+
else out.push(p);
|
|
3177
|
+
}
|
|
3178
|
+
};
|
|
3179
|
+
walk(dir);
|
|
3180
|
+
return out.sort();
|
|
3181
|
+
}
|
|
3182
|
+
function hashSkillBundle(skillDir) {
|
|
3183
|
+
const files = listFilesSorted(skillDir);
|
|
3184
|
+
const h = createHash2("sha256");
|
|
3185
|
+
for (const f of files) {
|
|
3186
|
+
const rel = relative(skillDir, f).split(sep).join("/");
|
|
3187
|
+
h.update(rel);
|
|
3188
|
+
h.update("\0");
|
|
3189
|
+
const ext = rel.slice(rel.lastIndexOf("."));
|
|
3190
|
+
const buf = readFileSync7(f);
|
|
3191
|
+
if (TEXT_EXT.has(ext)) h.update(buf.toString("utf8").replace(/\r\n/g, "\n"), "utf8");
|
|
3192
|
+
else h.update(buf);
|
|
3193
|
+
h.update("\0");
|
|
3194
|
+
}
|
|
3195
|
+
return { hash: h.digest("hex").slice(0, 16), files: files.length };
|
|
3196
|
+
}
|
|
3197
|
+
function buildManifest(skillsRoot, meta) {
|
|
3198
|
+
const names = readdirSync3(skillsRoot).filter((n) => existsSync7(join(skillsRoot, n, "SKILL.md"))).sort();
|
|
3199
|
+
const skills = {};
|
|
3200
|
+
for (const name of names) skills[name] = hashSkillBundle(join(skillsRoot, name));
|
|
3201
|
+
return { source: meta.source, skills };
|
|
3202
|
+
}
|
|
3203
|
+
function agentLabel(hostDir) {
|
|
3204
|
+
const name = hostDir.replace(/^\.+/, "");
|
|
3205
|
+
return name === "claude" ? "claude-code" : name || "unknown";
|
|
3206
|
+
}
|
|
3207
|
+
function agentFromDir(dir) {
|
|
3208
|
+
const parts = dir.split(sep).filter(Boolean);
|
|
3209
|
+
const i = parts.lastIndexOf("skills");
|
|
3210
|
+
return agentLabel(i > 0 ? parts[i - 1] : parts[parts.length - 1] ?? "");
|
|
3211
|
+
}
|
|
3212
|
+
function listSubdirs(dir) {
|
|
3213
|
+
try {
|
|
3214
|
+
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
3215
|
+
} catch {
|
|
3216
|
+
return [];
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
function discoverSkillRoots(base, scope) {
|
|
3220
|
+
const candidates = [];
|
|
3221
|
+
const add = (hostBase, host) => {
|
|
3222
|
+
const dir = join(hostBase, host, "skills");
|
|
3223
|
+
if (existsSync7(dir) && statSync2(dir).isDirectory())
|
|
3224
|
+
candidates.push({ dir, agent: agentLabel(host), scope });
|
|
3225
|
+
};
|
|
3226
|
+
for (const host of listSubdirs(base)) add(base, host);
|
|
3227
|
+
const xdg = join(base, ".config");
|
|
3228
|
+
for (const host of listSubdirs(xdg)) add(xdg, host);
|
|
3229
|
+
return candidates.sort((a, b) => {
|
|
3230
|
+
if (a.agent !== b.agent) {
|
|
3231
|
+
if (a.agent === "claude-code") return -1;
|
|
3232
|
+
if (b.agent === "claude-code") return 1;
|
|
3233
|
+
return a.agent.localeCompare(b.agent);
|
|
3234
|
+
}
|
|
3235
|
+
return a.dir.localeCompare(b.dir);
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
function scopeForDir(dir, home, cwd) {
|
|
3239
|
+
const norm = (p) => {
|
|
3240
|
+
const r = resolve(p);
|
|
3241
|
+
return r.endsWith(sep) ? r : r + sep;
|
|
3242
|
+
};
|
|
3243
|
+
const d = norm(dir);
|
|
3244
|
+
if (d.startsWith(norm(cwd))) return "project";
|
|
3245
|
+
if (d.startsWith(norm(home))) return "global";
|
|
3246
|
+
return "project";
|
|
3247
|
+
}
|
|
3248
|
+
function locateInstall(skillNames, opts = {}) {
|
|
3249
|
+
if (opts.dir) {
|
|
3250
|
+
return existsSync7(opts.dir) ? {
|
|
3251
|
+
dir: opts.dir,
|
|
3252
|
+
agent: agentFromDir(opts.dir),
|
|
3253
|
+
scope: scopeForDir(opts.dir, opts.home ?? homedir(), opts.cwd ?? process.cwd())
|
|
3254
|
+
} : null;
|
|
3255
|
+
}
|
|
3256
|
+
const roots = [
|
|
3257
|
+
...discoverSkillRoots(opts.home ?? homedir(), "global"),
|
|
3258
|
+
...discoverSkillRoots(opts.cwd ?? process.cwd(), "project")
|
|
3259
|
+
];
|
|
3260
|
+
for (const root of roots) {
|
|
3261
|
+
if (skillNames.some((n) => existsSync7(join(root.dir, n, "SKILL.md")))) return root;
|
|
3262
|
+
}
|
|
3263
|
+
return null;
|
|
3264
|
+
}
|
|
3265
|
+
function hashInstalled(root, skillNames) {
|
|
3266
|
+
const out = {};
|
|
3267
|
+
for (const name of skillNames) {
|
|
3268
|
+
const skillDir = join(root.dir, name);
|
|
3269
|
+
if (existsSync7(join(skillDir, "SKILL.md"))) out[name] = hashSkillBundle(skillDir);
|
|
3270
|
+
}
|
|
3271
|
+
return out;
|
|
3272
|
+
}
|
|
3273
|
+
function diffSkills(installed, latest) {
|
|
3274
|
+
const skills = [];
|
|
3275
|
+
const summary = { current: 0, outdated: 0, missing: 0, coreMissing: 0 };
|
|
3276
|
+
for (const name of Object.keys(latest.skills).sort()) {
|
|
3277
|
+
const latestEntry = latest.skills[name];
|
|
3278
|
+
const installedEntry = installed[name];
|
|
3279
|
+
let status;
|
|
3280
|
+
if (!installedEntry) status = "missing";
|
|
3281
|
+
else if (installedEntry.hash === latestEntry.hash) status = "current";
|
|
3282
|
+
else status = "outdated";
|
|
3283
|
+
if (status === "current") summary.current++;
|
|
3284
|
+
else if (status === "outdated") summary.outdated++;
|
|
3285
|
+
else {
|
|
3286
|
+
summary.missing++;
|
|
3287
|
+
if (isCoreSkill(name)) summary.coreMissing++;
|
|
3288
|
+
}
|
|
3289
|
+
skills.push({
|
|
3290
|
+
name,
|
|
3291
|
+
status,
|
|
3292
|
+
installedHash: installedEntry?.hash,
|
|
3293
|
+
latestHash: latestEntry.hash
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
return {
|
|
3297
|
+
updateAvailable: summary.outdated > 0 || summary.coreMissing > 0,
|
|
3298
|
+
summary,
|
|
3299
|
+
skills
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
function findRepoManifest(cwd = process.cwd()) {
|
|
3303
|
+
let dir = cwd;
|
|
3304
|
+
for (let i = 0; i < 16; i++) {
|
|
3305
|
+
const p = join(dir, MANIFEST_FILE);
|
|
3306
|
+
if (existsSync7(p)) return p;
|
|
3307
|
+
const parent = join(dir, "..");
|
|
3308
|
+
if (parent === dir) break;
|
|
3309
|
+
dir = parent;
|
|
3310
|
+
}
|
|
3311
|
+
return null;
|
|
3312
|
+
}
|
|
3313
|
+
function asSkillsManifest(data, sourceLabel) {
|
|
3314
|
+
const m = data;
|
|
3315
|
+
if (!m || typeof m !== "object" || typeof m.skills !== "object" || m.skills === null) {
|
|
3316
|
+
throw new Error(`Malformed skills manifest from ${sourceLabel}`);
|
|
3317
|
+
}
|
|
3318
|
+
return m;
|
|
3319
|
+
}
|
|
3320
|
+
async function fetchManifest(url) {
|
|
3321
|
+
const controller = new AbortController();
|
|
3322
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
3323
|
+
try {
|
|
3324
|
+
const res = await fetch(url, { signal: controller.signal, headers: { Connection: "close" } });
|
|
3325
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
3326
|
+
return asSkillsManifest(await res.json(), url);
|
|
3327
|
+
} finally {
|
|
3328
|
+
clearTimeout(timeout);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
async function remoteHeadSha(repoSlug) {
|
|
3332
|
+
try {
|
|
3333
|
+
const { stdout } = await execFileAsync(
|
|
3334
|
+
"git",
|
|
3335
|
+
["ls-remote", `https://github.com/${repoSlug}.git`, "refs/heads/main"],
|
|
3336
|
+
{ timeout: FETCH_TIMEOUT_MS, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }
|
|
3337
|
+
);
|
|
3338
|
+
const sha = stdout.split(/\s+/)[0]?.trim() ?? "";
|
|
3339
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
3340
|
+
} catch {
|
|
3341
|
+
return null;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
function resolveLocalManifest(source) {
|
|
3345
|
+
const direct = source.endsWith(".json") ? source : join(source, MANIFEST_FILE);
|
|
3346
|
+
if (existsSync7(direct)) return JSON.parse(readFileSync7(direct, "utf8"));
|
|
3347
|
+
const skillsRoot = source.endsWith("skills") ? source : join(source, "skills");
|
|
3348
|
+
if (existsSync7(skillsRoot)) return buildManifest(skillsRoot, { source: skillsRoot });
|
|
3349
|
+
throw new Error(`No skills manifest found at: ${source}`);
|
|
3350
|
+
}
|
|
3351
|
+
async function fetchRemoteManifest(source) {
|
|
3352
|
+
if (source?.startsWith("http")) return fetchManifest(source);
|
|
3353
|
+
const repoSlug = source ?? DEFAULT_REPO_SLUG;
|
|
3354
|
+
const sha = await remoteHeadSha(repoSlug);
|
|
3355
|
+
if (sha) {
|
|
3356
|
+
try {
|
|
3357
|
+
return await fetchManifest(
|
|
3358
|
+
`https://raw.githubusercontent.com/${repoSlug}/${sha}/${MANIFEST_FILE}`
|
|
3359
|
+
);
|
|
3360
|
+
} catch {
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
return fetchManifest(`https://raw.githubusercontent.com/${repoSlug}/main/${MANIFEST_FILE}`);
|
|
3364
|
+
}
|
|
3365
|
+
async function resolveLatestManifest(source, cwd = process.cwd(), opts = {}) {
|
|
3366
|
+
if (source && (source.startsWith(".") || isAbsolute(source))) {
|
|
3367
|
+
return resolveLocalManifest(source);
|
|
3368
|
+
}
|
|
3369
|
+
if (!source && !opts.canonical) {
|
|
3370
|
+
const repoManifest = findRepoManifest(cwd);
|
|
3371
|
+
if (repoManifest) return JSON.parse(readFileSync7(repoManifest, "utf8"));
|
|
3372
|
+
}
|
|
3373
|
+
return fetchRemoteManifest(source);
|
|
3374
|
+
}
|
|
3375
|
+
async function checkSkills(opts = {}) {
|
|
3376
|
+
const latest = await resolveLatestManifest(opts.source, opts.cwd, { canonical: opts.canonical });
|
|
3377
|
+
const skillNames = Object.keys(latest.skills);
|
|
3378
|
+
const root = locateInstall(skillNames, { dir: opts.dir, cwd: opts.cwd, home: opts.home });
|
|
3379
|
+
const installed = root ? hashInstalled(root, skillNames) : {};
|
|
3380
|
+
const diff = diffSkills(installed, latest);
|
|
3381
|
+
return {
|
|
3382
|
+
location: root?.dir ?? null,
|
|
3383
|
+
agent: root?.agent ?? null,
|
|
3384
|
+
scope: root?.scope ?? null,
|
|
3385
|
+
updateAvailable: diff.updateAvailable,
|
|
3386
|
+
summary: diff.summary,
|
|
3387
|
+
skills: diff.skills,
|
|
3388
|
+
lockMissing: false
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
var execFileAsync, TEXT_EXT, DEFAULT_REPO_SLUG, MANIFEST_FILE, FETCH_TIMEOUT_MS, ENTRY_SKILL;
|
|
3392
|
+
var init_skillsManifest = __esm({
|
|
3393
|
+
"src/utils/skillsManifest.ts"() {
|
|
3394
|
+
"use strict";
|
|
3395
|
+
execFileAsync = promisify(execFile);
|
|
3396
|
+
TEXT_EXT = /* @__PURE__ */ new Set([
|
|
3397
|
+
".md",
|
|
3398
|
+
".txt",
|
|
3399
|
+
".mjs",
|
|
3400
|
+
".js",
|
|
3401
|
+
".ts",
|
|
3402
|
+
".jsx",
|
|
3403
|
+
".tsx",
|
|
3404
|
+
".html",
|
|
3405
|
+
".css",
|
|
3406
|
+
".json",
|
|
3407
|
+
".svg",
|
|
3408
|
+
".csv",
|
|
3409
|
+
".yml",
|
|
3410
|
+
".yaml"
|
|
3411
|
+
]);
|
|
3412
|
+
DEFAULT_REPO_SLUG = "frogoe/engine";
|
|
3413
|
+
MANIFEST_FILE = "skills-manifest.json";
|
|
3414
|
+
FETCH_TIMEOUT_MS = 4e3;
|
|
3415
|
+
ENTRY_SKILL = "frogoe";
|
|
3416
|
+
}
|
|
3417
|
+
});
|
|
3418
|
+
|
|
3419
|
+
// src/commands/skills.ts
|
|
3420
|
+
var skills_exports = {};
|
|
3421
|
+
__export(skills_exports, {
|
|
3422
|
+
command: () => command8
|
|
3423
|
+
});
|
|
3424
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
3425
|
+
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
3426
|
+
function hasNpx() {
|
|
3427
|
+
try {
|
|
3428
|
+
const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
3429
|
+
const exe = process.platform === "win32" ? "cmd.exe" : cmd;
|
|
3430
|
+
const args = process.platform === "win32" ? ["/d", "/s", "/c", "npx.cmd", "--version"] : ["--version"];
|
|
3431
|
+
execFileSync(exe, args, { stdio: "ignore", timeout: 5e3 });
|
|
3432
|
+
return true;
|
|
3433
|
+
} catch {
|
|
3434
|
+
try {
|
|
3435
|
+
execFileSync("npx", ["--version"], { stdio: "ignore", timeout: 5e3 });
|
|
3436
|
+
return true;
|
|
3437
|
+
} catch {
|
|
3438
|
+
return false;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
function hasGit() {
|
|
3443
|
+
try {
|
|
3444
|
+
execFileSync("git", ["--version"], { stdio: "ignore", timeout: 5e3 });
|
|
3445
|
+
return true;
|
|
3446
|
+
} catch {
|
|
3447
|
+
return false;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
function buildNpxCommand(args) {
|
|
3451
|
+
if (process.platform === "win32") {
|
|
3452
|
+
return { command: "cmd.exe", args: ["/d", "/s", "/c", "npx.cmd", ...args] };
|
|
3453
|
+
}
|
|
3454
|
+
return { command: "npx", args: [...args] };
|
|
3455
|
+
}
|
|
3456
|
+
function spawnNpx(args) {
|
|
3457
|
+
const npx = buildNpxCommand(args);
|
|
3458
|
+
return new Promise((resolve2, reject) => {
|
|
3459
|
+
const child = spawn2(npx.command, npx.args, {
|
|
3460
|
+
stdio: ["inherit", 2, 2],
|
|
3461
|
+
timeout: 3e5,
|
|
3462
|
+
env: {
|
|
3463
|
+
...process.env,
|
|
3464
|
+
GIT_CLONE_PROTECTION_ACTIVE: "0",
|
|
3465
|
+
GIT_LFS_SKIP_SMUDGE: "1"
|
|
3466
|
+
}
|
|
3467
|
+
});
|
|
3468
|
+
child.on("close", (code, signal) => {
|
|
3469
|
+
if (code === 0) resolve2();
|
|
3470
|
+
else if (signal === "SIGINT" || code === 130) resolve2();
|
|
3471
|
+
else reject(new Error(`npx ${args.join(" ")} exited with code ${code}`));
|
|
3472
|
+
});
|
|
3473
|
+
child.on("error", reject);
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
async function installSkills(selection) {
|
|
3477
|
+
const skillArgs = selection === "*" ? ["--skill", "*"] : selection.flatMap((n) => ["--skill", n]);
|
|
3478
|
+
if (!hasNpx()) throw new Error("npx not found. Install Node.js and retry.");
|
|
3479
|
+
if (!hasGit()) throw new Error("git not found. Install git and retry.");
|
|
3480
|
+
await spawnNpx(["skills", "add", SOURCE_URL, ...skillArgs, ...GLOBAL_INSTALL_ARGS_TAIL]);
|
|
3481
|
+
}
|
|
3482
|
+
function renderCheck(result) {
|
|
3483
|
+
console.log();
|
|
3484
|
+
console.log("frogoe skills");
|
|
3485
|
+
console.log();
|
|
3486
|
+
if (!result.location) {
|
|
3487
|
+
console.log(" No frogoe skills found in the usual locations.");
|
|
3488
|
+
console.log(" Install: npx skills add frogoe/engine");
|
|
3489
|
+
console.log(" Or: frogoe skills update");
|
|
3490
|
+
console.log();
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
console.log(` Location ${result.location} (${result.agent})`);
|
|
3494
|
+
console.log();
|
|
3495
|
+
const parts = [];
|
|
3496
|
+
parts.push(`\u2713 ${result.summary.current} current`);
|
|
3497
|
+
if (result.summary.outdated) parts.push(`\u2191 ${result.summary.outdated} outdated`);
|
|
3498
|
+
if (result.summary.coreMissing) parts.push(`\u25E6 ${result.summary.coreMissing} core not installed`);
|
|
3499
|
+
const onDemandMissing = result.summary.missing - result.summary.coreMissing;
|
|
3500
|
+
if (onDemandMissing) parts.push(`\u25E6 ${onDemandMissing} available on demand`);
|
|
3501
|
+
console.log(` ${parts.join(" ")}`);
|
|
3502
|
+
for (const s of result.skills.filter((x) => x.status === "outdated")) {
|
|
3503
|
+
console.log(` \u2191 ${s.name}`);
|
|
3504
|
+
}
|
|
3505
|
+
for (const s of result.skills.filter((x) => x.status === "missing" && isCoreSkill(x.name))) {
|
|
3506
|
+
console.log(` \u25E6 ${s.name} (core)`);
|
|
3507
|
+
}
|
|
3508
|
+
console.log();
|
|
3509
|
+
if (result.updateAvailable) {
|
|
3510
|
+
console.log(" Update: frogoe skills update or npx skills add frogoe/engine");
|
|
3511
|
+
} else {
|
|
3512
|
+
console.log(" Installed skills are up to date");
|
|
3513
|
+
}
|
|
3514
|
+
console.log();
|
|
3515
|
+
}
|
|
3516
|
+
var GLOBAL_INSTALL_ARGS_TAIL, SOURCE_URL, checkCommand, updateCommand, command8;
|
|
3517
|
+
var init_skills = __esm({
|
|
3518
|
+
"src/commands/skills.ts"() {
|
|
3519
|
+
"use strict";
|
|
3520
|
+
init_skillsManifest();
|
|
3521
|
+
GLOBAL_INSTALL_ARGS_TAIL = [
|
|
3522
|
+
"--global",
|
|
3523
|
+
"--agent",
|
|
3524
|
+
"claude-code",
|
|
3525
|
+
"universal",
|
|
3526
|
+
"--copy",
|
|
3527
|
+
"--full-depth",
|
|
3528
|
+
"--yes"
|
|
3529
|
+
];
|
|
3530
|
+
SOURCE_URL = "https://github.com/frogoe/engine";
|
|
3531
|
+
checkCommand = defineCommand8({
|
|
3532
|
+
meta: { name: "check", description: "Check whether installed skills are the latest version" },
|
|
3533
|
+
args: {
|
|
3534
|
+
json: { type: "boolean", description: "Output as JSON", default: false },
|
|
3535
|
+
dir: { type: "string", description: "Skills directory to check" },
|
|
3536
|
+
source: { type: "string", description: "Where 'latest' comes from" }
|
|
3537
|
+
},
|
|
3538
|
+
async run({ args }) {
|
|
3539
|
+
try {
|
|
3540
|
+
const result = await checkSkills({
|
|
3541
|
+
dir: args.dir,
|
|
3542
|
+
source: args.source,
|
|
3543
|
+
canonical: true
|
|
3544
|
+
});
|
|
3545
|
+
if (args.json) {
|
|
3546
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3547
|
+
} else {
|
|
3548
|
+
renderCheck(result);
|
|
3549
|
+
}
|
|
3550
|
+
if (result.updateAvailable) process.exitCode = 1;
|
|
3551
|
+
} catch (err) {
|
|
3552
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3553
|
+
if (msg.includes("Malformed") || msg.includes("HTTP") || msg.includes("fetch")) {
|
|
3554
|
+
console.error(`Skills check failed (offline or GitHub unreachable): ${msg}`);
|
|
3555
|
+
console.error("Try: npx skills add frogoe/engine \u2014 or retry when online.");
|
|
3556
|
+
} else {
|
|
3557
|
+
console.error(`Skills check failed: ${msg}`);
|
|
3558
|
+
}
|
|
3559
|
+
process.exitCode = 1;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
});
|
|
3563
|
+
updateCommand = defineCommand8({
|
|
3564
|
+
meta: {
|
|
3565
|
+
name: "update",
|
|
3566
|
+
description: "Update frogoe skills to the latest (core + installed). Pass names to also install them."
|
|
3567
|
+
},
|
|
3568
|
+
args: {
|
|
3569
|
+
json: { type: "boolean", description: "Output as JSON", default: false }
|
|
3570
|
+
},
|
|
3571
|
+
async run({ args }) {
|
|
3572
|
+
const requested = (args._ ?? []).map(String).filter(Boolean);
|
|
3573
|
+
const invalid = requested.filter((n) => !/^[a-z0-9][a-z0-9._-]*$/i.test(n));
|
|
3574
|
+
if (invalid.length) {
|
|
3575
|
+
console.error(`Invalid skill name(s): ${invalid.join(", ")}`);
|
|
3576
|
+
process.exitCode = 1;
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
try {
|
|
3580
|
+
const check = await checkSkills({ canonical: true });
|
|
3581
|
+
const toInstall = /* @__PURE__ */ new Set();
|
|
3582
|
+
for (const s of check.skills) {
|
|
3583
|
+
if (s.status === "outdated" || s.status === "missing" && isCoreSkill(s.name))
|
|
3584
|
+
toInstall.add(s.name);
|
|
3585
|
+
}
|
|
3586
|
+
for (const n of requested) toInstall.add(n);
|
|
3587
|
+
if (toInstall.size === 0) {
|
|
3588
|
+
const msg = "Installed skills are already up to date.";
|
|
3589
|
+
if (args.json) console.log(JSON.stringify({ ...check, message: msg }, null, 2));
|
|
3590
|
+
else console.log(msg);
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3593
|
+
const list = [...toInstall];
|
|
3594
|
+
console.log(`Updating ${list.length} skill(s): ${list.join(", ")}`);
|
|
3595
|
+
await installSkills(list);
|
|
3596
|
+
const verify = await checkSkills({ canonical: true });
|
|
3597
|
+
if (args.json) console.log(JSON.stringify(verify, null, 2));
|
|
3598
|
+
else renderCheck(verify);
|
|
3599
|
+
if (verify.updateAvailable) process.exitCode = 1;
|
|
3600
|
+
} catch (err) {
|
|
3601
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3602
|
+
console.error(`Update failed: ${msg}`);
|
|
3603
|
+
if (msg.includes("npx") || msg.includes("git")) {
|
|
3604
|
+
console.error("Install Node.js and git, then retry: npx skills add frogoe/engine");
|
|
3605
|
+
}
|
|
3606
|
+
process.exitCode = 1;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
});
|
|
3610
|
+
command8 = defineCommand8({
|
|
3611
|
+
meta: {
|
|
3612
|
+
name: "skills",
|
|
3613
|
+
description: "Install, check, and update frogoe skills for AI coding tools"
|
|
3614
|
+
},
|
|
3615
|
+
subCommands: { check: checkCommand, update: updateCommand },
|
|
3616
|
+
args: {},
|
|
3617
|
+
async run() {
|
|
3618
|
+
try {
|
|
3619
|
+
console.log("Installing all frogoe skills...");
|
|
3620
|
+
await installSkills("*");
|
|
3621
|
+
const result = await checkSkills({ canonical: true });
|
|
3622
|
+
renderCheck(result);
|
|
3623
|
+
} catch (err) {
|
|
3624
|
+
console.error(`Install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3625
|
+
process.exitCode = 1;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3084
3632
|
// src/cli.ts
|
|
3085
|
-
import { defineCommand as
|
|
3633
|
+
import { defineCommand as defineCommand9, runMain } from "citty";
|
|
3086
3634
|
|
|
3087
3635
|
// package.json
|
|
3088
3636
|
var package_default = {
|
|
3089
3637
|
name: "frogoe",
|
|
3090
|
-
version: "0.
|
|
3638
|
+
version: "0.3.1",
|
|
3091
3639
|
description: "froge CLI \u2014 the agent's hands: init, add, run, check, bundle",
|
|
3092
3640
|
homepage: "https://github.com/frogoe/engine#readme",
|
|
3093
3641
|
bugs: "https://github.com/frogoe/engine/issues",
|
|
@@ -3153,23 +3701,27 @@ if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
|
3153
3701
|
var HELP = `frogoe ${VERSION} \u2014 write a closure, ship a game
|
|
3154
3702
|
|
|
3155
3703
|
Commands:
|
|
3156
|
-
init [name]
|
|
3157
|
-
add <block>
|
|
3158
|
-
run [dir]
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3704
|
+
init [name] scaffold a runnable game folder
|
|
3705
|
+
add <block> copy a registry HUD block into blocks/
|
|
3706
|
+
run [dir] serve with live reload + phone QR (--tunnel: any network)
|
|
3707
|
+
lint [dir] static contract lint \u2014 fast iteration (stable codes; --json)
|
|
3708
|
+
check [dir] full gate: lint + live Chrome sandbox (FPS, HUD outline)
|
|
3709
|
+
report [dir] last playtest session: fps dips, errors, when
|
|
3710
|
+
bundle [dir] dissolve externals \u2192 one self-contained HTML
|
|
3711
|
+
skills [check|update] skill freshness \u2014 check or update via npx skills add
|
|
3162
3712
|
|
|
3163
3713
|
Docs: skills/frogoe-core \u2014 the whole contract in five references.`;
|
|
3164
|
-
var main =
|
|
3714
|
+
var main = defineCommand9({
|
|
3165
3715
|
meta: { description: HELP },
|
|
3166
3716
|
subCommands: {
|
|
3167
3717
|
add: () => Promise.resolve().then(() => (init_add2(), add_exports)).then((m) => m.command),
|
|
3168
3718
|
bundle: () => Promise.resolve().then(() => (init_bundle2(), bundle_exports)).then((m) => m.command),
|
|
3169
3719
|
check: () => Promise.resolve().then(() => (init_check3(), check_exports)).then((m) => m.command),
|
|
3170
3720
|
init: () => Promise.resolve().then(() => (init_init2(), init_exports)).then((m) => m.command),
|
|
3721
|
+
lint: () => Promise.resolve().then(() => (init_lint(), lint_exports)).then((m) => m.command),
|
|
3171
3722
|
report: () => Promise.resolve().then(() => (init_report(), report_exports)).then((m) => m.command),
|
|
3172
|
-
run: () => Promise.resolve().then(() => (init_run2(), run_exports2)).then((m) => m.command)
|
|
3723
|
+
run: () => Promise.resolve().then(() => (init_run2(), run_exports2)).then((m) => m.command),
|
|
3724
|
+
skills: () => Promise.resolve().then(() => (init_skills(), skills_exports)).then((m) => m.command)
|
|
3173
3725
|
}
|
|
3174
3726
|
});
|
|
3175
3727
|
await runMain(main);
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
**Always read the relevant skill before writing or modifying game code.** Skills encode the frogoe contract and creative direction that generic docs don't cover. Skipping them produces broken games.
|
|
6
6
|
|
|
7
|
-
**Doing anything with frogoe?** Read the
|
|
7
|
+
**Doing anything with frogoe?** Read the `/frogoe` skill — it confirms the BRIEF (verb, mood, palette) up front and routes every request. The domain skills it routes to:
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- `/frogoe-core` — the technical contract: folder form, `defineGame` closure, four nouns, HUD bindings, external libraries. Read before writing any game code.
|
|
10
|
+
- `/frogoe-creative` — house style: three dials (VARIANCE/MOTION/DENSITY), lazy defaults, typography, palettes, game feel. Read when choosing how a game looks.
|
|
11
|
+
- `/frogoe-cli` — CLI dev loop: init, add, run, check, bundle, report. Finding codes split into `finding-codes.md` / `live-sandbox.md` / `bundle.md` for self-healing.
|
|
12
|
+
- `/frogoe-registry` — HUD block catalog: find, evaluate, install, author new blocks.
|
|
13
13
|
|
|
14
|
-
Skills live at `.agents/skills/` (install via `npx skills add frogoe/engine
|
|
14
|
+
Skills live at `.claude/skills/` and `.agents/skills/` (install via `npx skills add frogoe/engine`; both mirrors stay byte-identical). Missing or stale? Re-run the install and restart the agent session. Check freshness: `frogoe skills check`.
|
|
15
15
|
|
|
16
16
|
## The contract
|
|
17
17
|
|
|
@@ -54,15 +54,16 @@ The platform draws NOTHING. Everything visible is your code + HUD blocks from th
|
|
|
54
54
|
```bash
|
|
55
55
|
frogoe run # serve with live reload + phone QR (safe-area only exists on real devices)
|
|
56
56
|
frogoe run --tunnel # + public URL — phone works on any network (cloudflared, auto-downloaded once)
|
|
57
|
-
frogoe check # static contract lint (stable finding codes; --json for CI)
|
|
58
|
-
frogoe check --live # + headless Chrome: FPS, playability, HUD outline, screenshots
|
|
59
|
-
frogoe bundle # one self-contained HTML (externals dissolved, zero runtime requests)
|
|
60
57
|
frogoe add <block> # copy a HUD block into blocks/ (score, hearts, fuel, game-over, etc.)
|
|
58
|
+
frogoe lint # fast static contract lint (stable finding codes; --json for CI)
|
|
59
|
+
frogoe check # full gate: lint + headless Chrome — FPS, playability, HUD outline, screenshots
|
|
60
|
+
frogoe bundle # one self-contained HTML (externals dissolved) — only after check passes
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
> **Agents must run `frogoe check` after ANY code change** and fix all errors before
|
|
64
|
-
> presenting the result.
|
|
65
|
-
|
|
64
|
+
> presenting the result. `frogoe lint` is the fast static half for iteration; `frogoe
|
|
65
|
+
check` is the full gate (static + live sandbox) and MUST exit 0 before `frogoe
|
|
66
|
+
bundle`. Use `--json` for machine-readable findings that can be fixed programmatically.
|
|
66
67
|
|
|
67
68
|
## Project structure
|
|
68
69
|
|
|
@@ -79,8 +80,8 @@ frogoe add <block> # copy a HUD block into blocks/ (score, hearts, fuel
|
|
|
79
80
|
After creating or editing any file, **always** run:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
|
-
frogoe
|
|
83
|
-
frogoe check
|
|
83
|
+
frogoe lint # fast static: BRIEF validation, folder structure, input patterns
|
|
84
|
+
frogoe check # full gate: + browser — runtime errors, canvas painted, FPS, playability
|
|
84
85
|
```
|
|
85
86
|
|
|
86
87
|
Fix all errors before presenting the result. Common findings:
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
**Always read the relevant skill before writing or modifying game code.** Skills encode the frogoe contract and creative direction that generic docs don't cover. Skipping them produces broken games.
|
|
6
6
|
|
|
7
|
-
**Doing anything with frogoe?** Start at the
|
|
7
|
+
**Doing anything with frogoe?** Start at the `/frogoe` skill — it confirms the BRIEF (verb, mood, palette) up front and routes every request. The domain skills it routes to:
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- `/frogoe-core` — the technical contract: folder form, `defineGame` closure, four nouns, HUD bindings, external libraries. Read before writing any game code.
|
|
10
|
+
- `/frogoe-creative` — house style: three dials (VARIANCE/MOTION/DENSITY), lazy defaults, typography, palettes, game feel. Read when choosing how a game looks.
|
|
11
|
+
- `/frogoe-cli` — CLI dev loop: init, add, run, check, bundle, report. Finding codes split into `finding-codes.md` / `live-sandbox.md` / `bundle.md` for self-healing.
|
|
12
|
+
- `/frogoe-registry` — HUD block catalog: find, evaluate, install, author new blocks.
|
|
13
13
|
|
|
14
|
-
Skills live at `.agents/skills/` (install via `npx skills add frogoe/engine
|
|
14
|
+
Skills live at `.claude/skills/` and `.agents/skills/` (install via `npx skills add frogoe/engine`; both mirrors stay byte-identical). Missing or stale? Re-run the install and restart the agent session. Check freshness: `frogoe skills check`.
|
|
15
15
|
|
|
16
16
|
## The contract
|
|
17
17
|
|
|
@@ -54,15 +54,16 @@ The platform draws NOTHING. Everything visible is your code + HUD blocks from th
|
|
|
54
54
|
```bash
|
|
55
55
|
frogoe run # serve with live reload + phone QR (safe-area only exists on real devices)
|
|
56
56
|
frogoe run --tunnel # + public URL — phone works on any network (cloudflared, auto-downloaded once)
|
|
57
|
-
frogoe check # static contract lint (stable finding codes; --json for CI)
|
|
58
|
-
frogoe check --live # + headless Chrome: FPS, playability, HUD outline, screenshots
|
|
59
|
-
frogoe bundle # one self-contained HTML (externals dissolved, zero runtime requests)
|
|
60
57
|
frogoe add <block> # copy a HUD block into blocks/ (score, hearts, fuel, game-over, etc.)
|
|
58
|
+
frogoe lint # fast static contract lint (stable finding codes; --json for CI)
|
|
59
|
+
frogoe check # full gate: lint + headless Chrome — FPS, playability, HUD outline, screenshots
|
|
60
|
+
frogoe bundle # one self-contained HTML (externals dissolved) — only after check passes
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
> **Agents must run `frogoe check` after ANY code change** and fix all errors before
|
|
64
|
-
> presenting the result.
|
|
65
|
-
|
|
64
|
+
> presenting the result. `frogoe lint` is the fast static half for iteration; `frogoe
|
|
65
|
+
check` is the full gate (static + live sandbox) and MUST exit 0 before `frogoe
|
|
66
|
+
bundle`. Use `--json` for machine-readable findings that can be fixed programmatically.
|
|
66
67
|
|
|
67
68
|
## Project structure
|
|
68
69
|
|
|
@@ -79,8 +80,8 @@ frogoe add <block> # copy a HUD block into blocks/ (score, hearts, fuel
|
|
|
79
80
|
After creating or editing any file, **always** run:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
|
-
frogoe
|
|
83
|
-
frogoe check
|
|
83
|
+
frogoe lint # fast static: BRIEF validation, folder structure, input patterns
|
|
84
|
+
frogoe check # full gate: + browser — runtime errors, canvas painted, FPS, playability
|
|
84
85
|
```
|
|
85
86
|
|
|
86
87
|
Fix all errors before presenting the result. Common findings:
|
package/package.json
CHANGED