@homie-rocks/studio 0.51.6 → 0.51.8
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 +35 -0
- package/apps/APPS.md +26 -0
- package/bin/homie-studio.mjs +10 -5
- package/docs/toolkit-place-patterns.md +62 -0
- package/functions/FUNCTIONS.md +2 -2
- package/guides/app/SKILL.md +34 -0
- package/guides/app/examples/reserve.mjs +13 -0
- package/guides/functions/SKILL.md +2 -0
- package/guides/office/SKILL.md +27 -1
- package/guides/servers/SKILL.md +27 -1
- package/guides/shop/SKILL.md +38 -15
- package/guides/studio-setup/SKILL.md +4 -0
- package/lib/app-needs.mjs +8 -2
- package/lib/app-ready.mjs +31 -0
- package/lib/check.mjs +9 -6
- package/lib/cloudflare.mjs +4 -4
- package/lib/connector-token.mjs +30 -0
- package/lib/mcp-tools.mjs +1 -1
- package/lib/shop-links.mjs +70 -32
- package/lib/shop-mcp.mjs +37 -10
- package/lib/stripe-connect.mjs +4 -4
- package/lib/template-history.json +84 -0
- package/lib/worker-config.mjs +3 -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 +38 -15
- package/worker/app-graphics.mjs +8 -0
- package/worker/app-shell.mjs +36 -15
- package/worker/build-id.mjs +1 -1
- package/worker/connector-token.mjs +11 -0
- package/worker/index.mjs +5 -2
- package/worker/join-area.mjs +35 -0
- package/worker/mcp-tools.mjs +12 -3
- package/worker/mcp.mjs +14 -6
- package/worker/pages.mjs +6 -3
- package/worker/records-page.mjs +3 -2
- package/worker/records-prefs.mjs +24 -0
- package/worker/shop.mjs +2 -2
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,41 @@ 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.51.8] - 2026-10-15
|
|
21
|
+
|
|
22
|
+
**Plugin 0.51.8**
|
|
23
|
+
|
|
24
|
+
Run your studio across more screens with reliable shared records, clearer checks and a simpler owner connection.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **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.
|
|
29
|
+
- **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.
|
|
30
|
+
- **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.
|
|
31
|
+
- **Clean toolkit type checks:** Collision, terrain and tile-coordinate code now passes strict `build --types` checks when imported into your app or game.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **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.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **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.
|
|
40
|
+
- **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.
|
|
41
|
+
|
|
42
|
+
## [0.51.7] - 2026-10-14
|
|
43
|
+
|
|
44
|
+
**Plugin 0.51.7**
|
|
45
|
+
|
|
46
|
+
Finish setting up your shop through Stripe's official plugin, and see what your studio's scheduled work is doing.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **Finish shop setup with fewer calls:** Products, prices and payment links are set up together where possible, and setup remembers completed changes instead of repeatedly searching your Stripe account. Verified shop settings are saved and installed on deploy; checkout explains when private webhook setup is the remaining step.
|
|
51
|
+
- **Clear, private webhook setup:** The guided flow explains when you need one separate Stripe CLI browser approval. The installer passes the signing secret privately to Wrangler. You do not need to supply an API key, and payments stay with Stripe.
|
|
52
|
+
- **See your studio's work:** Owners and studio-wide staff can inspect function run history, errors and the next scheduled run through their authenticated studio connection. Record actions use your highest declared role by default and tell you which role was used.
|
|
53
|
+
- **Reliable handover:** Deploy distinguishes claiming your studio from listing it in the directory. Screenshots used as evidence that owner needs are met are copied into the studio so they remain available with its Codex handover.
|
|
54
|
+
|
|
20
55
|
## [0.51.6] - 2026-10-14
|
|
21
56
|
|
|
22
57
|
**Plugin 0.51.6**
|
package/apps/APPS.md
CHANGED
|
@@ -196,3 +196,29 @@ Keep buttons connected during delayed refreshes: create the button once and set
|
|
|
196
196
|
`button.textContent = label` when data arrives. Never replace a held button with
|
|
197
197
|
`innerHTML` or `replaceWith`; that can cancel navigation between pointer down and
|
|
198
198
|
up. The needs guide has a short complete example and explains outcome assertions.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
## Selling and operational checks over MCP
|
|
202
|
+
|
|
203
|
+
Use Stripe's official plugin and the shop skill's batched `stripe_shop_plan` flow.
|
|
204
|
+
Retain write responses, verify, then deploy the compact public links. Until the
|
|
205
|
+
private `stripe_webhook` step completes, checkout names that one remaining step.
|
|
206
|
+
The AI starts the tool; the owner approves Stripe CLI once if needed. No secrets
|
|
207
|
+
or API keys pass through chat. Deploy distinguishes **claimed, not listed** from
|
|
208
|
+
an actual directory listing; publishing is a separate action.
|
|
209
|
+
|
|
210
|
+
On the existing signed-in studio connector (`<site>/mcp/auth`), call
|
|
211
|
+
`studio_function_runs` with optional `name`, `errors`, `before` for actual attempts,
|
|
212
|
+
redacted errors and next due times. Owners and studio-wide `staff` grants can read
|
|
213
|
+
this operational history; per-app staff grants do not expose other apps' runs.
|
|
214
|
+
Do not start `functions runs --url` in a headless shell with no CLI OAuth session:
|
|
215
|
+
that is a different client and would need its own initial browser approval.
|
|
216
|
+
|
|
217
|
+
`app_records` without `role` selects the highest available declared role: signed-in
|
|
218
|
+
roles first, then the largest permission set, with role name as a stable tie-break.
|
|
219
|
+
Each response reports `role`; an explicit role stays explicit. Inspect `app_schema`
|
|
220
|
+
and pass a role when deliberately testing the customer experience.
|
|
221
|
+
|
|
222
|
+
`check --shots` may capture outside the studio, but persistent needs evidence copies
|
|
223
|
+
those pictures into `.studio/checks` and stores studio-relative paths. Keep that
|
|
224
|
+
folder with the Codex evidence. Deploy can render it without outside-path warnings.
|
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;
|
|
@@ -514,7 +517,7 @@ function print(result) {
|
|
|
514
517
|
// (lib/deploy-state.mjs): the hash `build` printed and the one the live site's manifest answers with.
|
|
515
518
|
...result.games.map((g) => ` ${g.id}: ${g.open ?? g.play}${g.hash ? ` [${deployWords(g)}]` : ''}${g.wall ? ` wall: ${g.wall}` : ''}`), ...(result.removed?.length ? [` no longer on the site: ${result.removed.join(', ')}`] : []), ...(result.notes ?? []).map((n) => ` Note: ${n}`), ...(result.songs ?? []).map((m) => ` song ${m.slug}: ${m.page}`), ...(result.videos ?? []).map((m) => ` video ${m.slug}: ${m.page}`),
|
|
516
519
|
...(result.media?.moved ?? []).map((m) => ` moved to R2 (checked by SHA-256): ${m.path}, still at ${m.url}`), ...(result.media?.failed ?? []).map((m) => ` not moved to R2: ${m.path}: ${m.why}`), '', `Cloudflare: Worker ${result.worker}, D1 ${result.d1}, Durable Objects Table + Lobby${result.r2 ? `, R2 ${result.r2}` : ' (no storage: none needed; `homie-studio storage add` adds it for large media)'}.`,
|
|
517
|
-
result.claim ?
|
|
520
|
+
result.claim ? `The site is ${result.listing??'claimed, not listed'}: list the games with the Homie MCP tool studio_publish, or: npx --no-install homie-studio publish` : 'No directory claim. Listing is the studio’s choice: set homie.directory to true or a directory URL before deploying to request one.',
|
|
518
521
|
...(result.standalone?.length ? ['', ...result.standalone] : []));
|
|
519
522
|
break;
|
|
520
523
|
case 'deploy plan':
|
|
@@ -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,62 @@
|
|
|
1
|
+
# Building places: layout, authority and recovery
|
|
2
|
+
|
|
3
|
+
## TV join safe area
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
Reserve an entire strip on either occupied axis, with a gap:
|
|
8
|
+
|
|
9
|
+
```css
|
|
10
|
+
.status { position: fixed; right: 20px;
|
|
11
|
+
bottom: calc(var(--homie-join-inset-bottom, 0px) + 20px); }
|
|
12
|
+
.tee-sheet { position: fixed; left: 20px; bottom: 20px;
|
|
13
|
+
right: calc(var(--homie-join-inset-right, 0px) + 20px); }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
## Authoritative capacity
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
## Text first, graphics optional
|
|
25
|
+
|
|
26
|
+
Bind navigation and text controls before creating a renderer. A failed WebGL context must not prevent those handlers from being installed:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
renderTextControls();
|
|
30
|
+
bindNavigation();
|
|
31
|
+
try {
|
|
32
|
+
const renderer = new THREE.WebGLRenderer({canvas});
|
|
33
|
+
startScene(renderer);
|
|
34
|
+
} catch {
|
|
35
|
+
canvas.hidden = true;
|
|
36
|
+
fallback.hidden = false; // a normal-flow status inside the scene section
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
## Action diagnostics and types
|
|
43
|
+
|
|
44
|
+
`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.
|
|
45
|
+
|
|
46
|
+
`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`.
|
|
47
|
+
|
|
48
|
+
## Connect another studio without another approval page
|
|
49
|
+
|
|
50
|
+
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:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
homie-studio connector --tools studio_stats --out /private/path/studio-connector.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
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.
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
## Deployment
|
|
61
|
+
|
|
62
|
+
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
|
@@ -87,8 +87,8 @@ homie-studio functions runs --before 101
|
|
|
87
87
|
The command uses the studio's deployed address unless `--url` overrides it. It
|
|
88
88
|
uses the existing owner OAuth connection (one initial browser approval; no office
|
|
89
89
|
key). After approval, checks need no browser. An AI connected to `<site>/mcp` calls
|
|
90
|
-
the owner-
|
|
91
|
-
`before`. Anonymous callers and staff cannot read it.
|
|
90
|
+
the owner/studio-staff `studio_function_runs` tool with optional `name`, `errors` and
|
|
91
|
+
`before`. Anonymous callers and per-app staff cannot read it. A studio-wide `staff` grant can read it; revocation takes effect on the next call. Reuse the authenticated connector at `/mcp/auth`; a headless CLI with no saved OAuth session requires a separate initial browser approval.
|
|
92
92
|
|
|
93
93
|
Results include function, event ID, trigger, started/finished UTC times, outcome,
|
|
94
94
|
error message and attempt. Failed attempts survive successful retries; history
|
package/guides/app/SKILL.md
CHANGED
|
@@ -328,3 +328,37 @@ text and attributes in place; never replace a button under a press. The short
|
|
|
328
328
|
example in NEEDS.md preserves its click listener and focus. A stale-element retry
|
|
329
329
|
helps a replaced handle before dispatch; it cannot recover a silently cancelled
|
|
330
330
|
press. Assert the resulting navigation as well as the button's presence.
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
## Selling and operational checks over MCP
|
|
334
|
+
|
|
335
|
+
Use Stripe's official plugin and the shop skill's batched `stripe_shop_plan` flow.
|
|
336
|
+
Retain write responses, verify, then deploy the compact public links. Until the
|
|
337
|
+
private `stripe_webhook` step completes, checkout names that one remaining step.
|
|
338
|
+
The AI starts the tool; the owner approves Stripe CLI once if needed. No secrets
|
|
339
|
+
or API keys pass through chat. Deploy distinguishes **claimed, not listed** from
|
|
340
|
+
an actual directory listing; publishing is a separate action.
|
|
341
|
+
|
|
342
|
+
On the existing signed-in studio connector (`<site>/mcp/auth`), call
|
|
343
|
+
`studio_function_runs` with optional `name`, `errors`, `before` for actual attempts,
|
|
344
|
+
redacted errors and next due times. Owners and studio-wide `staff` grants can read
|
|
345
|
+
this operational history; per-app staff grants do not expose other apps' runs.
|
|
346
|
+
Do not start `functions runs --url` in a headless shell with no CLI OAuth session:
|
|
347
|
+
that is a different client and would need its own initial browser approval.
|
|
348
|
+
|
|
349
|
+
`app_records` without `role` selects the highest available declared role: signed-in
|
|
350
|
+
roles first, then the largest permission set, with role name as a stable tie-break.
|
|
351
|
+
Each response reports `role`; an explicit role stays explicit. Inspect `app_schema`
|
|
352
|
+
and pass a role when deliberately testing the customer experience.
|
|
353
|
+
|
|
354
|
+
`check --shots` may capture outside the studio, but persistent needs evidence copies
|
|
355
|
+
those pictures into `.studio/checks` and stores studio-relative paths. Keep that
|
|
356
|
+
folder with the Codex evidence. Deploy can render it without outside-path warnings.
|
|
357
|
+
|
|
358
|
+
### Places: capacity, TV space and failed graphics
|
|
359
|
+
|
|
360
|
+
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.
|
|
361
|
+
|
|
362
|
+
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.
|
|
363
|
+
|
|
364
|
+
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/office/SKILL.md
CHANGED
|
@@ -118,7 +118,7 @@ build/deploy; the Worker dispatches due functions without an office browser.
|
|
|
118
118
|
Build and redeploy after upgrading; custom crons are kept. Check the deploy plan's
|
|
119
119
|
cron line. Do not add a default cron to a studio with no declared need.
|
|
120
120
|
|
|
121
|
-
To inspect runs from the owner's AI, call the owner
|
|
121
|
+
To inspect runs from the owner's AI, call the owner or studio-staff MCP tool
|
|
122
122
|
`studio_function_runs` (optional `name`, `errors`, `before`), or run
|
|
123
123
|
`homie-studio functions runs [<name>] [--errors] [--url <studio-site>]`.
|
|
124
124
|
Use the existing owner OAuth connection; never request an office key. Results
|
|
@@ -180,3 +180,29 @@ Issues and grant Notifications Write and Workers Observability Write. Cloudflare
|
|
|
180
180
|
posts signed events to the client's webhook so new or recurring reliability
|
|
181
181
|
problems reach the AI without polling; Homie hosts nothing for this. See
|
|
182
182
|
[Cloudflare's announcement](https://developers.cloudflare.com/changelog/post/2026-10-13-cloudflare-mcp-events/).
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
## Selling and operational checks over MCP
|
|
186
|
+
|
|
187
|
+
Use Stripe's official plugin and the shop skill's batched `stripe_shop_plan` flow.
|
|
188
|
+
Retain write responses, verify, then deploy the compact public links. Until the
|
|
189
|
+
private `stripe_webhook` step completes, checkout names that one remaining step.
|
|
190
|
+
The AI starts the tool; the owner approves Stripe CLI once if needed. No secrets
|
|
191
|
+
or API keys pass through chat. Deploy distinguishes **claimed, not listed** from
|
|
192
|
+
an actual directory listing; publishing is a separate action.
|
|
193
|
+
|
|
194
|
+
On the existing signed-in studio connector (`<site>/mcp/auth`), call
|
|
195
|
+
`studio_function_runs` with optional `name`, `errors`, `before` for actual attempts,
|
|
196
|
+
redacted errors and next due times. Owners and studio-wide `staff` grants can read
|
|
197
|
+
this operational history; per-app staff grants do not expose other apps' runs.
|
|
198
|
+
Do not start `functions runs --url` in a headless shell with no CLI OAuth session:
|
|
199
|
+
that is a different client and would need its own initial browser approval.
|
|
200
|
+
|
|
201
|
+
`app_records` without `role` selects the highest available declared role: signed-in
|
|
202
|
+
roles first, then the largest permission set, with role name as a stable tie-break.
|
|
203
|
+
Each response reports `role`; an explicit role stays explicit. Inspect `app_schema`
|
|
204
|
+
and pass a role when deliberately testing the customer experience.
|
|
205
|
+
|
|
206
|
+
`check --shots` may capture outside the studio, but persistent needs evidence copies
|
|
207
|
+
those pictures into `.studio/checks` and stores studio-relative paths. Keep that
|
|
208
|
+
folder with the Codex evidence. Deploy can render it without outside-path warnings.
|
package/guides/servers/SKILL.md
CHANGED
|
@@ -130,7 +130,7 @@ build/deploy; the Worker dispatches due functions without an office browser.
|
|
|
130
130
|
Build and redeploy after upgrading; custom crons are kept. Check the deploy plan's
|
|
131
131
|
cron line. Do not add a default cron to a studio with no declared need.
|
|
132
132
|
|
|
133
|
-
To inspect runs from the owner's AI, call the owner
|
|
133
|
+
To inspect runs from the owner's AI, call the owner or studio-staff MCP tool
|
|
134
134
|
`studio_function_runs` (optional `name`, `errors`, `before`), or run
|
|
135
135
|
`homie-studio functions runs [<name>] [--errors] [--url <studio-site>]`.
|
|
136
136
|
Use the existing owner OAuth connection; never request an office key. Results
|
|
@@ -181,3 +181,29 @@ Issues and grant Notifications Write and Workers Observability Write. Cloudflare
|
|
|
181
181
|
posts signed events to the client's webhook so new or recurring reliability
|
|
182
182
|
problems reach the AI without polling; Homie hosts nothing for this. See
|
|
183
183
|
[Cloudflare's announcement](https://developers.cloudflare.com/changelog/post/2026-10-13-cloudflare-mcp-events/).
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
## Selling and operational checks over MCP
|
|
187
|
+
|
|
188
|
+
Use Stripe's official plugin and the shop skill's batched `stripe_shop_plan` flow.
|
|
189
|
+
Retain write responses, verify, then deploy the compact public links. Until the
|
|
190
|
+
private `stripe_webhook` step completes, checkout names that one remaining step.
|
|
191
|
+
The AI starts the tool; the owner approves Stripe CLI once if needed. No secrets
|
|
192
|
+
or API keys pass through chat. Deploy distinguishes **claimed, not listed** from
|
|
193
|
+
an actual directory listing; publishing is a separate action.
|
|
194
|
+
|
|
195
|
+
On the existing signed-in studio connector (`<site>/mcp/auth`), call
|
|
196
|
+
`studio_function_runs` with optional `name`, `errors`, `before` for actual attempts,
|
|
197
|
+
redacted errors and next due times. Owners and studio-wide `staff` grants can read
|
|
198
|
+
this operational history; per-app staff grants do not expose other apps' runs.
|
|
199
|
+
Do not start `functions runs --url` in a headless shell with no CLI OAuth session:
|
|
200
|
+
that is a different client and would need its own initial browser approval.
|
|
201
|
+
|
|
202
|
+
`app_records` without `role` selects the highest available declared role: signed-in
|
|
203
|
+
roles first, then the largest permission set, with role name as a stable tie-break.
|
|
204
|
+
Each response reports `role`; an explicit role stays explicit. Inspect `app_schema`
|
|
205
|
+
and pass a role when deliberately testing the customer experience.
|
|
206
|
+
|
|
207
|
+
`check --shots` may capture outside the studio, but persistent needs evidence copies
|
|
208
|
+
those pictures into `.studio/checks` and stores studio-relative paths. Keep that
|
|
209
|
+
folder with the Codex evidence. Deploy can render it without outside-path warnings.
|
package/guides/shop/SKILL.md
CHANGED
|
@@ -42,20 +42,30 @@ calling it; tool argument shapes can change. See https://docs.stripe.com/mcp.
|
|
|
42
42
|
read `/v1/account` to record its account ID. Live mode only after the owner requests it.
|
|
43
43
|
2. Call Homie's `stripe_shop_plan` with `account`, `mode: "test"`, `slots` and
|
|
44
44
|
`observations: []`. CLI equivalent: `homie-studio shop plan --have plan-input.json`.
|
|
45
|
-
That JSON file contains the same fields.
|
|
46
|
-
method,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
That JSON file contains the same fields. `operations` contains every independent
|
|
46
|
+
operation currently ready, with method, path, parameters and create idempotency key.
|
|
47
|
+
Execute the batch through Stripe's official plugin, then replan for dependent work.
|
|
48
|
+
`next` remains the first operation for older clients.
|
|
49
|
+
3. Append `{ "method": "POST", "path": "…", "params": {…}, "body": <Stripe response> }`
|
|
50
|
+
for **writes as well as reads**. Keep this setup's observations across batches.
|
|
51
|
+
Product creation includes its default Price; link creation expands line items.
|
|
52
|
+
Product and Price Search use studio/origin metadata. Existing links are fetched by
|
|
53
|
+
IDs from the verified configuration; Stripe has no Payment Link metadata search.
|
|
54
|
+
Keep `.studio/stripe-mcp.json` when moving the studio to another computer. Without
|
|
55
|
+
it, repeat creates use their idempotency keys; outside Stripe's idempotency retention
|
|
56
|
+
window, recover the studio's existing link IDs before setup to avoid duplicates.
|
|
57
|
+
Follow search `next_page` cursors. Search is eventually consistent: write responses
|
|
58
|
+
are the immediate evidence. On an uncertain write, retry its exact idempotency key;
|
|
59
|
+
never discard successful responses or invent IDs. Start a new read set when checking
|
|
60
|
+
later external edits, changing accounts, or changing mode.
|
|
61
|
+
4. Call `stripe_shop_plan` with `verify: true` using those observations (CLI
|
|
62
|
+
`homie-studio shop verify --have plan-input.json`). Verification checks real fields,
|
|
63
|
+
line items, menus, slot limits and portal settings. It saves compact public Worker
|
|
64
|
+
configuration and provenance to `.studio/stripe-mcp.json`, never API response history.
|
|
65
|
+
Deploy installs those links. Checkout then reports only `webhook-secret` missing;
|
|
66
|
+
it cannot open until signed payment events can confirm orders. Changing shop.json,
|
|
67
|
+
the slot manifest or origin requires verification again. Local test configuration
|
|
68
|
+
goes to `.dev.vars`.
|
|
59
69
|
5. For a deployed HTTPS studio, run `stripe_webhook` (CLI `homie-studio shop webhook`).
|
|
60
70
|
Follow `studio_job`. This uses Stripe CLI only for private webhook installation,
|
|
61
71
|
checks the same account as the MCP, and installs the signing secret and verified
|
|
@@ -63,7 +73,7 @@ calling it; tool argument shapes can change. See https://docs.stripe.com/mcp.
|
|
|
63
73
|
studio profile is not already connected. MCP approval cannot be reused or exported.
|
|
64
74
|
Cloudflare authorization and an existing deployment are prerequisites.
|
|
65
75
|
6. After installation, perform the returned `retire` operations through Stripe MCP to
|
|
66
|
-
archive replaced catalog objects
|
|
76
|
+
archive replaced catalog objects. Use fresh scoped reads for later drift checks. Test a sandbox
|
|
67
77
|
purchase and its signed event, order grant, booking capacity or recurring invoice;
|
|
68
78
|
a browser return is never proof. Stripe CLI `trigger` can send a test event, and
|
|
69
79
|
`events resend <event> --webhook-endpoint <endpoint>` retries a delivery. Check the
|
|
@@ -72,6 +82,19 @@ calling it; tool argument shapes can change. See https://docs.stripe.com/mcp.
|
|
|
72
82
|
|
|
73
83
|
Never create a webhook through model-visible Stripe MCP: its response exposes the signing secret.
|
|
74
84
|
|
|
85
|
+
**Why the separate approval (checked 2026-10-14).** Stripe's
|
|
86
|
+
[event destination API](https://docs.stripe.com/api/v2/core/event-destinations/create)
|
|
87
|
+
also returns its signing secret only in the private create response when explicitly
|
|
88
|
+
included. Thin events still require signature verification and authenticated retrieval
|
|
89
|
+
of event details; they do not supply a secret handoff to Wrangler. The
|
|
90
|
+
[official MCP](https://docs.stripe.com/mcp) documents no credential or secret transfer
|
|
91
|
+
to a studio Worker. Therefore the AI starts `stripe_webhook`, the owner clicks
|
|
92
|
+
Approve once in Stripe's CLI browser page if needed, and the tool completes endpoint
|
|
93
|
+
creation and Wrangler secret installation privately. MCP approval and CLI approval
|
|
94
|
+
are separate. No API key is requested, no secret enters chat, and Homie is never in
|
|
95
|
+
the money path. A connected studio CLI profile needs no further approval.
|
|
96
|
+
|
|
97
|
+
|
|
75
98
|
The owner never handles an API key or signing secret. No Homie backend handles payments.
|
|
76
99
|
The Worker needs only public link configuration and the signing secret; buyers pay
|
|
77
100
|
Stripe directly even when the AI and the owner's computer are offline. The old
|
|
@@ -409,3 +409,7 @@ and slug. It fills missing generated files and retries installation, preserving
|
|
|
409
409
|
already there. A different studio or unrelated nonempty folder is still refused.
|
|
410
410
|
New studios do not request a directory listing by default; set `homie.directory: true`
|
|
411
411
|
or a directory URL only when the person chooses listing.
|
|
412
|
+
|
|
413
|
+
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.
|
|
414
|
+
|
|
415
|
+
An owner's existing Wrangler login can connect another studio without a new browser approval: in that studio run `homie-studio connector --tools <exact-tool-names> --out <private-config.json>`, then load the file directly into the AI client's connector configuration. Never print the bearer or paste the file in chat. Revoke with `homie-studio connector --revoke <id>`. See the installed `docs/toolkit-place-patterns.md` for scope and expiry.
|
package/lib/app-needs.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {actInApp,waitInApp} from './app-ready.mjs';
|
|
2
2
|
/** Opt-in acceptance evidence, sourced from the person's Codex, not starter labels. */
|
|
3
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join, resolve, relative, isAbsolute } from 'node:path';
|
|
3
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { join, resolve, relative, isAbsolute, basename } from 'node:path';
|
|
5
5
|
import { isDeepStrictEqual } from 'node:util';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
|
|
@@ -129,6 +129,12 @@ export async function proveAppNeeds({ plan, surfaces, shots, report, ready = asy
|
|
|
129
129
|
export function saveAppNeeds(root, id, plan, needs) {
|
|
130
130
|
if (!root) return;
|
|
131
131
|
const dir = join(root, '.studio', 'checks'); mkdirSync(dir, { recursive: true });
|
|
132
|
+
needs=needs.map(need=>({...need,captures:(need.captures??[]).map(file=>{
|
|
133
|
+
const source=realpathSync(resolve(root,file)),rel=relative(realpathSync(root),source);
|
|
134
|
+
if(!rel.startsWith('..')&&!isAbsolute(rel))return rel;
|
|
135
|
+
const target=join(dir,`${id}-${createHash('sha256').update(source).digest('hex').slice(0,12)}-${basename(source)}`);
|
|
136
|
+
copyFileSync(source,target);return relative(root,target);
|
|
137
|
+
})}));
|
|
132
138
|
writeFileSync(join(dir, `${id}-needs.json`), JSON.stringify({ at: new Date().toISOString(), codexHash: plan.hash, needs }, null, 2));
|
|
133
139
|
}
|
|
134
140
|
|
package/lib/app-ready.mjs
CHANGED
|
@@ -56,6 +56,28 @@ export async function actInApp(surface, selector, action, {ready=appReady,timeou
|
|
|
56
56
|
for (;;) {
|
|
57
57
|
try {
|
|
58
58
|
await waitInApp(surface,selector,frame=>frame.waitForSelector(selector,{visible:true,timeout:Math.max(1,end-Date.now())}),{ready,timeout:Math.max(1,end-Date.now())});
|
|
59
|
+
const handle=await surface.frame.$?.(selector);
|
|
60
|
+
if(handle)try {
|
|
61
|
+
await handle.scrollIntoView();
|
|
62
|
+
const point=await handle.clickablePoint();
|
|
63
|
+
const ancestors=[];
|
|
64
|
+
for(let frame=surface.frame;frame.parentFrame();frame=frame.parentFrame())ancestors.unshift(await frame.frameElement());
|
|
65
|
+
const describeHit=(el,p)=>{
|
|
66
|
+
const hit=document.elementFromPoint(p.x,p.y);
|
|
67
|
+
return hit&&!el.contains(hit)?hit.tagName.toLowerCase()+(hit.id?'#'+hit.id:'')+[...hit.classList].map(c=>'.'+c).join(''):null;
|
|
68
|
+
};
|
|
69
|
+
let local={...point};
|
|
70
|
+
try {
|
|
71
|
+
for(const ancestor of ancestors){
|
|
72
|
+
const covered=await ancestor.evaluate(describeHit,local);
|
|
73
|
+
if(covered)throw new Error(`${surface.name}: ${selector} is covered at the click point by ${covered}`);
|
|
74
|
+
const offset=await ancestor.evaluate(el=>{const r=el.getBoundingClientRect();return {x:r.x+el.clientLeft,y:r.y+el.clientTop};});
|
|
75
|
+
local={x:local.x-offset.x,y:local.y-offset.y};
|
|
76
|
+
}
|
|
77
|
+
const covered=await handle.evaluate(describeHit,local);
|
|
78
|
+
if(covered)throw new Error(`${surface.name}: ${selector} is covered at the click point by ${covered}`);
|
|
79
|
+
}finally{await Promise.all(ancestors.map(h=>h.dispose()));}
|
|
80
|
+
}finally{await handle.dispose();}
|
|
59
81
|
return await action(surface.frame);
|
|
60
82
|
} catch(error) {
|
|
61
83
|
if(!/node is detached|element.*(?:detach|stale)|stale.*element/i.test(error.message)) {
|
|
@@ -69,3 +91,12 @@ export async function actInApp(surface, selector, action, {ready=appReady,timeou
|
|
|
69
91
|
}
|
|
70
92
|
}
|
|
71
93
|
}
|
|
94
|
+
|
|
95
|
+
/** Install before navigation: every app iframe observes a real context creation failure. */
|
|
96
|
+
export function forceWebglFailure() {
|
|
97
|
+
for(const proto of [globalThis.HTMLCanvasElement?.prototype,globalThis.OffscreenCanvas?.prototype]) {
|
|
98
|
+
if(!proto)continue;
|
|
99
|
+
const original=proto.getContext;
|
|
100
|
+
proto.getContext=function(kind,...args){return /^(webgl2?|experimental-webgl)$/.test(kind)?null:Reflect.apply(original,this,[kind,...args]);};
|
|
101
|
+
}
|
|
102
|
+
}
|
package/lib/check.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {needsHeadless} from './needs-headless.mjs';
|
|
2
2
|
import {randomBytes} from 'node:crypto';
|
|
3
|
-
import {actInApp,appReady,observeEffects,waitInApp,answerDialogs} from './app-ready.mjs';
|
|
3
|
+
import {actInApp,appReady,observeEffects,forceWebglFailure,waitInApp,answerDialogs} from './app-ready.mjs';
|
|
4
4
|
import {localCredential,localCookie} from './local-test.mjs';
|
|
5
5
|
import { readAppNeeds, proveAppNeeds, saveAppNeeds } from './app-needs.mjs';
|
|
6
6
|
/**
|
|
@@ -132,7 +132,7 @@ export function judgeRounds(browsers, missed = new Map()) {
|
|
|
132
132
|
return null;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
export async function check({ isolate = true, root = null, url, game, roundTimeoutMs = 150_000, rounds = 3, shots = null, userAgentTag = 'homie-studio-check', log = () => {}, report = null }) {
|
|
135
|
+
export async function check({ webglFailure = false, isolate = true, root = null, url, game, roundTimeoutMs = 150_000, rounds = 3, shots = null, userAgentTag = 'homie-studio-check', log = () => {}, report = null }) {
|
|
136
136
|
if (!url || !game) throw new Error('usage: homie-studio check <game> --url <site url>');
|
|
137
137
|
const base = String(url).replace(/\/+$/, '');
|
|
138
138
|
const play = `${base}/${game}/play`;
|
|
@@ -152,14 +152,15 @@ export async function check({ isolate = true, root = null, url, game, roundTimeo
|
|
|
152
152
|
if(appMeta && !appMeta.check?.action) {
|
|
153
153
|
try {
|
|
154
154
|
const plan=readAppNeeds(root,game);
|
|
155
|
-
if(plan && !plan.needs.some(n=>[...(n.steps??[]),...(n.assertions??[])].some(i=>i.surface)))return checkApp({isolate,root,url:base,game,meta:appMeta,shots,log,report});
|
|
155
|
+
if(plan && !plan.needs.some(n=>[...(n.steps??[]),...(n.assertions??[])].some(i=>i.surface)))return checkApp({webglFailure,isolate,root,url:base,game,meta:appMeta,shots,log,report});
|
|
156
156
|
} catch(error) {return {ok:false,command:'check',kind:'app',why:error.message};}
|
|
157
157
|
}
|
|
158
158
|
const chrome = findChrome();
|
|
159
159
|
if (!chrome) return { ok: false, command: 'check', why: noChrome() };
|
|
160
160
|
let puppeteer;
|
|
161
161
|
try { puppeteer = (await import('puppeteer-core')).default; } catch { return { ok: false, command: 'check', why: 'puppeteer-core is not installed (it comes with @homie-rocks/studio; run npm install)' }; }
|
|
162
|
-
if (appMeta) return checkApp({ isolate, root, url: base, game, meta: appMeta, shots, log, report, puppeteer, chrome });
|
|
162
|
+
if (appMeta) return checkApp({ webglFailure, isolate, root, url: base, game, meta: appMeta, shots, log, report, puppeteer, chrome });
|
|
163
|
+
if(webglFailure)return {ok:false,command:'check',why:'--webgl-failure requires an app with UI checks'};
|
|
163
164
|
if (shots) mkdirSync(shots, { recursive: true });
|
|
164
165
|
const profiles = [];
|
|
165
166
|
const browsers = [];
|
|
@@ -354,7 +355,7 @@ export async function check({ isolate = true, root = null, url, game, roundTimeo
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
/** Apps prove a real action crossing surfaces and a reconnect, without inventing a round. */
|
|
357
|
-
export async function checkApp({ isolate = true, root = null, url, game, meta, shots = null, log = () => {}, report = null, puppeteer, chrome }) {
|
|
358
|
+
export async function checkApp({ webglFailure = false, isolate = true, root = null, url, game, meta, shots = null, log = () => {}, report = null, puppeteer, chrome }) {
|
|
358
359
|
let plan;
|
|
359
360
|
try { plan = readAppNeeds(root, game); } catch (error) { return { ok: false, command: 'check', kind: 'app', why: error.message }; }
|
|
360
361
|
const secret=localCredential(root,url);
|
|
@@ -364,6 +365,7 @@ export async function checkApp({ isolate = true, root = null, url, game, meta, s
|
|
|
364
365
|
const proof = meta.check??{};
|
|
365
366
|
const baseline=typeof proof.action==='string'&&typeof proof.observe==='string';
|
|
366
367
|
if (!baseline && !plan) return { ok: false, command: 'check', kind: 'app', why: 'app.json check needs action and observe CSS selectors for a real shared action' };
|
|
368
|
+
if(webglFailure&&!baseline&&!plan?.needs.some(n=>[...(n.steps??[]),...(n.assertions??[])].some(i=>i.surface)))return {ok:false,command:'check',why:'--webgl-failure requires UI steps or assertions'};
|
|
367
369
|
const captureWarnings=[];
|
|
368
370
|
const profile = mkdtempSync(join(tmpdir(), 'homie-app-check-'));
|
|
369
371
|
let browser;
|
|
@@ -382,6 +384,7 @@ export async function checkApp({ isolate = true, root = null, url, game, meta, s
|
|
|
382
384
|
const page=await context.newPage();
|
|
383
385
|
answerDialogs(page,name,dialogs,report);
|
|
384
386
|
await page.evaluateOnNewDocument(observeEffects);
|
|
387
|
+
if(webglFailure)await page.evaluateOnNewDocument(forceWebglFailure);
|
|
385
388
|
const [width,height]=dimensions[name];
|
|
386
389
|
await page.setViewport({width,height,isMobile:name!=='wall',hasTouch:name!=='wall'});
|
|
387
390
|
const params=new URLSearchParams(proof.params??{});params.set('room',room);params.set('role',role);params.set('surface',name==='staff'?'tablet':name==='phone-two'?'phone':name);
|
|
@@ -423,7 +426,7 @@ export async function checkApp({ isolate = true, root = null, url, game, meta, s
|
|
|
423
426
|
const needs = plan ? await proveAppNeeds({ plan, surfaces, shots, report, ready:appReady, root, headless:needsHeadless({url,secret,app:game,meta,scope:isolate?room:null}) }) : null;
|
|
424
427
|
if (plan) { saveAppNeeds(root, game, plan, needs); const { refreshCodexFile } = await import('./codex.mjs'); refreshCodexFile(root, game); }
|
|
425
428
|
const ok = !needs || needs.every(n => n.status === 'met');
|
|
426
|
-
return { ok, ...(needs ? { needs, why: ok ? undefined : 'Some needs from the person’s Codex are not yet met; see needs and captures' } : {}), command: 'check', kind: 'app', isolatedRecords:isolate, captureWarnings, dialogs, room, surfaces: surfaces.map((s) => s.name), action: proof.action, before, after, reconnect: baseline };
|
|
429
|
+
return { ok, ...(needs ? { needs, why: ok ? undefined : 'Some needs from the person’s Codex are not yet met; see needs and captures' } : {}), command: 'check', kind: 'app', webglFailure, isolatedRecords:isolate, captureWarnings, dialogs, room, surfaces: surfaces.map((s) => s.name), action: proof.action, before, after, reconnect: baseline };
|
|
427
430
|
} catch (error) { return { ok: false, command: 'check', kind: 'app', dialogs, why: error.message }; }
|
|
428
431
|
finally { await browser?.close().catch(() => {}); rmSync(profile, { recursive: true, force: true }); }
|
|
429
432
|
}
|