frogoe 0.2.2 → 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 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((resolve) => {
375
- setTimeout(resolve, ms);
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((resolve) => {
1337
- setTimeout(resolve, ms);
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((resolve) => {
1346
- setTimeout(resolve, 80);
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((resolve) => {
1373
- setTimeout(resolve, 100);
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((resolve) => {
1725
- setTimeout(resolve, ms);
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((resolve, reject) => {
2378
+ await new Promise((resolve2, reject) => {
2379
2379
  server.once("error", reject);
2380
- server.listen(requestedPort, "0.0.0.0", () => resolve());
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: { type: "boolean", description: "also run the headless-browser sandbox pass" }
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
- if (args.live) {
2577
- const { collectLive: collectLive2 } = await Promise.resolve().then(() => (init_live(), live_exports));
2578
- console.log(" live pass: boot \u2192 play \u2192 end \u2192 retry (headless chrome)\u2026");
2579
- const live = await collectLive2({ dir });
2580
- result.findings = [...result.findings, ...live.findings].sort(
2581
- (a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0)
2582
- );
2583
- result.errors = result.findings.filter((f) => f.severity === "error").length;
2584
- result.warnings = result.findings.filter((f) => f.severity === "warning").length;
2585
- if (live.screenshots.length > 0) {
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: { description: "contract lint with stable finding codes" }
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: () => command5
2680
+ command: () => command6
2640
2681
  });
2641
2682
  import { readFileSync as readFileSync6 } from "fs";
2642
- import { defineCommand as defineCommand5 } from "citty";
2643
- var command5;
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
- command5 = defineCommand5({
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((resolve, reject) => {
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
- resolve({ exited, stop: killTree, url });
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: () => command6
3003
+ command: () => command7
2963
3004
  });
2964
- import { defineCommand as defineCommand6 } from "citty";
2965
- var NUDGE_MS, printQr, message, command6;
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
- command6 = defineCommand6({
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,13 +3122,486 @@ 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 defineCommand7, runMain } from "citty";
3599
+ import { defineCommand as defineCommand9, runMain } from "citty";
3086
3600
 
3087
3601
  // package.json
3088
3602
  var package_default = {
3089
3603
  name: "frogoe",
3090
- version: "0.2.2",
3604
+ version: "0.3.0",
3091
3605
  description: "froge CLI \u2014 the agent's hands: init, add, run, check, bundle",
3092
3606
  homepage: "https://github.com/frogoe/engine#readme",
3093
3607
  bugs: "https://github.com/frogoe/engine/issues",
@@ -3153,23 +3667,27 @@ if (process.argv.includes("--version") || process.argv.includes("-v")) {
3153
3667
  var HELP = `frogoe ${VERSION} \u2014 write a closure, ship a game
3154
3668
 
3155
3669
  Commands:
3156
- init [name] scaffold a runnable game folder
3157
- add <block> copy a registry HUD block into blocks/
3158
- run [dir] serve with live reload + phone QR (--tunnel: any network)
3159
- check [dir] contract lint (stable finding codes; --json)
3160
- report [dir] last playtest session: fps dips, errors, when
3161
- bundle [dir] dissolve externals \u2192 one self-contained HTML
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
3162
3678
 
3163
3679
  Docs: skills/frogoe-core \u2014 the whole contract in five references.`;
3164
- var main = defineCommand7({
3680
+ var main = defineCommand9({
3165
3681
  meta: { description: HELP },
3166
3682
  subCommands: {
3167
3683
  add: () => Promise.resolve().then(() => (init_add2(), add_exports)).then((m) => m.command),
3168
3684
  bundle: () => Promise.resolve().then(() => (init_bundle2(), bundle_exports)).then((m) => m.command),
3169
3685
  check: () => Promise.resolve().then(() => (init_check3(), check_exports)).then((m) => m.command),
3170
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),
3171
3688
  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)
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)
3173
3691
  }
3174
3692
  });
3175
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 `frogoe` skill — it confirms the BRIEF (verb, mood, palette) up front and routes every request. The domain skills it routes to:
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
- - `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. Finding codes table for self-healing.
12
- - `frogoe-registry` — HUD block catalog: find, evaluate, install, author new blocks.
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`). Missing or stale? Re-run the install command and restart the agent session.
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. Warnings should be reviewed before bundling. Use `--json` for
65
- > machine-readable findings that can be fixed programmatically.
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 check # static: BRIEF validation, folder structure, input patterns
83
- frogoe check --live # browser: runtime errors, canvas painted, FPS, playability
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 `frogoe` skill — it confirms the BRIEF (verb, mood, palette) up front and routes every request. The domain skills it routes to:
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
- - `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. Finding codes table for self-healing.
12
- - `frogoe-registry` — HUD block catalog: find, evaluate, install, author new blocks.
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`). Missing or stale? Re-run the install command and restart the agent session.
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. Warnings should be reviewed before bundling. Use `--json` for
65
- > machine-readable findings that can be fixed programmatically.
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 check # static: BRIEF validation, folder structure, input patterns
83
- frogoe check --live # browser: runtime errors, canvas painted, FPS, playability
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frogoe",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "froge CLI — the agent's hands: init, add, run, check, bundle",
5
5
  "homepage": "https://github.com/frogoe/engine#readme",
6
6
  "bugs": "https://github.com/frogoe/engine/issues",