evolutionary-arcade 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Every publish ends with a **Share it** block: the game's link, a video-description line, an X post draft under 280 characters, an X intent link, and the GIF to attach. A fork or blend's draft credits each parent's creator, by X handle when their profile links one. The agent drafts; the creator posts. The CLI never posts anywhere.
6
+ - New `arcade share <slug>` reprints the block. `--profile` shares your whole library, `--badge` prints the "Play on Evolutionary Arcade" badge as Markdown and HTML (light and dark), and `--json` gives agents the whole thing. `arcade publish --json` includes it as `share`.
7
+ - The arcade serves the badge at `/badge/<slug>.svg` and `/badge/u/<handle>.svg` (`?theme=dark` for dark backgrounds), and each game has a `/g/<slug>/embed` page with the snippets.
8
+ - The `arcade-onboarding` and `arcade-fork` skills end with a share step: show the drafts, offer to tailor them to the creator's channel voice, remind them to put their profile link in video descriptions, and never post for them.
9
+
3
10
  ## 0.2.1
4
11
 
5
12
  - New `arcade-fork` skill, read with `arcade guide fork`: makes a viewer's own version of a game (a fork, or a blend with a second game), in the same order as onboarding: what they want to make, then setup (CLI, skills, sign-in, tools), then the work, one dry run and one go, and the link to the new game with its credit to the original creator. The game page's Fork and Blend buttons hand out a prompt that starts it.
package/README.md CHANGED
@@ -54,6 +54,7 @@ Everything you publish is public, including the source, and every game is open s
54
54
  | `arcade blend <slug> <slug> [...] [--into <dir>]` | Download 2 to 8 games to blend into one. |
55
55
  | `arcade regen <slug> [dir]` | Get a game's prompt, and no code, to build a fresh take. |
56
56
  | `arcade publish [dir] [--dry-run] [--yes]` | Check everything, list every file that goes public, and publish. |
57
+ | `arcade share <slug> [--profile] [--badge] [--json]` | Reprint the Share block publish ends with: the link, a video-description line, an X post draft, and the GIF to attach. `--profile` shares your whole library; `--badge` prints the "Play on Evolutionary Arcade" badge as Markdown and HTML. You post them; the arcade never posts for you. |
57
58
  | `arcade media preview [dir]` | Make the 12-second hover preview from your demo (needs ffmpeg). |
58
59
  | `arcade main <slug> <generation>` | (Owner) Pick the main generation for a version. |
59
60
  | `arcade unpublish <slug>` / `arcade republish <slug>` | (Owner) Take your game down, or bring it back. |
@@ -61,7 +62,7 @@ Everything you publish is public, including the source, and every game is open s
61
62
 
62
63
  When `arcade.json` has no token counts, `arcade publish` fills them in from your local Claude Code session logs, uploading only the numbers (`--no-stats` skips it). Subagent token counts aren't recorded reliably, so only the orchestrator's tokens are filled in. Subagents are listed by model and count.
63
64
 
64
- `arcade --help` and `arcade <command> --help` have every option. `--json` works on `whoami`, `info`, `search`, `publish`, and `profile show`.
65
+ `arcade --help` and `arcade <command> --help` have every option. `--json` works on `whoami`, `info`, `search`, `publish`, `share`, and `profile show`.
65
66
 
66
67
  ## Requirements
67
68
 
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.2.1",
16
+ version: "0.3.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",
@@ -5693,7 +5693,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5693
5693
  var NONE = 0;
5694
5694
  var ASSUMED = 1;
5695
5695
  var PROVEN = 2;
5696
- function isRecursive(inst, stack, resolve5) {
5696
+ function isRecursive(inst, stack, resolve6) {
5697
5697
  const cached2 = recursive.get(inst);
5698
5698
  if (cached2 !== void 0)
5699
5699
  return cached2 ? PROVEN : NONE;
@@ -5703,7 +5703,7 @@ function isRecursive(inst, stack, resolve5) {
5703
5703
  let result = NONE;
5704
5704
  const check2 = (child) => {
5705
5705
  if (result !== PROVEN && child?._zod) {
5706
- const answer = isRecursive(child, stack, resolve5);
5706
+ const answer = isRecursive(child, stack, resolve6);
5707
5707
  if (answer > result)
5708
5708
  result = answer;
5709
5709
  }
@@ -5714,7 +5714,7 @@ function isRecursive(inst, stack, resolve5) {
5714
5714
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5715
5715
  if (spread && !desc.enumerable)
5716
5716
  continue;
5717
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
5717
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
5718
5718
  if (child > answer)
5719
5719
  answer = child;
5720
5720
  }
@@ -5778,7 +5778,7 @@ function isRecursive(inst, stack, resolve5) {
5778
5778
  break;
5779
5779
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5780
5780
  case "lazy": {
5781
- const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
5781
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
5782
5782
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5783
5783
  break;
5784
5784
  }
@@ -21023,6 +21023,68 @@ function normalizeModel(raw) {
21023
21023
  return { name: raw.trim(), family, listed: false };
21024
21024
  }
21025
21025
 
21026
+ // ../format/src/shareText.ts
21027
+ var ARCADE_NAME = "Evolutionary Arcade";
21028
+ var gameUrl = (origin, slug) => `${origin}/g/${slug}`;
21029
+ var profileUrl = (origin, handle) => `${origin}/u/${handle}`;
21030
+ var creditName = (c) => c.x ? `@${c.x.replace(/^@/, "")}` : c.handle;
21031
+ var X_LIMIT = 280;
21032
+ var X_LINK = 23;
21033
+ function xLength(text) {
21034
+ return [...text.replace(/https?:\/\/\S+/g, "x".repeat(X_LINK))].length;
21035
+ }
21036
+ var xIntentUrl = (text) => `https://x.com/intent/post?text=${encodeURIComponent(text)}`;
21037
+ var listOf = (items) => items.length <= 2 ? items.join(" and ") : `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
21038
+ function fit(build) {
21039
+ for (let max = 80; max >= 8; max -= 4) {
21040
+ const text = build((t) => t.length > max ? `${t.slice(0, max - 1).trimEnd()}\u2026` : t);
21041
+ if (xLength(text) <= X_LIMIT) return text;
21042
+ }
21043
+ return build((t) => t.slice(0, 8));
21044
+ }
21045
+ function descriptionLine(game) {
21046
+ return `Play ${game.title} in your browser (and remix it): ${gameUrl(game.origin, game.slug)}`;
21047
+ }
21048
+ function publishShare(game) {
21049
+ const url2 = gameUrl(game.origin, game.slug);
21050
+ if (game.version && game.version > 1)
21051
+ return fit(
21052
+ (cut) => `${cut(game.title)} v${game.version} is live on ${ARCADE_NAME}. Play it in your browser, then remix it: ${url2}`
21053
+ );
21054
+ return fit(
21055
+ (cut) => `I made ${cut(game.title)}, a browser game you can play right now. It's open source, so you can remix it too: ${url2}`
21056
+ );
21057
+ }
21058
+ function remixShare(game) {
21059
+ const url2 = gameUrl(game.origin, game.slug);
21060
+ const parents = game.parents ?? [];
21061
+ if (!parents.length) return publishShare(game);
21062
+ return fit((cut) => {
21063
+ const credits = parents.map((p) => `${cut(p.title)} by ${creditName(p)}`);
21064
+ return game.kind === "blend" ? `I blended ${listOf(credits)} into ${cut(game.title)}. Play it in your browser: ${url2}` : `I remixed ${credits[0]} into ${cut(game.title)}. Play it in your browser: ${url2}`;
21065
+ });
21066
+ }
21067
+ function libraryShare(opts) {
21068
+ const n = opts.games === 1 ? "a game" : `${opts.games} games`;
21069
+ return `I put ${n} on ${ARCADE_NAME}. Play in your browser, no account needed, and remix anything: ${profileUrl(opts.origin, opts.handle)}`;
21070
+ }
21071
+ var libraryDescriptionLine = (origin, handle) => `Play my games (and remix them): ${profileUrl(origin, handle)}`;
21072
+ var shareDraft = (game) => game.kind === "fork" || game.kind === "blend" ? remixShare(game) : publishShare(game);
21073
+ function badgeUrls(t, theme = "light") {
21074
+ const path = t.slug ? `/badge/${t.slug}.svg` : `/badge/u/${t.handle}.svg`;
21075
+ const link = t.slug ? gameUrl(t.origin, t.slug) : profileUrl(t.origin, t.handle ?? "");
21076
+ return { image: `${t.origin}${path}${theme === "dark" ? "?theme=dark" : ""}`, link };
21077
+ }
21078
+ var BADGE_ALT = `Play on ${ARCADE_NAME}`;
21079
+ function badgeMarkdown(t, theme = "light") {
21080
+ const { image, link } = badgeUrls(t, theme);
21081
+ return `[![${BADGE_ALT}](${image})](${link})`;
21082
+ }
21083
+ function badgeHtml(t, theme = "light") {
21084
+ const { image, link } = badgeUrls(t, theme);
21085
+ return `<a href="${link}"><img src="${image}" alt="${BADGE_ALT}" height="36"></a>`;
21086
+ }
21087
+
21026
21088
  // src/commands/games.ts
21027
21089
  var STARTER_DESCRIPTION = "What a player reads on the card. One or two sentences.";
21028
21090
  var STARTER_CONTROLS = "WASD move, mouse aim, click to act";
@@ -21438,8 +21500,8 @@ function dev(dirArg, opts) {
21438
21500
 
21439
21501
  // src/commands/publish.ts
21440
21502
  import { execFileSync } from "child_process";
21441
- import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
21442
- import { isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve3, sep as sep4 } from "path";
21503
+ import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
21504
+ import { isAbsolute as isAbsolute2, join as join6, relative as relative2, resolve as resolve4, sep as sep4 } from "path";
21443
21505
 
21444
21506
  // src/files.ts
21445
21507
  import { createHash as createHash2 } from "crypto";
@@ -21731,10 +21793,203 @@ function missingSessions(named) {
21731
21793
  return named.filter((id) => !found.has(id));
21732
21794
  }
21733
21795
 
21796
+ // src/commands/share.ts
21797
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
21798
+ import { homedir as homedir3 } from "os";
21799
+ import { join as join5, resolve as resolve3 } from "path";
21800
+ var X_HOSTS = /* @__PURE__ */ new Set([
21801
+ "x.com",
21802
+ "www.x.com",
21803
+ "twitter.com",
21804
+ "www.twitter.com",
21805
+ "mobile.twitter.com"
21806
+ ]);
21807
+ var X_RESERVED = /* @__PURE__ */ new Set([
21808
+ "intent",
21809
+ "home",
21810
+ "i",
21811
+ "share",
21812
+ "search",
21813
+ "explore",
21814
+ "hashtag",
21815
+ "settings"
21816
+ ]);
21817
+ function xHandleFromLinks(links) {
21818
+ for (const l of links) {
21819
+ try {
21820
+ const u = new URL(l.url);
21821
+ const name = u.pathname.split("/").filter(Boolean)[0];
21822
+ if (X_HOSTS.has(u.hostname) && name && !X_RESERVED.has(name.toLowerCase()) && /^[A-Za-z0-9_]{1,15}$/.test(name))
21823
+ return name;
21824
+ } catch {
21825
+ }
21826
+ }
21827
+ return null;
21828
+ }
21829
+ async function xHandle(handle) {
21830
+ try {
21831
+ const p = await request(`/api/users/${encodeURIComponent(handle)}`, {
21832
+ auth: false
21833
+ });
21834
+ return xHandleFromLinks(p.user.links ?? []);
21835
+ } catch {
21836
+ return null;
21837
+ }
21838
+ }
21839
+ var badges = (t) => ({
21840
+ markdown: badgeMarkdown(t),
21841
+ html: badgeHtml(t),
21842
+ markdownDark: badgeMarkdown(t, "dark"),
21843
+ htmlDark: badgeHtml(t, "dark")
21844
+ });
21845
+ function findGif(slug, dir, hosted) {
21846
+ const folders = [dir, process.cwd(), join5(homedir3(), "arcade-games", slug)].filter(
21847
+ (d) => Boolean(d)
21848
+ );
21849
+ for (const folder of folders) {
21850
+ try {
21851
+ const raw = JSON.parse(readFileSync7(join5(folder, "arcade.json"), "utf8"));
21852
+ if (raw.slug !== slug || typeof raw.preview_gif !== "string") continue;
21853
+ const path = resolve3(folder, raw.preview_gif);
21854
+ if (existsSync5(path)) return path;
21855
+ } catch {
21856
+ }
21857
+ }
21858
+ return hosted ? new URL(hosted, apiUrl()).href : null;
21859
+ }
21860
+ async function gameShare(slug, opts = {}) {
21861
+ const d = await fetchGame(slug);
21862
+ if (!d || d.removed || !d.game.title) throw noSuchGame(slug);
21863
+ const origin = apiUrl();
21864
+ const handles = [...new Set((d.parents ?? []).map((p) => p.owner))];
21865
+ const xs = new Map(await Promise.all(handles.map(async (h) => [h, await xHandle(h)])));
21866
+ const parents = [];
21867
+ const seen = /* @__PURE__ */ new Set();
21868
+ for (const p of d.parents ?? []) {
21869
+ if (seen.has(p.slug)) continue;
21870
+ seen.add(p.slug);
21871
+ parents.push({ title: p.title, handle: p.owner, x: xs.get(p.owner) ?? null });
21872
+ }
21873
+ const kind = d.game.kind ?? (parents.length > 1 ? "blend" : parents.length ? "fork" : "original");
21874
+ const game = {
21875
+ origin,
21876
+ slug: d.game.slug,
21877
+ title: d.game.title,
21878
+ kind,
21879
+ parents,
21880
+ version: opts.version ?? d.generation?.version
21881
+ };
21882
+ const post = shareDraft(game);
21883
+ const hostedGif = d.game.gifUrl ?? null;
21884
+ return {
21885
+ kind: "game",
21886
+ slug: d.game.slug,
21887
+ title: d.game.title,
21888
+ url: gameUrl(origin, d.game.slug),
21889
+ description: descriptionLine(game),
21890
+ post,
21891
+ postLength: xLength(post),
21892
+ xIntent: xIntentUrl(post),
21893
+ gif: findGif(d.game.slug, opts.dir, hostedGif),
21894
+ badge: badges({ origin, slug: d.game.slug })
21895
+ };
21896
+ }
21897
+ async function signedInHandle() {
21898
+ const me = await request("/api/me");
21899
+ if (!me.user?.handle) throw new ExitError("You're not signed in. Run `arcade login`.");
21900
+ return me.user.handle;
21901
+ }
21902
+ async function profileShare(handleArg) {
21903
+ const handle = (handleArg ?? await signedInHandle()).replace(/^@/, "");
21904
+ const origin = apiUrl();
21905
+ let profile;
21906
+ try {
21907
+ profile = await request(`/api/users/${encodeURIComponent(handle)}`, {
21908
+ auth: false
21909
+ });
21910
+ } catch (e) {
21911
+ if (e instanceof ApiError && e.status === 404)
21912
+ throw new ExitError(`No creator called @${handle} on the arcade.`);
21913
+ throw e;
21914
+ }
21915
+ const list = await request(
21916
+ `/api/games?owner=${encodeURIComponent(handle)}&sort=new&limit=60`,
21917
+ { auth: false }
21918
+ );
21919
+ const post = libraryShare({ origin, handle, games: profile.stats.games });
21920
+ return {
21921
+ kind: "profile",
21922
+ handle,
21923
+ url: profileUrl(origin, handle),
21924
+ description: libraryDescriptionLine(origin, handle),
21925
+ post,
21926
+ postLength: xLength(post),
21927
+ xIntent: xIntentUrl(post),
21928
+ games: list.games.map((g) => ({ slug: g.slug, title: g.title, url: gameUrl(origin, g.slug) })),
21929
+ badge: badges({ origin, handle })
21930
+ };
21931
+ }
21932
+ function printBadge(b) {
21933
+ out(bold("Badge for a README, itch page, or website:"));
21934
+ out(` Markdown (light): ${b.markdown}`);
21935
+ out(` Markdown (dark): ${b.markdownDark}`);
21936
+ out(` HTML (light): ${b.html}`);
21937
+ out(` HTML (dark): ${b.htmlDark}`);
21938
+ }
21939
+ function printShare(s, opts = {}) {
21940
+ out("");
21941
+ out(pink(bold("Share it")) + dim(" (drafts for you to post; the arcade never posts for you)"));
21942
+ out(` Link: ${cyan(s.url)}`);
21943
+ out(` Video description: ${s.description}`);
21944
+ out(` X post (${s.postLength}/280):`);
21945
+ out(` ${s.post}`);
21946
+ out(` Post it on X: ${s.xIntent}`);
21947
+ if (s.kind === "game")
21948
+ out(
21949
+ ` Attach this GIF: ${s.gif ?? "none yet (run arcade media preview in the game folder, then publish)"}`
21950
+ );
21951
+ else for (const g of s.games) out(` ${g.title} ${dim(g.url)}`);
21952
+ if (opts.badge) {
21953
+ out("");
21954
+ printBadge(s.badge);
21955
+ } else
21956
+ out(
21957
+ dim(
21958
+ ` Badge for your README or itch page: arcade share ${s.kind === "game" ? s.slug : "--profile"} --badge`
21959
+ )
21960
+ );
21961
+ }
21962
+ async function share(slug, opts) {
21963
+ if (!slug && !opts.profile)
21964
+ throw new ExitError(
21965
+ "Name a game (arcade share <slug>), or use --profile for your whole library.",
21966
+ 2
21967
+ );
21968
+ const s = opts.profile ? await profileShare(typeof opts.profile === "string" ? opts.profile : void 0) : await gameShare(slug);
21969
+ if (opts.json) {
21970
+ printJson(s);
21971
+ return;
21972
+ }
21973
+ if (opts.badge && !opts.json) {
21974
+ out(`${bold(s.kind === "game" ? s.title : `@${s.handle}`)} ${cyan(s.url)}`);
21975
+ out("");
21976
+ printBadge(s.badge);
21977
+ return;
21978
+ }
21979
+ printShare(s);
21980
+ }
21981
+ async function shareAfterPublish(slug, dir, version2) {
21982
+ try {
21983
+ printShare(await gameShare(slug, { dir, version: version2 }));
21984
+ } catch {
21985
+ out(dim(`Share it later: arcade share ${slug}`));
21986
+ }
21987
+ }
21988
+
21734
21989
  // src/commands/publish.ts
21735
21990
  function readArcadeRaw(dir) {
21736
- const path = join5(dir, "arcade.json");
21737
- if (!existsSync5(path))
21991
+ const path = join6(dir, "arcade.json");
21992
+ if (!existsSync6(path))
21738
21993
  throw new ExitError(
21739
21994
  `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):`,
21740
21995
  2,
@@ -21742,7 +21997,7 @@ function readArcadeRaw(dir) {
21742
21997
  "Starting a brand-new game instead? arcade new <slug>"
21743
21998
  );
21744
21999
  try {
21745
- return JSON.parse(readFileSync7(path, "utf8"));
22000
+ return JSON.parse(readFileSync8(path, "utf8"));
21746
22001
  } catch (e) {
21747
22002
  throw new ExitError(`arcade.json isn't valid JSON: ${e.message}`, 2);
21748
22003
  }
@@ -21770,7 +22025,7 @@ function makePreview(dir, demo, target = "media/preview.mp4") {
21770
22025
  runFfmpeg(
21771
22026
  [
21772
22027
  "-i",
21773
- join5(dir, demo),
22028
+ join6(dir, demo),
21774
22029
  "-t",
21775
22030
  "12",
21776
22031
  "-an",
@@ -21786,7 +22041,7 @@ function makePreview(dir, demo, target = "media/preview.mp4") {
21786
22041
  "yuv420p",
21787
22042
  "-movflags",
21788
22043
  "+faststart",
21789
- join5(dir, target)
22044
+ join6(dir, target)
21790
22045
  ],
21791
22046
  "preview"
21792
22047
  );
@@ -21803,7 +22058,7 @@ function makeGif(dir, demo, target = "media/preview.gif") {
21803
22058
  runFfmpeg(
21804
22059
  [
21805
22060
  "-i",
21806
- join5(dir, demo),
22061
+ join6(dir, demo),
21807
22062
  "-t",
21808
22063
  String(t.seconds),
21809
22064
  "-an",
@@ -21811,11 +22066,11 @@ function makeGif(dir, demo, target = "media/preview.gif") {
21811
22066
  `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`,
21812
22067
  "-loop",
21813
22068
  "0",
21814
- join5(dir, target)
22069
+ join6(dir, target)
21815
22070
  ],
21816
22071
  "GIF"
21817
22072
  );
21818
- if (statSync5(join5(dir, target)).size <= MAX_GIF_BYTES) return target;
22073
+ if (statSync5(join6(dir, target)).size <= MAX_GIF_BYTES) return target;
21819
22074
  }
21820
22075
  throw new ExitError(
21821
22076
  `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.`
@@ -21828,7 +22083,7 @@ function demoFor(dir, raw, what) {
21828
22083
  `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").`,
21829
22084
  2
21830
22085
  );
21831
- if (!existsSync5(join5(dir, raw.demo_video)))
22086
+ if (!existsSync6(join6(dir, raw.demo_video)))
21832
22087
  throw new ExitError(
21833
22088
  `${raw.demo_video} isn't in the folder yet. Record a demo first (see arcade-publishing).`,
21834
22089
  2
@@ -21836,7 +22091,7 @@ function demoFor(dir, raw, what) {
21836
22091
  return raw.demo_video;
21837
22092
  }
21838
22093
  async function mediaPreview(dirArg) {
21839
- const dir = resolve3(dirArg ?? ".");
22094
+ const dir = resolve4(dirArg ?? ".");
21840
22095
  const raw = readArcadeRaw(dir);
21841
22096
  const demo = demoFor(dir, raw, "a preview");
21842
22097
  const made = makePreview(dir, demo);
@@ -21845,21 +22100,21 @@ async function mediaPreview(dirArg) {
21845
22100
  if (!gif) throw new ExitError(NO_FFMPEG);
21846
22101
  raw.preview_video = made;
21847
22102
  raw.preview_gif = gif;
21848
- writeFileSync3(join5(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22103
+ writeFileSync3(join6(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
21849
22104
  `);
21850
22105
  out(
21851
- `Made ${made} and ${gif} (${mb(statSync5(join5(dir, gif)).size)}) and added them to arcade.json.`
22106
+ `Made ${made} and ${gif} (${mb(statSync5(join6(dir, gif)).size)}) and added them to arcade.json.`
21852
22107
  );
21853
22108
  }
21854
22109
  async function mediaGif(dirArg) {
21855
- const dir = resolve3(dirArg ?? ".");
22110
+ const dir = resolve4(dirArg ?? ".");
21856
22111
  const raw = readArcadeRaw(dir);
21857
22112
  const gif = makeGif(dir, demoFor(dir, raw, "a GIF"));
21858
22113
  if (!gif) throw new ExitError(NO_FFMPEG);
21859
22114
  raw.preview_gif = gif;
21860
- writeFileSync3(join5(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22115
+ writeFileSync3(join6(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
21861
22116
  `);
21862
- out(`Made ${gif} (${mb(statSync5(join5(dir, gif)).size)}) and added it to arcade.json.`);
22117
+ out(`Made ${gif} (${mb(statSync5(join6(dir, gif)).size)}) and added it to arcade.json.`);
21863
22118
  }
21864
22119
  function describeAction(p, parentNames) {
21865
22120
  switch (p.action) {
@@ -21992,7 +22247,7 @@ function headsUp(arcade, files, alreadyOnArcade, secretWarnings, dir) {
21992
22247
  );
21993
22248
  const age = (p2) => {
21994
22249
  try {
21995
- return p2 ? statSync5(join5(dir, p2)).mtimeMs : null;
22250
+ return p2 ? statSync5(join6(dir, p2)).mtimeMs : null;
21996
22251
  } catch {
21997
22252
  return null;
21998
22253
  }
@@ -22028,9 +22283,9 @@ function headsUp(arcade, files, alreadyOnArcade, secretWarnings, dir) {
22028
22283
  return notes;
22029
22284
  }
22030
22285
  async function fillLicenseHolder(dir) {
22031
- const path = join5(dir, "LICENSE");
22032
- if (!existsSync5(path)) return;
22033
- const text = readFileSync7(path, "utf8");
22286
+ const path = join6(dir, "LICENSE");
22287
+ if (!existsSync6(path)) return;
22288
+ const text = readFileSync8(path, "utf8");
22034
22289
  if (!text.includes(LICENSE_HOLDER_PLACEHOLDER)) return;
22035
22290
  const handle = savedLogin()?.handle ?? (await request("/api/me").catch(() => null))?.user?.handle;
22036
22291
  if (!handle) return;
@@ -22053,14 +22308,14 @@ async function publish(dirArg, opts) {
22053
22308
  );
22054
22309
  useStdoutForJson();
22055
22310
  }
22056
- const dir = resolve3(dirArg ?? ".");
22311
+ const dir = resolve4(dirArg ?? ".");
22057
22312
  const raw = readArcadeRaw(dir);
22058
- if (typeof raw.demo_video === "string" && !raw.preview_video && existsSync5(join5(dir, raw.demo_video))) {
22313
+ if (typeof raw.demo_video === "string" && !raw.preview_video && existsSync6(join6(dir, raw.demo_video))) {
22059
22314
  try {
22060
22315
  const made = makePreview(dir, raw.demo_video);
22061
22316
  if (made) {
22062
22317
  raw.preview_video = made;
22063
- writeFileSync3(join5(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22318
+ writeFileSync3(join6(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22064
22319
  `);
22065
22320
  out(dim(`Made a 12 s hover preview (${made}).`));
22066
22321
  } else {
@@ -22075,7 +22330,7 @@ async function publish(dirArg, opts) {
22075
22330
  out(yellow(`No hover preview: ${e.message} Cards will show the thumbnail.`));
22076
22331
  }
22077
22332
  }
22078
- if (typeof raw.demo_video === "string" && !raw.preview_gif && existsSync5(join5(dir, raw.demo_video))) {
22333
+ if (typeof raw.demo_video === "string" && !raw.preview_gif && existsSync6(join6(dir, raw.demo_video))) {
22079
22334
  const made = makeGif(dir, raw.demo_video);
22080
22335
  if (!made)
22081
22336
  throw new ExitError(
@@ -22085,9 +22340,9 @@ async function publish(dirArg, opts) {
22085
22340
  "Then run `arcade media preview` (or publish again, which makes it for you)."
22086
22341
  );
22087
22342
  raw.preview_gif = made;
22088
- writeFileSync3(join5(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22343
+ writeFileSync3(join6(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22089
22344
  `);
22090
- out(dim(`Made a GIF from the demo (${made}, ${mb(statSync5(join5(dir, made)).size)}).`));
22345
+ out(dim(`Made a GIF from the demo (${made}, ${mb(statSync5(join6(dir, made)).size)}).`));
22091
22346
  }
22092
22347
  const prov = raw.provenance ?? {};
22093
22348
  const orch = prov.orchestrator ?? null;
@@ -22151,7 +22406,7 @@ async function publish(dirArg, opts) {
22151
22406
  bad.map((f) => `${f.path} (move it out of the folder, or delete it)`)
22152
22407
  );
22153
22408
  }
22154
- const notMit = files.filter((f) => isLicenseFile(f.path)).map((f) => licenseFileProblem(f.path, readFileSync7(f.abs, "utf8"))).filter((p) => p !== null);
22409
+ const notMit = files.filter((f) => isLicenseFile(f.path)).map((f) => licenseFileProblem(f.path, readFileSync8(f.abs, "utf8"))).filter((p) => p !== null);
22155
22410
  if (notMit.length) throw new ExitError("The game's license needs fixing:", 2, notMit);
22156
22411
  const media = [
22157
22412
  arcade.thumbnail,
@@ -22185,7 +22440,7 @@ async function publish(dirArg, opts) {
22185
22440
  path: f.path,
22186
22441
  size: f.size,
22187
22442
  get bytes() {
22188
- return readFileSync7(f.abs);
22443
+ return readFileSync8(f.abs);
22189
22444
  }
22190
22445
  })),
22191
22446
  arcade.play.root,
@@ -22323,7 +22578,7 @@ async function publish(dirArg, opts) {
22323
22578
  delete nextProv.build;
22324
22579
  nextProv.subagent_models = [];
22325
22580
  raw.provenance = nextProv;
22326
- writeFileSync3(join5(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22581
+ writeFileSync3(join6(dir, "arcade.json"), `${JSON.stringify(raw, null, 2)}
22327
22582
  `);
22328
22583
  }
22329
22584
  if (opts.json) {
@@ -22334,7 +22589,9 @@ async function publish(dirArg, opts) {
22334
22589
  version: result.version,
22335
22590
  generation: result.generation,
22336
22591
  main: result.main,
22337
- url: result.url
22592
+ url: result.url,
22593
+ // The drafts the agent shows the creator. Missing only if the arcade couldn't be read back.
22594
+ share: await gameShare(result.slug, { dir, version: result.version }).catch(() => null)
22338
22595
  });
22339
22596
  return;
22340
22597
  }
@@ -22354,6 +22611,7 @@ async function publish(dirArg, opts) {
22354
22611
  `It's in the stack. The owner decides which generation is main (arcade main ${result.slug} ${result.generation}).`
22355
22612
  )
22356
22613
  );
22614
+ await shareAfterPublish(result.slug, dir, result.version);
22357
22615
  if (becameUpdate)
22358
22616
  next(`keep building here. Publishing this folder again makes v${result.version + 1}.`);
22359
22617
  else next(`publishing this folder again adds another generation to v${result.version}'s stack.`);
@@ -22377,7 +22635,7 @@ async function uploadAll(files, missing) {
22377
22635
  for (; ; ) {
22378
22636
  const job = jobs.shift();
22379
22637
  if (!job) return;
22380
- const body = readFileSync7(job.f.abs);
22638
+ const body = readFileSync8(job.f.abs);
22381
22639
  const ext2 = job.f.path.split(".").pop() ?? "";
22382
22640
  const path = job.kind === "blob" ? `/api/uploads/blob/${job.f.sha256}` : `/api/uploads/media/${job.f.sha256}?ext=${ext2}`;
22383
22641
  await request(path, {
@@ -22447,14 +22705,14 @@ async function setPublished(slug, published) {
22447
22705
  }
22448
22706
 
22449
22707
  // src/commands/skills.ts
22450
- import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
22451
- import { homedir as homedir3 } from "os";
22452
- import { dirname as dirname3, join as join6, resolve as resolve4 } from "path";
22708
+ import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
22709
+ import { homedir as homedir4 } from "os";
22710
+ import { dirname as dirname3, join as join7, resolve as resolve5 } from "path";
22453
22711
  import { fileURLToPath } from "url";
22454
22712
  function skillsDir() {
22455
22713
  const here = dirname3(fileURLToPath(import.meta.url));
22456
- for (const candidate2 of [join6(here, "..", "skills"), join6(here, "..", "..", "skills")]) {
22457
- if (existsSync6(join6(candidate2, "arcade-getting-started", "SKILL.md"))) return candidate2;
22714
+ for (const candidate2 of [join7(here, "..", "skills"), join7(here, "..", "..", "skills")]) {
22715
+ if (existsSync7(join7(candidate2, "arcade-getting-started", "SKILL.md"))) return candidate2;
22458
22716
  }
22459
22717
  throw new ExitError(
22460
22718
  "The bundled skills are missing from this install. Reinstall evolutionary-arcade."
@@ -22479,7 +22737,7 @@ function guide(name, opts = {}) {
22479
22737
  return;
22480
22738
  }
22481
22739
  const skill = resolveSkill(name ?? "getting-started");
22482
- const text = readFileSync8(join6(skillsDir(), skill, "SKILL.md"), "utf8");
22740
+ const text = readFileSync9(join7(skillsDir(), skill, "SKILL.md"), "utf8");
22483
22741
  if (opts.raw) {
22484
22742
  process.stdout.write(text);
22485
22743
  return;
@@ -22493,13 +22751,13 @@ function guide(name, opts = {}) {
22493
22751
  process.stdout.write(text.replace(/^---\n[\s\S]*?\n---\n+/, ""));
22494
22752
  }
22495
22753
  var TARGETS = {
22496
- claude: () => join6(homedir3(), ".claude", "skills"),
22497
- codex: () => join6(homedir3(), ".agents", "skills")
22754
+ claude: () => join7(homedir4(), ".claude", "skills"),
22755
+ codex: () => join7(homedir4(), ".agents", "skills")
22498
22756
  };
22499
22757
  function installSkills(opts) {
22500
22758
  const src = skillsDir();
22501
22759
  const names = skillNames();
22502
- const targets = opts.dir ? [resolve4(opts.dir)] : (opts.target === "all" || !opts.target ? Object.keys(TARGETS) : [opts.target]).map((t) => {
22760
+ const targets = opts.dir ? [resolve5(opts.dir)] : (opts.target === "all" || !opts.target ? Object.keys(TARGETS) : [opts.target]).map((t) => {
22503
22761
  const f = TARGETS[t];
22504
22762
  if (!f) throw new ExitError(`Unknown --target ${t}. Use claude, codex, or all.`, 2);
22505
22763
  return f();
@@ -22507,7 +22765,7 @@ function installSkills(opts) {
22507
22765
  for (const target of targets) {
22508
22766
  mkdirSync3(target, { recursive: true });
22509
22767
  for (const name of names)
22510
- cpSync(join6(src, name), join6(target, name), { recursive: true, force: true });
22768
+ cpSync(join7(src, name), join7(target, name), { recursive: true, force: true });
22511
22769
  out(`Installed ${names.length} skills into ${target}`);
22512
22770
  }
22513
22771
  out(dim(names.join(", ")));
@@ -22599,6 +22857,12 @@ function buildProgram() {
22599
22857
  "after",
22600
22858
  "\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"
22601
22859
  );
22860
+ program.command("share [slug]").description(
22861
+ "print ready-to-post drafts for a game: link, video-description line, X post, GIF (you post them)"
22862
+ ).option("--profile [handle]", "share your whole library (default: you)").option("--badge", 'print the "Play on Evolutionary Arcade" badge as Markdown and HTML').option("--json", "print JSON (for agents)").action(share).addHelpText(
22863
+ "after",
22864
+ "\nExamples:\n arcade share night-shift the Share block publish printed\n arcade share --profile your whole library, for video descriptions\n arcade share night-shift --badge badge for a README or itch page"
22865
+ );
22602
22866
  const media = program.command("media").description("media helpers (they need ffmpeg)");
22603
22867
  media.command("preview [dir]").description(
22604
22868
  "remake media/preview.mp4 (12 s) and media/preview.gif (6 s) from your demo, after every new demo"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolutionary-arcade",
3
- "version": "0.2.1",
3
+ "version": "0.3.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-fork
3
- description: Use when someone wants to make their own version of a game on Evolutionary Arcade (evolutionaryarcade.com), usually right after they pasted the prompt from a game page's Fork or Blend button into their agent ("help me remix <game>", "help me blend <a> and <b>", `arcade guide fork`). Walks the session in Alex's order - vision (what they want to make from this game), pre-flight (CLI, skills, sign-in, tools, done while they're at the keyboard), then "let me cook": arcade fork or arcade blend, play the parent, make the changes, test, media, demo and GIF, one dry run and one go, then the link to their new game with its credit to the original creator.
3
+ description: Use when someone wants to make their own version of a game on Evolutionary Arcade (evolutionaryarcade.com), usually right after they pasted the prompt from a game page's Fork or Blend button into their agent ("help me remix <game>", "help me blend <a> and <b>", `arcade guide fork`). Walks the session in Alex's order - vision (what they want to make from this game), pre-flight (CLI, skills, sign-in, tools, done while they're at the keyboard), then "let me cook": arcade fork or arcade blend, play the parent, make the changes, test, media, demo and GIF, one dry run and one go, then the link to their new game with its credit to the original creator, and share drafts they post themselves.
4
4
  ---
5
5
 
6
6
  # Forking or blending a game on Evolutionary Arcade
@@ -23,6 +23,7 @@ Every question that needs them happens at the start, while they're excited and a
23
23
  2. **Pre-flight** (step 2): CLI, skills, sign-in, tools. All the human-in-the-loop setup, done now.
24
24
  3. **Let me cook** (steps 3-6): say "I've got it from here. I'll come back with one thing to approve," then do the work.
25
25
  4. **One go, then the link** (steps 7-8): the only question while you cook is the publish go, because it makes their game public under their name.
26
+ 5. **Share drafts** (step 9): the posts are theirs to send. You never post.
26
27
 
27
28
  ## Ground rules for the whole session
28
29
 
@@ -122,9 +123,24 @@ Failures: a taken slug (pick another with them), a game over 50 MB (compress med
122
123
 
123
124
  1. Give them the game's link: `https://evolutionaryarcade.com/g/<slug>`. Open it if you can, and check the page says "Forked from <title> by @<creator>" (or "Blended from" with both).
124
125
  2. Tell them what happens next: anyone can play it without an account, it appears in the original's family tree, and the original creator can see it was forked. To change it later, edit `~/arcade-games/<slug>/` and publish again; that makes v2.
125
- 3. Give them a ready line to share:
126
- > `I remixed <original title> by @<creator>: https://evolutionaryarcade.com/g/<slug>`
126
+
127
+ ## Step 9. Share it (you draft, they post)
128
+
129
+ `arcade publish` ended with a **Share it** block. Run `arcade share <slug> --json` for the same thing as data. Show them:
130
+
131
+ - the link and the video-description line;
132
+ - the X post draft, which credits the original's creator ("I remixed <title> by @<creator> into <their title>..."), and the X intent link that opens it ready to post;
133
+ - the GIF to attach (`media/preview.gif`);
134
+ - the badge for a README or itch page if they keep one (`arcade share <slug> --badge`).
135
+
136
+ Then:
137
+
138
+ - **You never post.** Not to X, YouTube, Discord, or anywhere else, even if they ask you to and even if you have a tool that could. You draft; they post.
139
+ - Offer once to tailor the drafts to their channel's voice (their usual tone, emoji or none, how they credit people). If they say yes, rewrite the post in that voice, keep the link and every credit, and keep it under 280 characters (X counts a link as 23).
140
+ - Remind them to put their profile link in their video descriptions: that is where viewers become players and remixers:
141
+
142
+ > `Play my games (and remix them): https://evolutionaryarcade.com/u/<handle>`
127
143
 
128
144
  ## Example
129
145
 
130
- A viewer pastes: "Help me remix "Last Signal" (evolutionaryarcade.com/g/last-signal) on Evolutionary Arcade..." You find Node but no CLI, so you install it and the skills, then `arcade info last-signal`. You ask the five vision questions. They want "the same thing but you're a stealth scout, no shooting", keep the radio station and dusk look, name it night-shift, touch yes, demo yes. Pre-flight: `arcade login` in the background, they sign in as @kai, Playwright is there, ffmpeg needs `brew install ffmpeg` and they say yes. "I've got it from here." You fork into `~/arcade-games/night-shift`, play it, replace the wave defense with a sneak-and-restore loop, add touch controls, test on desktop and phone sizes, capture media, record a demo and its GIF. The dry run says "a fork of Last Signal v1". You show one summary and ask one question. They say yes, you publish, and hand them `https://evolutionaryarcade.com/g/night-shift`, whose page reads "Forked from Last Signal by @alex".
146
+ A viewer pastes: "Help me remix "Last Signal" (evolutionaryarcade.com/g/last-signal) on Evolutionary Arcade..." You find Node but no CLI, so you install it and the skills, then `arcade info last-signal`. You ask the five vision questions. They want "the same thing but you're a stealth scout, no shooting", keep the radio station and dusk look, name it night-shift, touch yes, demo yes. Pre-flight: `arcade login` in the background, they sign in as @kai, Playwright is there, ffmpeg needs `brew install ffmpeg` and they say yes. "I've got it from here." You fork into `~/arcade-games/night-shift`, play it, replace the wave defense with a sneak-and-restore loop, add touch controls, test on desktop and phone sizes, capture media, record a demo and its GIF. The dry run says "a fork of Last Signal v1". You show one summary and ask one question. They say yes, you publish, and hand them `https://evolutionaryarcade.com/g/night-shift`, whose page reads "Forked from Last Signal by @alex". You show the Share block (the X draft credits Last Signal's creator), offer to match the voice of their TikTok captions, and remind them to put `/u/kai` in their video descriptions. They post it themselves.
@@ -1,6 +1,6 @@
1
1
  ---
2
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.
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 and share drafts (link, description line, X post, GIF, badge) that they post themselves. Also covers GitHub repos as a source, failures, and the 30-a-day cap.
4
4
  ---
5
5
 
6
6
  # Onboarding a creator onto Evolutionary Arcade
@@ -23,6 +23,7 @@ Every question that needs the creator happens at the start, while they're intere
23
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
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
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
+ 5. **Share drafts** (step 9): the posts are theirs to send. You never post.
26
27
 
27
28
  ## Ground rules for the whole session
28
29
 
@@ -145,13 +146,26 @@ Failures: a slug that's taken (pick another with them), a game over 50 MB (move
145
146
 
146
147
  ## Step 8. Hand back the link
147
148
 
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
+ 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). If they're on X, add their X profile too: share drafts for anyone who remixes their games will then credit them by their X handle.
149
150
  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>`).
151
+ 3. Give them their profile link and each game's own link (`https://evolutionaryarcade.com/g/<slug>`).
153
152
  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
153
 
154
+ ## Step 9. Share it (you draft, they post)
155
+
156
+ Each publish ended with a **Share it** block. Run `arcade share --profile --json` for the whole library and `arcade share <slug> --json` for any one game. Show them:
157
+
158
+ - the description line for their videos, `Play my games (and remix them): https://evolutionaryarcade.com/u/<handle>`, and each game's own line;
159
+ - the X post draft for the library (and for a game they want to lead with), with its X intent link;
160
+ - each game's GIF to attach (`media/preview.gif`);
161
+ - the badge for READMEs, itch pages, and their website (`arcade share <slug> --badge`, or `--profile --badge` for one badge that links to everything).
162
+
163
+ Then:
164
+
165
+ - **You never post.** Not to X, YouTube, Discord, or anywhere else, even if they ask you to and even if you have a tool that could. You draft; they post.
166
+ - Offer once to tailor the drafts to their channel's voice (their usual tone, emoji or none, how they credit people). If they say yes, rewrite the post in that voice, keep the link and every credit, and keep it under 280 characters (X counts a link as 23).
167
+ - Remind them to put their profile link in their video descriptions: that is where viewers become players and remixers.
168
+
155
169
  ## Example
156
170
 
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>`.
171
+ 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>`, the library's X draft, and the badge for their itch page. You offer to match the voice of their video captions; they post it themselves.