juicyjuicy 0.4.1 → 0.5.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/dist/index.js CHANGED
@@ -19983,6 +19983,15 @@ var relativePath = (what) => external_exports.string().transform(normalizePath).
19983
19983
  `${what} must be a relative path inside the project`
19984
19984
  );
19985
19985
  var tagSchema = external_exports.string().min(2).max(24).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "tags are lowercase words, e.g. puzzle or tower-defense");
19986
+ var languageNames = new Intl.DisplayNames(["en"], { type: "language", fallback: "none" });
19987
+ function languageName(code) {
19988
+ if (!/^[a-z]{2,3}$/.test(code)) return void 0;
19989
+ return languageNames.of(code);
19990
+ }
19991
+ var languageSchema = external_exports.string().trim().toLowerCase().refine(
19992
+ (code) => languageName(code) !== void 0,
19993
+ 'use a two-letter language code, like "en" or "pl"'
19994
+ );
19986
19995
  var SOURCE_URL_PATTERN = /^https:\/\/[a-z0-9.-]+(:\d{1,5})?(\/[\w.~%/@+=:-]*)?$/i;
19987
19996
  var sourceUrlSchema = external_exports.string().max(300).regex(
19988
19997
  SOURCE_URL_PATTERN,
@@ -20002,6 +20011,8 @@ var gameFieldSchemas = {
20002
20011
  engine: external_exports.enum(ENGINES),
20003
20012
  madeWith: external_exports.array(memberText().min(1).max(40)).max(5),
20004
20013
  tags: external_exports.array(tagSchema).max(5),
20014
+ /** The languages a player can play it in, the default first. Each one counts once. */
20015
+ languages: external_exports.array(languageSchema).min(1, "list at least one language").max(5).transform((codes) => [...new Set(codes)]),
20005
20016
  input: external_exports.array(external_exports.enum(INPUTS)),
20006
20017
  orientation: external_exports.enum(ORIENTATIONS),
20007
20018
  aspectRatio: external_exports.string().regex(/^(fill|\d{1,2}:\d{1,2})$/, 'use "W:H" like "16:9", or "fill"'),
@@ -20021,6 +20032,7 @@ var manifestSchema = external_exports.strictObject({
20021
20032
  engine: gameFieldSchemas.engine.default("other"),
20022
20033
  madeWith: gameFieldSchemas.madeWith.default([]),
20023
20034
  tags: gameFieldSchemas.tags.default([]),
20035
+ languages: gameFieldSchemas.languages.default(["en"]),
20024
20036
  input: gameFieldSchemas.input.default([]),
20025
20037
  orientation: gameFieldSchemas.orientation.default("any"),
20026
20038
  aspectRatio: gameFieldSchemas.aspectRatio.default("16:9"),
@@ -20084,6 +20096,7 @@ var updateGameRequestSchema = external_exports.object({
20084
20096
  engine: gameFieldSchemas.engine.optional(),
20085
20097
  madeWith: gameFieldSchemas.madeWith.optional(),
20086
20098
  tags: gameFieldSchemas.tags.optional(),
20099
+ languages: gameFieldSchemas.languages.optional(),
20087
20100
  input: gameFieldSchemas.input.optional(),
20088
20101
  orientation: gameFieldSchemas.orientation.optional(),
20089
20102
  aspectRatio: gameFieldSchemas.aspectRatio.optional(),
@@ -20785,8 +20798,16 @@ var REMIX_LICENSE_NAME = "JuicyJuicy remix license";
20785
20798
  var REMIX_LICENSE = "Members may download this game's files and publish a changed version of it on JuicyJuicy, credited to the original. It doesn't allow publishing the game or a remix of it anywhere else, or passing the files on. Turning Remixable off stops new remix downloads; remixes already published stay up.";
20786
20799
 
20787
20800
  // ../shared/src/listing.ts
20788
- var GAME_SORTS = ["newest", "upvoted", "downvoted", "played"];
20801
+ var GAME_SORTS = [
20802
+ "newest",
20803
+ "oldest",
20804
+ "trending",
20805
+ "upvoted",
20806
+ "downvoted",
20807
+ "played"
20808
+ ];
20789
20809
  var GAME_PERIODS = ["24h", "week", "month", "all"];
20810
+ var GAME_LISTS = ["featured", "favorites", "unplayed"];
20790
20811
  var DEFAULT_GAME_LIST_QUERY = {
20791
20812
  sort: "newest",
20792
20813
  period: "all",
@@ -20801,8 +20822,10 @@ var paramSchemas = {
20801
20822
  period: external_exports.preprocess(lowercase2, external_exports.enum(GAME_PERIODS)),
20802
20823
  engine: external_exports.preprocess(lowercase2, external_exports.enum(ENGINES)),
20803
20824
  tag: external_exports.preprocess(lowercase2, gameFieldSchemas.tags.element),
20825
+ lang: languageSchema,
20804
20826
  q: external_exports.string().max(MAX_GAME_SEARCH_LENGTH),
20805
20827
  remixes: external_exports.enum(["0", "1"]),
20828
+ list: external_exports.preprocess(lowercase2, external_exports.enum(GAME_LISTS)),
20806
20829
  page: external_exports.string().regex(/^\d{1,4}$/, "use a whole number").transform(Number).pipe(external_exports.number().min(1).max(MAX_GAME_LIST_PAGE))
20807
20830
  };
20808
20831
  function remixFilter(param) {
@@ -20814,8 +20837,10 @@ var gameListRequestSchema = external_exports.object({
20814
20837
  period: paramSchemas.period.default(DEFAULT_GAME_LIST_QUERY.period),
20815
20838
  engine: paramSchemas.engine.optional(),
20816
20839
  tag: paramSchemas.tag.optional(),
20840
+ lang: paramSchemas.lang.optional(),
20817
20841
  q: paramSchemas.q.optional(),
20818
20842
  remixes: paramSchemas.remixes.optional(),
20843
+ list: paramSchemas.list.optional(),
20819
20844
  page: paramSchemas.page.default(DEFAULT_GAME_LIST_QUERY.page)
20820
20845
  }).transform(({ remixes, ...rest }) => ({ ...rest, remixes: remixFilter(remixes) }));
20821
20846
 
@@ -24346,7 +24371,7 @@ var program = new Command();
24346
24371
  // package.json
24347
24372
  var package_default = {
24348
24373
  name: "juicyjuicy",
24349
- version: "0.4.1",
24374
+ version: "0.5.0",
24350
24375
  description: "Publish AI-made browser games to juicyjuicy.games",
24351
24376
  type: "module",
24352
24377
  license: "MIT",
@@ -25910,14 +25935,18 @@ async function deviceLogin(out, server, options) {
25910
25935
  userCode: start.userCode,
25911
25936
  verificationUrl: loginUrl,
25912
25937
  expiresIn: start.expiresIn,
25913
- message: `Open ${loginUrl} and approve code ${start.userCode}.`
25938
+ // An agent running this for someone passes the code on, so they can check it on the page (D46).
25939
+ message: `Show the user the code ${start.userCode} now: they check it on ${loginUrl} and approve. The login waits up to ${Math.round(start.expiresIn / 60)} minutes.`
25914
25940
  });
25915
25941
  out.info(`Your code: ${start.userCode}`);
25916
25942
  const opened = options.browser && await openBrowser(loginUrl);
25917
25943
  out.info(
25918
- opened ? `Approve it in the browser window that just opened.
25919
- If nothing opened, go to ${loginUrl}` : `Open ${loginUrl} and approve it.`
25944
+ opened ? `Check it in the browser window that just opened, then approve.
25945
+ If nothing opened, go to ${loginUrl}` : `Open ${loginUrl}, check the code and approve.`
25920
25946
  );
25947
+ if (!out.interactive && !out.json) {
25948
+ out.info(`AI agents: show the user the code ${start.userCode} and the link now.`);
25949
+ }
25921
25950
  const progress = out.interactive ? spinner() : null;
25922
25951
  progress?.start("Waiting for approval");
25923
25952
  let result;
@@ -26327,6 +26356,7 @@ var KEY_ORDER = [
26327
26356
  "engine",
26328
26357
  "madeWith",
26329
26358
  "tags",
26359
+ "languages",
26330
26360
  "input",
26331
26361
  "orientation",
26332
26362
  "aspectRatio",
@@ -26349,6 +26379,7 @@ async function buildManifest(dir, flags, ask) {
26349
26379
  orientation: pick2("orientation", "any"),
26350
26380
  aspectRatio: pick2("aspectRatio", "16:9"),
26351
26381
  tags: pick2("tags", []),
26382
+ languages: pick2("languages", ["en"]),
26352
26383
  input: pick2("input", []),
26353
26384
  remixable: pick2("remixable", false)
26354
26385
  };
@@ -27736,7 +27767,11 @@ function createProgram() {
27736
27767
  "--made-with <list>",
27737
27768
  'models and tools, e.g. "Claude Opus 5.5, Claude Code"',
27738
27769
  splitList
27739
- ).option("--tags <list>", "up to 5 tags, e.g. arcade,space", splitList).addOption(new Option("--input <list>", `controls: ${INPUTS.join(", ")}`).argParser(splitList)).addOption(new Option("--orientation <orientation>").choices(ORIENTATIONS)).option("--aspect-ratio <ratio>", 'like 16:9, or "fill"').option("--thumbnail <file>", "png/jpg/webp in the game folder").option("--remixable", `let other members remix it on JuicyJuicy only (${REMIX_LICENSE_NAME})`).option("--source-url <url>", "public source code, e.g. a GitHub repo").addHelpText(
27770
+ ).option("--tags <list>", "up to 5 tags, e.g. arcade,space", splitList).option(
27771
+ "--languages <list>",
27772
+ "language codes players can pick, the default first, e.g. en,pl",
27773
+ splitList
27774
+ ).addOption(new Option("--input <list>", `controls: ${INPUTS.join(", ")}`).argParser(splitList)).addOption(new Option("--orientation <orientation>").choices(ORIENTATIONS)).option("--aspect-ratio <ratio>", 'like 16:9, or "fill"').option("--thumbnail <file>", "png/jpg/webp in the game folder").option("--remixable", `let other members remix it on JuicyJuicy only (${REMIX_LICENSE_NAME})`).option("--source-url <url>", "public source code, e.g. a GitHub repo").addHelpText(
27740
27775
  "after",
27741
27776
  `
27742
27777
  ${REMIX_LICENSE_NAME} (--remixable): ${REMIX_LICENSE}`
@@ -19,8 +19,9 @@ npx -y juicyjuicy@0 whoami --json
19
19
 
20
20
  If it exits with `2`, log in. `login` opens the user's browser and **waits up to 10 minutes** for them to approve:
21
21
 
22
- - Tell the user first: *"A JuicyJuicy page will open in your browser. Check the code and click **Approve**."*
23
- - Run `npx -y juicyjuicy@0 login --json`. If your shell can run commands in the background, do that and read the first output line (`{"event": "device_code", "userCode", "verificationUrl"}`) so you can show the user the code and link. Otherwise run it in the foreground with a timeout of at least 10 minutes.
22
+ - Run `npx -y juicyjuicy@0 login --json` in the background if your shell can, and read its first output line (`{"event": "device_code", "userCode", "verificationUrl"}`) right away, and show the user the code and link without waiting for them to ask: the page shows the same code, and that's how they know the login is theirs.
23
+ - Then tell the user, with the real code and link: *"A JuicyJuicy page opened in your browser. Check it shows the code **WXTZ-BCDF** and click **Approve**. If no page opened, go to <verificationUrl>."*
24
+ - If your shell can't run commands in the background, say that first (without a code: it doesn't exist yet), then run it in the foreground with a timeout of at least 10 minutes. The code is also printed as `Your code: …`.
24
25
  - The user needs a JuicyJuicy account (it's invite-only for now). If the page says they can't sign in, they need an invite link from a friend who's a member.
25
26
 
26
27
  In CI there's no browser: the `JUICY_TOKEN` environment variable holds a token instead.
@@ -52,15 +53,22 @@ npx -y juicyjuicy@0 init --yes --json \
52
53
  - `init` keeps what an existing `juicy.json` already says (flags override), and detects `buildDir`, `entry` and `engine`. Check what it wrote.
53
54
  - `--made-with`: your real model and the tool you run in, e.g. `"Claude Opus 5.5, Claude Code"` or `"Gemini 3 Pro, Antigravity"`.
54
55
  - The slug (the `/g/<slug>` URL name) defaults to one made from the title. Once published it belongs to this user; reuse it to publish updates.
56
+ - `--languages`: the languages a player can switch between, English first, e.g. `en,pl`. Leave it out for a game that's only in English (the default is `en`).
55
57
  - Offer to write `HOW_I_MADE_IT.md` (a short story of how the game was made, up to 20 KB of markdown). `init` picks it up when it exists.
56
58
 
57
59
  All fields: [reference/manifest.md](reference/manifest.md).
58
60
 
61
+ **English first.** Every game on JuicyJuicy plays in English by default, so players anywhere can pick it up:
62
+
63
+ - If the game's text (menus, buttons, instructions, dialogue, text drawn in images) is in another language, build an English version before publishing: keep the player-facing text in one table per language, start in English, and add a visible switch (like `EN | PL`) to the language the game was made in. Tell the user in a sentence that you did. Pass both with `--languages en,pl`.
64
+ - `title` and `description` are always in English. A title that is a name (`Kasztanek`) stays as it is; one made of words in another language gets translated (`Wielka Ucieczka` → `The Great Escape`). Tell the user the English title you chose.
65
+ - Text in the thumbnail (step 4) is in English too.
66
+
59
67
  ### 4. Add a thumbnail
60
68
 
61
69
  A `thumbnail.png` (or `.jpg`/`.webp`, up to 2 MB and 4096×4096 pixels, ideally the game's shape, like 1280×720 for 16:9) in the project folder becomes the game's tile.
62
70
 
63
- - **If you can run the game in a browser and take a screenshot**, make a deliberate one: get past any title or "click to start" screen to real gameplay, then save it as `thumbnail.png` and pass `--thumbnail thumbnail.png` to `init`.
71
+ - **If you can run the game in a browser and take a screenshot**, make a deliberate one, with the game in English: get past any title or "click to start" screen to real gameplay, then save it as `thumbnail.png` and pass `--thumbnail thumbnail.png` to `init`.
64
72
  - **Otherwise the CLI takes one itself.** When `juicy.json` names no thumbnail, `check` and `publish` open the built game in Chrome, Edge, Chromium or Brave on this machine (headless, at the game's `aspectRatio`, sandboxed and offline: only the game's own files load) and use a screenshot taken a few seconds after it loads. A game that loads its engine, fonts or images from another site is captured without them. Don't block publishing on a thumbnail.
65
73
  - **Not for a remix**, unless the user agrees: the screenshot runs the other creator's code. Ask first; only if they say yes, pass `--auto-thumbnail` to `check` and `publish`. Without it you get a warning starting `no thumbnail: this is a remix`.
66
74
 
@@ -107,6 +115,8 @@ Keep it short when there's nothing to say: "Checked: no problems found" is enoug
107
115
  npx -y juicyjuicy@0 publish --json --message "<what changed, in a few words>"
108
116
  ```
109
117
 
118
+ This makes the new version live: when the user says "publish", that's what they want. Don't add `--draft` (see "Other commands") unless they asked in words for it not to go live yet. Not for a first publish, a big game or one you're unsure of either: the user would have to find the dashboard and make it live themselves.
119
+
110
120
  `publish` runs `check` again, uploads and finalizes. Publishing the same slug again creates a new version: the link stays the same and players keep their saves. Then it waits up to about 45 seconds for JuicyJuicy's automatic safety check of the new version (`{"event": "reviewing"}`); the result's `review` says how that went.
111
121
 
112
122
  ### 7. Tell the user
@@ -156,6 +166,6 @@ Everything else is in [reference/troubleshooting.md](reference/troubleshooting.m
156
166
  ## Other commands
157
167
 
158
168
  - `npx -y juicyjuicy@0 list --json`: the user's games; `list <slug>` shows one game's versions. `--all` lists every game on JuicyJuicy instead (60 a page, `--page 2` and on; `nextPage` in the result), `--owner <username>` another creator's. Narrow and order any list with `--search <title text>`, `--tag`, `--engine`, `--remixes`, `--sort newest|upvoted|downvoted|played` and `--period 24h|week|month|all`, e.g. to find a game to remix.
159
- - `npx -y juicyjuicy@0 publish --draft --json`: uploads a new version without making it live (players keep the current one), when the user wants to publish later. The result has `draft: true`; when they say so, make it live with `rollback <version>`, or they can on the dashboard.
169
+ - `npx -y juicyjuicy@0 publish --draft --json`: uploads a new version without making it live (players keep the current one). Only when the user asks for that in words (e.g. "upload it but don't put it live yet"), never on your own judgment. The result has `draft: true`; when they say so, make it live with `rollback <version>`, or they can on the dashboard.
160
170
  - `npx -y juicyjuicy@0 rollback <version> --slug <slug> --json`: makes an earlier version or a draft live, e.g. when the user says the last update broke the game. Run `list <slug>` first to pick the version with the user; one waiting for an admin's review can't be made live (`FORBIDDEN`).
161
171
  - `npx -y juicyjuicy@0 open <slug>`: opens the game page in the browser.
@@ -13,6 +13,7 @@ The manifest in the project folder. `juicyjuicy init` writes it; edit it by hand
13
13
  "engine": "phaser",
14
14
  "madeWith": ["Claude Opus 5.5", "Claude Code"],
15
15
  "tags": ["arcade", "space"],
16
+ "languages": ["en", "pl"],
16
17
  "input": ["keyboard", "touch"],
17
18
  "orientation": "landscape",
18
19
  "aspectRatio": "16:9",
@@ -33,6 +34,7 @@ The manifest in the project folder. `juicyjuicy init` writes it; edit it by hand
33
34
  | `engine` | | `vanilla`, `phaser`, `threejs`, `pixi`, `godot`, `unity` or `other`. | `--engine` |
34
35
  | `madeWith` | | Up to 5 models/tools, e.g. `["Claude Opus 5.5", "Claude Code"]`. | `--made-with "a, b"` |
35
36
  | `tags` | | Up to 5 lowercase words or `word-word`, e.g. `puzzle`, `tower-defense`. | `--tags a,b` |
37
+ | `languages` | | Up to 5 language codes a player can switch between, English first, e.g. `["en", "pl"]`. Default `["en"]`. Shown as a language filter on JuicyJuicy. | `--languages a,b` |
36
38
  | `input` | | Any of `keyboard`, `mouse`, `touch`, `gamepad`. | `--input a,b` |
37
39
  | `orientation` | | `landscape`, `portrait` or `any` (default). | `--orientation` |
38
40
  | `aspectRatio` | | `"W:H"` like `"16:9"` or `"4:3"`, or `"fill"` to fill the player. Default `16:9`. | `--aspect-ratio` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juicyjuicy",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Publish AI-made browser games to juicyjuicy.games",
5
5
  "type": "module",
6
6
  "license": "MIT",