@homie-rocks/studio 0.51.8 → 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 +32 -0
- package/apps/APPS.md +117 -0
- package/apps/apps.ts +51 -1
- package/docs/toolkit-place-patterns.md +6 -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 +3 -0
- package/guides/game/SKILL.md +3 -0
- package/guides/shop/SKILL.md +27 -12
- package/lib/app-needs.mjs +41 -6
- package/lib/build.mjs +5 -0
- package/lib/check.mjs +6 -6
- package/lib/cloudflare.mjs +2 -2
- 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 +42 -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/package.json +1 -1
- package/shop/SHOP.md +27 -12
- package/shop/shop.ts +1 -1
- package/tools/tools.ts +12 -0
- package/worker/app-records.mjs +5 -1
- package/worker/app-shell.mjs +27 -4
- 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/event-resources.mjs +1 -1
- package/worker/functions.mjs +5 -0
- package/worker/index.mjs +19 -4
- package/worker/mcp-events.mjs +2 -2
- package/worker/mcp-tools.mjs +3 -0
- package/worker/mcp.mjs +3 -3
- package/worker/pages.mjs +4 -4
- package/worker/records-page.mjs +2 -2
- 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,38 @@ 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
|
+
|
|
20
52
|
## [0.51.8] - 2026-10-15
|
|
21
53
|
|
|
22
54
|
**Plugin 0.51.8**
|
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
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Building places: layout, authority and recovery
|
|
2
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
|
+
|
|
3
9
|
## TV join safe area
|
|
4
10
|
|
|
5
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.
|
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
|
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
|
|
package/guides/shop/SKILL.md
CHANGED
|
@@ -49,27 +49,34 @@ calling it; tool argument shapes can change. See https://docs.stripe.com/mcp.
|
|
|
49
49
|
3. Append `{ "method": "POST", "path": "…", "params": {…}, "body": <Stripe response> }`
|
|
50
50
|
for **writes as well as reads**. Keep this setup's observations across batches.
|
|
51
51
|
Product creation includes its default Price; link creation expands line items.
|
|
52
|
-
Product and Price Search use studio/origin metadata.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
Product and Price Search use studio/origin metadata. Payment Links use paginated
|
|
53
|
+
list reads with `expand[]=data.line_items`, then filter studio/origin metadata locally;
|
|
54
|
+
Stripe has no Payment Link metadata search. There is no per-link read loop.
|
|
55
|
+
The planner saves public observations in `.studio/stripe-mcp-observations.json` and
|
|
56
|
+
reuses them on the next call; send only new responses. Keep that file and
|
|
57
|
+
`.studio/stripe-mcp.json` when moving computers. Changed shop/slot settings refresh
|
|
58
|
+
mutable links while retaining product/price discovery. Account, mode and origin
|
|
59
|
+
changes start separate reads. Pass `refresh: true` for Dashboard changes or a fresh
|
|
60
|
+
verification before webhook installation (CLI: include it in `--have` input).
|
|
61
|
+
Follow search `next_page` and list `starting_after` cursors. On an uncertain write,
|
|
62
|
+
retry its exact idempotency key; never discard successful responses or invent IDs.
|
|
61
63
|
4. Call `stripe_shop_plan` with `verify: true` using those observations (CLI
|
|
62
64
|
`homie-studio shop verify --have plan-input.json`). Verification checks real fields,
|
|
63
65
|
line items, menus, slot limits and portal settings. It saves compact public Worker
|
|
64
66
|
configuration and provenance to `.studio/stripe-mcp.json`, never API response history.
|
|
65
|
-
|
|
67
|
+
Build writes public links to `site/dist/_site/stripe-shop-links.json`; deploy ships
|
|
68
|
+
that asset, with only its path in Wrangler vars. Large catalogues never use a text
|
|
69
|
+
binding. Deploy after verifying, including when setup began on an already-live site. Checkout then reports only `webhook-secret` missing;
|
|
66
70
|
it cannot open until signed payment events can confirm orders. Changing shop.json,
|
|
67
71
|
the slot manifest or origin requires verification again. Local test configuration
|
|
68
72
|
goes to `.dev.vars`.
|
|
69
73
|
5. For a deployed HTTPS studio, run `stripe_webhook` (CLI `homie-studio shop webhook`).
|
|
70
74
|
Follow `studio_job`. This uses Stripe CLI only for private webhook installation,
|
|
71
|
-
checks the same account as the MCP, and installs the signing secret
|
|
72
|
-
|
|
75
|
+
checks the same account as the MCP, and installs only the signing secret through
|
|
76
|
+
Wrangler stdin. If Stripe CLI is missing, the tool installs the official
|
|
77
|
+
`@stripe/cli@latest` npm package automatically, checks the executable, then continues.
|
|
78
|
+
An installation failure reports npm permissions/PATH and a retry command. The CLI
|
|
79
|
+
needs one separate browser approval if its
|
|
73
80
|
studio profile is not already connected. MCP approval cannot be reused or exported.
|
|
74
81
|
Cloudflare authorization and an existing deployment are prerequisites.
|
|
75
82
|
6. After installation, perform the returned `retire` operations through Stripe MCP to
|
|
@@ -80,6 +87,14 @@ calling it; tool argument shapes can change. See https://docs.stripe.com/mcp.
|
|
|
80
87
|
endpoint's delivery result and the app's verified records. Do not type a card into
|
|
81
88
|
a page for an automated proof; use Stripe's documented test PaymentMethods/fixtures.
|
|
82
89
|
|
|
90
|
+
Upgrades accept 0.51.6 verifications without a source fingerprint only when their
|
|
91
|
+
saved content fingerprint still matches the current shop, slots, site and mode.
|
|
92
|
+
Actual changes name the affected settings and require verification. An unfinished
|
|
93
|
+
setup still builds and deploys with checkout closed and one next step: finish
|
|
94
|
+
`stripe_shop_plan` with `verify: true`. Missing/unreadable assets also close checkout;
|
|
95
|
+
old link secrets cannot reopen it. Cached observations never renew the verification
|
|
96
|
+
time; use fresh reads if the private webhook tool requests them.
|
|
97
|
+
|
|
83
98
|
Never create a webhook through model-visible Stripe MCP: its response exposes the signing secret.
|
|
84
99
|
|
|
85
100
|
**Why the separate approval (checked 2026-10-14).** Stripe's
|
package/lib/app-needs.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export function readAppNeeds(root, id) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Run real UI actions. A failed or absent flow stays visible; continue collecting other needs. */
|
|
26
|
-
export async function proveAppNeeds({ plan, surfaces, shots, report, ready = async () => {}, root, headless }) {
|
|
26
|
+
export async function proveAppNeeds({ plan, surfaces, shots, report, ready = async () => {}, root, headless, restart }) {
|
|
27
27
|
const results = [];
|
|
28
28
|
const appBarrier=ready;
|
|
29
29
|
ready=async (s,options)=>{await s.page.bringToFront?.();if(s.external){s.frame=s.page.mainFrame();return;}await appBarrier(s,options);};
|
|
@@ -31,7 +31,7 @@ export async function proveAppNeeds({ plan, surfaces, shots, report, ready = asy
|
|
|
31
31
|
const surface = name => { const s = surfaces.find(s => s.name === name && (!activeRole || s.role === activeRole)) ?? surfaces.find(s => s.name === name); if (!s) throw new Error(`unknown surface ${name}`); return s; };
|
|
32
32
|
for (const need of plan.needs) {
|
|
33
33
|
activeRole = need.role;
|
|
34
|
-
const names = [...new Set([...(need.steps
|
|
34
|
+
const names = [...new Set([...needSteps(need.steps), ...(need.assertions ?? [])].map(s => s.surface).filter(Boolean))];
|
|
35
35
|
const saved = new Map();
|
|
36
36
|
const run = async item => { if (!headless) throw new Error('headless MCP proof requires local dev and its test credential'); return headless(item,item.role ?? need.role); };
|
|
37
37
|
let waiting = 'validating assertions';
|
|
@@ -41,19 +41,34 @@ export async function proveAppNeeds({ plan, surfaces, shots, report, ready = asy
|
|
|
41
41
|
const popupNames=new Set((need.steps??[]).map(s=>s.popup).filter(Boolean));
|
|
42
42
|
for (const name of names) { if(popupNames.has(name))continue; const s = surface(name); if (need.role && s.role !== need.role) throw new Error(`${name} requires role ${need.role}, got ${s.role ?? 'unspecified'}`); }
|
|
43
43
|
for (const name of new Set(need.assertions.filter(a => a.effect).map(a => a.surface))) await surface(name).frame.evaluate(() => { window.__homieNeedsEffects = []; });
|
|
44
|
-
|
|
44
|
+
const perform = async rawStep => {
|
|
45
45
|
const step=resolveNeedResults(rawStep,saved);
|
|
46
|
+
if(step.concurrent){
|
|
47
|
+
if(!Array.isArray(step.concurrent)||step.concurrent.length<2)throw new Error('concurrent needs at least two steps');
|
|
48
|
+
if(needSteps(step.concurrent).some(s=>s.restart||s.capabilities))throw new Error('Restart and capabilities changes must be outside a race');
|
|
49
|
+
const results=await Promise.allSettled(step.concurrent.map(perform));
|
|
50
|
+
const failed=results.find(r=>r.status==='rejected');if(failed)throw failed.reason;return;
|
|
51
|
+
}
|
|
52
|
+
if(step.restart){if(!restart)throw new Error('Restart requires the local dev controller');await restart();for(const s of surfaces)await ready(s);return;}
|
|
53
|
+
|
|
46
54
|
if (step.mcp || step.schedule) {
|
|
47
55
|
waiting = step.mcp ? `MCP ${step.mcp.tool ?? step.mcp.method} as ${step.role ?? need.role ?? 'unspecified'}` : `schedule ${step.schedule.name}`;
|
|
48
56
|
const value = await run(step);
|
|
49
57
|
if (step.save) { if(saved.has(step.save))throw new Error(`duplicate saved result ${step.save}`); saved.set(step.save,value); }
|
|
50
|
-
|
|
58
|
+
return;
|
|
51
59
|
}
|
|
52
60
|
const target=surface(step.surface);
|
|
53
61
|
if(!step.reload)await ready(target);
|
|
54
62
|
const { page } = target;
|
|
63
|
+
if(step.capabilities){
|
|
64
|
+
if(typeof step.capabilities.webgl!=='boolean')throw new Error('capabilities.webgl must be boolean');
|
|
65
|
+
await page.evaluateOnNewDocument(configureWebGL,step.capabilities.webgl);
|
|
66
|
+
await page.reload({waitUntil:'domcontentloaded'});await ready(target);return;
|
|
67
|
+
}
|
|
68
|
+
if(Object.hasOwn(step,'clock')){await page.evaluate(value=>window.homieApp.setTestClock(value),step.clock);return;}
|
|
69
|
+
|
|
55
70
|
waiting = `${step.surface}: ${step.reload ? 'reload and arrival readiness' : step.selector}`;
|
|
56
|
-
if (step.reload) { await page.reload({ waitUntil: 'domcontentloaded' }); await ready(surface(step.surface));
|
|
71
|
+
if (step.reload) { await page.reload({ waitUntil: 'domcontentloaded' }); await ready(surface(step.surface)); return; }
|
|
57
72
|
const act = fn => actInApp(target,step.selector,fn,{ready});
|
|
58
73
|
if (step.click && step.popup) {
|
|
59
74
|
let listener,timer;
|
|
@@ -67,11 +82,15 @@ export async function proveAppNeeds({ plan, surfaces, shots, report, ready = asy
|
|
|
67
82
|
await page.bringToFront();
|
|
68
83
|
} finally {clearTimeout(timer);page.off('popup',listener);}
|
|
69
84
|
}
|
|
85
|
+
else if(step.forceClick){await act(frame=>frame.$eval(step.selector,el=>{
|
|
86
|
+
const disabled=el.disabled;try{el.disabled=false;el.click();}finally{el.disabled=disabled;}
|
|
87
|
+
}));}
|
|
70
88
|
else if (step.click) {await act(frame=>frame.click(step.selector));await page.bringToFront?.();}
|
|
71
89
|
else if (step.fill !== undefined) { await act(async frame=>{ await frame.$eval(step.selector, el => { el.value = ''; el.dispatchEvent(new Event('input', { bubbles: true })); }); await frame.type(step.selector, String(step.fill)); }); }
|
|
72
90
|
else if (step.select !== undefined) await act(frame=>frame.select(step.selector, String(step.select)));
|
|
73
91
|
else throw new Error('step needs click, fill, select or reload');
|
|
74
|
-
}
|
|
92
|
+
};
|
|
93
|
+
for(const step of need.steps??[])await perform(step);
|
|
75
94
|
for (const rawAssertion of need.assertions) {
|
|
76
95
|
const assertion=resolveNeedResults(rawAssertion,saved);
|
|
77
96
|
if (assertion.result || assertion.records || assertion.file || assertion.mcp) {
|
|
@@ -170,3 +189,19 @@ export function resolveNeedResults(value,saved) {
|
|
|
170
189
|
}
|
|
171
190
|
return value;
|
|
172
191
|
}
|
|
192
|
+
|
|
193
|
+
/** Flatten only action groups; used for browser/role discovery before a race starts. */
|
|
194
|
+
export function needSteps(steps = []) { return steps.flatMap(s => s.concurrent ? needSteps(s.concurrent) : [s]); }
|
|
195
|
+
export function configureWebGL(enabled) {
|
|
196
|
+
window.__homieWebGLDisabled = !enabled;
|
|
197
|
+
if(window.__homieWebGLWrapped)return;
|
|
198
|
+
window.__homieWebGLWrapped = true;
|
|
199
|
+
for(const proto of [globalThis.HTMLCanvasElement?.prototype,globalThis.OffscreenCanvas?.prototype]) {
|
|
200
|
+
if(!proto)continue;
|
|
201
|
+
const original = proto.getContext;
|
|
202
|
+
proto.getContext = function(kind, ...args) {
|
|
203
|
+
if(window.__homieWebGLDisabled && /^(webgl2?|experimental-webgl)$/.test(kind))return null;
|
|
204
|
+
return Reflect.apply(original,this,[kind,...args]);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
package/lib/build.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {verifiedMcpConfig,publicMcpSetup} from './shop-mcp.mjs';
|
|
1
2
|
import { directoryUrl } from '../worker/directory-choice.mjs';
|
|
2
3
|
import { domainOrigin } from './studio.mjs';
|
|
3
4
|
import { appProblems } from '../worker/app-format.mjs';
|
|
@@ -686,6 +687,10 @@ async function buildInto(dist, { esbuild, studio, shop, live, games, before, typ
|
|
|
686
687
|
// GAME PARTS: every packed version of every SHARED part into dist/parts/, and its index; a private part is never copied.
|
|
687
688
|
const partsBuilt = buildParts(root, dist, { studio, log });
|
|
688
689
|
mkdirSync(join(dist, '_site'), { recursive: true });
|
|
690
|
+
if(publicMcpSetup(root)) {
|
|
691
|
+
const verified=verifiedMcpConfig(root);
|
|
692
|
+
writeFileSync(join(dist,'_site','stripe-shop-links.json'),JSON.stringify({config:verified?.result.config??null,next:verified?null:'Ask your AI to finish stripe_shop_plan with verify:true.'}));
|
|
693
|
+
}
|
|
689
694
|
writeFileSync(join(dist, '_site', 'posts.json'), `${JSON.stringify({ v: 1, posts })}\n`);
|
|
690
695
|
const studioExtras = schemaExtras(s.schema, { where: 'studio.json site', types: 'Organization', log });
|
|
691
696
|
// The Lounge (0.29.0): studio.json "lounge", checked; off when a game already has /lounge/.
|