@genex-ai/cli-demo 0.97.0-dev.277 → 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.
- package/dist/index.js +1175 -859
- 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/
|
|
198
|
-
import
|
|
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/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
return
|
|
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
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
267
|
+
}
|
|
268
|
+
if (res.status === 402) {
|
|
312
269
|
try {
|
|
313
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
301
|
+
return res;
|
|
350
302
|
}
|
|
351
|
-
|
|
352
|
-
async function readSkillsMarker(skillsDir) {
|
|
303
|
+
async function fetchSignedInEmail(apiUrl, token) {
|
|
353
304
|
try {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
371
|
-
|
|
328
|
+
content = await fs2.readFile(envPath, "utf8");
|
|
329
|
+
existed = true;
|
|
372
330
|
} catch {
|
|
373
|
-
return false;
|
|
374
331
|
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
}
|
|
496
|
-
}
|
|
373
|
+
});
|
|
497
374
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
513
|
-
|
|
514
|
-
return Number.isFinite(at) && nowMs - at < CHECK_INTERVAL_MS;
|
|
381
|
+
function escapeRegExp(s) {
|
|
382
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
515
383
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return
|
|
384
|
+
|
|
385
|
+
// src/lib/pending-auth.ts
|
|
386
|
+
function pendingAuthPath() {
|
|
387
|
+
return path3.join(getGenexDir(), "pending-auth.json");
|
|
520
388
|
}
|
|
521
|
-
async function
|
|
389
|
+
async function readPendingAuth(apiUrl) {
|
|
522
390
|
try {
|
|
523
|
-
const raw = await
|
|
524
|
-
|
|
525
|
-
if (
|
|
526
|
-
return
|
|
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
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
488
|
+
function defaultLabel() {
|
|
571
489
|
try {
|
|
572
|
-
|
|
573
|
-
const pkg = JSON.parse(raw);
|
|
574
|
-
return typeof pkg.version === "string" ? pkg.version : null;
|
|
490
|
+
return os3.hostname();
|
|
575
491
|
} catch {
|
|
576
|
-
return
|
|
492
|
+
return "";
|
|
577
493
|
}
|
|
578
494
|
}
|
|
579
|
-
function
|
|
580
|
-
|
|
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
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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 (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
|
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 =
|
|
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/
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
|
897
|
-
const
|
|
898
|
-
|
|
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
|
|
902
|
-
const
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
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
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
]
|
|
914
|
-
|
|
915
|
-
|
|
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
|
|
918
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
910
|
+
log.warn("Your saved sign-in is no longer valid \u2014 reconnecting\u2026");
|
|
945
911
|
}
|
|
946
912
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
|
976
|
+
async function exists(p) {
|
|
973
977
|
try {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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) {
|
|
1351
|
+
try {
|
|
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
|
|
1070
|
-
import
|
|
1457
|
+
import fs8 from "fs/promises";
|
|
1458
|
+
import path8 from "path";
|
|
1071
1459
|
async function writeGitignore(dir, log) {
|
|
1072
|
-
const file =
|
|
1460
|
+
const file = path8.join(dir, ".gitignore");
|
|
1073
1461
|
let content = "";
|
|
1074
1462
|
try {
|
|
1075
|
-
content = await
|
|
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
|
|
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 =
|
|
1503
|
+
const file = path8.join(dir, ".gitattributes");
|
|
1116
1504
|
let content = "";
|
|
1117
1505
|
try {
|
|
1118
|
-
content = await
|
|
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
|
|
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
|
|
1264
|
-
import
|
|
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
|
|
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
|
|
1334
|
-
await writeIfAbsent(
|
|
1335
|
-
await writeIfAbsent(
|
|
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(
|
|
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 :
|
|
1376
|
-
const dest = t.full ? t.baseDir :
|
|
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(
|
|
1382
|
-
await writeSkillsMarker(
|
|
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
|
-
|
|
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() ||
|
|
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
|
-
|
|
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
|
|
1474
|
-
import
|
|
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
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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 =
|
|
1793
|
+
const file = path11.join(cwd, ".env");
|
|
1535
1794
|
let content;
|
|
1536
1795
|
try {
|
|
1537
|
-
content = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1887
|
+
inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
|
|
1626
1888
|
});
|
|
1627
1889
|
} catch (err) {
|
|
1628
|
-
|
|
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
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
|
1714
|
-
import
|
|
1715
|
-
import
|
|
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
|
|
1979
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
2557
|
-
await
|
|
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
|
|
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 =
|
|
3073
|
-
const maskPath =
|
|
3074
|
-
const annotatedPath =
|
|
3075
|
-
const overlayPath =
|
|
3076
|
-
const metaPath =
|
|
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
|
|
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 =
|
|
3140
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
3874
|
+
const absAssets = path12.resolve(cwd, assetDir);
|
|
3600
3875
|
try {
|
|
3601
|
-
if (!(await
|
|
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(
|
|
3608
|
-
(p) => AUDIT_SRC_EXTS.has(
|
|
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
|
|
3612
|
-
const ext =
|
|
3613
|
-
if (ext === ".html" || ext === ".css") srcFiles.push(
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
3692
|
-
mask = PNG2.sync.read(await
|
|
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
|
|
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(
|
|
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 =
|
|
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: `${
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
4210
|
+
const distDir = path13.join(cwd, "dist");
|
|
3936
4211
|
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
3937
|
-
const rel =
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
4252
|
-
|
|
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:
|
|
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
|
|
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
|
|
4618
|
+
return (await fs13.stat(p)).isDirectory();
|
|
4344
4619
|
} catch {
|
|
4345
4620
|
return false;
|
|
4346
4621
|
}
|
|
4347
4622
|
}
|
|
4348
|
-
var
|
|
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
|
|
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
|
|
4446
|
-
import
|
|
4720
|
+
import fs15 from "fs/promises";
|
|
4721
|
+
import path15 from "path";
|
|
4447
4722
|
|
|
4448
4723
|
// src/lib/generation-ledger.ts
|
|
4449
|
-
import
|
|
4450
|
-
import
|
|
4451
|
-
var ledgerPath = (cwd) =>
|
|
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
|
|
4455
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4837
|
+
const srcDir = path15.join(cwd, "src");
|
|
4563
4838
|
let entries;
|
|
4564
4839
|
try {
|
|
4565
|
-
entries = await
|
|
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
|
|
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 =
|
|
4857
|
+
const srcDir = path15.join(cwd, "src");
|
|
4583
4858
|
let entries;
|
|
4584
4859
|
try {
|
|
4585
|
-
entries = await
|
|
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
|
|
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,18 +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 =
|
|
4912
|
+
const srcDir = path15.join(cwd, "src");
|
|
4638
4913
|
let entries;
|
|
4639
4914
|
try {
|
|
4640
|
-
entries = await
|
|
4915
|
+
entries = await fs15.readdir(srcDir, { recursive: true });
|
|
4641
4916
|
} catch {
|
|
4642
4917
|
return found;
|
|
4643
4918
|
}
|
|
4644
|
-
for (const
|
|
4645
|
-
if (
|
|
4646
|
-
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(
|
|
4647
|
-
const raw = await
|
|
4919
|
+
for (const nativeRel of entries) {
|
|
4920
|
+
if (nativeRel.includes("node_modules")) continue;
|
|
4921
|
+
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
|
|
4922
|
+
const raw = await fs15.readFile(path15.join(srcDir, nativeRel), "utf8").catch(() => "");
|
|
4648
4923
|
if (!raw) continue;
|
|
4924
|
+
const rel = nativeRel.split(path15.sep).join("/");
|
|
4649
4925
|
const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
|
|
4650
4926
|
const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
4651
4927
|
let m;
|
|
@@ -4691,25 +4967,25 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
4691
4967
|
let haystack = "";
|
|
4692
4968
|
const read = async (file) => {
|
|
4693
4969
|
try {
|
|
4694
|
-
haystack += await
|
|
4970
|
+
haystack += await fs15.readFile(file, "utf8");
|
|
4695
4971
|
} catch {
|
|
4696
4972
|
}
|
|
4697
4973
|
};
|
|
4698
4974
|
try {
|
|
4699
|
-
for (const entry of await
|
|
4975
|
+
for (const entry of await fs15.readdir(cwd, { withFileTypes: true })) {
|
|
4700
4976
|
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
|
|
4701
|
-
await read(
|
|
4977
|
+
await read(path15.join(cwd, entry.name));
|
|
4702
4978
|
}
|
|
4703
4979
|
}
|
|
4704
4980
|
} catch {
|
|
4705
4981
|
}
|
|
4706
4982
|
for (const sub of ["src", "public"]) {
|
|
4707
4983
|
try {
|
|
4708
|
-
const entries = await
|
|
4984
|
+
const entries = await fs15.readdir(path15.join(cwd, sub), { recursive: true });
|
|
4709
4985
|
for (const rel of entries) {
|
|
4710
4986
|
if (rel.includes("node_modules")) continue;
|
|
4711
4987
|
if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
|
|
4712
|
-
await read(
|
|
4988
|
+
await read(path15.join(cwd, sub, rel));
|
|
4713
4989
|
}
|
|
4714
4990
|
} catch {
|
|
4715
4991
|
}
|
|
@@ -4835,7 +5111,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
4835
5111
|
} catch {
|
|
4836
5112
|
return true;
|
|
4837
5113
|
}
|
|
4838
|
-
const gitConfig = await
|
|
5114
|
+
const gitConfig = await fs15.readFile(path15.join(cwd, ".git", "config"), "utf8").catch(() => "");
|
|
4839
5115
|
for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
|
|
4840
5116
|
try {
|
|
4841
5117
|
const u = new URL(m[1]);
|
|
@@ -4843,7 +5119,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
4843
5119
|
} catch {
|
|
4844
5120
|
}
|
|
4845
5121
|
}
|
|
4846
|
-
const readme = await
|
|
5122
|
+
const readme = await fs15.readFile(path15.join(cwd, "README.md"), "utf8").catch(() => "");
|
|
4847
5123
|
return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
|
|
4848
5124
|
}
|
|
4849
5125
|
|
|
@@ -4993,8 +5269,8 @@ async function runPreview(opts) {
|
|
|
4993
5269
|
}
|
|
4994
5270
|
|
|
4995
5271
|
// src/commands/generate.ts
|
|
4996
|
-
import
|
|
4997
|
-
import
|
|
5272
|
+
import fs16 from "fs/promises";
|
|
5273
|
+
import path16 from "path";
|
|
4998
5274
|
import { PNG as PNG4 } from "pngjs";
|
|
4999
5275
|
|
|
5000
5276
|
// src/lib/glass.ts
|
|
@@ -5152,7 +5428,7 @@ async function* readSSE(body) {
|
|
|
5152
5428
|
}
|
|
5153
5429
|
|
|
5154
5430
|
// src/commands/generate.ts
|
|
5155
|
-
var
|
|
5431
|
+
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5156
5432
|
var INLINE_IMAGE_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
5157
5433
|
var IMAGE_MIME_BY_EXT = {
|
|
5158
5434
|
".png": "image/png",
|
|
@@ -5164,7 +5440,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
5164
5440
|
async function inlineLocalImage(filePath, flag) {
|
|
5165
5441
|
let bytes;
|
|
5166
5442
|
try {
|
|
5167
|
-
bytes = await
|
|
5443
|
+
bytes = await fs16.readFile(filePath);
|
|
5168
5444
|
} catch {
|
|
5169
5445
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
5170
5446
|
}
|
|
@@ -5174,7 +5450,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
5174
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.`
|
|
5175
5451
|
};
|
|
5176
5452
|
}
|
|
5177
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
5453
|
+
const mime = IMAGE_MIME_BY_EXT[path16.extname(filePath).toLowerCase()] ?? "image/png";
|
|
5178
5454
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
5179
5455
|
}
|
|
5180
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.";
|
|
@@ -5370,7 +5646,7 @@ async function runGenerate(kind, opts) {
|
|
|
5370
5646
|
return;
|
|
5371
5647
|
}
|
|
5372
5648
|
try {
|
|
5373
|
-
const bytes = await
|
|
5649
|
+
const bytes = await fs16.readFile(opts.inpaintUrl);
|
|
5374
5650
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
5375
5651
|
} catch {
|
|
5376
5652
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -5494,7 +5770,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
5494
5770
|
return;
|
|
5495
5771
|
}
|
|
5496
5772
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
5497
|
-
await
|
|
5773
|
+
await fs16.mkdir(outDir, { recursive: true });
|
|
5498
5774
|
const solved = [];
|
|
5499
5775
|
for (let i = 0; i < files.length; i++) {
|
|
5500
5776
|
const f = files[i];
|
|
@@ -5526,8 +5802,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
5526
5802
|
});
|
|
5527
5803
|
continue;
|
|
5528
5804
|
}
|
|
5529
|
-
const outPath =
|
|
5530
|
-
await
|
|
5805
|
+
const outPath = path16.join(outDir, `glass-${i + 1}.png`);
|
|
5806
|
+
await fs16.writeFile(outPath, PNG4.sync.write(r.png));
|
|
5531
5807
|
solved.push({
|
|
5532
5808
|
path: outPath,
|
|
5533
5809
|
url: f.url,
|
|
@@ -5686,14 +5962,14 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
|
|
|
5686
5962
|
});
|
|
5687
5963
|
} catch {
|
|
5688
5964
|
if (++connectFailures >= 3) return "unsupported";
|
|
5689
|
-
await
|
|
5965
|
+
await sleep3(2e3);
|
|
5690
5966
|
continue;
|
|
5691
5967
|
}
|
|
5692
5968
|
if (res.status === 404 || res.status === 405) return "unsupported";
|
|
5693
5969
|
const contentType = res.headers.get("content-type") ?? "";
|
|
5694
5970
|
if (!res.ok || !res.body || !contentType.includes("text/event-stream")) {
|
|
5695
5971
|
if (++connectFailures >= 3) return "unsupported";
|
|
5696
|
-
await
|
|
5972
|
+
await sleep3(2e3);
|
|
5697
5973
|
continue;
|
|
5698
5974
|
}
|
|
5699
5975
|
connectFailures = 0;
|
|
@@ -5714,7 +5990,7 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
|
|
|
5714
5990
|
}
|
|
5715
5991
|
} catch {
|
|
5716
5992
|
}
|
|
5717
|
-
await
|
|
5993
|
+
await sleep3(1e3);
|
|
5718
5994
|
} finally {
|
|
5719
5995
|
clearTimeout(timer);
|
|
5720
5996
|
abort.abort();
|
|
@@ -5742,7 +6018,7 @@ async function poll(apiUrl, token, id, onProgress, timeoutMs) {
|
|
|
5742
6018
|
}
|
|
5743
6019
|
} catch {
|
|
5744
6020
|
}
|
|
5745
|
-
await
|
|
6021
|
+
await sleep3(3e3);
|
|
5746
6022
|
}
|
|
5747
6023
|
return null;
|
|
5748
6024
|
}
|
|
@@ -6121,8 +6397,8 @@ async function toRow(e, v, cwd) {
|
|
|
6121
6397
|
}
|
|
6122
6398
|
|
|
6123
6399
|
// src/commands/controller.ts
|
|
6124
|
-
import
|
|
6125
|
-
import
|
|
6400
|
+
import fs18 from "fs/promises";
|
|
6401
|
+
import path18 from "path";
|
|
6126
6402
|
|
|
6127
6403
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
6128
6404
|
import { createHash } from "crypto";
|
|
@@ -15252,9 +15528,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
15252
15528
|
}
|
|
15253
15529
|
|
|
15254
15530
|
// src/lib/anims.ts
|
|
15255
|
-
import
|
|
15256
|
-
import
|
|
15257
|
-
var ANIMS_DEST =
|
|
15531
|
+
import fs17 from "fs/promises";
|
|
15532
|
+
import path17 from "path";
|
|
15533
|
+
var ANIMS_DEST = path17.join("public", "assets", "anims");
|
|
15258
15534
|
var HIDDEN_TAG = "reference";
|
|
15259
15535
|
async function runAnims(opts) {
|
|
15260
15536
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -15270,7 +15546,7 @@ async function runAnims(opts) {
|
|
|
15270
15546
|
printCatalog(log, manifest, selectors);
|
|
15271
15547
|
return;
|
|
15272
15548
|
}
|
|
15273
|
-
const controllerMarker =
|
|
15549
|
+
const controllerMarker = path17.join(root, "src", "controllers", "character");
|
|
15274
15550
|
if (!await exists2(controllerMarker)) {
|
|
15275
15551
|
log.error(
|
|
15276
15552
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -15279,11 +15555,11 @@ async function runAnims(opts) {
|
|
|
15279
15555
|
process.exitCode = 1;
|
|
15280
15556
|
return;
|
|
15281
15557
|
}
|
|
15282
|
-
const destDir =
|
|
15283
|
-
const gameManifestPath =
|
|
15558
|
+
const destDir = path17.join(root, ANIMS_DEST);
|
|
15559
|
+
const gameManifestPath = path17.join(destDir, "manifest.json");
|
|
15284
15560
|
if (opts.reset) {
|
|
15285
|
-
await
|
|
15286
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
15561
|
+
await fs17.rm(destDir, { recursive: true, force: true });
|
|
15562
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path17.sep)} (--reset)`);
|
|
15287
15563
|
}
|
|
15288
15564
|
if (selectors.length === 0) {
|
|
15289
15565
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -15321,35 +15597,35 @@ async function runAnims(opts) {
|
|
|
15321
15597
|
}
|
|
15322
15598
|
}
|
|
15323
15599
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
15324
|
-
const cacheDir =
|
|
15600
|
+
const cacheDir = path17.join(
|
|
15325
15601
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
15326
15602
|
`${manifest.library}-v${manifest.version}`
|
|
15327
15603
|
);
|
|
15328
|
-
await
|
|
15329
|
-
await
|
|
15604
|
+
await fs17.mkdir(cacheDir, { recursive: true });
|
|
15605
|
+
await fs17.mkdir(destDir, { recursive: true });
|
|
15330
15606
|
const base = getAnimsBase(opts.animsBase);
|
|
15331
15607
|
let installedCount = 0;
|
|
15332
15608
|
let presentCount = 0;
|
|
15333
15609
|
let addedBytes = 0;
|
|
15334
15610
|
const failures = [];
|
|
15335
15611
|
for (const entry of wanted) {
|
|
15336
|
-
const dest =
|
|
15612
|
+
const dest = path17.join(destDir, entry.file);
|
|
15337
15613
|
if (await hasSize(dest, entry.bytes)) {
|
|
15338
15614
|
presentCount++;
|
|
15339
15615
|
continue;
|
|
15340
15616
|
}
|
|
15341
15617
|
try {
|
|
15342
|
-
const cached =
|
|
15618
|
+
const cached = path17.join(cacheDir, entry.file);
|
|
15343
15619
|
if (!await hasSize(cached, entry.bytes)) {
|
|
15344
15620
|
const res = await fetch(base + entry.file);
|
|
15345
15621
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
15346
15622
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
15347
|
-
await
|
|
15623
|
+
await fs17.writeFile(cached, buf);
|
|
15348
15624
|
}
|
|
15349
|
-
await
|
|
15625
|
+
await fs17.copyFile(cached, dest);
|
|
15350
15626
|
installedCount++;
|
|
15351
15627
|
addedBytes += entry.bytes;
|
|
15352
|
-
log.dim(` ${
|
|
15628
|
+
log.dim(` ${path17.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
15353
15629
|
} catch (err) {
|
|
15354
15630
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
15355
15631
|
}
|
|
@@ -15365,13 +15641,13 @@ async function runAnims(opts) {
|
|
|
15365
15641
|
version: manifest.version,
|
|
15366
15642
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
15367
15643
|
};
|
|
15368
|
-
await
|
|
15644
|
+
await fs17.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
15369
15645
|
log.plain("");
|
|
15370
15646
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
15371
15647
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
15372
15648
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
15373
15649
|
log.success(
|
|
15374
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
15650
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path17.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
15375
15651
|
);
|
|
15376
15652
|
for (const [selector, entries] of resolved) {
|
|
15377
15653
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -15403,8 +15679,8 @@ async function loadManifest(baseOverride) {
|
|
|
15403
15679
|
}
|
|
15404
15680
|
} catch {
|
|
15405
15681
|
}
|
|
15406
|
-
const snapshotPath =
|
|
15407
|
-
const manifest = JSON.parse(await
|
|
15682
|
+
const snapshotPath = path17.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
15683
|
+
const manifest = JSON.parse(await fs17.readFile(snapshotPath, "utf8"));
|
|
15408
15684
|
return { manifest, source: "snapshot" };
|
|
15409
15685
|
}
|
|
15410
15686
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -15522,21 +15798,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
15522
15798
|
}
|
|
15523
15799
|
async function readGameManifest(file) {
|
|
15524
15800
|
try {
|
|
15525
|
-
return JSON.parse(await
|
|
15801
|
+
return JSON.parse(await fs17.readFile(file, "utf8"));
|
|
15526
15802
|
} catch {
|
|
15527
15803
|
return null;
|
|
15528
15804
|
}
|
|
15529
15805
|
}
|
|
15530
15806
|
async function hasSize(file, bytes) {
|
|
15531
15807
|
try {
|
|
15532
|
-
return (await
|
|
15808
|
+
return (await fs17.stat(file)).size === bytes;
|
|
15533
15809
|
} catch {
|
|
15534
15810
|
return false;
|
|
15535
15811
|
}
|
|
15536
15812
|
}
|
|
15537
15813
|
async function exists2(p) {
|
|
15538
15814
|
try {
|
|
15539
|
-
await
|
|
15815
|
+
await fs17.access(p);
|
|
15540
15816
|
return true;
|
|
15541
15817
|
} catch {
|
|
15542
15818
|
return false;
|
|
@@ -15705,8 +15981,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
15705
15981
|
]
|
|
15706
15982
|
}
|
|
15707
15983
|
};
|
|
15708
|
-
var CODE_DEST =
|
|
15709
|
-
var ASSETS_DEST =
|
|
15984
|
+
var CODE_DEST = path18.join("src", "controllers");
|
|
15985
|
+
var ASSETS_DEST = path18.join("public", "assets");
|
|
15710
15986
|
async function runController(opts) {
|
|
15711
15987
|
const log = createLogger({ quiet: opts.quiet });
|
|
15712
15988
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -15723,31 +15999,31 @@ async function runController(opts) {
|
|
|
15723
15999
|
process.exitCode = 1;
|
|
15724
16000
|
return;
|
|
15725
16001
|
}
|
|
15726
|
-
const srcDir =
|
|
16002
|
+
const srcDir = path18.join(getTemplatesDir(), "controllers");
|
|
15727
16003
|
const root = opts.cwd ?? process.cwd();
|
|
15728
16004
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
15729
16005
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
15730
16006
|
log.plain("");
|
|
15731
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
16007
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path18.sep)}`);
|
|
15732
16008
|
const plan = [
|
|
15733
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
16009
|
+
...set.code.map((rel) => ({ from: rel, rel: path18.join(CODE_DEST, rel) })),
|
|
15734
16010
|
...set.assets.map((rel) => ({
|
|
15735
16011
|
from: rel,
|
|
15736
|
-
rel:
|
|
16012
|
+
rel: path18.join(ASSETS_DEST, path18.basename(rel))
|
|
15737
16013
|
}))
|
|
15738
16014
|
];
|
|
15739
16015
|
let copied = 0;
|
|
15740
16016
|
let skipped = 0;
|
|
15741
16017
|
try {
|
|
15742
16018
|
for (const file of plan) {
|
|
15743
|
-
const dest =
|
|
16019
|
+
const dest = path18.join(root, file.rel);
|
|
15744
16020
|
if (!opts.force && await exists3(dest)) {
|
|
15745
16021
|
skipped++;
|
|
15746
16022
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
15747
16023
|
continue;
|
|
15748
16024
|
}
|
|
15749
|
-
await
|
|
15750
|
-
await
|
|
16025
|
+
await fs18.mkdir(path18.dirname(dest), { recursive: true });
|
|
16026
|
+
await fs18.copyFile(path18.join(srcDir, file.from), dest);
|
|
15751
16027
|
copied++;
|
|
15752
16028
|
log.dim(` ${file.rel}`);
|
|
15753
16029
|
}
|
|
@@ -15800,7 +16076,7 @@ async function runController(opts) {
|
|
|
15800
16076
|
for (const line of set.sketch) {
|
|
15801
16077
|
log.dim(` ${line}`);
|
|
15802
16078
|
}
|
|
15803
|
-
if (kind === "character" && !await exists3(
|
|
16079
|
+
if (kind === "character" && !await exists3(path18.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
15804
16080
|
log.plain("");
|
|
15805
16081
|
log.plain(
|
|
15806
16082
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -15832,9 +16108,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
15832
16108
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
15833
16109
|
}
|
|
15834
16110
|
assertCompleteMeshyControllerPack(manifest);
|
|
15835
|
-
const destination =
|
|
15836
|
-
await
|
|
15837
|
-
await
|
|
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(
|
|
15838
16114
|
destination,
|
|
15839
16115
|
`${JSON.stringify(manifest, null, 2)}
|
|
15840
16116
|
`
|
|
@@ -15972,14 +16248,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
15972
16248
|
}
|
|
15973
16249
|
async function installFallbackAvatar(args) {
|
|
15974
16250
|
const { root, srcDir, log } = args;
|
|
15975
|
-
const dest =
|
|
15976
|
-
await
|
|
15977
|
-
await
|
|
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);
|
|
15978
16254
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
15979
16255
|
}
|
|
15980
16256
|
async function exists3(p) {
|
|
15981
16257
|
try {
|
|
15982
|
-
await
|
|
16258
|
+
await fs18.access(p);
|
|
15983
16259
|
return true;
|
|
15984
16260
|
} catch {
|
|
15985
16261
|
return false;
|
|
@@ -16473,22 +16749,22 @@ async function context2(opts) {
|
|
|
16473
16749
|
const project = await readProject();
|
|
16474
16750
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
16475
16751
|
}
|
|
16476
|
-
async function readVideo(
|
|
16752
|
+
async function readVideo(path20, log) {
|
|
16477
16753
|
let bytes;
|
|
16478
16754
|
try {
|
|
16479
|
-
bytes = await readFile(
|
|
16755
|
+
bytes = await readFile(path20);
|
|
16480
16756
|
} catch {
|
|
16481
|
-
log.error(`Can't read ${
|
|
16757
|
+
log.error(`Can't read ${path20}.`);
|
|
16482
16758
|
return null;
|
|
16483
16759
|
}
|
|
16484
16760
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
16485
|
-
log.error(`${basename(
|
|
16761
|
+
log.error(`${basename(path20)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
16486
16762
|
return null;
|
|
16487
16763
|
}
|
|
16488
16764
|
return bytes;
|
|
16489
16765
|
}
|
|
16490
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
16491
|
-
const contentType = /\.mov$/i.test(
|
|
16766
|
+
async function uploadVideo(apiUrl, token, characterId, path20, bytes, log) {
|
|
16767
|
+
const contentType = /\.mov$/i.test(path20) ? "video/quicktime" : "video/mp4";
|
|
16492
16768
|
const minted = await apiFetch(
|
|
16493
16769
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
16494
16770
|
{
|
|
@@ -16503,7 +16779,7 @@ async function uploadVideo(apiUrl, token, characterId, path19, bytes, log) {
|
|
|
16503
16779
|
return null;
|
|
16504
16780
|
}
|
|
16505
16781
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
16506
|
-
log.dim(` uploading ${basename(
|
|
16782
|
+
log.dim(` uploading ${basename(path20)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
16507
16783
|
const put = await fetch(uploadUrl, {
|
|
16508
16784
|
method: "PUT",
|
|
16509
16785
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -16819,8 +17095,8 @@ function rank(items, query) {
|
|
|
16819
17095
|
}
|
|
16820
17096
|
|
|
16821
17097
|
// src/commands/motion.ts
|
|
16822
|
-
import
|
|
16823
|
-
import
|
|
17098
|
+
import fs19 from "fs/promises";
|
|
17099
|
+
import path19 from "path";
|
|
16824
17100
|
|
|
16825
17101
|
// src/lib/motion/npz.ts
|
|
16826
17102
|
import zlib from "zlib";
|
|
@@ -18071,7 +18347,7 @@ async function motionGen(opts, log) {
|
|
|
18071
18347
|
}
|
|
18072
18348
|
if (opts.constraintsPath !== void 0) {
|
|
18073
18349
|
try {
|
|
18074
|
-
const raw = await
|
|
18350
|
+
const raw = await fs19.readFile(opts.constraintsPath, "utf8");
|
|
18075
18351
|
generationOptions.constraints = JSON.parse(raw);
|
|
18076
18352
|
} catch {
|
|
18077
18353
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -18095,10 +18371,10 @@ async function motionGen(opts, log) {
|
|
|
18095
18371
|
async function expandTakes(selectors) {
|
|
18096
18372
|
const out = [];
|
|
18097
18373
|
for (const sel of selectors) {
|
|
18098
|
-
const st = await
|
|
18374
|
+
const st = await fs19.stat(sel).catch(() => null);
|
|
18099
18375
|
if (st?.isDirectory()) {
|
|
18100
|
-
const names = await
|
|
18101
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
18376
|
+
const names = await fs19.readdir(sel);
|
|
18377
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path19.join(sel, n));
|
|
18102
18378
|
} else if (st?.isFile()) {
|
|
18103
18379
|
out.push(sel);
|
|
18104
18380
|
} else {
|
|
@@ -18133,7 +18409,7 @@ async function motionVerify(opts, log) {
|
|
|
18133
18409
|
let gates = DEFAULT_GATES;
|
|
18134
18410
|
if (opts.gatesPath) {
|
|
18135
18411
|
try {
|
|
18136
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
18412
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs19.readFile(opts.gatesPath, "utf8")));
|
|
18137
18413
|
} catch {
|
|
18138
18414
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
18139
18415
|
process.exitCode = 1;
|
|
@@ -18155,9 +18431,9 @@ async function motionVerify(opts, log) {
|
|
|
18155
18431
|
}
|
|
18156
18432
|
const reports = [];
|
|
18157
18433
|
for (const file of files) {
|
|
18158
|
-
const stem =
|
|
18434
|
+
const stem = path19.basename(file).replace(/\.npz$/, "");
|
|
18159
18435
|
try {
|
|
18160
|
-
reports.push(analyzeTake(stem, await
|
|
18436
|
+
reports.push(analyzeTake(stem, await fs19.readFile(file), gates));
|
|
18161
18437
|
} catch (err) {
|
|
18162
18438
|
reports.push({
|
|
18163
18439
|
take: stem,
|
|
@@ -18195,7 +18471,7 @@ async function motionCompile(opts, log) {
|
|
|
18195
18471
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
18196
18472
|
if (opts.configPath) {
|
|
18197
18473
|
try {
|
|
18198
|
-
const patch = JSON.parse(await
|
|
18474
|
+
const patch = JSON.parse(await fs19.readFile(opts.configPath, "utf8"));
|
|
18199
18475
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
18200
18476
|
} catch {
|
|
18201
18477
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -18213,16 +18489,16 @@ async function motionCompile(opts, log) {
|
|
|
18213
18489
|
}
|
|
18214
18490
|
const inputs = [];
|
|
18215
18491
|
for (const file of files) {
|
|
18216
|
-
const stem =
|
|
18492
|
+
const stem = path19.basename(file).replace(/\.npz$/, "");
|
|
18217
18493
|
try {
|
|
18218
|
-
inputs.push({ stem, take: loadTake(await
|
|
18494
|
+
inputs.push({ stem, take: loadTake(await fs19.readFile(file)) });
|
|
18219
18495
|
} catch (err) {
|
|
18220
18496
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
18221
18497
|
process.exitCode = 1;
|
|
18222
18498
|
return;
|
|
18223
18499
|
}
|
|
18224
18500
|
}
|
|
18225
|
-
const setName = opts.set ??
|
|
18501
|
+
const setName = opts.set ?? path19.basename(opts.out).replace(/\.json$/, "");
|
|
18226
18502
|
let result;
|
|
18227
18503
|
try {
|
|
18228
18504
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -18237,9 +18513,9 @@ async function motionCompile(opts, log) {
|
|
|
18237
18513
|
process.exitCode = 1;
|
|
18238
18514
|
return;
|
|
18239
18515
|
}
|
|
18240
|
-
await
|
|
18516
|
+
await fs19.mkdir(path19.dirname(path19.resolve(opts.out)), { recursive: true });
|
|
18241
18517
|
const json = JSON.stringify(result.data);
|
|
18242
|
-
await
|
|
18518
|
+
await fs19.writeFile(opts.out, json);
|
|
18243
18519
|
if (opts.json) {
|
|
18244
18520
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
18245
18521
|
return;
|
|
@@ -18257,9 +18533,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
18257
18533
|
var MOTION_PRESETS = {
|
|
18258
18534
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
18259
18535
|
};
|
|
18260
|
-
var MOTION_DEST =
|
|
18536
|
+
var MOTION_DEST = path19.join("src", "motion");
|
|
18261
18537
|
async function motionInstall(opts, log) {
|
|
18262
|
-
const srcDir =
|
|
18538
|
+
const srcDir = path19.join(getTemplatesDir(), "motion");
|
|
18263
18539
|
const root = opts.cwd ?? process.cwd();
|
|
18264
18540
|
const preset = opts.set;
|
|
18265
18541
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -18270,21 +18546,21 @@ async function motionInstall(opts, log) {
|
|
|
18270
18546
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
18271
18547
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
18272
18548
|
log.plain("");
|
|
18273
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
18549
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path19.sep)}`);
|
|
18274
18550
|
let copied = 0, skipped = 0;
|
|
18275
18551
|
try {
|
|
18276
18552
|
for (const rel of files) {
|
|
18277
|
-
const dest =
|
|
18278
|
-
const exists4 = await
|
|
18553
|
+
const dest = path19.join(root, MOTION_DEST, rel);
|
|
18554
|
+
const exists4 = await fs19.access(dest).then(() => true, () => false);
|
|
18279
18555
|
if (!opts.force && exists4) {
|
|
18280
18556
|
skipped++;
|
|
18281
|
-
log.dim(` skipped ${
|
|
18557
|
+
log.dim(` skipped ${path19.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
18282
18558
|
continue;
|
|
18283
18559
|
}
|
|
18284
|
-
await
|
|
18285
|
-
await
|
|
18560
|
+
await fs19.mkdir(path19.dirname(dest), { recursive: true });
|
|
18561
|
+
await fs19.copyFile(path19.join(srcDir, rel), dest);
|
|
18286
18562
|
copied++;
|
|
18287
|
-
log.dim(` ${
|
|
18563
|
+
log.dim(` ${path19.join(MOTION_DEST, rel)}`);
|
|
18288
18564
|
}
|
|
18289
18565
|
} catch (err) {
|
|
18290
18566
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -18325,7 +18601,7 @@ async function motionConstraints(opts, log) {
|
|
|
18325
18601
|
}
|
|
18326
18602
|
const doc = directionConstraint(dir, speed, duration);
|
|
18327
18603
|
const out = opts.out ?? "constraints.json";
|
|
18328
|
-
await
|
|
18604
|
+
await fs19.writeFile(out, JSON.stringify(doc));
|
|
18329
18605
|
if (opts.json) {
|
|
18330
18606
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
18331
18607
|
return;
|
|
@@ -18367,6 +18643,11 @@ var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, aut
|
|
|
18367
18643
|
|
|
18368
18644
|
${c.bold("Usage")}
|
|
18369
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.
|
|
18370
18651
|
genex link <slug> [options] Re-link THIS folder to an existing game of yours
|
|
18371
18652
|
(lost folder / new machine); preview/publish then
|
|
18372
18653
|
update the same live game. Never creates a project.
|
|
@@ -18512,7 +18793,16 @@ ${c.bold("Options for `init`")}
|
|
|
18512
18793
|
--api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
|
|
18513
18794
|
--no-auth Only scaffold templates; skip authorization.
|
|
18514
18795
|
--force Overwrite existing files (default: never overwrite).
|
|
18515
|
-
--timeout <seconds> How long to wait for
|
|
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).
|
|
18516
18806
|
|
|
18517
18807
|
${c.bold("Options for `link`")}
|
|
18518
18808
|
<slug> The game to reconnect to (the name in your play URL). Required.
|
|
@@ -18520,7 +18810,9 @@ ${c.bold("Options for `link`")}
|
|
|
18520
18810
|
--env <path> Token env file (default: ~/.genex/env).
|
|
18521
18811
|
--auth-url <url> Override the auth site (used only if sign-in is needed).
|
|
18522
18812
|
--api-url <url> Override the API base URL.
|
|
18523
|
-
--timeout <seconds> How long to wait for
|
|
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.
|
|
18524
18816
|
|
|
18525
18817
|
${c.bold("Options for `preview` / `publish`")}
|
|
18526
18818
|
--no-build Skip the build step; deploy whatever is already built.
|
|
@@ -19168,6 +19460,9 @@ async function main() {
|
|
|
19168
19460
|
case "init":
|
|
19169
19461
|
await runInit(parsed.options);
|
|
19170
19462
|
break;
|
|
19463
|
+
case "auth":
|
|
19464
|
+
await runAuth(parsed.options);
|
|
19465
|
+
break;
|
|
19171
19466
|
case "link":
|
|
19172
19467
|
await runLink(parsed.options);
|
|
19173
19468
|
break;
|
|
@@ -19267,10 +19562,31 @@ async function main() {
|
|
|
19267
19562
|
await flushSentry();
|
|
19268
19563
|
}
|
|
19269
19564
|
}
|
|
19270
|
-
|
|
19271
|
-
|
|
19565
|
+
function checkNodeVersion() {
|
|
19566
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
19567
|
+
if (!Number.isFinite(major) || major >= 20) return true;
|
|
19272
19568
|
const log = createLogger();
|
|
19273
|
-
log.error(
|
|
19274
|
-
|
|
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
|
+
);
|
|
19275
19579
|
process.exitCode = 1;
|
|
19276
|
-
|
|
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
|
+
}
|