@homie-rocks/studio 0.31.1 → 0.32.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 +36 -1
- package/README.md +34 -0
- package/bin/homie-studio.mjs +33 -5
- package/lib/build.mjs +1 -1
- package/lib/doctor.mjs +13 -1
- package/lib/mcp-tools.mjs +5 -2
- package/lib/mcp.mjs +2 -0
- package/lib/standalone-cli.mjs +106 -0
- package/lib/standalone-files.mjs +360 -0
- package/lib/standalone-icons.mjs +106 -0
- package/lib/standalone-lock.json +3404 -0
- package/lib/standalone-pins.json +18 -0
- package/lib/standalone-tools.mjs +63 -0
- package/lib/standalone.mjs +868 -0
- package/lib/template-history.json +40 -0
- package/netplay/NETPLAY.md +10 -0
- package/netplay/netplay.ts +15 -5
- package/package.json +2 -1
- package/standalone/STANDALONE.md +313 -0
- package/standalone/electron/files.cjs +50 -0
- package/standalone/electron/main.cjs +139 -0
- package/standalone/electron/pack.mjs +82 -0
- package/standalone/shell/index.html +36 -0
- package/standalone/shell/shell.css +46 -0
- package/standalone/shell/shell.js +346 -0
- package/worker/index.mjs +55 -8
- package/worker/standalone.mjs +34 -0
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,44 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.32.0] - 2026-10-06
|
|
21
|
+
|
|
22
|
+
**Plugin 0.33.0** · [#54](https://github.com/homie-rocks/homie/pull/54)
|
|
23
|
+
|
|
24
|
+
A game can now be an app of its own: a desktop app for macOS, Windows and Linux and a phone app for iOS and Android, the same web game in a thin shell that still plays through your studio's own site.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Standalone copies of a game.** `homie-studio standalone build <game>` makes the game you already have into a desktop app (Electron) for macOS, Windows and Linux, and a phone app (Capacitor) for iOS and Android. The app shows the same files your site serves, so the game is not ported or rewritten. It finds its rooms on your studio's own deployed site, so a player in the app and a player in a browser meet in the same room; with no connection it plays offline with its bots. In chat it is the `game_standalone` tool, and the plugin has a `standalone` skill.
|
|
29
|
+
- **What the standalone game does not have (v1).** Say this to anyone who will ship one; the web game has all of these, and the app does not yet. No player accounts and no sign-in: a player in the app is a guest. No cloud saves: saves and settings are kept on the device only, do not follow a player to the site or to another device, and are lost when the app is uninstalled. No shop: nothing is sold in the app. No room chat: it is off until the app has report and mute controls. No watch page, no big screen and no owner's controls. Only the public server through an open door: a private or invite-only game, or a server behind an account or an invite, plays offline in the app. No automatic updates: a newer version reaches players only when you build again and ship it through Steam or the store. Steam's overlay, achievements and friends: none of it has been run, and Electron apps often show no overlay. Nothing is uploaded for you: the steps for Steam, App Store Connect and Google Play are printed, and you run them. The Windows build is unsigned.
|
|
30
|
+
- **What has been run and what has not.** Each build is the kind of file Steam or a store accepts for upload; whether a store takes your game is the store's decision, with its own review, fees and rules. On one Mac we started the macOS build (the game loaded, found a room on a local dev server and played), the iOS build in the Simulator and the Android build in the emulator. **The Windows and Linux builds have never been started on any computer.** Nothing has been started by Steam. A Developer ID signature, notarization and the iOS archive for the App Store have never been made. Nothing has run on a real phone. The GitHub workflow has never been run. `standalone/STANDALONE.md` has the whole list, and what each store asks before you pay it anything.
|
|
31
|
+
- `standalone plan <game>` says, before anything is built or downloaded: which of the five this computer can build, what each of the others needs and how to get it, the address that will be built into every copy, and what a standalone copy does not have. It changes nothing. A row says "ok" only for what it looked at: a signing name in the environment is "set, checked later", and a keychain it could not read is "could not check".
|
|
32
|
+
- A build's result says what is true of each target. **Built** means the file is at the path given. The desktop build for the computer it is made on is also started once, and says "started here and loaded the game: yes" or "NO"; every other target says "built, not started on this computer". **Skipped** means a tool is missing, with its fix. A release that could not be signed is **UNSIGNED**, and the command does not report success. The command succeeds only when something was built, nothing failed and no release came out unsigned.
|
|
33
|
+
- A target whose tool is missing on your computer is skipped, and the others still build. A Windows build can be made on a Mac (no Wine needed). An Android build uses Android Studio's own JDK when the Java on your PATH is too new for it. Nothing is installed for you.
|
|
34
|
+
- `standalone ci <game>` writes a GitHub workflow that builds all five on GitHub's own Windows, Linux and macOS machines when you start it by hand, for a target your computer cannot make. Its macOS and iOS builds are never releases: it has no Apple certificate.
|
|
35
|
+
- `standalone steam <game>` writes Steam's build file from the App ID and Depot IDs in `game.json`, prints the one `steamcmd` command to run yourself, and says the launch options to set in Steamworks (the Linux one needs `--no-sandbox`). `standalone run <game>` starts the built copy here.
|
|
36
|
+
- A release (`--release`) is signed from environment variables you set yourself, never from a file or the chat: `HOMIE_APPLE_IDENTITY` and `HOMIE_APPLE_NOTARY_PROFILE` for macOS, `HOMIE_APPLE_TEAM` for iOS, and the four `HOMIE_ANDROID_*` for Android. Only the one tool that signs is given them. A macOS app is called notarized only when Apple's own tool confirms it on the finished app, and an Android bundle is called signed only when its signature is read back from it. A release and a build to try are kept in separate folders.
|
|
37
|
+
- An optional `"standalone"` block in a game's `game.json`: the app's id, version, build number, how a phone holds it, its icon, and Steam's numbers. With no icon the app wears the game's cover, or a tile with its first letter.
|
|
38
|
+
- `homie-studio deploy` and `deploy --plan` say when a deploy makes a different netplay version live than the standalone copies you built, and what that means for them.
|
|
39
|
+
- `setup status` lists what the standalone builds need (Xcode with its iOS platform, the Android SDK with its platform, a JDK, signing) in a studio where a game has a `"standalone"` block.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- The netplay helper knows when it runs in an app (`HOMIE_NET.app`): its lines about a newer version read "A new version is out. Update the app to play online." and a tap on one reloads nothing, because a reload cannot bring a newer build to an app. Nothing on the wire changed, and a game on the web behaves as before.
|
|
44
|
+
- Your site's Worker lets a standalone copy read one answer from another origin: which public room to join. Nothing else is opened: no sign-in, saves, shop or office request is accepted from an app, and no cookie crosses.
|
|
45
|
+
- The Lobby keeps a bounded list of rooms: at most 512 for a game (rooms nobody is seated in make way first), rooms for the live version of a game and at most eight others at a time, and never more rooms on a server than the server may have, whatever version asks. When that last limit is reached by rooms still on an older version, a visitor on the new one waits at one of them instead of getting an extra room.
|
|
46
|
+
|
|
47
|
+
### Upgrade notes
|
|
48
|
+
|
|
49
|
+
- **Upgrade and deploy before a standalone copy can find rooms.** A copy talks to your live site, and only a site deployed with 0.32.0 or later answers it. Until then every copy plays offline.
|
|
50
|
+
- The address of your site is built into every copy and cannot be changed in a copy that has shipped. Put the studio on its own domain before you ship: a workers.dev address belongs to the Cloudflare account.
|
|
51
|
+
- Before you ship, name the game's revision in `game.json` (`"netplay": { "version": "1" }`) and raise it with every change an older copy cannot play with. Older copies then keep playing with each other, and say "Update <Name> to play online with everyone." when they meet a newer one.
|
|
52
|
+
- In an app the game's code is trusted with everything the app's page has, and on phones with Capacitor's native bridge. That is fine for your own code; do not load scripts from the network in a game you ship as an app.
|
|
53
|
+
- The first standalone build downloads the wrapper tools (a few hundred MB) into the studio's `.studio/` folder, which is not committed. `@homie-rocks/studio` itself gained no dependency.
|
|
54
|
+
|
|
20
55
|
## [0.31.1] - 2026-10-06
|
|
21
56
|
|
|
22
|
-
**Plugin 0.32.1** · [#53](https://github.com/homie-rocks/homie/pull/53)
|
|
57
|
+
**Plugin 0.32.1** · [#53](https://github.com/homie-rocks/homie/pull/53) · [release-2026-10-06-studio-0.31.1](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-06-studio-0.31.1)
|
|
23
58
|
|
|
24
59
|
Fixes from two creators' notes on 0.31.0: Homie's holds run in Grok, `publish --before` publishes nothing, a page that loses its room is never stuck, a missing setting is never a silent zero, and a part's preview fits a phone.
|
|
25
60
|
|
package/README.md
CHANGED
|
@@ -796,6 +796,40 @@ it, named like the asset tools: `parts_find`, `part_add`, `part_new` and `part_s
|
|
|
796
796
|
`package.json` is left as it is, and a package the part builds on that the studio lacks is named NOT INSTALLED
|
|
797
797
|
with the npm command that installs it.
|
|
798
798
|
|
|
799
|
+
## A game as an app of its own (`homie-studio standalone`)
|
|
800
|
+
|
|
801
|
+
`homie-studio standalone` turns a game into a desktop app for macOS, Windows and Linux (the kind of build Steam
|
|
802
|
+
accepts for upload) and a phone app for iOS and Android (the files the App Store and Google Play accept for
|
|
803
|
+
upload; each store's review, fees and rules are its own). It is the same web build of
|
|
804
|
+
the game in the thinnest shell each platform has (Electron on computers, Capacitor on phones), and nothing about
|
|
805
|
+
one game lives in a shell. Multiplayer still goes through your studio's own deployed site; with no connection the
|
|
806
|
+
game plays offline with its bots. [`standalone/STANDALONE.md`](standalone/STANDALONE.md) is the guide; the
|
|
807
|
+
plugin's `standalone` skill is the method, and in chat it is the `game_standalone` tool.
|
|
808
|
+
|
|
809
|
+
```sh
|
|
810
|
+
npx --no-install homie-studio standalone plan <id> # what would be built and what is missing; changes nothing
|
|
811
|
+
npx --no-install homie-studio standalone build <id> # every target this computer can make
|
|
812
|
+
npx --no-install homie-studio standalone run <id> # the built desktop copy, here
|
|
813
|
+
npx --no-install homie-studio standalone steam <id> # Steam's build file; uploads nothing
|
|
814
|
+
npx --no-install homie-studio standalone ci <id> # a GitHub workflow that builds all five
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
The wrapper tools are installed at exact versions into `.studio/standalone/<id>/` on the first build, so this
|
|
818
|
+
package gains no dependency and a studio that never makes a standalone copy downloads none of them. A target
|
|
819
|
+
whose tool is missing on this computer is skipped with its fix, and the others still build. Signing reads
|
|
820
|
+
environment variables only, and nothing is ever uploaded to Steam or a store: the exact steps are printed. The
|
|
821
|
+
desktop build for the computer it is made on is started once, and the result says whether the game loaded;
|
|
822
|
+
every other target says "built, not started on this computer".
|
|
823
|
+
|
|
824
|
+
**What the standalone game does not have (v1):** player accounts and sign-in, cloud saves (saves stay on the
|
|
825
|
+
device and are lost when the app is uninstalled), the shop, room chat, watching and the big screen, servers
|
|
826
|
+
other than the public one, automatic updates, and a signed Windows build. A studio must be on 0.32.0 or later
|
|
827
|
+
**and deployed** before a copy can find rooms.
|
|
828
|
+
|
|
829
|
+
**What has not been run:** the Windows and Linux builds have never been started; nothing has been started by
|
|
830
|
+
Steam (its overlay, which Electron apps often do not show, included); a Developer ID signature, notarization and
|
|
831
|
+
the iOS archive have never been made; nothing has run on a real phone. The guide lists it all.
|
|
832
|
+
|
|
799
833
|
## How it feels: the Game Lab (`homie-studio lab`)
|
|
800
834
|
|
|
801
835
|
```sh
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -279,6 +279,23 @@
|
|
|
279
279
|
* its own games from other studios. The plumbing behind the chat tools
|
|
280
280
|
* parts_find, part_add, part_new and part_share; nobody is asked to type it
|
|
281
281
|
*
|
|
282
|
+
* homie-studio standalone plan|build|run|steam|ci <game> [--for mac,windows,linux,ios,android] [--release]
|
|
283
|
+
* [--site <address>] [--build <n>]
|
|
284
|
+
* the game as an app of its own (standalone/STANDALONE.md): the same web build in
|
|
285
|
+
* Electron for macOS, Windows and Linux and Capacitor for iOS and Android (the
|
|
286
|
+
* files Steam and the stores accept for upload; their review is theirs), finding
|
|
287
|
+
* its rooms on the studio's own site and playing offline with its bots when it
|
|
288
|
+
* cannot. plan: what would be built, what each target needs on this computer,
|
|
289
|
+
* and what a standalone copy does not have (no accounts, cloud saves, shop or
|
|
290
|
+
* chat); build: every target this computer can make, in
|
|
291
|
+
* .studio/standalone/<game>/out/<target>/<debug|release>/ (a missing
|
|
292
|
+
* prerequisite skips that target with its fix; this computer's own desktop
|
|
293
|
+
* build is started once and the result says whether the game loaded;
|
|
294
|
+
* --release: signed from the environment only, game.json must name the app's
|
|
295
|
+
* id, and an unsigned release is said as UNSIGNED and is not a success); run:
|
|
296
|
+
* the built copy, here; steam: Steam's build file; ci: a GitHub workflow that
|
|
297
|
+
* builds all five. Nothing is ever uploaded to Steam or a store
|
|
298
|
+
*
|
|
282
299
|
* homie-studio statusline the current build in one line (what Claude Code's status line shows)
|
|
283
300
|
* homie-studio statusline --install [--project <folder>]
|
|
284
301
|
* turn it on in Claude Code for this studio (.claude/settings.local.json);
|
|
@@ -338,10 +355,13 @@ import { formatHandoff, handoff } from '../lib/handoff.mjs';
|
|
|
338
355
|
import { partsCommand, partsLines } from '../lib/parts-cli.mjs';
|
|
339
356
|
import { partsPlanLines, partsPublishReport } from '../lib/parts-build.mjs';
|
|
340
357
|
import { trailerCommand, trailerLines } from '../lib/trailer.mjs';
|
|
358
|
+
// STANDALONE COPIES (standalone/STANDALONE.md): `standalone …` is lib/standalone-cli.mjs; a deploy says what it means for them.
|
|
359
|
+
import { standaloneCommand, standaloneLines } from '../lib/standalone-cli.mjs';
|
|
360
|
+
import { standaloneDeployNotes } from '../lib/standalone.mjs';
|
|
341
361
|
|
|
342
362
|
const argv = process.argv.slice(2);
|
|
343
363
|
const flags = new Map();
|
|
344
|
-
const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'no-rig', 'supporter', 'managed', 'live', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before'];
|
|
364
|
+
const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'no-rig', 'supporter', 'managed', 'live', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release'];
|
|
345
365
|
const positional = [];
|
|
346
366
|
for (let i = 0; i < argv.length; i++) {
|
|
347
367
|
const a = argv[i];
|
|
@@ -357,6 +377,8 @@ const log = asJson ? () => {} : (line) => process.stderr.write(`${line}\n`);
|
|
|
357
377
|
|
|
358
378
|
function print(result) {
|
|
359
379
|
if (asJson) { process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); return; }
|
|
380
|
+
// A standalone plan or build says every target, built or not, and what a standalone copy does not have.
|
|
381
|
+
if (/^standalone( |$)/.test(String(result.command ?? ''))) { process.stdout.write(`${standaloneLines(result).join('\n')}\n`); return; }
|
|
360
382
|
// A check that found problems still prints them (its rows say what to fix), never a bare "failed".
|
|
361
383
|
if (result.ok === false && result.command !== 'port check' && !(result.command === 'look' && result.rows) && !(result.command === 'assets check' && result.rows) && !(result.command === 'parts check' && result.rows) && !(result.command === 'shoot' && result.frames !== undefined)) { process.stdout.write(`homie-studio: ${result.why ?? 'failed'}${result.instead ? `\n${result.instead}` : ''}\n`); return; }
|
|
362
384
|
if (result.ok === false && result.command === 'port check' && !result.rows) { process.stdout.write(`homie-studio: ${result.why ?? 'failed'}\n`); return; }
|
|
@@ -460,7 +482,8 @@ function print(result) {
|
|
|
460
482
|
// (lib/deploy-state.mjs): the hash `build` printed and the one the live site's manifest answers with.
|
|
461
483
|
...result.games.map((g) => ` ${g.id}: ${g.play}${g.hash ? ` [${deployWords(g)}]` : ''}`), ...(result.removed?.length ? [` no longer on the site: ${result.removed.join(', ')}`] : []), ...(result.notes ?? []).map((n) => ` Note: ${n}`), ...(result.songs ?? []).map((m) => ` song ${m.slug}: ${m.page}`), ...(result.videos ?? []).map((m) => ` video ${m.slug}: ${m.page}`),
|
|
462
484
|
...(result.media?.moved ?? []).map((m) => ` moved to R2 (checked by SHA-256): ${m.path}, still at ${m.url}`), ...(result.media?.failed ?? []).map((m) => ` not moved to R2: ${m.path}: ${m.why}`), '', `Cloudflare: Worker ${result.worker}, D1 ${result.d1}, Durable Objects Table + Lobby${result.r2 ? `, R2 ${result.r2}` : ' (no storage: none needed; `homie-studio storage add` adds it for large media)'}. All on the free Workers plan${result.r2 ? ' plus R2' : ''}.`,
|
|
463
|
-
result.claim ? 'The site claimed itself in the directory: list the games with the Homie MCP tool studio_publish, or: npx --no-install homie-studio publish' : 'No directory claim yet (the site claims itself when the directory first reads it; publish does).'
|
|
485
|
+
result.claim ? 'The site claimed itself in the directory: list the games with the Homie MCP tool studio_publish, or: npx --no-install homie-studio publish' : 'No directory claim yet (the site claims itself when the directory first reads it; publish does).',
|
|
486
|
+
...(result.standalone?.length ? ['', ...result.standalone] : []));
|
|
464
487
|
break;
|
|
465
488
|
case 'deploy plan':
|
|
466
489
|
lines.push(`What \`npm run deploy\` does for ${result.studio}, on the Cloudflare account the person approves:`, '',
|
|
@@ -468,7 +491,8 @@ function print(result) {
|
|
|
468
491
|
`Cost: ${result.cost}`, `Sign-in: ${result.login}`, `Address: ${result.address}`,
|
|
469
492
|
`The directory (${result.directory.site}) stores: ${result.directory.stores}`, result.never,
|
|
470
493
|
...(result.zone?.warning ? ['', `Warning: ${result.zone.warning}`] : result.zone?.why ? ['', result.zone.why] : []),
|
|
471
|
-
...(result.parts && partsPlanLines(result.parts).length ? ['', ...partsPlanLines(result.parts)] : [])
|
|
494
|
+
...(result.parts && partsPlanLines(result.parts).length ? ['', ...partsPlanLines(result.parts)] : []),
|
|
495
|
+
...(result.standalone?.length ? ['', ...result.standalone] : []));
|
|
472
496
|
break;
|
|
473
497
|
case 'storage add':
|
|
474
498
|
lines.push(result.already ? `Storage is already added: R2 bucket ${result.bucket}.` : `Storage added: R2 bucket ${result.bucket}.`, 'Next:', ...result.next.map((n) => ` ${n}`));
|
|
@@ -846,6 +870,9 @@ async function main() {
|
|
|
846
870
|
if (cmd === 'anim') return animCommand(root, sub, positional, flags, { log });
|
|
847
871
|
if (cmd === 'collision') return collisionCommand(root, sub, positional, flags);
|
|
848
872
|
if (cmd === 'parts') return partsCommand(root, sub, positional, flags, { log });
|
|
873
|
+
// A build takes minutes: its lines go to stderr even under --json (the result alone is on stdout), so a chat tool
|
|
874
|
+
// that runs it as a job has last lines to show; and into the progress feed, when one is open.
|
|
875
|
+
if (cmd === 'standalone') { const feed = currentFeed(root); return standaloneCommand(root, sub, positional, flags, { log: (line) => { process.stderr.write(`${line}\n`); try { feed?.log(String(line).slice(0, 200)); } catch { /* the feed is extra */ } } }); }
|
|
849
876
|
if (cmd === 'cast') { const games = listGames(root); const id = sub ?? (games.length === 1 ? games[0].id : null); if (!id) return { ok: false, command: 'cast', why: `name the game: homie-studio cast <id>${games.length ? ` (${games.map((g) => g.id).join(', ')})` : ''}` }; return castView(root, id); }
|
|
850
877
|
if (cmd === 'statusline') return installStatusLine(root, { remove: flags.has('remove'), replace: flags.has('replace'), project: flags.get('project') ?? null });
|
|
851
878
|
if (cmd === 'codex') return codexCommand(root, sub);
|
|
@@ -933,11 +960,12 @@ async function main() {
|
|
|
933
960
|
if (cmd === 'deploy' && flags.has('plan')) {
|
|
934
961
|
// On a custom domain the plan also reads the domain's Worker routes (lib/routes.mjs): a read, nothing changes.
|
|
935
962
|
const zone = await zonePlan(root);
|
|
936
|
-
return { ...deployPlan(root), parts: partsPublishReport(root), ...(zone ? { zone } : {}) };
|
|
963
|
+
return { ...deployPlan(root), parts: partsPublishReport(root), ...(zone ? { zone } : {}), standalone: standaloneDeployNotes(root) };
|
|
937
964
|
}
|
|
938
965
|
// Cloudflare's Workers Builds runs `npm run deploy` with WORKERS_CI=1 (and its own token for this one account).
|
|
939
966
|
if (cmd === 'deploy' && (process.env.WORKERS_CI === '1' || flags.has('ci'))) return tracked(root, 'deploy', () => ciDeploy(root, { log }), 'deploy');
|
|
940
|
-
|
|
967
|
+
// What the deploy means for standalone copies already made (a changed netplay version), read before it runs.
|
|
968
|
+
if (cmd === 'deploy') { const copies = standaloneDeployNotes(root); return tracked(root, 'deploy', async () => { const r = await deploy(root, { log, homie: flags.get('homie'), ownRoute: flags.has('own-route') }); return r?.ok ? { ...r, standalone: copies } : r; }, 'deploy'); }
|
|
941
969
|
if (cmd === 'storage' && sub === 'add') return storageAdd(root, { log });
|
|
942
970
|
if (cmd === 'storage') {
|
|
943
971
|
const cf = readStudio(root).cloudflare ?? {};
|
package/lib/build.mjs
CHANGED
|
@@ -94,7 +94,7 @@ function dirBytes(dir) {
|
|
|
94
94
|
return n;
|
|
95
95
|
}
|
|
96
96
|
/** From games/<id>/style.json: the light colour as paper, the dark as text, and the accent; null without one. */
|
|
97
|
-
function uiOf(g) {
|
|
97
|
+
export function uiOf(g) {
|
|
98
98
|
let pal = null;
|
|
99
99
|
try { pal = JSON.parse(readFileSync(join(g.dir, 'style.json'), 'utf8'))?.palette ?? null; } catch { pal = null; }
|
|
100
100
|
const hex = (v) => (/^#[0-9a-f]{6}$/i.test(String(v ?? '')) ? String(v).toLowerCase() : null);
|
package/lib/doctor.mjs
CHANGED
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
* Stripe for selling only in a studio with shop.json (0.24.3): Stripe's own agent plugin (its MCP server
|
|
28
28
|
* and skills) for this AI, set up when the studio starts selling; the shop's key itself
|
|
29
29
|
* goes in through `homie-studio shop connect`, never here
|
|
30
|
+
* Standalone builds optional only in a studio where a game.json has a "standalone" block: Xcode, the Android SDK
|
|
31
|
+
* and a JDK it builds on, Apple signing (a count of identities, never a name),
|
|
32
|
+
* notarization and steamcmd (lib/standalone.mjs standaloneRows)
|
|
30
33
|
* Clef locally optional Ollama with clef-flash: AI under dev for free (never downloaded without a yes)
|
|
31
34
|
* status line optional (Claude Code only) the build's progress under the prompt
|
|
32
35
|
*
|
|
@@ -47,7 +50,8 @@ import { detectLocalAi } from './local-ai.mjs';
|
|
|
47
50
|
import { findChrome } from './chrome.mjs';
|
|
48
51
|
import { whyFailed } from './net.mjs';
|
|
49
52
|
import { isOurs } from './statusline.mjs';
|
|
50
|
-
import { findStudio, readLocal, readStudio } from './studio.mjs';
|
|
53
|
+
import { findStudio, listGames, readLocal, readStudio } from './studio.mjs';
|
|
54
|
+
import { standaloneRows } from './standalone.mjs';
|
|
51
55
|
import { projectsCloudflareEnv, projectsElevenLabs } from './projects-env.mjs';
|
|
52
56
|
import { STUDIO_VERSION } from './version.mjs';
|
|
53
57
|
|
|
@@ -415,6 +419,14 @@ export async function setupStatus({
|
|
|
415
419
|
});
|
|
416
420
|
}
|
|
417
421
|
|
|
422
|
+
// Standalone copies (standalone/STANDALONE.md), only in a studio where a game has a "standalone" block: what the
|
|
423
|
+
// desktop and phone builds need here. Every one is optional: a target whose tool is missing is skipped, never blocking.
|
|
424
|
+
{
|
|
425
|
+
let asked = false;
|
|
426
|
+
try { asked = Boolean(root) && listGames(root).some((g) => g.standalone && typeof g.standalone === 'object'); } catch { asked = false; }
|
|
427
|
+
if (asked) rows.push(...await standaloneRows({ platform, exec, env }));
|
|
428
|
+
}
|
|
429
|
+
|
|
418
430
|
// The Claude Code status line (only in Claude Code, only in a studio).
|
|
419
431
|
if (root && env.CLAUDECODE === '1') {
|
|
420
432
|
let on = false;
|
package/lib/mcp-tools.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { pictureFor } from './pictures.mjs';
|
|
|
42
42
|
import { ART_UI, artToolDefs } from './art-tools.mjs';
|
|
43
43
|
// GAME PARTS (parts/PARTS.md): parts_find, part_add, part_new and part_share are lib/parts-tools.mjs.
|
|
44
44
|
import { PARTS_FIRST, partsToolDefs } from './parts-tools.mjs';
|
|
45
|
+
import { standaloneToolDefs } from './standalone-tools.mjs';
|
|
45
46
|
import { partsPlanLines } from './parts-build.mjs';
|
|
46
47
|
import { FEEDBACK_APPS, FEEDBACK_KINDS, FEEDBACK_LIMITS, appOf, cleanNote, draftId, noteBlock, sendNote, withLine } from './feedback.mjs';
|
|
47
48
|
|
|
@@ -1268,7 +1269,7 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1268
1269
|
},
|
|
1269
1270
|
{
|
|
1270
1271
|
name: 'studio_job', title: 'A job, so far',
|
|
1271
|
-
description: 'Where a job a tool handed back is (an npm install, a check, a deploy, a render): running or done, its last lines and its result. Waits up to 30 s for it to finish.',
|
|
1272
|
+
description: 'Where a job a tool handed back is (an npm install, a check, a deploy, a render, a standalone build): running or done, its last lines and its result. A result that comes with its own words for the person (a standalone build\'s) is given in those words: read them to the person. Waits up to 30 s for it to finish.',
|
|
1272
1273
|
inputSchema: { type: 'object', properties: { job: str('The job id (j_…)') }, required: ['job'] },
|
|
1273
1274
|
annotations: { title: 'A job', ...RO },
|
|
1274
1275
|
run: async (a) => {
|
|
@@ -1277,7 +1278,7 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1277
1278
|
await waitJob(job, Math.min(ctx.waitMs, 30_000));
|
|
1278
1279
|
const v = jobView(job, { lines: 20 });
|
|
1279
1280
|
if (job.endedAt) ctx.status = null;
|
|
1280
|
-
return ok(`${v.label}: ${v.state}${v.state === 'running' ? ` (${v.seconds} s so far)` : ` after ${v.seconds} s`}${v.state === 'failed' ? `: ${whyOf(job)}` : ''}${v.result ? `\n${JSON.stringify(v.result, null, 1).slice(0, 8000)}` : v.tail.length ? `\n${v.tail.join('\n')}` : ''}`, { kind: 'job', ...v });
|
|
1281
|
+
return ok(`${v.label}: ${v.state}${v.state === 'running' ? ` (${v.seconds} s so far)` : ` after ${v.seconds} s`}${v.state === 'failed' ? `: ${whyOf(job)}` : ''}${Array.isArray(v.result?.say) && v.result.say.length ? `\n${v.result.say.join('\n').slice(0, 16_000)}` : v.result ? `\n${JSON.stringify(v.result, null, 1).slice(0, 8000)}` : v.tail.length ? `\n${v.tail.join('\n')}` : ''}`, { kind: 'job', ...v });
|
|
1281
1282
|
},
|
|
1282
1283
|
},
|
|
1283
1284
|
{
|
|
@@ -1366,6 +1367,8 @@ export function toolDefs(ctx, avail = {}) {
|
|
|
1366
1367
|
// Art direction, the cast, the starter library, generated props, checks, the lineup and the rights (lib/art-tools.mjs).
|
|
1367
1368
|
tools.push(...artToolDefs(ctx, { ok, fail, cli, stillRunning, whyOf, needsInstall, ui, str, STUDIO_ARG, RO, RW, pictureFor, findNode, startJob, waitJob }));
|
|
1368
1369
|
tools.push(...partsToolDefs(ctx, { ok, fail, str, STUDIO_ARG, RO, RW }));
|
|
1370
|
+
// A game as an app of its own, for computers and phones (lib/standalone-tools.mjs).
|
|
1371
|
+
tools.push(...standaloneToolDefs(ctx, { ok, fail, cli, stillRunning, whyOf, needsInstall, str, STUDIO_ARG }));
|
|
1369
1372
|
const topics = guideTopics(ctx);
|
|
1370
1373
|
if (topics.length) {
|
|
1371
1374
|
tools.push({
|
package/lib/mcp.mjs
CHANGED
|
@@ -53,6 +53,8 @@ To make one mechanic of a game feel better (a jump, a hit, a dash), game_lab ope
|
|
|
53
53
|
|
|
54
54
|
A game's look is a set of decisions (render style, palette, light, camera, fonts, the cast and its budgets), automatic from the person's words until they steer or lock one (studio_guide { "topic": "style" }). style_explore draws three directions with the game engine for free; decision_set picks, steers and locks; assets_find searches the free CC0 starter library and asset_add copies a model in with its licence; asset_make makes a prop on the person's own fal account only after they agreed to its price; asset_check, asset_lineup and asset_rights keep it one game, phone-sized and properly licensed (studio_guide { "topic": "models" }).
|
|
55
55
|
|
|
56
|
+
A game as an app of its own (a desktop app for macOS, Windows and Linux; an iPhone or Android app): game_standalone, with plan: true first. It is the same web game in a thin shell and still plays through the studio's own site. Say plainly what its plan says a standalone copy does not have (no accounts, cloud saves, shop or chat), that a build is the kind of file Steam or a store accepts for upload and never a promise it is accepted, that nothing is uploaded for them, and what its result says was started and what was only built (studio_guide { "topic": "standalone" }).
|
|
57
|
+
|
|
56
58
|
Telling Homie (homie_feedback): when the person is stuck, confused or frustrated, after an error you could not fix, or at the end of their first studio setup or first publish, you may OFFER, once a session, to send the people who make Homie a short note about it. Draft it in plain words from what happened (it sends nothing), show it exactly as it would go (its card has Send, Edit and Don't send), and send only after they say yes. Never nag: a no is final for the session. When they ask to tell Homie something, draft it with offered: false.
|
|
57
59
|
|
|
58
60
|
Long work (npm install, check, playtest, deploy, renders) runs in the background: the tool answers at once with a card that follows it, and build_progress or studio_job reads where it is. studio_guide has Homie's full guide for each job (game, plan, port, playtest, publish, music, sound, art, video). Never put a key or password in a file or the chat. If Homie's homie.rocks connector is connected too, its tools of the same names say what to run; these run it.`;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `homie-studio standalone …`: a game as a desktop app (macOS, Windows, Linux) and a phone app (iOS, Android): the
|
|
3
|
+
* files Steam and the stores accept for upload. lib/standalone.mjs does the work; standalone/STANDALONE.md is the
|
|
4
|
+
* guide. In chat it is the game_standalone tool (lib/standalone-tools.mjs).
|
|
5
|
+
*
|
|
6
|
+
* standalone plan <game> [--for mac,windows,linux,ios,android] [--release] [--site <address>]
|
|
7
|
+
* what would be built, with what, what each target still needs on this computer, and what a standalone copy
|
|
8
|
+
* does not have. Changes nothing, installs nothing.
|
|
9
|
+
* standalone build <game> [--for …] [--release] [--site <address>] [--build <n>]
|
|
10
|
+
* builds the game for the web, then every target this computer can make; one it cannot is skipped with why.
|
|
11
|
+
* standalone run <game> [--for ios|android] the built copy, started here (none: this computer's desktop app)
|
|
12
|
+
* standalone steam <game> Steam's build files from game.json's numbers; uploads nothing
|
|
13
|
+
* standalone ci <game> .github/workflows/standalone.yml: every target on GitHub's machines
|
|
14
|
+
*
|
|
15
|
+
* `on` (tests): the computer the command is run "on" (its platform, environment and tools), handed to the library.
|
|
16
|
+
*/
|
|
17
|
+
import { STANDALONE_VERBS, standaloneBuild, standaloneCi, standalonePlan, standaloneRun, standaloneSteam } from './standalone.mjs';
|
|
18
|
+
|
|
19
|
+
export { STANDALONE_VERBS };
|
|
20
|
+
|
|
21
|
+
/** What follows the list of what a standalone copy lacks, wherever an AI reads it: never softened, never left out. */
|
|
22
|
+
export const SAY_MISSING = 'Say this to the person as it is, before they ship anything: it is what a player of the standalone game does not get.';
|
|
23
|
+
|
|
24
|
+
export async function standaloneCommand(root, sub, positional, flags, { log = () => {}, on = {} } = {}) {
|
|
25
|
+
const id = positional[2] ?? null;
|
|
26
|
+
const str = (k) => (typeof flags.get(k) === 'string' ? flags.get(k) : null);
|
|
27
|
+
// `--for` with nothing after it is passed on as it is (true), so the command can refuse it: never "all five".
|
|
28
|
+
const opts = { for: flags.has('for') ? flags.get('for') : null, release: flags.has('release'), site: str('site'), build: flags.has('build') ? flags.get('build') : null, log, ...on };
|
|
29
|
+
/*
|
|
30
|
+
* WHAT A PERSON IS TOLD rides in the result itself (`say`): the same lines the command prints, then the instruction
|
|
31
|
+
* about the missing list. A chat tool that started this as a job and reads the result later (studio_job) says
|
|
32
|
+
* these lines, not a page of JSON with the list buried in it.
|
|
33
|
+
*/
|
|
34
|
+
const told = (r) => (r && (r.targets || r.rows) ? { ...r, say: [...standaloneLines(r), '', SAY_MISSING] } : r);
|
|
35
|
+
switch (sub ?? 'plan') {
|
|
36
|
+
case 'plan': return told(await standalonePlan(root, id, opts));
|
|
37
|
+
case 'build': return told(await standaloneBuild(root, id, opts));
|
|
38
|
+
case 'run': return standaloneRun(root, id, { for: flags.has('for') ? flags.get('for') : null });
|
|
39
|
+
case 'steam': return standaloneSteam(root, id);
|
|
40
|
+
case 'ci': return standaloneCi(root, id);
|
|
41
|
+
default: return { ok: false, command: 'standalone', why: `"${sub}" is not a standalone command: ${STANDALONE_VERBS.join(', ')}` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ------------------------------------------------------------------ plain lines */
|
|
46
|
+
|
|
47
|
+
const MARK = { ready: '→', built: '✓', skipped: '○', failed: '✗' };
|
|
48
|
+
const fixLine = (t) => (t.fix ? ` ${t.fix.run ? `${t.fix.run} ` : ''}${t.fix.open ? `${t.fix.open} ` : ''}${t.fix.say ?? ''}` : null);
|
|
49
|
+
|
|
50
|
+
/** What a standalone copy does not have, under the heading every surface uses. */
|
|
51
|
+
export function missingLines(missing) {
|
|
52
|
+
return ['What the standalone game does not have (v1):', ...missing.map((m) => ` - ${m.what}: ${m.why}.`)];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function standaloneLines(r) {
|
|
56
|
+
const L = [];
|
|
57
|
+
// A refusal with something to do about it (a release with no id yet, Steam with no numbers) still says it all.
|
|
58
|
+
if (r.ok === false && !r.targets && !r.rows) { L.push(r.why ?? 'failed'); if (r.instead) L.push('', r.instead); return L; }
|
|
59
|
+
switch (r.command) {
|
|
60
|
+
case 'standalone plan': {
|
|
61
|
+
L.push(`${r.name} as a standalone game${r.release ? ' (a release)' : ' (a build to try)'}: ${r.appId}${r.appIdFrom === 'derived' ? ' (made up for now)' : ''}, version ${r.version} build ${r.build}, held ${r.orientation === 'any' ? 'any way' : r.orientation}.`,
|
|
62
|
+
` The same web build of the game, in Electron on computers and Capacitor on phones. Everything is made in ${r.dir}/ (never committed).`,
|
|
63
|
+
` Rooms: ${r.site ? `${r.site} (${r.siteFrom})` : 'none: this copy would play offline only'}${r.netplayVersion ? `, netplay version "${r.netplayVersion}"` : ''}.`,
|
|
64
|
+
` Icon: ${r.icon.note}.`, '');
|
|
65
|
+
for (const t of r.targets) {
|
|
66
|
+
L.push(` ${MARK[t.state]} ${t.target.padEnd(8)} ${t.state === 'ready' ? `${t.with}: ${t.output}` : `SKIPPED here: ${t.why}`}`);
|
|
67
|
+
if (t.state === 'skipped') { const f = fixLine(t); if (f) L.push(f); if (t.instead) L.push(` ${t.instead}`); }
|
|
68
|
+
}
|
|
69
|
+
L.push('', 'On this computer:');
|
|
70
|
+
for (const row of r.rows) L.push(` ${row.state === 'ok' ? '✓' : '○'} ${row.label} (${row.need}): ${row.detail}`);
|
|
71
|
+
if (r.warnings.length) L.push('', ...r.warnings.map((w) => `Warning: ${w}`));
|
|
72
|
+
L.push('', r.toolkitNote ?? r.needs, '', ...missingLines(r.missing));
|
|
73
|
+
if (r.ok === false) L.push('', r.why, '', r.instead);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'standalone build': {
|
|
77
|
+
L.push(`${r.name} ${r.version} build ${r.build}${r.release ? ' (release)' : ''}, ${r.appId}${r.appIdFrom === 'derived' ? ' (made up for now)' : ''}: in ${r.dir}/`);
|
|
78
|
+
for (const t of r.targets) {
|
|
79
|
+
L.push(` ${t.unsigned ? '!' : MARK[t.state]} ${t.target.padEnd(8)} ${t.state === 'built' ? `${t.unsigned ? 'BUILT, UNSIGNED (no store takes it): ' : ''}${t.path}` : t.state === 'skipped' ? `SKIPPED: ${t.why}` : `FAILED: ${t.why}`}`);
|
|
80
|
+
for (const n of t.notes ?? []) L.push(` ${n}`);
|
|
81
|
+
if (t.state === 'skipped') { const f = fixLine(t); if (f) L.push(f); if (t.instead) L.push(` ${t.instead}`); }
|
|
82
|
+
}
|
|
83
|
+
L.push(` Rooms: ${r.site ?? 'none: these copies play offline only'}. Icon: ${r.icon.note}.`);
|
|
84
|
+
if (r.warnings.length) L.push('', ...r.warnings.map((w) => `Warning: ${w}`));
|
|
85
|
+
if (r.next.length) L.push('', 'Next, by you (nothing was uploaded):', ...r.next.map((n) => ` ${n}`));
|
|
86
|
+
if (r.toolkitNote) L.push('', r.toolkitNote);
|
|
87
|
+
L.push('', ...missingLines(r.missing));
|
|
88
|
+
if (r.ok === false) L.push('', `NOT DONE: ${r.why}`);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case 'standalone run':
|
|
92
|
+
L.push(`${r.game} (${r.target}) ended.`);
|
|
93
|
+
break;
|
|
94
|
+
case 'standalone steam':
|
|
95
|
+
L.push(`Steam's build file for app ${r.app}: ${r.dir}/${r.wrote.join(', ')} (each depot is that system's release build, out/<system>/release/).`,
|
|
96
|
+
...(r.unbuilt.length ? [` NO RELEASE BUILD YET for ${r.unbuilt.join(', ')} (its depot would upload nothing): homie-studio standalone build ${r.game} --for ${r.unbuilt.join(',')} --release`] : []),
|
|
97
|
+
'In Steamworks:', ...r.setup, ` ${r.upload}`, r.note);
|
|
98
|
+
break;
|
|
99
|
+
case 'standalone ci':
|
|
100
|
+
L.push(`${r.changed ? 'Wrote' : 'Unchanged:'} ${r.file}.`, ...r.next.map((n) => ` ${n}`));
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
L.push(JSON.stringify(r));
|
|
104
|
+
}
|
|
105
|
+
return L;
|
|
106
|
+
}
|