@genex-ai/cli-demo 1.30.0-dev.645 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +0 -18
  2. package/dist/index.js +2839 -4768
  3. package/package.json +3 -3
  4. package/templates/controllers/character/follow-camera.ts +1 -16
  5. package/templates/controllers/character/meshy/meshy-loader.ts +2 -3
  6. package/templates/controllers/quality/pick-asset.ts +16 -49
  7. package/templates/controllers/shared/physics-world.ts +4 -6
  8. package/templates/skills/genex-ai-character/SKILL.md +15 -77
  9. package/templates/skills/genex-ai-menu/SKILL.md +11 -15
  10. package/templates/skills/genex-ai-model/SKILL.md +7 -45
  11. package/templates/skills/genex-ai-texture/SKILL.md +1 -1
  12. package/templates/skills/genex-ai-video/SKILL.md +17 -75
  13. package/templates/skills/genex-game-director/SKILL.md +46 -112
  14. package/templates/skills/genex-game-director/references/design-contract.md +5 -13
  15. package/templates/skills/genex-game-director/references/routing-map.md +45 -30
  16. package/templates/skills/genex-getting-started/SKILL.md +2 -2
  17. package/templates/skills/genex-monetization/SKILL.md +171 -0
  18. package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +0 -21
  19. package/templates/skills/genex-threejs-character-controller/SKILL.md +5 -17
  20. package/templates/skills/genex-threejs-creatures/SKILL.md +1 -8
  21. package/templates/skills/genex-threejs-embed-auth/SKILL.md +52 -8
  22. package/templates/skills/genex-threejs-game-ui/SKILL.md +6 -55
  23. package/templates/skills/genex-threejs-procedural-assets/SKILL.md +10 -17
  24. package/templates/skills/genex-threejs-visual-validation/SKILL.md +2 -12
  25. package/templates/skills/genex-tool-audio/SKILL.md +2 -3
  26. package/templates/skills/genex-tool-character/SKILL.md +5 -36
  27. package/templates/skills/genex-tool-image/SKILL.md +2 -4
  28. package/templates/skills/genex-tool-model/SKILL.md +6 -32
  29. package/templates/skills/genex-tool-texture/SKILL.md +1 -1
  30. package/templates/skills/genex-tool-video/SKILL.md +5 -28
  31. package/templates/skills/genex-tool-workflow/SKILL.md +1 -4
  32. package/templates/skills/genex-updates/SKILL.md +1 -1
  33. package/dist/blender-mcp-Q6PSFYSE.js +0 -241
  34. package/dist/blender-serve-BF4FZ55Z.js +0 -244
  35. package/dist/chunk-2COG4P3T.js +0 -968
  36. package/dist/chunk-HYCSNWYX.js +0 -126
  37. package/templates/blender-service/demo/castle.py +0 -117
  38. package/templates/blender-service/gpu_witness.py +0 -245
  39. package/templates/blender-service/ops.py +0 -225
  40. package/templates/blender-service/pool.py +0 -910
  41. package/templates/blender-service/server.py +0 -611
  42. package/templates/blender-service/supervisor.py +0 -221
  43. package/templates/blender-service/views.py +0 -281
  44. package/templates/controllers/quality/deadline.ts +0 -117
  45. package/templates/skills/genex-blender-scene/SKILL.md +0 -243
  46. package/templates/skills/genex-lane-card/SKILL.md +0 -78
  47. package/templates/skills/genex-tool-publish/SKILL.md +0 -100
@@ -1,968 +0,0 @@
1
- import {
2
- CLI_CHANNEL,
3
- ENV_FILE_ENV,
4
- ENV_TOKEN_KEY,
5
- c,
6
- getApiUrl,
7
- getAuthUrl,
8
- getCliVersion,
9
- getGenexEnvPath
10
- } from "./chunk-HYCSNWYX.js";
11
-
12
- // src/lib/terms.ts
13
- import readline from "readline";
14
-
15
- // src/lib/source-sync.ts
16
- import fs from "fs/promises";
17
- import path from "path";
18
- import os from "os";
19
-
20
- // src/lib/source-exclude.ts
21
- var SOURCE_EXCLUDE = [
22
- "node_modules/",
23
- "dist/",
24
- ".git/",
25
- // Local project metadata, per-device by design.
26
- ".genex/",
27
- // Secrets — never publish them; `!` keeps the non-secret template.
28
- ".env",
29
- ".env.*",
30
- "!.env.example"
31
- ];
32
- function excludeFile() {
33
- return [...SOURCE_EXCLUDE, ""].join("\n");
34
- }
35
-
36
- // src/utils/run.ts
37
- import { spawn } from "child_process";
38
- var WIN_SHELL_COMMANDS = /* @__PURE__ */ new Set(["npm", "npx"]);
39
- function run(cmd, args, env) {
40
- const shell = process.platform === "win32" && WIN_SHELL_COMMANDS.has(cmd);
41
- return new Promise((resolve) => {
42
- let child;
43
- try {
44
- child = spawn(cmd, args, { env: env ? { ...process.env, ...env } : process.env, shell });
45
- } catch {
46
- resolve({ code: -1, out: "", err: `${cmd} not found` });
47
- return;
48
- }
49
- let out = "";
50
- let err = "";
51
- child.stdout?.on("data", (d) => out += String(d));
52
- child.stderr?.on("data", (d) => err += String(d));
53
- child.on("error", () => resolve({ code: -1, out, err: `${cmd} not found` }));
54
- child.on("close", (code) => resolve({ code: code ?? -1, out, err }));
55
- });
56
- }
57
-
58
- // src/lib/source-sync.ts
59
- async function readRemoteSource(apiUrl, token, projectId) {
60
- try {
61
- const res = await apiFetch(`${apiUrl}/api/projects/${projectId}`, {
62
- headers: { Authorization: `Bearer ${token}` }
63
- });
64
- if (!res.ok) return null;
65
- const data = await res.json().catch(() => null);
66
- if (!data?.project) return null;
67
- return {
68
- stagingCommit: data.project.stagingCommitSha ?? null,
69
- commit: data.project.commitSha ?? null
70
- };
71
- } catch {
72
- return null;
73
- }
74
- }
75
- function isStale(local, remote) {
76
- if (!local || !remote) return false;
77
- return local !== remote;
78
- }
79
- function inHostedSession() {
80
- return process.env.GENEX_HOSTED_SESSION === "1";
81
- }
82
- function mayForceOverAnotherDevice() {
83
- return !inHostedSession();
84
- }
85
- function reportForceRefused(log) {
86
- log.error("`--force` is not available inside a Genex chat session.");
87
- log.dim(" It would replace work shipped from another machine, and that is not yours to discard here.");
88
- log.dim(` Do this instead \u2014 it keeps both sides:`);
89
- log.dim(` 1. ${c.cyan("npx genex pull --force")} takes the other machine's work (a copy of this folder is kept under .genex/replaced-*)`);
90
- log.dim(` 2. redo your change on top of it \u2014 you know what you just changed`);
91
- log.dim(` 3. ${c.cyan("npx genex preview")}`);
92
- log.dim(" If the user explicitly wants their chat version to win, ask them to run `npx genex preview --force` themselves.");
93
- }
94
- function reportStale(log, slug, local, remote) {
95
- log.error(`${c.cyan(slug)} was updated from another device \u2014 nothing was deployed.`);
96
- if (remote && local) {
97
- const short = remote.slice(0, 7) !== local.slice(0, 7);
98
- const r = short ? remote.slice(0, 7) : remote;
99
- const l = short ? local.slice(0, 7) : local;
100
- log.dim(` the draft is on ${r}, this folder last shipped ${l}`);
101
- }
102
- log.dim(` ${c.cyan("npx genex pull")} \u2014 take the other device's work (refuses if you have unshipped changes)`);
103
- log.dim(` ${c.cyan("npx genex preview --force")} \u2014 keep yours and replace theirs`);
104
- }
105
- async function sourceTreeHash(cwd) {
106
- const gitDir = await fs.mkdtemp(path.join(os.tmpdir(), "genex-tree-"));
107
- const base = { GIT_DIR: gitDir };
108
- try {
109
- if ((await run("git", ["init", "-q"], base)).code !== 0) return null;
110
- await fs.writeFile(path.join(gitDir, "info", "exclude"), excludeFile());
111
- if ((await run("git", ["lfs", "version"], base)).code === 0) {
112
- const filters = [
113
- ["filter.lfs.clean", "git-lfs clean -- %f"],
114
- ["filter.lfs.smudge", "git-lfs smudge -- %f"],
115
- ["filter.lfs.process", "git-lfs filter-process"],
116
- ["filter.lfs.required", "true"]
117
- ];
118
- for (const [key, value] of filters) await run("git", ["config", key, value], base);
119
- }
120
- const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path.join(gitDir, "index-tree") };
121
- if ((await run("git", ["add", "-A"], env)).code !== 0) return null;
122
- const tree = (await run("git", ["write-tree"], env)).out.trim();
123
- return /^[0-9a-f]{40}$/.test(tree) ? tree : null;
124
- } catch {
125
- return null;
126
- } finally {
127
- await fs.rm(gitDir, { recursive: true, force: true }).catch(() => {
128
- });
129
- }
130
- }
131
- function urlHasEmbeddedCredentials(url) {
132
- return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(url);
133
- }
134
- function credentialHelperOff(url) {
135
- if (!urlHasEmbeddedCredentials(url)) return {};
136
- return { GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "credential.helper", GIT_CONFIG_VALUE_0: "" };
137
- }
138
- async function fetchCloneGrant(apiUrl, token, projectId, log) {
139
- let res;
140
- try {
141
- res = await apiFetch(`${apiUrl}/api/projects/${projectId}/push-token`, {
142
- method: "POST",
143
- headers: { Authorization: `Bearer ${token}`, "X-Genex-Source-Intent": "read" }
144
- });
145
- } catch (err) {
146
- log.error(`Couldn't reach the API to authorize the source read: ${String(err)}`);
147
- return null;
148
- }
149
- if (res.status === 401) {
150
- log.error("Not authorized \u2014 your token may have expired. Re-run `genex auth`.");
151
- return null;
152
- }
153
- if (!res.ok) {
154
- log.error(`Couldn't authorize the source read (HTTP ${res.status}).`);
155
- return null;
156
- }
157
- const data = await res.json().catch(() => null);
158
- const url = data?.pushUrl ?? data?.cloneUrl;
159
- if (!url) {
160
- log.error("The API didn't return a source URL.");
161
- return null;
162
- }
163
- return { cloneUrl: url, sourceRef: data?.sourceRef ?? null };
164
- }
165
- async function cloneSource(grant, dest, log) {
166
- const args = grant.sourceRef ? ["clone", "--branch", grant.sourceRef, grant.cloneUrl, dest] : ["clone", grant.cloneUrl, dest];
167
- const env = {
168
- GIT_LFS_SKIP_SMUDGE: "1",
169
- GIT_TERMINAL_PROMPT: "0",
170
- ...credentialHelperOff(grant.cloneUrl)
171
- };
172
- const cloned = await run("git", args, env);
173
- if (cloned.code !== 0) {
174
- log.error("Couldn't download the game's source.");
175
- log.dim(` git clone exited ${cloned.code}`);
176
- return false;
177
- }
178
- const repo = { ...env, GIT_DIR: path.join(dest, ".git"), GIT_WORK_TREE: dest };
179
- const filters = [
180
- ["filter.lfs.clean", "git-lfs clean -- %f"],
181
- ["filter.lfs.smudge", "git-lfs smudge -- %f"],
182
- ["filter.lfs.process", "git-lfs filter-process"],
183
- ["filter.lfs.required", "true"]
184
- ];
185
- for (const [key, value] of filters) await run("git", ["config", key, value], repo);
186
- const pulled = await run("git", ["lfs", "pull"], repo);
187
- if (pulled.code !== 0) {
188
- log.warn("Binary assets (models, textures, audio) are still pointer files \u2014 `git lfs pull` failed.");
189
- log.dim(" The source is there; run `git lfs pull` in this folder once git-lfs works.");
190
- }
191
- return true;
192
- }
193
-
194
- // src/lib/terms.ts
195
- var TERMS_ERROR_CODE = "terms_acceptance_required";
196
- var TERMS_WAIT_MS = 1e5;
197
- var TERMS_POLL_MS = 3e3;
198
- async function isTermsRefusal(res) {
199
- if (res.status !== 403) return false;
200
- try {
201
- const body = await res.clone().json();
202
- return body?.error === TERMS_ERROR_CODE;
203
- } catch {
204
- return false;
205
- }
206
- }
207
- async function termsRefusalUrl(res) {
208
- try {
209
- const body = await res.clone().json();
210
- return typeof body?.url === "string" && body.url ? body.url : null;
211
- } catch {
212
- return null;
213
- }
214
- }
215
- function acceptUrl(fromServer, authUrl) {
216
- return fromServer || `${getAuthUrl(authUrl)}/accept`;
217
- }
218
- function reportTermsRefusal(log, url) {
219
- log.error("Your account needs to accept the updated Terms before it can generate or publish.");
220
- log.dim(" Nothing was charged, and nothing on your project changed.");
221
- log.dim(` Open ${c.cyan(acceptUrl(url))} and agree \u2014 one click.`);
222
- }
223
- async function waitForAcceptance(opts) {
224
- const timeoutMs = opts.timeoutMs ?? TERMS_WAIT_MS;
225
- const intervalMs = opts.intervalMs ?? TERMS_POLL_MS;
226
- const doFetch = opts.fetchImpl ?? fetch;
227
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
228
- const now = opts.now ?? Date.now;
229
- const base = opts.apiUrl.replace(/\/+$/, "");
230
- opts.log.dim(` Waiting for you to accept (up to ${Math.round(timeoutMs / 1e3)}s)\u2026`);
231
- const deadline = now() + timeoutMs;
232
- let first = true;
233
- while (first || now() < deadline) {
234
- if (!first) await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
235
- first = false;
236
- try {
237
- const res = await doFetch(`${base}/api/legal/status`, {
238
- headers: { Authorization: `Bearer ${opts.token}` }
239
- });
240
- if (!res.ok) return false;
241
- const body = await res.json().catch(() => null);
242
- if (body?.accepted === true) {
243
- opts.log.success("Accepted \u2014 continuing.");
244
- return true;
245
- }
246
- } catch {
247
- return false;
248
- }
249
- }
250
- opts.log.error("Still not accepted. Re-run this command once you have agreed.");
251
- return false;
252
- }
253
- function askOnStdin(question) {
254
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
255
- return new Promise((resolve) => {
256
- rl.question(question, (answer) => {
257
- rl.close();
258
- resolve(answer);
259
- });
260
- });
261
- }
262
- async function runAccept(opts) {
263
- const { token, log } = opts;
264
- const interactive = opts.interactive ?? Boolean(process.stdin.isTTY);
265
- const apiUrl = getApiUrl(opts.apiUrl);
266
- let acceptPage = null;
267
- const status = await apiFetch(`${apiUrl}/api/legal/status`, {
268
- headers: { Authorization: `Bearer ${token}` }
269
- }).catch(() => null);
270
- if (status?.ok) {
271
- const body = await status.json().catch(() => null);
272
- if (body?.accepted) {
273
- log.success("Already accepted \u2014 nothing to do.");
274
- return true;
275
- }
276
- acceptPage = typeof body?.acceptUrl === "string" ? body.acceptUrl : null;
277
- log.plain("Before generating or publishing, please read and agree to:");
278
- for (const doc of body?.documents ?? []) log.plain(` ${doc.title} ${c.cyan(doc.url)}`);
279
- log.plain("");
280
- }
281
- if (!interactive) {
282
- log.plain("Accepting the Terms needs a person \u2014 an agent cannot agree on your behalf.");
283
- log.plain(` Open ${c.cyan(acceptUrl(acceptPage))} and agree \u2014 one click.`);
284
- return waitForAcceptance({ apiUrl, token, log, ...opts.wait ?? {} });
285
- }
286
- const ask = opts.ask ?? askOnStdin;
287
- const answer = (await ask("Type 'agree' to accept: ")).trim().toLowerCase();
288
- if (answer !== "agree") {
289
- log.error("Not accepted \u2014 nothing was recorded.");
290
- return false;
291
- }
292
- const res = await apiFetch(`${apiUrl}/api/legal/accept`, {
293
- method: "POST",
294
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
295
- body: JSON.stringify({ surface: "cli" })
296
- }).catch(() => null);
297
- if (!res?.ok) {
298
- log.error(`Couldn't record your acceptance (HTTP ${res?.status ?? "no response"}).`);
299
- return false;
300
- }
301
- log.success("Accepted. Re-run your command.");
302
- return true;
303
- }
304
-
305
- // src/lib/api.ts
306
- var stderrLogger = /* @__PURE__ */ (() => {
307
- const err = (s) => void process.stderr.write(s + "\n");
308
- return {
309
- info: err,
310
- success: (m) => err(`${c.green("\u2713")} ${m}`),
311
- warn: (m) => err(`${c.yellow("!")} ${m}`),
312
- error: (m) => err(`${c.red("\u2717")} ${m}`),
313
- step: err,
314
- dim: (m) => err(c.dim(m)),
315
- plain: err
316
- };
317
- })();
318
- var CLI_VERSION_HEADER = "x-genex-cli-version";
319
- var WORKSPACE_HEADER = "x-genex-workspace";
320
- function workspaceHeaderValue(label) {
321
- if (label === null) return null;
322
- if (label === "tools") return "tools";
323
- const slug = label.game.trim();
324
- return slug ? `game:${slug}` : null;
325
- }
326
- var workspaceLabel = null;
327
- function setWorkspaceHeader(label) {
328
- workspaceLabel = workspaceHeaderValue(label);
329
- }
330
- var ASSETS_MODE_HEADER = "x-genex-assets-mode";
331
- var ASSETS_MODE_ENV = "GENEX_ASSETS_MODE";
332
- function assetsModeFromEnv() {
333
- const raw = process.env[ASSETS_MODE_ENV];
334
- if (typeof raw !== "string") return null;
335
- const value = raw.trim();
336
- return value ? value : null;
337
- }
338
- function formatInvalidAssetsMode(body) {
339
- const message = body.message ?? `the ${ASSETS_MODE_HEADER} header is malformed \u2014 send \`none\` or a comma-separated list of lanes.`;
340
- const current = assetsModeFromEnv();
341
- return [
342
- `${c.red("\u2717")} Assets mode refused \u2014 ${lowerFirst(message)}`,
343
- ` It comes from ${ASSETS_MODE_ENV}${current ? `="${current}"` : ""} in this shell \u2014 fix or unset it, then re-run. Nothing was generated or charged.`
344
- ];
345
- }
346
- function formatUpdateRequired(body) {
347
- const action = body.action ?? `npm i -D @genex-ai/cli-demo@${CLI_CHANNEL}`;
348
- const message = body.message ?? `Genex CLI ${body.clientVersion ?? getCliVersion()} is below the minimum supported version${body.minVersion ? ` ${body.minVersion}` : ""}.`;
349
- return [`${c.red("\u2717")} ${message}`, ` Update now \u2014 run: ${action} (then re-run this command)`];
350
- }
351
- function shortDate(iso) {
352
- const d = new Date(iso);
353
- if (Number.isNaN(d.getTime())) return iso;
354
- return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
355
- }
356
- function formatInsufficientCredits(body) {
357
- const message = body.message ?? `this generation costs ${body.price ?? "?"} credits; your balance is ${body.balance ?? 0}.`;
358
- const lines = [`${c.red("\u2717")} Out of credits \u2014 ${lowerFirst(message)}`];
359
- if (body.refillAt && body.refillTo) {
360
- lines.push(` Credits refill to ${body.refillTo} on ${shortDate(body.refillAt)}.`);
361
- }
362
- if (body.url) lines.push(` Get more or check your balance: ${body.url}`);
363
- return lines;
364
- }
365
- function formatVerificationRequired(body) {
366
- const lines = [
367
- `${c.red("\u2717")} Email not verified \u2014 verify your email to unlock your free generation credits.`
368
- ];
369
- if (body.url) lines.push(` Verify here: ${body.url} (then re-run this command)`);
370
- return lines;
371
- }
372
- function lowerFirst(s) {
373
- return s ? s[0].toLowerCase() + s.slice(1) : s;
374
- }
375
- var structuredPrinted = /* @__PURE__ */ new WeakSet();
376
- function printedStructuredError(res) {
377
- return structuredPrinted.has(res);
378
- }
379
- var termsWaitOverride = null;
380
- function replayable(body) {
381
- if (body === void 0 || body === null) return true;
382
- if (typeof body === "string") return true;
383
- if (body instanceof Uint8Array || body instanceof ArrayBuffer) return true;
384
- if (typeof FormData !== "undefined" && body instanceof FormData) return true;
385
- if (body instanceof URLSearchParams) return true;
386
- if (typeof Blob !== "undefined" && body instanceof Blob) return true;
387
- return false;
388
- }
389
- async function apiFetch(url, init = {}, opts = {}) {
390
- const headers = new Headers(init.headers);
391
- if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
392
- if (workspaceLabel && !headers.has(WORKSPACE_HEADER)) {
393
- headers.set(WORKSPACE_HEADER, workspaceLabel);
394
- }
395
- const assetsMode = assetsModeFromEnv();
396
- if (assetsMode && !headers.has(ASSETS_MODE_HEADER)) {
397
- headers.set(ASSETS_MODE_HEADER, assetsMode);
398
- }
399
- const res = await fetch(url, { ...init, headers });
400
- if (res.status === 403 && !opts.noTermsWait && await isTermsRefusal(res)) {
401
- const log = stderrLogger;
402
- reportTermsRefusal(log, await termsRefusalUrl(res));
403
- const auth = headers.get("authorization") ?? headers.get("Authorization");
404
- const token = auth?.replace(/^Bearer\s+/i, "").trim();
405
- let accepted = false;
406
- if (token && replayable(init.body)) {
407
- accepted = await waitForAcceptance({
408
- apiUrl: new URL(url).origin,
409
- token,
410
- log,
411
- ...termsWaitOverride ?? {}
412
- });
413
- }
414
- if (accepted) return apiFetch(url, init, { ...opts, noTermsWait: true });
415
- structuredPrinted.add(res);
416
- return res;
417
- }
418
- if (res.status === 426) {
419
- try {
420
- const body = await res.clone().json();
421
- if (body?.error === "cli_update_required") {
422
- for (const line of formatUpdateRequired(body)) process.stderr.write(line + "\n");
423
- }
424
- } catch {
425
- }
426
- }
427
- if (res.status === 402) {
428
- try {
429
- const body = await res.clone().json();
430
- if (body?.error === "insufficient_credits") {
431
- for (const line of formatInsufficientCredits(body)) process.stderr.write(line + "\n");
432
- structuredPrinted.add(res);
433
- }
434
- } catch {
435
- }
436
- }
437
- if (res.status === 403) {
438
- try {
439
- const body = await res.clone().json();
440
- if (body?.error === "email_verification_required") {
441
- for (const line of formatVerificationRequired(body)) process.stderr.write(line + "\n");
442
- structuredPrinted.add(res);
443
- }
444
- } catch {
445
- }
446
- }
447
- if (res.status === 400) {
448
- try {
449
- const body = await res.clone().json();
450
- if (body?.error === "invalid_assets_mode") {
451
- for (const line of formatInvalidAssetsMode(body)) process.stderr.write(line + "\n");
452
- structuredPrinted.add(res);
453
- }
454
- } catch {
455
- }
456
- }
457
- if (res.status === 503) {
458
- try {
459
- const body = await res.clone().json();
460
- if (body?.error === "generation_paused") {
461
- process.stderr.write(
462
- `${c.red("\u2717")} ${body.message ?? "Generation is temporarily paused platform-wide. Try again later."}
463
- `
464
- );
465
- structuredPrinted.add(res);
466
- }
467
- if (body?.error === "provider_unavailable") {
468
- process.stderr.write(
469
- `${c.red("\u2717")} ${body.message ?? "This generation lane is unavailable \u2014 our provider account is out of credit. Build it in code and move on."}
470
- `
471
- );
472
- structuredPrinted.add(res);
473
- }
474
- } catch {
475
- }
476
- }
477
- return res;
478
- }
479
- async function fetchSignedInEmail(apiUrl, token) {
480
- try {
481
- const res = await apiFetch(`${apiUrl}/api/auth/get-session`, {
482
- headers: { Authorization: `Bearer ${token}` }
483
- });
484
- if (!res.ok) return null;
485
- const data = await res.json().catch(() => null);
486
- return data?.user?.email ?? null;
487
- } catch {
488
- return null;
489
- }
490
- }
491
-
492
- // src/lib/blender-client.ts
493
- import fs4 from "fs";
494
- import path4 from "path";
495
-
496
- // src/lib/store.ts
497
- import fs3 from "fs/promises";
498
- import path3 from "path";
499
-
500
- // src/lib/env.ts
501
- import fs2 from "fs/promises";
502
- import path2 from "path";
503
- import { spawn as spawn2 } from "child_process";
504
- async function writeEnvVar(envPath, key, value) {
505
- let content = "";
506
- let existed = false;
507
- try {
508
- content = await fs2.readFile(envPath, "utf8");
509
- existed = true;
510
- } catch {
511
- }
512
- const assignment = `${key}=${formatValue(value)}`;
513
- const keyPattern = new RegExp(
514
- `^(\\s*export\\s+)?${escapeRegExp(key)}=.*$`,
515
- "gm"
516
- );
517
- let next;
518
- let mode;
519
- if (keyPattern.test(content)) {
520
- next = content.replace(keyPattern, assignment);
521
- mode = "updated";
522
- } else {
523
- let prefix = content;
524
- if (prefix.length > 0 && !prefix.endsWith("\n")) prefix += "\n";
525
- next = prefix + assignment + "\n";
526
- mode = existed ? "appended" : "created";
527
- }
528
- await fs2.mkdir(path2.dirname(envPath), { recursive: true });
529
- await fs2.writeFile(envPath, next, { mode: 384 });
530
- await restrictFilePermissions(envPath);
531
- return { mode, path: envPath };
532
- }
533
- async function restrictFilePermissions(filePath) {
534
- if (process.platform !== "win32") {
535
- await fs2.chmod(filePath, 384).catch(() => {
536
- });
537
- return;
538
- }
539
- const user = process.env.USERNAME ?? process.env.USER;
540
- if (!user) return;
541
- await new Promise((resolve) => {
542
- try {
543
- const child = spawn2(
544
- "icacls",
545
- [filePath, "/inheritance:r", "/grant:r", `${user}:F`],
546
- { stdio: "ignore" }
547
- );
548
- child.on("error", () => resolve());
549
- child.on("close", () => resolve());
550
- } catch {
551
- resolve();
552
- }
553
- });
554
- }
555
- function formatValue(value) {
556
- if (/[\s#"'$`\\]/.test(value)) {
557
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
558
- }
559
- return value;
560
- }
561
- function escapeRegExp(s) {
562
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
563
- }
564
-
565
- // src/lib/store.ts
566
- function getProjectMetadataPath(cwd = process.cwd()) {
567
- return path3.join(cwd, ".genex", "project.json");
568
- }
569
- function getWorkspacePath(cwd = process.cwd()) {
570
- return path3.join(cwd, ".genex", "workspace.json");
571
- }
572
- async function readWorkspace(cwd = process.cwd()) {
573
- try {
574
- const raw = await fs3.readFile(getWorkspacePath(cwd), "utf8");
575
- return JSON.parse(raw);
576
- } catch {
577
- return null;
578
- }
579
- }
580
- async function writeWorkspace(meta, cwd = process.cwd()) {
581
- const file = getWorkspacePath(cwd);
582
- await fs3.mkdir(path3.dirname(file), { recursive: true });
583
- await fs3.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
584
- await fs3.chmod(file, 384).catch(() => {
585
- });
586
- return { path: file };
587
- }
588
- async function writeUserToken(token, envPath) {
589
- const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
590
- return { path: written };
591
- }
592
- async function rotateRejectedEnv(envPath) {
593
- const file = getGenexEnvPath(envPath);
594
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
595
- const aside = `${file}.rejected-${stamp}`;
596
- try {
597
- await fs3.rename(file, aside);
598
- return aside;
599
- } catch {
600
- return null;
601
- }
602
- }
603
- async function readUserToken(envPath) {
604
- const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
605
- if (fromGenex) return fromGenex;
606
- if (!envPath && !process.env[ENV_FILE_ENV]) {
607
- return readTokenFromFile(path3.join(process.cwd(), ".env"));
608
- }
609
- return null;
610
- }
611
- async function readTokenFromFile(file) {
612
- let content;
613
- try {
614
- content = await fs3.readFile(file, "utf8");
615
- } catch {
616
- return null;
617
- }
618
- const m = content.match(/^\s*(?:export\s+)?GENEX_TOKEN=(.*)$/m);
619
- if (!m) return null;
620
- return stripQuotes(m[1].trim()) || null;
621
- }
622
- function stripQuotes(v) {
623
- if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
624
- return v.slice(1, -1);
625
- }
626
- return v;
627
- }
628
- async function readProject(cwd = process.cwd()) {
629
- try {
630
- const raw = await fs3.readFile(getProjectMetadataPath(cwd), "utf8");
631
- return JSON.parse(raw);
632
- } catch {
633
- return null;
634
- }
635
- }
636
- var SCRATCH_DIR = ".genex/scratch";
637
- async function writeProject(meta, cwd = process.cwd()) {
638
- const file = getProjectMetadataPath(cwd);
639
- await fs3.mkdir(path3.dirname(file), { recursive: true });
640
- await fs3.mkdir(path3.join(cwd, SCRATCH_DIR), { recursive: true }).catch(() => {
641
- });
642
- await fs3.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
643
- await fs3.chmod(file, 384).catch(() => {
644
- });
645
- return { path: file };
646
- }
647
-
648
- // src/lib/blender-client.ts
649
- var BLENDER_TIMEOUT_MS = 15 * 60 * 1e3;
650
- var RENDER_MODES = ["solid", "wireframe", "normals", "lit"];
651
- function isRenderMode(v) {
652
- return typeof v === "string" && RENDER_MODES.includes(v);
653
- }
654
- function blenderEndpoint() {
655
- const raw = process.env.GENEX_BLENDER_URL?.trim();
656
- return raw ? raw.replace(/\/+$/, "") : void 0;
657
- }
658
- function blenderSetupHint() {
659
- if (process.env.GENEX_HOSTED_SESSION === "1") {
660
- return [
661
- "The Blender lane is off on this stand \u2014 there is no seat to acquire.",
662
- " Build the space in code instead ($genex-threejs-procedural-assets); do not wait for it."
663
- ].join("\n");
664
- }
665
- return [
666
- "No Blender endpoint. Run the service against your own Blender in another terminal:",
667
- " npx genex blender serve",
668
- " export GENEX_BLENDER_URL=http://localhost:8088",
669
- " GENEX_BLENDER_URL may also point at any running genex-blender service."
670
- ].join("\n");
671
- }
672
- var SHEET_FORMATS = ["webp", "png"];
673
- function sheetOf(r) {
674
- if (r.contactSheet?.b64) return { b64: r.contactSheet.b64, mime: r.contactSheet.mime };
675
- if (r.contactSheetPng) return { b64: r.contactSheetPng, mime: "image/png" };
676
- return null;
677
- }
678
- function sheetExt(mime) {
679
- return mime === "image/webp" ? "webp" : "png";
680
- }
681
- var SEAT_FILE = path4.join(".genex", "blender-seat.json");
682
- function readSeatGrant(cwd = process.cwd()) {
683
- try {
684
- const raw = JSON.parse(fs4.readFileSync(path4.join(cwd, SEAT_FILE), "utf8"));
685
- if (typeof raw.url !== "string" || typeof raw.token !== "string") return null;
686
- return {
687
- url: raw.url,
688
- token: raw.token,
689
- ...typeof raw.seatId === "string" ? { seatId: raw.seatId } : {},
690
- ...raw.kind === "cli" || raw.kind === "hosted" ? { kind: raw.kind } : {}
691
- };
692
- } catch {
693
- return null;
694
- }
695
- }
696
- function writeSeatGrant(grant, cwd = process.cwd()) {
697
- const file = path4.join(cwd, SEAT_FILE);
698
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
699
- fs4.writeFileSync(file, JSON.stringify(grant, null, 2) + "\n", { mode: 384 });
700
- }
701
- function deleteSeatGrant(cwd = process.cwd()) {
702
- try {
703
- fs4.unlinkSync(path4.join(cwd, SEAT_FILE));
704
- } catch {
705
- }
706
- }
707
- var lastTouchAt = 0;
708
- var TOUCH_THROTTLE_MS = 6e4;
709
- async function touchCliSeat() {
710
- if (Date.now() - lastTouchAt < TOUCH_THROTTLE_MS) return;
711
- lastTouchAt = Date.now();
712
- try {
713
- const token = await readUserToken();
714
- if (!token) return;
715
- await apiFetch(`${getApiUrl()}/api/blender/seat/touch`, {
716
- method: "POST",
717
- headers: { Authorization: `Bearer ${token}` }
718
- });
719
- } catch {
720
- }
721
- }
722
- async function blenderCall(base, route, body) {
723
- const cwd = process.cwd();
724
- const seat = readSeatGrant();
725
- const ctl = new AbortController();
726
- const timer = setTimeout(() => ctl.abort(), BLENDER_TIMEOUT_MS);
727
- try {
728
- const res = await fetch(`${base}${route}`, {
729
- method: body === void 0 ? "GET" : "POST",
730
- headers: {
731
- // A REAL User-Agent, because the vendor proxy in front of a hosted pod
732
- // sits behind Cloudflare, and Cloudflare's error 1010 refuses the
733
- // default Python UA outright (measured: Python-urllib/3.x -> 403; any
734
- // override -> 200). Whether it also refuses undici's default is
735
- // untested, and this is the client every hosted agent will reach the
736
- // pod with -- so the question is closed here rather than found in a
737
- // sandbox as a 403 that looks like a bad token.
738
- "User-Agent": `genex-cli/${getCliVersion()}`,
739
- ...body === void 0 ? {} : { "Content-Type": "application/json" },
740
- // TWO credentials, two headers, and the router is strict about which is
741
- // which: a hosted SEAT presents its token as `x-genex-seat` on
742
- // `/s/<sid>/…`, while `x-genex-internal` is the POD secret and is honoured
743
- // only on `/pool/*`. The first cut sent the seat token under the pod
744
- // header and every hosted call would have been a 401 whose message
745
- // pointed at the wrong knob (review finding).
746
- ...seat?.token ? { "x-genex-seat": seat.token } : {},
747
- // Sent only when set. The service treats an unset secret as open, which
748
- // is right on localhost; the deployed compose makes it mandatory.
749
- ...!seat?.token && process.env.GENEX_BLENDER_SECRET ? { "x-genex-internal": process.env.GENEX_BLENDER_SECRET } : {}
750
- },
751
- body: body === void 0 ? void 0 : JSON.stringify(body),
752
- signal: ctl.signal
753
- });
754
- if (res.status === 401 && seat?.token) {
755
- const current = readSeatGrant(cwd);
756
- if (current?.url === seat.url && current.token === seat.token) deleteSeatGrant(cwd);
757
- await res.body?.cancel().catch(() => {
758
- });
759
- throw new Error(
760
- `${route} refused the seat token (401) \u2014 ${seat.kind === "cli" ? 'run "genex blender seat" to acquire a new one' : "run the command again to acquire a new one"}`
761
- );
762
- }
763
- const text = await res.text();
764
- let json;
765
- try {
766
- json = JSON.parse(text);
767
- } catch {
768
- throw new Error(`${route} answered ${res.status} with non-JSON: ${text.slice(0, 200)}`);
769
- }
770
- if (!res.ok) {
771
- if (res.status === 401) {
772
- throw new Error(
773
- seat ? `${route} refused the seat token (401) \u2014 the seat may have been closed; run the command again to acquire a new one` : `${route} refused the request (401) \u2014 set GENEX_BLENDER_SECRET to the service's secret`
774
- );
775
- }
776
- const detail = typeof json.detail === "string" ? ` \u2014 ${json.detail}` : "";
777
- const stage = typeof json.stage === "string" ? ` [${json.stage}]` : "";
778
- const mutationWarning = json.scriptStarted === true ? " (script may have changed the scene; inspect it before retrying)" : "";
779
- const trace = typeof json.trace === "string" ? `
780
- ${json.trace.slice(-4e3)}` : "";
781
- throw new Error(`${route} failed (${res.status}): ${json.error ?? text.slice(0, 200)}${stage}${detail}${mutationWarning}${trace}`);
782
- }
783
- if (seat?.kind === "cli") void touchCliSeat();
784
- if (seat?.kind === "hosted" && seat.seatId && route === "/health") {
785
- const token = await readUserToken();
786
- if (!token) throw new Error("Cannot acknowledge Blender readiness: not signed in");
787
- const claim = await apiFetch(`${getApiUrl()}/api/blender/seat/touch`, {
788
- method: "POST",
789
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
790
- body: JSON.stringify({ seatId: seat.seatId })
791
- });
792
- if (!claim.ok) throw new Error("Blender readiness acknowledgement failed; no scene command was sent. Run the command again.");
793
- }
794
- return json;
795
- } catch (err) {
796
- if (err instanceof Error && err.name === "AbortError") {
797
- throw new Error(`${route} timed out after ${BLENDER_TIMEOUT_MS / 1e3}s`);
798
- }
799
- throw err;
800
- } finally {
801
- clearTimeout(timer);
802
- }
803
- }
804
- function sceneSummary(s) {
805
- if (!s) return "";
806
- return `objects ${s.objectCount} meshes ${s.meshCount} tris ${s.totalTris} materials ${s.materialCount} radius ${s.bounds.radius}`;
807
- }
808
-
809
- // src/lib/blender-seat.ts
810
- var SEAT_WAIT_BUDGET_MS = 9e4;
811
- var SEAT_POLL_MS = 1e4;
812
- function hostedBlenderLane() {
813
- return process.env.GENEX_BLENDER_LANE === "1";
814
- }
815
- async function acquireSeat(opts) {
816
- const held = readSeatGrant(opts.cwd);
817
- if (held) return { kind: "granted", grant: held };
818
- if (!hostedBlenderLane()) return { kind: "off" };
819
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
820
- if (!token) return { kind: "refused", reason: "not signed in" };
821
- const doFetch = opts.fetchImpl ?? apiFetch;
822
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
823
- const deadline = Date.now() + (opts.budgetMs ?? SEAT_WAIT_BUDGET_MS);
824
- let announced = false;
825
- for (; ; ) {
826
- const res = await doFetch(`${getApiUrl()}/api/blender/seat`, {
827
- method: "POST",
828
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
829
- body: JSON.stringify({ acknowledgeReadiness: true })
830
- });
831
- if (res.status === 201) {
832
- const body = await res.json();
833
- if (typeof body.url !== "string" || typeof body.token !== "string") {
834
- return { kind: "refused", reason: "the API answered a seat without a url and token" };
835
- }
836
- const grant = { url: body.url, token: body.token, kind: "hosted", ...typeof body.seatId === "string" ? { seatId: body.seatId } : {} };
837
- writeSeatGrant(grant, opts.cwd);
838
- return { kind: "granted", grant };
839
- }
840
- if (res.status === 202 || res.status === 429) {
841
- if (!announced) {
842
- opts.log.step("Warming up a Blender seat (about 20 s on a warm pod, ~2 min on a cold one, longer on a host pulling the image for the first time)\u2026");
843
- announced = true;
844
- }
845
- if (Date.now() >= deadline) return { kind: "warming" };
846
- await sleep(SEAT_POLL_MS);
847
- continue;
848
- }
849
- if (res.status === 404) return { kind: "off" };
850
- let reason = `HTTP ${res.status}`;
851
- try {
852
- const body = await res.json();
853
- reason = body.reason ?? body.error ?? reason;
854
- } catch {
855
- }
856
- return { kind: "refused", reason };
857
- }
858
- }
859
- async function acquireCliSeat(opts) {
860
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
861
- if (!token) return { kind: "refused", reason: "not signed in" };
862
- const doFetch = opts.fetchImpl ?? apiFetch;
863
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
864
- const deadline = Date.now() + (opts.budgetMs ?? SEAT_WAIT_BUDGET_MS);
865
- let announced = false;
866
- for (; ; ) {
867
- const res = await doFetch(`${getApiUrl()}/api/blender/seat`, {
868
- method: "POST",
869
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
870
- body: JSON.stringify({ cli: true, scope: opts.scope, acknowledgeReadiness: true })
871
- });
872
- if (res.status === 201) {
873
- const body = await res.json();
874
- if (typeof body.url !== "string" || typeof body.token !== "string") {
875
- return { kind: "refused", reason: "the API answered a seat without a url and token" };
876
- }
877
- const grant = {
878
- url: body.url,
879
- token: body.token,
880
- kind: body.kind === "hosted" ? "hosted" : "cli",
881
- ...typeof body.seatId === "string" ? { seatId: body.seatId } : {}
882
- };
883
- writeSeatGrant(grant, opts.cwd);
884
- if (grant.kind === "cli" && body.pricing) {
885
- opts.log.step(
886
- `Blender seat: ${body.pricing.creditsPerBlock} credits per ${body.pricing.blockSeconds}s while it's open. It closes itself after ${body.pricing.idleMinutes} idle minutes, or run "genex blender release".`
887
- );
888
- }
889
- return { kind: "granted", grant };
890
- }
891
- if (res.status === 202 || res.status === 429) {
892
- if (!announced) {
893
- opts.log.step("Warming up a Blender seat (about 20 s on a warm pod, ~2 min on a cold one, longer on a host pulling the image for the first time)\u2026");
894
- announced = true;
895
- }
896
- if (Date.now() >= deadline) return { kind: "warming" };
897
- await sleep(SEAT_POLL_MS);
898
- continue;
899
- }
900
- if (res.status === 404) return { kind: "off" };
901
- if (res.status === 402) return { kind: "refused", reason: "insufficient credits" };
902
- let reason = `HTTP ${res.status}`;
903
- try {
904
- const body = await res.json();
905
- reason = body.reason ?? body.error ?? reason;
906
- } catch {
907
- }
908
- return { kind: "refused", reason };
909
- }
910
- }
911
- async function releaseCliSeat(opts) {
912
- const token = opts.token !== void 0 ? opts.token : await readUserToken();
913
- if (!token) return { ok: false, reason: "not signed in" };
914
- const doFetch = opts.fetchImpl ?? apiFetch;
915
- const res = await doFetch(`${getApiUrl()}/api/blender/seat`, {
916
- method: "DELETE",
917
- headers: { Authorization: `Bearer ${token}` }
918
- });
919
- if (!res.ok) return { ok: false, reason: `HTTP ${res.status}` };
920
- deleteSeatGrant(opts.cwd);
921
- return { ok: true };
922
- }
923
-
924
- export {
925
- excludeFile,
926
- run,
927
- readRemoteSource,
928
- isStale,
929
- inHostedSession,
930
- mayForceOverAnotherDevice,
931
- reportForceRefused,
932
- reportStale,
933
- sourceTreeHash,
934
- urlHasEmbeddedCredentials,
935
- fetchCloneGrant,
936
- cloneSource,
937
- isTermsRefusal,
938
- acceptUrl,
939
- reportTermsRefusal,
940
- runAccept,
941
- setWorkspaceHeader,
942
- ASSETS_MODE_ENV,
943
- assetsModeFromEnv,
944
- printedStructuredError,
945
- apiFetch,
946
- fetchSignedInEmail,
947
- restrictFilePermissions,
948
- readWorkspace,
949
- writeWorkspace,
950
- writeUserToken,
951
- rotateRejectedEnv,
952
- readUserToken,
953
- readProject,
954
- writeProject,
955
- RENDER_MODES,
956
- isRenderMode,
957
- blenderEndpoint,
958
- blenderSetupHint,
959
- SHEET_FORMATS,
960
- sheetOf,
961
- sheetExt,
962
- readSeatGrant,
963
- blenderCall,
964
- sceneSummary,
965
- acquireSeat,
966
- acquireCliSeat,
967
- releaseCliSeat
968
- };