@homie-rocks/studio 0.49.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/bin/homie-studio.mjs +5 -5
- package/functions/FUNCTIONS.md +1 -1
- package/guides/app/SKILL.md +107 -1
- package/guides/art/SKILL.md +154 -0
- package/guides/art/references/RIGHTS.md +20 -0
- package/guides/art/scripts/art.mjs +308 -0
- package/guides/models/SKILL.md +198 -0
- package/guides/models/references/RIGHTS.md +46 -0
- package/guides/models/references/models.json +175 -0
- package/guides/models/scripts/models.mjs +483 -0
- package/guides/music/SKILL.md +197 -0
- package/guides/music/references/COMPOSITION.md +59 -0
- package/guides/music/references/GAME-AUDIO.md +50 -0
- package/guides/music/references/RIGHTS.md +43 -0
- package/guides/music/scripts/lib/audio.mjs +258 -0
- package/guides/music/scripts/lib/eleven.mjs +260 -0
- package/guides/music/scripts/lib/studio.mjs +147 -0
- package/guides/music/scripts/music.mjs +644 -0
- package/guides/plan/SKILL.md +126 -0
- package/guides/plan/references/CODEX.md +105 -0
- package/guides/plan/references/INTERVIEW.md +86 -0
- package/guides/playtest/SKILL.md +205 -0
- package/guides/playtest/references/METHOD.md +124 -0
- package/guides/playtest/references/REVIEWER.md +78 -0
- package/guides/playtest/scripts/lib/exit.mjs +31 -0
- package/guides/playtest/scripts/lib/judge.mjs +502 -0
- package/guides/playtest/scripts/lib/pixels.mjs +128 -0
- package/guides/playtest/scripts/lib/preflight.mjs +69 -0
- package/guides/playtest/scripts/playtest.mjs +870 -0
- package/guides/shop/SKILL.md +4 -0
- package/guides/sound/SKILL.md +156 -0
- package/guides/sound/references/GAME-SOUND.md +100 -0
- package/guides/sound/references/SCORE.md +131 -0
- package/guides/sound/references/TRAPS.md +93 -0
- package/guides/sound/scripts/lib/measure.mjs +252 -0
- package/guides/sound/scripts/lib/score.mjs +415 -0
- package/guides/sound/scripts/lib/sfx.mjs +222 -0
- package/guides/sound/scripts/lib/synth.mjs +509 -0
- package/guides/sound/scripts/sound.mjs +520 -0
- package/guides/style/SKILL.md +105 -0
- package/lib/app-needs.mjs +63 -0
- package/lib/check.mjs +10 -4
- package/lib/codex.mjs +95 -18
- package/lib/decisions.mjs +9 -7
- package/lib/handoff.mjs +1 -1
- package/lib/mcp-tools.mjs +10 -9
- package/lib/mcp.mjs +1 -1
- package/lib/scaffold.mjs +13 -5
- package/lib/studio.mjs +3 -3
- package/lib/template-history.json +42 -0
- package/package.json +1 -1
- package/shop/SHOP.md +4 -0
- package/shop/shop.ts +4 -4
- package/worker/build-id.mjs +1 -1
- package/worker/functions-schema.mjs +15 -0
- package/worker/shop-page.mjs +1 -1
- package/worker/shop.mjs +6 -2
- package/worker/version.mjs +1 -1
|
@@ -3153,6 +3153,48 @@
|
|
|
3153
3153
|
}
|
|
3154
3154
|
}
|
|
3155
3155
|
}
|
|
3156
|
+
},
|
|
3157
|
+
"0.50.0": {
|
|
3158
|
+
"files": {
|
|
3159
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
3160
|
+
"README.md": "684b3cca8001673c",
|
|
3161
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
3162
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
3163
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
3164
|
+
"videos/README.md": "16babc3437d5545b",
|
|
3165
|
+
"posts/README.md": "37841aae476bd540",
|
|
3166
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
3167
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
3168
|
+
},
|
|
3169
|
+
"sections": {
|
|
3170
|
+
"AGENTS.md": {
|
|
3171
|
+
"(intro)": "394cbbe934e80722",
|
|
3172
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
3173
|
+
"## Layout": "f3a280b8c96cd068",
|
|
3174
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
3175
|
+
"## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
|
|
3176
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
3177
|
+
"## Making games": "bb715272fd1a615b",
|
|
3178
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
3179
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
3180
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
3181
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
3182
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
3183
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
3184
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
3185
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
3186
|
+
"## Beta": "9d02638207a26e73"
|
|
3187
|
+
}
|
|
3188
|
+
},
|
|
3189
|
+
"named": {
|
|
3190
|
+
"Homie Arcade": {
|
|
3191
|
+
"sections": {
|
|
3192
|
+
"AGENTS.md": {
|
|
3193
|
+
"## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3156
3198
|
}
|
|
3157
3199
|
}
|
|
3158
3200
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/shop/SHOP.md
CHANGED
|
@@ -215,3 +215,7 @@ itemButton.onclick = () => shop.open('supporter');
|
|
|
215
215
|
```
|
|
216
216
|
|
|
217
217
|
These calls open the same shop in games and apps. Inside a player card, purchases leave for the studio's shop; ordinary play/open pages start Stripe checkout. The studio's TV and kids policies still apply.
|
|
218
|
+
|
|
219
|
+
## App payment references
|
|
220
|
+
|
|
221
|
+
Create the booking/ticket/gift intention first. `shop.buy('full-tune', 1, undefined, { reference: booking.id })` or `shop.checkout({ reference: booking.id })` sends the app's own reference through checkout. Existing boolean returns are unchanged: checkout opened is not paid. The verified durable `order.paid` event carries `data.order`, `data.reference` (null if omitted) and `data.app`. Reopened Payment Links retain the reference. A reference is buyer-supplied correlation, not authorization: verify the record, app, amount and currency, deduplicate by event/order ID, then fulfil it. Keep personal data out of references. Before Stripe is connected, local prices and unpaid requests can be demonstrated; actual payment and recurring collection remain unverified. Test events must be labelled simulations.
|
package/shop/shop.ts
CHANGED
|
@@ -61,8 +61,8 @@ export interface Shop {
|
|
|
61
61
|
add(item: string, quantity?: number, amount?: number): void;
|
|
62
62
|
cart(): CartLine[];
|
|
63
63
|
clear(): void;
|
|
64
|
-
checkout(): Promise<boolean>;
|
|
65
|
-
buy(item: string, quantity?: number, amount?: number): Promise<boolean>;
|
|
64
|
+
checkout(options?: { reference?: string }): Promise<boolean>;
|
|
65
|
+
buy(item: string, quantity?: number, amount?: number, options?: { reference?: string }): Promise<boolean>;
|
|
66
66
|
/** Mark an item used; the studio's refundUsedItems policy decides whether self-service refunds still apply. */
|
|
67
67
|
used(key: string): Promise<boolean>;
|
|
68
68
|
on(ev: 'change', fn: (owns: string[]) => void): () => void;
|
|
@@ -129,8 +129,8 @@ export function createShop(opts: ShopOptions = {}): Shop {
|
|
|
129
129
|
add: (item, quantity = 1, amount) => { cart.push({ item, quantity, ...(amount === undefined ? {} : { amount }) }); },
|
|
130
130
|
cart: () => cart.map((line) => ({ ...line })),
|
|
131
131
|
clear: () => { cart = []; },
|
|
132
|
-
checkout: async () => { const ok = Boolean((await send({ op: 'checkout', lines: cart })).ok); if (ok) cart = []; return ok; },
|
|
133
|
-
buy: async (item, quantity = 1, amount) => Boolean((await send({ op: 'checkout', lines: [{ item, quantity, ...(amount === undefined ? {} : { amount }) }] })).ok),
|
|
132
|
+
checkout: async (options = {}) => { const ok = Boolean((await send({ op: 'checkout', lines: cart, reference: options.reference })).ok); if (ok) cart = []; return ok; },
|
|
133
|
+
buy: async (item, quantity = 1, amount, options = {}) => Boolean((await send({ op: 'checkout', reference: options.reference, lines: [{ item, quantity, ...(amount === undefined ? {} : { amount }) }] })).ok),
|
|
134
134
|
close: () => { void send({ op: 'close' }); },
|
|
135
135
|
used: async (key) => (KEY.test(String(key)) && owns.includes(String(key)) ? Boolean((await send({ op: 'used', key })).ok) : false),
|
|
136
136
|
on: ((ev: ShopEvent, fn: (x: unknown) => void) => {
|
package/worker/build-id.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by lib/build-identity.mjs; identifies the bundled runtime sources.
|
|
2
|
-
export const STUDIO_BUILD = Object.freeze({"version":"0.
|
|
2
|
+
export const STUDIO_BUILD = Object.freeze({"version":"0.50.0","sha256":"817f88f291b3f1dc92e51bb54784034debeabc202a269a2466a1efc2ef07d292"});
|
|
@@ -69,3 +69,18 @@ ELSE 0 END BEGIN
|
|
|
69
69
|
SELECT 'refund:' || json_extract(NEW.value,'$.id'),'payment.refunded',
|
|
70
70
|
json_object('refund',json_extract(NEW.value,'$.id'),'payment',json_extract(NEW.value,'$.payment_intent'),'amount',json_extract(NEW.value,'$.amount'),'currency',json_extract(NEW.value,'$.currency')),json_extract(NEW.value,'$.at')*1000;
|
|
71
71
|
END;`).join('\n');
|
|
72
|
+
|
|
73
|
+
/** Application correlation is saved before payment; only the paid transition emits it. */
|
|
74
|
+
export const FUNCTIONS_REFERENCE_MIGRATION_FILE = '0021_app_payment_reference.sql';
|
|
75
|
+
export const FUNCTIONS_REFERENCE_MIGRATION = `
|
|
76
|
+
DROP TRIGGER IF EXISTS shop_orders_function_paid;
|
|
77
|
+
CREATE TRIGGER shop_orders_function_paid AFTER UPDATE ON shop_orders
|
|
78
|
+
WHEN EXISTS (SELECT 1 FROM function_subscriptions WHERE type='order.paid')
|
|
79
|
+
AND OLD.paid_at IS NULL AND NEW.paid_at IS NOT NULL
|
|
80
|
+
AND NOT EXISTS (SELECT 1 FROM purchase_orders WHERE id=NEW.id) BEGIN
|
|
81
|
+
INSERT OR IGNORE INTO studio_events(id,type,data,at)
|
|
82
|
+
VALUES('shop_orders:' || NEW.id || ':paid','order.paid',
|
|
83
|
+
json_object('order',NEW.id,'source','shop_orders','amount',NEW.amount,'currency',NEW.currency,'mode',NEW.mode,
|
|
84
|
+
'app',NEW.game,'reference',(SELECT value FROM meta WHERE key='shop-reference:' || NEW.id)),NEW.paid_at);
|
|
85
|
+
END;
|
|
86
|
+
`;
|
package/worker/shop-page.mjs
CHANGED
|
@@ -491,7 +491,7 @@ export const SHOP_SHELL_JS = String.raw`(function () {
|
|
|
491
491
|
if (m.op === 'checkout') {
|
|
492
492
|
if (!cfg || kids || screen) return reply({ ok: false, error: 'policy' });
|
|
493
493
|
var checkoutTab = window.open('', '_blank');
|
|
494
|
-
return api('POST', '/api/shop/buy', { lines: m.lines, game: GAME, server: SERVER }).then(function (r) {
|
|
494
|
+
return api('POST', '/api/shop/buy', { lines: m.lines, reference: m.reference, game: GAME, server: SERVER }).then(function (r) {
|
|
495
495
|
if (r.ok && r.url) { leaveForCheckout(checkoutTab, r); watch(); } else if (checkoutTab) checkoutTab.close();
|
|
496
496
|
reply(r);
|
|
497
497
|
}).catch(function () { if (checkoutTab) checkoutTab.close(); reply({ ok: false }); });
|
package/worker/shop.mjs
CHANGED
|
@@ -461,7 +461,7 @@ export async function reconcileOrders(env, shop, player = null, { expireOpen = f
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
/** Make the order row, then Stripe's Checkout Session. Returns { order, url } or a refusal Response. */
|
|
464
|
-
async function startCheckout(env, url, cat, shop, item, { player, game, via, amount, parent = false, lines = null, requestAt = Date.now() }) {
|
|
464
|
+
async function startCheckout(env, url, cat, shop, item, { player, game, via, amount, parent = false, lines = null, reference = null, requestAt = Date.now() }) {
|
|
465
465
|
lines ??= [{ item, amount, quantity: 1 }];
|
|
466
466
|
const keyless = amount > 0 && !hasStripeKey(env);
|
|
467
467
|
if (keyless && (lines.length !== 1 || fullerReason(shop))) return { error: fail(409, 'one-item', fullerReason(shop) ?? 'Buy one item at a time. A combined cart needs the fuller checkout connection.') };
|
|
@@ -480,6 +480,7 @@ async function startCheckout(env, url, cat, shop, item, { player, game, via, amo
|
|
|
480
480
|
lines = lines.map((line, i) => ({ ...line, id: `${order.id}_${i}` }));
|
|
481
481
|
const reserve = async () => {
|
|
482
482
|
const results = await env.DB.batch([reservation(),
|
|
483
|
+
...(reference === null ? [] : [env.DB.prepare("INSERT INTO meta(key,value) SELECT ?1,?2 WHERE EXISTS (SELECT 1 FROM shop_orders WHERE id=?3)").bind(`shop-reference:${order.id}`, reference, order.id)]),
|
|
483
484
|
...lines.map((line, i) => env.DB.prepare("INSERT INTO shop_order_lines (id, order_id, position, item, quantity, unit_amount, amount, snapshot) SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8 WHERE EXISTS (SELECT 1 FROM shop_orders WHERE id = ?2)")
|
|
484
485
|
.bind(line.id, order.id, i, line.item.id, line.quantity, line.amount, line.amount * line.quantity, JSON.stringify(line.item))),
|
|
485
486
|
env.DB.prepare('UPDATE shop_orders SET referral_terms = ?2, checkout_player = player WHERE id = ?1').bind(order.id, shop.referrals ? JSON.stringify(shop.referrals) : null),
|
|
@@ -564,6 +565,8 @@ function purchaseLimited(request, shop, player, kind) {
|
|
|
564
565
|
async function buyRoute(request, env, url, cat, shop, ready, ctx, requestAt) {
|
|
565
566
|
const b = await bodyOf(request, requestSize(shop));
|
|
566
567
|
if (b.error) return b.error;
|
|
568
|
+
const reference = b.body.reference ?? null;
|
|
569
|
+
if (reference !== null && (typeof reference !== 'string' || !reference.trim() || /[\u0000-\u001f\u007f]/.test(reference))) return fail(400, 'reference', 'reference must be a nonempty string without control characters');
|
|
567
570
|
const freeOnly = (b.body.lines ?? [{ item: b.body.item }]).every?.((l) => shop.items.some((i) => i.id === l?.item && i.price === 0));
|
|
568
571
|
if (!ready.ready && !(freeOnly && ready.missing.every((m) => ['payment-links', 'webhook-secret', 'fuller-checkout'].includes(m)))) return fail(503, 'closed', 'The shop is not open yet.', { missing: ready.missing, migration: ready.migration });
|
|
569
572
|
const requested = b.body.lines ?? [{ item: b.body.item, quantity: b.body.quantity ?? 1, amount: b.body.amount }];
|
|
@@ -603,7 +606,7 @@ async function buyRoute(request, env, url, cat, shop, ready, ctx, requestAt) {
|
|
|
603
606
|
cookies = guest.session.cookies;
|
|
604
607
|
}
|
|
605
608
|
const via = await viaFor(request, env, url, shop, player);
|
|
606
|
-
const r = await startCheckout(env, url, cat, shop, item, { player, game, via, amount, lines, requestAt });
|
|
609
|
+
const r = await startCheckout(env, url, cat, shop, item, { player, game, via, amount, lines, reference, requestAt });
|
|
607
610
|
const response = r.error ?? json({ ok: true, order: r.order.id, url: r.url });
|
|
608
611
|
if (!r.error && !r.reconciled && ctx?.waitUntil) ctx.waitUntil(reconcileOrders(env, shop, player.id).catch((error) => logOrderError(env, r.order.id, error)));
|
|
609
612
|
for (const cookie of cookies) response.headers.append('set-cookie', cookie);
|
|
@@ -864,6 +867,7 @@ async function bindLinkSession(env, session, mode) {
|
|
|
864
867
|
const id = `ord_${(await sha256Hex(session.id)).slice(0, 20)}`;
|
|
865
868
|
await env.DB.batch([
|
|
866
869
|
env.DB.prepare("INSERT INTO shop_orders (id, player, checkout_player, item, game, amount, currency, till, mode, status, parent, via, created_at, updated_at, session, referral_terms) SELECT ?2, player, checkout_player, item, game, ?4, currency, till, mode, 'started', parent, via, ?5, ?5, ?3, referral_terms FROM shop_orders WHERE id = ?1 ON CONFLICT DO NOTHING").bind(source.id, id, session.id, amount, Date.now()),
|
|
870
|
+
env.DB.prepare("INSERT OR IGNORE INTO meta(key,value) SELECT ?1,value FROM meta WHERE key=?2").bind(`shop-reference:${id}`, `shop-reference:${source.id}`),
|
|
867
871
|
env.DB.prepare('INSERT INTO shop_order_lines (id, order_id, position, item, quantity, unit_amount, amount, snapshot) VALUES (?1, ?2, 0, ?3, ?4, ?5, ?6, ?7) ON CONFLICT DO NOTHING').bind(`${id}_0`, id, item.id, quantity, amount / quantity, amount, lines[0].snapshot),
|
|
868
872
|
]);
|
|
869
873
|
order = await orderBySession(env, session.id);
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.50.0';
|