@genex-ai/cli-demo 0.97.0-dev.278 → 0.98.0-dev.280

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.
Files changed (2) hide show
  1. package/dist/index.js +1172 -857
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -194,440 +194,379 @@ async function flushSentry(timeoutMs = 2e3) {
194
194
  // src/index.ts
195
195
  import * as Sentry2 from "@sentry/node";
196
196
 
197
- // src/commands/init.ts
198
- import path9 from "path";
197
+ // src/lib/auth.ts
198
+ import http from "http";
199
+ import crypto from "crypto";
200
+ import os3 from "os";
201
+ import readline from "readline";
202
+ import { spawn as spawn2 } from "child_process";
203
+ import { URL as URL2 } from "url";
199
204
 
200
- // src/lib/copy-templates.ts
201
- import fs2 from "fs/promises";
202
- import path2 from "path";
203
- function isGenexManaged(rel) {
204
- return rel.split(path2.sep).some((seg) => seg.startsWith("genex"));
205
+ // src/utils/colors.ts
206
+ var useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
207
+ var ESC = String.fromCharCode(27);
208
+ var code = (open, close) => (s) => useColor ? `${ESC}[${open}m${s}${ESC}[${close}m` : s;
209
+ var c = {
210
+ bold: code(1, 22),
211
+ dim: code(2, 22),
212
+ red: code(31, 39),
213
+ green: code(32, 39),
214
+ yellow: code(33, 39),
215
+ blue: code(34, 39),
216
+ cyan: code(36, 39),
217
+ gray: code(90, 39)
218
+ };
219
+
220
+ // src/lib/api.ts
221
+ var CLI_VERSION_HEADER = "x-genex-cli-version";
222
+ function formatUpdateRequired(body) {
223
+ const action = body.action ?? `npm i -D @genex-ai/cli-demo@${CLI_CHANNEL}`;
224
+ const message = body.message ?? `Genex CLI ${body.clientVersion ?? getCliVersion()} is below the minimum supported version${body.minVersion ? ` ${body.minVersion}` : ""}.`;
225
+ return [`${c.red("\u2717")} ${message}`, ` Update now \u2014 run: ${action} (then re-run this command)`];
205
226
  }
206
- async function copyTemplates(srcDir, destDir, opts = {}) {
207
- const result = { copied: [], updated: [], skipped: [] };
208
- await walk(srcDir, srcDir, destDir, opts, result);
209
- return result;
227
+ function shortDate(iso) {
228
+ const d = new Date(iso);
229
+ if (Number.isNaN(d.getTime())) return iso;
230
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
210
231
  }
211
- async function walk(rootSrc, src, dest, opts, result) {
212
- const entries = await fs2.readdir(src, { withFileTypes: true });
213
- for (const entry of entries) {
214
- const srcPath = path2.join(src, entry.name);
215
- const destPath = path2.join(dest, entry.name);
216
- const rel = path2.relative(rootSrc, srcPath);
217
- if (opts.exclude?.includes(rel)) continue;
218
- if (entry.isDirectory()) {
219
- await fs2.mkdir(destPath, { recursive: true });
220
- await walk(rootSrc, srcPath, destPath, opts, result);
221
- continue;
222
- }
223
- if (!entry.isFile()) {
224
- continue;
225
- }
226
- const present = await exists(destPath);
227
- const mayOverwrite = opts.force || isGenexManaged(rel);
228
- if (present && !mayOverwrite) {
229
- result.skipped.push(rel);
230
- continue;
231
- }
232
- await fs2.mkdir(path2.dirname(destPath), { recursive: true });
233
- await fs2.copyFile(srcPath, destPath);
234
- result.copied.push(rel);
235
- if (present) result.updated.push(rel);
232
+ function formatInsufficientCredits(body) {
233
+ const message = body.message ?? `this generation costs ${body.price ?? "?"} credits; your balance is ${body.balance ?? 0}.`;
234
+ const lines = [`${c.red("\u2717")} Out of credits \u2014 ${lowerFirst(message)}`];
235
+ if (body.refillAt && body.refillTo) {
236
+ lines.push(` Credits refill to ${body.refillTo} on ${shortDate(body.refillAt)}.`);
236
237
  }
238
+ if (body.url) lines.push(` Get more or check your balance: ${body.url}`);
239
+ return lines;
237
240
  }
238
- async function exists(p) {
239
- try {
240
- await fs2.access(p);
241
- return true;
242
- } catch {
243
- return false;
244
- }
241
+ function formatVerificationRequired(body) {
242
+ const lines = [
243
+ `${c.red("\u2717")} Email not verified \u2014 verify your email to unlock your free generation credits.`
244
+ ];
245
+ if (body.url) lines.push(` Verify here: ${body.url} (then re-run this command)`);
246
+ return lines;
245
247
  }
246
-
247
- // src/lib/agents-contract.ts
248
- import fs3 from "fs/promises";
249
- import path3 from "path";
250
- var CONTRACT_BEGIN = "<!-- genex:contract:begin (managed by genex \u2014 edits inside this block are overwritten on sync) -->";
251
- var CONTRACT_END = "<!-- genex:contract:end -->";
252
- var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
253
- # Genex build contract (always in effect for this game)
254
-
255
- Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
256
-
257
- 1. Load the \`genex-game-director\` skill before starting the requested work. Route from the player's latest clear request: a focused request starts directly without replaying discovery or commissioning unrelated lanes. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
258
- 2. Ask only when one unresolved answer materially changes the work. If the request is clear, do not repeat an interview, confirm the pitch, force a concept round, or ask whole-game-vs-one-part again. For a genuinely broad new game, ask one decision: whole coordinated build or one request-relevant part first. If there is no clear request \u2014 a setup prompt pasted with nothing of their own \u2014 ask in PLAIN CHAT what they want to make, in their own words, and never fill the blank by pitching concepts. That opening question stays in chat on purpose: their reply carries the whole request, including anything they say about HOW you should work, and a menu of options answers a narrower question than the one they need to answer. Once you know what you are building, ask one decision at a time with your built-in question tool with clickable answer options when you have one; use a short plain-chat question otherwise. If the player stays silent after a necessary question, proceed on stated assumptions where reasonable and record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
259
- 3. \`DESIGN.md\` at the project root is the durable design contract AND build plan. Keep three truths distinct: the player's requested outcome, the current \`Now:\` focus, and open commitments. It must carry a \`## Build plan & status\` section with the working mode (\`whole coordinated build\`, \`step by step\`, or \`focused change\`), numbered milestones with status marks, and a \`Now:\` line naming the current one \u2014 a milestone is done only when its work reached a preview. The latest clear request may replace \`Now:\` immediately; it never silently shrinks the requested outcome or deletes unrelated commitments. Keep every decision, assumption, generation id/URL, local output, and wiring state current. When the plan first lands, tell the player in one plain line that you recorded what they asked for in \`DESIGN.md\` and will keep it current.
260
- 4. ALL generated art, audio, video, characters, and UI come from \`genex\` commands \u2014 never from any other generation tool your platform bundles, unless the player explicitly asks for that tool by name. A local reference image is not a reason to switch tools: pass its file path to genex (\`--edit\` and \`--inpaint\` accept local paths). The same exclusivity covers shipping: building, previewing, and publishing go only through \`genex preview\` / \`genex publish\` \u2014 never load your platform's own site-building, hosting, or deploy skills for this game.
261
- 5. Generated UI art is a tool you reach for, not a pipeline you owe. A restrained interface built in clean CSS is a finished, legitimate HUD \u2014 not a placeholder. Reach for the sprite lane (\`genex-ai-hud\`) when the game's own style genuinely wants drawn chrome \u2014 ornate, painterly, comic, hand-made \u2014 or when the player asks for HUD art. Generating ONE element you decided the game needs \u2014 a frame, a mask, an icon, a wordmark, a menu backdrop, a menu video \u2014 is a normal use of these tools, never a half-run pipeline. There is NO global game-concept image and no UI plan recited in chat: the art direction lives in the game's brief in words. A lane's own concept step survives only where the player is choosing a concrete thing (a character's candidates). Whatever you do generate, run its quality steps in full \u2014 extraction, masks, wiring, \`npx genex ui audit\`.
262
- 6. Never draw a rectangular backing plate behind bars, digits, or icons \u2014 in sprites or CSS. Ornament lives on the widget's own silhouette; a truly needed shaped plate comes from \`npx genex ui plate\`.
263
- 7. Fonts: the brief's display + body pair comes from the menu skill's genre table (or carries a one-line stated reason) and is LOADED for real in \`index.html\`.
264
- 8. Never park ready work behind a question, and never stall on an unanswered one \u2014 decide, state the decision in chat, record it, keep building.
265
- 9. Before any publish and before ending a session: run \`npx genex wait\` on every generation you enqueued and wire in what landed \u2014 never park landed assets. Fonts the brief names are LOADED for real, Escape pauses, the loader shows something of the game rather than a black screen, and the player wears the game's own generated character (or DESIGN.md records why it doesn't).
266
- 10. The player's body is the game's own generated character (\`npx genex character "<look>"\` \u2192 \`npx genex controller character --character <id>\`), enqueued with your first art actions, not after them. It applies wherever a human body appears on screen \u2014 first-person included, the moment remotes, a look-down body, a shadow, or a menu portrait shows one. The profile VRM avatar is the FALLBACK: a temporary body while the character renders (say in one plain line that it's temporary), or the stand-in when generation genuinely could not happen \u2014 out of credits, failed, unverified; record which in DESIGN.md as \`Player character: VRM \u2014 <reason>\`. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
267
- 11. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification. Local-test evidence proves visuals and controls ONLY \u2014 label it that way when you show the player, and never work around the draft sign-in gate any other way.
268
- 12. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
269
- 13. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\` with \`<dashboard>\` from \`.genex/project.json\`, \`/world/<slug>\` once published) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. After every round of player feedback, end with a preview push.
270
- 14. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files. When the player asks for parallel work, repeated refinement passes, a critic reviewing your output, or names any way of working your platform provides, that IS your instruction \u2014 wherever it reaches you, including inside their answer to a question you asked \u2014 so adopt it as the working mode for the rest of the build, starting with the work in front of you, and never file it as a later milestone. While delegated work runs, keep building or talk to the player; never idle in a foreground wait for something your platform will tell you about.
271
- 15. Talk to the player in plain game language \u2014 what changed in the game and what to try; never code, file names, build output, or tool internals unless they ask. Short status lines while you work; long silent stretches are a failure.
272
- 16. Never add debug-only code to the game to check your own work \u2014 no hidden test modes, no special URL parameters, no forced-visible flags, no auth mocks, no pixel-sampling hooks. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
273
- 17. Input directions match their labels: A/\u2190 moves or turns the player screen-LEFT, D/\u2192 screen-RIGHT, mouse-up looks up, and drag-pan axes share ONE convention. The cursor is either the gameplay tool (RTS, card, builder) or locked away during play \u2014 keyboard-only games included. Check it in every milestone's smoke pass.
274
- 18. v0 is a milestone, not the destination. When the ask was bigger than one loop, every milestone after v0 grows back toward the FULL ask with DESIGN.md's content lines as the checklist \u2014 a slice that previewed well never quietly becomes the game. Cosmetics never jump the queue past promised content.
275
- ${CONTRACT_END}
276
- `;
277
- var CLAUDE_IMPORT_LINE = "@AGENTS.md";
278
- function mergeContractBlock(existing) {
279
- const block = GENEX_CONTRACT_BLOCK.trimEnd();
280
- if (existing === null || existing.trim() === "") return `${block}
281
- `;
282
- const region = /<!-- genex:contract:begin[^\n]*-->[\s\S]*?<!-- genex:contract:end -->/;
283
- if (region.test(existing)) {
284
- return existing.replace(region, block);
285
- }
286
- const cleaned = existing.split("\n").filter(
287
- (line) => !line.includes("genex:contract:begin") && !line.includes("genex:contract:end")
288
- ).join("\n").trimEnd();
289
- return cleaned === "" ? `${block}
290
- ` : `${cleaned}
291
-
292
- ${block}
293
- `;
248
+ function lowerFirst(s) {
249
+ return s ? s[0].toLowerCase() + s.slice(1) : s;
294
250
  }
295
- async function writeAgentsContract(projectDir) {
296
- let changed = false;
297
- try {
298
- const agentsPath = path3.join(projectDir, "AGENTS.md");
299
- let existing = null;
251
+ var structuredPrinted = /* @__PURE__ */ new WeakSet();
252
+ function printedStructuredError(res) {
253
+ return structuredPrinted.has(res);
254
+ }
255
+ async function apiFetch(url, init2 = {}) {
256
+ const headers = new Headers(init2.headers);
257
+ if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
258
+ const res = await fetch(url, { ...init2, headers });
259
+ if (res.status === 426) {
300
260
  try {
301
- existing = await fs3.readFile(agentsPath, "utf8");
261
+ const body = await res.clone().json();
262
+ if (body?.error === "cli_update_required") {
263
+ for (const line of formatUpdateRequired(body)) process.stderr.write(line + "\n");
264
+ }
302
265
  } catch {
303
- existing = null;
304
- }
305
- const next = mergeContractBlock(existing);
306
- if (next !== existing) {
307
- await fs3.writeFile(agentsPath, next, "utf8");
308
- changed = true;
309
266
  }
310
- const claudePath = path3.join(projectDir, "CLAUDE.md");
311
- let claude = null;
267
+ }
268
+ if (res.status === 402) {
312
269
  try {
313
- claude = await fs3.readFile(claudePath, "utf8");
270
+ const body = await res.clone().json();
271
+ if (body?.error === "insufficient_credits") {
272
+ for (const line of formatInsufficientCredits(body)) process.stderr.write(line + "\n");
273
+ structuredPrinted.add(res);
274
+ }
314
275
  } catch {
315
- claude = null;
316
276
  }
317
- if (claude === null) {
318
- await fs3.writeFile(claudePath, `${CLAUDE_IMPORT_LINE}
319
- `, "utf8");
320
- changed = true;
321
- } else if (!claude.split("\n").some((line) => line.trim() === CLAUDE_IMPORT_LINE)) {
322
- const sep = claude.endsWith("\n") ? "" : "\n";
323
- await fs3.writeFile(claudePath, `${claude}${sep}
324
- ${CLAUDE_IMPORT_LINE}
325
- `, "utf8");
326
- changed = true;
277
+ }
278
+ if (res.status === 403) {
279
+ try {
280
+ const body = await res.clone().json();
281
+ if (body?.error === "email_verification_required") {
282
+ for (const line of formatVerificationRequired(body)) process.stderr.write(line + "\n");
283
+ structuredPrinted.add(res);
284
+ }
285
+ } catch {
327
286
  }
328
- } catch {
329
287
  }
330
- return changed;
331
- }
332
-
333
- // src/lib/updates.ts
334
- import fs4 from "fs/promises";
335
- import os3 from "os";
336
- import path4 from "path";
337
- function parseSemver(v) {
338
- const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
339
- if (!m) return null;
340
- return [Number(m[1]), Number(m[2]), Number(m[3])];
341
- }
342
- function isNewerVersion(a, b) {
343
- const pa = parseSemver(a);
344
- const pb = parseSemver(b);
345
- if (!pa || !pb) return false;
346
- for (let i = 0; i < 3; i++) {
347
- if (pa[i] !== pb[i]) return pa[i] > pb[i];
288
+ if (res.status === 503) {
289
+ try {
290
+ const body = await res.clone().json();
291
+ if (body?.error === "generation_paused") {
292
+ process.stderr.write(
293
+ `${c.red("\u2717")} ${body.message ?? "Generation is temporarily paused platform-wide. Try again later."}
294
+ `
295
+ );
296
+ structuredPrinted.add(res);
297
+ }
298
+ } catch {
299
+ }
348
300
  }
349
- return false;
301
+ return res;
350
302
  }
351
- var SKILLS_VERSION_MARKER = "genex-skills-version.json";
352
- async function readSkillsMarker(skillsDir) {
303
+ async function fetchSignedInEmail(apiUrl, token) {
353
304
  try {
354
- const raw = await fs4.readFile(path4.join(skillsDir, SKILLS_VERSION_MARKER), "utf8");
355
- const parsed = JSON.parse(raw);
356
- return typeof parsed.version === "string" ? parsed.version : null;
305
+ const res = await apiFetch(`${apiUrl}/api/auth/get-session`, {
306
+ headers: { Authorization: `Bearer ${token}` }
307
+ });
308
+ if (!res.ok) return null;
309
+ const data = await res.json().catch(() => null);
310
+ return data?.user?.email ?? null;
357
311
  } catch {
358
312
  return null;
359
313
  }
360
314
  }
361
- async function writeSkillsMarker(skillsDir, version = getCliVersion()) {
362
- await fs4.mkdir(skillsDir, { recursive: true });
363
- await fs4.writeFile(
364
- path4.join(skillsDir, SKILLS_VERSION_MARKER),
365
- JSON.stringify({ version, syncedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
366
- );
367
- }
368
- async function hasGenexSkills(skillsDir) {
315
+
316
+ // src/lib/pending-auth.ts
317
+ import fs3 from "fs/promises";
318
+ import path3 from "path";
319
+
320
+ // src/lib/env.ts
321
+ import fs2 from "fs/promises";
322
+ import path2 from "path";
323
+ import { spawn } from "child_process";
324
+ async function writeEnvVar(envPath, key, value) {
325
+ let content = "";
326
+ let existed = false;
369
327
  try {
370
- const entries = await fs4.readdir(skillsDir);
371
- return entries.some((name) => name.startsWith("genex-"));
328
+ content = await fs2.readFile(envPath, "utf8");
329
+ existed = true;
372
330
  } catch {
373
- return false;
374
331
  }
375
- }
376
- var REMOVED_SKILLS = [
377
- "genex-explore",
378
- "genex-threejs-skill-router",
379
- "genex-threejs-bloom",
380
- "genex-threejs-screen-space-ambient-occlusion",
381
- "genex-threejs-atmosphere-aerial-perspective",
382
- "genex-threejs-volumetric-clouds",
383
- "genex-threejs-spectral-ocean",
384
- "genex-threejs-water-optics",
385
- "genex-threejs-temporal-surfaces",
386
- "genex-threejs-raymarched-space-effects",
387
- "genex-threejs-procedural-architecture",
388
- "genex-threejs-procedural-fields",
389
- "genex-threejs-procedural-geometry",
390
- "genex-threejs-procedural-planets",
391
- "genex-threejs-procedural-vegetation",
392
- "genex-threejs-image-pipeline",
393
- "genex-threejs-lighting-design",
394
- "genex-threejs-precipitation-surfaces",
395
- "genex-threejs-game-content",
396
- "genex-threejs-open-world"
397
- ];
398
- async function pruneRemovedSkills(skillsDir, log) {
399
- const removed = [];
400
- for (const name of REMOVED_SKILLS) {
401
- const target = path4.join(skillsDir, name);
402
- try {
403
- await fs4.access(target);
404
- } catch {
405
- continue;
406
- }
407
- try {
408
- await fs4.rm(target, { recursive: true });
409
- removed.push(name);
410
- } catch {
411
- }
412
- }
413
- if (removed.length > 0) {
414
- log?.plain(
415
- `\u{1F9F9} Removed retired Genex skill${removed.length > 1 ? "s" : ""}: ${removed.join(", ")}`
416
- );
332
+ const assignment = `${key}=${formatValue(value)}`;
333
+ const keyPattern = new RegExp(
334
+ `^(\\s*export\\s+)?${escapeRegExp(key)}=.*$`,
335
+ "gm"
336
+ );
337
+ let next;
338
+ let mode;
339
+ if (keyPattern.test(content)) {
340
+ next = content.replace(keyPattern, assignment);
341
+ mode = "updated";
342
+ } else {
343
+ let prefix = content;
344
+ if (prefix.length > 0 && !prefix.endsWith("\n")) prefix += "\n";
345
+ next = prefix + assignment + "\n";
346
+ mode = existed ? "appended" : "created";
417
347
  }
418
- return removed.length > 0;
419
- }
420
- async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion(), log) {
421
- const skillsDir = path4.join(target.baseDir, "skills");
422
- if (!await hasGenexSkills(skillsDir)) return false;
423
- if (await readSkillsMarker(skillsDir) === version) return false;
424
- const src = target.full ? templatesDir : path4.join(templatesDir, "skills");
425
- const dest = target.full ? target.baseDir : skillsDir;
426
- await copyTemplates(src, dest, { exclude: ["controllers", "motion"] });
427
- await pruneRemovedSkills(skillsDir, log);
428
- await writeSkillsMarker(skillsDir, version);
429
- return true;
348
+ await fs2.mkdir(path2.dirname(envPath), { recursive: true });
349
+ await fs2.writeFile(envPath, next, { mode: 384 });
350
+ await restrictFilePermissions(envPath);
351
+ return { mode, path: envPath };
430
352
  }
431
- async function cleanupLegacyGlobalSkills(log) {
432
- try {
433
- const home = os3.homedir();
434
- const [realCwd, realHome] = await Promise.all([
435
- fs4.realpath(process.cwd()).catch(() => path4.resolve(process.cwd())),
436
- fs4.realpath(home).catch(() => path4.resolve(home))
437
- ]);
438
- if (realCwd === realHome) return false;
439
- let removed = false;
440
- for (const dirName of [".claude", ".codex", ".cursor"]) {
441
- const skillsDir = path4.join(home, dirName, "skills");
442
- let entries = [];
443
- try {
444
- entries = await fs4.readdir(skillsDir);
445
- } catch {
446
- continue;
447
- }
448
- for (const name of entries) {
449
- if (!name.startsWith("genex-")) continue;
450
- try {
451
- await fs4.rm(path4.join(skillsDir, name), { recursive: true });
452
- removed = true;
453
- } catch {
454
- }
455
- }
456
- }
457
- for (const rel of [
458
- path4.join("agents", "genex-helper.md"),
459
- path4.join("commands", "genex-status.md")
460
- ]) {
461
- try {
462
- await fs4.rm(path4.join(home, ".claude", rel));
463
- removed = true;
464
- } catch {
465
- }
466
- }
467
- if (removed) {
468
- log?.plain("\u{1F9F9} Removed legacy GLOBAL Genex skills \u2014 installs are project-local now");
469
- }
470
- return removed;
471
- } catch {
472
- return false;
353
+ async function restrictFilePermissions(filePath) {
354
+ if (process.platform !== "win32") {
355
+ await fs2.chmod(filePath, 384).catch(() => {
356
+ });
357
+ return;
473
358
  }
474
- }
475
- async function syncSkills(log) {
476
- try {
477
- await cleanupLegacyGlobalSkills(log);
478
- const version = getCliVersion();
479
- const templatesDir = getTemplatesDir();
480
- let refreshed = false;
481
- for (const target of resolveAgentTargets()) {
482
- try {
483
- refreshed = await syncSkillsForTarget(target, templatesDir, version, log) || refreshed;
484
- } catch {
485
- }
486
- }
487
- if (refreshed) log.plain(`\u{1F504} Genex skills updated to ${version}`);
359
+ const user = process.env.USERNAME ?? process.env.USER;
360
+ if (!user) return;
361
+ await new Promise((resolve) => {
488
362
  try {
489
- await fs4.access(path4.join(process.cwd(), ".genex"));
490
- if (await writeAgentsContract(process.cwd())) {
491
- log.plain("\u{1F504} Genex build contract refreshed (AGENTS.md managed block)");
492
- }
363
+ const child = spawn(
364
+ "icacls",
365
+ [filePath, "/inheritance:r", "/grant:r", `${user}:F`],
366
+ { stdio: "ignore" }
367
+ );
368
+ child.on("error", () => resolve());
369
+ child.on("close", () => resolve());
493
370
  } catch {
371
+ resolve();
494
372
  }
495
- } catch {
496
- }
373
+ });
497
374
  }
498
- var PUBLISHED_PACKAGES = [
499
- { name: "@genex-ai/cli-demo", label: "CLI", install: "npm i -D @genex-ai/cli-demo@latest" },
500
- { name: "@genex-ai/embed-sdk", label: "embed SDK", install: "npm i @genex-ai/embed-sdk@latest" },
501
- {
502
- name: "@genex-ai/multiplayer",
503
- label: "multiplayer SDK",
504
- install: "npm i @genex-ai/multiplayer@latest"
375
+ function formatValue(value) {
376
+ if (/[\s#"'$`\\]/.test(value)) {
377
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
505
378
  }
506
- ];
507
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
508
- var REGISTRY_TIMEOUT_MS = 1500;
509
- function getUpdateCachePath() {
510
- return path4.join(getGenexDir(), "update-check.json");
379
+ return value;
511
380
  }
512
- function isCacheFresh(cache, nowMs) {
513
- const at = Date.parse(cache.checkedAt);
514
- return Number.isFinite(at) && nowMs - at < CHECK_INTERVAL_MS;
381
+ function escapeRegExp(s) {
382
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
515
383
  }
516
- function wasRecentlyNotified(cache, name, latest, nowMs) {
517
- if (cache.notified?.[name] !== latest) return false;
518
- const at = Date.parse(cache.notifiedAt ?? "");
519
- return Number.isFinite(at) && nowMs - at < CHECK_INTERVAL_MS;
384
+
385
+ // src/lib/pending-auth.ts
386
+ function pendingAuthPath() {
387
+ return path3.join(getGenexDir(), "pending-auth.json");
520
388
  }
521
- async function readUpdateCache() {
389
+ async function readPendingAuth(apiUrl) {
522
390
  try {
523
- const raw = await fs4.readFile(getUpdateCachePath(), "utf8");
524
- const parsed = JSON.parse(raw);
525
- if (typeof parsed.checkedAt !== "string" || typeof parsed.latest !== "object") return null;
526
- return parsed;
391
+ const raw = JSON.parse(await fs3.readFile(pendingAuthPath(), "utf8"));
392
+ if (!raw?.deviceCode || !raw.userCode || raw.apiUrl !== apiUrl) return null;
393
+ if (Date.now() >= raw.expiresAt) return null;
394
+ return raw;
527
395
  } catch {
528
396
  return null;
529
397
  }
530
398
  }
531
- async function fetchLatestFromRegistry(name) {
399
+ async function writePendingAuth(pending) {
400
+ const file = pendingAuthPath();
401
+ await fs3.mkdir(path3.dirname(file), { recursive: true });
402
+ await fs3.writeFile(file, JSON.stringify(pending, null, 2), { mode: 384 });
403
+ await restrictFilePermissions(file);
404
+ }
405
+ async function clearPendingAuth() {
406
+ await fs3.rm(pendingAuthPath(), { force: true }).catch(() => {
407
+ });
408
+ }
409
+
410
+ // src/lib/auth.ts
411
+ var DEFAULT_INLINE_WAIT_MS = 100 * 1e3;
412
+ var MIN_POLL_INTERVAL_MS = 1e3;
413
+ var REASSURE_EVERY_MS = 30 * 1e3;
414
+ var AuthPendingError = class extends Error {
415
+ // Plain fields, not parameter properties: `erasableSyntaxOnly` (Node runs the
416
+ // .ts sources directly by stripping types) forbids the shorthand.
417
+ userCode;
418
+ verifyUrl;
419
+ constructor(userCode, verifyUrl) {
420
+ super("Authorization is still pending.");
421
+ this.name = "AuthPendingError";
422
+ this.userCode = userCode;
423
+ this.verifyUrl = verifyUrl;
424
+ }
425
+ };
426
+ function formatUserCode(code2) {
427
+ return code2.length === 8 ? `${code2.slice(0, 4)}-${code2.slice(4)}` : code2;
428
+ }
429
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
430
+ async function authorize(apiBaseUrl, authBaseUrl, options) {
431
+ const { log, inlineWaitMs = DEFAULT_INLINE_WAIT_MS, open = openBrowser, label } = options;
432
+ const resumed = await readPendingAuth(apiBaseUrl);
433
+ if (resumed) {
434
+ log.step("Picking up where the last sign-in left off\u2026");
435
+ printCode(log, resumed.userCode, resumed.verifyUrl);
436
+ return await pollForToken(apiBaseUrl, resumed, { log, inlineWaitMs });
437
+ }
438
+ let started;
532
439
  try {
533
- const res = await fetch(
534
- `https://registry.npmjs.org/-/package/${encodeURIComponent(name)}/dist-tags`,
535
- { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) }
536
- );
537
- if (!res.ok) return null;
538
- const tags = await res.json();
539
- return typeof tags.latest === "string" && parseSemver(tags.latest) ? tags.latest : null;
540
- } catch {
541
- return null;
440
+ started = await startDeviceAuth(apiBaseUrl, label);
441
+ } catch (err) {
442
+ if (err instanceof LegacyApiError) {
443
+ log.dim("This Genex API doesn't support device sign-in yet \u2014 using the legacy browser flow.");
444
+ return await authorizeLoopback(authBaseUrl, { log, open, timeoutMs: inlineWaitMs });
445
+ }
446
+ throw err;
542
447
  }
448
+ const pending = {
449
+ deviceCode: started.deviceCode,
450
+ userCode: started.userCode,
451
+ verifyUrl: started.verifyUrl,
452
+ expiresAt: Date.now() + started.expiresIn * 1e3,
453
+ apiUrl: apiBaseUrl
454
+ };
455
+ await writePendingAuth(pending);
456
+ printCode(log, started.userCode, started.verifyUrl);
457
+ let warned = false;
458
+ const warnManual = () => {
459
+ if (warned) return;
460
+ warned = true;
461
+ log.dim(" (couldn't open a browser here \u2014 open the link above yourself)");
462
+ };
463
+ if (!open(started.verifyUrl, warnManual)) warnManual();
464
+ return await pollForToken(apiBaseUrl, pending, { log, inlineWaitMs, interval: started.interval });
543
465
  }
544
- function startUpdateCheck() {
545
- return (async () => {
546
- try {
547
- const cached = await readUpdateCache();
548
- if (cached && isCacheFresh(cached, Date.now())) return cached;
549
- const latest = { ...cached?.latest ?? {} };
550
- const results = await Promise.allSettled(
551
- PUBLISHED_PACKAGES.map((p) => fetchLatestFromRegistry(p.name))
466
+ var LegacyApiError = class extends Error {
467
+ };
468
+ async function startDeviceAuth(apiBaseUrl, label) {
469
+ const url = `${apiBaseUrl}/api/cli/device/start`;
470
+ const body = JSON.stringify({ label: label ?? defaultLabel() });
471
+ for (let attempt = 0; attempt < 2; attempt++) {
472
+ const res = await apiFetch(url, {
473
+ method: "POST",
474
+ headers: { "Content-Type": "application/json" },
475
+ body
476
+ });
477
+ if (res.status === 404 || res.status === 501) throw new LegacyApiError();
478
+ if (res.status === 409) continue;
479
+ if (!res.ok) {
480
+ throw new Error(
481
+ `Couldn't start sign-in (HTTP ${res.status}). Check your connection and re-run this command.`
552
482
  );
553
- results.forEach((r, i) => {
554
- if (r.status === "fulfilled" && r.value) latest[PUBLISHED_PACKAGES[i].name] = r.value;
555
- });
556
- const next = {
557
- checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
558
- latest,
559
- ...cached?.notified ? { notified: cached.notified } : {},
560
- ...cached?.notifiedAt ? { notifiedAt: cached.notifiedAt } : {}
561
- };
562
- await fs4.mkdir(getGenexDir(), { recursive: true });
563
- await fs4.writeFile(getUpdateCachePath(), JSON.stringify(next, null, 2) + "\n");
564
- return next;
565
- } catch {
566
- return null;
567
483
  }
568
- })();
484
+ return await res.json();
485
+ }
486
+ throw new Error("Couldn't start sign-in \u2014 please re-run this command.");
569
487
  }
570
- async function installedPackageVersion(cwd, name) {
488
+ function defaultLabel() {
571
489
  try {
572
- const raw = await fs4.readFile(path4.join(cwd, "node_modules", name, "package.json"), "utf8");
573
- const pkg = JSON.parse(raw);
574
- return typeof pkg.version === "string" ? pkg.version : null;
490
+ return os3.hostname();
575
491
  } catch {
576
- return null;
492
+ return "";
577
493
  }
578
494
  }
579
- function formatNudge(label, latest, installed, install) {
580
- return `\u2B06 Genex ${label} ${latest} available (installed ${installed}) \u2014 run: ${install}`;
495
+ function printCode(log, userCode, verifyUrl) {
496
+ log.plain("");
497
+ log.step("Connect this project to your Genex account.");
498
+ log.plain(
499
+ [
500
+ "",
501
+ ` ${c.cyan("\u2192")} Open: ${c.cyan(verifyUrl)}`,
502
+ ` ${c.cyan("\u2192")} Code: ${c.bold(formatUserCode(userCode))}`,
503
+ "",
504
+ " The link works on any device \u2014 your phone, or another computer.",
505
+ " Sign up there if you don't have an account yet; this waits for you.",
506
+ ""
507
+ ].join("\n")
508
+ );
581
509
  }
582
- async function reportUpdateNudges(check, log, cwd = process.cwd(), cachePath = getUpdateCachePath()) {
583
- try {
584
- const cache = await check;
585
- if (!cache) return;
586
- const now = Date.now();
587
- const lines = [];
588
- const announced = {};
589
- let whatsNew = null;
590
- for (const pkg of PUBLISHED_PACKAGES) {
591
- if (pkg.name === "@genex-ai/cli-demo" && CLI_CHANNEL === "dev") continue;
592
- const latest = cache.latest[pkg.name];
593
- if (!latest) continue;
594
- if (wasRecentlyNotified(cache, pkg.name, latest, now)) continue;
595
- const installed = pkg.name === "@genex-ai/cli-demo" ? getCliVersion() : await installedPackageVersion(cwd, pkg.name);
596
- if (!installed) continue;
597
- if (isNewerVersion(latest, installed)) {
598
- lines.push(formatNudge(pkg.label, latest, installed, pkg.install));
599
- announced[pkg.name] = latest;
600
- whatsNew ??= `https://www.npmjs.com/package/${pkg.name}`;
510
+ async function pollForToken(apiBaseUrl, pending, opts) {
511
+ const { log, inlineWaitMs } = opts;
512
+ const deadline = Date.now() + inlineWaitMs;
513
+ let intervalMs = Math.max((opts.interval ?? 2) * 1e3, MIN_POLL_INTERVAL_MS);
514
+ let nextReassureAt = Date.now() + REASSURE_EVERY_MS;
515
+ for (; ; ) {
516
+ let res;
517
+ try {
518
+ res = await apiFetch(`${apiBaseUrl}/api/cli/device/poll`, {
519
+ method: "POST",
520
+ headers: { "Content-Type": "application/json" },
521
+ body: JSON.stringify({ deviceCode: pending.deviceCode })
522
+ });
523
+ } catch {
524
+ res = new Response(null, { status: 599 });
525
+ }
526
+ if (res.status === 410) {
527
+ await clearPendingAuth();
528
+ throw new Error("That sign-in code expired. Re-run this command for a fresh one.");
529
+ }
530
+ if (res.ok) {
531
+ const body = await res.json().catch(() => null);
532
+ if (body?.status === "approved" && body.token) {
533
+ await clearPendingAuth();
534
+ return body.token;
601
535
  }
536
+ if (body?.status === "denied") {
537
+ await clearPendingAuth();
538
+ throw new Error("Sign-in was cancelled in the browser. Re-run this command to try again.");
539
+ }
540
+ if (body?.interval) intervalMs = Math.max(body.interval * 1e3, MIN_POLL_INTERVAL_MS);
602
541
  }
603
- if (lines.length === 0) return;
604
- for (const line of lines) log.plain(line);
605
- log.dim(
606
- ` Apply at a safe moment (never mid-task) \u2014 see the genex-updates skill. What's new: ${whatsNew}`
607
- );
608
- await recordNotified(cache, announced, cachePath);
609
- } catch {
542
+ if (Date.now() + intervalMs > deadline) {
543
+ throw new AuthPendingError(pending.userCode, pending.verifyUrl);
544
+ }
545
+ if (Date.now() >= nextReassureAt) {
546
+ log.dim(" still waiting for you to approve\u2026");
547
+ nextReassureAt = Date.now() + REASSURE_EVERY_MS;
548
+ }
549
+ await sleep(intervalMs);
610
550
  }
611
551
  }
612
- async function recordNotified(cache, announced, cachePath) {
613
- try {
614
- const next = {
615
- ...cache,
616
- notified: { ...cache.notified ?? {}, ...announced },
617
- notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
618
- };
619
- await fs4.mkdir(path4.dirname(cachePath), { recursive: true });
620
- await fs4.writeFile(cachePath, JSON.stringify(next, null, 2) + "\n");
621
- } catch {
622
- }
552
+ function printAuthHandoff(log, err) {
553
+ log.plain("");
554
+ log.warn("Not approved yet \u2014 nothing is broken, the sign-in is still waiting for you.");
555
+ log.plain(
556
+ [
557
+ ` ${c.cyan("\u2192")} Approve at ${c.cyan(err.verifyUrl)} (code ${c.bold(formatUserCode(err.userCode))})`,
558
+ ` ${c.cyan("\u2192")} Then run ${c.cyan("npx genex auth")} \u2014 it picks up exactly where this left off.`,
559
+ ""
560
+ ].join("\n")
561
+ );
562
+ }
563
+ async function resumeAuthorization(apiBaseUrl, options) {
564
+ const pending = await readPendingAuth(apiBaseUrl);
565
+ if (!pending) return null;
566
+ const { log, inlineWaitMs = DEFAULT_INLINE_WAIT_MS } = options;
567
+ printCode(log, pending.userCode, pending.verifyUrl);
568
+ return await pollForToken(apiBaseUrl, pending, { log, inlineWaitMs });
623
569
  }
624
-
625
- // src/lib/auth.ts
626
- import http from "http";
627
- import crypto from "crypto";
628
- import readline from "readline";
629
- import { spawn } from "child_process";
630
- import { URL as URL2 } from "url";
631
570
  var SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Genex</title>
632
571
  <style>body{font-family:system-ui,sans-serif;background:#0b0b0f;color:#eaeaea;display:grid;place-items:center;height:100vh;margin:0}
633
572
  .card{text-align:center;padding:2rem 3rem;border:1px solid #26262e;border-radius:14px;background:#13131a}
@@ -636,7 +575,7 @@ h1{font-size:1.3rem;margin:0 0 .5rem}p{color:#9a9aa6;margin:0}</style></head>
636
575
  var ERROR_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Genex</title></head>
637
576
  <body style="font-family:system-ui,sans-serif"><h1>Authorization failed</h1>
638
577
  <p>No token was provided. Please return to your terminal and try again.</p></body></html>`;
639
- async function authorize(authBaseUrl, options) {
578
+ async function authorizeLoopback(authBaseUrl, options) {
640
579
  const {
641
580
  log,
642
581
  timeoutMs = 5 * 60 * 1e3,
@@ -833,7 +772,7 @@ function openBrowser(url, onError = () => {
833
772
  }
834
773
  }
835
774
  try {
836
- const child = spawn(command, args, {
775
+ const child = spawn2(command, args, {
837
776
  stdio: "ignore",
838
777
  detached: true
839
778
  });
@@ -871,117 +810,566 @@ function tokenizeCommand(input) {
871
810
  return tokens2;
872
811
  }
873
812
 
874
- // src/utils/colors.ts
875
- var useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
876
- var ESC = String.fromCharCode(27);
877
- var code = (open, close) => (s) => useColor ? `${ESC}[${open}m${s}${ESC}[${close}m` : s;
878
- var c = {
879
- bold: code(1, 22),
880
- dim: code(2, 22),
881
- red: code(31, 39),
882
- green: code(32, 39),
883
- yellow: code(33, 39),
884
- blue: code(34, 39),
885
- cyan: code(36, 39),
886
- gray: code(90, 39)
887
- };
888
-
889
- // src/lib/api.ts
890
- var CLI_VERSION_HEADER = "x-genex-cli-version";
891
- function formatUpdateRequired(body) {
892
- const action = body.action ?? `npm i -D @genex-ai/cli-demo@${CLI_CHANNEL}`;
893
- const message = body.message ?? `Genex CLI ${body.clientVersion ?? getCliVersion()} is below the minimum supported version${body.minVersion ? ` ${body.minVersion}` : ""}.`;
894
- return [`${c.red("\u2717")} ${message}`, ` Update now \u2014 run: ${action} (then re-run this command)`];
813
+ // src/lib/store.ts
814
+ import fs4 from "fs/promises";
815
+ import path4 from "path";
816
+ function getProjectMetadataPath(cwd = process.cwd()) {
817
+ return path4.join(cwd, ".genex", "project.json");
895
818
  }
896
- function shortDate(iso) {
897
- const d = new Date(iso);
898
- if (Number.isNaN(d.getTime())) return iso;
899
- return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
819
+ async function writeUserToken(token, envPath) {
820
+ const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
821
+ return { path: written };
900
822
  }
901
- function formatInsufficientCredits(body) {
902
- const message = body.message ?? `this generation costs ${body.price ?? "?"} credits; your balance is ${body.balance ?? 0}.`;
903
- const lines = [`${c.red("\u2717")} Out of credits \u2014 ${lowerFirst(message)}`];
904
- if (body.refillAt && body.refillTo) {
905
- lines.push(` Credits refill to ${body.refillTo} on ${shortDate(body.refillAt)}.`);
823
+ async function rotateRejectedEnv(envPath) {
824
+ const file = getGenexEnvPath(envPath);
825
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
826
+ const aside = `${file}.rejected-${stamp}`;
827
+ try {
828
+ await fs4.rename(file, aside);
829
+ return aside;
830
+ } catch {
831
+ return null;
906
832
  }
907
- if (body.url) lines.push(` Get more or check your balance: ${body.url}`);
908
- return lines;
909
833
  }
910
- function formatVerificationRequired(body) {
911
- const lines = [
912
- `${c.red("\u2717")} Email not verified \u2014 verify your email to unlock your free generation credits.`
913
- ];
914
- if (body.url) lines.push(` Verify here: ${body.url} (then re-run this command)`);
915
- return lines;
834
+ async function readUserToken(envPath) {
835
+ const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
836
+ if (fromGenex) return fromGenex;
837
+ if (!envPath && !process.env[ENV_FILE_ENV]) {
838
+ return readTokenFromFile(path4.join(process.cwd(), ".env"));
839
+ }
840
+ return null;
916
841
  }
917
- function lowerFirst(s) {
918
- return s ? s[0].toLowerCase() + s.slice(1) : s;
842
+ async function readTokenFromFile(file) {
843
+ let content;
844
+ try {
845
+ content = await fs4.readFile(file, "utf8");
846
+ } catch {
847
+ return null;
848
+ }
849
+ const m = content.match(/^\s*(?:export\s+)?GENEX_TOKEN=(.*)$/m);
850
+ if (!m) return null;
851
+ return stripQuotes(m[1].trim()) || null;
919
852
  }
920
- var structuredPrinted = /* @__PURE__ */ new WeakSet();
921
- function printedStructuredError(res) {
922
- return structuredPrinted.has(res);
853
+ function stripQuotes(v) {
854
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
855
+ return v.slice(1, -1);
856
+ }
857
+ return v;
923
858
  }
924
- async function apiFetch(url, init2 = {}) {
925
- const headers = new Headers(init2.headers);
926
- if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
927
- const res = await fetch(url, { ...init2, headers });
928
- if (res.status === 426) {
929
- try {
930
- const body = await res.clone().json();
931
- if (body?.error === "cli_update_required") {
932
- for (const line of formatUpdateRequired(body)) process.stderr.write(line + "\n");
933
- }
934
- } catch {
935
- }
859
+ async function readProject(cwd = process.cwd()) {
860
+ try {
861
+ const raw = await fs4.readFile(getProjectMetadataPath(cwd), "utf8");
862
+ return JSON.parse(raw);
863
+ } catch {
864
+ return null;
936
865
  }
937
- if (res.status === 402) {
938
- try {
939
- const body = await res.clone().json();
940
- if (body?.error === "insufficient_credits") {
941
- for (const line of formatInsufficientCredits(body)) process.stderr.write(line + "\n");
942
- structuredPrinted.add(res);
866
+ }
867
+ async function writeProject(meta, cwd = process.cwd()) {
868
+ const file = getProjectMetadataPath(cwd);
869
+ await fs4.mkdir(path4.dirname(file), { recursive: true });
870
+ await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
871
+ await fs4.chmod(file, 384).catch(() => {
872
+ });
873
+ return { path: file };
874
+ }
875
+
876
+ // src/utils/logger.ts
877
+ function createLogger(opts = {}) {
878
+ const out = (s) => {
879
+ if (!opts.quiet) process.stdout.write(s + "\n");
880
+ };
881
+ const err = (s) => {
882
+ process.stderr.write(s + "\n");
883
+ };
884
+ return {
885
+ info: (m) => out(`${c.cyan("i")} ${m}`),
886
+ success: (m) => out(`${c.green("\u2713")} ${m}`),
887
+ warn: (m) => out(`${c.yellow("!")} ${m}`),
888
+ error: (m) => err(`${c.red("\u2717")} ${m}`),
889
+ step: (m) => out(`${c.blue("\u203A")} ${m}`),
890
+ dim: (m) => out(c.dim(m)),
891
+ plain: (m) => out(m)
892
+ };
893
+ }
894
+
895
+ // src/commands/auth.ts
896
+ async function runAuth(opts) {
897
+ const log = createLogger({ quiet: opts.quiet });
898
+ const apiUrl = getApiUrl(opts.apiUrl);
899
+ const authUrl = getAuthUrl(opts.authUrl);
900
+ const inlineWaitMs = opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0;
901
+ if (!opts.force) {
902
+ const existing = await readUserToken(opts.envPath);
903
+ if (existing) {
904
+ const email2 = await fetchSignedInEmail(apiUrl, existing);
905
+ if (email2) {
906
+ log.success(`Already connected as ${c.cyan(email2)}.`);
907
+ log.dim(" Pass --force to connect a different account.");
908
+ return;
943
909
  }
944
- } catch {
910
+ log.warn("Your saved sign-in is no longer valid \u2014 reconnecting\u2026");
945
911
  }
946
912
  }
947
- if (res.status === 403) {
948
- try {
949
- const body = await res.clone().json();
950
- if (body?.error === "email_verification_required") {
951
- for (const line of formatVerificationRequired(body)) process.stderr.write(line + "\n");
952
- structuredPrinted.add(res);
953
- }
954
- } catch {
913
+ let token;
914
+ try {
915
+ token = await resumeAuthorization(apiUrl, { log, inlineWaitMs }) ?? "";
916
+ if (!token) token = await authorize(apiUrl, authUrl, { log, inlineWaitMs });
917
+ } catch (err) {
918
+ if (err instanceof AuthPendingError) {
919
+ printAuthHandoff(log, err);
920
+ process.exitCode = 1;
921
+ return;
955
922
  }
923
+ log.error(err instanceof Error ? err.message : String(err));
924
+ process.exitCode = 1;
925
+ return;
956
926
  }
957
- if (res.status === 503) {
958
- try {
959
- const body = await res.clone().json();
960
- if (body?.error === "generation_paused") {
961
- process.stderr.write(
962
- `${c.red("\u2717")} ${body.message ?? "Generation is temporarily paused platform-wide. Try again later."}
963
- `
964
- );
965
- structuredPrinted.add(res);
966
- }
967
- } catch {
927
+ const { path: tokenPath } = await writeUserToken(token, opts.envPath);
928
+ log.success(`Connected. Saved your token to ${c.cyan(tokenPath)}.`);
929
+ const email = await fetchSignedInEmail(apiUrl, token);
930
+ if (email) log.plain(` signed in as ${c.cyan(email)}`);
931
+ log.plain("");
932
+ log.dim("Now re-run the command you were doing (`genex init`, `genex preview`, \u2026).");
933
+ }
934
+
935
+ // src/commands/init.ts
936
+ import path10 from "path";
937
+
938
+ // src/lib/copy-templates.ts
939
+ import fs5 from "fs/promises";
940
+ import path5 from "path";
941
+ function isGenexManaged(rel) {
942
+ return rel.split(path5.sep).some((seg) => seg.startsWith("genex"));
943
+ }
944
+ async function copyTemplates(srcDir, destDir, opts = {}) {
945
+ const result = { copied: [], updated: [], skipped: [] };
946
+ await walk(srcDir, srcDir, destDir, opts, result);
947
+ return result;
948
+ }
949
+ async function walk(rootSrc, src, dest, opts, result) {
950
+ const entries = await fs5.readdir(src, { withFileTypes: true });
951
+ for (const entry of entries) {
952
+ const srcPath = path5.join(src, entry.name);
953
+ const destPath = path5.join(dest, entry.name);
954
+ const rel = path5.relative(rootSrc, srcPath);
955
+ if (opts.exclude?.includes(rel)) continue;
956
+ if (entry.isDirectory()) {
957
+ await fs5.mkdir(destPath, { recursive: true });
958
+ await walk(rootSrc, srcPath, destPath, opts, result);
959
+ continue;
960
+ }
961
+ if (!entry.isFile()) {
962
+ continue;
963
+ }
964
+ const present = await exists(destPath);
965
+ const mayOverwrite = opts.force || isGenexManaged(rel);
966
+ if (present && !mayOverwrite) {
967
+ result.skipped.push(rel);
968
+ continue;
968
969
  }
970
+ await fs5.mkdir(path5.dirname(destPath), { recursive: true });
971
+ await fs5.copyFile(srcPath, destPath);
972
+ result.copied.push(rel);
973
+ if (present) result.updated.push(rel);
969
974
  }
970
- return res;
971
975
  }
972
- async function fetchSignedInEmail(apiUrl, token) {
976
+ async function exists(p) {
977
+ try {
978
+ await fs5.access(p);
979
+ return true;
980
+ } catch {
981
+ return false;
982
+ }
983
+ }
984
+
985
+ // src/lib/agents-contract.ts
986
+ import fs6 from "fs/promises";
987
+ import path6 from "path";
988
+ var CONTRACT_BEGIN = "<!-- genex:contract:begin (managed by genex \u2014 edits inside this block are overwritten on sync) -->";
989
+ var CONTRACT_END = "<!-- genex:contract:end -->";
990
+ var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
991
+ # Genex build contract (always in effect for this game)
992
+
993
+ Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
994
+
995
+ 1. Load the \`genex-game-director\` skill before starting the requested work. Route from the player's latest clear request: a focused request starts directly without replaying discovery or commissioning unrelated lanes. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
996
+ 2. Ask only when one unresolved answer materially changes the work. If the request is clear, do not repeat an interview, confirm the pitch, force a concept round, or ask whole-game-vs-one-part again. For a genuinely broad new game, ask one decision: whole coordinated build or one request-relevant part first. If there is no clear request \u2014 a setup prompt pasted with nothing of their own \u2014 ask in PLAIN CHAT what they want to make, in their own words, and never fill the blank by pitching concepts. That opening question stays in chat on purpose: their reply carries the whole request, including anything they say about HOW you should work, and a menu of options answers a narrower question than the one they need to answer. Once you know what you are building, ask one decision at a time with your built-in question tool with clickable answer options when you have one; use a short plain-chat question otherwise. If the player stays silent after a necessary question, proceed on stated assumptions where reasonable and record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
997
+ 3. \`DESIGN.md\` at the project root is the durable design contract AND build plan. Keep three truths distinct: the player's requested outcome, the current \`Now:\` focus, and open commitments. It must carry a \`## Build plan & status\` section with the working mode (\`whole coordinated build\`, \`step by step\`, or \`focused change\`), numbered milestones with status marks, and a \`Now:\` line naming the current one \u2014 a milestone is done only when its work reached a preview. The latest clear request may replace \`Now:\` immediately; it never silently shrinks the requested outcome or deletes unrelated commitments. Keep every decision, assumption, generation id/URL, local output, and wiring state current. When the plan first lands, tell the player in one plain line that you recorded what they asked for in \`DESIGN.md\` and will keep it current.
998
+ 4. ALL generated art, audio, video, characters, and UI come from \`genex\` commands \u2014 never from any other generation tool your platform bundles, unless the player explicitly asks for that tool by name. A local reference image is not a reason to switch tools: pass its file path to genex (\`--edit\` and \`--inpaint\` accept local paths). The same exclusivity covers shipping: building, previewing, and publishing go only through \`genex preview\` / \`genex publish\` \u2014 never load your platform's own site-building, hosting, or deploy skills for this game.
999
+ 5. Generated UI art is a tool you reach for, not a pipeline you owe. A restrained interface built in clean CSS is a finished, legitimate HUD \u2014 not a placeholder. Reach for the sprite lane (\`genex-ai-hud\`) when the game's own style genuinely wants drawn chrome \u2014 ornate, painterly, comic, hand-made \u2014 or when the player asks for HUD art. Generating ONE element you decided the game needs \u2014 a frame, a mask, an icon, a wordmark, a menu backdrop, a menu video \u2014 is a normal use of these tools, never a half-run pipeline. There is NO global game-concept image and no UI plan recited in chat: the art direction lives in the game's brief in words. A lane's own concept step survives only where the player is choosing a concrete thing (a character's candidates). Whatever you do generate, run its quality steps in full \u2014 extraction, masks, wiring, \`npx genex ui audit\`.
1000
+ 6. Never draw a rectangular backing plate behind bars, digits, or icons \u2014 in sprites or CSS. Ornament lives on the widget's own silhouette; a truly needed shaped plate comes from \`npx genex ui plate\`.
1001
+ 7. Fonts: the brief's display + body pair comes from the menu skill's genre table (or carries a one-line stated reason) and is LOADED for real in \`index.html\`.
1002
+ 8. Never park ready work behind a question, and never stall on an unanswered one \u2014 decide, state the decision in chat, record it, keep building.
1003
+ 9. Before any publish and before ending a session: run \`npx genex wait\` on every generation you enqueued and wire in what landed \u2014 never park landed assets. Fonts the brief names are LOADED for real, Escape pauses, the loader shows something of the game rather than a black screen, and the player wears the game's own generated character (or DESIGN.md records why it doesn't).
1004
+ 10. The player's body is the game's own generated character (\`npx genex character "<look>"\` \u2192 \`npx genex controller character --character <id>\`), enqueued with your first art actions, not after them. It applies wherever a human body appears on screen \u2014 first-person included, the moment remotes, a look-down body, a shadow, or a menu portrait shows one. The profile VRM avatar is the FALLBACK: a temporary body while the character renders (say in one plain line that it's temporary), or the stand-in when generation genuinely could not happen \u2014 out of credits, failed, unverified; record which in DESIGN.md as \`Player character: VRM \u2014 <reason>\`. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
1005
+ 11. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification. Local-test evidence proves visuals and controls ONLY \u2014 label it that way when you show the player, and never work around the draft sign-in gate any other way.
1006
+ 12. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
1007
+ 13. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\` with \`<dashboard>\` from \`.genex/project.json\`, \`/world/<slug>\` once published) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. After every round of player feedback, end with a preview push.
1008
+ 14. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files. When the player asks for parallel work, repeated refinement passes, a critic reviewing your output, or names any way of working your platform provides, that IS your instruction \u2014 wherever it reaches you, including inside their answer to a question you asked \u2014 so adopt it as the working mode for the rest of the build, starting with the work in front of you, and never file it as a later milestone. While delegated work runs, keep building or talk to the player; never idle in a foreground wait for something your platform will tell you about.
1009
+ 15. Talk to the player in plain game language \u2014 what changed in the game and what to try; never code, file names, build output, or tool internals unless they ask. Short status lines while you work; long silent stretches are a failure.
1010
+ 16. Never add debug-only code to the game to check your own work \u2014 no hidden test modes, no special URL parameters, no forced-visible flags, no auth mocks, no pixel-sampling hooks. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
1011
+ 17. Input directions match their labels: A/\u2190 moves or turns the player screen-LEFT, D/\u2192 screen-RIGHT, mouse-up looks up, and drag-pan axes share ONE convention. The cursor is either the gameplay tool (RTS, card, builder) or locked away during play \u2014 keyboard-only games included. Check it in every milestone's smoke pass.
1012
+ 18. v0 is a milestone, not the destination. When the ask was bigger than one loop, every milestone after v0 grows back toward the FULL ask with DESIGN.md's content lines as the checklist \u2014 a slice that previewed well never quietly becomes the game. Cosmetics never jump the queue past promised content.
1013
+ ${CONTRACT_END}
1014
+ `;
1015
+ var CLAUDE_IMPORT_LINE = "@AGENTS.md";
1016
+ function mergeContractBlock(existing) {
1017
+ const block = GENEX_CONTRACT_BLOCK.trimEnd();
1018
+ if (existing === null || existing.trim() === "") return `${block}
1019
+ `;
1020
+ const region = /<!-- genex:contract:begin[^\n]*-->[\s\S]*?<!-- genex:contract:end -->/;
1021
+ if (region.test(existing)) {
1022
+ return existing.replace(region, block);
1023
+ }
1024
+ const cleaned = existing.split("\n").filter(
1025
+ (line) => !line.includes("genex:contract:begin") && !line.includes("genex:contract:end")
1026
+ ).join("\n").trimEnd();
1027
+ return cleaned === "" ? `${block}
1028
+ ` : `${cleaned}
1029
+
1030
+ ${block}
1031
+ `;
1032
+ }
1033
+ async function writeAgentsContract(projectDir) {
1034
+ let changed = false;
1035
+ try {
1036
+ const agentsPath = path6.join(projectDir, "AGENTS.md");
1037
+ let existing = null;
1038
+ try {
1039
+ existing = await fs6.readFile(agentsPath, "utf8");
1040
+ } catch {
1041
+ existing = null;
1042
+ }
1043
+ const next = mergeContractBlock(existing);
1044
+ if (next !== existing) {
1045
+ await fs6.writeFile(agentsPath, next, "utf8");
1046
+ changed = true;
1047
+ }
1048
+ const claudePath = path6.join(projectDir, "CLAUDE.md");
1049
+ let claude = null;
1050
+ try {
1051
+ claude = await fs6.readFile(claudePath, "utf8");
1052
+ } catch {
1053
+ claude = null;
1054
+ }
1055
+ if (claude === null) {
1056
+ await fs6.writeFile(claudePath, `${CLAUDE_IMPORT_LINE}
1057
+ `, "utf8");
1058
+ changed = true;
1059
+ } else if (!claude.split("\n").some((line) => line.trim() === CLAUDE_IMPORT_LINE)) {
1060
+ const sep = claude.endsWith("\n") ? "" : "\n";
1061
+ await fs6.writeFile(claudePath, `${claude}${sep}
1062
+ ${CLAUDE_IMPORT_LINE}
1063
+ `, "utf8");
1064
+ changed = true;
1065
+ }
1066
+ } catch {
1067
+ }
1068
+ return changed;
1069
+ }
1070
+
1071
+ // src/lib/updates.ts
1072
+ import fs7 from "fs/promises";
1073
+ import os4 from "os";
1074
+ import path7 from "path";
1075
+ function parseSemver(v) {
1076
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
1077
+ if (!m) return null;
1078
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
1079
+ }
1080
+ function isNewerVersion(a, b) {
1081
+ const pa = parseSemver(a);
1082
+ const pb = parseSemver(b);
1083
+ if (!pa || !pb) return false;
1084
+ for (let i = 0; i < 3; i++) {
1085
+ if (pa[i] !== pb[i]) return pa[i] > pb[i];
1086
+ }
1087
+ return false;
1088
+ }
1089
+ var SKILLS_VERSION_MARKER = "genex-skills-version.json";
1090
+ async function readSkillsMarker(skillsDir) {
1091
+ try {
1092
+ const raw = await fs7.readFile(path7.join(skillsDir, SKILLS_VERSION_MARKER), "utf8");
1093
+ const parsed = JSON.parse(raw);
1094
+ return typeof parsed.version === "string" ? parsed.version : null;
1095
+ } catch {
1096
+ return null;
1097
+ }
1098
+ }
1099
+ async function writeSkillsMarker(skillsDir, version = getCliVersion()) {
1100
+ await fs7.mkdir(skillsDir, { recursive: true });
1101
+ await fs7.writeFile(
1102
+ path7.join(skillsDir, SKILLS_VERSION_MARKER),
1103
+ JSON.stringify({ version, syncedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
1104
+ );
1105
+ }
1106
+ async function hasGenexSkills(skillsDir) {
1107
+ try {
1108
+ const entries = await fs7.readdir(skillsDir);
1109
+ return entries.some((name) => name.startsWith("genex-"));
1110
+ } catch {
1111
+ return false;
1112
+ }
1113
+ }
1114
+ var REMOVED_SKILLS = [
1115
+ "genex-explore",
1116
+ "genex-threejs-skill-router",
1117
+ "genex-threejs-bloom",
1118
+ "genex-threejs-screen-space-ambient-occlusion",
1119
+ "genex-threejs-atmosphere-aerial-perspective",
1120
+ "genex-threejs-volumetric-clouds",
1121
+ "genex-threejs-spectral-ocean",
1122
+ "genex-threejs-water-optics",
1123
+ "genex-threejs-temporal-surfaces",
1124
+ "genex-threejs-raymarched-space-effects",
1125
+ "genex-threejs-procedural-architecture",
1126
+ "genex-threejs-procedural-fields",
1127
+ "genex-threejs-procedural-geometry",
1128
+ "genex-threejs-procedural-planets",
1129
+ "genex-threejs-procedural-vegetation",
1130
+ "genex-threejs-image-pipeline",
1131
+ "genex-threejs-lighting-design",
1132
+ "genex-threejs-precipitation-surfaces",
1133
+ "genex-threejs-game-content",
1134
+ "genex-threejs-open-world"
1135
+ ];
1136
+ async function pruneRemovedSkills(skillsDir, log) {
1137
+ const removed = [];
1138
+ for (const name of REMOVED_SKILLS) {
1139
+ const target = path7.join(skillsDir, name);
1140
+ try {
1141
+ await fs7.access(target);
1142
+ } catch {
1143
+ continue;
1144
+ }
1145
+ try {
1146
+ await fs7.rm(target, { recursive: true });
1147
+ removed.push(name);
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ if (removed.length > 0) {
1152
+ log?.plain(
1153
+ `\u{1F9F9} Removed retired Genex skill${removed.length > 1 ? "s" : ""}: ${removed.join(", ")}`
1154
+ );
1155
+ }
1156
+ return removed.length > 0;
1157
+ }
1158
+ async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion(), log) {
1159
+ const skillsDir = path7.join(target.baseDir, "skills");
1160
+ if (!await hasGenexSkills(skillsDir)) return false;
1161
+ if (await readSkillsMarker(skillsDir) === version) return false;
1162
+ const src = target.full ? templatesDir : path7.join(templatesDir, "skills");
1163
+ const dest = target.full ? target.baseDir : skillsDir;
1164
+ await copyTemplates(src, dest, { exclude: ["controllers", "motion"] });
1165
+ await pruneRemovedSkills(skillsDir, log);
1166
+ await writeSkillsMarker(skillsDir, version);
1167
+ return true;
1168
+ }
1169
+ async function cleanupLegacyGlobalSkills(log) {
1170
+ try {
1171
+ const home = os4.homedir();
1172
+ const [realCwd, realHome] = await Promise.all([
1173
+ fs7.realpath(process.cwd()).catch(() => path7.resolve(process.cwd())),
1174
+ fs7.realpath(home).catch(() => path7.resolve(home))
1175
+ ]);
1176
+ if (realCwd === realHome) return false;
1177
+ let removed = false;
1178
+ for (const dirName of [".claude", ".codex", ".cursor"]) {
1179
+ const skillsDir = path7.join(home, dirName, "skills");
1180
+ let entries = [];
1181
+ try {
1182
+ entries = await fs7.readdir(skillsDir);
1183
+ } catch {
1184
+ continue;
1185
+ }
1186
+ for (const name of entries) {
1187
+ if (!name.startsWith("genex-")) continue;
1188
+ try {
1189
+ await fs7.rm(path7.join(skillsDir, name), { recursive: true });
1190
+ removed = true;
1191
+ } catch {
1192
+ }
1193
+ }
1194
+ }
1195
+ for (const rel of [
1196
+ path7.join("agents", "genex-helper.md"),
1197
+ path7.join("commands", "genex-status.md")
1198
+ ]) {
1199
+ try {
1200
+ await fs7.rm(path7.join(home, ".claude", rel));
1201
+ removed = true;
1202
+ } catch {
1203
+ }
1204
+ }
1205
+ if (removed) {
1206
+ log?.plain("\u{1F9F9} Removed legacy GLOBAL Genex skills \u2014 installs are project-local now");
1207
+ }
1208
+ return removed;
1209
+ } catch {
1210
+ return false;
1211
+ }
1212
+ }
1213
+ async function syncSkills(log) {
1214
+ try {
1215
+ await cleanupLegacyGlobalSkills(log);
1216
+ const version = getCliVersion();
1217
+ const templatesDir = getTemplatesDir();
1218
+ let refreshed = false;
1219
+ for (const target of resolveAgentTargets()) {
1220
+ try {
1221
+ refreshed = await syncSkillsForTarget(target, templatesDir, version, log) || refreshed;
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ if (refreshed) log.plain(`\u{1F504} Genex skills updated to ${version}`);
1226
+ try {
1227
+ await fs7.access(path7.join(process.cwd(), ".genex"));
1228
+ if (await writeAgentsContract(process.cwd())) {
1229
+ log.plain("\u{1F504} Genex build contract refreshed (AGENTS.md managed block)");
1230
+ }
1231
+ } catch {
1232
+ }
1233
+ } catch {
1234
+ }
1235
+ }
1236
+ var PUBLISHED_PACKAGES = [
1237
+ { name: "@genex-ai/cli-demo", label: "CLI", install: "npm i -D @genex-ai/cli-demo@latest" },
1238
+ { name: "@genex-ai/embed-sdk", label: "embed SDK", install: "npm i @genex-ai/embed-sdk@latest" },
1239
+ {
1240
+ name: "@genex-ai/multiplayer",
1241
+ label: "multiplayer SDK",
1242
+ install: "npm i @genex-ai/multiplayer@latest"
1243
+ }
1244
+ ];
1245
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1246
+ var REGISTRY_TIMEOUT_MS = 1500;
1247
+ function getUpdateCachePath() {
1248
+ return path7.join(getGenexDir(), "update-check.json");
1249
+ }
1250
+ function isCacheFresh(cache, nowMs) {
1251
+ const at = Date.parse(cache.checkedAt);
1252
+ return Number.isFinite(at) && nowMs - at < CHECK_INTERVAL_MS;
1253
+ }
1254
+ function wasRecentlyNotified(cache, name, latest, nowMs) {
1255
+ if (cache.notified?.[name] !== latest) return false;
1256
+ const at = Date.parse(cache.notifiedAt ?? "");
1257
+ return Number.isFinite(at) && nowMs - at < CHECK_INTERVAL_MS;
1258
+ }
1259
+ async function readUpdateCache() {
1260
+ try {
1261
+ const raw = await fs7.readFile(getUpdateCachePath(), "utf8");
1262
+ const parsed = JSON.parse(raw);
1263
+ if (typeof parsed.checkedAt !== "string" || typeof parsed.latest !== "object") return null;
1264
+ return parsed;
1265
+ } catch {
1266
+ return null;
1267
+ }
1268
+ }
1269
+ async function fetchLatestFromRegistry(name) {
1270
+ try {
1271
+ const res = await fetch(
1272
+ `https://registry.npmjs.org/-/package/${encodeURIComponent(name)}/dist-tags`,
1273
+ { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) }
1274
+ );
1275
+ if (!res.ok) return null;
1276
+ const tags = await res.json();
1277
+ return typeof tags.latest === "string" && parseSemver(tags.latest) ? tags.latest : null;
1278
+ } catch {
1279
+ return null;
1280
+ }
1281
+ }
1282
+ function startUpdateCheck() {
1283
+ return (async () => {
1284
+ try {
1285
+ const cached = await readUpdateCache();
1286
+ if (cached && isCacheFresh(cached, Date.now())) return cached;
1287
+ const latest = { ...cached?.latest ?? {} };
1288
+ const results = await Promise.allSettled(
1289
+ PUBLISHED_PACKAGES.map((p) => fetchLatestFromRegistry(p.name))
1290
+ );
1291
+ results.forEach((r, i) => {
1292
+ if (r.status === "fulfilled" && r.value) latest[PUBLISHED_PACKAGES[i].name] = r.value;
1293
+ });
1294
+ const next = {
1295
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1296
+ latest,
1297
+ ...cached?.notified ? { notified: cached.notified } : {},
1298
+ ...cached?.notifiedAt ? { notifiedAt: cached.notifiedAt } : {}
1299
+ };
1300
+ await fs7.mkdir(getGenexDir(), { recursive: true });
1301
+ await fs7.writeFile(getUpdateCachePath(), JSON.stringify(next, null, 2) + "\n");
1302
+ return next;
1303
+ } catch {
1304
+ return null;
1305
+ }
1306
+ })();
1307
+ }
1308
+ async function installedPackageVersion(cwd, name) {
1309
+ try {
1310
+ const raw = await fs7.readFile(path7.join(cwd, "node_modules", name, "package.json"), "utf8");
1311
+ const pkg = JSON.parse(raw);
1312
+ return typeof pkg.version === "string" ? pkg.version : null;
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ }
1317
+ function formatNudge(label, latest, installed, install) {
1318
+ return `\u2B06 Genex ${label} ${latest} available (installed ${installed}) \u2014 run: ${install}`;
1319
+ }
1320
+ async function reportUpdateNudges(check, log, cwd = process.cwd(), cachePath = getUpdateCachePath()) {
1321
+ try {
1322
+ const cache = await check;
1323
+ if (!cache) return;
1324
+ const now = Date.now();
1325
+ const lines = [];
1326
+ const announced = {};
1327
+ let whatsNew = null;
1328
+ for (const pkg of PUBLISHED_PACKAGES) {
1329
+ if (pkg.name === "@genex-ai/cli-demo" && CLI_CHANNEL === "dev") continue;
1330
+ const latest = cache.latest[pkg.name];
1331
+ if (!latest) continue;
1332
+ if (wasRecentlyNotified(cache, pkg.name, latest, now)) continue;
1333
+ const installed = pkg.name === "@genex-ai/cli-demo" ? getCliVersion() : await installedPackageVersion(cwd, pkg.name);
1334
+ if (!installed) continue;
1335
+ if (isNewerVersion(latest, installed)) {
1336
+ lines.push(formatNudge(pkg.label, latest, installed, pkg.install));
1337
+ announced[pkg.name] = latest;
1338
+ whatsNew ??= `https://www.npmjs.com/package/${pkg.name}`;
1339
+ }
1340
+ }
1341
+ if (lines.length === 0) return;
1342
+ for (const line of lines) log.plain(line);
1343
+ log.dim(
1344
+ ` Apply at a safe moment (never mid-task) \u2014 see the genex-updates skill. What's new: ${whatsNew}`
1345
+ );
1346
+ await recordNotified(cache, announced, cachePath);
1347
+ } catch {
1348
+ }
1349
+ }
1350
+ async function recordNotified(cache, announced, cachePath) {
973
1351
  try {
974
- const res = await apiFetch(`${apiUrl}/api/auth/get-session`, {
975
- headers: { Authorization: `Bearer ${token}` }
976
- });
977
- if (!res.ok) return null;
978
- const data = await res.json().catch(() => null);
979
- return data?.user?.email ?? null;
1352
+ const next = {
1353
+ ...cache,
1354
+ notified: { ...cache.notified ?? {}, ...announced },
1355
+ notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
1356
+ };
1357
+ await fs7.mkdir(path7.dirname(cachePath), { recursive: true });
1358
+ await fs7.writeFile(cachePath, JSON.stringify(next, null, 2) + "\n");
980
1359
  } catch {
981
- return null;
982
1360
  }
983
1361
  }
984
1362
 
1363
+ // src/lib/printed.ts
1364
+ var printed = /* @__PURE__ */ new WeakSet();
1365
+ function markPrinted(err) {
1366
+ if (typeof err === "object" && err !== null) printed.add(err);
1367
+ return err;
1368
+ }
1369
+ function wasPrinted(err) {
1370
+ return typeof err === "object" && err !== null && printed.has(err);
1371
+ }
1372
+
985
1373
  // src/lib/project.ts
986
1374
  import crypto2 from "crypto";
987
1375
  async function createDraftProject(opts) {
@@ -1066,13 +1454,13 @@ async function fetchProjectStatus(apiUrl, token, slug) {
1066
1454
  }
1067
1455
 
1068
1456
  // src/lib/ssh.ts
1069
- import fs5 from "fs/promises";
1070
- import path5 from "path";
1457
+ import fs8 from "fs/promises";
1458
+ import path8 from "path";
1071
1459
  async function writeGitignore(dir, log) {
1072
- const file = path5.join(dir, ".gitignore");
1460
+ const file = path8.join(dir, ".gitignore");
1073
1461
  let content = "";
1074
1462
  try {
1075
- content = await fs5.readFile(file, "utf8");
1463
+ content = await fs8.readFile(file, "utf8");
1076
1464
  } catch {
1077
1465
  }
1078
1466
  const present = new Set(content.split("\n").map((l) => l.trim()));
@@ -1082,7 +1470,7 @@ async function writeGitignore(dir, log) {
1082
1470
  if (next.length > 0 && !next.endsWith("\n")) next += "\n";
1083
1471
  if (!content.trim()) next += "# genex local metadata + secrets \u2014 never publish\n";
1084
1472
  next += toAdd.join("\n") + "\n";
1085
- await fs5.writeFile(file, next);
1473
+ await fs8.writeFile(file, next);
1086
1474
  log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
1087
1475
  }
1088
1476
  var LFS_PATTERNS = [
@@ -1112,10 +1500,10 @@ var LFS_PATTERNS = [
1112
1500
  "*.webm"
1113
1501
  ];
1114
1502
  async function writeGitattributes(dir, log) {
1115
- const file = path5.join(dir, ".gitattributes");
1503
+ const file = path8.join(dir, ".gitattributes");
1116
1504
  let content = "";
1117
1505
  try {
1118
- content = await fs5.readFile(file, "utf8");
1506
+ content = await fs8.readFile(file, "utf8");
1119
1507
  } catch {
1120
1508
  }
1121
1509
  const present = new Set(content.split("\n").map((l) => l.trim().split(/\s+/)[0]));
@@ -1125,143 +1513,13 @@ async function writeGitattributes(dir, log) {
1125
1513
  if (next.length > 0 && !next.endsWith("\n")) next += "\n";
1126
1514
  if (!content.trim()) next += "# Binary game assets go to Git LFS (R2) \u2014 keeps the source push small\n";
1127
1515
  next += toAdd.map((p) => `${p} filter=lfs diff=lfs merge=lfs -text`).join("\n") + "\n";
1128
- await fs5.writeFile(file, next);
1516
+ await fs8.writeFile(file, next);
1129
1517
  log.dim(`Updated .gitattributes (${toAdd.length} binary globs \u2192 Git LFS).`);
1130
1518
  }
1131
1519
 
1132
- // src/lib/store.ts
1133
- import fs7 from "fs/promises";
1134
- import path7 from "path";
1135
-
1136
- // src/lib/env.ts
1137
- import fs6 from "fs/promises";
1138
- import path6 from "path";
1139
- import { spawn as spawn2 } from "child_process";
1140
- async function writeEnvVar(envPath, key, value) {
1141
- let content = "";
1142
- let existed = false;
1143
- try {
1144
- content = await fs6.readFile(envPath, "utf8");
1145
- existed = true;
1146
- } catch {
1147
- }
1148
- const assignment = `${key}=${formatValue(value)}`;
1149
- const keyPattern = new RegExp(
1150
- `^(\\s*export\\s+)?${escapeRegExp(key)}=.*$`,
1151
- "gm"
1152
- );
1153
- let next;
1154
- let mode;
1155
- if (keyPattern.test(content)) {
1156
- next = content.replace(keyPattern, assignment);
1157
- mode = "updated";
1158
- } else {
1159
- let prefix = content;
1160
- if (prefix.length > 0 && !prefix.endsWith("\n")) prefix += "\n";
1161
- next = prefix + assignment + "\n";
1162
- mode = existed ? "appended" : "created";
1163
- }
1164
- await fs6.mkdir(path6.dirname(envPath), { recursive: true });
1165
- await fs6.writeFile(envPath, next, { mode: 384 });
1166
- await restrictFilePermissions(envPath);
1167
- return { mode, path: envPath };
1168
- }
1169
- async function restrictFilePermissions(filePath) {
1170
- if (process.platform !== "win32") {
1171
- await fs6.chmod(filePath, 384).catch(() => {
1172
- });
1173
- return;
1174
- }
1175
- const user = process.env.USERNAME ?? process.env.USER;
1176
- if (!user) return;
1177
- await new Promise((resolve) => {
1178
- try {
1179
- const child = spawn2(
1180
- "icacls",
1181
- [filePath, "/inheritance:r", "/grant:r", `${user}:F`],
1182
- { stdio: "ignore" }
1183
- );
1184
- child.on("error", () => resolve());
1185
- child.on("close", () => resolve());
1186
- } catch {
1187
- resolve();
1188
- }
1189
- });
1190
- }
1191
- function formatValue(value) {
1192
- if (/[\s#"'$`\\]/.test(value)) {
1193
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1194
- }
1195
- return value;
1196
- }
1197
- function escapeRegExp(s) {
1198
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1199
- }
1200
-
1201
- // src/lib/store.ts
1202
- function getProjectMetadataPath(cwd = process.cwd()) {
1203
- return path7.join(cwd, ".genex", "project.json");
1204
- }
1205
- async function writeUserToken(token, envPath) {
1206
- const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
1207
- return { path: written };
1208
- }
1209
- async function rotateRejectedEnv(envPath) {
1210
- const file = getGenexEnvPath(envPath);
1211
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1212
- const aside = `${file}.rejected-${stamp}`;
1213
- try {
1214
- await fs7.rename(file, aside);
1215
- return aside;
1216
- } catch {
1217
- return null;
1218
- }
1219
- }
1220
- async function readUserToken(envPath) {
1221
- const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
1222
- if (fromGenex) return fromGenex;
1223
- if (!envPath && !process.env[ENV_FILE_ENV]) {
1224
- return readTokenFromFile(path7.join(process.cwd(), ".env"));
1225
- }
1226
- return null;
1227
- }
1228
- async function readTokenFromFile(file) {
1229
- let content;
1230
- try {
1231
- content = await fs7.readFile(file, "utf8");
1232
- } catch {
1233
- return null;
1234
- }
1235
- const m = content.match(/^\s*(?:export\s+)?GENEX_TOKEN=(.*)$/m);
1236
- if (!m) return null;
1237
- return stripQuotes(m[1].trim()) || null;
1238
- }
1239
- function stripQuotes(v) {
1240
- if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
1241
- return v.slice(1, -1);
1242
- }
1243
- return v;
1244
- }
1245
- async function readProject(cwd = process.cwd()) {
1246
- try {
1247
- const raw = await fs7.readFile(getProjectMetadataPath(cwd), "utf8");
1248
- return JSON.parse(raw);
1249
- } catch {
1250
- return null;
1251
- }
1252
- }
1253
- async function writeProject(meta, cwd = process.cwd()) {
1254
- const file = getProjectMetadataPath(cwd);
1255
- await fs7.mkdir(path7.dirname(file), { recursive: true });
1256
- await fs7.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
1257
- await fs7.chmod(file, 384).catch(() => {
1258
- });
1259
- return { path: file };
1260
- }
1261
-
1262
1520
  // src/lib/game-config.ts
1263
- import fs8 from "fs/promises";
1264
- import path8 from "path";
1521
+ import fs9 from "fs/promises";
1522
+ import path9 from "path";
1265
1523
  var DEFAULT_DASHBOARD_ORIGIN = new URL(DEFAULT_AUTH_URL).origin;
1266
1524
  function renderGenexConfig(slug) {
1267
1525
  return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
@@ -1318,7 +1576,7 @@ ${overrides.join("\n")}
1318
1576
  }
1319
1577
  async function writeIfAbsent(file, content, log) {
1320
1578
  try {
1321
- await fs8.writeFile(file, content, { flag: "wx" });
1579
+ await fs9.writeFile(file, content, { flag: "wx" });
1322
1580
  log.dim(` wrote ${c.cyan(file)}`);
1323
1581
  return true;
1324
1582
  } catch (err) {
@@ -1330,34 +1588,15 @@ async function writeIfAbsent(file, content, log) {
1330
1588
  }
1331
1589
  }
1332
1590
  async function writeGameConfigFiles(meta, log, cwd = process.cwd()) {
1333
- await fs8.mkdir(path8.join(cwd, "src"), { recursive: true });
1334
- await writeIfAbsent(path8.join(cwd, "src", "genex.config.ts"), renderGenexConfig(meta.slug), log);
1335
- await writeIfAbsent(path8.join(cwd, ".env"), renderSlugEnv(meta.slug), log);
1591
+ await fs9.mkdir(path9.join(cwd, "src"), { recursive: true });
1592
+ await writeIfAbsent(path9.join(cwd, "src", "genex.config.ts"), renderGenexConfig(meta.slug), log);
1593
+ await writeIfAbsent(path9.join(cwd, ".env"), renderSlugEnv(meta.slug), log);
1336
1594
  const overrides = renderDevOverrides(meta);
1337
1595
  if (overrides) {
1338
- await writeIfAbsent(path8.join(cwd, ".env.development.local"), overrides, log);
1596
+ await writeIfAbsent(path9.join(cwd, ".env.development.local"), overrides, log);
1339
1597
  }
1340
1598
  }
1341
1599
 
1342
- // src/utils/logger.ts
1343
- function createLogger(opts = {}) {
1344
- const out = (s) => {
1345
- if (!opts.quiet) process.stdout.write(s + "\n");
1346
- };
1347
- const err = (s) => {
1348
- process.stderr.write(s + "\n");
1349
- };
1350
- return {
1351
- info: (m) => out(`${c.cyan("i")} ${m}`),
1352
- success: (m) => out(`${c.green("\u2713")} ${m}`),
1353
- warn: (m) => out(`${c.yellow("!")} ${m}`),
1354
- error: (m) => err(`${c.red("\u2717")} ${m}`),
1355
- step: (m) => out(`${c.blue("\u203A")} ${m}`),
1356
- dim: (m) => out(c.dim(m)),
1357
- plain: (m) => out(m)
1358
- };
1359
- }
1360
-
1361
1600
  // src/commands/init.ts
1362
1601
  async function runInit(opts) {
1363
1602
  const log = createLogger({ quiet: opts.quiet });
@@ -1372,14 +1611,14 @@ async function runInit(opts) {
1372
1611
  let totalNew = 0;
1373
1612
  let totalUpdated = 0;
1374
1613
  for (const t of targets) {
1375
- const src = t.full ? templatesDir : path9.join(templatesDir, "skills");
1376
- const dest = t.full ? t.baseDir : path9.join(t.baseDir, "skills");
1614
+ const src = t.full ? templatesDir : path10.join(templatesDir, "skills");
1615
+ const dest = t.full ? t.baseDir : path10.join(t.baseDir, "skills");
1377
1616
  const { copied, updated } = await copyTemplates(src, dest, {
1378
1617
  force: opts.force,
1379
1618
  exclude: ["controllers", "motion"]
1380
1619
  });
1381
- await pruneRemovedSkills(path9.join(t.baseDir, "skills"), log);
1382
- await writeSkillsMarker(path9.join(t.baseDir, "skills"));
1620
+ await pruneRemovedSkills(path10.join(t.baseDir, "skills"), log);
1621
+ await writeSkillsMarker(path10.join(t.baseDir, "skills"));
1383
1622
  const added = copied.length - updated.length;
1384
1623
  totalNew += added;
1385
1624
  totalUpdated += updated.length;
@@ -1395,18 +1634,24 @@ async function runInit(opts) {
1395
1634
  return;
1396
1635
  }
1397
1636
  const authBaseUrl = getAuthUrl(opts.authUrl);
1637
+ const authApiUrl = getApiUrl(opts.apiUrl);
1398
1638
  let token = await readUserToken(opts.envPath);
1399
1639
  let savedToken = Boolean(token);
1400
1640
  if (!token) {
1401
1641
  try {
1402
- token = await authorize(authBaseUrl, {
1642
+ token = await authorize(authApiUrl, authBaseUrl, {
1403
1643
  log,
1404
- timeoutMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1644
+ inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1405
1645
  });
1406
1646
  } catch (err) {
1647
+ if (err instanceof AuthPendingError) {
1648
+ printAuthHandoff(log, err);
1649
+ log.dim("Everything else is installed \u2014 `genex auth` finishes the sign-in, then re-run `genex init`.");
1650
+ return;
1651
+ }
1407
1652
  log.error(err instanceof Error ? err.message : String(err));
1408
1653
  log.dim("Your workspace files were installed. Re-run `genex init` to finish authorizing.");
1409
- throw err;
1654
+ throw markPrinted(err);
1410
1655
  }
1411
1656
  const { path: tokenPath } = await writeUserToken(token, opts.envPath);
1412
1657
  log.success(`Authorized. Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
@@ -1423,7 +1668,7 @@ async function runInit(opts) {
1423
1668
  if (email) log.plain(` signed in as ${c.cyan(email)}`);
1424
1669
  };
1425
1670
  await echoIdentity();
1426
- const projectName = opts.name?.trim() || path9.basename(process.cwd());
1671
+ const projectName = opts.name?.trim() || path10.basename(process.cwd());
1427
1672
  const create = (bearer) => createDraftProject({
1428
1673
  apiUrl,
1429
1674
  token: bearer,
@@ -1441,14 +1686,19 @@ async function runInit(opts) {
1441
1686
  const aside = await rotateRejectedEnv(opts.envPath);
1442
1687
  if (aside) log.dim(` moved the rejected credential to ${c.cyan(aside)}`);
1443
1688
  try {
1444
- token = await authorize(authBaseUrl, {
1689
+ token = await authorize(authApiUrl, authBaseUrl, {
1445
1690
  log,
1446
- timeoutMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1691
+ inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1447
1692
  });
1448
1693
  } catch (err) {
1694
+ if (err instanceof AuthPendingError) {
1695
+ printAuthHandoff(log, err);
1696
+ log.dim("Then re-run `genex init` \u2014 your workspace files are already installed.");
1697
+ return;
1698
+ }
1449
1699
  log.error(err instanceof Error ? err.message : String(err));
1450
1700
  log.dim("Your workspace files were installed. Re-run `genex init` to finish authorizing.");
1451
- throw err;
1701
+ throw markPrinted(err);
1452
1702
  }
1453
1703
  const { path: tokenPath } = await writeUserToken(token, opts.envPath);
1454
1704
  log.success(`Authorized. Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
@@ -1470,8 +1720,8 @@ async function runInit(opts) {
1470
1720
  }
1471
1721
 
1472
1722
  // src/commands/link.ts
1473
- import fs9 from "fs/promises";
1474
- import path10 from "path";
1723
+ import fs10 from "fs/promises";
1724
+ import path11 from "path";
1475
1725
  async function runLink(opts) {
1476
1726
  const log = createLogger({ quiet: opts.quiet });
1477
1727
  log.plain(c.bold("genex link"));
@@ -1491,17 +1741,26 @@ async function runLink(opts) {
1491
1741
  return;
1492
1742
  }
1493
1743
  const authBaseUrl = getAuthUrl(opts.authUrl);
1744
+ const apiUrl = getApiUrl(opts.apiUrl);
1494
1745
  let token = await readUserToken(opts.envPath);
1495
1746
  if (!token) {
1496
- token = await authorize(authBaseUrl, {
1497
- log,
1498
- timeoutMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1499
- });
1747
+ try {
1748
+ token = await authorize(apiUrl, authBaseUrl, {
1749
+ log,
1750
+ inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1751
+ });
1752
+ } catch (err) {
1753
+ if (err instanceof AuthPendingError) {
1754
+ printAuthHandoff(log, err);
1755
+ log.dim(`Then re-run ${c.cyan(`genex link ${slug}`)}.`);
1756
+ return;
1757
+ }
1758
+ throw err;
1759
+ }
1500
1760
  const { path: tokenPath } = await writeUserToken(token, opts.envPath);
1501
1761
  log.success(`Authorized. Saved your token to ${c.cyan(tokenPath)}.`);
1502
1762
  log.plain("");
1503
1763
  }
1504
- const apiUrl = getApiUrl(opts.apiUrl);
1505
1764
  const email = await fetchSignedInEmail(apiUrl, token);
1506
1765
  if (email) log.plain(` signed in as ${c.cyan(email)}`);
1507
1766
  log.step(`Looking up ${c.cyan(slug)}\u2026`);
@@ -1531,23 +1790,23 @@ async function runLink(opts) {
1531
1790
  if (project.playUrl) log.dim(` play URL: ${project.playUrl}`);
1532
1791
  }
1533
1792
  async function ensureSlugEnv(slug, log, cwd = process.cwd()) {
1534
- const file = path10.join(cwd, ".env");
1793
+ const file = path11.join(cwd, ".env");
1535
1794
  let content;
1536
1795
  try {
1537
- content = await fs9.readFile(file, "utf8");
1796
+ content = await fs10.readFile(file, "utf8");
1538
1797
  } catch {
1539
1798
  return;
1540
1799
  }
1541
1800
  const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
1542
1801
  const m = content.match(re);
1543
1802
  if (!m) {
1544
- await fs9.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
1803
+ await fs10.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
1545
1804
  `);
1546
1805
  log.dim(` added VITE_GENEX_SLUG=${slug} to .env`);
1547
1806
  return;
1548
1807
  }
1549
1808
  if (m[2].trim() === slug) return;
1550
- await fs9.writeFile(file, content.replace(re, `$1${slug}`));
1809
+ await fs10.writeFile(file, content.replace(re, `$1${slug}`));
1551
1810
  log.dim(` updated .env: VITE_GENEX_SLUG=${slug} (was ${m[2].trim()})`);
1552
1811
  }
1553
1812
  async function fetchOwnProject(apiUrl, token, slug, log) {
@@ -1611,21 +1870,29 @@ async function runList(opts) {
1611
1870
  process.exitCode = 1;
1612
1871
  return;
1613
1872
  }
1873
+ const meta = await readProject();
1874
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
1875
+ const dashOrigin = (meta?.dashboardOrigins?.[0] ?? getAuthUrl(opts.authUrl)).replace(/\/+$/, "");
1614
1876
  let token = opts.token ?? await readUserToken(opts.envPath);
1615
1877
  if (!token) {
1616
1878
  if (opts.noAuth) {
1617
- log.error("Not signed in. Re-run without --no-auth to connect in the browser.");
1879
+ log.error("Not signed in. Re-run without --no-auth to connect.");
1618
1880
  process.exitCode = 1;
1619
1881
  return;
1620
1882
  }
1621
- log.plain("Not signed in \u2014 opening the browser to connect\u2026");
1883
+ log.plain("Not signed in \u2014 connecting\u2026");
1622
1884
  try {
1623
- token = await authorize(getAuthUrl(opts.authUrl), {
1885
+ token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
1624
1886
  log,
1625
- timeoutMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1887
+ inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1626
1888
  });
1627
1889
  } catch (err) {
1628
- log.error(`Sign-in didn't complete: ${String(err)}`);
1890
+ if (err instanceof AuthPendingError) {
1891
+ printAuthHandoff(log, err);
1892
+ log.dim(`Then re-run ${c.cyan("genex list")}.`);
1893
+ return;
1894
+ }
1895
+ log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
1629
1896
  process.exitCode = 1;
1630
1897
  return;
1631
1898
  }
@@ -1633,20 +1900,28 @@ async function runList(opts) {
1633
1900
  log.success(`Authorized. Saved your token to ${c.cyan(tokenPath)}.`);
1634
1901
  log.plain("");
1635
1902
  }
1636
- const meta = await readProject();
1637
- const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
1638
- const dashOrigin = (meta?.dashboardOrigins?.[0] ?? getAuthUrl(opts.authUrl)).replace(/\/+$/, "");
1639
1903
  const projectsUrl = `${apiUrl}/api/projects`;
1640
1904
  const fetchProjects = (t) => apiFetch(projectsUrl, { headers: { Authorization: `Bearer ${t}` } });
1641
1905
  let res;
1642
1906
  try {
1643
1907
  res = await fetchProjects(token);
1644
1908
  if (res.status === 401 && !opts.token && !opts.noAuth) {
1645
- log.plain("Your saved sign-in was rejected \u2014 reconnecting in the browser\u2026");
1646
- token = await authorize(getAuthUrl(opts.authUrl), {
1647
- log,
1648
- timeoutMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1649
- });
1909
+ log.plain("Your saved sign-in was rejected \u2014 reconnecting\u2026");
1910
+ try {
1911
+ token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
1912
+ log,
1913
+ inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
1914
+ });
1915
+ } catch (err) {
1916
+ if (err instanceof AuthPendingError) {
1917
+ printAuthHandoff(log, err);
1918
+ log.dim(`Then re-run ${c.cyan("genex list")}.`);
1919
+ return;
1920
+ }
1921
+ log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
1922
+ process.exitCode = 1;
1923
+ return;
1924
+ }
1650
1925
  await writeUserToken(token, opts.envPath);
1651
1926
  res = await fetchProjects(token);
1652
1927
  }
@@ -1710,9 +1985,9 @@ function relTime(iso) {
1710
1985
  // src/lib/deploy.ts
1711
1986
  import { spawn as spawn3 } from "child_process";
1712
1987
  import crypto3 from "crypto";
1713
- import fs12 from "fs/promises";
1714
- import os4 from "os";
1715
- import path12 from "path";
1988
+ import fs13 from "fs/promises";
1989
+ import os5 from "os";
1990
+ import path13 from "path";
1716
1991
 
1717
1992
  // ../../packages/mobile-scan/src/image-dims.ts
1718
1993
  function u32be(b, o) {
@@ -1975,12 +2250,12 @@ function tierFor(estVramMb) {
1975
2250
  }
1976
2251
 
1977
2252
  // src/commands/ui.ts
1978
- import fs11 from "fs/promises";
1979
- import path11 from "path";
2253
+ import fs12 from "fs/promises";
2254
+ import path12 from "path";
1980
2255
  import { PNG as PNG2 } from "pngjs";
1981
2256
 
1982
2257
  // src/lib/png-tools.ts
1983
- import fs10 from "fs/promises";
2258
+ import fs11 from "fs/promises";
1984
2259
  import { PNG } from "pngjs";
1985
2260
  var ALPHA_TRANSPARENT_MAX = 16;
1986
2261
  var isHttpUrl = (s) => /^https?:\/\//i.test(s);
@@ -1991,12 +2266,12 @@ async function loadPng(input) {
1991
2266
  if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
1992
2267
  buf = Buffer.from(await res.arrayBuffer());
1993
2268
  } else {
1994
- buf = await fs10.readFile(input);
2269
+ buf = await fs11.readFile(input);
1995
2270
  }
1996
2271
  return PNG.sync.read(buf);
1997
2272
  }
1998
2273
  async function writePng(file, png) {
1999
- await fs10.writeFile(file, PNG.sync.write(png));
2274
+ await fs11.writeFile(file, PNG.sync.write(png));
2000
2275
  }
2001
2276
  function cropPng(image, box) {
2002
2277
  const out = new PNG({ width: box.w, height: box.h });
@@ -2296,7 +2571,7 @@ async function uiExtract(opts, log) {
2296
2571
  const dilatePx = opts.dilate ?? 0;
2297
2572
  const sheet = await loadPng(input);
2298
2573
  const { width: W, height: H, data } = sheet;
2299
- await fs11.mkdir(outDir, { recursive: true });
2574
+ await fs12.mkdir(outDir, { recursive: true });
2300
2575
  log.plain(c.bold("genex ui extract"));
2301
2576
  log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
2302
2577
  let hasTransparency = false;
@@ -2525,7 +2800,7 @@ async function uiExtract(opts, log) {
2525
2800
  rimPixels: speckle.sampled
2526
2801
  });
2527
2802
  }
2528
- const outPath = path11.join(outDir, `${name}.png`);
2803
+ const outPath = path12.join(outDir, `${name}.png`);
2529
2804
  await writePng(outPath, out);
2530
2805
  const sidecar = {
2531
2806
  name,
@@ -2544,7 +2819,7 @@ async function uiExtract(opts, log) {
2544
2819
  defringed
2545
2820
  };
2546
2821
  const { name: _n, out: _o, ...sidecarBody } = sidecar;
2547
- await fs11.writeFile(
2822
+ await fs12.writeFile(
2548
2823
  outPath.replace(/\.png$/i, "") + ".bbox.json",
2549
2824
  JSON.stringify(sidecarBody, null, 2)
2550
2825
  );
@@ -2553,8 +2828,8 @@ async function uiExtract(opts, log) {
2553
2828
  `${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
2554
2829
  );
2555
2830
  }
2556
- const debugPath = path11.join(outDir, "extract-debug.json");
2557
- await fs11.writeFile(
2831
+ const debugPath = path12.join(outDir, "extract-debug.json");
2832
+ await fs12.writeFile(
2558
2833
  debugPath,
2559
2834
  JSON.stringify(
2560
2835
  {
@@ -3016,7 +3291,7 @@ async function uiMasks(opts, log) {
3016
3291
  const registrationTolerance = opts.registrationTolerance ?? 0.04;
3017
3292
  const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
3018
3293
  const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
3019
- await fs11.mkdir(outDir, { recursive: true });
3294
+ await fs12.mkdir(outDir, { recursive: true });
3020
3295
  log.plain(c.bold("genex ui masks"));
3021
3296
  log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
3022
3297
  const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
@@ -3069,11 +3344,11 @@ async function uiMasks(opts, log) {
3069
3344
  });
3070
3345
  }
3071
3346
  const overlay = makeOverlay(clean2, converted.png);
3072
- const framePath = path11.join(outDir, `${pair.name}-frame.png`);
3073
- const maskPath = path11.join(outDir, `${pair.name}-mask.png`);
3074
- const annotatedPath = path11.join(outDir, `${pair.name}-annotated-source.png`);
3075
- const overlayPath = path11.join(outDir, `${pair.name}-overlay.png`);
3076
- const metaPath = path11.join(outDir, `${pair.name}.annotated-progress.json`);
3347
+ const framePath = path12.join(outDir, `${pair.name}-frame.png`);
3348
+ const maskPath = path12.join(outDir, `${pair.name}-mask.png`);
3349
+ const annotatedPath = path12.join(outDir, `${pair.name}-annotated-source.png`);
3350
+ const overlayPath = path12.join(outDir, `${pair.name}-overlay.png`);
3351
+ const metaPath = path12.join(outDir, `${pair.name}.annotated-progress.json`);
3077
3352
  await writePng(framePath, clean2);
3078
3353
  await writePng(maskPath, converted.png);
3079
3354
  await writePng(annotatedPath, annotated);
@@ -3120,7 +3395,7 @@ async function uiMasks(opts, log) {
3120
3395
  },
3121
3396
  overlay: overlayPath
3122
3397
  };
3123
- await fs11.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
3398
+ await fs12.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
3124
3399
  `);
3125
3400
  results.push(meta);
3126
3401
  const fb = converted.bbox;
@@ -3136,8 +3411,8 @@ async function uiMasks(opts, log) {
3136
3411
  );
3137
3412
  }
3138
3413
  }
3139
- const indexPath = path11.join(outDir, "annotated-progress.json");
3140
- await fs11.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
3414
+ const indexPath = path12.join(outDir, "annotated-progress.json");
3415
+ await fs12.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
3141
3416
  `);
3142
3417
  log.plain("");
3143
3418
  log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
@@ -3263,7 +3538,7 @@ async function uiTextColor(opts, log) {
3263
3538
  };
3264
3539
  process.stdout.write(`${JSON.stringify(result, null, 2)}
3265
3540
  `);
3266
- if (opts.out) await fs11.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
3541
+ if (opts.out) await fs12.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
3267
3542
  `);
3268
3543
  if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
3269
3544
  }
@@ -3295,7 +3570,7 @@ async function uiTrim(opts, log) {
3295
3570
  const sidecar = computeBBoxes(trimmed);
3296
3571
  const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
3297
3572
  const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
3298
- await fs11.writeFile(
3573
+ await fs12.writeFile(
3299
3574
  sidecarPath,
3300
3575
  JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
3301
3576
  );
@@ -3402,7 +3677,7 @@ async function uiPlate(opts, log) {
3402
3677
  fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
3403
3678
  }
3404
3679
  await writePng(outPath, out);
3405
- const name = path11.basename(outPath);
3680
+ const name = path12.basename(outPath);
3406
3681
  log.plain(c.bold("genex ui plate"));
3407
3682
  log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
3408
3683
  log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
@@ -3567,13 +3842,13 @@ async function walkFiles(dir) {
3567
3842
  const out = [];
3568
3843
  let entries;
3569
3844
  try {
3570
- entries = await fs11.readdir(dir, { withFileTypes: true });
3845
+ entries = await fs12.readdir(dir, { withFileTypes: true });
3571
3846
  } catch {
3572
3847
  return out;
3573
3848
  }
3574
3849
  for (const entry of entries) {
3575
3850
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
3576
- const p = path11.join(dir, entry.name);
3851
+ const p = path12.join(dir, entry.name);
3577
3852
  if (entry.isDirectory()) out.push(...await walkFiles(p));
3578
3853
  else out.push(p);
3579
3854
  }
@@ -3582,7 +3857,7 @@ async function walkFiles(dir) {
3582
3857
  async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3583
3858
  const viewportFindings = [];
3584
3859
  try {
3585
- const indexHtml = await fs11.readFile(path11.join(cwd, "index.html"), "utf8");
3860
+ const indexHtml = await fs12.readFile(path12.join(cwd, "index.html"), "utf8");
3586
3861
  if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
3587
3862
  viewportFindings.push({
3588
3863
  kind: "viewport-meta",
@@ -3596,28 +3871,28 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3596
3871
  }
3597
3872
  } catch {
3598
3873
  }
3599
- const absAssets = path11.resolve(cwd, assetDir);
3874
+ const absAssets = path12.resolve(cwd, assetDir);
3600
3875
  try {
3601
- if (!(await fs11.stat(absAssets)).isDirectory()) {
3876
+ if (!(await fs12.stat(absAssets)).isDirectory()) {
3602
3877
  return viewportFindings.length > 0 ? viewportFindings : null;
3603
3878
  }
3604
3879
  } catch {
3605
3880
  return viewportFindings.length > 0 ? viewportFindings : null;
3606
3881
  }
3607
- const srcFiles = (await walkFiles(path11.resolve(cwd, srcDir))).filter(
3608
- (p) => AUDIT_SRC_EXTS.has(path11.extname(p).toLowerCase())
3882
+ const srcFiles = (await walkFiles(path12.resolve(cwd, srcDir))).filter(
3883
+ (p) => AUDIT_SRC_EXTS.has(path12.extname(p).toLowerCase())
3609
3884
  );
3610
3885
  try {
3611
- for (const name of await fs11.readdir(cwd)) {
3612
- const ext = path11.extname(name).toLowerCase();
3613
- if (ext === ".html" || ext === ".css") srcFiles.push(path11.join(cwd, name));
3886
+ for (const name of await fs12.readdir(cwd)) {
3887
+ const ext = path12.extname(name).toLowerCase();
3888
+ if (ext === ".html" || ext === ".css") srcFiles.push(path12.join(cwd, name));
3614
3889
  }
3615
3890
  } catch {
3616
3891
  }
3617
3892
  const sources = [];
3618
3893
  for (const p of srcFiles) {
3619
3894
  try {
3620
- sources.push({ rel: path11.relative(cwd, p), text: await fs11.readFile(p, "utf8") });
3895
+ sources.push({ rel: path12.relative(cwd, p), text: await fs12.readFile(p, "utf8") });
3621
3896
  } catch {
3622
3897
  }
3623
3898
  }
@@ -3627,11 +3902,11 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3627
3902
  const metaByName = /* @__PURE__ */ new Map();
3628
3903
  const bboxByPng = /* @__PURE__ */ new Map();
3629
3904
  for (const p of assetFiles) {
3630
- const base = path11.basename(p);
3905
+ const base = path12.basename(p);
3631
3906
  const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
3632
3907
  if (metaMatch) {
3633
3908
  try {
3634
- const meta = JSON.parse(await fs11.readFile(p, "utf8"));
3909
+ const meta = JSON.parse(await fs12.readFile(p, "utf8"));
3635
3910
  metaByName.set(metaMatch[1], {
3636
3911
  cleanCrop: meta.clean?.crop ?? null,
3637
3912
  loosened: meta.loosened === true
@@ -3642,7 +3917,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3642
3917
  }
3643
3918
  if (base.endsWith(".bbox.json")) {
3644
3919
  try {
3645
- const sidecar = JSON.parse(await fs11.readFile(p, "utf8"));
3920
+ const sidecar = JSON.parse(await fs12.readFile(p, "utf8"));
3646
3921
  if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
3647
3922
  } catch {
3648
3923
  }
@@ -3651,7 +3926,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3651
3926
  for (const [name, meta] of metaByName) {
3652
3927
  if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
3653
3928
  for (const p of assetFiles) {
3654
- const base = path11.basename(p);
3929
+ const base = path12.basename(p);
3655
3930
  if (!base.toLowerCase().endsWith(".png")) continue;
3656
3931
  if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
3657
3932
  if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
@@ -3675,7 +3950,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3675
3950
  }
3676
3951
  const pngByBase = /* @__PURE__ */ new Map();
3677
3952
  for (const p of assetFiles) {
3678
- const base = path11.basename(p);
3953
+ const base = path12.basename(p);
3679
3954
  if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
3680
3955
  }
3681
3956
  for (const [maskBase, maskPath] of pngByBase) {
@@ -3688,8 +3963,8 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3688
3963
  let frame;
3689
3964
  let mask;
3690
3965
  try {
3691
- frame = PNG2.sync.read(await fs11.readFile(framePath));
3692
- mask = PNG2.sync.read(await fs11.readFile(maskPath));
3966
+ frame = PNG2.sync.read(await fs12.readFile(framePath));
3967
+ mask = PNG2.sync.read(await fs12.readFile(maskPath));
3693
3968
  } catch {
3694
3969
  continue;
3695
3970
  }
@@ -3708,7 +3983,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3708
3983
  if (!referenced(base)) continue;
3709
3984
  let png;
3710
3985
  try {
3711
- png = PNG2.sync.read(await fs11.readFile(p));
3986
+ png = PNG2.sync.read(await fs12.readFile(p));
3712
3987
  } catch {
3713
3988
  continue;
3714
3989
  }
@@ -3728,7 +4003,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3728
4003
  }
3729
4004
  const maskReported = /* @__PURE__ */ new Set();
3730
4005
  for (const p of assetFiles) {
3731
- const m = /^(.+)\.annotated-progress\.json$/.exec(path11.basename(p));
4006
+ const m = /^(.+)\.annotated-progress\.json$/.exec(path12.basename(p));
3732
4007
  if (!m) continue;
3733
4008
  const maskBase = `${m[1]}-mask.png`;
3734
4009
  if (!referenced(maskBase)) {
@@ -3740,14 +4015,14 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3740
4015
  }
3741
4016
  }
3742
4017
  for (const p of assetFiles) {
3743
- const base = path11.basename(p);
4018
+ const base = path12.basename(p);
3744
4019
  if (!base.toLowerCase().endsWith(".png")) continue;
3745
4020
  if (/-annotated-source\.png$|-overlay\.png$/.test(base)) continue;
3746
4021
  if (maskReported.has(base)) continue;
3747
4022
  if (!referenced(base)) {
3748
4023
  findings.push({
3749
4024
  kind: "unwired-sprite",
3750
- message: `${path11.relative(cwd, p)} is on disk but never referenced in ${srcDir}/index.html/CSS \u2014 wire it, or record the one-line reason it was cut. (Computed-string references are invisible here \u2014 check.)`
4025
+ message: `${path12.relative(cwd, p)} is on disk but never referenced in ${srcDir}/index.html/CSS \u2014 wire it, or record the one-line reason it was cut. (Computed-string references are invisible here \u2014 check.)`
3751
4026
  });
3752
4027
  }
3753
4028
  }
@@ -3881,7 +4156,7 @@ async function printUiAuditPreflight(log) {
3881
4156
  }
3882
4157
  async function printPipelineStatePreflight(log, cwd = process.cwd()) {
3883
4158
  try {
3884
- const design = await fs12.readFile(path12.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
4159
+ const design = await fs13.readFile(path13.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
3885
4160
  const warnings = [];
3886
4161
  if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
3887
4162
  warnings.push(
@@ -3889,7 +4164,7 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
3889
4164
  );
3890
4165
  }
3891
4166
  if (!/player character:/i.test(design)) {
3892
- const hasCharacter = await fs12.access(path12.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
4167
+ const hasCharacter = await fs13.access(path13.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
3893
4168
  if (!hasCharacter && await loadsPlayerBody(cwd)) {
3894
4169
  warnings.push(
3895
4170
  `Player character is the stock avatar \u2014 no generated character is wired. The game's own generated character is the player's body wherever a human body appears on screen, first-person included (genex-ai-character). Generate it, or record "Player character: VRM \u2014 <reason>" in DESIGN.md (no human body in this game / out of credits / player declined).`
@@ -3903,12 +4178,12 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
3903
4178
  async function loadsPlayerBody(cwd) {
3904
4179
  const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
3905
4180
  try {
3906
- const entries = await fs12.readdir(path12.join(cwd, "src"), { recursive: true });
4181
+ const entries = await fs13.readdir(path13.join(cwd, "src"), { recursive: true });
3907
4182
  for (const rel of entries) {
3908
4183
  if (rel.includes("node_modules")) continue;
3909
- if (rel.split(path12.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
4184
+ if (rel.split(path13.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
3910
4185
  if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
3911
- const text = await fs12.readFile(path12.join(cwd, "src", rel), "utf8").catch(() => "");
4186
+ const text = await fs13.readFile(path13.join(cwd, "src", rel), "utf8").catch(() => "");
3912
4187
  if (BODY_LOADERS.test(text)) return true;
3913
4188
  }
3914
4189
  } catch {
@@ -3932,9 +4207,9 @@ async function deployGame(ctx, opts, log) {
3932
4207
  }
3933
4208
  log.success("Built.");
3934
4209
  }
3935
- const distDir = path12.join(cwd, "dist");
4210
+ const distDir = path13.join(cwd, "dist");
3936
4211
  const siteDir = await isDir2(distDir) ? distDir : cwd;
3937
- const rel = path12.relative(cwd, siteDir) || ".";
4212
+ const rel = path13.relative(cwd, siteDir) || ".";
3938
4213
  if (siteDir === cwd) await writeGitignore(cwd, log);
3939
4214
  const files = await collectFiles(siteDir);
3940
4215
  if (files.length === 0) {
@@ -3992,7 +4267,7 @@ async function deployGame(ctx, opts, log) {
3992
4267
  }
3993
4268
  async function hasBuildScript(cwd) {
3994
4269
  try {
3995
- const pkg = JSON.parse(await fs12.readFile(path12.join(cwd, "package.json"), "utf8"));
4270
+ const pkg = JSON.parse(await fs13.readFile(path13.join(cwd, "package.json"), "utf8"));
3996
4271
  return Boolean(pkg.scripts?.build);
3997
4272
  } catch {
3998
4273
  return false;
@@ -4001,12 +4276,12 @@ async function hasBuildScript(cwd) {
4001
4276
  async function collectFiles(root) {
4002
4277
  const out = [];
4003
4278
  const walk2 = async (dir, prefix) => {
4004
- for (const e of await fs12.readdir(dir, { withFileTypes: true })) {
4279
+ for (const e of await fs13.readdir(dir, { withFileTypes: true })) {
4005
4280
  const relPath = prefix ? `${prefix}/${e.name}` : e.name;
4006
4281
  if (e.isDirectory()) {
4007
- if (!EXCLUDE_DIRS.has(e.name)) await walk2(path12.join(dir, e.name), relPath);
4282
+ if (!EXCLUDE_DIRS.has(e.name)) await walk2(path13.join(dir, e.name), relPath);
4008
4283
  } else if (e.isFile() && !isSecretEnvFile(e.name)) {
4009
- out.push({ relPath, bytes: await fs12.readFile(path12.join(dir, e.name)) });
4284
+ out.push({ relPath, bytes: await fs13.readFile(path13.join(dir, e.name)) });
4010
4285
  }
4011
4286
  }
4012
4287
  };
@@ -4233,7 +4508,7 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
4233
4508
  log.error("Couldn't save your game's source \u2014 please try again.");
4234
4509
  return false;
4235
4510
  };
4236
- const gitDir = await fs12.mkdtemp(path12.join(os4.tmpdir(), "genex-source-"));
4511
+ const gitDir = await fs13.mkdtemp(path13.join(os5.tmpdir(), "genex-source-"));
4237
4512
  const base = { GIT_DIR: gitDir };
4238
4513
  if (urlHasEmbeddedCredentials(pushUrl)) {
4239
4514
  base.GIT_CONFIG_COUNT = "1";
@@ -4248,12 +4523,12 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
4248
4523
  };
4249
4524
  try {
4250
4525
  if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
4251
- await fs12.writeFile(
4252
- path12.join(gitDir, "info", "exclude"),
4526
+ await fs13.writeFile(
4527
+ path13.join(gitDir, "info", "exclude"),
4253
4528
  // .env* are secrets — never publish them; `!` keeps the non-secret template.
4254
4529
  ["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
4255
4530
  );
4256
- const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path12.join(gitDir, "index-source") };
4531
+ const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path13.join(gitDir, "index-source") };
4257
4532
  let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
4258
4533
  if (!lfs) {
4259
4534
  log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
@@ -4304,7 +4579,7 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
4304
4579
  } catch {
4305
4580
  return failed();
4306
4581
  } finally {
4307
- await fs12.rm(gitDir, { recursive: true, force: true }).catch(() => {
4582
+ await fs13.rm(gitDir, { recursive: true, force: true }).catch(() => {
4308
4583
  });
4309
4584
  }
4310
4585
  }
@@ -4340,12 +4615,12 @@ async function fetchPushUrl(ctx, log) {
4340
4615
  }
4341
4616
  async function isDir2(p) {
4342
4617
  try {
4343
- return (await fs12.stat(p)).isDirectory();
4618
+ return (await fs13.stat(p)).isDirectory();
4344
4619
  } catch {
4345
4620
  return false;
4346
4621
  }
4347
4622
  }
4348
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
4623
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
4349
4624
  function fingerprintOf(html) {
4350
4625
  const m = html.match(/[A-Za-z0-9_]+-[A-Za-z0-9_-]{8}\.(?:js|css)/);
4351
4626
  return m ? m[0] : null;
@@ -4366,7 +4641,7 @@ async function waitUntilLive(playUrl, fingerprint, timeoutMs, log) {
4366
4641
  }
4367
4642
  } catch {
4368
4643
  }
4369
- await sleep(1500);
4644
+ await sleep2(1500);
4370
4645
  }
4371
4646
  log.success(`Published. \u{1F310} ${playUrl}`);
4372
4647
  log.dim(" (If the previous build shows, hard-refresh in a few seconds.)");
@@ -4442,17 +4717,17 @@ async function runMakeRemixable(opts) {
4442
4717
  }
4443
4718
 
4444
4719
  // src/lib/detect-features.ts
4445
- import fs14 from "fs/promises";
4446
- import path14 from "path";
4720
+ import fs15 from "fs/promises";
4721
+ import path15 from "path";
4447
4722
 
4448
4723
  // src/lib/generation-ledger.ts
4449
- import fs13 from "fs/promises";
4450
- import path13 from "path";
4451
- var ledgerPath = (cwd) => path13.join(cwd, ".genex", "generations.ndjson");
4724
+ import fs14 from "fs/promises";
4725
+ import path14 from "path";
4726
+ var ledgerPath = (cwd) => path14.join(cwd, ".genex", "generations.ndjson");
4452
4727
  async function append(cwd, event) {
4453
4728
  try {
4454
- await fs13.access(path13.join(cwd, ".genex"));
4455
- await fs13.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
4729
+ await fs14.access(path14.join(cwd, ".genex"));
4730
+ await fs14.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
4456
4731
  `, "utf8");
4457
4732
  } catch {
4458
4733
  }
@@ -4460,7 +4735,7 @@ async function append(cwd, event) {
4460
4735
  async function readLedger(cwd = process.cwd()) {
4461
4736
  let raw;
4462
4737
  try {
4463
- raw = await fs13.readFile(ledgerPath(cwd), "utf8");
4738
+ raw = await fs14.readFile(ledgerPath(cwd), "utf8");
4464
4739
  } catch {
4465
4740
  return [];
4466
4741
  }
@@ -4514,7 +4789,7 @@ async function countFailed(kind, cwd = process.cwd()) {
4514
4789
  // src/lib/detect-features.ts
4515
4790
  async function detectEmbedSdkVersion(cwd = process.cwd()) {
4516
4791
  try {
4517
- const raw = await fs14.readFile(path14.join(cwd, "package.json"), "utf8");
4792
+ const raw = await fs15.readFile(path15.join(cwd, "package.json"), "utf8");
4518
4793
  const pkg = JSON.parse(raw);
4519
4794
  const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
4520
4795
  return typeof version === "string" && version ? version : null;
@@ -4524,7 +4799,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
4524
4799
  }
4525
4800
  async function detectMultiplayer(cwd = process.cwd()) {
4526
4801
  try {
4527
- const raw = await fs14.readFile(path14.join(cwd, "package.json"), "utf8");
4802
+ const raw = await fs15.readFile(path15.join(cwd, "package.json"), "utf8");
4528
4803
  const pkg = JSON.parse(raw);
4529
4804
  return Boolean(
4530
4805
  pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
@@ -4536,7 +4811,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
4536
4811
  async function detectMatchmaking(log, cwd = process.cwd()) {
4537
4812
  let pkg;
4538
4813
  try {
4539
- pkg = JSON.parse(await fs14.readFile(path14.join(cwd, "package.json"), "utf8"));
4814
+ pkg = JSON.parse(await fs15.readFile(path15.join(cwd, "package.json"), "utf8"));
4540
4815
  } catch (err) {
4541
4816
  log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
4542
4817
  return null;
@@ -4554,15 +4829,15 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
4554
4829
  var TOUCH_KIT_MARKERS = /controllers\/touch\/|controllers\/character\/touch-joystick|TouchJoystick|VirtualButton|DragZone|RotateOverlay/;
4555
4830
  async function detectMobileControls(cwd = process.cwd()) {
4556
4831
  try {
4557
- const raw = await fs14.readFile(path14.join(cwd, "package.json"), "utf8");
4832
+ const raw = await fs15.readFile(path15.join(cwd, "package.json"), "utf8");
4558
4833
  const pkg = JSON.parse(raw);
4559
4834
  if (pkg.genex?.mobileControls === true) return true;
4560
4835
  } catch {
4561
4836
  }
4562
- const srcDir = path14.join(cwd, "src");
4837
+ const srcDir = path15.join(cwd, "src");
4563
4838
  let entries;
4564
4839
  try {
4565
- entries = await fs14.readdir(srcDir, { recursive: true });
4840
+ entries = await fs15.readdir(srcDir, { recursive: true });
4566
4841
  } catch {
4567
4842
  return false;
4568
4843
  }
@@ -4570,7 +4845,7 @@ async function detectMobileControls(cwd = process.cwd()) {
4570
4845
  if (rel.includes("node_modules")) continue;
4571
4846
  if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
4572
4847
  try {
4573
- const content = await fs14.readFile(path14.join(srcDir, rel), "utf8");
4848
+ const content = await fs15.readFile(path15.join(srcDir, rel), "utf8");
4574
4849
  if (TOUCH_KIT_MARKERS.test(content)) return true;
4575
4850
  } catch {
4576
4851
  }
@@ -4579,10 +4854,10 @@ async function detectMobileControls(cwd = process.cwd()) {
4579
4854
  }
4580
4855
  var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
4581
4856
  async function detectGameStateUsage(cwd = process.cwd()) {
4582
- const srcDir = path14.join(cwd, "src");
4857
+ const srcDir = path15.join(cwd, "src");
4583
4858
  let entries;
4584
4859
  try {
4585
- entries = await fs14.readdir(srcDir, { recursive: true });
4860
+ entries = await fs15.readdir(srcDir, { recursive: true });
4586
4861
  } catch {
4587
4862
  return false;
4588
4863
  }
@@ -4590,7 +4865,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
4590
4865
  if (rel.includes("node_modules")) continue;
4591
4866
  if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
4592
4867
  try {
4593
- const content = await fs14.readFile(path14.join(srcDir, rel), "utf8");
4868
+ const content = await fs15.readFile(path15.join(srcDir, rel), "utf8");
4594
4869
  if (GAME_STATE_CALLS.test(content)) return true;
4595
4870
  } catch {
4596
4871
  }
@@ -4634,19 +4909,19 @@ var lineOf = (content, index) => content.slice(0, index).split("\n").length;
4634
4909
  var DEPTH_RATIO_LIMIT = 1e6;
4635
4910
  async function detectSurfaceScan(cwd = process.cwd()) {
4636
4911
  const found = { guessedRepeat: [], squarePoints: [], depthRange: [] };
4637
- const srcDir = path14.join(cwd, "src");
4912
+ const srcDir = path15.join(cwd, "src");
4638
4913
  let entries;
4639
4914
  try {
4640
- entries = await fs14.readdir(srcDir, { recursive: true });
4915
+ entries = await fs15.readdir(srcDir, { recursive: true });
4641
4916
  } catch {
4642
4917
  return found;
4643
4918
  }
4644
4919
  for (const nativeRel of entries) {
4645
4920
  if (nativeRel.includes("node_modules")) continue;
4646
4921
  if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
4647
- const raw = await fs14.readFile(path14.join(srcDir, nativeRel), "utf8").catch(() => "");
4922
+ const raw = await fs15.readFile(path15.join(srcDir, nativeRel), "utf8").catch(() => "");
4648
4923
  if (!raw) continue;
4649
- const rel = nativeRel.split(path14.sep).join("/");
4924
+ const rel = nativeRel.split(path15.sep).join("/");
4650
4925
  const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
4651
4926
  const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
4652
4927
  let m;
@@ -4692,25 +4967,25 @@ async function detectGenerationAudit(cwd = process.cwd()) {
4692
4967
  let haystack = "";
4693
4968
  const read = async (file) => {
4694
4969
  try {
4695
- haystack += await fs14.readFile(file, "utf8");
4970
+ haystack += await fs15.readFile(file, "utf8");
4696
4971
  } catch {
4697
4972
  }
4698
4973
  };
4699
4974
  try {
4700
- for (const entry of await fs14.readdir(cwd, { withFileTypes: true })) {
4975
+ for (const entry of await fs15.readdir(cwd, { withFileTypes: true })) {
4701
4976
  if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
4702
- await read(path14.join(cwd, entry.name));
4977
+ await read(path15.join(cwd, entry.name));
4703
4978
  }
4704
4979
  }
4705
4980
  } catch {
4706
4981
  }
4707
4982
  for (const sub of ["src", "public"]) {
4708
4983
  try {
4709
- const entries = await fs14.readdir(path14.join(cwd, sub), { recursive: true });
4984
+ const entries = await fs15.readdir(path15.join(cwd, sub), { recursive: true });
4710
4985
  for (const rel of entries) {
4711
4986
  if (rel.includes("node_modules")) continue;
4712
4987
  if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
4713
- await read(path14.join(cwd, sub, rel));
4988
+ await read(path15.join(cwd, sub, rel));
4714
4989
  }
4715
4990
  } catch {
4716
4991
  }
@@ -4836,7 +5111,7 @@ async function borrowEvidence(meta, cwd) {
4836
5111
  } catch {
4837
5112
  return true;
4838
5113
  }
4839
- const gitConfig = await fs14.readFile(path14.join(cwd, ".git", "config"), "utf8").catch(() => "");
5114
+ const gitConfig = await fs15.readFile(path15.join(cwd, ".git", "config"), "utf8").catch(() => "");
4840
5115
  for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
4841
5116
  try {
4842
5117
  const u = new URL(m[1]);
@@ -4844,7 +5119,7 @@ async function borrowEvidence(meta, cwd) {
4844
5119
  } catch {
4845
5120
  }
4846
5121
  }
4847
- const readme = await fs14.readFile(path14.join(cwd, "README.md"), "utf8").catch(() => "");
5122
+ const readme = await fs15.readFile(path15.join(cwd, "README.md"), "utf8").catch(() => "");
4848
5123
  return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
4849
5124
  }
4850
5125
 
@@ -4994,8 +5269,8 @@ async function runPreview(opts) {
4994
5269
  }
4995
5270
 
4996
5271
  // src/commands/generate.ts
4997
- import fs15 from "fs/promises";
4998
- import path15 from "path";
5272
+ import fs16 from "fs/promises";
5273
+ import path16 from "path";
4999
5274
  import { PNG as PNG4 } from "pngjs";
5000
5275
 
5001
5276
  // src/lib/glass.ts
@@ -5153,7 +5428,7 @@ async function* readSSE(body) {
5153
5428
  }
5154
5429
 
5155
5430
  // src/commands/generate.ts
5156
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
5431
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
5157
5432
  var INLINE_IMAGE_LIMIT_BYTES = 4 * 1024 * 1024;
5158
5433
  var IMAGE_MIME_BY_EXT = {
5159
5434
  ".png": "image/png",
@@ -5165,7 +5440,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
5165
5440
  async function inlineLocalImage(filePath, flag) {
5166
5441
  let bytes;
5167
5442
  try {
5168
- bytes = await fs15.readFile(filePath);
5443
+ bytes = await fs16.readFile(filePath);
5169
5444
  } catch {
5170
5445
  return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
5171
5446
  }
@@ -5175,7 +5450,7 @@ async function inlineLocalImage(filePath, flag) {
5175
5450
  error: `${flag} file is ${(bytes.length / 1048576).toFixed(1)} MB \u2014 over the ~4 MB inline limit. Downscale/compress it first, or pass an asset URL instead.`
5176
5451
  };
5177
5452
  }
5178
- const mime = IMAGE_MIME_BY_EXT[path15.extname(filePath).toLowerCase()] ?? "image/png";
5453
+ const mime = IMAGE_MIME_BY_EXT[path16.extname(filePath).toLowerCase()] ?? "image/png";
5179
5454
  return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
5180
5455
  }
5181
5456
  var SKYBOX_ENVIRONMENT_SUFFIX = ". The image contains ONLY sky: cloud, atmosphere, light, weather and distant haze at the horizon. Every structure, object, plant and ground surface is outside the frame.";
@@ -5371,7 +5646,7 @@ async function runGenerate(kind, opts) {
5371
5646
  return;
5372
5647
  }
5373
5648
  try {
5374
- const bytes = await fs15.readFile(opts.inpaintUrl);
5649
+ const bytes = await fs16.readFile(opts.inpaintUrl);
5375
5650
  opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
5376
5651
  } catch {
5377
5652
  log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
@@ -5495,7 +5770,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
5495
5770
  return;
5496
5771
  }
5497
5772
  await recordTerminal(view.id, "completed", files.map((f) => f.url));
5498
- await fs15.mkdir(outDir, { recursive: true });
5773
+ await fs16.mkdir(outDir, { recursive: true });
5499
5774
  const solved = [];
5500
5775
  for (let i = 0; i < files.length; i++) {
5501
5776
  const f = files[i];
@@ -5527,8 +5802,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
5527
5802
  });
5528
5803
  continue;
5529
5804
  }
5530
- const outPath = path15.join(outDir, `glass-${i + 1}.png`);
5531
- await fs15.writeFile(outPath, PNG4.sync.write(r.png));
5805
+ const outPath = path16.join(outDir, `glass-${i + 1}.png`);
5806
+ await fs16.writeFile(outPath, PNG4.sync.write(r.png));
5532
5807
  solved.push({
5533
5808
  path: outPath,
5534
5809
  url: f.url,
@@ -5687,14 +5962,14 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
5687
5962
  });
5688
5963
  } catch {
5689
5964
  if (++connectFailures >= 3) return "unsupported";
5690
- await sleep2(2e3);
5965
+ await sleep3(2e3);
5691
5966
  continue;
5692
5967
  }
5693
5968
  if (res.status === 404 || res.status === 405) return "unsupported";
5694
5969
  const contentType = res.headers.get("content-type") ?? "";
5695
5970
  if (!res.ok || !res.body || !contentType.includes("text/event-stream")) {
5696
5971
  if (++connectFailures >= 3) return "unsupported";
5697
- await sleep2(2e3);
5972
+ await sleep3(2e3);
5698
5973
  continue;
5699
5974
  }
5700
5975
  connectFailures = 0;
@@ -5715,7 +5990,7 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
5715
5990
  }
5716
5991
  } catch {
5717
5992
  }
5718
- await sleep2(1e3);
5993
+ await sleep3(1e3);
5719
5994
  } finally {
5720
5995
  clearTimeout(timer);
5721
5996
  abort.abort();
@@ -5743,7 +6018,7 @@ async function poll(apiUrl, token, id, onProgress, timeoutMs) {
5743
6018
  }
5744
6019
  } catch {
5745
6020
  }
5746
- await sleep2(3e3);
6021
+ await sleep3(3e3);
5747
6022
  }
5748
6023
  return null;
5749
6024
  }
@@ -6122,8 +6397,8 @@ async function toRow(e, v, cwd) {
6122
6397
  }
6123
6398
 
6124
6399
  // src/commands/controller.ts
6125
- import fs17 from "fs/promises";
6126
- import path17 from "path";
6400
+ import fs18 from "fs/promises";
6401
+ import path18 from "path";
6127
6402
 
6128
6403
  // ../../packages/meshy-animation-catalog/src/index.ts
6129
6404
  import { createHash } from "crypto";
@@ -15253,9 +15528,9 @@ function searchMeshyAnimations(query, options = {}) {
15253
15528
  }
15254
15529
 
15255
15530
  // src/lib/anims.ts
15256
- import fs16 from "fs/promises";
15257
- import path16 from "path";
15258
- var ANIMS_DEST = path16.join("public", "assets", "anims");
15531
+ import fs17 from "fs/promises";
15532
+ import path17 from "path";
15533
+ var ANIMS_DEST = path17.join("public", "assets", "anims");
15259
15534
  var HIDDEN_TAG = "reference";
15260
15535
  async function runAnims(opts) {
15261
15536
  const log = createLogger({ quiet: opts.quiet });
@@ -15271,7 +15546,7 @@ async function runAnims(opts) {
15271
15546
  printCatalog(log, manifest, selectors);
15272
15547
  return;
15273
15548
  }
15274
- const controllerMarker = path16.join(root, "src", "controllers", "character");
15549
+ const controllerMarker = path17.join(root, "src", "controllers", "character");
15275
15550
  if (!await exists2(controllerMarker)) {
15276
15551
  log.error(
15277
15552
  `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
@@ -15280,11 +15555,11 @@ async function runAnims(opts) {
15280
15555
  process.exitCode = 1;
15281
15556
  return;
15282
15557
  }
15283
- const destDir = path16.join(root, ANIMS_DEST);
15284
- const gameManifestPath = path16.join(destDir, "manifest.json");
15558
+ const destDir = path17.join(root, ANIMS_DEST);
15559
+ const gameManifestPath = path17.join(destDir, "manifest.json");
15285
15560
  if (opts.reset) {
15286
- await fs16.rm(destDir, { recursive: true, force: true });
15287
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path16.sep)} (--reset)`);
15561
+ await fs17.rm(destDir, { recursive: true, force: true });
15562
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path17.sep)} (--reset)`);
15288
15563
  }
15289
15564
  if (selectors.length === 0) {
15290
15565
  const installed = await readGameManifest(gameManifestPath);
@@ -15322,35 +15597,35 @@ async function runAnims(opts) {
15322
15597
  }
15323
15598
  }
15324
15599
  const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
15325
- const cacheDir = path16.join(
15600
+ const cacheDir = path17.join(
15326
15601
  opts.cacheDir ?? getAnimsCacheDir(),
15327
15602
  `${manifest.library}-v${manifest.version}`
15328
15603
  );
15329
- await fs16.mkdir(cacheDir, { recursive: true });
15330
- await fs16.mkdir(destDir, { recursive: true });
15604
+ await fs17.mkdir(cacheDir, { recursive: true });
15605
+ await fs17.mkdir(destDir, { recursive: true });
15331
15606
  const base = getAnimsBase(opts.animsBase);
15332
15607
  let installedCount = 0;
15333
15608
  let presentCount = 0;
15334
15609
  let addedBytes = 0;
15335
15610
  const failures = [];
15336
15611
  for (const entry of wanted) {
15337
- const dest = path16.join(destDir, entry.file);
15612
+ const dest = path17.join(destDir, entry.file);
15338
15613
  if (await hasSize(dest, entry.bytes)) {
15339
15614
  presentCount++;
15340
15615
  continue;
15341
15616
  }
15342
15617
  try {
15343
- const cached = path16.join(cacheDir, entry.file);
15618
+ const cached = path17.join(cacheDir, entry.file);
15344
15619
  if (!await hasSize(cached, entry.bytes)) {
15345
15620
  const res = await fetch(base + entry.file);
15346
15621
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
15347
15622
  const buf = Buffer.from(await res.arrayBuffer());
15348
- await fs16.writeFile(cached, buf);
15623
+ await fs17.writeFile(cached, buf);
15349
15624
  }
15350
- await fs16.copyFile(cached, dest);
15625
+ await fs17.copyFile(cached, dest);
15351
15626
  installedCount++;
15352
15627
  addedBytes += entry.bytes;
15353
- log.dim(` ${path16.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
15628
+ log.dim(` ${path17.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
15354
15629
  } catch (err) {
15355
15630
  failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
15356
15631
  }
@@ -15366,13 +15641,13 @@ async function runAnims(opts) {
15366
15641
  version: manifest.version,
15367
15642
  clips: [...union].sort((a, b) => a.localeCompare(b))
15368
15643
  };
15369
- await fs16.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
15644
+ await fs17.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
15370
15645
  log.plain("");
15371
15646
  const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
15372
15647
  if (presentCount > 0) parts.push(`${presentCount} already present`);
15373
15648
  if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
15374
15649
  log.success(
15375
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path16.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
15650
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path17.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
15376
15651
  );
15377
15652
  for (const [selector, entries] of resolved) {
15378
15653
  const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
@@ -15404,8 +15679,8 @@ async function loadManifest(baseOverride) {
15404
15679
  }
15405
15680
  } catch {
15406
15681
  }
15407
- const snapshotPath = path16.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
15408
- const manifest = JSON.parse(await fs16.readFile(snapshotPath, "utf8"));
15682
+ const snapshotPath = path17.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
15683
+ const manifest = JSON.parse(await fs17.readFile(snapshotPath, "utf8"));
15409
15684
  return { manifest, source: "snapshot" };
15410
15685
  }
15411
15686
  function resolveSelectors(manifest, selectors) {
@@ -15523,21 +15798,21 @@ function printCatalog(log, manifest, selectors) {
15523
15798
  }
15524
15799
  async function readGameManifest(file) {
15525
15800
  try {
15526
- return JSON.parse(await fs16.readFile(file, "utf8"));
15801
+ return JSON.parse(await fs17.readFile(file, "utf8"));
15527
15802
  } catch {
15528
15803
  return null;
15529
15804
  }
15530
15805
  }
15531
15806
  async function hasSize(file, bytes) {
15532
15807
  try {
15533
- return (await fs16.stat(file)).size === bytes;
15808
+ return (await fs17.stat(file)).size === bytes;
15534
15809
  } catch {
15535
15810
  return false;
15536
15811
  }
15537
15812
  }
15538
15813
  async function exists2(p) {
15539
15814
  try {
15540
- await fs16.access(p);
15815
+ await fs17.access(p);
15541
15816
  return true;
15542
15817
  } catch {
15543
15818
  return false;
@@ -15706,8 +15981,8 @@ var CONTROLLER_FILE_SETS = {
15706
15981
  ]
15707
15982
  }
15708
15983
  };
15709
- var CODE_DEST = path17.join("src", "controllers");
15710
- var ASSETS_DEST = path17.join("public", "assets");
15984
+ var CODE_DEST = path18.join("src", "controllers");
15985
+ var ASSETS_DEST = path18.join("public", "assets");
15711
15986
  async function runController(opts) {
15712
15987
  const log = createLogger({ quiet: opts.quiet });
15713
15988
  if (opts.kind?.trim() === "anims") {
@@ -15724,31 +15999,31 @@ async function runController(opts) {
15724
15999
  process.exitCode = 1;
15725
16000
  return;
15726
16001
  }
15727
- const srcDir = path17.join(getTemplatesDir(), "controllers");
16002
+ const srcDir = path18.join(getTemplatesDir(), "controllers");
15728
16003
  const root = opts.cwd ?? process.cwd();
15729
16004
  const set = CONTROLLER_FILE_SETS[kind];
15730
16005
  log.plain(c.bold(`genex controller ${kind}`));
15731
16006
  log.plain("");
15732
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path17.sep)}`);
16007
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path18.sep)}`);
15733
16008
  const plan = [
15734
- ...set.code.map((rel) => ({ from: rel, rel: path17.join(CODE_DEST, rel) })),
16009
+ ...set.code.map((rel) => ({ from: rel, rel: path18.join(CODE_DEST, rel) })),
15735
16010
  ...set.assets.map((rel) => ({
15736
16011
  from: rel,
15737
- rel: path17.join(ASSETS_DEST, path17.basename(rel))
16012
+ rel: path18.join(ASSETS_DEST, path18.basename(rel))
15738
16013
  }))
15739
16014
  ];
15740
16015
  let copied = 0;
15741
16016
  let skipped = 0;
15742
16017
  try {
15743
16018
  for (const file of plan) {
15744
- const dest = path17.join(root, file.rel);
16019
+ const dest = path18.join(root, file.rel);
15745
16020
  if (!opts.force && await exists3(dest)) {
15746
16021
  skipped++;
15747
16022
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
15748
16023
  continue;
15749
16024
  }
15750
- await fs17.mkdir(path17.dirname(dest), { recursive: true });
15751
- await fs17.copyFile(path17.join(srcDir, file.from), dest);
16025
+ await fs18.mkdir(path18.dirname(dest), { recursive: true });
16026
+ await fs18.copyFile(path18.join(srcDir, file.from), dest);
15752
16027
  copied++;
15753
16028
  log.dim(` ${file.rel}`);
15754
16029
  }
@@ -15801,7 +16076,7 @@ async function runController(opts) {
15801
16076
  for (const line of set.sketch) {
15802
16077
  log.dim(` ${line}`);
15803
16078
  }
15804
- if (kind === "character" && !await exists3(path17.join(root, ASSETS_DEST, "meshy-character.json"))) {
16079
+ if (kind === "character" && !await exists3(path18.join(root, ASSETS_DEST, "meshy-character.json"))) {
15805
16080
  log.plain("");
15806
16081
  log.plain(
15807
16082
  ` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
@@ -15833,9 +16108,9 @@ async function installMeshyCharacterManifest(args) {
15833
16108
  throw new Error("The API returned an invalid Meshy character manifest.");
15834
16109
  }
15835
16110
  assertCompleteMeshyControllerPack(manifest);
15836
- const destination = path17.join(args.root, ASSETS_DEST, "meshy-character.json");
15837
- await fs17.mkdir(path17.dirname(destination), { recursive: true });
15838
- await fs17.writeFile(
16111
+ const destination = path18.join(args.root, ASSETS_DEST, "meshy-character.json");
16112
+ await fs18.mkdir(path18.dirname(destination), { recursive: true });
16113
+ await fs18.writeFile(
15839
16114
  destination,
15840
16115
  `${JSON.stringify(manifest, null, 2)}
15841
16116
  `
@@ -15973,14 +16248,14 @@ function assertCompleteMeshyControllerPack(manifest) {
15973
16248
  }
15974
16249
  async function installFallbackAvatar(args) {
15975
16250
  const { root, srcDir, log } = args;
15976
- const dest = path17.join(root, ASSETS_DEST, "avatar.vrm");
15977
- await fs17.mkdir(path17.dirname(dest), { recursive: true });
15978
- await fs17.copyFile(path17.join(srcDir, "assets", "default-avatar.vrm"), dest);
16251
+ const dest = path18.join(root, ASSETS_DEST, "avatar.vrm");
16252
+ await fs18.mkdir(path18.dirname(dest), { recursive: true });
16253
+ await fs18.copyFile(path18.join(srcDir, "assets", "default-avatar.vrm"), dest);
15979
16254
  log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
15980
16255
  }
15981
16256
  async function exists3(p) {
15982
16257
  try {
15983
- await fs17.access(p);
16258
+ await fs18.access(p);
15984
16259
  return true;
15985
16260
  } catch {
15986
16261
  return false;
@@ -16474,22 +16749,22 @@ async function context2(opts) {
16474
16749
  const project = await readProject();
16475
16750
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
16476
16751
  }
16477
- async function readVideo(path19, log) {
16752
+ async function readVideo(path20, log) {
16478
16753
  let bytes;
16479
16754
  try {
16480
- bytes = await readFile(path19);
16755
+ bytes = await readFile(path20);
16481
16756
  } catch {
16482
- log.error(`Can't read ${path19}.`);
16757
+ log.error(`Can't read ${path20}.`);
16483
16758
  return null;
16484
16759
  }
16485
16760
  if (bytes.byteLength > MAX_VIDEO_BYTES) {
16486
- log.error(`${basename(path19)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
16761
+ log.error(`${basename(path20)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
16487
16762
  return null;
16488
16763
  }
16489
16764
  return bytes;
16490
16765
  }
16491
- async function uploadVideo(apiUrl, token, characterId, path19, bytes, log) {
16492
- const contentType = /\.mov$/i.test(path19) ? "video/quicktime" : "video/mp4";
16766
+ async function uploadVideo(apiUrl, token, characterId, path20, bytes, log) {
16767
+ const contentType = /\.mov$/i.test(path20) ? "video/quicktime" : "video/mp4";
16493
16768
  const minted = await apiFetch(
16494
16769
  `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
16495
16770
  {
@@ -16504,7 +16779,7 @@ async function uploadVideo(apiUrl, token, characterId, path19, bytes, log) {
16504
16779
  return null;
16505
16780
  }
16506
16781
  const { uploadUrl, videoUrl } = await minted.json();
16507
- log.dim(` uploading ${basename(path19)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
16782
+ log.dim(` uploading ${basename(path20)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
16508
16783
  const put = await fetch(uploadUrl, {
16509
16784
  method: "PUT",
16510
16785
  headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
@@ -16820,8 +17095,8 @@ function rank(items, query) {
16820
17095
  }
16821
17096
 
16822
17097
  // src/commands/motion.ts
16823
- import fs18 from "fs/promises";
16824
- import path18 from "path";
17098
+ import fs19 from "fs/promises";
17099
+ import path19 from "path";
16825
17100
 
16826
17101
  // src/lib/motion/npz.ts
16827
17102
  import zlib from "zlib";
@@ -18072,7 +18347,7 @@ async function motionGen(opts, log) {
18072
18347
  }
18073
18348
  if (opts.constraintsPath !== void 0) {
18074
18349
  try {
18075
- const raw = await fs18.readFile(opts.constraintsPath, "utf8");
18350
+ const raw = await fs19.readFile(opts.constraintsPath, "utf8");
18076
18351
  generationOptions.constraints = JSON.parse(raw);
18077
18352
  } catch {
18078
18353
  log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
@@ -18096,10 +18371,10 @@ async function motionGen(opts, log) {
18096
18371
  async function expandTakes(selectors) {
18097
18372
  const out = [];
18098
18373
  for (const sel of selectors) {
18099
- const st = await fs18.stat(sel).catch(() => null);
18374
+ const st = await fs19.stat(sel).catch(() => null);
18100
18375
  if (st?.isDirectory()) {
18101
- const names = await fs18.readdir(sel);
18102
- for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path18.join(sel, n));
18376
+ const names = await fs19.readdir(sel);
18377
+ for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path19.join(sel, n));
18103
18378
  } else if (st?.isFile()) {
18104
18379
  out.push(sel);
18105
18380
  } else {
@@ -18134,7 +18409,7 @@ async function motionVerify(opts, log) {
18134
18409
  let gates = DEFAULT_GATES;
18135
18410
  if (opts.gatesPath) {
18136
18411
  try {
18137
- gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs18.readFile(opts.gatesPath, "utf8")));
18412
+ gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs19.readFile(opts.gatesPath, "utf8")));
18138
18413
  } catch {
18139
18414
  log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
18140
18415
  process.exitCode = 1;
@@ -18156,9 +18431,9 @@ async function motionVerify(opts, log) {
18156
18431
  }
18157
18432
  const reports = [];
18158
18433
  for (const file of files) {
18159
- const stem = path18.basename(file).replace(/\.npz$/, "");
18434
+ const stem = path19.basename(file).replace(/\.npz$/, "");
18160
18435
  try {
18161
- reports.push(analyzeTake(stem, await fs18.readFile(file), gates));
18436
+ reports.push(analyzeTake(stem, await fs19.readFile(file), gates));
18162
18437
  } catch (err) {
18163
18438
  reports.push({
18164
18439
  take: stem,
@@ -18196,7 +18471,7 @@ async function motionCompile(opts, log) {
18196
18471
  let cfg = DEFAULT_MOTION_CONFIG;
18197
18472
  if (opts.configPath) {
18198
18473
  try {
18199
- const patch = JSON.parse(await fs18.readFile(opts.configPath, "utf8"));
18474
+ const patch = JSON.parse(await fs19.readFile(opts.configPath, "utf8"));
18200
18475
  cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
18201
18476
  } catch {
18202
18477
  log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
@@ -18214,16 +18489,16 @@ async function motionCompile(opts, log) {
18214
18489
  }
18215
18490
  const inputs = [];
18216
18491
  for (const file of files) {
18217
- const stem = path18.basename(file).replace(/\.npz$/, "");
18492
+ const stem = path19.basename(file).replace(/\.npz$/, "");
18218
18493
  try {
18219
- inputs.push({ stem, take: loadTake(await fs18.readFile(file)) });
18494
+ inputs.push({ stem, take: loadTake(await fs19.readFile(file)) });
18220
18495
  } catch (err) {
18221
18496
  log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
18222
18497
  process.exitCode = 1;
18223
18498
  return;
18224
18499
  }
18225
18500
  }
18226
- const setName = opts.set ?? path18.basename(opts.out).replace(/\.json$/, "");
18501
+ const setName = opts.set ?? path19.basename(opts.out).replace(/\.json$/, "");
18227
18502
  let result;
18228
18503
  try {
18229
18504
  result = compileSet(inputs, setName, cfg);
@@ -18238,9 +18513,9 @@ async function motionCompile(opts, log) {
18238
18513
  process.exitCode = 1;
18239
18514
  return;
18240
18515
  }
18241
- await fs18.mkdir(path18.dirname(path18.resolve(opts.out)), { recursive: true });
18516
+ await fs19.mkdir(path19.dirname(path19.resolve(opts.out)), { recursive: true });
18242
18517
  const json = JSON.stringify(result.data);
18243
- await fs18.writeFile(opts.out, json);
18518
+ await fs19.writeFile(opts.out, json);
18244
18519
  if (opts.json) {
18245
18520
  writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
18246
18521
  return;
@@ -18258,9 +18533,9 @@ var MOTION_RUNTIME_FILES = [
18258
18533
  var MOTION_PRESETS = {
18259
18534
  rifle: ["sets/rifle.json", "sets/jumps.json"]
18260
18535
  };
18261
- var MOTION_DEST = path18.join("src", "motion");
18536
+ var MOTION_DEST = path19.join("src", "motion");
18262
18537
  async function motionInstall(opts, log) {
18263
- const srcDir = path18.join(getTemplatesDir(), "motion");
18538
+ const srcDir = path19.join(getTemplatesDir(), "motion");
18264
18539
  const root = opts.cwd ?? process.cwd();
18265
18540
  const preset = opts.set;
18266
18541
  if (preset !== void 0 && !MOTION_PRESETS[preset]) {
@@ -18271,21 +18546,21 @@ async function motionInstall(opts, log) {
18271
18546
  const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
18272
18547
  log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
18273
18548
  log.plain("");
18274
- log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path18.sep)}`);
18549
+ log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path19.sep)}`);
18275
18550
  let copied = 0, skipped = 0;
18276
18551
  try {
18277
18552
  for (const rel of files) {
18278
- const dest = path18.join(root, MOTION_DEST, rel);
18279
- const exists4 = await fs18.access(dest).then(() => true, () => false);
18553
+ const dest = path19.join(root, MOTION_DEST, rel);
18554
+ const exists4 = await fs19.access(dest).then(() => true, () => false);
18280
18555
  if (!opts.force && exists4) {
18281
18556
  skipped++;
18282
- log.dim(` skipped ${path18.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
18557
+ log.dim(` skipped ${path19.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
18283
18558
  continue;
18284
18559
  }
18285
- await fs18.mkdir(path18.dirname(dest), { recursive: true });
18286
- await fs18.copyFile(path18.join(srcDir, rel), dest);
18560
+ await fs19.mkdir(path19.dirname(dest), { recursive: true });
18561
+ await fs19.copyFile(path19.join(srcDir, rel), dest);
18287
18562
  copied++;
18288
- log.dim(` ${path18.join(MOTION_DEST, rel)}`);
18563
+ log.dim(` ${path19.join(MOTION_DEST, rel)}`);
18289
18564
  }
18290
18565
  } catch (err) {
18291
18566
  log.error(`Copy failed: ${String(err)}`);
@@ -18326,7 +18601,7 @@ async function motionConstraints(opts, log) {
18326
18601
  }
18327
18602
  const doc = directionConstraint(dir, speed, duration);
18328
18603
  const out = opts.out ?? "constraints.json";
18329
- await fs18.writeFile(out, JSON.stringify(doc));
18604
+ await fs19.writeFile(out, JSON.stringify(doc));
18330
18605
  if (opts.json) {
18331
18606
  writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
18332
18607
  return;
@@ -18368,6 +18643,11 @@ var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, aut
18368
18643
 
18369
18644
  ${c.bold("Usage")}
18370
18645
  genex init [<name>] [options] Scaffold + authorize + create the draft project.
18646
+ genex auth [options] Connect this machine to your Genex account, or
18647
+ finish a sign-in another command started \u2014 it
18648
+ resumes the same code, so an interrupted
18649
+ 'genex init' costs nothing. --force switches
18650
+ accounts.
18371
18651
  genex link <slug> [options] Re-link THIS folder to an existing game of yours
18372
18652
  (lost folder / new machine); preview/publish then
18373
18653
  update the same live game. Never creates a project.
@@ -18513,7 +18793,16 @@ ${c.bold("Options for `init`")}
18513
18793
  --api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
18514
18794
  --no-auth Only scaffold templates; skip authorization.
18515
18795
  --force Overwrite existing files (default: never overwrite).
18516
- --timeout <seconds> How long to wait for the auth redirect (default: 300).
18796
+ --timeout <seconds> How long to wait inline for sign-in approval before handing
18797
+ off to 'genex auth' (default: 100). The code itself stays
18798
+ valid for 15 minutes either way.
18799
+
18800
+ ${c.bold("Options for `auth`")}
18801
+ --force Connect a different account even though one is already saved.
18802
+ --env <path> Token env file (default: ~/.genex/env).
18803
+ --auth-url <url> Override the auth site (default: ${DEFAULT_AUTH_URL}).
18804
+ --api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
18805
+ --timeout <seconds> Inline wait before printing the approval link again (default: 100).
18517
18806
 
18518
18807
  ${c.bold("Options for `link`")}
18519
18808
  <slug> The game to reconnect to (the name in your play URL). Required.
@@ -18521,7 +18810,9 @@ ${c.bold("Options for `link`")}
18521
18810
  --env <path> Token env file (default: ~/.genex/env).
18522
18811
  --auth-url <url> Override the auth site (used only if sign-in is needed).
18523
18812
  --api-url <url> Override the API base URL.
18524
- --timeout <seconds> How long to wait for the auth redirect (default: 300).
18813
+ --timeout <seconds> How long to wait inline for sign-in approval before handing
18814
+ off to 'genex auth' (default: 100). The code itself stays
18815
+ valid for 15 minutes either way.
18525
18816
 
18526
18817
  ${c.bold("Options for `preview` / `publish`")}
18527
18818
  --no-build Skip the build step; deploy whatever is already built.
@@ -19169,6 +19460,9 @@ async function main() {
19169
19460
  case "init":
19170
19461
  await runInit(parsed.options);
19171
19462
  break;
19463
+ case "auth":
19464
+ await runAuth(parsed.options);
19465
+ break;
19172
19466
  case "link":
19173
19467
  await runLink(parsed.options);
19174
19468
  break;
@@ -19268,10 +19562,31 @@ async function main() {
19268
19562
  await flushSentry();
19269
19563
  }
19270
19564
  }
19271
- main().catch(async (err) => {
19272
- Sentry2.captureException(err);
19565
+ function checkNodeVersion() {
19566
+ const major = Number(process.versions.node.split(".")[0]);
19567
+ if (!Number.isFinite(major) || major >= 20) return true;
19273
19568
  const log = createLogger();
19274
- log.error(err instanceof Error ? err.message : String(err));
19275
- await flushSentry();
19569
+ log.error(`Genex needs Node 20 or newer \u2014 this is Node ${process.versions.node}.`);
19570
+ log.plain(
19571
+ [
19572
+ ` ${c.cyan("\u2192")} macOS: ${c.cyan("brew install node")} (no Homebrew? use the installer at nodejs.org/download)`,
19573
+ ` ${c.cyan("\u2192")} Windows: ${c.cyan("winget install OpenJS.NodeJS.LTS")}`,
19574
+ ` ${c.cyan("\u2192")} Linux: your distro's node package, or nodejs.org/download`,
19575
+ "",
19576
+ " Then re-run this command."
19577
+ ].join("\n")
19578
+ );
19276
19579
  process.exitCode = 1;
19277
- });
19580
+ return false;
19581
+ }
19582
+ if (checkNodeVersion()) {
19583
+ main().catch(async (err) => {
19584
+ Sentry2.captureException(err);
19585
+ if (!wasPrinted(err)) {
19586
+ const log = createLogger();
19587
+ log.error(err instanceof Error ? err.message : String(err));
19588
+ }
19589
+ await flushSentry();
19590
+ process.exitCode = 1;
19591
+ });
19592
+ }