@homie-rocks/studio 0.51.7 → 0.52.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 +54 -0
- package/apps/APPS.md +117 -0
- package/apps/apps.ts +51 -1
- package/bin/homie-studio.mjs +9 -4
- package/docs/toolkit-place-patterns.md +68 -0
- package/functions/FUNCTIONS.md +27 -0
- package/functions/functions.ts +4 -0
- package/guides/app/PLACES.md +123 -0
- package/guides/app/SKILL.md +11 -0
- package/guides/app/examples/reserve.mjs +13 -0
- package/guides/functions/SKILL.md +2 -0
- package/guides/game/SKILL.md +3 -0
- package/guides/shop/SKILL.md +27 -12
- package/guides/studio-setup/SKILL.md +4 -0
- package/lib/app-needs.mjs +41 -6
- package/lib/app-ready.mjs +31 -0
- package/lib/build.mjs +5 -0
- package/lib/check.mjs +13 -10
- package/lib/cloudflare.mjs +2 -2
- package/lib/connector-token.mjs +30 -0
- package/lib/dev-restart.mjs +25 -0
- package/lib/dev.mjs +6 -1
- package/lib/functions-build.mjs +3 -0
- package/lib/mcp-tools.mjs +2 -2
- package/lib/scaffold.mjs +22 -1
- package/lib/shop-links.mjs +4 -4
- package/lib/shop-mcp.mjs +82 -14
- package/lib/stripe-connect.mjs +9 -4
- package/lib/template-history.json +84 -0
- package/lib/upgrade-places.mjs +49 -0
- package/lib/upgrade.mjs +2 -1
- package/lib/worker-config.mjs +6 -3
- package/links/links.d.mts +5 -0
- package/links/links.d.ts +2 -0
- package/links/links.mjs +2 -0
- package/links/time.mjs +20 -0
- package/netplay/netplay.ts +3 -0
- package/package.json +1 -1
- package/rules/collision.ts +4 -2
- package/rules/terrain.ts +2 -1
- package/rules/tiles.ts +8 -4
- package/shop/SHOP.md +27 -12
- package/shop/shop.ts +1 -1
- package/tools/tools.ts +12 -0
- package/worker/app-graphics.mjs +8 -0
- package/worker/app-records.mjs +5 -1
- package/worker/app-shell.mjs +61 -17
- package/worker/app-streams.mjs +57 -0
- package/worker/app-tools.mjs +44 -0
- package/worker/booking-holds.mjs +87 -0
- package/worker/build-id.mjs +1 -1
- package/worker/business-events.mjs +31 -0
- package/worker/connector-token.mjs +11 -0
- package/worker/event-resources.mjs +1 -1
- package/worker/functions.mjs +5 -0
- package/worker/index.mjs +24 -6
- package/worker/join-area.mjs +35 -0
- package/worker/mcp-events.mjs +2 -2
- package/worker/mcp-tools.mjs +3 -0
- package/worker/mcp.mjs +17 -9
- package/worker/pages.mjs +10 -7
- package/worker/records-page.mjs +5 -4
- package/worker/records-prefs.mjs +24 -0
- package/worker/shop-links.mjs +12 -0
- package/worker/shop.mjs +14 -4
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,60 @@ 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.52.0] - 2026-10-15
|
|
21
|
+
|
|
22
|
+
**Plugin 0.52.0**
|
|
23
|
+
|
|
24
|
+
Run your place with trusted actions across screens and AI, live business records, shared booking capacity and stronger proofs that it works for your customers.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **One trusted action from any screen or AI:** App screens call the same studio tools as MCP through `createAppTools`, with strict input schemas, server role checks and versioned writes.
|
|
29
|
+
- **Screens that stay in step:** Live record subscriptions and presence use the existing Lobby without polling or a dummy game room. Supported app context supplies your public address and a local proof clock; isolated proofs stay separate.
|
|
30
|
+
- **Business events with useful history:** Record changes include the previous value. Trusted functions publish schema-checked business feeds with durable receipts that prevent duplicate events.
|
|
31
|
+
- **Bookings that share one capacity:** App holds and Stripe session checkout use the same capacity row, including group quantities, ownership, version checks and scheduled release. Confirmations distinguish unpaid reservations from verified payments.
|
|
32
|
+
- **Proofs for difficult situations:** Needs can force disabled clicks to check server refusals, race actions from separate browsers, restart local development and reload without WebGL.
|
|
33
|
+
- **Time at your place:** IANA time zones support local scheduling and explicitly reject nonexistent or ambiguous wall times around clock changes.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Shops that survive upgrades:** Matching 0.51.6 Stripe plugin verifications remain valid; real shop, slot or site changes explain what needs verification. Unfinished setup still builds and deploys with checkout closed and a clear next step.
|
|
38
|
+
- **Room for your whole catalogue:** Verified public links ship as a JSON asset instead of a size-limited Worker text binding, including catalogues with more than 100 links. Missing assets keep checkout closed.
|
|
39
|
+
- **Faster Stripe setup and replanning:** Saved public observations avoid repeated discovery. Explicit refresh paginates expanded Payment Link lists, refreshes changed links and preserves verification freshness. Private webhook setup installs Stripe's official npm CLI automatically when missing, then continues to browser approval.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **Guides grounded in six real builds:** App and game guides include place patterns, security alternatives and small examples that compile against the toolkit.
|
|
44
|
+
- **Cleaner upgrades:** Recognized polling workarounds and exact obsolete capability notes are retired; custom business logic remains available for review.
|
|
45
|
+
|
|
46
|
+
### Upgrade notes
|
|
47
|
+
|
|
48
|
+
- Apply migrations `0027_app_live.sql`, `0028_business_events.sql` and `0029_booking_holds.sql` in order.
|
|
49
|
+
- Replace refresh timers with record subscriptions, and private origin, peer and clock reads with `createAppContext`.
|
|
50
|
+
- Shared local booking holds require Stripe session checkout. Keyless Payment Links cannot claim those holds and are explicitly refused.
|
|
51
|
+
|
|
52
|
+
## [0.51.8] - 2026-10-15
|
|
53
|
+
|
|
54
|
+
**Plugin 0.51.8**
|
|
55
|
+
|
|
56
|
+
Run your studio across more screens with reliable shared records, clearer checks and a simpler owner connection.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Reliable screens and saved settings:** App screens share simultaneous record reads across tabs, queue bursts and back off after connection failures. Writes stay separate and refresh cached reads; reconnect notices use plain language. Records-only apps remember device preferences and saved references after reload while keeping their iframe sandbox.
|
|
61
|
+
- **Room for your wall display:** TV join cards reserve a measured safe area for your app, stay within a bounded size and wrap long addresses. Hiding the card returns that space to your content.
|
|
62
|
+
- **Checks that match what people can use:** Covered controls fail checks immediately with the blocking element identified, including overlays outside the app frame. You can also check text navigation with graphics disabled using `check --webgl-failure`; fallback notices stay in the page flow so controls remain reachable.
|
|
63
|
+
- **Clean toolkit type checks:** Collision, terrain and tile-coordinate code now passes strict `build --types` checks when imported into your app or game.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **Connect your owner's AI with your existing login:** `connector` uses your Wrangler sign-in to save a private MCP configuration limited to the chosen tools, studio origin and expiry. You can revoke access; the server stores only token hashes and command output contains no bearer secret.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- **Safer bookings and usable text controls:** Authoring guides show server-owned capacity with atomic versioned writes, tested with six simultaneous requests and one successful reservation. They explain how to initialise text controls before graphics and keep the app usable when 3D is unavailable.
|
|
72
|
+
- **Clear deploy guidance:** Wrangler remains the path for uploading your site's static assets. The guide explains why Cloudflare's official MCP request interface cannot perform the required upload authorization.
|
|
73
|
+
|
|
20
74
|
## [0.51.7] - 2026-10-14
|
|
21
75
|
|
|
22
76
|
**Plugin 0.51.7**
|
package/apps/APPS.md
CHANGED
|
@@ -222,3 +222,120 @@ and pass a role when deliberately testing the customer experience.
|
|
|
222
222
|
`check --shots` may capture outside the studio, but persistent needs evidence copies
|
|
223
223
|
those pictures into `.studio/checks` and stores studio-relative paths. Keep that
|
|
224
224
|
folder with the Codex evidence. Deploy can render it without outside-path warnings.
|
|
225
|
+
|
|
226
|
+
## Screens and AI share tools
|
|
227
|
+
|
|
228
|
+
Import `createAppTools` from `@homie-rocks/studio/apps`, then call
|
|
229
|
+
`await tools.call('place_book', {slot, request})`. It runs the studio's `defineTool`
|
|
230
|
+
handler, with its strict input schema, audience checks, rate policy and versioned
|
|
231
|
+
`context.database`. The shell supplies identity; message fields cannot grant a
|
|
232
|
+
role. Only studio-defined tools are exposed. Priced tools still use MCP's payment
|
|
233
|
+
approval flow. Dispose the client on unmount. Use a stable request ID across
|
|
234
|
+
retries; a lost response does not mean the write failed. Keep rules in a shared
|
|
235
|
+
`domain.ts`, but only the server handler commits them.
|
|
236
|
+
|
|
237
|
+
## Live records and frame context
|
|
238
|
+
|
|
239
|
+
`records.subscribe(() => void refresh())` returns an unsubscribe function. It
|
|
240
|
+
notifies on connection/reconnection and when the collection changes; `refresh`
|
|
241
|
+
calls `records.list()` to obtain currently authorized data. Do an initial read as
|
|
242
|
+
well. Coalesce overlapping refreshes in a busy screen. This uses the existing
|
|
243
|
+
Lobby, without a rules file or a dummy map. Notifications contain collection names,
|
|
244
|
+
not records or private IDs. A 15-second server heartbeat repairs missed wakes;
|
|
245
|
+
screens do not poll. Closing the page closes its stream. Apply `0027_app_live.sql`
|
|
246
|
+
on upgrade before starting the new Worker.
|
|
247
|
+
|
|
248
|
+
`createAppContext()` provides `ready`, `current`, `subscribe`, `now()` and `dispose`.
|
|
249
|
+
After `await context.ready`, `current` has `app`, `origin`, public `url`, and
|
|
250
|
+
`presence` (connected screens in this app/room and isolated proof scope, including
|
|
251
|
+
the current screen). Presence is screens, not unique people or paid customers.
|
|
252
|
+
The public address excludes role grants and booking credentials. Do not inspect
|
|
253
|
+
`HOMIE_NET` or cast room internals to read peers.
|
|
254
|
+
|
|
255
|
+
For a signed local proof session, the shell supports
|
|
256
|
+
`window.homieApp.setTestClock(epochMilliseconds)` and `null` to restore wall time;
|
|
257
|
+
frame code reads `context.now()`. Production shells refuse the setter. This is a
|
|
258
|
+
view clock, never authority for capacity, payment or server deadlines. Scheduled
|
|
259
|
+
needs supply their own explicit `scheduledTime`.
|
|
260
|
+
|
|
261
|
+
See `guides/app/PLACES.md` for the six-build recipe and small typed examples.
|
|
262
|
+
|
|
263
|
+
## One capacity budget for holds and Stripe
|
|
264
|
+
|
|
265
|
+
A collection with `booking: {slots:'slots', item:'ticket'}` uses the same
|
|
266
|
+
`shop_bookings` row from hold through payment. A trusted tool can call:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const held = await context.bookings.hold({
|
|
270
|
+
app: 'place', collection: 'bookings', slot: 'morning', quantity: 2,
|
|
271
|
+
id: input.request, expiresAt: Date.now() + 60_000 // choose the hold window on the server
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Require a signed-in caller, or pass a private, random 32–128 hex-character `token`
|
|
276
|
+
for a guest. Keep the request ID stable on retries; a retry returns the original expiry and never extends it. The returned `id` is
|
|
277
|
+
scoped by app/collection/proof; it is public identification, never authorization.
|
|
278
|
+
Only the owner (or holder of that private token) can `get`, `confirm` or `release`.
|
|
279
|
+
Confirm/release take `{id, version, token?}`. Another owner's ID is not permission.
|
|
280
|
+
The entire group quantity reserves atomically or fails; there is no partial group.
|
|
281
|
+
Slot reads report availability including held and confirmed quantities.
|
|
282
|
+
|
|
283
|
+
Use `shop.book('ticket', {collection:'bookings', slot:'morning', hold:held.id,
|
|
284
|
+
token}, 2)` to pay for that same row. Session checkout claims it once, in the order
|
|
285
|
+
transaction. A second checkout cannot take it; verified payment updates it.
|
|
286
|
+
`confirmed` means a capacity commitment; only `paid:true` means payment was
|
|
287
|
+
verified. Do not label a confirmation as a paid receipt.
|
|
288
|
+
|
|
289
|
+
A scheduled owner function calls
|
|
290
|
+
`context.bookings.releaseExpired({app:'place',collection:'bookings',now:event.data.scheduledTime})`.
|
|
291
|
+
Only unconfirmed local holds expire this way. After checkout claims a hold,
|
|
292
|
+
Stripe's verified payment/expiry/refund owns its lifecycle; a local timer cannot
|
|
293
|
+
release a possibly paid seat. Keyless Payment Links cannot safely claim local
|
|
294
|
+
holds: configure session checkout, or keep using their existing pending-to-paid
|
|
295
|
+
path. The toolkit refuses to mix active keyless links with new local holds.
|
|
296
|
+
Apply `0029_booking_holds.sql`; public projections never include actors or tokens.
|
|
297
|
+
|
|
298
|
+
## Adversarial needs
|
|
299
|
+
|
|
300
|
+
Actions inside `concurrent` start together on separate browser contexts:
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{"concurrent":[{"surface":"phone","selector":"#book","click":true},{"surface":"phone-two","selector":"#book","click":true}]}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Assert exactly one saved booking afterwards. `{"surface":"staff","selector":"#close","forceClick":true}`
|
|
307
|
+
temporarily bypasses the disabled attribute, clicks, and restores it: assert the
|
|
308
|
+
server refusal and unchanged data. It is not a permission grant.
|
|
309
|
+
`{"surface":"phone","capabilities":{"webgl":false}}` installs a WebGL blocker
|
|
310
|
+
before reloading that surface, leaving ordinary controls functional. Set `true`
|
|
311
|
+
to restore WebGL on another reload. `{"surface":"phone","clock":1792800000000}`
|
|
312
|
+
sets the supported local frame clock.
|
|
313
|
+
|
|
314
|
+
`{"restart":true}` asks the authenticated local dev controller to restart its own
|
|
315
|
+
Wrangler child, keeps disk state and proof sessions, and waits for readiness.
|
|
316
|
+
It never kills a PID discovered by port. Start the current toolkit's dev server;
|
|
317
|
+
remote URLs and old servers without this controller refuse or time out. Put
|
|
318
|
+
restart/capability steps outside concurrent groups, then assert saved state.
|
|
319
|
+
|
|
320
|
+
## Place time and upgrades
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import {placeTime} from '@homie-rocks/studio/links';
|
|
324
|
+
const startsAt = placeTime('2027-03-20', '09:30', 'America/Vancouver');
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Store the occurrence's local date/time and IANA zone in place config; derive epoch
|
|
328
|
+
milliseconds with the runtime's zone database. Never append `-07:00`. Invalid dates
|
|
329
|
+
and spring gaps are refused; repeated autumn times require explicit
|
|
330
|
+
`{disambiguation:'earlier'}` or `'later'`. Generate future occurrence dates from the
|
|
331
|
+
calendar rule, not a constant “next Saturday”. Update runtime zone data with the
|
|
332
|
+
runtime itself.
|
|
333
|
+
|
|
334
|
+
`upgrade --diff` includes recognized obsolete capability notes and standalone
|
|
335
|
+
2–3-second named refresh timers over `createAppRecords` clients. `upgrade --apply`
|
|
336
|
+
replaces those timers with subscriptions and corrects exact stale notes without
|
|
337
|
+
marking old needs passed. Timer conversion uses the studio’s installed TypeScript
|
|
338
|
+
compiler; run `npm install` in the studio first if it is missing. Review the diff and rerun the affected needs. Custom
|
|
339
|
+
combined timer/jobs, hand-written routes and business-event mirrors need a semantic
|
|
340
|
+
migration to the APIs above; they are not automatically deleted. The owner's
|
|
341
|
+
words, historical evidence and unresolved integration work remain intact.
|
package/apps/apps.ts
CHANGED
|
@@ -4,7 +4,9 @@ export interface AppRecord<T> { id: string; data: T; version: number; updatedAt:
|
|
|
4
4
|
export function createAppRecords<T extends Record<string, unknown>>({ collection }: { collection: string }) {
|
|
5
5
|
if (!/^[a-z][a-z0-9-]{0,39}$/.test(collection)) throw new Error('invalid collection');
|
|
6
6
|
const pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>();
|
|
7
|
+
const listeners = new Set<() => void>();
|
|
7
8
|
const receive = (e: MessageEvent) => {
|
|
9
|
+
if(e.source === parent && e.data?.type === 'homie-app-changed' && e.data.collections?.includes(collection)) { for(const listener of listeners) listener(); return; }
|
|
8
10
|
if (e.source !== parent || e.data?.type !== 'homie-app-result') return;
|
|
9
11
|
const p = pending.get(e.data.id); if (!p) return;
|
|
10
12
|
pending.delete(e.data.id); clearTimeout(p.timer);
|
|
@@ -18,11 +20,59 @@ export function createAppRecords<T extends Record<string, unknown>>({ collection
|
|
|
18
20
|
parent.postMessage({ type: 'homie-app', id, collection, method, record, data, version }, '*');
|
|
19
21
|
});
|
|
20
22
|
return {
|
|
23
|
+
subscribe(listener: () => void) { listeners.add(listener); parent.postMessage({type:'homie-app-subscribe',collections:[collection]}, '*'); return () => {listeners.delete(listener);}; },
|
|
21
24
|
async list(): Promise<AppRecord<T>[]> { return (await ask('GET')).records; },
|
|
22
25
|
async get(id: string): Promise<AppRecord<T> | null> { return (await ask('GET', id)).records[0] ?? null; },
|
|
23
26
|
create: (data: T, id = randomId()) => ask('POST', id, data),
|
|
24
27
|
update: (record: AppRecord<T>, data: T) => ask('PUT', record.id, data, record.version),
|
|
25
28
|
remove: (record: AppRecord<T>) => ask('DELETE', record.id, undefined, record.version),
|
|
26
|
-
dispose() { removeEventListener('message', receive); for (const p of pending.values()) { clearTimeout(p.timer); p.reject(new Error('disposed')); } pending.clear(); },
|
|
29
|
+
dispose() { listeners.clear(); removeEventListener('message', receive); for (const p of pending.values()) { clearTimeout(p.timer); p.reject(new Error('disposed')); } pending.clear(); },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Call the same trusted studio definition used by MCP. The shell supplies identity. */
|
|
34
|
+
export function createAppTools() {
|
|
35
|
+
const pending = new Map<string, {resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout>}>();
|
|
36
|
+
const receive = (e: MessageEvent) => {
|
|
37
|
+
if (e.source !== parent || e.data?.type !== 'homie-app-result') return;
|
|
38
|
+
const p = pending.get(e.data.id); if (!p) return;
|
|
39
|
+
pending.delete(e.data.id); clearTimeout(p.timer);
|
|
40
|
+
if (e.data.result?.ok) p.resolve(e.data.result.data); else p.reject(new Error(e.data.result?.error ?? 'tool failed'));
|
|
41
|
+
};
|
|
42
|
+
addEventListener('message', receive);
|
|
43
|
+
return {
|
|
44
|
+
call<T = unknown>(name: string, input: Record<string, unknown> = {}): Promise<T> {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const id = randomId();
|
|
47
|
+
const timer = setTimeout(() => { pending.delete(id); reject(new Error('app shell did not answer')); }, 15000);
|
|
48
|
+
pending.set(id, {resolve: value => resolve(value as T), reject, timer});
|
|
49
|
+
parent.postMessage({type:'homie-app-tool', id, name, input}, '*');
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
dispose() { removeEventListener('message', receive); for (const p of pending.values()) { clearTimeout(p.timer); p.reject(new Error('disposed')); } pending.clear(); }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AppContext { app: string; origin: string; url: string; presence: number; now: number | null }
|
|
57
|
+
/** Public self address and live presence supplied by the authenticated shell. */
|
|
58
|
+
export function createAppContext() {
|
|
59
|
+
let value: AppContext | null = null;
|
|
60
|
+
const listeners = new Set<(context: AppContext) => void>();
|
|
61
|
+
let resolve!: (context: AppContext) => void;
|
|
62
|
+
const ready = new Promise<AppContext>(r => {resolve = r;});
|
|
63
|
+
const receive = (e: MessageEvent) => {
|
|
64
|
+
if(e.source !== parent || e.data?.type !== 'homie-app-context') return;
|
|
65
|
+
value = Object.freeze(e.data.context) as AppContext;
|
|
66
|
+
resolve(value); for(const listener of listeners) listener(value);
|
|
67
|
+
};
|
|
68
|
+
addEventListener('message', receive);
|
|
69
|
+
parent.postMessage({type:'homie-app-context'}, '*');
|
|
70
|
+
parent.postMessage({type:'homie-app-subscribe',collections:[]}, '*');
|
|
71
|
+
return {
|
|
72
|
+
ready,
|
|
73
|
+
get current() {return value;},
|
|
74
|
+
now: () => value?.now ?? Date.now(),
|
|
75
|
+
subscribe(listener: (context: AppContext) => void) {listeners.add(listener); if(value)listener(value); return () => {listeners.delete(listener);};},
|
|
76
|
+
dispose() {removeEventListener('message',receive);listeners.clear();}
|
|
27
77
|
};
|
|
28
78
|
}
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
* AI guides, chat review and game decisions think with Clef on this computer
|
|
38
38
|
* when Ollama has clef-flash (free; dev never downloads it), else scripted;
|
|
39
39
|
* --remote-ai: the real Workers AI, billed)
|
|
40
|
-
* homie-studio check <id> [--url <site>] [--shots <dir>]
|
|
40
|
+
* homie-studio check <id> [--url <site>] [--shots <dir>] [--webgl-failure]
|
|
41
|
+
* homie-studio connector --tools <name,...> --out <private-config.json> [--days 30]
|
|
42
|
+
* homie-studio connector --revoke <id>
|
|
41
43
|
* homie-studio shoot <id> [--url <site> | --preview] [--frames 60] [--fps 30] [--device computer|phone]
|
|
42
44
|
* [--out <dir>] [--hold <key code>] [--no-smoke] [--timeout 180]
|
|
43
45
|
* pictures of the game on a clock this command steps (one frame = 1/fps s
|
|
@@ -380,14 +382,14 @@ import { standaloneDeployNotes } from '../lib/standalone.mjs';
|
|
|
380
382
|
const argv = process.argv.slice(2);
|
|
381
383
|
const flags = new Map();
|
|
382
384
|
// build --quick keeps compilation and type checks, skips rules generated play/smoke checks.
|
|
383
|
-
const BOOL_FLAGS = ['quick', 'public', 'lan', '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', 'giving', 'managed', 'live', 'manual', 'renew', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release', 'device'];
|
|
385
|
+
const BOOL_FLAGS = ['webgl-failure', 'quick', 'public', 'lan', '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', 'giving', 'managed', 'live', 'manual', 'renew', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release', 'device'];
|
|
384
386
|
const positional = [];
|
|
385
387
|
for (let i = 0; i < argv.length; i++) {
|
|
386
388
|
const a = argv[i];
|
|
387
389
|
if (a.startsWith('--')) {
|
|
388
390
|
const [k, v] = a.slice(2).split(/=(.*)/s, 2);
|
|
389
391
|
if (v !== undefined) flags.set(k, v);
|
|
390
|
-
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--') && (!BOOL_FLAGS.includes(k) || k === 'profile' && argv[0] === 'shop')) flags.set(k, argv[++i]);
|
|
392
|
+
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--') && (!BOOL_FLAGS.includes(k) || k === 'revoke' && argv[0] === 'connector' || k === 'profile' && argv[0] === 'shop')) flags.set(k, argv[++i]);
|
|
391
393
|
else flags.set(k, true);
|
|
392
394
|
} else positional.push(a);
|
|
393
395
|
}
|
|
@@ -407,6 +409,7 @@ function print(result) {
|
|
|
407
409
|
if (/^(style|assets|cast|anim)( |$)/.test(String(result.command ?? ''))) { process.stdout.write(`${artLines(result).join('\n')}\n`); return; }
|
|
408
410
|
if (/^collision /.test(String(result.command ?? '')) && result.lines) { process.stdout.write(`${result.lines.join('\n')}\n`); return; }
|
|
409
411
|
switch (result.command) {
|
|
412
|
+
case 'connector': lines.push(JSON.stringify(result,null,2)); break;
|
|
410
413
|
case 'new':
|
|
411
414
|
lines.push(`${result.name} is a studio now: ${result.dir}`, '', 'Wrote:', ...result.wrote.map((f) => ` ${f}`), '', `Dependencies: ${result.installed}`, '', 'Next:', ...result.next.map((n) => ` ${n}`), '', result.online);
|
|
412
415
|
break;
|
|
@@ -831,6 +834,7 @@ const OUTWARD_FLAGS = {
|
|
|
831
834
|
'setup attach': ['homie', 'client'],
|
|
832
835
|
handoff: ['homie', 'client'],
|
|
833
836
|
'players owner': ['url', 'revoke'],
|
|
837
|
+
'connector': ['url','tools','out','days','revoke'],
|
|
834
838
|
'stats key': ['url', 'hours'], 'stats link': ['url'], 'stats revoke': ['url'], 'stats share': [],
|
|
835
839
|
'office link': ['url', 'to'], 'office key': ['url', 'hours'], 'office announce': ['url', 'game', 'room', 'seconds'],
|
|
836
840
|
'office invite': ['url', 'label', 'uses', 'count', 'days', 'server'], 'office launch': ['url', 'max'],
|
|
@@ -914,6 +918,7 @@ async function main() {
|
|
|
914
918
|
}
|
|
915
919
|
if (cmd === 'events') return (await import('../lib/events-cli.mjs')).eventsCommand(root, sub, positional, flags, { log });
|
|
916
920
|
if (cmd === 'function' || cmd === 'functions') return (await import('../lib/functions-cli.mjs')).functionCommand(root, sub, positional, flags, { log });
|
|
921
|
+
if (cmd === 'connector') return (await import('../lib/connector-token.mjs')).connectorToken(root,{url:flags.get('url'),tools:typeof flags.get('tools')==='string'?flags.get('tools').split(','):[],out:flags.get('out'),days:flags.has('days')?Number(flags.get('days')):30,revoke:flags.get('revoke')});
|
|
917
922
|
if (cmd === 'tool') return (await import('../lib/tools-cli.mjs')).toolCommand(root, sub, positional, flags, { log });
|
|
918
923
|
// A trailer is the plugin's video skill; this hands the words after the id over to it (lib/trailer.mjs).
|
|
919
924
|
if (cmd === 'trailer') return trailerCommand(root, sub, argv.slice(argv.indexOf(sub) + 1), { skills: flags.get('skills') ?? null, slug: flags.get('slug') ?? null });
|
|
@@ -995,7 +1000,7 @@ async function main() {
|
|
|
995
1000
|
const results = [];
|
|
996
1001
|
for (const game of games) {
|
|
997
1002
|
const shots = flags.get('shots') ? resolve(flags.get('shots'), ...(games.length > 1 ? [game] : [])) : null;
|
|
998
|
-
const r = await tracked(root, 'checks', (report) => check({ root, url: site.url, game, shots, log, report, isolate:!flags.has('no-isolate') }), 'check');
|
|
1003
|
+
const r = await tracked(root, 'checks', (report) => check({ webglFailure:flags.has('webgl-failure'), root, url: site.url, game, shots, log, report, isolate:!flags.has('no-isolate') }), 'check');
|
|
999
1004
|
results.push({ ...r, game, site: site.url, local: site.local });
|
|
1000
1005
|
const receiptDir = join(root, '.studio', 'checks');
|
|
1001
1006
|
mkdirSync(receiptDir, { recursive: true });
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Building places: layout, authority and recovery
|
|
2
|
+
|
|
3
|
+
Start with [the six-build place recipe](../guides/app/PLACES.md) and
|
|
4
|
+
[the app contracts](../apps/APPS.md) for screen-to-tool calls, live records,
|
|
5
|
+
shared booking holds, named events, IANA time and adversarial needs. The small
|
|
6
|
+
CAS example below teaches authority; paid inventory uses `context.bookings` so
|
|
7
|
+
checkout and app capacity remain one system.
|
|
8
|
+
|
|
9
|
+
## TV join safe area
|
|
10
|
+
|
|
11
|
+
On `/<id>/tv`, the shell sets `body.homie-join-visible` while the join card is present. It publishes CSS pixel values on the document root in both the shell and the sandboxed app: `--homie-join-x`, `--homie-join-y`, `--homie-join-width`, `--homie-join-height`, and `--homie-join-inset-top/right/bottom/left`. The insets include the card's distance from its configured screen edge. Inactive edges are zero; no card means all values are zero and the body class is absent. Measurements update on resize and iframe navigation. The card is at most 480 × 280 CSS pixels; its address wraps within that box.
|
|
12
|
+
|
|
13
|
+
Reserve an entire strip on either occupied axis, with a gap:
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
.status { position: fixed; right: 20px;
|
|
17
|
+
bottom: calc(var(--homie-join-inset-bottom, 0px) + 20px); }
|
|
18
|
+
.tee-sheet { position: fixed; left: 20px; bottom: 20px;
|
|
19
|
+
right: calc(var(--homie-join-inset-right, 0px) + 20px); }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For top/left join corners, use the corresponding top/left insets. Never position critical controls underneath the card merely because it ignores pointer events. Regression coverage measures actual rectangles at 1920×1080, 1280×720 and 3840×2160.
|
|
23
|
+
|
|
24
|
+
## Authoritative capacity
|
|
25
|
+
|
|
26
|
+
Bookings and inventory belong to server-side `defineTool` mutations, with strict schemas (`additionalProperties: false`) and atomic versioned writes. A client-writable generic ledger is not an inventory authority: six browsers can all read one remaining place and overwrite one another. Clients may display availability and request a reservation; they cannot supply remaining capacity, identity or a new inventory total.
|
|
27
|
+
|
|
28
|
+
The executable [reservation example](../guides/app/examples/reserve.mjs) accepts only a slot ID. An owner-only action seeds `ctx.database.put('tee-0900', {remaining: 1, bookings: []}, {version: 0})`. `reserve` reads that row, derives the booking identity from `ctx.caller`, and writes with `{version: row.version}`. One SQLite statement checks the version and updates the entire row; a conflict is a failed reservation, never a success. Six simultaneous requests produce one winner and five conflicts. Do not expose this namespace through a generic client record mutation. Read availability through a separate read-only action. For retryable purchases add a server-owned idempotency key and settlement handling before charging.
|
|
29
|
+
|
|
30
|
+
## Text first, graphics optional
|
|
31
|
+
|
|
32
|
+
Bind navigation and text controls before creating a renderer. A failed WebGL context must not prevent those handlers from being installed:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
renderTextControls();
|
|
36
|
+
bindNavigation();
|
|
37
|
+
try {
|
|
38
|
+
const renderer = new THREE.WebGLRenderer({canvas});
|
|
39
|
+
startScene(renderer);
|
|
40
|
+
} catch {
|
|
41
|
+
canvas.hidden = true;
|
|
42
|
+
fallback.hidden = false; // a normal-flow status inside the scene section
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Keep the fallback inside the scene section, outside navigation; never use a fullscreen or fixed overlay. The shell's automatic fallback is a normal-flow status beside the canvas and does not intercept input. Author a Codex need that clicks navigation and asserts the resulting text; then run `homie-studio check <id> --webgl-failure --shots .checks/no-webgl`. This disables WebGL context creation before scripts run in every app frame and executes the same UI needs. A passing rendering check alone does not establish usable text controls.
|
|
47
|
+
|
|
48
|
+
## Action diagnostics and types
|
|
49
|
+
|
|
50
|
+
`check` tests whether the action target is covered before dispatch. An obstruction reports the surface, target selector and covering element immediately; remove the overlay or repair its layout before retrying. Navigation/detachment handling does not replay an ambiguous dispatched action.
|
|
51
|
+
|
|
52
|
+
`build --types` checks imported toolkit sources too. Collision/terrain closure narrowing and tile-coordinate narrowing are fixed in the toolkit; fresh studios need no local suppressions or edits to `node_modules`.
|
|
53
|
+
|
|
54
|
+
## Connect another studio without another approval page
|
|
55
|
+
|
|
56
|
+
Use the new studio's directory and the owner's existing Wrangler login (the same Cloudflare account must have D1 access). One command writes a connector config, never its bearer to stdout:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
homie-studio connector --tools studio_stats --out /private/path/studio-connector.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Load that file directly into the AI client's connector configuration using its local configuration/import mechanism. Do not paste, print or include the file in chat or commit it. The file is created exclusively with mode 0600; existing files are never overwritten. Only the file path, token ID, scopes and expiry are reported. The config targets this studio's `/mcp/auth` endpoint. No browser approval is needed when Wrangler is already authenticated; this command does not deploy anything.
|
|
63
|
+
|
|
64
|
+
Tokens are bound to this origin, limited to the exact named tools, expire after 30 days by default (`--days 1` through `--days 90`), and cannot read MCP resources or subscriptions. D1 stores only the token hash and its scope metadata. Revoke with `homie-studio connector --revoke <id>` from the same studio. Every dispatch rechecks expiry and revocation. A login to a different studio alone does not grant authority over this database.
|
|
65
|
+
|
|
66
|
+
## Deployment
|
|
67
|
+
|
|
68
|
+
Cloudflare's official MCP request interface cannot authorize new static-asset uploads with an upload JWT; deploy with Wrangler, Cloudflare's own CLI using browser OAuth.
|
package/functions/FUNCTIONS.md
CHANGED
|
@@ -383,3 +383,30 @@ A booking payment can be tested with:
|
|
|
383
383
|
Create any records the handler reads first. First fire registers subscriptions
|
|
384
384
|
before writing the event, so the first event is delivered too. No Stripe checkout
|
|
385
385
|
or browser approval is needed for this explicitly synthetic local event.
|
|
386
|
+
|
|
387
|
+
## Previous records and named business events
|
|
388
|
+
|
|
389
|
+
`record.changed` data includes `previous`: `null` for insert, the old data for
|
|
390
|
+
update/delete. `data` remains the new value for update and the deleted value for
|
|
391
|
+
delete. Both are captured by the same database transaction as the write. Import
|
|
392
|
+
`RecordChanged<T>` and `ScheduledEvent` from `@homie-rocks/studio/functions`;
|
|
393
|
+
scheduled time is `event.data.scheduledTime` (epoch milliseconds), not `event.at`.
|
|
394
|
+
|
|
395
|
+
Declare a feed in `studio.json` and its allowed payload:
|
|
396
|
+
|
|
397
|
+
```json
|
|
398
|
+
{"mcp":{"events":{"arrivals":{"event":"place.arrived","audience":"owner","schema":{"type":"object","properties":{"booking":{"type":"string"}},"required":["booking"],"additionalProperties":false}}}}}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
A function declares `publish: ['arrivals']`, then calls
|
|
402
|
+
`await context.events.publish({feed:'arrivals', id:event.id, data:{booking:'...'}})`.
|
|
403
|
+
Use the source event ID, or a stable business request ID; repeats with the same
|
|
404
|
+
payload are no-ops, and changed payloads under the same ID are refused. Publication
|
|
405
|
+
and its deduplication receipt commit atomically. Receipts survive feed pruning.
|
|
406
|
+
Do not mirror every record or build a sleep-and-retry log. Keep contact details
|
|
407
|
+
out of operational events. Owner AIs use the existing feed cursor, MCP subscription
|
|
408
|
+
or declared webhook; functions never send email/SMS themselves.
|
|
409
|
+
|
|
410
|
+
Apply `0028_business_events.sql` on upgrade. Local proof publications carry their
|
|
411
|
+
scope and do not appear in production feed reads. Event feeds retain their declared
|
|
412
|
+
type; give different business feeds distinct types when their content differs.
|
package/functions/functions.ts
CHANGED
|
@@ -8,6 +8,10 @@ export interface StudioFunction<Data = unknown> {
|
|
|
8
8
|
/** Permit an explicit owner replay command to consume retained history. */
|
|
9
9
|
replay?: boolean;
|
|
10
10
|
events?: StudioTool['events'];
|
|
11
|
+
publish?: string[];
|
|
11
12
|
handler(event: StudioEvent<Data>, context: ToolContext): unknown | Promise<unknown>;
|
|
12
13
|
}
|
|
13
14
|
export function defineFunction<Data = unknown>(definition: StudioFunction<Data>): StudioFunction<Data> { return definition; }
|
|
15
|
+
|
|
16
|
+
export interface RecordChanged<T = Record<string, unknown>> { app: string; collection: string; record: string; version: number; operation: "insert" | "update" | "delete"; data: T; previous: T | null }
|
|
17
|
+
export interface ScheduledEvent { function: string; cron: string; scheduledTime: number }
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Build a place people recognize
|
|
2
|
+
|
|
3
|
+
Use this with APPS.md and TOOLS.md. Six place builds exposed the same rule:
|
|
4
|
+
make the owner's dream visible, with one trusted operation behind every action.
|
|
5
|
+
|
|
6
|
+
1. **Start with their paragraph.** Quote the owner's actual words in CODEX.md,
|
|
7
|
+
including sensory details and operating rules. Write one executable need per
|
|
8
|
+
clause. Describe Scene as data: “held = white ball; confirmed = gold; mine =
|
|
9
|
+
gold ring; empty capacity = empty pegs.” List phone, wall, signed-in staff,
|
|
10
|
+
customer AI and owner AI with their addresses and actions. Record what is
|
|
11
|
+
simulated, evidence viewports and who reviewed it. Local review is not independent.
|
|
12
|
+
2. **Make records visible.** Give each meaningful record an object, each state a
|
|
13
|
+
material, and the visitor's object a clear marker. Arrivals pop and chime;
|
|
14
|
+
outcomes become bags, cups or flags. Empty capacity can be negative space.
|
|
15
|
+
3. **Spend geometry where it reads.** Use one layout curve for paths, trees and
|
|
16
|
+
objects. Add a gradient sky shader and sun, fog matching the horizon, terrain
|
|
17
|
+
vertex colours by zone, and seeded low-poly local vegetation. Animate water
|
|
18
|
+
and a flag in vertex shaders; avoid recomputing a large mesh's normals every
|
|
19
|
+
frame. Ease role-specific cameras with `1 - Math.exp(-dt * 4)`. Cap pixel
|
|
20
|
+
ratio on phones, pause hidden frames, respect reduced motion. Generate a
|
|
21
|
+
filtered noise ambient bed after the first sound opt-in; its gain follows the
|
|
22
|
+
scene. Compose a share card from the live scene and actual outcome.
|
|
23
|
+
4. **One set of rules.** Put validation and transitions in `domain.ts`; the view
|
|
24
|
+
may preview them, but a `defineTool` handler decides and saves. Screens call
|
|
25
|
+
`createAppTools().call`, AIs call the same definition over MCP. Use strict
|
|
26
|
+
schemas, bounded inputs, versioned writes with revalidation on conflict, and
|
|
27
|
+
a caller-generated request ID. A repeated ID with different data is refused.
|
|
28
|
+
Do not rebuild the operation with browser record writes or a custom route.
|
|
29
|
+
5. **Plan the AI's whole visit.** Provide `place_info` (“Call this first”: rules,
|
|
30
|
+
availability, prices, notice), `place_book` (all choices in one call, bounded
|
|
31
|
+
enums from config, a private management credential and a `next` sentence),
|
|
32
|
+
and `place_booking` (owner-authorized lookup/change/cancel). Never expose
|
|
33
|
+
management credentials in public records. Owner tools read an outbox, mark
|
|
34
|
+
messages sent, consume event feeds, and repeat next month by a calendar rule.
|
|
35
|
+
Homie composes; the owner's AI sends through its own connector. A repeat
|
|
36
|
+
invites past visitors; it never silently enrols them.
|
|
37
|
+
6. **Operate from one config.** Put the place's IANA zone, schedule rule, capacity,
|
|
38
|
+
prices, add-ons and notice in one config. Derive UI, tools and `shop.json` from
|
|
39
|
+
it during build. Do not maintain a second price list. Model holds → confirm →
|
|
40
|
+
scheduled release, with fixed spot IDs and version checks on reuse. Group
|
|
41
|
+
allocation must be atomic, never a loop that reports partial success. Use the
|
|
42
|
+
toolkit's booking capacity for paid inventory. HANDOVER.md says what runs
|
|
43
|
+
where, the monthly rhythm, how to connect an AI, what to do when the owner
|
|
44
|
+
leaves, and which integrations are still unproven.
|
|
45
|
+
|
|
46
|
+
## Small working boundaries
|
|
47
|
+
|
|
48
|
+
The examples use the installed toolkit's exports. Declare `reserve` in `tools/`;
|
|
49
|
+
this small example reserves one whole slot for a signed-in account. Keep the
|
|
50
|
+
public collection a projection without account IDs or secrets. A production
|
|
51
|
+
paid slot uses the shop booking system, not a second ledger.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { defineTool } from '@homie-rocks/studio/tools';
|
|
55
|
+
export default defineTool<{ slot: string; request: string }>({
|
|
56
|
+
name: 'reserve', description: 'Reserve one slot; repeat the same request safely.',
|
|
57
|
+
audience: 'signed-in',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object', additionalProperties: false, required: ['slot', 'request'],
|
|
60
|
+
properties: {
|
|
61
|
+
slot: { type: 'string', enum: ['morning', 'afternoon'] },
|
|
62
|
+
request: { type: 'string', pattern: '^[a-f0-9]{32}$' }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async handler(input, ctx) {
|
|
66
|
+
const owner = ctx.caller.id;
|
|
67
|
+
const key = 'slot:' + input.slot;
|
|
68
|
+
const old = await ctx.database.getVersioned(key);
|
|
69
|
+
if (old) {
|
|
70
|
+
const saved = old.value as { owner: string; request: string };
|
|
71
|
+
if (saved.owner === owner && saved.request === input.request) return { booked: true };
|
|
72
|
+
return { booked: false, next: 'That slot is full. No place has been taken.' };
|
|
73
|
+
}
|
|
74
|
+
const result = await ctx.database.put(key, { owner, request: input.request }, { version: 0 });
|
|
75
|
+
return { booked: result.ok, next: result.ok ? 'Your place is reserved.' : 'That slot filled. Choose another.' };
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createAppTools, createAppContext, createAppRecords } from '@homie-rocks/studio/apps';
|
|
82
|
+
const tools = createAppTools();
|
|
83
|
+
const context = createAppContext();
|
|
84
|
+
const self = await context.ready; // self.url is public; it contains no private access link
|
|
85
|
+
const records = createAppRecords<{ status: string }>({ collection: 'spots' });
|
|
86
|
+
const refresh = async () => {
|
|
87
|
+
const rows = await records.list();
|
|
88
|
+
document.querySelector('output')!.textContent = `${rows.length} places · ${self.url}`;
|
|
89
|
+
};
|
|
90
|
+
const stop = records.subscribe(() => { void refresh(); });
|
|
91
|
+
await refresh();
|
|
92
|
+
// In the visitor's click handler, preserve request across network retries:
|
|
93
|
+
const request = crypto.randomUUID().replaceAll('-', '');
|
|
94
|
+
await tools.call('reserve', { slot: 'morning', request });
|
|
95
|
+
// On unmount: stop(); records.dispose(); context.dispose(); tools.dispose();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Build the controls first: an area menu is the primary navigation, and tapping a
|
|
99
|
+
3D object selects the same area. Catch renderer creation failure and context loss;
|
|
100
|
+
keep every booking/control usable without WebGL. Refusals are human sentences
|
|
101
|
+
that say what happened. Prove refusals by bypassing disabled controls, race two
|
|
102
|
+
browsers for the last seat, reload and restart, and run with WebGL unavailable.
|
|
103
|
+
Document the exact commands and results; an empty assertion list is not proof.
|
|
104
|
+
|
|
105
|
+
## Do not teach the mistakes
|
|
106
|
+
|
|
107
|
+
- No booking codes in public collections or anonymous edits of arbitrary bookings.
|
|
108
|
+
Bind tickets to their account/seat, or verify a secret only the booker holds.
|
|
109
|
+
- No public staff console. Use a signed-in role and check it on the server.
|
|
110
|
+
Label an intentionally public simulator in both UI and Codex.
|
|
111
|
+
- Never interpolate visitor text into `innerHTML`: a stored XSS reached a staff
|
|
112
|
+
tablet in the study. Use `textContent` for names, contacts, notices and outbox text.
|
|
113
|
+
- Never issue an “official receipt” for an unpaid donation. Record a pledge as a
|
|
114
|
+
pledge; count a paid gift only after a verified, deduplicated `order.paid` event,
|
|
115
|
+
with the gift's ID as its reference.
|
|
116
|
+
- No fixed UTC offsets or one hard-coded event date. Use IANA zones and recurring
|
|
117
|
+
calendar rules; carry the same occurrence into site copy and all screens.
|
|
118
|
+
- No sleep-and-retry event arrays or staff-device cron jobs. Use durable functions,
|
|
119
|
+
version checks, declared feeds and the server scheduler.
|
|
120
|
+
- No live-only assets. Commit every asset the site serves, with its provenance,
|
|
121
|
+
and verify a clean checkout can build it. Declare only packages actually used.
|
|
122
|
+
- No invented reviews, impact figures or verification. Label estimates and test
|
|
123
|
+
payments honestly; remove stale workaround claims when upgrading.
|
package/guides/app/SKILL.md
CHANGED
|
@@ -6,6 +6,9 @@ compatibility: Node 22 and Chrome; the studio's pinned toolkit and Wrangler.
|
|
|
6
6
|
|
|
7
7
|
# Make an app
|
|
8
8
|
|
|
9
|
+
For place-based builds, read [the six-build patterns](../app/PLACES.md): owner words to needs, records as objects, a local scene and ambience, one trusted operation for screens and AI, graceful graphics, and adversarial proof. Apply the same authority and honest-evidence rules to game economies and tickets.
|
|
10
|
+
|
|
11
|
+
|
|
9
12
|
For the installed version’s payment, records and local-check contracts, start with
|
|
10
13
|
`node_modules/@homie-rocks/studio/apps/APPS.md`. Its “Real Stripe test checkout on
|
|
11
14
|
this computer” section is the supported offline-hosting path: real Stripe test
|
|
@@ -354,3 +357,11 @@ and pass a role when deliberately testing the customer experience.
|
|
|
354
357
|
`check --shots` may capture outside the studio, but persistent needs evidence copies
|
|
355
358
|
those pictures into `.studio/checks` and stores studio-relative paths. Keep that
|
|
356
359
|
folder with the Codex evidence. Deploy can render it without outside-path warnings.
|
|
360
|
+
|
|
361
|
+
### Places: capacity, TV space and failed graphics
|
|
362
|
+
|
|
363
|
+
Authoritative bookings/inventory are server-side `defineTool` actions with strict schemas and atomic versioned writes, never a client-writable generic ledger. Use [the capacity example](examples/reserve.mjs): seed one place through an owner action; six simultaneous reservation requests must yield exactly one winner. Derive identity from the caller; expose neither inventory totals nor the authoritative namespace as client mutations.
|
|
364
|
+
|
|
365
|
+
Initialise text controls and navigation before 3D rendering. Catch renderer creation failures inside the scene section; keep fallback text in normal flow, never over navigation. Add a navigation need and run `homie-studio check <id> --webgl-failure --shots .checks/no-webgl` to prove the controls work without WebGL.
|
|
366
|
+
|
|
367
|
+
On TV, reserve `--homie-join-inset-bottom/right` (or the configured corner's top/left) plus a gap; `body.homie-join-visible` and `--homie-join-x/y/width/height` describe the shell card in the app frame too. See `node_modules/@homie-rocks/studio/docs/toolkit-place-patterns.md` for layout and text-first examples, connector setup and diagnostics.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {defineTool} from '@homie-rocks/studio/tools';
|
|
2
|
+
export default defineTool({
|
|
3
|
+
name:'reserve',description:'Reserve one place in a server-owned slot',audience:'signed-in',namespace:'capacity',
|
|
4
|
+
inputSchema:{type:'object',properties:{slot:{type:'string',pattern:'^tee-[0-9]{4}$'}},required:['slot'],additionalProperties:false},
|
|
5
|
+
async handler({slot},ctx){
|
|
6
|
+
const row=await ctx.database.getVersioned(slot);
|
|
7
|
+
if(!row)return {ok:false,reason:'Unknown slot'};
|
|
8
|
+
const value=row.value;
|
|
9
|
+
if(value.remaining<1)return {ok:false,reason:'Full'};
|
|
10
|
+
const result=await ctx.database.put(slot,{remaining:value.remaining-1,bookings:[...value.bookings,ctx.caller.id]},{version:row.version});
|
|
11
|
+
return result.ok?{ok:true}:{ok:false,reason:'Taken; refresh availability'};
|
|
12
|
+
}
|
|
13
|
+
});
|
|
@@ -28,3 +28,5 @@ isolated records. Assert actual sound/haptic/notification triggers where request
|
|
|
28
28
|
Check waits through reload readiness and reports dialogs and named timeout conditions.
|
|
29
29
|
Do not weaken a need to obtain a green result; inspect captures and complete the
|
|
30
30
|
owner's workflow. Do not deploy merely to prove a local handler.
|
|
31
|
+
|
|
32
|
+
For authoritative bookings/inventory use strict `defineTool` actions and `database.put(key, next, {version: row.version})`; a conflict is a failed reservation. Never make the inventory a client-writable generic ledger. The [capacity example](../app/examples/reserve.mjs) proves one winner from six simultaneous requests for one place; initialise capacity through an owner-only action.
|
package/guides/game/SKILL.md
CHANGED
|
@@ -25,6 +25,9 @@ cover, doors and platforms, use declared entity `collider` state and movement's
|
|
|
25
25
|
|
|
26
26
|
# Make a game
|
|
27
27
|
|
|
28
|
+
For place-based builds, read [the six-build patterns](../app/PLACES.md): owner words to needs, records as objects, a local scene and ambience, one trusted operation for screens and AI, graceful graphics, and adversarial proof. Apply the same authority and honest-evidence rules to game economies and tickets.
|
|
29
|
+
|
|
30
|
+
|
|
28
31
|
You are in a Homie studio when the folder (or one above it) has `studio.json`. If there
|
|
29
32
|
is none, use the `studio-setup` skill first.
|
|
30
33
|
|