@homie-rocks/studio 0.37.0 → 0.37.1

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 CHANGED
@@ -17,6 +17,24 @@ 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.37.1] - 2026-10-09
21
+
22
+ **Plugin 0.38.1** · [#74](https://github.com/homie-rocks/homie/pull/74)
23
+
24
+ Your AI carries first setup through with defaults you can change by asking.
25
+
26
+ ### Fixed
27
+
28
+ - Setup no longer makes you play a demo, request a practice change, name the studio or answer a design interview before your AI builds your request.
29
+ - Deploy registers an initial workers.dev address through the existing Cloudflare sign-in, keeping an existing address unchanged.
30
+ - The Desktop tools can install a private Node and npm. Your AI can prepare a custom hostname for Cloudflare to create its DNS and certificate during deploy.
31
+ - Public descriptions now describe games, apps, engine packages and media skills, your own Cloudflare and your own Stripe with one approval.
32
+ - App requests go directly to the app build and shared-action check. The large shop-read regression measures CPU work, so other concurrent test processes cannot exhaust its unchanged limit.
33
+
34
+ ### Upgrade notes
35
+
36
+ - Ask your AI to upgrade; it shows the changes and applies them after one yes, keeping your edited files.
37
+
20
38
  ## [0.37.0] - 2026-10-09
21
39
 
22
40
  **Plugin 0.38.0** · [#72](https://github.com/homie-rocks/homie/pull/72)
@@ -226,6 +226,8 @@
226
226
  * pages; the account id goes into studio.json, the token stays in Projects'
227
227
  * vault and its git-ignored .env, and Wrangler runs with it here. The usual
228
228
  * way, npx wrangler login, stays the default. lib/projects.mjs)
229
+ * homie-studio domain <hostname> AI configures DNS/TLS for the next deploy
230
+ * homie-studio setup prepare AI installs a private Node/npm if missing
229
231
  * homie-studio setup status [--connector yes|no] [--client claude|codex|grok] (also: homie-studio doctor)
230
232
  * what this computer and the person's accounts have for a studio: Node, the Homie
231
233
  * connector, Cloudflare (signed in, email verified), Chrome, ffmpeg, GitHub,
@@ -325,6 +327,8 @@ import { build } from '../lib/build.mjs';
325
327
  import { PREVIEW_PORT, previewServer } from '../lib/preview.mjs';
326
328
  import { check } from '../lib/check.mjs';
327
329
  import { ciDeploy, deploy, deployPlan, mediaMove, storageAdd, whoami, wranglerBin, zonePlan } from '../lib/cloudflare.mjs';
330
+ import { studioDomain } from '../lib/domain.mjs';
331
+ import { prepareNode } from '../lib/prepare.mjs';
328
332
  import { setupAttach } from '../lib/setup.mjs';
329
333
  import { chromeArgs, findChrome, installChrome, noChrome } from '../lib/chrome.mjs';
330
334
  import { deployWords } from '../lib/deploy-state.mjs';
@@ -863,6 +867,7 @@ async function main() {
863
867
  if (cmd === 'port' && sub === 'plan') return planPort(positional[2] ?? '.');
864
868
  if (cmd === 'chrome' && sub === 'install') return installChrome({ log, fresh: flags.has('fresh') });
865
869
  if (cmd === 'chrome') { const chrome = findChrome(); return chrome ? { ok: true, command: 'chrome', chrome, args: chromeArgs() } : { ok: false, command: 'chrome', why: noChrome() }; }
870
+ if (cmd === 'setup' && sub === 'prepare') return prepareNode();
866
871
  if ((cmd === 'setup' && sub === 'status') || cmd === 'doctor') return setupStatus({ connector: flags.get('connector') ?? null, homie: flags.get('homie') ?? null, client: flags.get('client') ?? null });
867
872
  if (cmd === 'statusline' && !flags.has('install') && !flags.has('remove')) {
868
873
  const line = statusLine({ columns: Number(process.env.COLUMNS) || 100, color: !process.env.NO_COLOR && !asJson && process.stdout.isTTY });
@@ -891,6 +896,7 @@ async function main() {
891
896
  if (cmd === 'statusline') return installStatusLine(root, { remove: flags.has('remove'), replace: flags.has('replace'), project: flags.get('project') ?? null });
892
897
  if (cmd === 'codex') return codexCommand(root, sub);
893
898
  if (cmd === 'progress') return progressCommand(root, sub);
899
+ if (cmd === 'domain') return studioDomain(root, sub);
894
900
  if (cmd === 'setup' && sub === 'attach') return setupAttach(root, positional[2], { homie: flags.get('homie'), client: flags.get('client') });
895
901
  if (cmd === 'setup' && flags.has('via')) {
896
902
  if (flags.get('via') !== 'stripe-projects') return { ok: false, command: 'setup', why: '--via stripe-projects is the one other way this version knows (the default is npx wrangler login)' };
@@ -31,6 +31,7 @@ import { spawn, spawnSync } from 'node:child_process';
31
31
  import { createHash } from 'node:crypto';
32
32
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
33
33
  import { join } from 'node:path';
34
+ import { registerWorkersAddress } from './domain.mjs';
34
35
  import { cloudflareAuth } from './brain-probe.mjs';
35
36
  import { build } from './build.mjs';
36
37
  import { builtGames, compareDeploy, deployWords, lastDeploy, lastDeployRecord, lockDeploy, recordDeploy } from './deploy-state.mjs';
@@ -93,10 +94,10 @@ export function explainCloudflare(out, accountId = null) {
93
94
  const text = String(out ?? '');
94
95
  const dash = accountId ? `https://dash.cloudflare.com/${accountId}` : 'https://dash.cloudflare.com';
95
96
  if (/\b10034\b|verify your email/i.test(text)) {
96
- return { needs: 'cloudflare-verify-email', why: `Cloudflare needs this account's email address verified before it runs a Worker. Ask the person to open the verification email from Cloudflare (or ${dash}/profile and "Send verification email"), click it, then run \`npm run deploy\` again. No payment method is needed.` };
97
+ return { needs: 'cloudflare-verify-email', why: `Cloudflare needs this account's email address verified before it runs a Worker. Ask the person to open the verification email from Cloudflare (or ${dash}/profile and "Send verification email"), click it. The AI resumes deployment after verification.` };
97
98
  }
98
99
  if (/register a workers\.dev subdomain|workers\/onboarding/i.test(text)) {
99
- return { needs: 'workers-dev-subdomain', why: `This Cloudflare account has no workers.dev address yet. Ask the person to open ${dash}/workers/onboarding once and pick one (it is free), then run \`npm run deploy\` again.` };
100
+ return { needs: 'workers-dev-subdomain', why: `This Cloudflare account has no workers.dev address yet. AI: retry deploy to register it automatically through the existing sign-in. If Cloudflare refuses, open ${dash}/workers/onboarding and handle the setting in the browser; the person only handles provider authorization.` };
100
101
  }
101
102
  if (/maximum number of (D1 )?databases|database limit|too many databases/i.test(text)) {
102
103
  return { needs: 'd1-limit', why: 'This Cloudflare account has used all the D1 databases its plan allows (10 on the free plan). Nothing was deployed. Ask the person which unused database of theirs to remove, or use another account; never delete one yourself.' };
@@ -366,7 +367,15 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null, ownRo
366
367
  }
367
368
 
368
369
  const started = Date.now();
369
- const dep = w(['deploy', ...repoVar(root)]);
370
+ let dep = w(['deploy', ...repoVar(root)]);
371
+ if (dep.code !== 0 && explainCloudflare(dep.out, accountId)?.needs === 'workers-dev-subdomain') {
372
+ const headers = await cloudflareAuth({ env: { ...process.env, ...projectsCloudflareEnv(root) }, bin: wranglerBin(root), cwd: root,
373
+ exec: async (_bin, args) => { const r = w(args, { cwd: root }); return { code: r.code, stdout: r.stdout }; } });
374
+ const address = await registerWorkersAddress({ accountId, slug: studio.slug, headers, ...(fetchFn ? { fetchFn } : {}) });
375
+ if (!address.ok) return { ok: false, command: 'deploy', needs: 'workers-dev-subdomain', why: address.next };
376
+ step(address.created ? 'registered the account’s workers.dev address automatically' : 'kept the account’s existing workers.dev address');
377
+ dep = w(['deploy', ...repoVar(root)]);
378
+ }
370
379
  if (dep.code !== 0) return refuse(`wrangler deploy failed: ${dep.out.trim().split('\n').slice(-6).join(' ')}`, dep.out);
371
380
  remember(`worker:${cf.worker}`);
372
381
  // The workers.dev address names the Cloudflare account (often after its owner): it stays on this computer, in
package/lib/doctor.mjs CHANGED
@@ -192,7 +192,7 @@ export async function setupStatus({
192
192
  rows.push({
193
193
  id: 'node', label: 'Node.js', need: 'required', state: major >= 22 ? 'ok' : 'missing', detail: `v${node}${major >= 22 ? '' : ' (22 or newer is needed)'}`,
194
194
  unlocks: 'everything: the studio\'s toolkit runs on it',
195
- fix: major >= 22 ? null : { who: 'person', open: 'https://nodejs.org/en/download', say: 'Install Node.js 22 or newer (the LTS download), then start a new session.' },
195
+ fix: major >= 22 ? null : { who: 'ai', say: 'Call setup_prepare in the Desktop extension; with a shell, install Node.js 22 or newer for this computer and continue setup. The person never runs a command.' },
196
196
  });
197
197
 
198
198
  // The Homie connector (the AI says; the network is checked here), Cloudflare, GitHub and the providers, at once.
@@ -318,15 +318,15 @@ export async function setupStatus({
318
318
  { label: 'email verified', state: verified, detail: deployed ? 'a deploy went through' : verified === 'act' ? 'the last deploy was refused until it is' : 'Cloudflare checks it at the first deploy' },
319
319
  ];
320
320
  const fix = attached ? null : signedIn === 'act'
321
- ? { who: 'ai', run: 'npx wrangler login', say: 'Cloudflare opens in your browser: approve once. A free account works; no payment method.', open: 'https://dash.cloudflare.com/sign-up' }
321
+ ? { who: 'ai', run: 'npx wrangler login', say: 'Cloudflare opens in your browser: approve once.', open: 'https://dash.cloudflare.com/sign-up' }
322
322
  : verified === 'act'
323
- ? { who: 'person', open: 'https://dash.cloudflare.com/profile', say: 'Open the email Cloudflare sent and click its link (or Profile, "Send verification email"); then deploy again.' }
323
+ ? { who: 'person', open: 'https://dash.cloudflare.com/profile', say: 'Open the email Cloudflare sent and click its link (or Profile, "Send verification email"); your AI resumes deployment.' }
324
324
  : signedIn === 'later' || verified === 'later'
325
- ? { who: 'person', open: 'https://dash.cloudflare.com/sign-up', say: 'No Cloudflare account yet? Make a free one now (no payment method) and click the link in the email it sends. Your AI signs in for you later; you approve once.' }
325
+ ? { who: 'ai', say: 'Continue making the studio. When it is ready to go live, call cloudflare_login; the person signs in or creates an account in that browser flow.' }
326
326
  : null;
327
327
  rows.push({
328
328
  id: 'cloudflare', label: 'Cloudflare', need: 'to go online', state: worst(parts.map((p) => p.state)), detail: parts.map((p) => `${p.label}: ${p.detail}`).join('; '), parts,
329
- unlocks: 'putting the studio\'s site and public rooms online, on its own free Cloudflare account', fix,
329
+ unlocks: 'putting the studio\'s site and public rooms online, on its own Cloudflare account', fix,
330
330
  });
331
331
  }
332
332
 
@@ -357,7 +357,7 @@ export async function setupStatus({
357
357
  unlocks: 'sound effects and a synthesized theme (free, on this computer), trailers, and footage in a landing page\'s hero',
358
358
  fix: ok ? null : mac ? { who: 'ai', run: 'brew install ffmpeg', say: 'about a minute with Homebrew; you approve the install.' }
359
359
  : win ? { who: 'ai', run: 'winget install --id Gyan.FFmpeg -e', say: 'you approve the install.' }
360
- : { who: 'person', say: 'Install your system\'s ffmpeg package (for example: sudo apt-get install -y ffmpeg).' },
360
+ : { who: 'ai', run: 'sudo apt-get install -y ffmpeg', say: 'Install ffmpeg with this system’s package manager; only an OS authorization prompt belongs to the person.' },
361
361
  });
362
362
  }
363
363
 
@@ -398,7 +398,7 @@ export async function setupStatus({
398
398
  id: 'fal', label: 'fal', need: 'optional', state: fal.key ? (fal.valid === false ? 'act' : 'ok') : 'optional',
399
399
  detail: !fal.key ? 'no FAL_KEY in this environment' : fal.valid === true ? 'FAL_KEY is set and fal accepts it' : fal.valid === false ? 'fal refused the FAL_KEY that is set (wrong or revoked)' : 'FAL_KEY is set (fal did not answer just now)',
400
400
  unlocks: 'painted art and backdrops (the art skill) and generated video clips (the video skill), on your own fal account under a budget; covers from a real frame and captured trailers are free',
401
- fix: fal.key && fal.valid !== false ? null : { who: 'person', open: 'https://fal.ai/dashboard/keys', say: 'Make a key, put FAL_KEY in the environment your AI runs in (for example a line in your shell profile), and start a new session. Never paste it into the chat.' },
401
+ fix: fal.key && fal.valid !== false ? null : { who: 'ai', say: 'Use local art and captured footage. Paid fal scripts need an already connected account; never ask the person to find or set a key. Offer the provider browser connection only when generated media is requested.', open: 'https://fal.ai/dashboard' },
402
402
  });
403
403
 
404
404
  // Stripe, only in a studio that sells. Installing agent tooling is not proof of Worker credentials.
package/lib/domain.mjs ADDED
@@ -0,0 +1,43 @@
1
+ /** Configure one requested hostname. Wrangler creates its DNS and TLS on deploy using OAuth.
2
+ * No zone ids or DNS values pass through the person. Existing unrelated routes stay intact.
3
+ */
4
+ import { writeFileSync } from 'node:fs';
5
+ import { readConfig, routesOf } from './routes.mjs';
6
+ import { configPath, readStudio, writeStudio } from './studio.mjs';
7
+
8
+ export function studioDomain(root, hostname) {
9
+ const host = String(hostname ?? '').trim().toLowerCase();
10
+ if (host.length > 253 || !host.includes('.') || !host.split('.').every((p) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(p)) || host.endsWith('.workers.dev')) {
11
+ return { ok: false, next: 'AI: use the requested domain hostname alone, for example play.example.com.' };
12
+ }
13
+ const config = readConfig(root);
14
+ if (!config) return { ok: false, next: 'AI: repair the studio’s Wrangler configuration, then retry studio_domain.' };
15
+ const routes = routesOf(config);
16
+ if (!routes.some((r) => r.pattern === host && r.custom_domain === true)) routes.push({ pattern: host, custom_domain: true });
17
+ const studio = readStudio(root);
18
+ writeFileSync(configPath(root), `${JSON.stringify({ ...config, routes }, null, 2)}\n`);
19
+ writeStudio(root, { ...studio, cloudflare: { ...studio.cloudflare, domain: host } });
20
+ return { ok: true, host, next: 'AI: run studio_deploy with plan: true, then deploy within the person’s request. Cloudflare creates DNS and TLS with the existing sign-in. If the zone is not active on this account, keep the current live address and help connect the domain in the browser.', open: 'https://dash.cloudflare.com/?to=/:account/domains' };
21
+ }
22
+
23
+ /** A first deploy may need the account's workers.dev name. Never rename an existing one. */
24
+ export async function registerWorkersAddress({ accountId, slug = 'studio', headers, fetchFn = fetch } = {}) {
25
+ const next = 'AI: retry Cloudflare sign-in with cloudflare_login, then deploy again.';
26
+ if (!headers || !accountId) return { ok: false, next };
27
+ const url = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/workers/subdomain`;
28
+ try {
29
+ const old = await fetchFn(url, { headers, signal: AbortSignal.timeout(15_000) });
30
+ const body = await old.json();
31
+ if (old.ok && body.success !== false && body.result?.subdomain) return { ok: true, created: false };
32
+ if (old.status !== 404 && !(old.ok && body.success !== false && !body.result?.subdomain)) return { ok: false, next, status: old.status };
33
+ // Stable, unguessable from the account name, and no choice of a technical setting for the person.
34
+ const { createHash } = await import('node:crypto');
35
+ const stem = String(slug).toLowerCase().replace(/[^a-z0-9-]/g, '').replace(/^-+|-+$/g, '').slice(0, 40) || 'studio';
36
+ const suffix = createHash('sha256').update(accountId).digest('hex').slice(0, 10);
37
+ const res = await fetchFn(url, { method: 'PUT', headers: { ...headers, 'content-type': 'application/json' }, body: JSON.stringify({ subdomain: `${stem}-${suffix}` }), signal: AbortSignal.timeout(15_000) });
38
+ const made = await res.json();
39
+ return res.ok && made.success !== false && made.result?.subdomain
40
+ ? { ok: true, created: true }
41
+ : { ok: false, status: res.status, next: 'AI: open https://dash.cloudflare.com/?to=/:account/workers-and-pages and resolve the account address with the existing sign-in, then retry deploy. Do not ask the person for a setting or id.' };
42
+ } catch { return { ok: false, next: 'AI: retry deploy when Cloudflare is reachable; no account setting was confirmed.' }; }
43
+ }
package/lib/jobs.mjs CHANGED
@@ -17,6 +17,7 @@ import { appendFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
17
17
  import { homedir } from 'node:os';
18
18
  import { delimiter, dirname, join } from 'node:path';
19
19
  import { randomBytes } from 'node:crypto';
20
+ import { nodeHome } from './prepare.mjs';
20
21
  import { PACKAGE_ROOT } from './studio.mjs';
21
22
 
22
23
  const WIN = process.platform === 'win32';
@@ -38,7 +39,7 @@ export function toolPath(env = process.env, platform = process.platform, home =
38
39
  : ['/opt/homebrew/bin', '/usr/local/bin', join(home, '.volta', 'bin'), join(home, '.asdf', 'shims'), join(home, '.local', 'bin'), join(home, '.fnm', 'aliases', 'default', 'bin'), join(home, '.bun', 'bin'), ...nvm, '/usr/bin', '/bin'];
39
40
  const seen = new Set();
40
41
  const out = [];
41
- for (const p of [...String(env.PATH ?? env.Path ?? '').split(delimiter), ...extra]) if (p && !seen.has(p)) { seen.add(p); out.push(p); }
42
+ for (const p of [...String(env.PATH ?? env.Path ?? '').split(delimiter), join(nodeHome(home), platform === 'win32' ? '' : 'bin'), ...extra]) if (p && !seen.has(p)) { seen.add(p); out.push(p); }
42
43
  return out.join(delimiter);
43
44
  }
44
45
 
@@ -52,6 +53,7 @@ export function findNode({ fresh = false } = {}) {
52
53
  if (!process.versions.electron) candidates.push(process.execPath);
53
54
  for (const dir of toolPath().split(delimiter)) candidates.push(join(dir, exe));
54
55
  const tried = new Set();
56
+ let withoutNpm = null;
55
57
  for (const bin of candidates) {
56
58
  if (tried.has(bin) || !existsSync(bin)) continue;
57
59
  tried.add(bin);
@@ -61,11 +63,12 @@ export function findNode({ fresh = false } = {}) {
61
63
  const version = String(r.stdout ?? '').trim().replace(/^v/, '');
62
64
  if (r.status === 0 && Number(version.split('.')[0]) >= 22) {
63
65
  const npmNext = join(dirname(bin), WIN ? 'npm.cmd' : 'npm');
64
- nodeCache = { bin, version, npm: existsSync(npmNext) ? npmNext : which(WIN ? 'npm.cmd' : 'npm') };
65
- return nodeCache;
66
+ const found = { bin, version, npm: existsSync(npmNext) ? npmNext : which(WIN ? 'npm.cmd' : 'npm') };
67
+ if (found.npm) { nodeCache = found; return nodeCache; }
68
+ withoutNpm ??= found;
66
69
  }
67
70
  }
68
- return null;
71
+ return withoutNpm;
69
72
  }
70
73
 
71
74
  export function which(name) {
@@ -161,16 +164,23 @@ export function jobView(job, { lines = 12 } = {}) {
161
164
  /** Run the studio's pinned CLI as a job: `homie-studio <args> --json`. */
162
165
  export function cliJob(root, label, args, { cli = null, ...opts } = {}) {
163
166
  const node = findNode();
164
- if (!node) throw new Error('no Node.js 22 or newer on this computer: install it from https://nodejs.org/en/download (the LTS), then ask again');
167
+ if (!node) throw new Error('Node.js is missing. AI: call setup_prepare, then retry this tool.');
165
168
  // `cli`: another homie-studio than the studio's pinned one (this package's, for an upgrade to it).
166
169
  return startJob({ root, label, cmd: node.bin, args: [cli ?? cliOf(root), ...args, '--json'], json: true, ...opts });
167
170
  }
168
171
 
172
+ /** Windows command shims need a shell; run npm's JavaScript with Node instead. */
173
+ export function npmInvocation(node, platform = process.platform) {
174
+ return platform === 'win32'
175
+ ? { cmd: node.bin, args: [join(dirname(node.npm), 'node_modules', 'npm', 'bin', 'npm-cli.js'), 'install', '--no-audit', '--no-fund'] }
176
+ : { cmd: node.npm, args: ['install', '--no-audit', '--no-fund'] };
177
+ }
178
+
169
179
  /** `npm install` in a studio (its pinned toolkit and Wrangler). */
170
180
  export function installJob(root, opts = {}) {
171
181
  const node = findNode();
172
- if (!node?.npm) throw new Error('npm was not found next to Node.js on this computer: install Node.js 22 or newer from https://nodejs.org/en/download, then ask again');
173
- return startJob({ root, label: 'npm install (the studio\'s pinned toolkit and Wrangler)', cmd: node.npm, args: ['install', '--no-audit', '--no-fund'], ...opts });
182
+ if (!node?.npm) throw new Error('npm is missing. AI: call setup_prepare, then retry studio_install.');
183
+ return startJob({ root, label: 'npm install (the studio\'s pinned toolkit and Wrangler)', ...npmInvocation(node), ...opts });
174
184
  }
175
185
 
176
186
  /** When the server ends (the app quit), nothing it started keeps running, except what asked to (none today). */
package/lib/mcp-tools.mjs CHANGED
@@ -16,6 +16,8 @@
16
16
  * AI in a game's guide seat, marked " · AI". This process holds the socket between turns; the game's own bot code
17
17
  * moves the body while the AI thinks; the AI chooses only the game's own goals and lines (agents.json).
18
18
  */
19
+ import { prepareNode } from './prepare.mjs';
20
+ import { studioDomain } from './domain.mjs';
19
21
  import { spawnSync } from 'node:child_process';
20
22
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync } from 'node:fs';
21
23
  import { homedir } from 'node:os';
@@ -192,7 +194,7 @@ function needsInstall(ctx, root) {
192
194
  if (installed(root) || !ctx.install) return null;
193
195
  const running = runningJobs(root).find((j) => j.label.startsWith('npm install'));
194
196
  const job = running ?? installJob(root);
195
- return ok(`The studio's toolkit is still being installed (npm install, job ${job.id}; usually 20 to 60 s). Ask again in a moment, or follow it with studio_job { "job": "${job.id}" }.`, { kind: 'job', ...jobView(job) });
197
+ return ok(`The studio's toolkit is still being installed (npm install, job ${job.id}; usually 20 to 60 s). AI: follow it with studio_job { "job": "${job.id}" }.`, { kind: 'job', ...jobView(job) });
196
198
  }
197
199
 
198
200
  /* ------------------------------------------------------------------ the setup card */
@@ -217,7 +219,7 @@ async function statusOf(ctx, root, { fresh = false } = {}) {
217
219
  }
218
220
 
219
221
  function checklist(ctx, root) {
220
- const steps = ['Setup status', 'The studio', 'See a working game', 'One small change', 'Plan your game: its Game Codex', 'Build it, with progress you can watch', 'Playtest it, then put it online'];
222
+ const steps = ['Setup status', 'The studio', 'Plan from your request', 'Build and check it', 'Put it online'];
221
223
  const done = [true, Boolean(root)];
222
224
  if (root) {
223
225
  const games = listGames(root);
@@ -228,7 +230,7 @@ function checklist(ctx, root) {
228
230
  const checked = note.checked || (() => {
229
231
  try { return readdirSync(join(root, '.studio', 'progress')).some((f) => f.endsWith('.json') && readFeed(root, f.slice(0, -5))?.stages?.some((x) => x.id === 'checks' && x.state === 'done')); } catch { return false; }
230
232
  })();
231
- done.push(Boolean(note.demo || games.length || codex), Boolean(note.changed || codex), codex, Boolean(checked), Boolean(local.deployedAt || local.connectedAt));
233
+ done.push(Boolean(codex || games.length || checked || local.deployedAt), Boolean(checked || local.deployedAt), Boolean(local.deployedAt || local.connectedAt));
232
234
  }
233
235
  // In order: a step counts as done only when every step before it is (an early file edit is not "one small change").
234
236
  for (let i = 1; i < done.length; i++) done[i] = done[i] && done[i - 1];
@@ -618,9 +620,18 @@ async function openFromGitHub(ctx, a) {
618
620
  }
619
621
 
620
622
  /** github_login: GitHub's device sign-in through the GitHub CLI; the person types the one-time code in the browser. */
621
- async function githubLogin(ctx) {
622
- const { gh, signedIn } = ghState();
623
- if (!gh) return fail('The GitHub CLI is not on this computer: it is one download from https://cli.github.com (or, on a Mac with Homebrew, brew install gh). Then ask again. No token is ever pasted anywhere.');
623
+ async function githubLogin(ctx, { state = ghState, find = which, start = startJob, activeJobs = runningJobs, view = jobView } = {}) {
624
+ const { gh, signedIn } = state();
625
+ if (!gh) {
626
+ const brew = find('brew');
627
+ if (brew) {
628
+ const root = ctx.current ?? ctx.studiosDir ?? homedir();
629
+ const active = activeJobs(root).find((j) => j.label === 'Install GitHub CLI');
630
+ const job = active ?? start({ root, label: 'Install GitHub CLI', cmd: brew, args: ['install', 'gh'] });
631
+ return ok(`AI: follow studio_job { "job": "${job.id}" }, then retry github_login.`, { kind: 'job', ...view(job) });
632
+ }
633
+ return fail('AI: install the GitHub CLI from https://cli.github.com, then retry github_login. The person only approves an OS installer prompt; never ask them for a command or token.');
634
+ }
624
635
  if (signedIn) return ok('This computer is signed in to GitHub already.');
625
636
  const job = startJob({ root: ctx.current ?? ctx.studiosDir ?? homedir(), label: 'GitHub sign-in (gh auth login)', cmd: gh, args: ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web'], env: toolEnv() });
626
637
  let code = null;
@@ -758,6 +769,26 @@ const RW = { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
758
769
 
759
770
  export function toolDefs(ctx, avail = {}) {
760
771
  const tools = [
772
+ {
773
+ name: 'setup_prepare', title: 'Prepare this computer',
774
+ description: 'AI setup: install a private Node.js and npm when missing, without a terminal or administrator rights. Then resume studio_scaffold; install Chrome with studio_run ["chrome","install"] when checks need it. No provider account is needed.',
775
+ inputSchema: { type: 'object', properties: {} },
776
+ annotations: { title: 'Prepare this computer', readOnlyHint: false, destructiveHint: false, openWorldHint: true },
777
+ run: async () => {
778
+ if (findNode({ fresh: true })?.npm) return ok('Node and npm are ready. AI: continue studio_scaffold.');
779
+ const r = await prepareNode();
780
+ ctx.status = null;
781
+ findNode({ fresh: true });
782
+ return r.ok ? ok('Node and npm are ready. AI: continue studio_scaffold.', r) : fail(r.next, r);
783
+ },
784
+ },
785
+ {
786
+ name: 'studio_domain', title: 'Set the studio domain',
787
+ description: 'Prepare a requested custom hostname for deployment. Cloudflare creates DNS and TLS using its existing browser sign-in. The AI handles configuration; never ask for an account id, zone id or DNS value. Does not deploy or change the domain registrar.',
788
+ inputSchema: { type: 'object', properties: { hostname: str('Requested hostname, e.g. play.example.com'), ...STUDIO_ARG }, required: ['hostname'] },
789
+ annotations: { title: 'Set the studio domain', ...RW },
790
+ run: async (a) => { const r = studioDomain(ctx.root(a.studio), a.hostname); return r.ok ? ok(r.next, r) : fail(r.next, r); },
791
+ },
761
792
  {
762
793
  name: 'setup_status', title: 'Setup status',
763
794
  description: 'Step 0 of a new studio, and any time the person asks what they need or whether they are set up: what this computer and their accounts have (Node, Cloudflare signed in and email verified, Chrome for the checks, ffmpeg, and the optional GitHub, ElevenLabs and fal), what each unlocks, and the exact fix; the studios in the studios folder; and the new-studio checklist with where they are on it. For a studio that is not made yet, pass its name as studio: the checklist is then the new studio\'s, never another studio\'s that happens to be in the folder. Read-only, a few seconds. Optional rows never block anything.',
@@ -772,12 +803,11 @@ export function toolDefs(ctx, avail = {}) {
772
803
  },
773
804
  {
774
805
  name: 'studio_scaffold', title: 'Make a studio',
775
- description: 'Step 1: make a new game studio ON THIS COMPUTER, in the studios folder: one folder with AGENTS.md, games/, music/, videos/, posts/ and a site that later goes online on the studio\'s own Cloudflare (free plan). It has NO game: its home page says "First game coming soon". Then installs its pinned toolkit (npm install, in the background). Never put a game in it the person did not ask for.',
776
- inputSchema: { type: 'object', properties: { name: str('The studio\'s name, e.g. "Night Owls"'), folder: str('Optional: the folder name inside the studios folder (default: from the name)') }, required: ['name'] },
806
+ description: 'Step 1: make a new game studio ON THIS COMPUTER, in the studios folder: one folder with AGENTS.md, games/, music/, videos/, posts/ and a site that later goes online on the studio\'s own Cloudflare (their account). It has NO game: its home page says "First game coming soon". Then installs its pinned toolkit (npm install, in the background). Never put a game in it the person did not ask for.',
807
+ inputSchema: { type: 'object', properties: { name: str('The studio\'s name, e.g. "Night Owls"'), folder: str('Optional: the folder name inside the studios folder (default: from the name)') } },
777
808
  annotations: { title: 'Make a studio', ...RW }, _meta: ui(UI.setup),
778
809
  run: async (a) => {
779
- const name = String(a.name ?? '').trim().slice(0, 60);
780
- if (!name) return fail('name the studio, e.g. "Night Owls"');
810
+ const name = String(a.name ?? '').trim().slice(0, 60) || 'My Studio';
781
811
  if (!ctx.studiosDir) return fail('no studios folder is set: in Claude, Settings > Extensions > Homie, choose the folder your studios live in');
782
812
  mkdirSync(ctx.studiosDir, { recursive: true });
783
813
  const folder = a.folder ? String(a.folder).trim() : slugify(name);
@@ -797,6 +827,7 @@ export function toolDefs(ctx, avail = {}) {
797
827
  ctx.note(made.dir).earlier = earlier.map((e) => e.path);
798
828
  let install = null;
799
829
  if (ctx.install) {
830
+ if (!findNode()?.npm) { const ready = await prepareNode(); if (!ready.ok) return fail(ready.next, ready); findNode({ fresh: true }); }
800
831
  try { install = installJob(made.dir); } catch (error) { return fail(`${name} is made at ${made.dir}, but ${error.message}`); }
801
832
  await waitJob(install, Math.min(ctx.waitMs, 35_000));
802
833
  }
@@ -937,8 +968,8 @@ export function toolDefs(ctx, avail = {}) {
937
968
  },
938
969
  {
939
970
  name: 'game_plan', title: 'Plan a game (its Game Codex)',
940
- description: 'Step 4: start the game\'s Game Codex (games/<id>/CODEX.md, every section) and get the plan interview to run with the person: two or three questions a message, each with options and your pick, about game type and genre, style, devices, players and rooms, art and film, music and sound, and scope. A game is planned before it is made: with no game <id> yet, this starts its folder with only the codex. Fill CODEX.md from their answers in one call (file_write of the whole file, or one file_edit with an edits list), then game_codex shows it. While planning, run `parts find` (the parts_find tool) for each system the game needs, so the plan starts from pieces of existing games and the @homie-rocks/* packages rather than from nothing; the codex\'s Built from section records what was found.',
941
- inputSchema: { type: 'object', properties: { id: str('The game\'s id (lowercase, digits, hyphens)'), name: str('Its name, when the game is not made yet'), ...STUDIO_ARG }, required: ['id'] },
971
+ description: 'Step 4: start the game\'s Game Codex (games/<id>/CODEX.md, every section) and record sensible defaults from the request for genre, style, devices, rooms, art, sound and scope. Never route engineering or design questions to the person. A game is planned before it is made: with no game <id> yet, this starts its folder with only the codex. Fill CODEX.md from their request and your defaults in one call (file_write of the whole file, or one file_edit with an edits list), then game_codex shows it. While planning, run `parts find` (the parts_find tool) for each system the game needs, so the plan starts from pieces of existing games and the @homie-rocks/* packages rather than from nothing; the codex\'s Built from section records what was found.',
972
+ inputSchema: { type: 'object', properties: { id: str('The game\'s id (lowercase, digits, hyphens)'), name: str('Its name, when the game is not made yet'), interview: { type: 'boolean', description: 'Only when the person explicitly requests a planning conversation' }, ...STUDIO_ARG }, required: ['id'] },
942
973
  annotations: { title: 'Plan a game', ...RW },
943
974
  run: async (a) => {
944
975
  const root = ctx.root(a.studio);
@@ -948,12 +979,12 @@ export function toolDefs(ctx, avail = {}) {
948
979
  made = newCodex(root, String(a.id), { name: a.name ?? null });
949
980
  if (!made.ok) return fail(made.why);
950
981
  }
951
- const guide = ctx.skillsDir && existsSync(join(ctx.skillsDir, 'plan', 'references', 'INTERVIEW.md')) ? readFileSync(join(ctx.skillsDir, 'plan', 'references', 'INTERVIEW.md'), 'utf8') : null;
982
+ const guide = a.interview === true && ctx.skillsDir && existsSync(join(ctx.skillsDir, 'plan', 'references', 'INTERVIEW.md')) ? readFileSync(join(ctx.skillsDir, 'plan', 'references', 'INTERVIEW.md'), 'utf8') : null;
952
983
  return ok([
953
984
  made ? `Wrote games/${a.id}/CODEX.md (every section${made.planned ? '; the game is planned here before it is made' : ''}).` : `games/${a.id}/CODEX.md is there already: change it, never replace it.`,
954
- 'Now the interview: two or three questions a message, each with concrete options and your pick, so "yes" is an answer. Say back what you heard in one line before the next. Stop after three or four rounds; "just build it" means fill the rest with your own choices and list them under Open questions. Then fill CODEX.md in one call (file_read it once; then file_write the whole file, or one file_edit whose edits list has every section) and show it with game_codex.',
985
+ 'From the request and sensible defaults, fill CODEX.md in one call, then show it with game_codex and continue building. Choose phone and computer controls, a small first version, local sound and CC0 art. Infer persistent progress from the premise. No interview unless the person explicitly asks to plan together; no engineering or design questions.',
955
986
  '', PARTS_FIRST,
956
- ...(guide ? ['', guide] : []),
987
+ ...(guide ? ['', 'Optional reference for a person who explicitly requests an interview:', guide] : []),
957
988
  ].join('\n'), { kind: 'plan', id: a.id, file: `games/${a.id}/CODEX.md`, created: Boolean(made) });
958
989
  },
959
990
  },
@@ -1105,7 +1136,7 @@ export function toolDefs(ctx, avail = {}) {
1105
1136
  },
1106
1137
  {
1107
1138
  name: 'studio_deploy', title: 'Put the studio online',
1108
- description: 'Put the studio\'s site online on the studio\'s OWN Cloudflare account: one Worker, one D1 database and two Durable Objects, free plan, no payment method. Call it with plan: true first and tell the person in two or three lines what it creates and costs. If Cloudflare is not signed in, cloudflare_login opens it in their browser to approve once. Runs in the background with the build card.',
1139
+ description: 'Put the studio\'s site online on the studio\'s OWN Cloudflare account: one Worker, one D1 database and two Durable Objects, with its own resources. Call it with plan: true first and tell the person in two or three lines what it creates and costs. If Cloudflare is not signed in, cloudflare_login opens it in their browser to approve once. Runs in the background with the build card.',
1109
1140
  inputSchema: { type: 'object', properties: { plan: { type: 'boolean', description: 'Only say what it will create and what it costs; change nothing' }, ownRoute: { type: 'boolean', description: 'Only after the plan or a deploy warned that another site\'s route on the studio\'s custom domain covers its hostname, and the person agreed: add the studio\'s own exact-host route (the one line the warning gives) and deploy. It never edits or removes any other route' }, ...STUDIO_ARG } },
1110
1141
  annotations: { title: 'Put the studio online', readOnlyHint: false, destructiveHint: false, openWorldHint: true }, _meta: ui(UI.build),
1111
1142
  run: async (a) => {
@@ -1139,7 +1170,7 @@ export function toolDefs(ctx, avail = {}) {
1139
1170
  },
1140
1171
  {
1141
1172
  name: 'cloudflare_login', title: 'Sign in to Cloudflare',
1142
- description: 'Open Cloudflare in the person\'s browser to approve this computer once (Wrangler\'s own sign-in; a free account, no payment method, no key pasted anywhere). Say in one line that Cloudflare opened and they should approve it.',
1173
+ description: 'Open Cloudflare in the person\'s browser to approve this computer once (Wrangler\'s own sign-in; no key pasted anywhere). Say in one line that Cloudflare opened and they should approve it.',
1143
1174
  inputSchema: { type: 'object', properties: { ...STUDIO_ARG } },
1144
1175
  annotations: { title: 'Sign in to Cloudflare', readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1145
1176
  run: async (a) => {
@@ -1464,4 +1495,4 @@ export function slimCodex(html, { max = 140_000, pictures = true } = {}) {
1464
1495
  return h.length <= max ? h : null;
1465
1496
  }
1466
1497
 
1467
- export const _test = { checklist, feedFor, slimCodex, expandHome, feedbackFacts };
1498
+ export const _test = { githubLogin, checklist, feedFor, slimCodex, expandHome, feedbackFacts };
package/lib/mcp.mjs CHANGED
@@ -33,22 +33,17 @@ export const MCP_APP_MIME = 'text/html;profile=mcp-app';
33
33
 
34
34
  export const INSTRUCTIONS = `Homie Studio, on this computer: make game studios and their multiplayer web games, music and videos, run them here, and put them online on the studio's own Cloudflare. (It is not the Homie house app: that app's own server, with its room and TV tools, is a different one.) These tools do the work in this chat; the person never types a command and never needs a terminal. A studio is a folder in the studios folder; the file tools (file_list, file_read, file_edit, file_write, file_search) work inside it.
35
35
 
36
- A new studio follows one checklist, in order and never ahead. Show it in your first reply and again, ticked, as each step ends:
37
- 0. Setup status (setup_status; for a studio that is not made yet, pass its name as studio, so the checklist is the new studio's and never another's): what this computer and their accounts have; optional rows never block.
38
- 1. The studio (studio_scaffold): it has NO game; its home page says "First game coming soon".
39
- 2. See a working game (game_demo): a live game on Homie Arcade with its Play link, nothing copied in. Copy a starter in (game_make) only if they ask.
40
- 3. One small change from one sentence of theirs: to the copied game, or to the studio's home (site/theme.json colours, a tagline in studio.json, a first post in posts/). Then build and preview_run, and they reload.
41
- 4. Plan their game (game_plan): a short interview, two or three questions a message with options and your pick; then fill games/<id>/CODEX.md and show it (game_codex). Look for pieces first: the @homie-rocks/* packages for general mechanisms, and parts_find for pieces other studios shared (part_add brings one in).
42
- 5. Build it: build_open, then game_make under the planned id (the codex stays), file edits (few and large: one file_edit carries every change to a file in its edits list, never a call per change), build, preview_run, check. The card follows every build.
43
- For a shop: stripe_login (shop connect) syncs Products, Prices, Payment Links and the webhook through Stripe CLI browser approval. Follow studio_job. Default is keyless: no API key in the Worker, no human-handled key. Rerun after shop.json changes. Never expose CLI credentials or webhook secrets. Follow the single next step, verify purchase/webhook/grant and a refund in Stripe or through the connected AI. An existing Worker key selects fuller carts, caps, expiry, automatic recovery and office refunds; explain that only when requested. Never choose manual automatically. Lost webhook: resend from Stripe, never grant from the return URL. Live only when requested.
44
- 6. Playtest it (playtest), then put it online: studio_deploy with plan: true first (say what it creates and costs, free), cloudflare_login when not signed in (they approve once in their browser), studio_deploy, then studio_publish.
45
- If they ask for everything at once, show the list, make your own choices for steps 2 to 4 in one line each, and go on.
36
+ Carry the person's request through to a working result without a lesson or an interview. Use their name or business name, otherwise "My Studio". Setup, dependencies, a plan, building and checking are AI work, not approval gates. Choose engineering and design defaults from the request; record them in CODEX.md so the person can change them later by asking. Phone and computer controls, a small first version, local sound and CC0 art are the defaults. No paid media unless requested with a budget. A business or charity may need an app or a site, not a game: do not make them play a demo or choose a genre.
37
+ Run setup_status (for a studio that is not made yet, pass its name as studio), studio_scaffold (NO game until one is requested), then build the requested thing and check it. A demo and a small-change lesson are optional only when requested. Look for existing engine mechanisms and parts_find before building systems. Use few, large edits: one file_edit carries every change to a file in its edits list. game_plan records your decisions; it does not require an interview. Install missing prerequisites yourself with setup_prepare; follow studio_job without asking the person to retry. Optional providers never block.
38
+ Show progress briefly and give the preview link as soon as it works. For going live, studio_deploy with plan: true says what it creates and costs; cloudflare_login asks for browser approval once; then studio_deploy. Publish to the directory only when requested (offer once after going live). Honour an existing request to deploy or list; do not ask again except for a host's mandatory approval.
39
+ For a shop, stripe_login (shop connect) syncs Products, Prices, Payment Links and the webhook through one Stripe browser approval. No human-handled key. Follow studio_job and the result's single next step; rerun after shop.json changes. Test purchase, webhook, grant and refund with stand-ins before going live. Live payments only when requested.
40
+ For a custom domain already on Cloudflare, studio_domain prepares the exact hostname route using the existing sign-in; studio_deploy applies it and Cloudflare creates DNS and TLS. Never ask for zone or account ids. Keep workers.dev until a domain is requested.
46
41
 
47
42
  If studio_scaffold finds an earlier folder of the studio's name that is not a studio, ask the person whether to fold its premise in (studio_fold), and remove it only with a second yes.
48
43
 
49
44
  A studio made elsewhere (on a phone, with Deploy to Cloudflare, so it lives on GitHub): studio_open with its repo clones it into the studios folder with this computer's own GitHub sign-in; github_login signs the computer in with GitHub's one-time code. Never ask for a token.
50
45
 
51
- Links are the person's to open: give them in your reply (the cards have their own buttons). Never open a browser or run a command such as open to open one yourself.
46
+ Give preview and finished-site links in your reply (the cards have their own buttons). Open provider authorization flows when needed; the person signs in and approves, while you handle configuration.
52
47
 
53
48
  To make one mechanic of a game feel better (a jump, a hit, a dash), game_lab opens the Game Lab: New beside Today, one take, its phases and sliders (studio_guide { "topic": "lab" }).
54
49
 
@@ -0,0 +1,47 @@
1
+ /** Private Node installation for a Desktop host with a bundled runtime but no system Node.
2
+ * Official archive + SHA-256, no shell, administrator rights or shell profile edits.
3
+ */
4
+ import { createHash } from 'node:crypto';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { join } from 'node:path';
9
+
10
+ export const nodeHome = (home = homedir()) => join(home, '.cache', 'homie-studio', 'node');
11
+ export async function prepareNode({ home = homedir(), platform = process.platform, arch = process.arch, fetchFn = fetch, exec = spawnSync } = {}) {
12
+ const base = nodeHome(home);
13
+ const bin = join(base, platform === 'win32' ? 'node.exe' : 'bin/node');
14
+ if (existsSync(bin)) return { ok: true, bin, installed: false };
15
+ if (!['darwin', 'linux', 'win32'].includes(platform) || !['arm64', 'x64'].includes(arch)) {
16
+ return { ok: false, next: 'Ask the connected AI to install Node.js 22 or newer for this operating system.', open: 'https://nodejs.org/en/download' };
17
+ }
18
+ let scratch;
19
+ try {
20
+ const get = async (url) => {
21
+ const r = await fetchFn(url, { signal: AbortSignal.timeout(15_000) });
22
+ if (!r.ok) throw new Error(`Node download answered ${r.status}`);
23
+ return r;
24
+ };
25
+ const url = 'https://nodejs.org/dist/latest-v22.x';
26
+ const sums = await (await get(`${url}/SHASUMS256.txt`)).text();
27
+ const suffix = `${platform === 'win32' ? 'win' : platform}-${arch}.${platform === 'win32' ? 'zip' : 'tar.gz'}`;
28
+ const entry = sums.split('\n').map((s) => s.trim().split(/\s+/)).find(([hash, file]) => /^[a-f0-9]{64}$/.test(hash) && new RegExp(`^node-v22\\.\\d+\\.\\d+-${suffix.replaceAll('.', '\\.')}$`).test(file ?? ''));
29
+ if (!entry) throw new Error('No supported Node archive in the official checksum list');
30
+ const [hash, file] = entry;
31
+ const data = Buffer.from(await (await get(`https://nodejs.org/dist/${file.match(/^node-(v22\.\d+\.\d+)-/)[1]}/${file}`)).arrayBuffer());
32
+ if (createHash('sha256').update(data).digest('hex') !== hash) throw new Error('Node archive checksum did not match');
33
+ mkdirSync(join(home, '.cache', 'homie-studio'), { recursive: true });
34
+ scratch = mkdtempSync(join(home, '.cache', 'homie-studio', 'node-'));
35
+ const archive = join(scratch, file);
36
+ writeFileSync(archive, data);
37
+ const r = exec('tar', ['-xf', archive, '-C', scratch], { encoding: 'utf8', timeout: 10_000 });
38
+ if (r.status !== 0) throw new Error('Could not unpack Node with the system archive tool');
39
+ const unpacked = join(scratch, file.replace(/\.(tar\.gz|zip)$/, ''));
40
+ const check = exec(join(unpacked, platform === 'win32' ? 'node.exe' : 'bin/node'), ['--version'], { encoding: 'utf8', timeout: 3000 });
41
+ if (check.status !== 0 || !/^v22\./.test(check.stdout ?? '')) throw new Error('Downloaded Node did not start');
42
+ renameSync(unpacked, base);
43
+ return { ok: true, bin, installed: true };
44
+ } catch (error) {
45
+ return { ok: false, why: error.message, next: 'AI: retry setup_prepare; if the network still refuses, use the Node installer.', open: 'https://nodejs.org/en/download' };
46
+ } finally { if (scratch) rmSync(scratch, { recursive: true, force: true }); }
47
+ }
package/lib/scaffold.mjs CHANGED
@@ -132,6 +132,16 @@ directory lists its games; homie.rocks does not host them.
132
132
  | \`studio.json\` | The studio's name, slug, Cloudflare resource names, custom domain and stats sharing. \`.studio/\` (git-ignored) is this computer's own state. |
133
133
  | \`.claude/skills/\` | Skills only this studio uses. Homie's own skills come from the Homie plugin. |
134
134
 
135
+ The person describes an outcome; you carry it through setup, building and checks without a tutorial
136
+ or a planning interview. Choose engineering and design defaults from their words and record them
137
+ in the codex. Use "My Studio" when unnamed, phones and computers, local sound and CC0 art.
138
+ They can change anything by asking. A business or charity may want an app, not a game.
139
+ Install missing prerequisites yourself; never ask the person to run a command or handle a key,
140
+ account id, zone id or setting. Honour an existing request to go live or list, subject to host holds.
141
+ For a requested custom hostname, add its custom_domain route in Wrangler along with cloudflare.domain;
142
+ Cloudflare creates DNS and TLS during deploy with the existing sign-in.
143
+
144
+
135
145
  ## Apps: one screen, roles and parts
136
146
 
137
147
  An app lives in \`apps/<id>/app.json\` and \`src/\`. Make one with \`homie-studio app new <id>\` (or \`app_make\`). Use the Homie plugin's \`app\` skill for businesses, venues, causes and customer apps. No game demo, rounds, scores, bots or mandatory fun: build what the person needs with the fewest human steps.
@@ -2030,6 +2030,47 @@
2030
2030
  }
2031
2031
  }
2032
2032
  }
2033
+ },
2034
+ "0.37.1": {
2035
+ "files": {
2036
+ "CLAUDE.md": "ddf2b2a0376202f6",
2037
+ "README.md": "684b3cca8001673c",
2038
+ "HANDOFF.md": "29337e2ace95b2e7",
2039
+ "games/README.md": "5ad32fe80c61f117",
2040
+ "music/README.md": "a3dc042dadd39df8",
2041
+ "videos/README.md": "16babc3437d5545b",
2042
+ "posts/README.md": "37841aae476bd540",
2043
+ "site/README.md": "732894273a32dc3f",
2044
+ "site/src/worker.mjs": "fc90f3609cad18ba"
2045
+ },
2046
+ "sections": {
2047
+ "AGENTS.md": {
2048
+ "(intro)": "214aa46eb3a895d5",
2049
+ "## Layout": "c18362ed28975d81",
2050
+ "## Apps: one screen, roles and parts": "4db0abe4cdfac09f",
2051
+ "## Commands (all through the pinned CLI in node_modules)": "a7f290579585368c",
2052
+ "## The site": "0cb8dd9a6f8a0096",
2053
+ "## Making games": "2da7fa74c8884834",
2054
+ "## The Game Codex and progress": "0738ef719cef3a4c",
2055
+ "## Art direction and models": "a48fff7bf0a5f2fc",
2056
+ "## Running live games (the back office)": "51ebf2ab756f6e4d",
2057
+ "## Servers and AI seats": "98d08705087a33ec",
2058
+ "## Selling things (the shop)": "9e020d69dd6a954e",
2059
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
2060
+ "## Rules": "2e6b29afb85dcf7e",
2061
+ "## Telling Homie": "80b46fdd9e578db8",
2062
+ "## Beta": "9d02638207a26e73"
2063
+ }
2064
+ },
2065
+ "named": {
2066
+ "Homie Arcade": {
2067
+ "sections": {
2068
+ "AGENTS.md": {
2069
+ "## Commands (all through the pinned CLI in node_modules)": "7cad8fd10f62fef1"
2070
+ }
2071
+ }
2072
+ }
2073
+ }
2033
2074
  }
2034
2075
  }
2035
2076
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homie-rocks/studio",
3
- "version": "0.37.0",
3
+ "version": "0.37.1",
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",
@@ -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.37.0';
2
+ export const STUDIO_VERSION_TAG = '0.37.1';