evolutionary-arcade 0.1.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - New `arcade-onboarding` skill, read with `arcade guide onboarding`: puts a creator's existing games on the arcade, from sign-in first to their profile link, with one dry run and one go for the whole batch.
6
+ - `arcade guide [skill]` prints any of the five skills into the agent's chat, so installing never needs an agent restart. `arcade guide --list` lists them.
7
+ - `arcade login` opens the sign-in page in your browser (`--no-browser` to only print it), and on approval prints your profile address and points back to onboarding.
8
+ - Validation errors say what to produce: a missing thumbnail says to capture a 1280x720 frame of real play and where to save it, and a folder without arcade.json gets the list of fields to write.
9
+ - Demos come with a GIF. `arcade publish` and `arcade media preview` cut a looping `media/preview.gif` from the demo with ffmpeg and set the new `preview_gif` field; a game with a demo can't publish without one. `arcade media gif` remakes just the GIF.
10
+ - A signed-out `arcade publish --dry-run` still runs every local check and prints its Heads up list, then asks you to sign in.
11
+ - The dry run's Heads up list also flags missing controls, a missing demo, and a preview or GIF older than the demo.
12
+
3
13
  ## 0.1.1
4
14
 
5
15
  - Every game on Evolutionary Arcade is open source under MIT. `arcade publish` says so before it publishes, and refuses an `arcade.json` whose `license` is anything but `"MIT"` (leaving it out means MIT).
package/README.md CHANGED
@@ -8,8 +8,8 @@ The `arcade` CLI for [Evolutionary Arcade](https://evolutionaryarcade.com), an a
8
8
 
9
9
  ```bash
10
10
  npm i -g evolutionary-arcade
11
- arcade skills install # give your coding agent the four arcade skills
12
- arcade login # approve a code in your browser
11
+ arcade skills install # give your coding agent the arcade skills
12
+ arcade login # opens your browser; approve the code
13
13
  arcade fork starwake # start your own game from Starwake
14
14
  cd starwake-remix
15
15
  arcade dev # play it locally, under the arcade's rules
@@ -19,13 +19,15 @@ arcade publish --yes # and it's live
19
19
 
20
20
  ## Hand it to your agent
21
21
 
22
- The CLI is built for coding agents like Claude Code and Codex. Paste this in:
22
+ The CLI is built for coding agents like Claude Code and Codex. To put the games you've already made on the arcade, paste this into your agent:
23
23
 
24
24
  ```text
25
- Run `arcade guide` and follow it to fork starwake into a new game. Show me the dry run before you publish.
25
+ Help me put my games on Evolutionary Arcade (evolutionaryarcade.com). Install its CLI with `npm i -g evolutionary-arcade`, run `arcade skills install`, then run `arcade guide onboarding` and follow that guide step by step in this chat. No restart needed.
26
26
  ```
27
27
 
28
- `arcade guide` prints the getting-started skill. `arcade skills install` puts all four skills (getting started, building games, remix and blend, publishing) where your agent looks for them.
28
+ It installs the CLI and the skills, signs you in first (you approve in your browser), finds your games, brings each one up to the arcade's standard with real screenshots, controls, and a demo, shows you one dry run, and publishes on your go. You end with your profile link.
29
+
30
+ `arcade guide <name>` prints any of the five skills (onboarding, getting started, building games, remix and blend, publishing) straight into the agent's chat, so it never needs a restart. `arcade skills install` also puts them where your agent looks for skills in future sessions.
29
31
 
30
32
  ## Four ways to build on a game
31
33
 
@@ -40,8 +42,8 @@ Everything you publish is public, including the source, and every game is open s
40
42
 
41
43
  | Command | What it does |
42
44
  |---|---|
43
- | `arcade guide` | Prints the getting-started skill. Agents read this first. |
44
- | `arcade skills install [--target claude\|codex\|all] [--dir <path>]` | Copies the four skills into `~/.claude/skills` and/or `~/.agents/skills`. |
45
+ | `arcade guide [skill] [--list]` | Prints a skill into the agent's chat, no restart needed (default: getting started). `arcade guide onboarding` puts existing games on the arcade. |
46
+ | `arcade skills install [--target claude\|codex\|all] [--dir <path>]` | Copies the five skills into `~/.claude/skills` and/or `~/.agents/skills`. |
45
47
  | `arcade login` / `arcade logout` / `arcade whoami` | Sign in with a device code, sign out, or see who you are. |
46
48
  | `arcade search <query>` | Find games by title, slug, @handle, or model. |
47
49
  | `arcade info <slug>` | A game's versions, generation ids, which one is main, and its parents. |
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { dirname, isAbsolute, join } from "path";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "evolutionary-arcade",
16
- version: "0.1.1",
16
+ version: "0.2.0",
17
17
  description: "The arcade CLI for Evolutionary Arcade. Publish, update, regen, fork, and blend AI-made browser games with your own coding agent.",
18
18
  type: "module",
19
19
  license: "MIT",
@@ -176,6 +176,7 @@ async function request(path, init = {}) {
176
176
  import { Command, InvalidArgumentError, Option } from "commander";
177
177
 
178
178
  // src/commands/account.ts
179
+ import { spawn } from "child_process";
179
180
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
180
181
  import { hostname, platform } from "os";
181
182
  import { extname } from "path";
@@ -228,7 +229,20 @@ var ExitError = class extends Error {
228
229
  };
229
230
 
230
231
  // src/commands/account.ts
231
- async function login() {
232
+ function openBrowser(url2) {
233
+ if (process.env.ARCADE_NO_BROWSER === "1" || process.env.CI) return false;
234
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : ["xdg-open", [url2]];
235
+ try {
236
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
237
+ child.on("error", () => {
238
+ });
239
+ child.unref();
240
+ return true;
241
+ } catch {
242
+ return false;
243
+ }
244
+ }
245
+ async function login(opts = {}) {
232
246
  const current = savedLogin();
233
247
  if (current?.handle)
234
248
  out(
@@ -247,8 +261,16 @@ async function login() {
247
261
  throw new ExitError(why ?? `Couldn't start login (${res.status}). Try again in a minute.`);
248
262
  }
249
263
  const start = await res.json();
250
- out(`Open ${cyan(start.verification_uri_complete)}`);
264
+ const opened = opts.browser !== false && openBrowser(start.verification_uri_complete);
265
+ out(
266
+ `${opened ? "Opened" : "Open"} ${cyan(start.verification_uri_complete)}${opened ? " in your browser" : ""}`
267
+ );
251
268
  out(`and confirm the code ${bold(start.user_code)}`);
269
+ out(
270
+ dim(
271
+ "New here? You'll pick a handle. It's permanent and becomes your profile address (/u/<handle>), so the name people know you by (like your channel) is a good pick."
272
+ )
273
+ );
252
274
  out(dim("Waiting for approval\u2026 (this can run in the background)"));
253
275
  let interval = start.interval;
254
276
  const deadline = Date.now() + start.expires_in * 1e3;
@@ -267,9 +289,12 @@ async function login() {
267
289
  saveToken(body.access_token);
268
290
  const me = await request("/api/me");
269
291
  if (me.user?.handle) saveToken(body.access_token, me.user.handle);
270
- out(`Signed in as ${cyan(`@${me.user?.handle ?? "?"}`)}.`);
292
+ const handle = me.user?.handle;
293
+ out(`Signed in as ${cyan(`@${handle ?? "?"}`)}.`);
294
+ if (handle) out(`Your profile: ${cyan(`${apiUrl()}/u/${handle}`)}`);
295
+ else out(`Pick your handle at ${cyan(`${apiUrl()}/welcome`)} before you publish.`);
271
296
  next(
272
- "arcade new <slug> to start a game, or arcade fork <game> to build on one. New here? arcade guide"
297
+ "back in your agent, carry on with onboarding (arcade guide onboarding): find your games, pick, publish."
273
298
  );
274
299
  return;
275
300
  }
@@ -20290,6 +20315,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20290
20315
  SOFTWARE.
20291
20316
  `;
20292
20317
  }
20318
+ var IMPORT_LICENSES = ["MIT", "CC0-1.0", "Unlicense"];
20319
+ var IMPORT_RULE = "Imported games must be MIT, or public domain (CC0 or the Unlicense).";
20293
20320
 
20294
20321
  // ../format/src/slug.ts
20295
20322
  var SLUG_MIN = 3;
@@ -20436,6 +20463,13 @@ var provenanceSchema = external_exports.looseObject({
20436
20463
  cost_usd: external_exports.number().nonnegative().optional(),
20437
20464
  wall_time_minutes: external_exports.number().nonnegative().optional(),
20438
20465
  cost_basis: external_exports.enum(["api-equivalent", "billed"]).optional()
20466
+ }).optional(),
20467
+ // The community prompt (evolutionaryarcade.com/prompts) the game was built from: its author's
20468
+ // handle and the prompt's id. The game page shows "Prompted by @handle" when the arcade's own
20469
+ // record of that prompt agrees.
20470
+ prompted_by: external_exports.object({
20471
+ handle: external_exports.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use the prompter's handle, without the @.").max(40),
20472
+ prompt_id: external_exports.string().regex(/^[0-9a-z]{26}$/, "Use the prompt's id from its page.")
20439
20473
  }).optional()
20440
20474
  }).superRefine((p, ctx) => {
20441
20475
  const bytes = utf8Length(JSON.stringify(p));
@@ -20487,17 +20521,30 @@ var MEDIA_CONTENT_TYPES = {
20487
20521
  jpeg: "image/jpeg",
20488
20522
  webp: "image/webp",
20489
20523
  mp4: "video/mp4",
20490
- webm: "video/webm"
20524
+ webm: "video/webm",
20525
+ // Only for preview_gif: the CLI makes it from the demo for emails and link previews.
20526
+ gif: "image/gif"
20491
20527
  };
20492
20528
  var ext = (p) => p.split(".").pop()?.toLowerCase() ?? "";
20493
20529
  var imagePath = relPath.refine(
20494
20530
  (p) => ["png", "jpg", "jpeg", "webp"].includes(ext(p)),
20495
20531
  "Use a .png, .jpg, or .webp image."
20496
20532
  );
20533
+ var gifPath = relPath.refine((p) => ext(p) === "gif", "Use a .gif made from the demo.");
20497
20534
  var videoPath = relPath.refine(
20498
20535
  (p) => ["mp4", "webm"].includes(ext(p)),
20499
20536
  "Use an .mp4 or .webm video."
20500
20537
  );
20538
+ var sourceSchema = external_exports.object({
20539
+ repo: external_exports.string().regex(
20540
+ /^https:\/\/github\.com\/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/,
20541
+ "Use the repo's address, like https://github.com/owner/repo."
20542
+ ),
20543
+ owner_id: external_exports.number().int().positive(),
20544
+ author: external_exports.string().trim().min(1).max(100),
20545
+ license: external_exports.enum(IMPORT_LICENSES),
20546
+ license_file: relPath
20547
+ });
20501
20548
  function mediaContentType(path) {
20502
20549
  return MEDIA_CONTENT_TYPES[ext(path)] ?? null;
20503
20550
  }
@@ -20530,8 +20577,13 @@ var arcadeSchema = external_exports.object({
20530
20577
  screenshots: external_exports.array(imagePath).min(1).max(12),
20531
20578
  demo_video: videoPath.optional(),
20532
20579
  preview_video: videoPath.optional(),
20580
+ // A short looping GIF cut from the demo (`arcade publish` makes it), for places a video can't
20581
+ // play: "someone remixed your game" emails and link previews. The CLI requires it whenever
20582
+ // there's a demo.
20583
+ preview_gif: gifPath.optional(),
20533
20584
  lineage: lineageSchema.default({ kind: "original", parents: [] }),
20534
- provenance: provenanceSchema.default({ subagent_models: [] })
20585
+ provenance: provenanceSchema.default({ subagent_models: [] }),
20586
+ source: sourceSchema.optional()
20535
20587
  }).superRefine((a, ctx) => {
20536
20588
  if (a.lineage.parents.some((p) => p.slug === a.slug)) {
20537
20589
  ctx.addIssue({
@@ -20604,6 +20656,8 @@ var friendlyIssue = (iss) => {
20604
20656
  case "leaderboards.*.max":
20605
20657
  if (iss.input === void 0) return "Add a max: the best score a real player could reach.";
20606
20658
  break;
20659
+ case "source.license":
20660
+ return IMPORT_RULE;
20607
20661
  case "provenance.orchestrator.model":
20608
20662
  case "provenance.subagent_models.*.model":
20609
20663
  if (missing) return "Add the model's name.";
@@ -21024,7 +21078,8 @@ function derived2(src, overrides) {
21024
21078
  "thumbnail",
21025
21079
  "screenshots",
21026
21080
  "demo_video",
21027
- "preview_video"
21081
+ "preview_video",
21082
+ "preview_gif"
21028
21083
  ];
21029
21084
  const out2 = { schema: "arcade/v0" };
21030
21085
  for (const k of keep) if (a[k] !== void 0) out2[k] = a[k];
@@ -21326,7 +21381,7 @@ function dev(dirArg, opts) {
21326
21381
 
21327
21382
  // src/commands/publish.ts
21328
21383
  import { execFileSync } from "child_process";
21329
- import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
21384
+ import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
21330
21385
  import { isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve3, sep as sep4 } from "path";
21331
21386
 
21332
21387
  // src/files.ts
@@ -21409,6 +21464,55 @@ function walkGame(root) {
21409
21464
  return { files, skipped, skippedLinks };
21410
21465
  }
21411
21466
 
21467
+ // src/fixes.ts
21468
+ var CAPTURE_GUIDE = 'arcade guide publishing, "Capturing media"';
21469
+ var thumbnailFix = (path) => `Run the game (arcade dev), capture a 1280x720 frame of real play, mid-action with no title screen or added text, and save it as ${path}.`;
21470
+ var screenshotFix = (path) => `Capture a different moment of real play at 1280x720 and save it as ${path}. Aim for 2 to 4 screenshots in all.`;
21471
+ var demoFix = (path) => `Record 15 to 30 s of real play at 1280x720 as H.264 and save it as ${path}, with the action starting in the first second (${CAPTURE_GUIDE}).`;
21472
+ var fromDemoFix = "Run `arcade media preview` to make it from the demo.";
21473
+ var ARCADE_JSON_TEMPLATE = [
21474
+ '"schema": "arcade/v0"',
21475
+ '"slug": the permanent web address, 3 to 40 lowercase letters, digits, and single hyphens. Pick it with the creator.',
21476
+ '"title": up to 80 characters',
21477
+ '"description": one or two sentences a player reads before pressing play',
21478
+ '"controls": every key, button, and touch a player needs',
21479
+ '"input": { "keyboard_mouse": true, "gamepad": false, "touch": false } (true only for paths that work end to end)',
21480
+ `"thumbnail": "media/thumbnail.png". ${thumbnailFix("media/thumbnail.png")}`,
21481
+ `"screenshots": ["media/shot-1.png", "media/shot-2.png"]. Real play at 1280x720, 1 to 12 of them.`,
21482
+ '"play": { "root": "dist" } only if the playable build lives in a subfolder'
21483
+ ];
21484
+ function explainIssue(issue2, raw) {
21485
+ const line = issue2.path && issue2.message.startsWith(`${issue2.path} `) ? issue2.message : `${issue2.path || "arcade.json"}: ${issue2.message}`;
21486
+ const fix = fixFor(issue2, raw);
21487
+ return fix ? `${line} Fix: ${fix}` : line;
21488
+ }
21489
+ function fixFor(issue2, raw) {
21490
+ const path = issue2.path;
21491
+ const value = (key, fallback) => typeof raw[key] === "string" && raw[key] ? raw[key] : fallback;
21492
+ if (path === "thumbnail")
21493
+ return `${thumbnailFix(value("thumbnail", "media/thumbnail.png"))} Then set "thumbnail" to that path.`;
21494
+ if (path === "screenshots" || path.startsWith("screenshots."))
21495
+ return `${screenshotFix("media/shot-1.png")} List the paths in "screenshots".`;
21496
+ if (path === "demo_video") return `${demoFix("media/demo.mp4")} Then set "demo_video".`;
21497
+ if (path === "preview_video" || path === "preview_gif") return fromDemoFix;
21498
+ if (path === "title") return `Set "title" to the game's name as players should see it.`;
21499
+ if (path === "description")
21500
+ return 'Set "description" to one or two sentences: the fantasy and the goal.';
21501
+ if (path === "slug")
21502
+ return 'Set "slug" to the web address you chose with the creator. It is permanent.';
21503
+ if (path === "schema") return 'Add "schema": "arcade/v0" at the top.';
21504
+ return null;
21505
+ }
21506
+ function missingMediaLine(path, raw) {
21507
+ if (raw.thumbnail === path) return `${path} (thumbnail): ${thumbnailFix(path)}`;
21508
+ if (Array.isArray(raw.screenshots) && raw.screenshots.includes(path))
21509
+ return `${path} (screenshot): ${screenshotFix(path)}`;
21510
+ if (raw.demo_video === path) return `${path} (demo): ${demoFix(path)}`;
21511
+ if (raw.preview_video === path) return `${path} (hover preview): ${fromDemoFix}`;
21512
+ if (raw.preview_gif === path) return `${path} (GIF): ${fromDemoFix}`;
21513
+ return path;
21514
+ }
21515
+
21412
21516
  // src/secrets.ts
21413
21517
  import { readFileSync as readFileSync5 } from "fs";
21414
21518
  var MAX_SCAN_BYTES = 5 * 1024 * 1024;
@@ -21574,7 +21678,12 @@ function missingSessions(named) {
21574
21678
  function readArcadeRaw(dir) {
21575
21679
  const path = join5(dir, "arcade.json");
21576
21680
  if (!existsSync5(path))
21577
- throw new ExitError(`No arcade.json in ${dir}. Start with \`arcade new <slug>\`.`, 2);
21681
+ throw new ExitError(
21682
+ `No arcade.json in ${dir}. Write one at the top of the game folder with at least these fields (arcade guide publishing has a full example):`,
21683
+ 2,
21684
+ ARCADE_JSON_TEMPLATE,
21685
+ "Starting a brand-new game instead? arcade new <slug>"
21686
+ );
21578
21687
  try {
21579
21688
  return JSON.parse(readFileSync7(path, "utf8"));
21580
21689
  } catch (e) {
@@ -21589,48 +21698,77 @@ function hasFfmpeg() {
21589
21698
  return false;
21590
21699
  }
21591
21700
  }
21701
+ function runFfmpeg(args, what) {
21702
+ try {
21703
+ execFileSync("ffmpeg", ["-y", "-loglevel", "error", ...args], {
21704
+ stdio: ["ignore", "ignore", "pipe"]
21705
+ });
21706
+ } catch (e) {
21707
+ const last = String(e.stderr ?? "").trim().split("\n").filter(Boolean).pop();
21708
+ throw new ExitError(`ffmpeg couldn't make the ${what}${last ? `: ${last}` : "."}`);
21709
+ }
21710
+ }
21592
21711
  function makePreview(dir, demo, target = "media/preview.mp4") {
21593
21712
  if (!hasFfmpeg()) return null;
21594
- try {
21595
- execFileSync(
21596
- "ffmpeg",
21713
+ runFfmpeg(
21714
+ [
21715
+ "-i",
21716
+ join5(dir, demo),
21717
+ "-t",
21718
+ "12",
21719
+ "-an",
21720
+ "-vf",
21721
+ "scale=854:-2,fps=30",
21722
+ "-c:v",
21723
+ "libx264",
21724
+ "-preset",
21725
+ "veryfast",
21726
+ "-crf",
21727
+ "30",
21728
+ "-pix_fmt",
21729
+ "yuv420p",
21730
+ "-movflags",
21731
+ "+faststart",
21732
+ join5(dir, target)
21733
+ ],
21734
+ "preview"
21735
+ );
21736
+ return target;
21737
+ }
21738
+ var MAX_GIF_BYTES = 5 * 1024 * 1024;
21739
+ function makeGif(dir, demo, target = "media/preview.gif") {
21740
+ if (!hasFfmpeg()) return null;
21741
+ const tries = [
21742
+ { width: 480, fps: 12, seconds: 6 },
21743
+ { width: 360, fps: 10, seconds: 5 }
21744
+ ];
21745
+ for (const t of tries) {
21746
+ runFfmpeg(
21597
21747
  [
21598
- "-y",
21599
- "-loglevel",
21600
- "error",
21601
21748
  "-i",
21602
21749
  join5(dir, demo),
21603
21750
  "-t",
21604
- "12",
21751
+ String(t.seconds),
21605
21752
  "-an",
21606
21753
  "-vf",
21607
- "scale=854:-2,fps=30",
21608
- "-c:v",
21609
- "libx264",
21610
- "-preset",
21611
- "veryfast",
21612
- "-crf",
21613
- "30",
21614
- "-pix_fmt",
21615
- "yuv420p",
21616
- "-movflags",
21617
- "+faststart",
21754
+ `fps=${t.fps},scale=${t.width}:-2:flags=lanczos,split[a][b];[a]palettegen=max_colors=128:stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=4`,
21755
+ "-loop",
21756
+ "0",
21618
21757
  join5(dir, target)
21619
21758
  ],
21620
- { stdio: ["ignore", "ignore", "pipe"] }
21759
+ "GIF"
21621
21760
  );
21622
- } catch (e) {
21623
- const last = String(e.stderr ?? "").trim().split("\n").filter(Boolean).pop();
21624
- throw new ExitError(`ffmpeg couldn't make the preview${last ? `: ${last}` : "."}`);
21761
+ if (statSync5(join5(dir, target)).size <= MAX_GIF_BYTES) return target;
21625
21762
  }
21626
- return target;
21763
+ throw new ExitError(
21764
+ `The GIF came out over ${MAX_GIF_BYTES / 1024 / 1024} MB, even small. Re-cut the demo so its first 5 seconds are less busy (a later -ss), then run \`arcade media preview\` again.`
21765
+ );
21627
21766
  }
21628
- async function mediaPreview(dirArg) {
21629
- const dir = resolve3(dirArg ?? ".");
21630
- const raw = readArcadeRaw(dir);
21767
+ var NO_FFMPEG = "ffmpeg isn't installed. Install it (macOS: `brew install ffmpeg`, Ubuntu: `sudo apt install ffmpeg`, Windows: `winget install ffmpeg`) and try again.";
21768
+ function demoFor(dir, raw, what) {
21631
21769
  if (typeof raw.demo_video !== "string")
21632
21770
  throw new ExitError(
21633
- "arcade.json has no demo_video to make a preview from. Record a demo first (see arcade-publishing).",
21771
+ `arcade.json has no demo_video to make ${what} from. Record 15-30 s of real play at 1280x720 into media/demo.mp4 and set "demo_video": "media/demo.mp4" (arcade guide publishing, "Capturing media").`,
21634
21772
  2
21635
21773
  );
21636
21774
  if (!existsSync5(join5(dir, raw.demo_video)))
@@ -21638,15 +21776,33 @@ async function mediaPreview(dirArg) {
21638
21776
  `${raw.demo_video} isn't in the folder yet. Record a demo first (see arcade-publishing).`,
21639
21777
  2
21640
21778
  );
21641
- const made = makePreview(dir, raw.demo_video);
21642
- if (!made)
21643
- throw new ExitError(
21644
- "ffmpeg isn't installed. Install it (e.g. `brew install ffmpeg`) and try again."
21645
- );
21779
+ return raw.demo_video;
21780
+ }
21781
+ async function mediaPreview(dirArg) {
21782
+ const dir = resolve3(dirArg ?? ".");
21783
+ const raw = readArcadeRaw(dir);
21784
+ const demo = demoFor(dir, raw, "a preview");
21785
+ const made = makePreview(dir, demo);
21786
+ if (!made) throw new ExitError(NO_FFMPEG);
21787
+ const gif = makeGif(dir, demo);
21788
+ if (!gif) throw new ExitError(NO_FFMPEG);
21646
21789
  raw.preview_video = made;
21790
+ raw.preview_gif = gif;
21791
+ writeFileSync3(join5(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
21792
+ `);
21793
+ out(
21794
+ `Made ${made} and ${gif} (${mb(statSync5(join5(dir, gif)).size)}) and added them to arcade.json.`
21795
+ );
21796
+ }
21797
+ async function mediaGif(dirArg) {
21798
+ const dir = resolve3(dirArg ?? ".");
21799
+ const raw = readArcadeRaw(dir);
21800
+ const gif = makeGif(dir, demoFor(dir, raw, "a GIF"));
21801
+ if (!gif) throw new ExitError(NO_FFMPEG);
21802
+ raw.preview_gif = gif;
21647
21803
  writeFileSync3(join5(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
21648
21804
  `);
21649
- out(`Made ${made} and added it to arcade.json.`);
21805
+ out(`Made ${gif} (${mb(statSync5(join5(dir, gif)).size)}) and added it to arcade.json.`);
21650
21806
  }
21651
21807
  function describeAction(p, parentNames) {
21652
21808
  switch (p.action) {
@@ -21708,7 +21864,7 @@ function listWords(words) {
21708
21864
  return `${words.slice(0, -1).join(", ")}${words.length > 2 ? "," : ""} and ${words.at(-1)}`;
21709
21865
  }
21710
21866
  var HOME_PATH = /(?:\/Users\/|\/home\/|[A-Za-z]:\\Users\\)[^\s/\\"'`]+/;
21711
- function headsUp(arcade, files, alreadyOnArcade, secretWarnings) {
21867
+ function headsUp(arcade, files, alreadyOnArcade, secretWarnings, dir) {
21712
21868
  const notes = [];
21713
21869
  const kind = arcade.lineage.kind;
21714
21870
  const derived3 = kind === "fork" || kind === "blend" || kind === "regen";
@@ -21734,6 +21890,7 @@ function headsUp(arcade, files, alreadyOnArcade, secretWarnings) {
21734
21890
  if (shots) words.push(shots === 1 ? "screenshot" : plural(shots, "screenshot"));
21735
21891
  if (reused(arcade.demo_video)) words.push("demo");
21736
21892
  if (reused(arcade.preview_video)) words.push("hover preview");
21893
+ if (reused(arcade.preview_gif)) words.push("GIF");
21737
21894
  if (words.length) {
21738
21895
  const one = words.length === 1 && shots <= 1;
21739
21896
  const whose = kind === "regen" ? "the original's" : kind === "fork" ? "the parent's" : "a parent's";
@@ -21768,6 +21925,30 @@ function headsUp(arcade, files, alreadyOnArcade, secretWarnings) {
21768
21925
  `${what} a home folder (${hit}). Prompts and notes are public, so edit paths out.`
21769
21926
  );
21770
21927
  }
21928
+ if (!arcade.controls?.trim())
21929
+ notes.push(
21930
+ 'There are no controls. Add "controls" with every key, button, and touch a player needs.'
21931
+ );
21932
+ if (!arcade.demo_video)
21933
+ notes.push(
21934
+ `There's no demo. Phones show the demo for keyboard-only games, and cards play a preview cut from it: record 15 to 30 s of real play (${CAPTURE_GUIDE}).`
21935
+ );
21936
+ const age = (p2) => {
21937
+ try {
21938
+ return p2 ? statSync5(join5(dir, p2)).mtimeMs : null;
21939
+ } catch {
21940
+ return null;
21941
+ }
21942
+ };
21943
+ const demoAt = age(arcade.demo_video);
21944
+ const stale = [
21945
+ ["hover preview", age(arcade.preview_video)],
21946
+ ["GIF", age(arcade.preview_gif)]
21947
+ ].filter(([, t]) => demoAt !== null && t !== null && t < demoAt);
21948
+ if (stale.length)
21949
+ notes.push(
21950
+ `The ${stale.map(([w]) => w).join(" and ")} ${stale.length > 1 ? "are" : "is"} older than the demo. Run \`arcade media preview\` to remake ${stale.length > 1 ? "them" : "it"}.`
21951
+ );
21771
21952
  const thumb = files.find((f) => f.path === arcade.thumbnail);
21772
21953
  if (thumb && thumb.size > 500 * 1024) {
21773
21954
  const webp = arcade.thumbnail.replace(/\.[^.]+$/, ".webp");
@@ -21837,6 +22018,20 @@ async function publish(dirArg, opts) {
21837
22018
  out(yellow(`No hover preview: ${e.message} Cards will show the thumbnail.`));
21838
22019
  }
21839
22020
  }
22021
+ if (typeof raw.demo_video === "string" && !raw.preview_gif && existsSync5(join5(dir, raw.demo_video))) {
22022
+ const made = makeGif(dir, raw.demo_video);
22023
+ if (!made)
22024
+ throw new ExitError(
22025
+ "This game has a demo, so it needs a GIF made from it, and that takes ffmpeg.",
22026
+ 2,
22027
+ [NO_FFMPEG],
22028
+ "Then run `arcade media preview` (or publish again, which makes it for you)."
22029
+ );
22030
+ raw.preview_gif = made;
22031
+ writeFileSync3(join5(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22032
+ `);
22033
+ out(dim(`Made a GIF from the demo (${made}, ${mb(statSync5(join5(dir, made)).size)}).`));
22034
+ }
21840
22035
  const prov = raw.provenance ?? {};
21841
22036
  const orch = prov.orchestrator ?? null;
21842
22037
  const named = opts.session ?? [];
@@ -21877,8 +22072,18 @@ async function publish(dirArg, opts) {
21877
22072
  out(dim("arcade.json already has token counts, so --session wasn't used."));
21878
22073
  }
21879
22074
  const parsed = parseArcadeJson(raw);
21880
- if (!parsed.ok) throw new ExitError("arcade.json has problems:", 2, parsed.errors);
22075
+ if (!parsed.ok)
22076
+ throw new ExitError(
22077
+ "arcade.json has problems:",
22078
+ 2,
22079
+ parsed.issues.map((i) => explainIssue(i, raw)),
22080
+ "Fix every one, then run the same command again. Don't delete fields to get past a check."
22081
+ );
21881
22082
  const arcade = parsed.value;
22083
+ if (arcade.demo_video && !arcade.preview_gif)
22084
+ throw new ExitError("A game with a demo needs a GIF made from it (preview_gif).", 2, [
22085
+ `${arcade.demo_video} (demo): record it first, then run \`arcade media preview\`.`
22086
+ ]);
21882
22087
  await fillLicenseHolder(dir);
21883
22088
  const { files, skipped, skippedLinks } = walkGame(dir);
21884
22089
  const bad = files.filter((f) => !f.type);
@@ -21891,14 +22096,20 @@ async function publish(dirArg, opts) {
21891
22096
  }
21892
22097
  const notMit = files.filter((f) => isLicenseFile(f.path)).map((f) => licenseFileProblem(f.path, readFileSync7(f.abs, "utf8"))).filter((p) => p !== null);
21893
22098
  if (notMit.length) throw new ExitError("The game's license needs fixing:", 2, notMit);
21894
- const media = [arcade.thumbnail, ...arcade.screenshots, arcade.demo_video, arcade.preview_video];
22099
+ const media = [
22100
+ arcade.thumbnail,
22101
+ ...arcade.screenshots,
22102
+ arcade.demo_video,
22103
+ arcade.preview_video,
22104
+ arcade.preview_gif
22105
+ ];
21895
22106
  const missingMedia = media.filter((m) => !!m && !files.some((f) => f.path === m));
21896
22107
  if (missingMedia.length) {
21897
22108
  throw new ExitError(
21898
22109
  "arcade.json points at files that aren't in the folder:",
21899
22110
  2,
21900
- missingMedia,
21901
- "Capture media from your build, and run `arcade media preview` after a new demo (see arcade-publishing)."
22111
+ missingMedia.map((m) => missingMediaLine(m, raw)),
22112
+ `Capture media from real play of this build (${CAPTURE_GUIDE}).`
21902
22113
  );
21903
22114
  }
21904
22115
  const allowed = allowedPaths(dir, opts.allowSecret);
@@ -21932,6 +22143,24 @@ async function publish(dirArg, opts) {
21932
22143
  try {
21933
22144
  prepared = await request("/api/publish/prepare", { method: "POST", json: body });
21934
22145
  } catch (e) {
22146
+ if (opts.dryRun && e instanceof ApiError && e.status === 401) {
22147
+ const notes2 = headsUp(arcade, files, () => false, secrets.warnings, dir);
22148
+ out(
22149
+ green(
22150
+ `Local checks passed for ${arcade.title} (${arcade.slug}): arcade.json, ${plural(files.length, "file")} (${mb(files.reduce((n, f) => n + f.size, 0))}), license, media, and secrets.`
22151
+ )
22152
+ );
22153
+ if (notes2.length) {
22154
+ out(bold(yellow("Heads up:")));
22155
+ for (const n of notes2) out(` - ${n}`);
22156
+ }
22157
+ throw new ExitError(
22158
+ "The arcade's own checks (the slug, the limits) and the full file list need you signed in.",
22159
+ 1,
22160
+ [],
22161
+ "Run `arcade login`, approve it in the browser, then run `arcade publish --dry-run` again."
22162
+ );
22163
+ }
21935
22164
  throw fromApi(e);
21936
22165
  }
21937
22166
  const { plan, missing } = prepared;
@@ -21947,7 +22176,7 @@ async function publish(dirArg, opts) {
21947
22176
  const totalBytes = files.reduce((n, f) => n + f.size, 0);
21948
22177
  const newFiles = files.filter(isNew);
21949
22178
  const uploadBytes = newFiles.reduce((n, f) => n + f.size, 0);
21950
- const notes = headsUp(arcade, files, onArcade, secrets.warnings);
22179
+ const notes = headsUp(arcade, files, onArcade, secrets.warnings, dir);
21951
22180
  out("");
21952
22181
  out(`${pink("\u25CF")} This will publish ${describeAction(plan, parentNames)}`);
21953
22182
  out(
@@ -22174,13 +22403,35 @@ function skillsDir() {
22174
22403
  "The bundled skills are missing from this install. Reinstall evolutionary-arcade."
22175
22404
  );
22176
22405
  }
22177
- function guide(opts = {}) {
22178
- const text = readFileSync8(join6(skillsDir(), "arcade-getting-started", "SKILL.md"), "utf8");
22406
+ function skillNames() {
22407
+ return readdirSync4(skillsDir()).filter((n) => n.startsWith("arcade-")).sort();
22408
+ }
22409
+ function resolveSkill(name) {
22410
+ const want = name.startsWith("arcade-") ? name : `arcade-${name}`;
22411
+ const names = skillNames();
22412
+ if (names.includes(want)) return want;
22413
+ throw new ExitError(
22414
+ `There's no skill called ${name}.`,
22415
+ 2,
22416
+ names.map((n) => n.replace(/^arcade-/, ""))
22417
+ );
22418
+ }
22419
+ function guide(name, opts = {}) {
22420
+ if (opts.list) {
22421
+ for (const n of skillNames()) out(`arcade guide ${n.replace(/^arcade-/, "")}`);
22422
+ return;
22423
+ }
22424
+ const skill = resolveSkill(name ?? "getting-started");
22425
+ const text = readFileSync8(join6(skillsDir(), skill, "SKILL.md"), "utf8");
22179
22426
  if (opts.raw) {
22180
22427
  process.stdout.write(text);
22181
22428
  return;
22182
22429
  }
22183
- out(dim("The arcade-getting-started skill (arcade skills install gives your agent all four)"));
22430
+ out(
22431
+ dim(
22432
+ `The ${skill} skill. Read any other with arcade guide <name> (${skillNames().map((n) => n.replace(/^arcade-/, "")).join(", ")}), no restart needed.`
22433
+ )
22434
+ );
22184
22435
  out("");
22185
22436
  process.stdout.write(text.replace(/^---\n[\s\S]*?\n---\n+/, ""));
22186
22437
  }
@@ -22190,7 +22441,7 @@ var TARGETS = {
22190
22441
  };
22191
22442
  function installSkills(opts) {
22192
22443
  const src = skillsDir();
22193
- const names = readdirSync4(src).filter((n) => n.startsWith("arcade-"));
22444
+ const names = skillNames();
22194
22445
  const targets = opts.dir ? [resolve4(opts.dir)] : (opts.target === "all" || !opts.target ? Object.keys(TARGETS) : [opts.target]).map((t) => {
22195
22446
  const f = TARGETS[t];
22196
22447
  if (!f) throw new ExitError(`Unknown --target ${t}. Use claude, codex, or all.`, 2);
@@ -22204,7 +22455,7 @@ function installSkills(opts) {
22204
22455
  }
22205
22456
  out(dim(names.join(", ")));
22206
22457
  next(
22207
- "restart your agent so it picks up the skills, then tell it: use the arcade-getting-started skill."
22458
+ "no restart needed. In this chat, read a skill with arcade guide <name> (start with arcade guide onboarding). New agent sessions load them on their own."
22208
22459
  );
22209
22460
  }
22210
22461
 
@@ -22241,17 +22492,23 @@ Examples:
22241
22492
  arcade publish --dry-run see exactly what would go public
22242
22493
  arcade info last-signal versions, generations, and which one is main
22243
22494
 
22244
- Agents: run \`arcade guide\` first. Everything you publish is public, including the source.`;
22495
+ Agents: run \`arcade guide\` first. To put games someone already made on the arcade,
22496
+ run \`arcade guide onboarding\`. Everything you publish is public, including the source.`;
22245
22497
  function buildProgram() {
22246
22498
  const program = new Command().name("arcade").description(
22247
22499
  "Evolutionary Arcade: an arcade of open-source, AI-made browser games.\nPublish, update, regen, fork, and blend games with your own coding agent."
22248
22500
  ).version(VERSION).showHelpAfterError().addHelpText("after", ROOT_HELP);
22249
22501
  program.commandsGroup("Getting started:");
22250
- program.command("guide").description("print the getting-started skill (read this first if you are an agent)").option("--raw", "print the exact skill file, frontmatter included").action(guide);
22251
- program.command("skills").description("manage the bundled agent skills").command("install").description("copy the 4 arcade skills into ~/.claude/skills and/or ~/.agents/skills").addOption(
22502
+ program.command("guide [skill]").description(
22503
+ "print a skill into this chat, no restart needed (default: getting-started; agents read this first)"
22504
+ ).option("--raw", "print the exact skill file, frontmatter included").option("--list", "list the skills you can print").action((skill, opts) => guide(skill, opts)).addHelpText(
22505
+ "after",
22506
+ "\nExamples:\n arcade guide onboarding put games you already made on the arcade\n arcade guide publishing arcade.json, media, the dry run"
22507
+ );
22508
+ program.command("skills").description("manage the bundled agent skills").command("install").description("copy the arcade skills into ~/.claude/skills and/or ~/.agents/skills").addOption(
22252
22509
  new Option("--target <target>", "where to install").choices(["claude", "codex", "all"]).default("all")
22253
22510
  ).option("--dir <path>", "install into this folder instead").action(installSkills);
22254
- program.command("login").description("sign in through your browser (device code)").action(login);
22511
+ program.command("login").description("open the sign-in page in your browser and wait for your approval (device code)").option("--no-browser", "only print the link (also: ARCADE_NO_BROWSER=1)").action(login);
22255
22512
  program.command("logout").description("sign out and revoke this machine's token").action(logout);
22256
22513
  program.command("whoami").description("show who you're signed in as").option("--json", "print JSON").action(whoami);
22257
22514
  program.helpCommand("help [command]", "show help for a command");
@@ -22283,7 +22540,11 @@ function buildProgram() {
22283
22540
  "after",
22284
22541
  "\nExamples:\n arcade publish --dry-run check it and see the file list\n arcade publish --yes publish after you've read the dry run\n arcade publish --yes --no-stats when these tokens aren't from this build"
22285
22542
  );
22286
- program.command("media").description("media helpers").command("preview [dir]").description("make media/preview.mp4 (12 s) from your demo with ffmpeg").action(mediaPreview);
22543
+ const media = program.command("media").description("media helpers (they need ffmpeg)");
22544
+ media.command("preview [dir]").description(
22545
+ "remake media/preview.mp4 (12 s) and media/preview.gif (6 s) from your demo, after every new demo"
22546
+ ).action(mediaPreview);
22547
+ media.command("gif [dir]").description("remake only media/preview.gif from your demo").action(mediaGif);
22287
22548
  program.command("main <slug> <generation>").description("(owner) pick which generation is main for its version").action(setMain);
22288
22549
  program.command("unpublish <slug>").description("(owner) take your game down").action((slug) => setPublished(slug, false));
22289
22550
  program.command("republish <slug>").description("(owner) bring your game back").action((slug) => setPublished(slug, true));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolutionary-arcade",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "The arcade CLI for Evolutionary Arcade. Publish, update, regen, fork, and blend AI-made browser games with your own coding agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: arcade-getting-started
3
- description: Start here for any Evolutionary Arcade (evolutionaryarcade.com) task. Use when you are asked to make, update, regenerate, remix, fork, blend, or publish a browser game for Evolutionary Arcade, when the `arcade` CLI or the `evolutionary-arcade` npm package comes up, or when you find an arcade.json in the working folder. Covers the five kinds of build (original, update, regen, fork, blend), install and login, the path to a first published game, the rules every upload must meet, and which sibling skill to read next.
3
+ description: Start here for any Evolutionary Arcade (evolutionaryarcade.com) task. Use when you are asked to make, update, regenerate, remix, fork, blend, or publish a browser game for Evolutionary Arcade, when the `arcade` CLI or the `evolutionary-arcade` npm package comes up, or when you find an arcade.json in the working folder. Routes a creator who wants to put games they already made on the arcade to `arcade-onboarding`. Covers the five kinds of build (original, update, regen, fork, blend), install and login, the path to a first published game, the rules every upload must meet, and which sibling skill to read next.
4
4
  ---
5
5
 
6
6
  # Evolutionary Arcade: getting started
@@ -9,6 +9,8 @@ Evolutionary Arcade is an arcade of open-source browser games made by AI agents.
9
9
 
10
10
  What good looks like: a game that plays well in an iframe on the site, has honest metadata, and was published the way the user meant. The *kind* of build matters as much as the code, because it decides where the game lands and which game it's linked to.
11
11
 
12
+ **Putting games you already made on the arcade?** That's onboarding: run `arcade guide onboarding` and follow it. It covers signing in first, finding the games, bringing each one up to the arcade's standard, the demo, and one dry run for the whole batch.
13
+
12
14
  ## Versions, generations, and the main one
13
15
 
14
16
  A game has numbered versions (v1, v2, ...). Each version holds a stack of generations, which are alternative builds of that version, and one generation per version is the main one, the build players get (the site marks it MAIN). An update adds a version. A regen adds a generation to an existing version's stack. The owner picks the main one with `arcade main <slug> <generation>`. `arcade info <slug>` lists a game's versions, every generation id, and which one is main.
@@ -37,15 +39,16 @@ Use your judgment on the edges:
37
39
 
38
40
  ```bash
39
41
  npm i -g evolutionary-arcade
40
- arcade login # prints a URL and a code, then waits for the human to approve in a browser
42
+ arcade login # opens the sign-in page, prints the URL and a code, waits for the human to approve
41
43
  arcade whoami
42
44
  ```
43
45
 
44
- Only a human can approve the login. Run `arcade login` in the background, give the user the URL and code, and keep working.
46
+ Only a human can approve the login. Run `arcade login` in the background: it opens the sign-in page in their browser (`--no-browser` only prints it). Give the user the code, and keep working. When they approve, it prints their handle and profile address and exits.
45
47
 
46
48
  - `ARCADE_TOKEN` overrides the saved login, for CI. The only way to get a token is `arcade login`, which saves it in `~/.config/evolutionary-arcade/credentials.json` (or under `$XDG_CONFIG_HOME`), keyed by API URL. CLI tokens expire after 90 days.
47
49
  - `ARCADE_API_URL` points the CLI at a preview or local server. It defaults to `https://evolutionaryarcade.com`.
48
- - `arcade skills install [--target claude|codex|all]` copies the four arcade skills to where your harness looks for skills. Restart the agent afterwards so it picks them up.
50
+ - `arcade guide <name>` prints any arcade skill into the current chat, so you never need a restart: `onboarding`, `building-games`, `remix-and-blend`, `publishing` (`arcade guide --list`).
51
+ - `arcade skills install [--target claude|codex|all]` also copies the skills to where your harness looks for them, for future sessions. Don't ask the user to restart you; read them with `arcade guide <name>` in this one.
49
52
  - `arcade --help` and `arcade <command> --help` are the command reference. Check them rather than guessing a flag.
50
53
 
51
54
  ## Your first game
@@ -69,7 +72,7 @@ arcade publish --yes # only after the user says go
69
72
  - **Publish on the user's go, with `--yes`.** Publishing puts the game on the public site under the user's name. Run `arcade publish --dry-run` and show them the file list and the Model card. Their go in chat is the confirmation, so then run `arcade publish --yes`. Without `--yes`, the CLI asks a y/N question your shell can't answer, and it stops without publishing. If you change the folder after they've seen the dry run, show them a new one.
70
73
  - **The game must be static and self-contained.** Use relative URLs, and make no requests to other origins (CDNs, APIs, web fonts, analytics). Loading your own files by relative URL is fine. `arcade-building-games` covers the details.
71
74
  - **Only web asset and source types upload.** That means html, js, css, json, md, images, audio, video, fonts, glb, wasm, plain-text source and config (ts, yml, toml, csv, and dotfiles like `.gitignore`), and a few more (`arcade-building-games` has the full list). The CLI exits 2 on anything else, such as `yarn.lock`, a `.zip`, `.fbx`, `.blend`, or `.psd`. Move those out of the game folder, and convert models to .glb. It skips `.git`, `node_modules`, and editor folders on its own.
72
- - **Every build needs its own media.** In arcade.json, `thumbnail` and 1 to 12 `screenshots` are required. Each one is a .png, .jpg, or .webp under 5 MB, given as a relative path inside the folder and captured from real play of your build. A regen downloads no media, and a fork or blend arrives with the parent's media, so capture new images every time. `arcade-publishing` covers capture, the demo video, and the hover preview.
75
+ - **Every build needs its own media.** In arcade.json, `thumbnail` and 1 to 12 `screenshots` are required, and a game with a `demo_video` also needs its `preview_gif` (`arcade publish` makes it with ffmpeg). Each one is a .png, .jpg, or .webp under 5 MB, given as a relative path inside the folder and captured from real play of your build. A regen downloads no media, and a fork or blend arrives with the parent's media, so capture new images every time. `arcade-publishing` covers capture, the demo video, and the hover preview.
73
76
  - **Provenance must be true:** the models, harness, and process you actually used. Prompts are optional. Share one only if the creator wants it public. When you leave token counts blank, `arcade publish` fills them from local Claude Code session logs for this folder and labels the harness Claude Code. Check those numbers in the dry run. If they aren't from this build, for example because you used another harness, pass `--no-stats`.
74
77
  - **Exit code 2 means something to fix,** and each problem is listed. Fix every problem and run the command again. Don't delete fields to make the errors go away.
75
78
 
@@ -77,6 +80,7 @@ arcade publish --yes # only after the user says go
77
80
 
78
81
  - `arcade-building-games`: making the game itself. Covers the iframe and CSP, input and pointer lock, audio, performance, file types, and playtesting. Read it before you write code, for every kind of build.
79
82
  - `arcade-remix-and-blend`: update, fork, blend, and regen. Covers choosing between them, reading parent code, `BLEND.md`, reusing a prompt faithfully, and keeping lineage intact.
83
+ - `arcade-onboarding`: putting a creator's existing games on the arcade, from sign-in to their profile link.
80
84
  - `arcade-publishing`: arcade.json fields, the Model card, capturing media, reading the dry run, picking the main generation, and unpublishing.
81
85
 
82
86
  ## Examples
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: arcade-onboarding
3
+ description: Use when a creator wants to put games they already made on Evolutionary Arcade (evolutionaryarcade.com), usually right after they pasted the arcade's prompt into their agent ("help me put my games on Evolutionary Arcade", `arcade guide onboarding`). Walks the first session in Alex's order - vision (what they want, asked all up front), pre-flight (sign-in, tools, the ownership and MIT check) while they're still at the keyboard, then "let me cook": find the games, a fit table with your own time estimates, as-is or optimize, demos and GIFs, one combined dry run and one go, then a ready line with their profile link. Also covers GitHub repos as a source, failures, and the 30-a-day cap.
4
+ ---
5
+
6
+ # Onboarding a creator onto Evolutionary Arcade
7
+
8
+ ## The situation
9
+
10
+ A creator pasted the arcade's prompt into you. They make browser games with AI, often for a YouTube channel, and they want those games on the arcade with a profile page they can link from their videos. They probably already have several games on their computer or on GitHub.
11
+
12
+ The arcade is opinionated on purpose. The `arcade` CLI does very little: it validates strictly and publishes. **You do the work**: write `arcade.json`, capture the thumbnail and screenshots from real play, write the controls, record the demo, and bring each game up to the arcade's standard. When the CLI refuses something, its error says exactly what to produce. Do that; don't argue with it and never delete a field to get past a check.
13
+
14
+ Creators get a high-quality showcase out of this even for games they later publish elsewhere, so do it properly.
15
+
16
+ What done looks like: the creator's chosen games are live, each with honest metadata, a real thumbnail and screenshots, a demo (and its GIF) if they accepted the offer, and the creator has a line for their video description with their profile link.
17
+
18
+ ## The shape: vision, pre-flight, then let me cook
19
+
20
+ Every question that needs the creator happens at the start, while they're interested and at the keyboard. Then you work on your own and come back with one thing to approve and a link.
21
+
22
+ 1. **Vision** (step 1): what they want out of the arcade, which games, and their defaults for optimizing and demos.
23
+ 2. **Pre-flight** (step 2): sign-in, the tools you'll need, and the ownership check. All the human-in-the-loop setup, done now.
24
+ 3. **Let me cook** (steps 3-6): say "I've got it from here. I'll come back with one summary to approve," then do the work.
25
+ 4. **One go, then the link** (steps 7-8): the only question while you cook is the publish go, because it makes their games public under their name.
26
+
27
+ ## Ground rules for the whole session
28
+
29
+ - **One question at a time, each with a default.** Creators are busy. "Want me to ...? (default: yes)" beats an open question.
30
+ - **You never sign in, and never ask for a token or password.** Only the creator approves the sign-in in their browser.
31
+ - **Nothing goes public without their go.** One go covers one batch, after they've seen one combined dry run. If anything changes after that, show it again.
32
+ - **Don't change their system silently.** Installing Node, ffmpeg, or Playwright is their call. Give the one-line command and ask.
33
+ - **Work on copies.** Copy each game to `~/arcade-games/<slug>/` (skip `node_modules`, `.git`, build caches, `.env*`) and work there. Their original folders stay untouched unless they ask you to work in place.
34
+ - **Read `arcade guide <name>` when a step needs it.** The other skills print the same way, with no restart: `building-games` (iframe, CSP, input, saves, leaderboards), `publishing` (arcade.json, media capture, the dry run), `remix-and-blend`.
35
+
36
+ ## Step 0. Install (the prompt already asked for it)
37
+
38
+ 1. `node -v`. It needs 20 or newer. If it's missing or old, tell the creator how to install it (nodejs.org, or `brew install node`) and wait.
39
+ 2. `npm i -g evolutionary-arcade`, then `arcade skills install`. Then `arcade guide onboarding` (this file). **Don't ask them to restart you.** The skills are installed for future sessions; in this one you read them with `arcade guide <name>`.
40
+ 3. `arcade --version` should say 0.2.0 or newer. If it's older, run the install again.
41
+
42
+ ## Step 1. Vision: what they want
43
+
44
+ Ask these together, in one message, each with a default, and wait for one reply:
45
+
46
+ > "Before I set anything up, four quick things:
47
+ > 1. What do you want out of the arcade? A showcase to link from your videos, a place to share with friends, or both? (default: a showcase for your channel)
48
+ > 2. Which games? Tell me where they are (folders or GitHub links), or I can survey your computer for games that look like a good fit. (default: survey)
49
+ > 3. Upload them as they are, or optimize them for the arcade first: touch controls (most YouTube viewers are on phones), gamepad, saved progress, leaderboards? (default: as-is now, optimize later)
50
+ > 4. Want me to play each game and record a short demo? It doesn't take long, and you get a preview and a GIF for sharing. I highly recommend it. (default: yes)"
51
+
52
+ Their answers are the plan for the rest of the session. Don't ask them again later; mention any change you make to the plan in the final summary.
53
+
54
+ ## Step 2. Pre-flight: sign-in, tools, ownership
55
+
56
+ Do all the setup that needs them now, before you start cooking.
57
+
58
+
59
+ **Sign-in.** Publishing needs it and the browser step is the only part only they can do.
60
+
61
+ 1. `arcade whoami`. If it prints a handle, say "You're signed in as @handle" and continue.
62
+ 2. Otherwise run `arcade login` **in the background**. It opens the sign-in page in their browser and prints the link and a code. Tell them:
63
+ > "I opened the arcade's sign-in page. Sign in (Google, GitHub, Discord, or X), confirm the code `XXXX-XXXX`, and come back here. You'll pick a handle: it's permanent and becomes your profile address, so your channel name is a good choice."
64
+ 3. If the browser didn't open (a remote machine, `--no-browser`), give them the printed link.
65
+ 4. Keep going with the rest of pre-flight while they sign in. When `arcade login` finishes it prints "Signed in as @handle" and their profile address. Check `arcade whoami` before the dry run. If it's still waiting after ~10 minutes, remind them once; everything up to the dry run can be prepared without it.
66
+
67
+ **Tools.** If they want demos, check for `ffmpeg` and Playwright now. If one is missing, give the one-line install and ask. Don't install silently. If they asked for a survey, confirm the search scope now (default: home folder, about 4 levels deep).
68
+
69
+
70
+ **Ownership.** Say this plainly, once, now:
71
+
72
+ > "Only publish games you made. Everything on the arcade is open source under MIT, so anyone can play, read, and remix it. Don't upload a game you didn't make unless its license is MIT. Bundled assets (art, music, fonts, libraries) keep their own licenses and must be yours to share."
73
+
74
+ While you cook, check each game yourself: a non-MIT `LICENSE` at the top of the folder, assets from a store or another game, a copied clone of a commercial game. If something is unclear, ask. Don't publish a game until the creator confirms it's theirs.
75
+
76
+ When pre-flight is done, say: **"I've got it from here. I'll come back with one summary to approve."** Then cook.
77
+
78
+ ## Step 3. Let me cook: find the games
79
+
80
+ Use their step-1 answer: the folders or links they named, or a survey within the scope they confirmed.
81
+
82
+ **Survey (if they say so):** search only where they agree. Default to their home folder, about 4 levels deep, skipping `Library`, `node_modules`, `.git`, `dist`/`build` copies, caches, and cloud-sync folders. Look for `index.html` beside game signs: a `<canvas>`, `requestAnimationFrame`, Phaser, Three.js, PixiJS, Kaboom, Babylon, a `game` in the name. Group hits by project folder. Don't open files that look private (`.env`, keys, documents).
83
+
84
+ **GitHub links are a source only.** Clone the repo (`git clone --depth 1`) into `~/arcade-games/src/`, then treat it like a local folder: bring it up to the standard and publish with the CLI. There is no direct GitHub publish. If a clone needs credentials, ask them to clone it themselves; never ask for a token.
85
+
86
+ ## Step 4. The fit table
87
+
88
+ For each candidate, decide from facts, then build one numbered table. It goes in the final summary; the picks are every **ready** and **small fixes** game unless they named specific ones in step 1.
89
+
90
+ **A good fit for the arcade:**
91
+ - It's a game: input, a loop, and something to do within the first minute. Not a tech demo, a tool, or an unfinished prototype (unless they insist).
92
+ - It runs in a browser from static files, or a build step outputs static files.
93
+ - It needs no server at play time: no multiplayer backend, no API calls, no paid AI API, no login. The arcade's CSP blocks every outside request. CDN imports and web fonts are fine to fix by vendoring them into the folder.
94
+ - It fits: under 50 MB and 800 files, each file under 25 MB, only web file types (`arcade guide building-games` has the list).
95
+ - It's theirs to publish under MIT (step 2).
96
+
97
+ Verdicts: **ready** (publishes as-is once it has metadata and media), **small fixes** (absolute paths, a CDN import to vendor, a build step, a stray big file), **not a fit** (needs a server, no web build, mostly someone else's work). Give the reason in a few words.
98
+
99
+ **Time estimates are yours.** Look at the code before you estimate, and say they're estimates. As a starting point: as-is with metadata and media takes you a few minutes per game; touch controls 10-20 min; gamepad 5-10; saves or a leaderboard 5-10 each; a demo 3-5. Adjust for what you see.
100
+
101
+ ```
102
+ # Game Where Fit What it needs As-is / optimized
103
+ 1 Neon Drift ~/games/neon-drift ready media, controls text ~4 min / ~25 min
104
+ 2 Tile Tower github.com/me/tile-tower small fixes vendor the Phaser CDN ~8 min / ~30 min
105
+ 3 Mech Arena ~/proj/mech not a fit needs its websocket server -
106
+ Publishing: 1, 2 (3 is not a fit)
107
+ ```
108
+
109
+ Up to 30 games publish per day per account (step 7). For a bigger list, publish the strongest first and say so in the summary.
110
+
111
+ ## Step 5. As-is or optimize: do the work
112
+
113
+ Follow their step-1 answer for the whole batch.
114
+
115
+ **As-is still meets the standard.** For every game, you:
116
+ - write `arcade.json` (`arcade guide publishing` has a full example): a slug you pick with them (permanent, it's the game's address), the title, a one-or-two-sentence description, exact `controls`, and honest `input` flags;
117
+ - write `LICENSE` (MIT, "Copyright (c) <year> @<handle>") unless they already have an MIT one;
118
+ - run it with `arcade dev`, fix anything that breaks under the arcade's CSP (outside requests, absolute paths);
119
+ - capture a 1280x720 thumbnail mid-action and 2-4 screenshots from real play (`arcade guide publishing`, "Capturing media");
120
+ - fill the Model card with what they tell you (the model and harness they used); leave out anything unknown. Never invent numbers.
121
+
122
+ **Optimize** adds, per game, only what makes sense for it, using the arcade's format (`arcade guide building-games` has each one):
123
+ - **Touch controls** and `"input": {"touch": true}`: recommend for any game without them. Phones get the game only when touch is on; otherwise they see the demo.
124
+ - **Gamepad** and `"input": {"gamepad": true}`.
125
+ - **Saves** with the `arcade-saves.js` helper and `"profile_saves": true`, for games with progress.
126
+ - **Leaderboards** with `arcade-scores.js` and `"leaderboards"`, for score or time games.
127
+ - Polish the first ten seconds: a clear "click to play", audio that starts on the first click, a pause on blur.
128
+
129
+ Play every game after you change it. Set an input flag only when that path works end to end.
130
+
131
+ ## Step 6. Demos
132
+
133
+ If they said yes in step 1 (the default), then for each game: play it with Playwright at 1280x720 against `arcade dev` (drive real inputs, or the game's autopilot if it has one), cut 15-30 s with the action in the first second, save it as `media/demo.mp4`, set `"demo_video"`, and run `arcade media preview`. That makes `media/preview.mp4` (the hover preview) and `media/preview.gif`, and sets both fields. **A game with a demo must have its GIF**; `arcade publish` makes it when it's missing and refuses without ffmpeg. Watch the clips and look at the GIF before moving on. Bot-driven footage is fine; say so in `provenance.notes`. Keep recordings outside the game folder, since everything in it is uploaded. Recording needs Playwright and ffmpeg; if they're missing, ask before installing.
134
+
135
+ ## Step 7. One dry run, one go, publish
136
+
137
+ 1. Run `arcade publish --dry-run --json` in each game folder. Signed out, it runs only the local checks and says to sign in; check `arcade whoami` and wait for the creator if needed.
138
+ 2. Exit code 2 means something to fix, and each line says what to produce. Fix it and rerun. If a game still fails after a real attempt, mark it "needs you" and move on.
139
+ 3. Show **one** combined summary: for each game, the slug, title, what it becomes (a new game), file count and size, demo yes/no, and every Heads up item. Mention anything you changed in their game.
140
+ 4. Ask for one go for the batch: "Publish these N games publicly under @handle, MIT? (yes / list changes)".
141
+ 5. On yes, `arcade publish --yes` in each folder, one at a time. If they changed something, change it and show that game's dry run again.
142
+ 6. **30 publishes a day** per account. Publish the ones they ranked highest first; tell them the rest will go tomorrow and leave those folders ready.
143
+
144
+ Failures: a slug that's taken (pick another with them), a game over 50 MB (move recordings and source art out, compress media), a flagged secret (remove it and tell them to rotate it; `--allow-secret` only for keys meant to be public, like a Firebase web key), a network error (rerun; uploads resume). Never retry the same failing thing more than twice.
145
+
146
+ ## Step 8. Hand back the link
147
+
148
+ 1. Ask for their channel URL and a one-line bio, then `arcade profile set --bio "<line>" --link "YouTube <url>"` (pass every link they want; it replaces the list).
149
+ 2. `arcade profile show` for the profile address.
150
+ 3. Give them a ready line for their video descriptions:
151
+ > `Play my games (and remix them): https://evolutionaryarcade.com/u/<handle>`
152
+ and each game's own link (`https://evolutionaryarcade.com/g/<slug>`).
153
+ 4. Tell them what happens next: people can play without an account, fork and blend their games, and those remixes show up in each game's family tree. To update a game later, change it in its `~/arcade-games/<slug>/` folder and publish again; that makes the next version.
154
+
155
+ ## Example
156
+
157
+ Creator: "help me put my games on Evolutionary Arcade." You install, then ask the four vision questions in one message. They want a showcase for their channel, the games are in `~/Desktop/jams`, as-is, and yes to demos. Pre-flight: you start `arcade login` in the background and they approve it in the browser, ffmpeg is already there, Playwright needs one install line and they say yes, and you give the ownership warning. They mention that one game used music from a paid pack. "I've got it from here." You find five games; one needs a websocket server. You copy the four fits, write arcade.json, LICENSE and controls, vendor one CDN import, drop the paid music, capture media, and record four demos (each with its GIF). You come back with one summary (the fit table, what you changed, the combined dry run) and one question: "Publish these 4 publicly under @handle, MIT?" They say yes, four publishes, and you hand them the description line with `/u/<handle>`.
@@ -34,7 +34,7 @@ Required: `schema`, `slug`, `title`, `description`, `thumbnail`, and at least on
34
34
  - `input` and `play`: see `arcade-building-games`. Set an input flag only when that path works end to end, because the site shows badges from them and phones get the demo when `touch` is false. `tilt` (default `false`) is for games you steer by tilting the phone; it needs `touch` too.
35
35
  - `profile_saves` (default `false`): the game saves progress to the player's profile with the `arcade-saves.js` helper. Set it only when the game uses the helper; `arcade-building-games` has the rules.
36
36
  - `leaderboards` (up to 4, default none): global leaderboards the game posts to with the `arcade-scores.js` helper. Each has an `id`, a `label`, and a `max`, and the dry run lists them with their limits. `arcade-building-games` has the rules.
37
- - `thumbnail` and `screenshots` (1 to 12) are .png, .jpg, or .webp. `demo_video` and `preview_video` are .mp4 or .webm. Every path is relative to the folder, with no `..`.
37
+ - `thumbnail` and `screenshots` (1 to 12) are .png, .jpg, or .webp. `demo_video` and `preview_video` are .mp4 or .webm. `preview_gif` is a .gif cut from the demo, required whenever there's a demo; `arcade media preview` (or `arcade publish`) makes it. Every path is relative to the folder, with no `..`.
38
38
  - `license` (optional): every game on the arcade is open source under MIT, so leave it out or set it to `"MIT"`. Anything else fails validation. The folder's `LICENSE` names the creator ("Copyright (c) <year> @handle"); if `arcade new` wrote it before you logged in, `arcade publish` fills in the handle. A license file at the top of the folder (`LICENSE`, `LICENSE.md`, `COPYING`, and the like) that isn't MIT stops the publish; other people's licenses go in `licenses/<name>/` or beside their code.
39
39
  - `lineage`: the CLI writes it. Don't edit it.
40
40
  - `provenance`: the Model card.
@@ -79,7 +79,7 @@ Everything in `provenance` is optional, and the site labels build stats "creator
79
79
  - `orchestrator`: `model` (required once you include `orchestrator`), `model_id`, `harness`, and `tokens`. `subagent_models`: one entry per model, with an optional `count` and `tokens`. Leave the list empty if the orchestrator did everything.
80
80
  - `tokens`, as integers: `input` is uncached input plus cache writes, `cached_input` is cache reads, and `output` is output. Once you include `tokens`, both `input` and `output` are required. If your harness counts cached tokens inside its input total, subtract them so nothing is counted twice.
81
81
  - `build`: `cost_usd`, `wall_time_minutes`, and `cost_basis` (`api-equivalent` for what the tokens would cost at API prices, or `billed` for what you paid).
82
- - Extra fields you add, such as `engine` or `tools`, are kept verbatim. Keep the whole block under 256 KB.
82
+ - Extra fields you add, such as `engine` or `tools`, are kept verbatim, except `github`: the arcade sets that itself when it publishes from a GitHub repo, and drops it from uploads. Keep the whole block under 256 KB.
83
83
 
84
84
  Hard rules:
85
85
  - **Never invent a number.** Leave out any key you don't know. Don't write `null`, an empty string, or `0`. `null` fails validation. An empty prompt or notes counts as not shared, so leave the key out. `0` publishes a made-up number.
@@ -116,7 +116,7 @@ ffmpeg -ss 4 -i ../rec/<file>.webm -t 24 -an -vf scale=1280:720 \
116
116
  - **Thumbnail:** pick the best candidate and copy it to `media/`. It must be a real in-game frame at 16:9 (the card crops anything else). No title card and no added text.
117
117
  - **Screenshots:** two to four different moments, such as the core action, a quiet good-looking moment, the HUD under pressure, and the end screen.
118
118
  - **Demo:** 15 to 30 seconds of real gameplay, at 1280x720, in H.264 mp4 or webm. No audio is needed. Use `-ss` to skip the title screen, so action starts in the first second or two.
119
- - **Preview: remake it every time you make a new demo.** Run `arcade media preview`. It cuts the first 12 s of `demo_video`, overwrites `media/preview.mp4`, and sets `preview_video`. `arcade publish` makes a preview only when `preview_video` is empty, and a folder from `arcade pull`, `fork`, or `regen` arrives with the parent's `preview_video` already set (a regen has the path but not the file, so publish exits 2 until you make one). After your first publish it stays set too. The preview plays muted, loops, and restarts on every hover, so those 12 seconds are the pitch. Watch it. If it misses the best moment, re-cut the demo with a later `-ss` and run the command again.
119
+ - **Preview and GIF: remake them every time you make a new demo.** Run `arcade media preview`. It cuts the first 12 s of `demo_video` into `media/preview.mp4` and the first 6 s into a looping `media/preview.gif` (under 5 MB, for "someone remixed your game" emails and link previews), and sets `preview_video` and `preview_gif`. `arcade media gif` remakes only the GIF. A game with a demo can't publish without its GIF. `arcade publish` makes the preview only when `preview_video` is empty, and the GIF only when `preview_gif` is empty (the dry run warns when either is older than the demo), and a folder from `arcade pull`, `fork`, or `regen` arrives with the parent's `preview_video` already set (a regen has the path but not the file, so publish exits 2 until you make one). After your first publish it stays set too. The preview plays muted, loops, and restarts on every hover, so those 12 seconds are the pitch. Watch it. If it misses the best moment, re-cut the demo with a later `-ss` and run the command again.
120
120
  - **Budget:** the whole upload must stay under 50 MB and 800 files. The thumbnail and each screenshot must be at most 5 MB, and the demo, preview, and any other file at most 25 MB. A 24-second demo at CRF 23 is about 8 MB.
121
121
  - **Look at every file** before you publish. For video, a contact sheet is quick: `ffmpeg -i media/demo.mp4 -vf fps=1/3,scale=320:-1,tile=4x3 -frames:v 1 ../rec/sheet.png` (one image, covering 36 s). If headless WebGL renders black, run headed or pass GPU flags to Chromium (on macOS the seeds used `--use-angle=metal --ignore-gpu-blocklist`).
122
122
 
@@ -124,8 +124,9 @@ ffmpeg -ss 4 -i ../rec/<file>.webm -t 24 -an -vf scale=1280:720 \
124
124
 
125
125
  Run `arcade publish --dry-run` and read all of it. It uploads nothing, but know what it is:
126
126
 
127
- - It needs `arcade login` and a network connection, because the arcade checks the upload before anything is listed. Problems the arcade finds (a file over 25 MB, too many files, a slug that's taken, a stale base) exit before the file list prints.
128
- - It may make `media/preview.mp4` and write `preview_video` into arcade.json.
127
+ - Signed out, it runs only the local checks (arcade.json, file types, license, media, secrets), prints any Heads up, and exits 1 asking you to sign in. The full dry run needs `arcade login` and a network connection, because the arcade checks the upload before anything is listed. Problems the arcade finds (a file over 25 MB, too many files, a slug that's taken, a stale base) exit before the file list prints.
128
+ - Every problem it exits 2 on says what to produce: a missing thumbnail names the size and path to capture, a missing arcade.json lists the fields to write.
129
+ - It may make `media/preview.mp4` and `media/preview.gif` and write `preview_video` and `preview_gif` into arcade.json.
129
130
  - It lists the first 40 files, then "…and N more", but files inside hidden folders (like `.claude/`) are always listed. New files are tagged `new`. Review the whole tree yourself: `find . -type f -not -path './node_modules/*' -not -path './.git/*'`.
130
131
  - It follows symlinks only when they point inside the folder. Anything else is listed as skipped, and never uploaded.
131
132
  - It says "Published games are open source under MIT" under the action line. Anything bundled from others keeps its own license and must be the user's to share.
@@ -150,6 +151,10 @@ After an original, fork, blend, or update publishes, the CLI rewrites the folder
150
151
 
151
152
  **Generation ids, for `arcade main <slug> <generation>`:** `arcade info <slug>` lists every generation id and marks the main one. After a non-regen publish, the new id is also in `lineage.based_on.generation`. A regen doesn't become the main one on its own; its publish prints the exact `arcade main` command for the owner.
152
153
 
154
+ ## Games on GitHub
155
+
156
+ A GitHub repo is a source, not a way to publish. Clone it (`git clone --depth 1 <url>`) into a working folder, bring it up to the arcade's standard like any other game (arcade.json, LICENSE, media from real play, a demo and its GIF), and publish it with `arcade publish`. Only publish a repo the user made, or one under MIT. If the clone needs credentials, ask the user to clone it; never ask for a token.
157
+
153
158
  ## Taking a game down
154
159
 
155
160
  `arcade unpublish <slug>` takes your game down, and `arcade republish <slug>` brings it back. The slug stays yours and is never reused. Unpublishing doesn't un-leak anything: if a secret shipped, unpublish, then rotate the secret right away, because public source may already have been copied.