@homie-rocks/studio 0.51.1 → 0.51.3
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 +45 -0
- package/README.md +15 -4
- package/app-starters/gathering/app.json +2 -44
- package/app-starters/gathering/index.html +1 -1
- package/app-starters/gathering/src/view.ts +6 -19
- package/apps/APPS.md +55 -1
- package/bin/homie-studio.mjs +12 -8
- package/guides/app/SKILL.md +22 -2
- package/guides/office/SKILL.md +9 -0
- package/guides/shop/SKILL.md +8 -0
- package/guides/studio-setup/SKILL.md +9 -1
- package/lib/app-needs.mjs +17 -2
- package/lib/app-preset-history.json +840 -0
- package/lib/app-ready.mjs +1 -0
- package/lib/art-cli.mjs +2 -2
- package/lib/art-tools.mjs +2 -2
- package/lib/build.mjs +4 -2
- package/lib/check.mjs +2 -0
- package/lib/cloudflare.mjs +19 -10
- package/lib/decisions.mjs +9 -5
- package/lib/dev.mjs +4 -1
- package/lib/directory.mjs +22 -5
- package/lib/giving-preset.mjs +9 -0
- package/lib/mcp-tools.mjs +1 -1
- package/lib/office.mjs +2 -1
- package/lib/scaffold.mjs +7 -4
- package/lib/shop-links.mjs +4 -2
- package/lib/shop-local.mjs +31 -0
- package/lib/shop.mjs +2 -1
- package/lib/style-presets.mjs +1 -0
- package/lib/template-history.json +92 -2
- package/lib/upgrade-app-presets.mjs +49 -0
- package/lib/upgrade.mjs +2 -1
- package/mcp/ui/build.js +3 -2
- package/package.json +1 -1
- package/shop/SHOP.md +6 -0
- package/site/SITE.md +13 -0
- package/tools/TOOLS.md +9 -0
- package/worker/app-records.mjs +3 -3
- package/worker/arrival.mjs +7 -5
- package/worker/build-id.mjs +1 -1
- package/worker/directory-choice.mjs +9 -0
- package/worker/index.mjs +25 -9
- package/worker/mcp.mjs +2 -2
- package/worker/pages.mjs +12 -5
- package/worker/records-page.mjs +1 -1
- package/worker/shop-page.mjs +2 -2
- package/worker/shop-subscriptions.mjs +42 -0
- package/worker/signin-target.mjs +8 -0
- package/worker/site.mjs +11 -7
- package/worker/stats-page.mjs +6 -3
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,51 @@ 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.3] - 2026-10-14
|
|
21
|
+
|
|
22
|
+
**Plugin 0.51.3**
|
|
23
|
+
|
|
24
|
+
Business and cause apps use their own words, count every paid monthly gift, and test real Stripe checkout locally.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Local real Stripe test checkout: `shop connect --local --profile <sandbox> --url <Wi-Fi address>` syncs test Payment Links without Cloudflare or a public webhook. Stripe CLI forwards signed events to `dev`; phones return to the same Wi-Fi thank-you page.
|
|
29
|
+
- App acceptance needs can name a checkout popup, interact with it, and continue asserting the original phone.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Monthly subscription invoices create deduplicated paid orders and app events with the original reference, so every gift reaches fulfilment.
|
|
34
|
+
- Business/cause menus name their own pages and apps. App shells use app names, website buttons have independent wording, thank-you pages name the organisation, and default phone sharing stays above bottom tabs.
|
|
35
|
+
- Gathering is a task check-in starter with its own records and no giving or quiz workflow. App style defaults use illustration and AI refinements retain AI attribution.
|
|
36
|
+
- `dev` loads root `.dev.vars`. App collections no longer stop accepting records at 1,000.
|
|
37
|
+
- Requests and scheduled runs remove referral metadata recreated by an old Worker during a rolling deployment.
|
|
38
|
+
- The installed app reference documents the supported local payment/checker path and distinguishes local MCP tools from hosted connector tools.
|
|
39
|
+
|
|
40
|
+
### Upgrade notes
|
|
41
|
+
|
|
42
|
+
- Restart local dev after updating `.dev.vars`. Local Stripe links are test-only; live connection and deployment are unchanged.
|
|
43
|
+
- Existing app `screen.share` choices and authored site headers remain in control. Subscription renewals now emit `order.paid` as well as lifecycle events; fulfilment listening to both should deduplicate by payment identity.
|
|
44
|
+
|
|
45
|
+
## [0.51.2] - 2026-10-14
|
|
46
|
+
|
|
47
|
+
**Plugin 0.51.2** · Deploying and listing do what the studio chose, under one address.
|
|
48
|
+
|
|
49
|
+
Your studio follows your listing choice, shares one public address, and keeps its apps and tools working after deploy and upgrade.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Your studio only joins the directory when you choose to list it. Existing claims remain honoured, and you can publish a listing after an unlisted deploy.
|
|
54
|
+
- Your custom domain is the shared address for pages and app links. Visitors arriving at the workers.dev address reach that domain, while app APIs, multiplayer connections and AI tools keep working.
|
|
55
|
+
- Deploy shows your app, game and wall links without guessing your bill. Local database tools keep working after deploy, interrupted setup can resume, and office sign-in takes you straight to the page you requested.
|
|
56
|
+
- Upgrades refresh untouched app presets and defaults while preserving your edits, including giving choices. App walls use the app's own join wording.
|
|
57
|
+
- Decorative app hero frames let visitors scroll the page; apps can share and download files while keeping their sandbox protection.
|
|
58
|
+
- Signed-in AI connectors can discover how to connect through `/mcp/auth`; public tools remain available through `/mcp`.
|
|
59
|
+
|
|
60
|
+
### Upgrade notes
|
|
61
|
+
|
|
62
|
+
- Ask Homie to upgrade your studio, install dependencies and rebuild. Existing directory claims remain honoured. To list a new studio, choose `homie.directory: true` or a directory URL before deploy.
|
|
63
|
+
- Use `/mcp/auth` for staff AI connectors and `/mcp` for public connectors.
|
|
64
|
+
|
|
20
65
|
## [0.51.1] - 2026-10-14
|
|
21
66
|
|
|
22
67
|
**Plugin 0.51.1** · A player's own runner never jumps across the screen.
|
package/README.md
CHANGED
|
@@ -110,10 +110,21 @@ is put in a studio unless the person asks for one), and its Home has a
|
|
|
110
110
|
In the session that works in the repository, `homie-studio setup attach <hs_…> [--client claude|codex|grok]` then gives the studio
|
|
111
111
|
the name chosen in the chat and removes the band.
|
|
112
112
|
|
|
113
|
-
**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
**Listing is the studio’s choice.** Set `homie.directory: true` for homie.rocks or a
|
|
114
|
+
chosen directory URL before deploying. Omitted, null or false makes no new claim;
|
|
115
|
+
existing stored claims remain honoured. An explicit `publish` also records the choice
|
|
116
|
+
and stores the directory claim without requiring a second deploy. The manifest and
|
|
117
|
+
publish use the custom domain when configured. Workers.dev browser pages redirect to
|
|
118
|
+
it; discovery and tool routes remain accessible on both addresses.
|
|
119
|
+
|
|
120
|
+
Deploy prints `/<id>/open` for apps, `/<id>/play` for games and `/<id>/tv` for walls.
|
|
121
|
+
It reports no unmeasured Cloudflare plan. Local tools retain their dev database binding
|
|
122
|
+
after the first deploy. Rerunning an interrupted `new` fills missing files without
|
|
123
|
+
overwriting the matching studio’s edits. Office links sign in directly and retain
|
|
124
|
+
`--to` destinations, including app and connection pages.
|
|
125
|
+
|
|
126
|
+
For signed-in staff AI connectors use `/mcp/auth`; it returns a 401 OAuth discovery
|
|
127
|
+
challenge without a token. `/mcp` continues to serve public tools anonymously.
|
|
117
128
|
|
|
118
129
|
## The site: the hub's shape, and a landing for every game
|
|
119
130
|
|
|
@@ -39,14 +39,8 @@
|
|
|
39
39
|
"many": "people",
|
|
40
40
|
"action": "Check in",
|
|
41
41
|
"total": "Check-ins",
|
|
42
|
-
"choice": "Choose
|
|
43
|
-
"monthly": "Gift frequency",
|
|
44
|
-
"once": "One time",
|
|
45
|
-
"month": "Monthly",
|
|
46
|
-
"give": "Give securely on Stripe",
|
|
47
|
-
"goal": "Intention goal \u00b7 not payments received",
|
|
42
|
+
"choice": "Choose a task",
|
|
48
43
|
"prompt": "Choose what works for you.",
|
|
49
|
-
"finished": "All three answers are in. Thank you!",
|
|
50
44
|
"complete": "Mark complete",
|
|
51
45
|
"done": "Complete",
|
|
52
46
|
"pending": "Checked in",
|
|
@@ -68,14 +62,6 @@
|
|
|
68
62
|
"type": "string",
|
|
69
63
|
"required": true,
|
|
70
64
|
"maxLength": 60
|
|
71
|
-
},
|
|
72
|
-
"amount": {
|
|
73
|
-
"type": "number",
|
|
74
|
-
"required": true
|
|
75
|
-
},
|
|
76
|
-
"monthly": {
|
|
77
|
-
"type": "boolean",
|
|
78
|
-
"required": true
|
|
79
65
|
}
|
|
80
66
|
}
|
|
81
67
|
}
|
|
@@ -129,34 +115,6 @@
|
|
|
129
115
|
"Packing",
|
|
130
116
|
"Deliveries"
|
|
131
117
|
],
|
|
132
|
-
"color": "#184b43"
|
|
133
|
-
"currency": "CAD",
|
|
134
|
-
"goal": 2500,
|
|
135
|
-
"questions": [
|
|
136
|
-
{
|
|
137
|
-
"text": "Which planet is called the red planet?",
|
|
138
|
-
"choices": [
|
|
139
|
-
"Venus",
|
|
140
|
-
"Mars",
|
|
141
|
-
"Jupiter"
|
|
142
|
-
]
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
"text": "How many sides does a hexagon have?",
|
|
146
|
-
"choices": [
|
|
147
|
-
"5",
|
|
148
|
-
"6",
|
|
149
|
-
"8"
|
|
150
|
-
]
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
"text": "Which is a mammal?",
|
|
154
|
-
"choices": [
|
|
155
|
-
"Dolphin",
|
|
156
|
-
"Trout",
|
|
157
|
-
"Octopus"
|
|
158
|
-
]
|
|
159
|
-
}
|
|
160
|
-
]
|
|
118
|
+
"color": "#184b43"
|
|
161
119
|
}
|
|
162
120
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Live together</title><style>
|
|
2
2
|
*{box-sizing:border-box}body{margin:0;background:var(--tone,#183b55);color:#fff7df;font-family:system-ui,sans-serif}main{min-height:100svh;padding:clamp(24px,5vw,64px);display:grid;grid-template-columns:1.1fr 1fr;gap:40px;align-items:center}h1{font-size:clamp(44px,6vw,88px);line-height:1;letter-spacing:-.05em;margin:20px 0}p{line-height:1.6;max-width:40em}#connection{font-size:12px;text-transform:uppercase;letter-spacing:.18em}.panel{background:#fff7df;color:#20352e;border-radius:28px;padding:28px;box-shadow:0 30px 80px #0003}#count{font-size:80px;font-weight:800;line-height:1}ul{padding:0;list-style:none}li{padding:12px 0;border-bottom:1px solid #20352e22}button,select,a{font:inherit;min-height:48px;border-radius:12px}button,select{padding:10px 16px;width:100%;margin-top:8px}button{background:var(--tone);color:#fff7df;border:0;cursor:pointer}button:disabled{opacity:.5}a{display:inline-flex;align-items:center;color:inherit}label{display:block;margin-top:14px}select{background:white;color:#20352e;border:1px solid #20352e66}progress{width:100%;height:22px;accent-color:var(--tone)}#status{min-height:24px}#amount{font-size:40px;font-weight:700}button:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid #df9325;outline-offset:4px}.wall #controls{display:none}#giving-link[hidden],[hidden]{display:none!important}@media(max-width:650px){main{display:block;padding:72px 22px 24px}h1{font-size:46px}.panel{margin-top:24px;padding:22px}#count{font-size:58px}li{font-size:14px}}@media(max-height:520px) and (min-width:651px){main{padding:65px 25px 20px;gap:22px}h1{font-size:38px}.panel{padding:18px}}
|
|
3
|
-
</style><main><header><span id="connection"></span><h1></h1><p id="intro"></p><
|
|
3
|
+
</style><main><header><span id="connection"></span><h1></h1><p id="intro"></p><ul id="entries" aria-live="polite"></ul></header><section class="panel"><div id="label"></div><div id="count" data-count>0</div><div id="controls"><p id="prompt"></p><label><span id="choice-label"></span><select id="choice"></select></label><button data-action></button></div><p id="status" role="status"></p></section></main><script type="module" src="./assets/main.js"></script></html>
|
|
@@ -3,7 +3,7 @@ import { openRoom } from '@homie-rocks/studio/rules/view';
|
|
|
3
3
|
import type rules from './rules';
|
|
4
4
|
import { createAppRecords, type AppRecord } from '@homie-rocks/studio/apps';
|
|
5
5
|
import { randomId } from '@homie-rocks/studio/links';
|
|
6
|
-
type Entry = { choice: string; status: string
|
|
6
|
+
type Entry = { choice: string; status: string };
|
|
7
7
|
const room = openRoom<typeof rules>();
|
|
8
8
|
const net = room.net;
|
|
9
9
|
const role = net.params.role ?? 'customer';
|
|
@@ -11,41 +11,29 @@ const words: Record<string, string> = app.words;
|
|
|
11
11
|
const config = app.starter;
|
|
12
12
|
const $ = <T extends HTMLElement = HTMLElement>(s: string) => document.querySelector<T>(s)!;
|
|
13
13
|
const records = createAppRecords<Entry>({ collection: 'entries' });
|
|
14
|
-
let rows: AppRecord<Entry>[] = [], busy = false, ready = false
|
|
14
|
+
let rows: AppRecord<Entry>[] = [], busy = false, ready = false;
|
|
15
15
|
const action = $<HTMLButtonElement>('[data-action]');
|
|
16
16
|
document.body.className = role;
|
|
17
17
|
document.body.style.setProperty('--tone', config.color);
|
|
18
18
|
$('h1').textContent = app.name; $('#intro').textContent = app.blurb;
|
|
19
19
|
$('#label').textContent = words.total; action.textContent = words.action;
|
|
20
20
|
$('#choice-label').textContent = words.choice;
|
|
21
|
-
$('#frequency-label').textContent = words.monthly;
|
|
22
|
-
$('#once').textContent = words.once; $('#month').textContent = words.month;
|
|
23
|
-
$('#giving-link').textContent = words.give;
|
|
24
|
-
$('#giving-link').hidden = config.kind !== 'giving';
|
|
25
|
-
$('#frequency').hidden = config.kind !== 'giving';
|
|
26
|
-
$('#goal').hidden = config.kind !== 'giving';
|
|
27
21
|
$('#controls').hidden = role === 'wall';
|
|
28
22
|
const select = $<HTMLSelectElement>('#choice');
|
|
29
23
|
function choices() {
|
|
30
24
|
select.replaceChildren();
|
|
31
|
-
const list = config.
|
|
25
|
+
const list = config.choices;
|
|
32
26
|
for (const value of list) { const option = document.createElement('option'); option.value = value; option.textContent = value; select.append(option); }
|
|
33
|
-
$('#prompt').textContent =
|
|
34
|
-
action.hidden = config.kind === 'quiz' && question >= config.questions.length;
|
|
27
|
+
$('#prompt').textContent = words.prompt;
|
|
35
28
|
}
|
|
36
29
|
choices();
|
|
37
30
|
function paint() {
|
|
38
31
|
$('[data-count]').textContent = String(rows.length);
|
|
39
|
-
const amount = rows.reduce((n, r) => n + r.data.amount, 0);
|
|
40
|
-
$('#amount').textContent = new Intl.NumberFormat(undefined, { style: 'currency', currency: config.currency }).format(amount);
|
|
41
|
-
$('#target').textContent = new Intl.NumberFormat(undefined, { style: 'currency', currency: config.currency }).format(config.goal);
|
|
42
|
-
$<HTMLProgressElement>('progress').max = config.goal; $<HTMLProgressElement>('progress').value = amount;
|
|
43
|
-
$('#goal-label').textContent = words.goal;
|
|
44
32
|
action.disabled = busy || !ready;
|
|
45
33
|
$('#entries').replaceChildren();
|
|
46
34
|
for (const row of rows.slice(-6).reverse()) {
|
|
47
35
|
const li = document.createElement('li');
|
|
48
|
-
li.textContent = `${row.data.choice} · ${row.data.status}
|
|
36
|
+
li.textContent = `${row.data.choice} · ${row.data.status}`;
|
|
49
37
|
if (role === 'staff' && row.data.status !== words.done) {
|
|
50
38
|
const button = document.createElement('button'); button.textContent = words.complete;
|
|
51
39
|
button.onclick = () => { void records.update(row, { ...row.data, status: words.done }).then(refresh).catch(e => { $('#status').textContent = e.message; }); }; li.append(button);
|
|
@@ -61,9 +49,8 @@ action.onclick = async () => {
|
|
|
61
49
|
busy = true; paint();
|
|
62
50
|
try {
|
|
63
51
|
const choice = select.value;
|
|
64
|
-
await records.create({ choice
|
|
52
|
+
await records.create({ choice, status: words.pending }, randomId());
|
|
65
53
|
$('#status').textContent = words.thanks;
|
|
66
|
-
if (config.kind === 'quiz') { question++; choices(); }
|
|
67
54
|
room.command('refresh', {}); await refresh();
|
|
68
55
|
} catch (error) { $('#status').textContent = (error as Error).message; }
|
|
69
56
|
finally { busy = false; paint(); }
|
package/apps/APPS.md
CHANGED
|
@@ -18,7 +18,7 @@ await queue.remove(rows[0]);
|
|
|
18
18
|
queue.dispose();
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The shell handles requests only from its own frame and uses its own role, link and account, never authority claimed by the message. The Worker checks every request. Roles declare `signIn: boolean` and `can: ["read:queue", "create:queue", "update:queue", "delete:queue"]`. Collections declare fields with `type` (string/number/boolean), optional `required`, `maxLength`, `enum`, and `initial` (the only value allowed on create). Unknown fields are refused. Records are at most 8 KB
|
|
21
|
+
The shell handles requests only from its own frame and uses its own role, link and account, never authority claimed by the message. The Worker checks every request. Roles declare `signIn: boolean` and `can: ["read:queue", "create:queue", "update:queue", "delete:queue"]`. Collections declare fields with `type` (string/number/boolean), optional `required`, `maxLength`, `enum`, and `initial` (the only value allowed on create). Unknown fields are refused. Records are at most 8 KB and request bodies at most 12 KB. Homie imposes no collection count limit; the studio’s database capacity applies.
|
|
22
22
|
|
|
23
23
|
Generate a fresh ID for every new record; never reuse a deleted ID. Updates and deletes compare `version` atomically. Conflicts return 409; reload and resolve them. A successful write is committed before acknowledgement. Records are scoped by app and collection, not by room. Deleting a room does not delete records. With `records.persist: false`, use netplay keyed state for transient data instead. Database failures are visible and never replaced with fabricated local success.
|
|
24
24
|
|
|
@@ -127,3 +127,57 @@ Use the Manage my gifts link for the buyer-owned Stripe portal in the shop.
|
|
|
127
127
|
Records-only apps support the same shop bridge as room apps. In the sandboxed
|
|
128
128
|
view, use `createShop({target: parent})` to send checkout and booking requests to
|
|
129
129
|
the authenticated outer shell; a room simulation is not required for payments.
|
|
130
|
+
|
|
131
|
+
## Real Stripe test checkout on this computer
|
|
132
|
+
|
|
133
|
+
Use the installed toolkit's app guide and this reference for its exact version.
|
|
134
|
+
The local toolkit MCP exposes `parts_find`; the hosted directory connector may not.
|
|
135
|
+
With a shell, `homie-studio parts find "giving"` is the supported equivalent. Read a
|
|
136
|
+
part's contract: a pledge-only example is not payment fulfilment. The giving starter
|
|
137
|
+
uses the shop bridge; `shop.catalog()` is available in this version. Field names
|
|
138
|
+
start with a lowercase letter and continue with lowercase letters, digits or `_`.
|
|
139
|
+
|
|
140
|
+
No public HTTPS address or Cloudflare account is needed for local Stripe test
|
|
141
|
+
payments. Use the owner's existing Stripe CLI sandbox profile; log in only when
|
|
142
|
+
that profile is unavailable or expired. Never create an imitation checkout.
|
|
143
|
+
|
|
144
|
+
1. Create the app and `shop init --giving --currency CAD` (choose the actual currency).
|
|
145
|
+
2. Choose the computer's Wi-Fi address and a fixed dev port. Run
|
|
146
|
+
`homie-studio shop connect --local --profile <sandbox-profile> --url http://192.168.1.8:8787`.
|
|
147
|
+
This syncs real test Products, Prices and Payment Links through Stripe CLI and
|
|
148
|
+
saves local bindings in the project's `.dev.vars`; it does not deploy or create
|
|
149
|
+
a public webhook. Local products use a separate identity from deployed products.
|
|
150
|
+
3. Run `stripe --project-name <sandbox-profile> listen --all-snapshot --forward-to http://localhost:8787/api/shop/hook`.
|
|
151
|
+
Put its signing secret in root `.dev.vars` as `STRIPE_WEBHOOK_SECRET=whsec_…`.
|
|
152
|
+
Keep that listener running. Never commit the secrets file or print its contents.
|
|
153
|
+
4. Run `homie-studio dev --lan --port 8787`. Dev loads root `.dev.vars` beside its
|
|
154
|
+
generated Wrangler configuration. Restart after changing bindings. If dev reports
|
|
155
|
+
another port, update the listener and repeat local shop connection with that port.
|
|
156
|
+
5. Open **the same Wi-Fi address** on desktop and phones. Checkout returns there,
|
|
157
|
+
so its guest cookie and thank-you page work on both. Localhost is only suitable
|
|
158
|
+
when every browser is on this computer. Use Stripe's test card; verify the signed
|
|
159
|
+
webhook, paid order and app record before reporting success.
|
|
160
|
+
|
|
161
|
+
The first Checkout emits `order.paid`. Later paid subscription-cycle invoices each
|
|
162
|
+
create a distinct paid order and emit `order.paid` with the original app and reference.
|
|
163
|
+
Invoice retries do not count again; the initial invoice does not duplicate Checkout.
|
|
164
|
+
Keep fulfilment idempotent by order/event ID. Subscription lifecycle events remain
|
|
165
|
+
available separately. Failed invoices never create paid gifts.
|
|
166
|
+
|
|
167
|
+
`check` uses isolated records on localhost by default; `--no-isolate` opts into the
|
|
168
|
+
existing records. Stripe webhooks use the ordinary local database, so for a real
|
|
169
|
+
payment acceptance run use a disposable studio and `check --no-isolate`. Test money
|
|
170
|
+
must not be mistaken for live giving. Named staff/tablet/kiosk surfaces and roles
|
|
171
|
+
are supported; only the surfaces named by each need are asserted.
|
|
172
|
+
|
|
173
|
+
For a Give button opening a new tab, name its popup in the click step:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{"surface":"phone","selector":"[data-give]","click":true,"popup":"checkout"}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Later steps and assertions can use `"surface":"checkout"` for the provider page.
|
|
180
|
+
The phone remains its own surface; assert its confirmed gift after checkout. The
|
|
181
|
+
checker brings each page forward before reading or capturing it. Use the real
|
|
182
|
+
provider's controls, never a test-only app action. App sharing defaults to the top
|
|
183
|
+
right, clear of bottom tabs; `screen.share` remains an author-controlled override.
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -507,25 +507,25 @@ function print(result) {
|
|
|
507
507
|
lines.push(JSON.stringify(result.result, null, 2));
|
|
508
508
|
break;
|
|
509
509
|
case 'deploy':
|
|
510
|
-
if (result.mcp) lines.push(`Connect your AI: ${result.mcp} (approve in your studio's browser page; no key)`);
|
|
510
|
+
if (result.mcp) lines.push(`Connect your AI: ${result.mcp}/auth (approve in your studio's browser page; no key)`);
|
|
511
511
|
if (result.ci) {
|
|
512
|
-
lines.push(`Live: ${result.url ?? '(Wrangler printed no address)'}${result.commit ? ` (commit ${result.commit.slice(0, 7)}${result.branch ? ` on ${result.branch}` : ''})` : ''}`, ...result.steps.map((x) => ` ${x.what}`), ...result.games.map((g) => ` ${g.id}: ${g.play}${g.hash ? ` [${deployWords(g)}]` : ''}`));
|
|
512
|
+
lines.push(`Live: ${result.url ?? '(Wrangler printed no address)'}${result.commit ? ` (commit ${result.commit.slice(0, 7)}${result.branch ? ` on ${result.branch}` : ''})` : ''}`, ...result.steps.map((x) => ` ${x.what}`), ...result.games.map((g) => ` ${g.id}: ${g.open ?? g.play}${g.hash ? ` [${deployWords(g)}]` : ''}${g.wall ? ` wall: ${g.wall}` : ''}`));
|
|
513
513
|
break;
|
|
514
514
|
}
|
|
515
515
|
lines.push(`Live: ${result.url}`, ...(result.workersDev && result.workersDev !== result.url ? [` also at ${result.workersDev}`] : []),
|
|
516
516
|
...(result.local ? [` (the workers.dev address names your Cloudflare account, so it is kept in ${result.local} on this computer, never in studio.json)`] : []),
|
|
517
517
|
// Which games this deploy changed since the last deploy from here, and each one's build hash
|
|
518
518
|
// (lib/deploy-state.mjs): the hash `build` printed and the one the live site's manifest answers with.
|
|
519
|
-
...result.games.map((g) => ` ${g.id}: ${g.play}${g.hash ? ` [${deployWords(g)}]` : ''}`), ...(result.removed?.length ? [` no longer on the site: ${result.removed.join(', ')}`] : []), ...(result.notes ?? []).map((n) => ` Note: ${n}`), ...(result.songs ?? []).map((m) => ` song ${m.slug}: ${m.page}`), ...(result.videos ?? []).map((m) => ` video ${m.slug}: ${m.page}`),
|
|
520
|
-
...(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)'}
|
|
521
|
-
result.claim ? 'The site claimed itself in the directory: list the games with the Homie MCP tool studio_publish, or: npx --no-install homie-studio publish' : 'No directory claim
|
|
519
|
+
...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}`),
|
|
520
|
+
...(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)'}.`,
|
|
521
|
+
result.claim ? 'The site claimed itself in the directory: list the games with the Homie MCP tool studio_publish, or: npx --no-install homie-studio publish' : 'No directory claim. Listing is the studio’s choice: set homie.directory to true or a directory URL before deploying to request one.',
|
|
522
522
|
...(result.standalone?.length ? ['', ...result.standalone] : []));
|
|
523
523
|
break;
|
|
524
524
|
case 'deploy plan':
|
|
525
525
|
lines.push(`What \`npm run deploy\` does for ${result.studio}, on the Cloudflare account the person approves:`, '',
|
|
526
526
|
...result.cloudflare.map((r) => ` ${r.kind}${r.name ? ` ${r.name}` : ''}: ${r.what} [${r.state}${r.plan ? `; ${r.plan}` : ''}]`), '',
|
|
527
|
-
`Cost: ${result.cost}
|
|
528
|
-
`The directory (${result.directory.site}) stores: ${result.directory.stores}
|
|
527
|
+
...(result.cost ? [`Cost: ${result.cost}`] : []), `Sign-in: ${result.login}`, `Address: ${result.address}`,
|
|
528
|
+
result.directory.choice, ...(result.directory.site ? [`The directory (${result.directory.site}) stores: ${result.directory.stores}`] : []), result.never,
|
|
529
529
|
...(result.zone?.warning ? ['', `Warning: ${result.zone.warning}`] : result.zone?.why ? ['', result.zone.why] : []),
|
|
530
530
|
...(result.parts && partsPlanLines(result.parts).length ? ['', ...partsPlanLines(result.parts)] : []),
|
|
531
531
|
...(result.standalone?.length ? ['', ...result.standalone] : []));
|
|
@@ -846,7 +846,7 @@ const OUTWARD_FLAGS = {
|
|
|
846
846
|
'chat remove': ['url', 'all'], 'chat budget': ['url'],
|
|
847
847
|
'servers close': ['url', 'reopen'], 'servers level': ['url'], 'servers member': ['url', 'role', 'remove'],
|
|
848
848
|
'parts reissue': ['claim-hash', 'receipt-verified', 'buyer'], 'parts retire': [], 'parts keys': [], 'parts refund': ['game'],
|
|
849
|
-
'shop connect': ['url', 'managed', 'live', 'manual', 'renew', 'from-clipboard'], 'shop disconnect': ['url'], 'shop refund': ['url', 'reason', 'note', 'manual-transaction'],
|
|
849
|
+
'shop connect': ['local', 'profile', 'url', 'managed', 'live', 'manual', 'renew', 'from-clipboard'], 'shop disconnect': ['url'], 'shop refund': ['url', 'reason', 'note', 'manual-transaction'],
|
|
850
850
|
'agents pass': ['url', 'label', 'server', 'hands', 'role', 'days'], 'agents revoke': ['url'], 'agents brain': ['url', 'budget', 'remove'],
|
|
851
851
|
'agents sit': ['url', 'server', 'pass', 'label', 'brain'],
|
|
852
852
|
'events webhook': ['url','after'],
|
|
@@ -1115,6 +1115,10 @@ async function main() {
|
|
|
1115
1115
|
if (!sub) return shopStatus(root, { url });
|
|
1116
1116
|
if (sub === 'init') return shopInit(root, { supporter: flags.has('supporter'), giving: flags.has('giving'), currency: flags.get('currency') ?? null, price: flags.get('price') ?? 500, managed: flags.has('managed') });
|
|
1117
1117
|
if (sub === 'check') return shopCheck(root);
|
|
1118
|
+
if (sub === 'connect' && flags.has('local')) {
|
|
1119
|
+
if(flags.has('live'))throw new Error('Local checkout requires test mode');
|
|
1120
|
+
return (await import('../lib/shop-local.mjs')).shopConnectLocal(root,{url:flags.get('url'),profile:flags.get('profile')});
|
|
1121
|
+
}
|
|
1118
1122
|
if (sub === 'connect') return shopConnect(root, { managed: flags.has('managed') ? true : null, live: flags.has('live'), manual: flags.has('manual'), fromClipboard: flags.has('from-clipboard'), renew: flags.has('renew'), log: (line) => process.stderr.write(`${line}\n`) });
|
|
1119
1123
|
if (sub === 'disconnect') return shopDisconnect(root);
|
|
1120
1124
|
if (sub === 'catalog') {
|
package/guides/app/SKILL.md
CHANGED
|
@@ -6,6 +6,12 @@ compatibility: Node 22 and Chrome; the studio's pinned toolkit and Wrangler.
|
|
|
6
6
|
|
|
7
7
|
# Make an app
|
|
8
8
|
|
|
9
|
+
For the installed version’s payment, records and local-check contracts, start with
|
|
10
|
+
`node_modules/@homie-rocks/studio/apps/APPS.md`. Its “Real Stripe test checkout on
|
|
11
|
+
this computer” section is the supported offline-hosting path: real Stripe test
|
|
12
|
+
checkout, CLI webhook forwarding, root `.dev.vars`, Wi-Fi return pages and named
|
|
13
|
+
checkout popups. Use an existing approved sandbox profile; no deploy is needed.
|
|
14
|
+
|
|
9
15
|
An app is **one screen, its roles, and its parts**. The screen transforms with the person and the work: a camera moves, a ticket unfolds, controls appear in context. Do not build page-to-page navigation, a sidebar of links, or a conventional admin website inside it. A studio's public website can still have pages around the app.
|
|
10
16
|
|
|
11
17
|
Read the person's brief and existing `apps/<id>/` first. Choose sensible engineering defaults from the brief; never ask the person how to implement shared state. A business or cause needs no game demo, mandatory entertainment, invented customers, scores or rounds. Never impose a shop, directory listing, sign-in for public visitors, or paid media. Make the fewest human steps possible; run commands yourself.
|
|
@@ -38,7 +44,7 @@ Before writing scene components:
|
|
|
38
44
|
|
|
39
45
|
1. Run `style init <id> --prompt "<the person's own scene words>"`. Record the direction in the Codex: composition, camera, materials, colour, typography and motion. Use the same style machinery as games, not the starter's default theme.
|
|
40
46
|
2. Inspect the installed `@homie-rocks/*` packages for mechanisms. `geom`, `props`, `render`, `camera`, `postfx` and `fx` can make the place; `input`, `ui`, `ui-world`, `device`, `audio` and `loop` make it usable. Read the package exports and examples rather than guessing imports. Install every dependency reported by the starter or parts before building.
|
|
41
|
-
3. Search `parts_find` for each meaningful piece; shell equivalent: `homie-studio parts find "<piece>"`. Read its contract, source studio and licence. Add fitting pieces with `part_add` / `parts add`; write the missing parts yourself. The example studios share real app pieces: search for venue, workshop, giving, arrivals, scene, queue and sound, not just game characters. If local example studios are available, read their shared `parts/` and manifests without modifying the source studios. A failed catalogue request is not proof that nothing exists.
|
|
47
|
+
3. Search the local toolkit MCP `parts_find` for each meaningful piece (the hosted connector may not expose it); shell equivalent: `homie-studio parts find "<piece>"`. Read its contract, source studio and licence. Add fitting pieces with `part_add` / `parts add`; write the missing parts yourself. The example studios share real app pieces: search for venue, workshop, giving, arrivals, scene, queue and sound, not just game characters. If local example studios are available, read their shared `parts/` and manifests without modifying the source studios. A failed catalogue request is not proof that nothing exists.
|
|
42
48
|
4. Use the art/models/sound/music skills with connected providers and an authorized budget. Generate distinctive assets when useful. If no provider is connected, continue with original geometry, locally drawn SVG, CC0 assets and local synthesized sound. Report the means honestly. Provider absence never justifies replacing the scene with a board.
|
|
43
49
|
5. Record packages, parts with attribution, original work and media routes under **Built from**. Keep costs and provenance visible.
|
|
44
50
|
|
|
@@ -124,7 +130,7 @@ Without Stripe, keep real services and prices visible and say payment is not con
|
|
|
124
130
|
- `apps/<id>/app.json` describes roles (`signIn`, `can`), surfaces (`phone`, `wall`, `kiosk`, `tablet`), wording, record collections, parts and the real UI action used by check. The app's entry and assets live beside it, just like a game.
|
|
125
131
|
- Before inventing each component, inspect engine packages for the mechanism and `parts_find` for the piece. A queue, quiz, menu board, live total, music loop or video intro can be a part. Record its licence and credit. Sharing happens only when requested; selling parts is not part of this workflow.
|
|
126
132
|
- Use the same engine packages as a game: `@homie-rocks/geom` and `props` for original scenery; `render`, `camera`, `postfx`, `fx` for light, composition and motion; `input`, `ui`, `ui-world`, `audio`, `device`, `loop` for interaction and feel. An app can be fully 3D and wild. Legibility and the actual task come first.
|
|
127
|
-
- Use the `style`, `art`, `models`, `animate`, `sound`, `music`, `video` and `perf` skills where they help.
|
|
133
|
+
- Use the `style`, `art`, `models`, `animate`, `sound`, `music`, `video` and `perf` skills where they help. Give the organisation a coherent scene with readable controls and purposeful motion. Media cost and licensing decisions remain the person's.
|
|
128
134
|
- Keep native buttons at least 44 px, keyboard focus visible, text as text, motion reducible, and the wall QR clear of useful information. Pointer gesture guards belong on the canvas, not native click buttons.
|
|
129
135
|
|
|
130
136
|
## Roles and records
|
|
@@ -268,3 +274,17 @@ Use the Manage my gifts link for the buyer-owned Stripe portal in the shop.
|
|
|
268
274
|
For a records-only booking app, use `createShop({target: parent})` inside its
|
|
269
275
|
sandboxed view. The shell includes the authenticated shop bridge; do not add a
|
|
270
276
|
room simulation merely to make a payment button work.
|
|
277
|
+
|
|
278
|
+
## Frames and upgrades (0.51.1)
|
|
279
|
+
|
|
280
|
+
App frames permit Web Share and downloads, while keeping their opaque-origin
|
|
281
|
+
sandbox. Share still requires the guest’s click and browser support; offer a download
|
|
282
|
+
or copy link too. Both the iframe permissions and the served frame’s CSP matter.
|
|
283
|
+
Use `.hero-media` for a decorative iframe in a site hero: it lets scroll gestures
|
|
284
|
+
reach the page. An intentionally interactive hero frame opts in with `data-interactive`.
|
|
285
|
+
App shells allow normal touch scrolling and selection.
|
|
286
|
+
|
|
287
|
+
Upgrade compares copied app presets with recorded toolkit versions. Unchanged
|
|
288
|
+
preset files and default values take the new version; custom scene files, names and
|
|
289
|
+
edited labels remain the studio’s. An app wall uses its join label rather than game
|
|
290
|
+
instructions. Rebuild after upgrade and inspect both the wall and giving page.
|
package/guides/office/SKILL.md
CHANGED
|
@@ -147,3 +147,12 @@ Before Stripe is connected, both live and local shop/giving pages show items and
|
|
|
147
147
|
amounts with checkout disabled and clear not-taking-payment wording. A fresh studio
|
|
148
148
|
has an empty `/shop/` preview. `dev` watches authoring HTML/CSS/JS; check reports
|
|
149
149
|
accepted dialogs and precise waits, and reacquires frames during rebuilds.
|
|
150
|
+
|
|
151
|
+
## Direct sign-in links (0.51.1)
|
|
152
|
+
|
|
153
|
+
`office link --to /<app>/open` and `--to /_studio/office/connections` retain the
|
|
154
|
+
same-site destination, including its query string. Opening the link signs the browser
|
|
155
|
+
in and continues there. A plain link-preview GET spends nothing; the loaded page
|
|
156
|
+
submits the same-origin sign-in form. Keep the link private.
|
|
157
|
+
Local office and role commands reuse the running dev server’s D1 configuration even
|
|
158
|
+
after deploy records the production database id.
|
package/guides/shop/SKILL.md
CHANGED
|
@@ -173,3 +173,11 @@ CLI fixture/API tokens; never enter card digits into a page. The local listener
|
|
|
173
173
|
for development; production sends signed events directly to the studio's webhook.
|
|
174
174
|
The optional keyed fixture remains available for testing explicitly selected session
|
|
175
175
|
checkout. Label offline fixtures simulations.
|
|
176
|
+
|
|
177
|
+
## Local Stripe test payments
|
|
178
|
+
|
|
179
|
+
Use `shop connect --local --profile <sandbox-profile> --url http://<Wi-Fi-IP>:8787`
|
|
180
|
+
and Stripe CLI `listen --all-snapshot --forward-to http://localhost:8787/api/shop/hook`. The full
|
|
181
|
+
recipe is in the installed toolkit’s `apps/APPS.md`, “Real Stripe test checkout on
|
|
182
|
+
this computer”. It uses root `.dev.vars`, needs no public HTTPS or Cloudflare, and
|
|
183
|
+
keeps the existing approved Stripe CLI session. Do not write a stand-in checkout.
|
|
@@ -338,7 +338,7 @@ existing Cloudflare sign-in to create DNS and TLS. Never ask the person for a zo
|
|
|
338
338
|
or wildcard route already on that domain belongs to another site there, and the deploy's warning gives the
|
|
339
339
|
one line to add instead (the `publish` skill has the rest).
|
|
340
340
|
|
|
341
|
-
**The directory, only when asked.**
|
|
341
|
+
**The directory, only when asked.** A deploy never opts a studio in by itself: listing in the homie.rocks directory
|
|
342
342
|
is its own step, it is public, and it is the person's to ask for. When they asked to be listed (in their first
|
|
343
343
|
message or now), or say yes when you offer it once the site is live, call the Homie MCP tool `studio_publish`
|
|
344
344
|
with the live site address (without the connector, `npx --no-install homie-studio publish`). It lists the
|
|
@@ -417,3 +417,11 @@ there (checked by SHA-256, at the same addresses); `homie-studio media move --dr
|
|
|
417
417
|
Every studio serves its own MCP at `<site>/mcp`. Use the `tools` skill for studio-defined business actions, signed integrations and staff AI access. Reuse office and app-record built-ins first. After an authorized deploy, hand over the MCP address and `<site>/_studio/office/connections`: the person approves with the studio account they already have, with no key to copy. Staff connect their own account and retain only their app grants. Client connector setup is separate from the studio approval. Never deploy merely to set up a local tool test.
|
|
418
418
|
|
|
419
419
|
For a business or cause, use `studio_scaffold {name, kind:"business"}` or `homie-studio new <folder> --kind cause`. Continue directly with the app skill and the appropriate `app new --from` starter; no game demo or planning interview unless requested.
|
|
420
|
+
|
|
421
|
+
## Resuming setup (0.51.1)
|
|
422
|
+
|
|
423
|
+
If `homie-studio new` was interrupted, rerun it in the same folder with the same name
|
|
424
|
+
and slug. It fills missing generated files and retries installation, preserving files
|
|
425
|
+
already there. A different studio or unrelated nonempty folder is still refused.
|
|
426
|
+
New studios do not request a directory listing by default; set `homie.directory: true`
|
|
427
|
+
or a directory URL only when the person chooses listing.
|
package/lib/app-needs.mjs
CHANGED
|
@@ -24,6 +24,8 @@ export function readAppNeeds(root, id) {
|
|
|
24
24
|
/** Run real UI actions. A failed or absent flow stays visible; continue collecting other needs. */
|
|
25
25
|
export async function proveAppNeeds({ plan, surfaces, shots, report, ready = async () => {} }) {
|
|
26
26
|
const results = [];
|
|
27
|
+
const appBarrier=ready;
|
|
28
|
+
ready=async s=>{await s.page.bringToFront?.();if(s.external){s.frame=s.page.mainFrame();return;}await appBarrier(s);};
|
|
27
29
|
let activeRole;
|
|
28
30
|
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; };
|
|
29
31
|
for (const need of plan.needs) {
|
|
@@ -33,7 +35,8 @@ export async function proveAppNeeds({ plan, surfaces, shots, report, ready = asy
|
|
|
33
35
|
const result = { id: need.id, statement: need.statement, status: 'not', captures: [] };
|
|
34
36
|
try {
|
|
35
37
|
if (!Array.isArray(need.assertions) || !need.assertions.length) throw new Error('need requires assertions on its named surfaces');
|
|
36
|
-
|
|
38
|
+
const popupNames=new Set((need.steps??[]).map(s=>s.popup).filter(Boolean));
|
|
39
|
+
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'}`); }
|
|
37
40
|
for (const name of new Set(need.assertions.filter(a => a.effect).map(a => a.surface))) await surface(name).frame.evaluate(() => { window.__homieNeedsEffects = []; });
|
|
38
41
|
for (const step of need.steps ?? []) {
|
|
39
42
|
const target=surface(step.surface);
|
|
@@ -43,7 +46,19 @@ export async function proveAppNeeds({ plan, surfaces, shots, report, ready = asy
|
|
|
43
46
|
if (step.reload) { await page.reload({ waitUntil: 'domcontentloaded' }); await ready(surface(step.surface)); continue; }
|
|
44
47
|
await waitInApp(target,step.selector,f=>f.waitForSelector(step.selector, { visible: true, timeout:10000 }),{ready});
|
|
45
48
|
const frame=target.frame;
|
|
46
|
-
if (step.click
|
|
49
|
+
if (step.click && step.popup) {
|
|
50
|
+
let listener,timer;
|
|
51
|
+
const opened=new Promise((resolve,reject)=>{
|
|
52
|
+
listener=popup=>resolve(popup);page.once('popup',listener);
|
|
53
|
+
timer=setTimeout(()=>reject(new Error('payment popup did not open')),10000);
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const [popup]=await Promise.all([opened,frame.click(step.selector)]);
|
|
57
|
+
surfaces.push({name:step.popup,page:popup,frame:popup.mainFrame(),external:true,role:target.role});
|
|
58
|
+
await page.bringToFront();
|
|
59
|
+
} finally {clearTimeout(timer);page.off('popup',listener);}
|
|
60
|
+
}
|
|
61
|
+
else if (step.click) {await frame.click(step.selector);await page.bringToFront?.();}
|
|
47
62
|
else if (step.fill !== undefined) { await frame.$eval(step.selector, el => { el.value = ''; el.dispatchEvent(new Event('input', { bubbles: true })); }); await frame.type(step.selector, String(step.fill)); }
|
|
48
63
|
else if (step.select !== undefined) await frame.select(step.selector, String(step.select));
|
|
49
64
|
else throw new Error('step needs click, fill, select or reload');
|