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 +10 -0
- package/README.md +9 -7
- package/dist/cli.js +320 -59
- package/package.json +1 -1
- package/skills/arcade-getting-started/SKILL.md +9 -5
- package/skills/arcade-onboarding/SKILL.md +157 -0
- package/skills/arcade-publishing/SKILL.md +10 -5
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
|
|
12
|
-
arcade login #
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
| `arcade skills install [--target claude\|codex\|all] [--dir <path>]` | Copies the
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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(
|
|
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
|
-
|
|
21595
|
-
|
|
21596
|
-
"
|
|
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
|
-
|
|
21751
|
+
String(t.seconds),
|
|
21605
21752
|
"-an",
|
|
21606
21753
|
"-vf",
|
|
21607
|
-
|
|
21608
|
-
"-
|
|
21609
|
-
"
|
|
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
|
-
|
|
21759
|
+
"GIF"
|
|
21621
21760
|
);
|
|
21622
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21642
|
-
|
|
21643
|
-
|
|
21644
|
-
|
|
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 ${
|
|
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)
|
|
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 = [
|
|
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
|
-
|
|
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
|
|
22178
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
|
22251
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
128
|
-
-
|
|
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.
|