frogoe 0.2.1 → 0.3.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/README.md +84 -0
- package/dist/cli.js +615 -46
- 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
|
@@ -371,8 +371,8 @@ var init_fetch_policy = __esm({
|
|
|
371
371
|
url;
|
|
372
372
|
status;
|
|
373
373
|
};
|
|
374
|
-
sleep = (ms) => new Promise((
|
|
375
|
-
setTimeout(
|
|
374
|
+
sleep = (ms) => new Promise((resolve2) => {
|
|
375
|
+
setTimeout(resolve2, ms);
|
|
376
376
|
});
|
|
377
377
|
fetchWithPolicy = async (url, options) => {
|
|
378
378
|
const policy = { ...DEFAULT_FETCH_POLICY, ...options?.policy };
|
|
@@ -1333,8 +1333,8 @@ var init_driver = __esm({
|
|
|
1333
1333
|
async hold(x, y, ms) {
|
|
1334
1334
|
await page.mouse.move(x, y);
|
|
1335
1335
|
await page.mouse.down();
|
|
1336
|
-
await new Promise((
|
|
1337
|
-
setTimeout(
|
|
1336
|
+
await new Promise((resolve2) => {
|
|
1337
|
+
setTimeout(resolve2, ms);
|
|
1338
1338
|
});
|
|
1339
1339
|
await page.mouse.up();
|
|
1340
1340
|
},
|
|
@@ -1342,8 +1342,8 @@ var init_driver = __esm({
|
|
|
1342
1342
|
await page.mouse.move(x1, y1);
|
|
1343
1343
|
await page.mouse.down();
|
|
1344
1344
|
await page.mouse.move(x2, y2, { steps: 6 });
|
|
1345
|
-
await new Promise((
|
|
1346
|
-
setTimeout(
|
|
1345
|
+
await new Promise((resolve2) => {
|
|
1346
|
+
setTimeout(resolve2, 80);
|
|
1347
1347
|
});
|
|
1348
1348
|
await page.mouse.up();
|
|
1349
1349
|
},
|
|
@@ -1369,8 +1369,8 @@ var init_driver = __esm({
|
|
|
1369
1369
|
if (Date.now() - started > grace) {
|
|
1370
1370
|
return false;
|
|
1371
1371
|
}
|
|
1372
|
-
await new Promise((
|
|
1373
|
-
setTimeout(
|
|
1372
|
+
await new Promise((resolve2) => {
|
|
1373
|
+
setTimeout(resolve2, 100);
|
|
1374
1374
|
});
|
|
1375
1375
|
}
|
|
1376
1376
|
const navigated = page.waitForNavigation({ timeout: timeoutMs, waitUntil: "domcontentloaded" }).then(() => true).catch(() => false);
|
|
@@ -1721,8 +1721,8 @@ var init_phases = __esm({
|
|
|
1721
1721
|
STABILITY_CYCLES = 2;
|
|
1722
1722
|
START_BURST_TAPS = 3;
|
|
1723
1723
|
DESKTOP_FPS_MS = 2e3;
|
|
1724
|
-
sleep2 = (ms) => new Promise((
|
|
1725
|
-
setTimeout(
|
|
1724
|
+
sleep2 = (ms) => new Promise((resolve2) => {
|
|
1725
|
+
setTimeout(resolve2, ms);
|
|
1726
1726
|
});
|
|
1727
1727
|
jitterX = (step) => step * 37 % 121 - 60;
|
|
1728
1728
|
jitterY = (step) => step * 53 % 181 - 90;
|
|
@@ -2375,9 +2375,9 @@ var init_run = __esm({
|
|
|
2375
2375
|
return c.body(body, 200, { "content-type": type });
|
|
2376
2376
|
});
|
|
2377
2377
|
const server = createAdaptorServer({ fetch: app.fetch });
|
|
2378
|
-
await new Promise((
|
|
2378
|
+
await new Promise((resolve2, reject) => {
|
|
2379
2379
|
server.once("error", reject);
|
|
2380
|
-
server.listen(requestedPort, "0.0.0.0", () =>
|
|
2380
|
+
server.listen(requestedPort, "0.0.0.0", () => resolve2());
|
|
2381
2381
|
});
|
|
2382
2382
|
const address = server.address();
|
|
2383
2383
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
@@ -2568,23 +2568,27 @@ var init_check3 = __esm({
|
|
|
2568
2568
|
args: {
|
|
2569
2569
|
dir: { type: "positional", required: false, description: "game folder (default: cwd)" },
|
|
2570
2570
|
json: { type: "boolean", description: "machine-readable findings" },
|
|
2571
|
-
live: {
|
|
2571
|
+
live: {
|
|
2572
|
+
type: "boolean",
|
|
2573
|
+
description: "deprecated no-op \u2014 the live sandbox always runs now"
|
|
2574
|
+
}
|
|
2572
2575
|
},
|
|
2573
2576
|
async run({ args }) {
|
|
2577
|
+
if (args.live) {
|
|
2578
|
+
console.error(" note: --live is deprecated \u2014 the live sandbox always runs now");
|
|
2579
|
+
}
|
|
2574
2580
|
const dir = args.dir ? String(args.dir) : process.cwd();
|
|
2575
2581
|
const result = checkProject(dir);
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
console.log(` snapshots: ${live.screenshots.join(", ")}`);
|
|
2587
|
-
}
|
|
2582
|
+
const { collectLive: collectLive2 } = await Promise.resolve().then(() => (init_live(), live_exports));
|
|
2583
|
+
console.log(" live pass: boot \u2192 play \u2192 end \u2192 retry (headless chrome)\u2026");
|
|
2584
|
+
const live = await collectLive2({ dir });
|
|
2585
|
+
result.findings = [...result.findings, ...live.findings].sort(
|
|
2586
|
+
(a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0)
|
|
2587
|
+
);
|
|
2588
|
+
result.errors = result.findings.filter((f) => f.severity === "error").length;
|
|
2589
|
+
result.warnings = result.findings.filter((f) => f.severity === "warning").length;
|
|
2590
|
+
if (live.screenshots.length > 0) {
|
|
2591
|
+
console.log(` snapshots: ${live.screenshots.join(", ")}`);
|
|
2588
2592
|
}
|
|
2589
2593
|
if (args.json) {
|
|
2590
2594
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -2597,7 +2601,9 @@ var init_check3 = __esm({
|
|
|
2597
2601
|
process.exitCode = 1;
|
|
2598
2602
|
}
|
|
2599
2603
|
},
|
|
2600
|
-
meta: {
|
|
2604
|
+
meta: {
|
|
2605
|
+
description: "full gate: contract lint + live browser sandbox (FPS, playability, HUD outline, audio recovery, screenshots)"
|
|
2606
|
+
}
|
|
2601
2607
|
});
|
|
2602
2608
|
}
|
|
2603
2609
|
});
|
|
@@ -2633,20 +2639,55 @@ var init_init2 = __esm({
|
|
|
2633
2639
|
}
|
|
2634
2640
|
});
|
|
2635
2641
|
|
|
2642
|
+
// src/commands/lint.ts
|
|
2643
|
+
var lint_exports = {};
|
|
2644
|
+
__export(lint_exports, {
|
|
2645
|
+
command: () => command5
|
|
2646
|
+
});
|
|
2647
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
2648
|
+
var command5;
|
|
2649
|
+
var init_lint = __esm({
|
|
2650
|
+
"src/commands/lint.ts"() {
|
|
2651
|
+
"use strict";
|
|
2652
|
+
init_check2();
|
|
2653
|
+
command5 = defineCommand5({
|
|
2654
|
+
args: {
|
|
2655
|
+
dir: { type: "positional", required: false, description: "game folder (default: cwd)" },
|
|
2656
|
+
json: { type: "boolean", description: "machine-readable findings" }
|
|
2657
|
+
},
|
|
2658
|
+
async run({ args }) {
|
|
2659
|
+
const dir = args.dir ? String(args.dir) : process.cwd();
|
|
2660
|
+
const result = checkProject(dir);
|
|
2661
|
+
if (args.json) {
|
|
2662
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2663
|
+
} else {
|
|
2664
|
+
console.log(formatFindings(result));
|
|
2665
|
+
console.log(`
|
|
2666
|
+
${result.errors} error(s), ${result.warnings} warning(s)`);
|
|
2667
|
+
}
|
|
2668
|
+
if (result.errors > 0) {
|
|
2669
|
+
process.exitCode = 1;
|
|
2670
|
+
}
|
|
2671
|
+
},
|
|
2672
|
+
meta: { description: "static contract lint only \u2014 fast iteration (check is the full gate)" }
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2636
2677
|
// src/commands/report.ts
|
|
2637
2678
|
var report_exports = {};
|
|
2638
2679
|
__export(report_exports, {
|
|
2639
|
-
command: () =>
|
|
2680
|
+
command: () => command6
|
|
2640
2681
|
});
|
|
2641
2682
|
import { readFileSync as readFileSync6 } from "fs";
|
|
2642
|
-
import { defineCommand as
|
|
2643
|
-
var
|
|
2683
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
2684
|
+
var command6;
|
|
2644
2685
|
var init_report = __esm({
|
|
2645
2686
|
"src/commands/report.ts"() {
|
|
2646
2687
|
"use strict";
|
|
2647
2688
|
init_records();
|
|
2648
2689
|
init_session();
|
|
2649
|
-
|
|
2690
|
+
command6 = defineCommand6({
|
|
2650
2691
|
args: {
|
|
2651
2692
|
dir: { type: "positional", required: false, description: "game folder (default: cwd)" }
|
|
2652
2693
|
},
|
|
@@ -2884,7 +2925,7 @@ var init_tunnel = __esm({
|
|
|
2884
2925
|
startTunnel = async (port, options) => {
|
|
2885
2926
|
const timeoutMs = options?.timeoutMs ?? 2e4;
|
|
2886
2927
|
const bin = await resolveBinary(options?.onProgress);
|
|
2887
|
-
return new Promise((
|
|
2928
|
+
return new Promise((resolve2, reject) => {
|
|
2888
2929
|
const child = spawn(
|
|
2889
2930
|
bin.path,
|
|
2890
2931
|
["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"],
|
|
@@ -2922,7 +2963,7 @@ var init_tunnel = __esm({
|
|
|
2922
2963
|
reject(new Error(tail ? `${base} \u2014 ${tail}` : base));
|
|
2923
2964
|
return;
|
|
2924
2965
|
}
|
|
2925
|
-
|
|
2966
|
+
resolve2({ exited, stop: killTree, url });
|
|
2926
2967
|
};
|
|
2927
2968
|
const timer = setTimeout(() => {
|
|
2928
2969
|
finish(
|
|
@@ -2959,10 +3000,10 @@ var init_tunnel = __esm({
|
|
|
2959
3000
|
// src/commands/run.ts
|
|
2960
3001
|
var run_exports2 = {};
|
|
2961
3002
|
__export(run_exports2, {
|
|
2962
|
-
command: () =>
|
|
3003
|
+
command: () => command7
|
|
2963
3004
|
});
|
|
2964
|
-
import { defineCommand as
|
|
2965
|
-
var NUDGE_MS, printQr, message,
|
|
3005
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
3006
|
+
var NUDGE_MS, printQr, message, command7;
|
|
2966
3007
|
var init_run2 = __esm({
|
|
2967
3008
|
"src/commands/run.ts"() {
|
|
2968
3009
|
"use strict";
|
|
@@ -2981,7 +3022,7 @@ var init_run2 = __esm({
|
|
|
2981
3022
|
});
|
|
2982
3023
|
};
|
|
2983
3024
|
message = (error) => error instanceof Error ? error.message : String(error);
|
|
2984
|
-
|
|
3025
|
+
command7 = defineCommand7({
|
|
2985
3026
|
args: {
|
|
2986
3027
|
dir: { type: "positional", required: false, description: "game folder (default: cwd)" },
|
|
2987
3028
|
port: { type: "string", description: "port (default: random free)" },
|
|
@@ -3081,11 +3122,535 @@ var init_run2 = __esm({
|
|
|
3081
3122
|
}
|
|
3082
3123
|
});
|
|
3083
3124
|
|
|
3125
|
+
// src/utils/skillsManifest.ts
|
|
3126
|
+
import { execFile } from "child_process";
|
|
3127
|
+
import { createHash as createHash2 } from "crypto";
|
|
3128
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
3129
|
+
import { homedir } from "os";
|
|
3130
|
+
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
3131
|
+
import { promisify } from "util";
|
|
3132
|
+
function isCoreSkill(name) {
|
|
3133
|
+
return name === ENTRY_SKILL || name.startsWith("frogoe-");
|
|
3134
|
+
}
|
|
3135
|
+
function listFilesSorted(dir) {
|
|
3136
|
+
const out = [];
|
|
3137
|
+
const walk = (d) => {
|
|
3138
|
+
for (const name of readdirSync3(d)) {
|
|
3139
|
+
if (name === ".DS_Store") continue;
|
|
3140
|
+
const p = join(d, name);
|
|
3141
|
+
if (statSync2(p).isDirectory()) walk(p);
|
|
3142
|
+
else out.push(p);
|
|
3143
|
+
}
|
|
3144
|
+
};
|
|
3145
|
+
walk(dir);
|
|
3146
|
+
return out.sort();
|
|
3147
|
+
}
|
|
3148
|
+
function hashSkillBundle(skillDir) {
|
|
3149
|
+
const files = listFilesSorted(skillDir);
|
|
3150
|
+
const h = createHash2("sha256");
|
|
3151
|
+
for (const f of files) {
|
|
3152
|
+
const rel = relative(skillDir, f).split(sep).join("/");
|
|
3153
|
+
h.update(rel);
|
|
3154
|
+
h.update("\0");
|
|
3155
|
+
const ext = rel.slice(rel.lastIndexOf("."));
|
|
3156
|
+
const buf = readFileSync7(f);
|
|
3157
|
+
if (TEXT_EXT.has(ext)) h.update(buf.toString("utf8").replace(/\r\n/g, "\n"), "utf8");
|
|
3158
|
+
else h.update(buf);
|
|
3159
|
+
h.update("\0");
|
|
3160
|
+
}
|
|
3161
|
+
return { hash: h.digest("hex").slice(0, 16), files: files.length };
|
|
3162
|
+
}
|
|
3163
|
+
function buildManifest(skillsRoot, meta) {
|
|
3164
|
+
const names = readdirSync3(skillsRoot).filter((n) => existsSync7(join(skillsRoot, n, "SKILL.md"))).sort();
|
|
3165
|
+
const skills = {};
|
|
3166
|
+
for (const name of names) skills[name] = hashSkillBundle(join(skillsRoot, name));
|
|
3167
|
+
return { source: meta.source, skills };
|
|
3168
|
+
}
|
|
3169
|
+
function agentLabel(hostDir) {
|
|
3170
|
+
const name = hostDir.replace(/^\.+/, "");
|
|
3171
|
+
return name === "claude" ? "claude-code" : name || "unknown";
|
|
3172
|
+
}
|
|
3173
|
+
function agentFromDir(dir) {
|
|
3174
|
+
const parts = dir.split(sep).filter(Boolean);
|
|
3175
|
+
const i = parts.lastIndexOf("skills");
|
|
3176
|
+
return agentLabel(i > 0 ? parts[i - 1] : parts[parts.length - 1] ?? "");
|
|
3177
|
+
}
|
|
3178
|
+
function listSubdirs(dir) {
|
|
3179
|
+
try {
|
|
3180
|
+
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
3181
|
+
} catch {
|
|
3182
|
+
return [];
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
function discoverSkillRoots(base, scope) {
|
|
3186
|
+
const candidates = [];
|
|
3187
|
+
const add = (hostBase, host) => {
|
|
3188
|
+
const dir = join(hostBase, host, "skills");
|
|
3189
|
+
if (existsSync7(dir) && statSync2(dir).isDirectory())
|
|
3190
|
+
candidates.push({ dir, agent: agentLabel(host), scope });
|
|
3191
|
+
};
|
|
3192
|
+
for (const host of listSubdirs(base)) add(base, host);
|
|
3193
|
+
const xdg = join(base, ".config");
|
|
3194
|
+
for (const host of listSubdirs(xdg)) add(xdg, host);
|
|
3195
|
+
return candidates.sort((a, b) => {
|
|
3196
|
+
if (a.agent !== b.agent) {
|
|
3197
|
+
if (a.agent === "claude-code") return -1;
|
|
3198
|
+
if (b.agent === "claude-code") return 1;
|
|
3199
|
+
return a.agent.localeCompare(b.agent);
|
|
3200
|
+
}
|
|
3201
|
+
return a.dir.localeCompare(b.dir);
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
function scopeForDir(dir, home, cwd) {
|
|
3205
|
+
const norm = (p) => {
|
|
3206
|
+
const r = resolve(p);
|
|
3207
|
+
return r.endsWith(sep) ? r : r + sep;
|
|
3208
|
+
};
|
|
3209
|
+
const d = norm(dir);
|
|
3210
|
+
if (d.startsWith(norm(cwd))) return "project";
|
|
3211
|
+
if (d.startsWith(norm(home))) return "global";
|
|
3212
|
+
return "project";
|
|
3213
|
+
}
|
|
3214
|
+
function locateInstall(skillNames, opts = {}) {
|
|
3215
|
+
if (opts.dir) {
|
|
3216
|
+
return existsSync7(opts.dir) ? {
|
|
3217
|
+
dir: opts.dir,
|
|
3218
|
+
agent: agentFromDir(opts.dir),
|
|
3219
|
+
scope: scopeForDir(opts.dir, opts.home ?? homedir(), opts.cwd ?? process.cwd())
|
|
3220
|
+
} : null;
|
|
3221
|
+
}
|
|
3222
|
+
const roots = [
|
|
3223
|
+
...discoverSkillRoots(opts.home ?? homedir(), "global"),
|
|
3224
|
+
...discoverSkillRoots(opts.cwd ?? process.cwd(), "project")
|
|
3225
|
+
];
|
|
3226
|
+
for (const root of roots) {
|
|
3227
|
+
if (skillNames.some((n) => existsSync7(join(root.dir, n, "SKILL.md")))) return root;
|
|
3228
|
+
}
|
|
3229
|
+
return null;
|
|
3230
|
+
}
|
|
3231
|
+
function hashInstalled(root, skillNames) {
|
|
3232
|
+
const out = {};
|
|
3233
|
+
for (const name of skillNames) {
|
|
3234
|
+
const skillDir = join(root.dir, name);
|
|
3235
|
+
if (existsSync7(join(skillDir, "SKILL.md"))) out[name] = hashSkillBundle(skillDir);
|
|
3236
|
+
}
|
|
3237
|
+
return out;
|
|
3238
|
+
}
|
|
3239
|
+
function diffSkills(installed, latest) {
|
|
3240
|
+
const skills = [];
|
|
3241
|
+
const summary = { current: 0, outdated: 0, missing: 0, coreMissing: 0 };
|
|
3242
|
+
for (const name of Object.keys(latest.skills).sort()) {
|
|
3243
|
+
const latestEntry = latest.skills[name];
|
|
3244
|
+
const installedEntry = installed[name];
|
|
3245
|
+
let status;
|
|
3246
|
+
if (!installedEntry) status = "missing";
|
|
3247
|
+
else if (installedEntry.hash === latestEntry.hash) status = "current";
|
|
3248
|
+
else status = "outdated";
|
|
3249
|
+
if (status === "current") summary.current++;
|
|
3250
|
+
else if (status === "outdated") summary.outdated++;
|
|
3251
|
+
else {
|
|
3252
|
+
summary.missing++;
|
|
3253
|
+
if (isCoreSkill(name)) summary.coreMissing++;
|
|
3254
|
+
}
|
|
3255
|
+
skills.push({
|
|
3256
|
+
name,
|
|
3257
|
+
status,
|
|
3258
|
+
installedHash: installedEntry?.hash,
|
|
3259
|
+
latestHash: latestEntry.hash
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
return {
|
|
3263
|
+
updateAvailable: summary.outdated > 0 || summary.coreMissing > 0,
|
|
3264
|
+
summary,
|
|
3265
|
+
skills
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
function findRepoManifest(cwd = process.cwd()) {
|
|
3269
|
+
let dir = cwd;
|
|
3270
|
+
for (let i = 0; i < 16; i++) {
|
|
3271
|
+
const p = join(dir, MANIFEST_FILE);
|
|
3272
|
+
if (existsSync7(p)) return p;
|
|
3273
|
+
const parent = join(dir, "..");
|
|
3274
|
+
if (parent === dir) break;
|
|
3275
|
+
dir = parent;
|
|
3276
|
+
}
|
|
3277
|
+
return null;
|
|
3278
|
+
}
|
|
3279
|
+
function asSkillsManifest(data, sourceLabel) {
|
|
3280
|
+
const m = data;
|
|
3281
|
+
if (!m || typeof m !== "object" || typeof m.skills !== "object" || m.skills === null) {
|
|
3282
|
+
throw new Error(`Malformed skills manifest from ${sourceLabel}`);
|
|
3283
|
+
}
|
|
3284
|
+
return m;
|
|
3285
|
+
}
|
|
3286
|
+
async function fetchManifest(url) {
|
|
3287
|
+
const controller = new AbortController();
|
|
3288
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
3289
|
+
try {
|
|
3290
|
+
const res = await fetch(url, { signal: controller.signal, headers: { Connection: "close" } });
|
|
3291
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
3292
|
+
return asSkillsManifest(await res.json(), url);
|
|
3293
|
+
} finally {
|
|
3294
|
+
clearTimeout(timeout);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
async function remoteHeadSha(repoSlug) {
|
|
3298
|
+
try {
|
|
3299
|
+
const { stdout } = await execFileAsync(
|
|
3300
|
+
"git",
|
|
3301
|
+
["ls-remote", `https://github.com/${repoSlug}.git`, "refs/heads/main"],
|
|
3302
|
+
{ timeout: FETCH_TIMEOUT_MS, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }
|
|
3303
|
+
);
|
|
3304
|
+
const sha = stdout.split(/\s+/)[0]?.trim() ?? "";
|
|
3305
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
3306
|
+
} catch {
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
function resolveLocalManifest(source) {
|
|
3311
|
+
const direct = source.endsWith(".json") ? source : join(source, MANIFEST_FILE);
|
|
3312
|
+
if (existsSync7(direct)) return JSON.parse(readFileSync7(direct, "utf8"));
|
|
3313
|
+
const skillsRoot = source.endsWith("skills") ? source : join(source, "skills");
|
|
3314
|
+
if (existsSync7(skillsRoot)) return buildManifest(skillsRoot, { source: skillsRoot });
|
|
3315
|
+
throw new Error(`No skills manifest found at: ${source}`);
|
|
3316
|
+
}
|
|
3317
|
+
async function fetchRemoteManifest(source) {
|
|
3318
|
+
if (source?.startsWith("http")) return fetchManifest(source);
|
|
3319
|
+
const repoSlug = source ?? DEFAULT_REPO_SLUG;
|
|
3320
|
+
const sha = await remoteHeadSha(repoSlug);
|
|
3321
|
+
if (sha) {
|
|
3322
|
+
try {
|
|
3323
|
+
return await fetchManifest(
|
|
3324
|
+
`https://raw.githubusercontent.com/${repoSlug}/${sha}/${MANIFEST_FILE}`
|
|
3325
|
+
);
|
|
3326
|
+
} catch {
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
return fetchManifest(`https://raw.githubusercontent.com/${repoSlug}/main/${MANIFEST_FILE}`);
|
|
3330
|
+
}
|
|
3331
|
+
async function resolveLatestManifest(source, cwd = process.cwd(), opts = {}) {
|
|
3332
|
+
if (source && (source.startsWith(".") || isAbsolute(source))) {
|
|
3333
|
+
return resolveLocalManifest(source);
|
|
3334
|
+
}
|
|
3335
|
+
if (!source && !opts.canonical) {
|
|
3336
|
+
const repoManifest = findRepoManifest(cwd);
|
|
3337
|
+
if (repoManifest) return JSON.parse(readFileSync7(repoManifest, "utf8"));
|
|
3338
|
+
}
|
|
3339
|
+
return fetchRemoteManifest(source);
|
|
3340
|
+
}
|
|
3341
|
+
async function checkSkills(opts = {}) {
|
|
3342
|
+
const latest = await resolveLatestManifest(opts.source, opts.cwd, { canonical: opts.canonical });
|
|
3343
|
+
const skillNames = Object.keys(latest.skills);
|
|
3344
|
+
const root = locateInstall(skillNames, { dir: opts.dir, cwd: opts.cwd, home: opts.home });
|
|
3345
|
+
const installed = root ? hashInstalled(root, skillNames) : {};
|
|
3346
|
+
const diff = diffSkills(installed, latest);
|
|
3347
|
+
return {
|
|
3348
|
+
location: root?.dir ?? null,
|
|
3349
|
+
agent: root?.agent ?? null,
|
|
3350
|
+
scope: root?.scope ?? null,
|
|
3351
|
+
updateAvailable: diff.updateAvailable,
|
|
3352
|
+
summary: diff.summary,
|
|
3353
|
+
skills: diff.skills,
|
|
3354
|
+
lockMissing: false
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
var execFileAsync, TEXT_EXT, DEFAULT_REPO_SLUG, MANIFEST_FILE, FETCH_TIMEOUT_MS, ENTRY_SKILL;
|
|
3358
|
+
var init_skillsManifest = __esm({
|
|
3359
|
+
"src/utils/skillsManifest.ts"() {
|
|
3360
|
+
"use strict";
|
|
3361
|
+
execFileAsync = promisify(execFile);
|
|
3362
|
+
TEXT_EXT = /* @__PURE__ */ new Set([
|
|
3363
|
+
".md",
|
|
3364
|
+
".txt",
|
|
3365
|
+
".mjs",
|
|
3366
|
+
".js",
|
|
3367
|
+
".ts",
|
|
3368
|
+
".jsx",
|
|
3369
|
+
".tsx",
|
|
3370
|
+
".html",
|
|
3371
|
+
".css",
|
|
3372
|
+
".json",
|
|
3373
|
+
".svg",
|
|
3374
|
+
".csv",
|
|
3375
|
+
".yml",
|
|
3376
|
+
".yaml"
|
|
3377
|
+
]);
|
|
3378
|
+
DEFAULT_REPO_SLUG = "frogoe/engine";
|
|
3379
|
+
MANIFEST_FILE = "skills-manifest.json";
|
|
3380
|
+
FETCH_TIMEOUT_MS = 4e3;
|
|
3381
|
+
ENTRY_SKILL = "frogoe";
|
|
3382
|
+
}
|
|
3383
|
+
});
|
|
3384
|
+
|
|
3385
|
+
// src/commands/skills.ts
|
|
3386
|
+
var skills_exports = {};
|
|
3387
|
+
__export(skills_exports, {
|
|
3388
|
+
command: () => command8
|
|
3389
|
+
});
|
|
3390
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
3391
|
+
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
3392
|
+
function hasNpx() {
|
|
3393
|
+
try {
|
|
3394
|
+
const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
3395
|
+
const exe = process.platform === "win32" ? "cmd.exe" : cmd;
|
|
3396
|
+
const args = process.platform === "win32" ? ["/d", "/s", "/c", "npx.cmd", "--version"] : ["--version"];
|
|
3397
|
+
execFileSync(exe, args, { stdio: "ignore", timeout: 5e3 });
|
|
3398
|
+
return true;
|
|
3399
|
+
} catch {
|
|
3400
|
+
try {
|
|
3401
|
+
execFileSync("npx", ["--version"], { stdio: "ignore", timeout: 5e3 });
|
|
3402
|
+
return true;
|
|
3403
|
+
} catch {
|
|
3404
|
+
return false;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
function hasGit() {
|
|
3409
|
+
try {
|
|
3410
|
+
execFileSync("git", ["--version"], { stdio: "ignore", timeout: 5e3 });
|
|
3411
|
+
return true;
|
|
3412
|
+
} catch {
|
|
3413
|
+
return false;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
function buildNpxCommand(args) {
|
|
3417
|
+
if (process.platform === "win32") {
|
|
3418
|
+
return { command: "cmd.exe", args: ["/d", "/s", "/c", "npx.cmd", ...args] };
|
|
3419
|
+
}
|
|
3420
|
+
return { command: "npx", args: [...args] };
|
|
3421
|
+
}
|
|
3422
|
+
function spawnNpx(args) {
|
|
3423
|
+
const npx = buildNpxCommand(args);
|
|
3424
|
+
return new Promise((resolve2, reject) => {
|
|
3425
|
+
const child = spawn2(npx.command, npx.args, {
|
|
3426
|
+
stdio: ["inherit", 2, 2],
|
|
3427
|
+
timeout: 3e5,
|
|
3428
|
+
env: {
|
|
3429
|
+
...process.env,
|
|
3430
|
+
GIT_CLONE_PROTECTION_ACTIVE: "0",
|
|
3431
|
+
GIT_LFS_SKIP_SMUDGE: "1"
|
|
3432
|
+
}
|
|
3433
|
+
});
|
|
3434
|
+
child.on("close", (code, signal) => {
|
|
3435
|
+
if (code === 0) resolve2();
|
|
3436
|
+
else if (signal === "SIGINT" || code === 130) resolve2();
|
|
3437
|
+
else reject(new Error(`npx ${args.join(" ")} exited with code ${code}`));
|
|
3438
|
+
});
|
|
3439
|
+
child.on("error", reject);
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
async function installSkills(selection) {
|
|
3443
|
+
const skillArgs = selection === "*" ? ["--skill", "*"] : selection.flatMap((n) => ["--skill", n]);
|
|
3444
|
+
if (!hasNpx()) throw new Error("npx not found. Install Node.js and retry.");
|
|
3445
|
+
if (!hasGit()) throw new Error("git not found. Install git and retry.");
|
|
3446
|
+
await spawnNpx(["skills", "add", SOURCE_URL, ...skillArgs, ...GLOBAL_INSTALL_ARGS_TAIL]);
|
|
3447
|
+
}
|
|
3448
|
+
function renderCheck(result) {
|
|
3449
|
+
console.log();
|
|
3450
|
+
console.log("frogoe skills");
|
|
3451
|
+
console.log();
|
|
3452
|
+
if (!result.location) {
|
|
3453
|
+
console.log(" No frogoe skills found in the usual locations.");
|
|
3454
|
+
console.log(" Install: npx skills add frogoe/engine");
|
|
3455
|
+
console.log(" Or: frogoe skills update");
|
|
3456
|
+
console.log();
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3459
|
+
console.log(` Location ${result.location} (${result.agent})`);
|
|
3460
|
+
console.log();
|
|
3461
|
+
const parts = [];
|
|
3462
|
+
parts.push(`\u2713 ${result.summary.current} current`);
|
|
3463
|
+
if (result.summary.outdated) parts.push(`\u2191 ${result.summary.outdated} outdated`);
|
|
3464
|
+
if (result.summary.coreMissing) parts.push(`\u25E6 ${result.summary.coreMissing} core not installed`);
|
|
3465
|
+
const onDemandMissing = result.summary.missing - result.summary.coreMissing;
|
|
3466
|
+
if (onDemandMissing) parts.push(`\u25E6 ${onDemandMissing} available on demand`);
|
|
3467
|
+
console.log(` ${parts.join(" ")}`);
|
|
3468
|
+
for (const s of result.skills.filter((x) => x.status === "outdated")) {
|
|
3469
|
+
console.log(` \u2191 ${s.name}`);
|
|
3470
|
+
}
|
|
3471
|
+
for (const s of result.skills.filter((x) => x.status === "missing" && isCoreSkill(x.name))) {
|
|
3472
|
+
console.log(` \u25E6 ${s.name} (core)`);
|
|
3473
|
+
}
|
|
3474
|
+
console.log();
|
|
3475
|
+
if (result.updateAvailable) {
|
|
3476
|
+
console.log(" Update: frogoe skills update or npx skills add frogoe/engine");
|
|
3477
|
+
} else {
|
|
3478
|
+
console.log(" Installed skills are up to date");
|
|
3479
|
+
}
|
|
3480
|
+
console.log();
|
|
3481
|
+
}
|
|
3482
|
+
var GLOBAL_INSTALL_ARGS_TAIL, SOURCE_URL, checkCommand, updateCommand, command8;
|
|
3483
|
+
var init_skills = __esm({
|
|
3484
|
+
"src/commands/skills.ts"() {
|
|
3485
|
+
"use strict";
|
|
3486
|
+
init_skillsManifest();
|
|
3487
|
+
GLOBAL_INSTALL_ARGS_TAIL = [
|
|
3488
|
+
"--global",
|
|
3489
|
+
"--agent",
|
|
3490
|
+
"claude-code",
|
|
3491
|
+
"universal",
|
|
3492
|
+
"--copy",
|
|
3493
|
+
"--full-depth",
|
|
3494
|
+
"--yes"
|
|
3495
|
+
];
|
|
3496
|
+
SOURCE_URL = "https://github.com/frogoe/engine";
|
|
3497
|
+
checkCommand = defineCommand8({
|
|
3498
|
+
meta: { name: "check", description: "Check whether installed skills are the latest version" },
|
|
3499
|
+
args: {
|
|
3500
|
+
json: { type: "boolean", description: "Output as JSON", default: false },
|
|
3501
|
+
dir: { type: "string", description: "Skills directory to check" },
|
|
3502
|
+
source: { type: "string", description: "Where 'latest' comes from" }
|
|
3503
|
+
},
|
|
3504
|
+
async run({ args }) {
|
|
3505
|
+
try {
|
|
3506
|
+
const result = await checkSkills({
|
|
3507
|
+
dir: args.dir,
|
|
3508
|
+
source: args.source,
|
|
3509
|
+
canonical: true
|
|
3510
|
+
});
|
|
3511
|
+
if (args.json) {
|
|
3512
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3513
|
+
} else {
|
|
3514
|
+
renderCheck(result);
|
|
3515
|
+
}
|
|
3516
|
+
if (result.updateAvailable) process.exitCode = 1;
|
|
3517
|
+
} catch (err) {
|
|
3518
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3519
|
+
if (msg.includes("Malformed") || msg.includes("HTTP") || msg.includes("fetch")) {
|
|
3520
|
+
console.error(`Skills check failed (offline or GitHub unreachable): ${msg}`);
|
|
3521
|
+
console.error("Try: npx skills add frogoe/engine \u2014 or retry when online.");
|
|
3522
|
+
} else {
|
|
3523
|
+
console.error(`Skills check failed: ${msg}`);
|
|
3524
|
+
}
|
|
3525
|
+
process.exitCode = 1;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
});
|
|
3529
|
+
updateCommand = defineCommand8({
|
|
3530
|
+
meta: {
|
|
3531
|
+
name: "update",
|
|
3532
|
+
description: "Update frogoe skills to the latest (core + installed). Pass names to also install them."
|
|
3533
|
+
},
|
|
3534
|
+
args: {
|
|
3535
|
+
json: { type: "boolean", description: "Output as JSON", default: false }
|
|
3536
|
+
},
|
|
3537
|
+
async run({ args }) {
|
|
3538
|
+
const requested = (args._ ?? []).map(String).filter(Boolean);
|
|
3539
|
+
const invalid = requested.filter((n) => !/^[a-z0-9][a-z0-9._-]*$/i.test(n));
|
|
3540
|
+
if (invalid.length) {
|
|
3541
|
+
console.error(`Invalid skill name(s): ${invalid.join(", ")}`);
|
|
3542
|
+
process.exitCode = 1;
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
try {
|
|
3546
|
+
const check = await checkSkills({ canonical: true });
|
|
3547
|
+
const toInstall = /* @__PURE__ */ new Set();
|
|
3548
|
+
for (const s of check.skills) {
|
|
3549
|
+
if (s.status === "outdated" || s.status === "missing" && isCoreSkill(s.name))
|
|
3550
|
+
toInstall.add(s.name);
|
|
3551
|
+
}
|
|
3552
|
+
for (const n of requested) toInstall.add(n);
|
|
3553
|
+
if (toInstall.size === 0) {
|
|
3554
|
+
const msg = "Installed skills are already up to date.";
|
|
3555
|
+
if (args.json) console.log(JSON.stringify({ ...check, message: msg }, null, 2));
|
|
3556
|
+
else console.log(msg);
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
const list = [...toInstall];
|
|
3560
|
+
console.log(`Updating ${list.length} skill(s): ${list.join(", ")}`);
|
|
3561
|
+
await installSkills(list);
|
|
3562
|
+
const verify = await checkSkills({ canonical: true });
|
|
3563
|
+
if (args.json) console.log(JSON.stringify(verify, null, 2));
|
|
3564
|
+
else renderCheck(verify);
|
|
3565
|
+
if (verify.updateAvailable) process.exitCode = 1;
|
|
3566
|
+
} catch (err) {
|
|
3567
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3568
|
+
console.error(`Update failed: ${msg}`);
|
|
3569
|
+
if (msg.includes("npx") || msg.includes("git")) {
|
|
3570
|
+
console.error("Install Node.js and git, then retry: npx skills add frogoe/engine");
|
|
3571
|
+
}
|
|
3572
|
+
process.exitCode = 1;
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
command8 = defineCommand8({
|
|
3577
|
+
meta: {
|
|
3578
|
+
name: "skills",
|
|
3579
|
+
description: "Install, check, and update frogoe skills for AI coding tools"
|
|
3580
|
+
},
|
|
3581
|
+
subCommands: { check: checkCommand, update: updateCommand },
|
|
3582
|
+
args: {},
|
|
3583
|
+
async run() {
|
|
3584
|
+
try {
|
|
3585
|
+
console.log("Installing all frogoe skills...");
|
|
3586
|
+
await installSkills("*");
|
|
3587
|
+
const result = await checkSkills({ canonical: true });
|
|
3588
|
+
renderCheck(result);
|
|
3589
|
+
} catch (err) {
|
|
3590
|
+
console.error(`Install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3591
|
+
process.exitCode = 1;
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
});
|
|
3597
|
+
|
|
3084
3598
|
// src/cli.ts
|
|
3085
|
-
import { defineCommand as
|
|
3599
|
+
import { defineCommand as defineCommand9, runMain } from "citty";
|
|
3600
|
+
|
|
3601
|
+
// package.json
|
|
3602
|
+
var package_default = {
|
|
3603
|
+
name: "frogoe",
|
|
3604
|
+
version: "0.3.0",
|
|
3605
|
+
description: "froge CLI \u2014 the agent's hands: init, add, run, check, bundle",
|
|
3606
|
+
homepage: "https://github.com/frogoe/engine#readme",
|
|
3607
|
+
bugs: "https://github.com/frogoe/engine/issues",
|
|
3608
|
+
license: "Apache-2.0",
|
|
3609
|
+
repository: {
|
|
3610
|
+
type: "git",
|
|
3611
|
+
url: "git+https://github.com/frogoe/engine.git",
|
|
3612
|
+
directory: "packages/cli"
|
|
3613
|
+
},
|
|
3614
|
+
bin: {
|
|
3615
|
+
frogoe: "bin/frogoe.mjs"
|
|
3616
|
+
},
|
|
3617
|
+
files: [
|
|
3618
|
+
"bin",
|
|
3619
|
+
"dist"
|
|
3620
|
+
],
|
|
3621
|
+
type: "module",
|
|
3622
|
+
exports: {
|
|
3623
|
+
".": {
|
|
3624
|
+
import: "./dist/cli.js",
|
|
3625
|
+
types: "./dist/cli.d.ts"
|
|
3626
|
+
}
|
|
3627
|
+
},
|
|
3628
|
+
scripts: {
|
|
3629
|
+
test: "bun test",
|
|
3630
|
+
"check-types": "tsc --noEmit",
|
|
3631
|
+
build: "tsup && bun scripts/build-copy.mjs",
|
|
3632
|
+
dev: "bun src/cli.ts"
|
|
3633
|
+
},
|
|
3634
|
+
dependencies: {
|
|
3635
|
+
"@hono/node-server": "^2.1.1",
|
|
3636
|
+
"@puppeteer/browsers": "^3.2.1",
|
|
3637
|
+
citty: "^0.2.2",
|
|
3638
|
+
esbuild: "^0.28.2",
|
|
3639
|
+
hono: "^4.13.5",
|
|
3640
|
+
"puppeteer-core": "^25.9.0",
|
|
3641
|
+
"qrcode-terminal": "^0.12.0"
|
|
3642
|
+
},
|
|
3643
|
+
devDependencies: {
|
|
3644
|
+
"@frogoe/lint": "workspace:*",
|
|
3645
|
+
tsup: "^8.5.1"
|
|
3646
|
+
},
|
|
3647
|
+
engines: {
|
|
3648
|
+
node: ">=22"
|
|
3649
|
+
}
|
|
3650
|
+
};
|
|
3086
3651
|
|
|
3087
3652
|
// src/version.ts
|
|
3088
|
-
var VERSION =
|
|
3653
|
+
var VERSION = package_default.version;
|
|
3089
3654
|
|
|
3090
3655
|
// src/cli.ts
|
|
3091
3656
|
for (const stream of [process.stdout, process.stderr]) {
|
|
@@ -3102,23 +3667,27 @@ if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
|
3102
3667
|
var HELP = `frogoe ${VERSION} \u2014 write a closure, ship a game
|
|
3103
3668
|
|
|
3104
3669
|
Commands:
|
|
3105
|
-
init [name]
|
|
3106
|
-
add <block>
|
|
3107
|
-
run [dir]
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3670
|
+
init [name] scaffold a runnable game folder
|
|
3671
|
+
add <block> copy a registry HUD block into blocks/
|
|
3672
|
+
run [dir] serve with live reload + phone QR (--tunnel: any network)
|
|
3673
|
+
lint [dir] static contract lint \u2014 fast iteration (stable codes; --json)
|
|
3674
|
+
check [dir] full gate: lint + live Chrome sandbox (FPS, HUD outline)
|
|
3675
|
+
report [dir] last playtest session: fps dips, errors, when
|
|
3676
|
+
bundle [dir] dissolve externals \u2192 one self-contained HTML
|
|
3677
|
+
skills [check|update] skill freshness \u2014 check or update via npx skills add
|
|
3111
3678
|
|
|
3112
3679
|
Docs: skills/frogoe-core \u2014 the whole contract in five references.`;
|
|
3113
|
-
var main =
|
|
3680
|
+
var main = defineCommand9({
|
|
3114
3681
|
meta: { description: HELP },
|
|
3115
3682
|
subCommands: {
|
|
3116
3683
|
add: () => Promise.resolve().then(() => (init_add2(), add_exports)).then((m) => m.command),
|
|
3117
3684
|
bundle: () => Promise.resolve().then(() => (init_bundle2(), bundle_exports)).then((m) => m.command),
|
|
3118
3685
|
check: () => Promise.resolve().then(() => (init_check3(), check_exports)).then((m) => m.command),
|
|
3119
3686
|
init: () => Promise.resolve().then(() => (init_init2(), init_exports)).then((m) => m.command),
|
|
3687
|
+
lint: () => Promise.resolve().then(() => (init_lint(), lint_exports)).then((m) => m.command),
|
|
3120
3688
|
report: () => Promise.resolve().then(() => (init_report(), report_exports)).then((m) => m.command),
|
|
3121
|
-
run: () => Promise.resolve().then(() => (init_run2(), run_exports2)).then((m) => m.command)
|
|
3689
|
+
run: () => Promise.resolve().then(() => (init_run2(), run_exports2)).then((m) => m.command),
|
|
3690
|
+
skills: () => Promise.resolve().then(() => (init_skills(), skills_exports)).then((m) => m.command)
|
|
3122
3691
|
}
|
|
3123
3692
|
});
|
|
3124
3693
|
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