@homie-rocks/studio 0.31.0 → 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 +68 -1
- package/README.md +44 -6
- package/bin/homie-studio.mjs +101 -8
- package/lib/build.mjs +1 -1
- package/lib/cloudflare.mjs +80 -6
- package/lib/directory.mjs +61 -9
- package/lib/doctor.mjs +23 -10
- package/lib/mcp-tools.mjs +18 -11
- package/lib/mcp.mjs +2 -0
- package/lib/parts.mjs +27 -0
- package/lib/routes.mjs +110 -2
- package/lib/scaffold.mjs +3 -3
- 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 +80 -0
- package/netplay/NETPLAY.md +148 -10
- package/netplay/netplay.ts +234 -33
- package/package.json +2 -1
- package/parts/PARTS.md +8 -2
- package/port/index.ts +1 -1
- package/port/room.ts +228 -23
- 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/starters/ember-vale/src/main.ts +4 -0
- package/starters/gem-rush/src/main.ts +29 -7
- package/starters/gem-rush-3d/src/main.ts +36 -7
- package/starters/hero-rush-3d/src/main.ts +36 -7
- package/worker/index.mjs +55 -8
- package/worker/pages.mjs +19 -10
- package/worker/parts.mjs +43 -2
- package/worker/standalone.mjs +34 -0
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,76 @@ 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
|
+
|
|
55
|
+
## [0.31.1] - 2026-10-06
|
|
56
|
+
|
|
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)
|
|
58
|
+
|
|
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.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- `@homie-rocks/camera/heading.js` (camera 0.2.1): `headingFrom({ zero: '+x', toward: '+z' })` converts your game's own heading into the camera's and back, and `headingOfVector(dx, dz)` does it from a forward vector. The package now says plainly which way its heading points (yaw 0 faces +Z, a positive yaw turns toward +X, in radians), with a picture in the README and in the types. In development the follow camera warns once when the subject keeps travelling one way while its heading says another, and names the adapter; mark reversing frames with `reversing: true`, or turn it off with `followState({ headingCheck: false })`.
|
|
64
|
+
- `net.prefs.number`, `net.prefs.boolean` and `net.prefs.string` read a setting with your fallback for a missing key, a null, a wrong type or a read before the prefs arrived, and say so once in the console. `net.prefs.set` refuses a `NaN` or anything else that would be kept as `null`.
|
|
65
|
+
- `createRoom({ fallback })`, seat or solo: `'solo'` plays a private round with bots until the room has a body for the player, `'spectate'` watches, and `room.standing` says where the player stands. NETPLAY.md section 28 is the recipe, and the late-join checklist has the slow-boot, mid-round join.
|
|
66
|
+
- A shared part can say the shape of its preview: `"aspect": "4:3"` and, for phones, `"phoneAspect": "3:4"` in `preview` (whole numbers 1 to 32, no flatter than 3:1, no taller than 1:2; 16:9 if you say nothing). Every part preview has an "Open the preview" link that shows it in a tab of its own.
|
|
67
|
+
- On a custom domain, a deploy and its plan warn before anything goes live when another site's catch-all or wildcard route covers your studio's address: leave that route alone, and add the one line shown (or `deploy --own-route`). When the routes cannot be read, it says so rather than saying all is well.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Homie's holds run in Grok once you trust the plugin.** 0.30.2 said Grok runs no plugin's hooks. That was wrong: Grok takes a plugin's hooks from its main manifest, ours named none there, and Grok was loading the Claude Code file instead and finding nothing. A secret is also now really taken out of what the AI reads in Grok (it was only being flagged beside the output). `setup status --client grok` says whether the holds ran and how to turn them on.
|
|
72
|
+
- **`publish --before` publishes nothing.** It asks the directory whether you are listed and how many publishes are left today. In 0.31.0 the command line ignored the flag and published.
|
|
73
|
+
- Commands that change something outside your computer (deploy, publish, storage, the office and others) stop at a flag they do not know, before anything is sent. `deploy --dry-run` was one such flag: it used to deploy.
|
|
74
|
+
- A page that loses its room no longer shows "Reconnecting…" for ever: after 20 seconds (`reconnectMaxMs`) it plays on its own with bots, says so, and rejoins when the room answers. The line over the game says what is true: "Reconnecting…" only to a page that was connected, and a full room is said in words with a way to another room.
|
|
75
|
+
- A player who is seated while every body is taken gets one as soon as a body frees up or the next round starts, instead of watching for the rest of the visit.
|
|
76
|
+
- A browser that freezes for a few seconds while a heavy game boots no longer drops its connection when it wakes.
|
|
77
|
+
- A part's preview is no longer cut off on a phone: a narrow frame is at least 320 px tall instead of about 197.
|
|
78
|
+
- The 3D starters say which of their angles a camera can take as it stands and which needs converting.
|
|
79
|
+
|
|
80
|
+
### Upgrade notes
|
|
81
|
+
|
|
82
|
+
- Grok: install or reinstall the plugin with trust, `grok plugin install homie-rocks/homie#plugins/homie --trust`. From then on a deploy, a Cloudflare change and a paid call are held for your "proceed", as in Codex.
|
|
83
|
+
- A studio that pins 0.31.0 still publishes on `publish --before` from its own command line until it upgrades.
|
|
84
|
+
- If your game reads settings with `net.prefs.get`, move numbers to `net.prefs.number(key, fallback)`: a `null` kept by an older build is now read as nothing kept.
|
|
85
|
+
- `@homie-rocks/camera` is 0.2.1.
|
|
86
|
+
|
|
20
87
|
## [0.31.0] - 2026-10-06
|
|
21
88
|
|
|
22
|
-
**Plugin 0.32.0** · [#50](https://github.com/homie-rocks/homie/pull/50)
|
|
89
|
+
**Plugin 0.32.0** · [#50](https://github.com/homie-rocks/homie/pull/50) · [release-2026-10-06-studio-0.31.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-06-studio-0.31.0)
|
|
23
90
|
|
|
24
91
|
Fixes from two creators' logs of building real studios: games now build on each other by sharing parts (remix is retired), a slow 3D game keeps its host, local play stays local, and synthesized bass plays at its written pitch.
|
|
25
92
|
|
package/README.md
CHANGED
|
@@ -496,8 +496,8 @@ this toolkit alone (`new`, `deploy`, `publish`), and the connector row says so.
|
|
|
496
496
|
an address or a command; the command itself is never read out.
|
|
497
497
|
- **Homie's holds**: whether the plugin's hooks ran in this app in the last ten minutes, from the dated mark
|
|
498
498
|
they leave in `.cache/homie-studio/holds/` of the home folder. Off means nothing is held: Codex runs no plugin's hooks
|
|
499
|
-
until the person trusts them in `/hooks`, and
|
|
500
|
-
|
|
499
|
+
until the person trusts them in `/hooks`, and Grok Build runs them once the plugin is trusted
|
|
500
|
+
(`grok plugin install … --trust`); the row says which.
|
|
501
501
|
|
|
502
502
|
`homie-studio demo` answers without a network too (0.30.2): where it cannot reach the arcade it names the arcade's
|
|
503
503
|
standing first pick, with `reached: false`.
|
|
@@ -659,10 +659,14 @@ as `cloudflare.domain` and is what the directory claim, `publish`, `check` and `
|
|
|
659
659
|
|
|
660
660
|
**Routes.** `deploy` writes `wrangler.jsonc` again each time, and keeps the routes that are the studio's own exactly
|
|
661
661
|
as they were written: a custom domain (`{ "pattern": "play.example.com", "custom_domain": true }`) and an exact-host
|
|
662
|
-
route (`{ "pattern": "play.example.com/*", "zone_name": "example.com" }`). A zone's other routes are never
|
|
663
|
-
changed or removed.
|
|
664
|
-
|
|
665
|
-
|
|
662
|
+
route (`{ "pattern": "play.example.com/*", "zone_name": "example.com" }`). A zone's other routes are never
|
|
663
|
+
changed or removed. On a custom domain, `deploy` and `deploy --plan` read the domain's Worker routes first (two GETs
|
|
664
|
+
with the sign-in Wrangler already has) and warn, before anything is deployed, when another Worker's catch-all (`*/*`)
|
|
665
|
+
or wildcard (`*example.com/*`) covers the studio's hostname: that route answers the hostname before the studio does,
|
|
666
|
+
it belongs to another site on the domain, and it must not be edited or removed. The warning gives the one safe fix,
|
|
667
|
+
the studio's own exact-host route, as the exact line; `deploy --own-route` adds that line and nothing else, only when
|
|
668
|
+
the read showed it is needed. Routes that could not be read are said as unmeasured, never as fine; after the deploy,
|
|
669
|
+
a domain that answers as something else still gets the route to add. A wildcard or catch-all route (`*/*`) in the studio's own config would hand every
|
|
666
670
|
hostname of the zone to the studio's Worker, so `deploy` refuses it before it asks Cloudflare anything; a studio that
|
|
667
671
|
really owns the whole zone says so in studio.json (`"cloudflare": { "allowWildcardRoutes": true }`).
|
|
668
672
|
|
|
@@ -792,6 +796,40 @@ it, named like the asset tools: `parts_find`, `part_add`, `part_new` and `part_s
|
|
|
792
796
|
`package.json` is left as it is, and a package the part builds on that the studio lacks is named NOT INSTALLED
|
|
793
797
|
with the npm command that installs it.
|
|
794
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
|
+
|
|
795
833
|
## How it feels: the Game Lab (`homie-studio lab`)
|
|
796
834
|
|
|
797
835
|
```sh
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -87,9 +87,17 @@
|
|
|
87
87
|
* One deploy of a studio at a time (a lock in .studio/); it says which games changed
|
|
88
88
|
* with each one's content hash, keeps the studio's own custom-domain and exact-host
|
|
89
89
|
* routes in wrangler.jsonc, and never deploys a wildcard or catch-all route.
|
|
90
|
+
* On a custom domain, the plan and the deploy read the domain's Worker routes and
|
|
91
|
+
* warn when another site's catch-all or wildcard covers the studio's hostname
|
|
92
|
+
* (never edit or remove that route). --own-route adds the studio's own
|
|
93
|
+
* exact-host route, the one safe fix, when that read shows it is needed.
|
|
90
94
|
* In Cloudflare's Workers Builds (WORKERS_CI=1, or --ci) it only applies the D1
|
|
91
95
|
* migrations and deploys: the Worker and database are the Deploy button's.
|
|
92
|
-
* homie-studio publish
|
|
96
|
+
* homie-studio publish [--before] (the directory's beta has a daily cap: it says how many publishes are left)
|
|
97
|
+
* --before publishes NOTHING: it asks the directory (a read) whether the site
|
|
98
|
+
* is listed and how many publishes are left today, and says the parts' licences.
|
|
99
|
+
* A command that changes something outside this computer (deploy, publish,
|
|
100
|
+
* storage add, the office, …) stops at a flag it does not know: nothing is sent.
|
|
93
101
|
* homie-studio storage add (large media only: an R2 bucket; needs R2 turned on for the account)
|
|
94
102
|
* homie-studio media list (every song and video page, where each file is served from, what moves to R2)
|
|
95
103
|
* homie-studio media move [<file>...] [--dry-run] [--verify]
|
|
@@ -271,6 +279,23 @@
|
|
|
271
279
|
* its own games from other studios. The plumbing behind the chat tools
|
|
272
280
|
* parts_find, part_add, part_new and part_share; nobody is asked to type it
|
|
273
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
|
+
*
|
|
274
299
|
* homie-studio statusline the current build in one line (what Claude Code's status line shows)
|
|
275
300
|
* homie-studio statusline --install [--project <folder>]
|
|
276
301
|
* turn it on in Claude Code for this studio (.claude/settings.local.json);
|
|
@@ -287,11 +312,11 @@ import { basename, join, relative, resolve } from 'node:path';
|
|
|
287
312
|
import { build } from '../lib/build.mjs';
|
|
288
313
|
import { PREVIEW_PORT, previewServer } from '../lib/preview.mjs';
|
|
289
314
|
import { check } from '../lib/check.mjs';
|
|
290
|
-
import { ciDeploy, deploy, deployPlan, mediaMove, storageAdd, whoami, wranglerBin } from '../lib/cloudflare.mjs';
|
|
315
|
+
import { ciDeploy, deploy, deployPlan, mediaMove, storageAdd, whoami, wranglerBin, zonePlan } from '../lib/cloudflare.mjs';
|
|
291
316
|
import { setupAttach } from '../lib/setup.mjs';
|
|
292
317
|
import { chromeArgs, findChrome, installChrome, noChrome } from '../lib/chrome.mjs';
|
|
293
318
|
import { deployWords } from '../lib/deploy-state.mjs';
|
|
294
|
-
import { publish } from '../lib/directory.mjs';
|
|
319
|
+
import { publish, publishBefore } from '../lib/directory.mjs';
|
|
295
320
|
import { look } from '../lib/look.mjs';
|
|
296
321
|
import { importPort, planPort } from '../lib/port.mjs';
|
|
297
322
|
import { portCheck } from '../lib/port-check.mjs';
|
|
@@ -330,10 +355,13 @@ import { formatHandoff, handoff } from '../lib/handoff.mjs';
|
|
|
330
355
|
import { partsCommand, partsLines } from '../lib/parts-cli.mjs';
|
|
331
356
|
import { partsPlanLines, partsPublishReport } from '../lib/parts-build.mjs';
|
|
332
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';
|
|
333
361
|
|
|
334
362
|
const argv = process.argv.slice(2);
|
|
335
363
|
const flags = new Map();
|
|
336
|
-
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'];
|
|
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'];
|
|
337
365
|
const positional = [];
|
|
338
366
|
for (let i = 0; i < argv.length; i++) {
|
|
339
367
|
const a = argv[i];
|
|
@@ -349,6 +377,8 @@ const log = asJson ? () => {} : (line) => process.stderr.write(`${line}\n`);
|
|
|
349
377
|
|
|
350
378
|
function print(result) {
|
|
351
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; }
|
|
352
382
|
// A check that found problems still prints them (its rows say what to fix), never a bare "failed".
|
|
353
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; }
|
|
354
384
|
if (result.ok === false && result.command === 'port check' && !result.rows) { process.stdout.write(`homie-studio: ${result.why ?? 'failed'}\n`); return; }
|
|
@@ -452,18 +482,24 @@ function print(result) {
|
|
|
452
482
|
// (lib/deploy-state.mjs): the hash `build` printed and the one the live site's manifest answers with.
|
|
453
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}`),
|
|
454
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' : ''}.`,
|
|
455
|
-
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] : []));
|
|
456
487
|
break;
|
|
457
488
|
case 'deploy plan':
|
|
458
489
|
lines.push(`What \`npm run deploy\` does for ${result.studio}, on the Cloudflare account the person approves:`, '',
|
|
459
490
|
...result.cloudflare.map((r) => ` ${r.kind}${r.name ? ` ${r.name}` : ''}: ${r.what} [${r.state}${r.plan ? `; ${r.plan}` : ''}]`), '',
|
|
460
491
|
`Cost: ${result.cost}`, `Sign-in: ${result.login}`, `Address: ${result.address}`,
|
|
461
492
|
`The directory (${result.directory.site}) stores: ${result.directory.stores}`, result.never,
|
|
462
|
-
...(result.
|
|
493
|
+
...(result.zone?.warning ? ['', `Warning: ${result.zone.warning}`] : result.zone?.why ? ['', result.zone.why] : []),
|
|
494
|
+
...(result.parts && partsPlanLines(result.parts).length ? ['', ...partsPlanLines(result.parts)] : []),
|
|
495
|
+
...(result.standalone?.length ? ['', ...result.standalone] : []));
|
|
463
496
|
break;
|
|
464
497
|
case 'storage add':
|
|
465
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}`));
|
|
466
499
|
break;
|
|
500
|
+
case 'publish before':
|
|
501
|
+
lines.push(...result.lines);
|
|
502
|
+
break;
|
|
467
503
|
case 'publish':
|
|
468
504
|
lines.push(`Listed in the directory: ${result.studioPage ?? result.directory}`, ...(result.games ?? []).map((g) => ` ${g.name}: ${g.play}`), ...(result.publishes?.line ? [result.publishes.line] : []),
|
|
469
505
|
// The parts in what was listed, in the deploy plan's own lines (lib/parts-build.mjs).
|
|
@@ -743,6 +779,52 @@ function print(result) {
|
|
|
743
779
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
744
780
|
}
|
|
745
781
|
|
|
782
|
+
/*
|
|
783
|
+
* A COMMAND THAT CHANGES SOMETHING OUTSIDE THIS COMPUTER NEVER RUNS WITH A FLAG IT DOES NOT KNOW. The parser above
|
|
784
|
+
* keeps every `--word`, and a command reads the ones it knows: `publish --before` (a flag the MCP tool had and this
|
|
785
|
+
* command did not) was read as plain `publish`, and listed the studio while its caller believed it was a preflight.
|
|
786
|
+
* So each command below names every flag it takes, and anything else stops it before anything is sent: a typo, a
|
|
787
|
+
* flag of another command, or one from a newer toolkit's notes. The key is the command and its sub-command, else
|
|
788
|
+
* the command alone. Commands that only read, or only change files in this folder, are not in this table.
|
|
789
|
+
*/
|
|
790
|
+
const OUTWARD_FLAGS = {
|
|
791
|
+
publish: ['homie', 'site', 'before'],
|
|
792
|
+
deploy: ['plan', 'ci', 'homie', 'own-route'],
|
|
793
|
+
'storage add': [],
|
|
794
|
+
'media put': ['as'],
|
|
795
|
+
'media move': ['dry-run', 'plan', 'verify'],
|
|
796
|
+
'setup attach': ['homie', 'client'],
|
|
797
|
+
handoff: ['homie', 'client'],
|
|
798
|
+
'players owner': ['url', 'revoke'],
|
|
799
|
+
'stats key': ['url', 'hours'], 'stats link': ['url'], 'stats revoke': ['url'], 'stats share': [],
|
|
800
|
+
'office link': ['url', 'to'], 'office key': ['url', 'hours'], 'office announce': ['url', 'game', 'room', 'seconds'],
|
|
801
|
+
'office invite': ['url', 'label', 'uses', 'count', 'days', 'server'], 'office launch': ['url', 'max'],
|
|
802
|
+
'office kick': ['url', 'minutes'], 'office mute': ['url', 'minutes', 'off'], 'office close': ['url', 'minutes', 'reopen'], 'office revoke': ['url'],
|
|
803
|
+
'lounge mod': ['url', 'remove'], 'lounge remove': ['url', 'all'],
|
|
804
|
+
'chat remove': ['url', 'all'], 'chat budget': ['url'],
|
|
805
|
+
'servers close': ['url', 'reopen'], 'servers level': ['url'], 'servers member': ['url', 'role', 'remove'],
|
|
806
|
+
'shop connect': ['url', 'managed', 'live'], 'shop disconnect': ['url'], 'shop refund': ['url', 'reason', 'note'], 'shop statements': ['url', 'period', 'send'],
|
|
807
|
+
'agents pass': ['url', 'label', 'server', 'hands', 'role', 'days'], 'agents revoke': ['url'], 'agents brain': ['url', 'budget', 'remove'],
|
|
808
|
+
'agents sit': ['url', 'server', 'pass', 'label', 'brain'],
|
|
809
|
+
};
|
|
810
|
+
/** Every command takes these: how it answers, never what it does. */
|
|
811
|
+
const ANY_COMMAND = ['json', 'help', 'version'];
|
|
812
|
+
|
|
813
|
+
/** The refusal for a flag an outward-writing command does not take, or null. Nothing has run when this is asked. */
|
|
814
|
+
function unknownFlags(words, given) {
|
|
815
|
+
const [cmd, sub] = words;
|
|
816
|
+
const key = sub !== undefined && OUTWARD_FLAGS[`${cmd} ${sub}`] ? `${cmd} ${sub}` : OUTWARD_FLAGS[cmd] ? cmd : null;
|
|
817
|
+
if (!key) return null;
|
|
818
|
+
const takes = OUTWARD_FLAGS[key];
|
|
819
|
+
const strange = [...given.keys()].filter((k) => !takes.includes(k) && !ANY_COMMAND.includes(k));
|
|
820
|
+
if (!strange.length) return null;
|
|
821
|
+
const named = strange.map((k) => `--${k}`).join(', ');
|
|
822
|
+
return {
|
|
823
|
+
ok: false, command: key, needs: 'flag', flags: strange, takes: takes.map((k) => `--${k}`),
|
|
824
|
+
why: `homie-studio ${key} does not take ${named}, so it did not run: nothing was sent or changed. It changes something outside this computer, and a flag it does not know is never guessed at. It takes: ${takes.length ? takes.map((k) => `--${k}`).join(', ') : 'no flags of its own'} (and --json).`,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
746
828
|
async function main() {
|
|
747
829
|
const [cmd, sub] = positional;
|
|
748
830
|
if (!cmd || cmd === 'help' || flags.has('help')) {
|
|
@@ -751,6 +833,8 @@ async function main() {
|
|
|
751
833
|
return { ok: true, command: 'help' };
|
|
752
834
|
}
|
|
753
835
|
if (cmd === 'version' || flags.has('version')) return { ok: true, command: 'version', version: STUDIO_VERSION };
|
|
836
|
+
const strange = unknownFlags(positional, flags);
|
|
837
|
+
if (strange) return strange;
|
|
754
838
|
if (cmd === 'new') return newStudio(positional[1], { name: flags.get('name'), homie: flags.get('homie'), slug: flags.get('slug'), install: !flags.has('no-install'), template: flags.has('template') });
|
|
755
839
|
if (cmd === 'starters') return { ok: true, command: 'starters', starters: starters() };
|
|
756
840
|
// The floor's built-in words need no studio: anyone can read what a studio's chat always holds.
|
|
@@ -786,6 +870,9 @@ async function main() {
|
|
|
786
870
|
if (cmd === 'anim') return animCommand(root, sub, positional, flags, { log });
|
|
787
871
|
if (cmd === 'collision') return collisionCommand(root, sub, positional, flags);
|
|
788
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 */ } } }); }
|
|
789
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); }
|
|
790
877
|
if (cmd === 'statusline') return installStatusLine(root, { remove: flags.has('remove'), replace: flags.has('replace'), project: flags.get('project') ?? null });
|
|
791
878
|
if (cmd === 'codex') return codexCommand(root, sub);
|
|
@@ -870,16 +957,22 @@ async function main() {
|
|
|
870
957
|
const only = flags.get('only') ? String(flags.get('only')).split(',').filter((d) => ['computer', 'phone', 'sideways'].includes(d)) : undefined;
|
|
871
958
|
return look({ url, paths, shots: flags.get('shots') ? resolve(flags.get('shots')) : join(root, '.studio', 'look'), devices: only, log });
|
|
872
959
|
}
|
|
873
|
-
if (cmd === 'deploy' && flags.has('plan'))
|
|
960
|
+
if (cmd === 'deploy' && flags.has('plan')) {
|
|
961
|
+
// On a custom domain the plan also reads the domain's Worker routes (lib/routes.mjs): a read, nothing changes.
|
|
962
|
+
const zone = await zonePlan(root);
|
|
963
|
+
return { ...deployPlan(root), parts: partsPublishReport(root), ...(zone ? { zone } : {}), standalone: standaloneDeployNotes(root) };
|
|
964
|
+
}
|
|
874
965
|
// Cloudflare's Workers Builds runs `npm run deploy` with WORKERS_CI=1 (and its own token for this one account).
|
|
875
966
|
if (cmd === 'deploy' && (process.env.WORKERS_CI === '1' || flags.has('ci'))) return tracked(root, 'deploy', () => ciDeploy(root, { log }), 'deploy');
|
|
876
|
-
|
|
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'); }
|
|
877
969
|
if (cmd === 'storage' && sub === 'add') return storageAdd(root, { log });
|
|
878
970
|
if (cmd === 'storage') {
|
|
879
971
|
const cf = readStudio(root).cloudflare ?? {};
|
|
880
972
|
const has = Boolean(cf.r2 && (cf.created ?? []).includes(`r2:${cf.r2}`));
|
|
881
973
|
return { ok: true, command: 'storage', storage: has ? { kind: 'r2', bucket: cf.r2 } : null, why: has ? undefined : 'no storage yet: the studio runs without it; `homie-studio storage add` adds an R2 bucket for large media (Cloudflare asks for a payment method before R2 works)' };
|
|
882
974
|
}
|
|
975
|
+
if (cmd === 'publish' && flags.has('before')) return publishBefore(root, { homie: flags.get('homie'), site: flags.get('site'), log: () => {} });
|
|
883
976
|
if (cmd === 'publish') return publish(root, { homie: flags.get('homie'), site: flags.get('site'), log });
|
|
884
977
|
if (cmd === 'players' && sub === 'owner') return playersOwner(root, { url: flags.get('url'), revoke: flags.has('revoke') });
|
|
885
978
|
if (cmd === 'players' && !sub) return playersShow(root, { url: flags.get('url') });
|
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/cloudflare.mjs
CHANGED
|
@@ -19,21 +19,28 @@
|
|
|
19
19
|
* It REFUSES to touch a Worker, database or bucket of that name that this
|
|
20
20
|
* studio did not create (studio.json `cloudflare.created` is the record), so it
|
|
21
21
|
* can never overwrite someone's existing site. `deploy --plan` says all of this
|
|
22
|
-
* before anything happens and
|
|
22
|
+
* before anything happens and changes nothing.
|
|
23
|
+
*
|
|
24
|
+
* A studio on a CUSTOM DOMAIN shares that domain with whatever else its owner
|
|
25
|
+
* runs there. The plan and the deploy read the domain's Worker routes (a read,
|
|
26
|
+
* with Wrangler's own sign-in) and warn when another site's catch-all or
|
|
27
|
+
* wildcard covers the studio's hostname: that route is never the studio's to
|
|
28
|
+
* edit or remove (lib/routes.mjs, zoneCheck below).
|
|
23
29
|
*/
|
|
24
30
|
import { spawn, spawnSync } from 'node:child_process';
|
|
25
31
|
import { createHash } from 'node:crypto';
|
|
26
32
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
33
|
import { join } from 'node:path';
|
|
34
|
+
import { cloudflareAuth } from './brain-probe.mjs';
|
|
28
35
|
import { build } from './build.mjs';
|
|
29
36
|
import { builtGames, compareDeploy, deployWords, lastDeploy, lastDeployRecord, lockDeploy, recordDeploy } from './deploy-state.mjs';
|
|
30
37
|
import { listedHere, publish } from './directory.mjs';
|
|
31
38
|
import { MAX_ASSET_BYTES, R2_COST, mediaPlan, r2OverOf, recordMove, sha256File, sizeOf } from './media.mjs';
|
|
32
39
|
import { whyFailed } from './net.mjs';
|
|
33
40
|
import { repoOf } from './repo.mjs';
|
|
34
|
-
import { keptRoutes, readConfig, shadowedDomain, wideRouteRefusal } from './routes.mjs';
|
|
41
|
+
import { keptRoutes, readConfig, readZoneRoutes, routesOf, shadowedDomain, wideRouteRefusal, zoneFinding } from './routes.mjs';
|
|
35
42
|
import { ensureLocalIgnored, ensureMigrations, migrationWord, wranglerConfig } from './scaffold.mjs';
|
|
36
|
-
import { LOCAL_STATE, configPath, isWorkersDev, layoutOf, readLocal, readStudio, siteUrl, workerDir, writeLocal, writeStudio } from './studio.mjs';
|
|
43
|
+
import { LOCAL_STATE, configPath, domainOrigin, isWorkersDev, layoutOf, readLocal, readStudio, siteUrl, workerDir, writeLocal, writeStudio } from './studio.mjs';
|
|
37
44
|
import { projectsCloudflareEnv } from './projects-env.mjs';
|
|
38
45
|
|
|
39
46
|
const ANSI = /\u001b\[[0-9;]*m/g;
|
|
@@ -106,6 +113,54 @@ export function explainCloudflare(out, accountId = null) {
|
|
|
106
113
|
return null;
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
/** The studio's custom hostname ("play.example.com"), or null for a studio on its workers.dev address. */
|
|
117
|
+
export function customHost(studio) {
|
|
118
|
+
const cf = studio?.cloudflare ?? {};
|
|
119
|
+
const origin = domainOrigin(cf.domain) ?? (cf.url && !isWorkersDev(cf.url) ? domainOrigin(cf.url) : null);
|
|
120
|
+
return origin ? new URL(origin).hostname : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* For a studio on a custom domain: what the domain's other Worker routes mean for it (lib/routes.mjs zoneFinding),
|
|
125
|
+
* read from Cloudflare with the sign-in Wrangler already has (lib/brain-probe.mjs cloudflareAuth: a token in the
|
|
126
|
+
* environment, else `wrangler auth token`). Null for a studio with no custom domain, which is asked nothing. It
|
|
127
|
+
* only reads; a list that cannot be read comes back as state 'unmeasured' with the reason. `config` stands in for
|
|
128
|
+
* wrangler.jsonc (a deploy that is about to add a route), `fetchFn` for the network in tests.
|
|
129
|
+
*/
|
|
130
|
+
export async function zoneCheck(root, { studio = readStudio(root), config = readConfig(root), accountId = null, fetchFn = null } = {}) {
|
|
131
|
+
const hostname = customHost(studio);
|
|
132
|
+
if (!hostname) return null;
|
|
133
|
+
const worker = studio.cloudflare?.worker ?? null;
|
|
134
|
+
// The toolkit's own tests (HOMIE_STUDIO_WARM=0) with no stand-in network ask nobody, as readLiveSite does.
|
|
135
|
+
if (!fetchFn && process.env.HOMIE_STUDIO_WARM === '0') return zoneFinding({ hostname, worker, config, zone: { read: false, why: 'not asked' } });
|
|
136
|
+
const bin = wranglerBin(root);
|
|
137
|
+
let headers = null;
|
|
138
|
+
try {
|
|
139
|
+
const w = bin ? runner(root, accountId ? { CLOUDFLARE_ACCOUNT_ID: accountId } : {}) : null;
|
|
140
|
+
headers = await cloudflareAuth({
|
|
141
|
+
env: { ...process.env, ...projectsCloudflareEnv(root) }, bin, cwd: root,
|
|
142
|
+
exec: async (_bin, args) => { const r = w(args, { cwd: root }); return { code: r.code, stdout: r.stdout }; },
|
|
143
|
+
});
|
|
144
|
+
} catch { headers = null; }
|
|
145
|
+
const zone = await readZoneRoutes({ hostname, config, headers, ...(fetchFn ? { fetchFn } : {}) });
|
|
146
|
+
return zoneFinding({ hostname, worker, config, zone });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A zone finding as a deploy's result and its plan carry it: the state, the routes that are not the studio's, the one line. */
|
|
150
|
+
function zoneView(z) {
|
|
151
|
+
return {
|
|
152
|
+
state: z.state, host: z.host, domain: z.zone, foreign: z.foreign.map((r) => ({ pattern: r.pattern, worker: r.script, kind: r.kind })),
|
|
153
|
+
...(z.state === 'foreign' || z.state === 'unmeasured' ? { route: z.route, line: z.line } : {}),
|
|
154
|
+
...(z.warning ? { warning: z.warning } : {}), ...(z.why ? { why: z.why } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** `deploy --plan` for a studio on a custom domain: the plan, and what the domain's other routes mean for it. */
|
|
159
|
+
export async function zonePlan(root, opts = {}) {
|
|
160
|
+
const z = await zoneCheck(root, opts);
|
|
161
|
+
return z ? zoneView(z) : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
109
164
|
/** What `deploy` will create and what it costs, from studio.json alone. It calls nothing. */
|
|
110
165
|
export function deployPlan(root) {
|
|
111
166
|
const studio = readStudio(root);
|
|
@@ -150,7 +205,7 @@ export async function deploy(root, opts = {}) {
|
|
|
150
205
|
try { return await deployLocked(root, opts); } finally { process.off('exit', onExit); lock.release(); }
|
|
151
206
|
}
|
|
152
207
|
|
|
153
|
-
async function deployLocked(root, { log = () => {}, homie, fetchFn = null } = {}) {
|
|
208
|
+
async function deployLocked(root, { log = () => {}, homie, fetchFn = null, ownRoute = false } = {}) {
|
|
154
209
|
const studio = readStudio(root);
|
|
155
210
|
const cf = { r2: null, created: [], ...studio.cloudflare };
|
|
156
211
|
const created = new Set(cf.created ?? []);
|
|
@@ -159,7 +214,7 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null } = {}
|
|
|
159
214
|
// studio's own routes exactly as its owner wrote them.
|
|
160
215
|
const wide = wideRouteRefusal(readConfig(root), studio);
|
|
161
216
|
if (wide) return { ok: false, command: 'deploy', ...wide };
|
|
162
|
-
|
|
217
|
+
let routes = keptRoutes(root, studio);
|
|
163
218
|
const who = whoami(root);
|
|
164
219
|
if (!who) {
|
|
165
220
|
if (cf.auth === 'stripe-projects') {
|
|
@@ -186,6 +241,25 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null } = {}
|
|
|
186
241
|
};
|
|
187
242
|
const steps = [];
|
|
188
243
|
const step = (what, extra = {}) => { steps.push({ what, ...extra }); log(what); };
|
|
244
|
+
const notes = [];
|
|
245
|
+
|
|
246
|
+
// A studio on a custom domain: is another site's catch-all or wildcard route on the same domain covering its
|
|
247
|
+
// hostname? Read (never written) and said before Wrangler changes anything: the repair that suggests itself,
|
|
248
|
+
// editing or removing that route, takes the other site down (lib/routes.mjs). `--own-route` adds the one line
|
|
249
|
+
// that is the studio's to add, and only when the domain's routes were read and show it is needed.
|
|
250
|
+
let zone = await zoneCheck(root, { studio, accountId, fetchFn });
|
|
251
|
+
if (zone) {
|
|
252
|
+
if (ownRoute && zone.state === 'foreign') {
|
|
253
|
+
routes = [...(routes ?? []), zone.route];
|
|
254
|
+
step(`added the studio's own exact-host route to "routes" in wrangler.jsonc: ${zone.line}. It covers ${zone.host} only, and every deploy keeps it; the other site's route was not touched.`, { route: zone.route });
|
|
255
|
+
const config = readConfig(root) ?? {};
|
|
256
|
+
zone = await zoneCheck(root, { studio, accountId, fetchFn, config: { ...config, routes: [...routesOf(config), zone.route] } });
|
|
257
|
+
} else if (ownRoute) {
|
|
258
|
+
step(`--own-route added nothing: ${zone.state === 'unmeasured' ? 'the domain\'s routes could not be read, so the domain\'s name in Cloudflare is not known for sure' : zone.state === 'taken' ? `${zone.host} is routed to another Worker, which only the person can change` : zone.state === 'own-route' ? 'the studio\'s own exact-host route is already there' : `no other route covers ${zone.host}`}.`);
|
|
259
|
+
}
|
|
260
|
+
if (zone.warning) { notes.push(zone.warning); step(`warning: ${zone.warning}`, { needs: 'cloudflare-routes', ...(zone.state === 'foreign' ? { route: zone.route } : {}) }); }
|
|
261
|
+
else if (zone.why) step(zone.why);
|
|
262
|
+
} else if (ownRoute) step('--own-route added nothing: this studio has no custom domain (studio.json cloudflare.domain).');
|
|
189
263
|
|
|
190
264
|
// The first deploy says what it is about to create, and what it costs, before it creates anything: the person
|
|
191
265
|
// reads it in the transcript whether or not the AI repeated it.
|
|
@@ -290,7 +364,6 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null } = {}
|
|
|
290
364
|
const directory = homie || studio.homie?.directory || 'https://homie.rocks';
|
|
291
365
|
recordDeploy(root, builtNow);
|
|
292
366
|
let claim = null;
|
|
293
|
-
const notes = [];
|
|
294
367
|
if (url) {
|
|
295
368
|
const read = await readLiveSite(url, fetchFn ? { fetchFn } : {});
|
|
296
369
|
claim = read.claim;
|
|
@@ -340,6 +413,7 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null } = {}
|
|
|
340
413
|
games: b.catalogue.map((id) => { const g = delta.games.find((x) => x.id === id); return { id, page: url ? `${url}/${id}/` : null, play: url ? `${url}/${id}/play` : null, hash: g?.hash ?? null, change: delta.first ? 'first deploy from this computer' : g?.change ?? null }; }),
|
|
341
414
|
...(delta.removed.length ? { removed: delta.removed } : {}),
|
|
342
415
|
...(routes ? { routes: routes.map((r) => r.pattern) } : {}),
|
|
416
|
+
...(zone ? { zone: zoneView(zone) } : {}),
|
|
343
417
|
...(notes.length ? { notes } : {}),
|
|
344
418
|
...(reread ? { reread } : {}),
|
|
345
419
|
songs: b.songs.map((slug) => ({ slug, page: url ? `${url}/music/${slug}/` : null })),
|