@genex-ai/cli-demo 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,1015 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync } from "fs";
5
+ import path8 from "path";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
+
8
+ // src/commands/init.ts
9
+ import path6 from "path";
10
+
11
+ // src/config.ts
12
+ import os from "os";
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+ var DEFAULT_AUTH_URL = "https://demo-web.glotech.world";
16
+ var DEFAULT_API_URL = "https://demo-api.glotech.world";
17
+ var DEFAULT_COLYSEUS_URL = "wss://demo-colyseus.glotech.world";
18
+ var ENV_TOKEN_KEY = "GENEX_TOKEN";
19
+ var AUTH_URL_ENV = "GENEX_AUTH_URL";
20
+ var API_URL_ENV = "GENEX_API_URL";
21
+ var COLYSEUS_URL_ENV = "GENEX_COLYSEUS_URL";
22
+ function getAuthUrl(override) {
23
+ const raw = override || process.env[AUTH_URL_ENV] || DEFAULT_AUTH_URL;
24
+ return raw.replace(/\/+$/, "");
25
+ }
26
+ function getApiUrl(override) {
27
+ const raw = override || process.env[API_URL_ENV] || DEFAULT_API_URL;
28
+ return raw.replace(/\/+$/, "");
29
+ }
30
+ function getColyseusUrl(override) {
31
+ const raw = override || process.env[COLYSEUS_URL_ENV] || DEFAULT_COLYSEUS_URL;
32
+ return raw.replace(/\/+$/, "");
33
+ }
34
+ function getGenexDir() {
35
+ return path.join(os.homedir(), ".genex");
36
+ }
37
+ function getGenexEnvPath(override) {
38
+ if (override) return path.resolve(override);
39
+ return path.join(getGenexDir(), "env");
40
+ }
41
+ function getTemplatesDir() {
42
+ const here = path.dirname(fileURLToPath(import.meta.url));
43
+ return path.resolve(here, "..", "templates");
44
+ }
45
+ function getClaudeDir(override) {
46
+ if (override) return path.resolve(override);
47
+ return path.join(os.homedir(), ".claude");
48
+ }
49
+
50
+ // src/lib/copy-templates.ts
51
+ import fs from "fs/promises";
52
+ import path2 from "path";
53
+ async function copyTemplates(srcDir, destDir, opts = {}) {
54
+ const result = { copied: [], skipped: [] };
55
+ await walk(srcDir, srcDir, destDir, opts, result);
56
+ return result;
57
+ }
58
+ async function walk(rootSrc, src, dest, opts, result) {
59
+ const entries = await fs.readdir(src, { withFileTypes: true });
60
+ for (const entry of entries) {
61
+ const srcPath = path2.join(src, entry.name);
62
+ const destPath = path2.join(dest, entry.name);
63
+ const rel = path2.relative(rootSrc, srcPath);
64
+ if (entry.isDirectory()) {
65
+ await fs.mkdir(destPath, { recursive: true });
66
+ await walk(rootSrc, srcPath, destPath, opts, result);
67
+ continue;
68
+ }
69
+ if (!entry.isFile()) {
70
+ continue;
71
+ }
72
+ if (!opts.force && await exists(destPath)) {
73
+ result.skipped.push(rel);
74
+ continue;
75
+ }
76
+ await fs.mkdir(path2.dirname(destPath), { recursive: true });
77
+ await fs.copyFile(srcPath, destPath);
78
+ result.copied.push(rel);
79
+ }
80
+ }
81
+ async function exists(p) {
82
+ try {
83
+ await fs.access(p);
84
+ return true;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ // src/lib/auth.ts
91
+ import http from "http";
92
+ import crypto from "crypto";
93
+ import readline from "readline";
94
+ import { spawn } from "child_process";
95
+ import { URL } from "url";
96
+ var SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Genex</title>
97
+ <style>body{font-family:system-ui,sans-serif;background:#0b0b0f;color:#eaeaea;display:grid;place-items:center;height:100vh;margin:0}
98
+ .card{text-align:center;padding:2rem 3rem;border:1px solid #26262e;border-radius:14px;background:#13131a}
99
+ h1{font-size:1.3rem;margin:0 0 .5rem}p{color:#9a9aa6;margin:0}</style></head>
100
+ <body><div class="card"><h1>\u2713 Authorized</h1><p>You can close this tab and return to your terminal.</p></div></body></html>`;
101
+ var ERROR_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Genex</title></head>
102
+ <body style="font-family:system-ui,sans-serif"><h1>Authorization failed</h1>
103
+ <p>No token was provided. Please return to your terminal and try again.</p></body></html>`;
104
+ async function authorize(authBaseUrl, options) {
105
+ const {
106
+ log,
107
+ timeoutMs = 5 * 60 * 1e3,
108
+ open = openBrowser,
109
+ interactive = Boolean(process.stdin.isTTY)
110
+ } = options;
111
+ const state = crypto.randomBytes(16).toString("hex");
112
+ const server = http.createServer();
113
+ return await new Promise((resolve, reject) => {
114
+ let settled = false;
115
+ let timer;
116
+ let rl;
117
+ const cleanup = () => {
118
+ if (timer) clearTimeout(timer);
119
+ if (rl) rl.close();
120
+ server.close();
121
+ };
122
+ const succeed = (token) => {
123
+ if (settled) return;
124
+ settled = true;
125
+ cleanup();
126
+ resolve(token);
127
+ };
128
+ const fail = (err) => {
129
+ if (settled) return;
130
+ settled = true;
131
+ cleanup();
132
+ reject(err);
133
+ };
134
+ server.on("request", (req, res) => {
135
+ handleCallback(req, res, state, log).then((token) => {
136
+ if (token) succeed(token);
137
+ }).catch((err) => {
138
+ log.dim(`callback error: ${String(err)}`);
139
+ if (!res.writableEnded) {
140
+ res.writeHead(500);
141
+ res.end();
142
+ }
143
+ });
144
+ });
145
+ server.on("error", (err) => fail(err));
146
+ server.listen(0, "127.0.0.1", () => {
147
+ const { port } = server.address();
148
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
149
+ const authUrl = buildAuthUrl(authBaseUrl, redirectUri, state);
150
+ log.step("Opening your browser to authorize\u2026");
151
+ log.dim(authUrl);
152
+ let warned = false;
153
+ const warnManual = () => {
154
+ if (warned || settled) return;
155
+ warned = true;
156
+ log.warn(
157
+ "Couldn't open a browser automatically. Open the URL above manually to continue."
158
+ );
159
+ };
160
+ const opened = open(authUrl, warnManual);
161
+ if (!opened) warnManual();
162
+ if (interactive) {
163
+ rl = readline.createInterface({
164
+ input: process.stdin,
165
+ output: process.stdout
166
+ });
167
+ rl.question(
168
+ "\nWaiting for the browser redirect\u2026 or paste your token here and press Enter:\n> ",
169
+ (answer) => {
170
+ const token = answer.trim();
171
+ if (token) succeed(token);
172
+ }
173
+ );
174
+ }
175
+ timer = setTimeout(() => {
176
+ fail(
177
+ new Error(
178
+ "Timed out waiting for authorization. Re-run the command to try again."
179
+ )
180
+ );
181
+ }, timeoutMs);
182
+ });
183
+ });
184
+ }
185
+ function buildAuthUrl(authBaseUrl, redirectUri, state) {
186
+ const url = new URL(`${authBaseUrl}/cli/auth`);
187
+ url.searchParams.set("redirect_uri", redirectUri);
188
+ url.searchParams.set("state", state);
189
+ return url.toString();
190
+ }
191
+ async function handleCallback(req, res, expectedState, log) {
192
+ res.setHeader("Access-Control-Allow-Origin", "*");
193
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
194
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
195
+ const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
196
+ if (req.method === "OPTIONS") {
197
+ res.writeHead(204);
198
+ res.end();
199
+ return null;
200
+ }
201
+ if (reqUrl.pathname !== "/callback") {
202
+ res.writeHead(404);
203
+ res.end();
204
+ return null;
205
+ }
206
+ let token = null;
207
+ let state = null;
208
+ if (req.method === "GET") {
209
+ token = reqUrl.searchParams.get("token");
210
+ state = reqUrl.searchParams.get("state");
211
+ } else if (req.method === "POST") {
212
+ let parsed;
213
+ try {
214
+ parsed = await readBody(req);
215
+ } catch (err) {
216
+ log.dim(`ignoring malformed callback body: ${String(err)}`);
217
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
218
+ res.end(ERROR_HTML);
219
+ return null;
220
+ }
221
+ token = parsed.token;
222
+ state = parsed.state ?? reqUrl.searchParams.get("state");
223
+ } else {
224
+ res.writeHead(405);
225
+ res.end();
226
+ return null;
227
+ }
228
+ if (state !== expectedState) {
229
+ log.dim("ignoring callback with missing or mismatched state");
230
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
231
+ res.end(ERROR_HTML);
232
+ return null;
233
+ }
234
+ if (!token) {
235
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
236
+ res.end(ERROR_HTML);
237
+ return null;
238
+ }
239
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
240
+ res.end(SUCCESS_HTML);
241
+ return token;
242
+ }
243
+ async function readBody(req) {
244
+ const chunks = [];
245
+ let total = 0;
246
+ const MAX = 1024 * 1024;
247
+ for await (const chunk of req) {
248
+ const buf = chunk;
249
+ total += buf.length;
250
+ if (total > MAX) {
251
+ throw new Error("request body too large");
252
+ }
253
+ chunks.push(buf);
254
+ }
255
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
256
+ if (!raw) return { token: null, state: null };
257
+ const contentType = req.headers["content-type"] ?? "";
258
+ if (contentType.includes("application/json")) {
259
+ try {
260
+ const obj = JSON.parse(raw);
261
+ return {
262
+ token: typeof obj.token === "string" ? obj.token : null,
263
+ state: typeof obj.state === "string" ? obj.state : null
264
+ };
265
+ } catch {
266
+ return { token: null, state: null };
267
+ }
268
+ }
269
+ const params = new URLSearchParams(raw);
270
+ return {
271
+ token: params.get("token"),
272
+ state: params.get("state")
273
+ };
274
+ }
275
+ function openBrowser(url, onError = () => {
276
+ }) {
277
+ let command;
278
+ let args;
279
+ const custom = (process.env.GENEX_BROWSER || process.env.BROWSER)?.trim();
280
+ const customParts = custom ? tokenizeCommand(custom) : [];
281
+ if (customParts.length > 0) {
282
+ command = customParts[0];
283
+ args = [...customParts.slice(1), url];
284
+ } else {
285
+ switch (process.platform) {
286
+ case "darwin":
287
+ command = "open";
288
+ args = [url];
289
+ break;
290
+ case "win32":
291
+ command = "cmd";
292
+ args = ["/c", "start", "", url];
293
+ break;
294
+ default:
295
+ command = "xdg-open";
296
+ args = [url];
297
+ break;
298
+ }
299
+ }
300
+ try {
301
+ const child = spawn(command, args, {
302
+ stdio: "ignore",
303
+ detached: true
304
+ });
305
+ child.on("error", () => onError());
306
+ child.unref();
307
+ return true;
308
+ } catch {
309
+ return false;
310
+ }
311
+ }
312
+ function tokenizeCommand(input) {
313
+ const tokens = [];
314
+ let current = "";
315
+ let quote = null;
316
+ let inToken = false;
317
+ for (const ch of input) {
318
+ if (quote) {
319
+ if (ch === quote) quote = null;
320
+ else current += ch;
321
+ } else if (ch === '"' || ch === "'") {
322
+ quote = ch;
323
+ inToken = true;
324
+ } else if (/\s/.test(ch)) {
325
+ if (inToken) {
326
+ tokens.push(current);
327
+ current = "";
328
+ inToken = false;
329
+ }
330
+ } else {
331
+ current += ch;
332
+ inToken = true;
333
+ }
334
+ }
335
+ if (inToken) tokens.push(current);
336
+ return tokens;
337
+ }
338
+
339
+ // src/lib/project.ts
340
+ import crypto2 from "crypto";
341
+
342
+ // src/utils/colors.ts
343
+ var useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
344
+ var ESC = String.fromCharCode(27);
345
+ var code = (open, close) => (s) => useColor ? `${ESC}[${open}m${s}${ESC}[${close}m` : s;
346
+ var c = {
347
+ bold: code(1, 22),
348
+ dim: code(2, 22),
349
+ red: code(31, 39),
350
+ green: code(32, 39),
351
+ yellow: code(33, 39),
352
+ blue: code(34, 39),
353
+ cyan: code(36, 39),
354
+ gray: code(90, 39)
355
+ };
356
+
357
+ // src/lib/project.ts
358
+ async function createDraftProject(opts) {
359
+ const { apiUrl, token, deployKey, colyseusUrl, dashboardUrl, log } = opts;
360
+ log.step("Creating your project\u2026");
361
+ const names = [opts.name, `${opts.name}-${randomSuffix()}`];
362
+ for (let i = 0; i < names.length; i++) {
363
+ const name = names[i];
364
+ let res;
365
+ try {
366
+ res = await fetch(`${apiUrl}/api/projects`, {
367
+ method: "POST",
368
+ headers: {
369
+ "Content-Type": "application/json",
370
+ Authorization: `Bearer ${token}`
371
+ },
372
+ body: JSON.stringify({ name, deployKey })
373
+ });
374
+ } catch (err) {
375
+ log.warn(`Couldn't reach the API at ${apiUrl} to create the project.`);
376
+ log.dim(` ${String(err)}`);
377
+ return null;
378
+ }
379
+ if (res.status === 409 && i === 0) continue;
380
+ if (res.status === 401) {
381
+ log.warn("Not authorized to create the project (token rejected).");
382
+ return null;
383
+ }
384
+ if (res.status === 400) {
385
+ log.warn("The API rejected the deploy key (must be an OpenSSH public key).");
386
+ return null;
387
+ }
388
+ if (!res.ok) {
389
+ log.warn(`Couldn't create the project (HTTP ${res.status}).`);
390
+ return null;
391
+ }
392
+ const data = await res.json().catch(() => null);
393
+ const project = data?.project;
394
+ if (!project || !data?.sshUrl) {
395
+ log.warn("Project created, but the API response was unexpected.");
396
+ return null;
397
+ }
398
+ log.success(`Created project ${c.cyan(project.slug)}.`);
399
+ if (project.playUrl) log.dim(` play (after publish): ${project.playUrl}`);
400
+ log.dim(` dashboard: ${dashboardUrl}/dashboard`);
401
+ return {
402
+ id: project.id,
403
+ slug: project.slug,
404
+ sshUrl: data.sshUrl,
405
+ apiUrl,
406
+ colyseusUrl,
407
+ playUrl: project.playUrl ?? void 0,
408
+ status: "draft"
409
+ };
410
+ }
411
+ log.warn("Couldn't create a project with a unique name. Try `--name <unique>`.");
412
+ return null;
413
+ }
414
+ function randomSuffix() {
415
+ return crypto2.randomBytes(3).toString("hex");
416
+ }
417
+
418
+ // src/lib/ssh.ts
419
+ import fs2 from "fs/promises";
420
+ import path3 from "path";
421
+ import { spawn as spawn2 } from "child_process";
422
+ var KEY_NAME = "genex_key";
423
+ async function generateSshKeypair(dir, log) {
424
+ const keyPath = path3.join(dir, KEY_NAME);
425
+ const pubPath = `${keyPath}.pub`;
426
+ try {
427
+ const existing = (await fs2.readFile(pubPath, "utf8")).trim();
428
+ if (existing) {
429
+ log.dim(`Reusing existing deploy key (${KEY_NAME}).`);
430
+ return { publicKey: existing };
431
+ }
432
+ } catch {
433
+ }
434
+ log.step("Generating a deploy key\u2026");
435
+ const ok = await runSshKeygen(keyPath, log);
436
+ if (!ok) return null;
437
+ try {
438
+ const pub = (await fs2.readFile(pubPath, "utf8")).trim();
439
+ if (!pub) {
440
+ log.warn("ssh-keygen produced no public key.");
441
+ return null;
442
+ }
443
+ await fs2.chmod(keyPath, 384).catch(() => {
444
+ });
445
+ return { publicKey: pub };
446
+ } catch (err) {
447
+ log.warn(`Couldn't read the generated public key: ${String(err)}`);
448
+ return null;
449
+ }
450
+ }
451
+ function runSshKeygen(keyPath, log) {
452
+ return new Promise((resolve) => {
453
+ let child;
454
+ try {
455
+ child = spawn2(
456
+ "ssh-keygen",
457
+ ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "genex-agent"],
458
+ { stdio: "ignore" }
459
+ );
460
+ } catch {
461
+ log.warn("ssh-keygen not found \u2014 install OpenSSH (ssh-keygen) and re-run.");
462
+ resolve(false);
463
+ return;
464
+ }
465
+ child.on("error", () => {
466
+ log.warn("ssh-keygen not found \u2014 install OpenSSH (ssh-keygen) and re-run.");
467
+ resolve(false);
468
+ });
469
+ child.on("close", (code2) => resolve(code2 === 0));
470
+ });
471
+ }
472
+ async function writeGitignore(dir, log) {
473
+ const file = path3.join(dir, ".gitignore");
474
+ let content = "";
475
+ try {
476
+ content = await fs2.readFile(file, "utf8");
477
+ } catch {
478
+ }
479
+ const present = new Set(content.split("\n").map((l) => l.trim()));
480
+ const toAdd = [KEY_NAME, `${KEY_NAME}.pub`, ".genex/"].filter((e) => !present.has(e));
481
+ if (toAdd.length === 0) return;
482
+ let next = content;
483
+ if (next.length > 0 && !next.endsWith("\n")) next += "\n";
484
+ if (!content.trim()) next += "# genex (deploy key + local metadata \u2014 never publish)\n";
485
+ next += toAdd.join("\n") + "\n";
486
+ await fs2.writeFile(file, next);
487
+ log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
488
+ }
489
+
490
+ // src/lib/store.ts
491
+ import fs4 from "fs/promises";
492
+ import path5 from "path";
493
+
494
+ // src/lib/env.ts
495
+ import fs3 from "fs/promises";
496
+ import path4 from "path";
497
+ import { spawn as spawn3 } from "child_process";
498
+ async function writeEnvVar(envPath, key, value) {
499
+ let content = "";
500
+ let existed = false;
501
+ try {
502
+ content = await fs3.readFile(envPath, "utf8");
503
+ existed = true;
504
+ } catch {
505
+ }
506
+ const assignment = `${key}=${formatValue(value)}`;
507
+ const keyPattern = new RegExp(
508
+ `^(\\s*export\\s+)?${escapeRegExp(key)}=.*$`,
509
+ "gm"
510
+ );
511
+ let next;
512
+ let mode;
513
+ if (keyPattern.test(content)) {
514
+ next = content.replace(keyPattern, assignment);
515
+ mode = "updated";
516
+ } else {
517
+ let prefix = content;
518
+ if (prefix.length > 0 && !prefix.endsWith("\n")) prefix += "\n";
519
+ next = prefix + assignment + "\n";
520
+ mode = existed ? "appended" : "created";
521
+ }
522
+ await fs3.mkdir(path4.dirname(envPath), { recursive: true });
523
+ await fs3.writeFile(envPath, next, { mode: 384 });
524
+ await restrictFilePermissions(envPath);
525
+ return { mode, path: envPath };
526
+ }
527
+ async function restrictFilePermissions(filePath) {
528
+ if (process.platform !== "win32") {
529
+ await fs3.chmod(filePath, 384).catch(() => {
530
+ });
531
+ return;
532
+ }
533
+ const user = process.env.USERNAME ?? process.env.USER;
534
+ if (!user) return;
535
+ await new Promise((resolve) => {
536
+ try {
537
+ const child = spawn3(
538
+ "icacls",
539
+ [filePath, "/inheritance:r", "/grant:r", `${user}:F`],
540
+ { stdio: "ignore" }
541
+ );
542
+ child.on("error", () => resolve());
543
+ child.on("close", () => resolve());
544
+ } catch {
545
+ resolve();
546
+ }
547
+ });
548
+ }
549
+ function formatValue(value) {
550
+ if (/[\s#"'$`\\]/.test(value)) {
551
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
552
+ }
553
+ return value;
554
+ }
555
+ function escapeRegExp(s) {
556
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
557
+ }
558
+
559
+ // src/lib/store.ts
560
+ function getProjectMetadataPath(cwd = process.cwd()) {
561
+ return path5.join(cwd, ".genex", "project.json");
562
+ }
563
+ async function writeUserToken(token, envPath) {
564
+ const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
565
+ return { path: written };
566
+ }
567
+ async function readUserToken(envPath) {
568
+ const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
569
+ if (fromGenex) return fromGenex;
570
+ if (!envPath) {
571
+ return readTokenFromFile(path5.join(process.cwd(), ".env"));
572
+ }
573
+ return null;
574
+ }
575
+ async function readTokenFromFile(file) {
576
+ let content;
577
+ try {
578
+ content = await fs4.readFile(file, "utf8");
579
+ } catch {
580
+ return null;
581
+ }
582
+ const m = content.match(/^\s*(?:export\s+)?GENEX_TOKEN=(.*)$/m);
583
+ if (!m) return null;
584
+ return stripQuotes(m[1].trim()) || null;
585
+ }
586
+ function stripQuotes(v) {
587
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
588
+ return v.slice(1, -1);
589
+ }
590
+ return v;
591
+ }
592
+ async function readProject(cwd = process.cwd()) {
593
+ try {
594
+ const raw = await fs4.readFile(getProjectMetadataPath(cwd), "utf8");
595
+ return JSON.parse(raw);
596
+ } catch {
597
+ return null;
598
+ }
599
+ }
600
+ async function writeProject(meta, cwd = process.cwd()) {
601
+ const file = getProjectMetadataPath(cwd);
602
+ await fs4.mkdir(path5.dirname(file), { recursive: true });
603
+ await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
604
+ await fs4.chmod(file, 384).catch(() => {
605
+ });
606
+ return { path: file };
607
+ }
608
+
609
+ // src/utils/logger.ts
610
+ function createLogger(opts = {}) {
611
+ const out = (s) => {
612
+ if (!opts.quiet) process.stdout.write(s + "\n");
613
+ };
614
+ const err = (s) => {
615
+ process.stderr.write(s + "\n");
616
+ };
617
+ return {
618
+ info: (m) => out(`${c.cyan("i")} ${m}`),
619
+ success: (m) => out(`${c.green("\u2713")} ${m}`),
620
+ warn: (m) => out(`${c.yellow("!")} ${m}`),
621
+ error: (m) => err(`${c.red("\u2717")} ${m}`),
622
+ step: (m) => out(`${c.blue("\u203A")} ${m}`),
623
+ dim: (m) => out(c.dim(m)),
624
+ plain: (m) => out(m)
625
+ };
626
+ }
627
+
628
+ // src/commands/init.ts
629
+ async function runInit(opts) {
630
+ const log = createLogger({ quiet: opts.quiet });
631
+ log.plain(c.bold("genex init"));
632
+ log.plain("");
633
+ const templatesDir = getTemplatesDir();
634
+ const claudeDir = getClaudeDir(opts.dir);
635
+ log.step(`Setting up your workspace at ${c.cyan(claudeDir)}`);
636
+ const { copied, skipped } = await copyTemplates(templatesDir, claudeDir, {
637
+ force: opts.force
638
+ });
639
+ if (copied.length > 0) {
640
+ log.success(`Added ${copied.length} file${copied.length === 1 ? "" : "s"}.`);
641
+ for (const f of copied) log.dim(` + ${f}`);
642
+ }
643
+ if (skipped.length > 0) {
644
+ log.info(
645
+ `Left ${skipped.length} existing file${skipped.length === 1 ? "" : "s"} untouched.`
646
+ );
647
+ for (const f of skipped) log.dim(` = ${f}`);
648
+ }
649
+ if (copied.length === 0 && skipped.length === 0) {
650
+ log.info("No template files to install.");
651
+ }
652
+ log.plain("");
653
+ if (opts.noAuth) {
654
+ log.info("Skipping authorization (--no-auth).");
655
+ log.success("Done.");
656
+ return;
657
+ }
658
+ const authBaseUrl = getAuthUrl(opts.authUrl);
659
+ let token;
660
+ try {
661
+ token = await authorize(authBaseUrl, {
662
+ log,
663
+ timeoutMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
664
+ });
665
+ } catch (err) {
666
+ log.error(err instanceof Error ? err.message : String(err));
667
+ log.dim("Your workspace files were installed. Re-run `genex init` to finish authorizing.");
668
+ throw err;
669
+ }
670
+ log.success("Authorized.");
671
+ const { path: tokenPath } = await writeUserToken(token, opts.envPath);
672
+ log.success(`Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
673
+ log.plain("");
674
+ const key = await generateSshKeypair(process.cwd(), log);
675
+ await writeGitignore(process.cwd(), log);
676
+ if (!key) {
677
+ log.warn(
678
+ "Skipping project creation \u2014 no deploy key. Install ssh-keygen (OpenSSH) and re-run `genex init`."
679
+ );
680
+ log.plain("");
681
+ log.success("Workspace ready (no project created yet).");
682
+ return;
683
+ }
684
+ const apiUrl = getApiUrl(opts.apiUrl);
685
+ const colyseusUrl = getColyseusUrl(opts.colyseusUrl);
686
+ const projectName = opts.name?.trim() || path6.basename(process.cwd());
687
+ const meta = await createDraftProject({
688
+ apiUrl,
689
+ token,
690
+ name: projectName,
691
+ deployKey: key.publicKey,
692
+ colyseusUrl,
693
+ dashboardUrl: authBaseUrl,
694
+ log
695
+ });
696
+ if (meta) {
697
+ const { path: metaPath } = await writeProject(meta);
698
+ log.dim(` saved ${c.cyan(metaPath)}`);
699
+ }
700
+ log.plain("");
701
+ log.success("All set. \u{1F680}");
702
+ }
703
+
704
+ // src/commands/publish.ts
705
+ import { spawn as spawn4 } from "child_process";
706
+ import fs5 from "fs/promises";
707
+ import path7 from "path";
708
+ function run(cmd, args, env) {
709
+ return new Promise((resolve) => {
710
+ let child;
711
+ try {
712
+ child = spawn4(cmd, args, { env: env ? { ...process.env, ...env } : process.env });
713
+ } catch {
714
+ resolve({ code: -1, out: "", err: `${cmd} not found` });
715
+ return;
716
+ }
717
+ let out = "";
718
+ let err = "";
719
+ child.stdout?.on("data", (d) => out += String(d));
720
+ child.stderr?.on("data", (d) => err += String(d));
721
+ child.on("error", () => resolve({ code: -1, out, err: `${cmd} not found` }));
722
+ child.on("close", (code2) => resolve({ code: code2 ?? -1, out, err }));
723
+ });
724
+ }
725
+ async function runPublish(opts) {
726
+ const log = createLogger({ quiet: opts.quiet });
727
+ log.plain(c.bold("genex publish"));
728
+ log.plain("");
729
+ const token = opts.token ?? await readUserToken(opts.envPath);
730
+ if (!token) {
731
+ log.error("Not authorized. Run `genex init` first to sign in.");
732
+ process.exitCode = 1;
733
+ return;
734
+ }
735
+ const meta = await readProject();
736
+ if (!meta) {
737
+ log.error("No genex project here. Run `genex init` in this directory first.");
738
+ process.exitCode = 1;
739
+ return;
740
+ }
741
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
742
+ let pushed = null;
743
+ if (opts.noPush) {
744
+ log.info("Skipping git push (--no-push).");
745
+ } else {
746
+ pushed = await pushGame(meta.sshUrl, log);
747
+ }
748
+ log.step("Publishing to the gallery\u2026");
749
+ let res;
750
+ try {
751
+ const body = {};
752
+ if (opts.title) body.title = opts.title;
753
+ if (opts.description) body.description = opts.description;
754
+ res = await fetch(`${apiUrl}/api/projects/${meta.id}/publish`, {
755
+ method: "POST",
756
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
757
+ body: JSON.stringify(body)
758
+ });
759
+ } catch (err) {
760
+ log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
761
+ process.exitCode = 1;
762
+ return;
763
+ }
764
+ if (!res.ok) {
765
+ log.error(`Publish failed (HTTP ${res.status}).`);
766
+ process.exitCode = 1;
767
+ return;
768
+ }
769
+ log.plain("");
770
+ if (pushed === false) {
771
+ log.warn(
772
+ "Listed in the gallery, but the game wasn't pushed \u2014 the play URL won't work until the push succeeds."
773
+ );
774
+ log.dim(" Fix the push error above and re-run `genex publish`.");
775
+ } else {
776
+ log.success("Published. \u{1F389}");
777
+ }
778
+ if (meta.playUrl) {
779
+ log.dim(` play: ${meta.playUrl}`);
780
+ if (pushed === true) log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
781
+ }
782
+ }
783
+ async function pushGame(sshUrl, log) {
784
+ try {
785
+ await fs5.access(path7.join(process.cwd(), "index.html"));
786
+ } catch {
787
+ log.warn("No index.html at the project root \u2014 GitHub Pages needs one to serve the game.");
788
+ }
789
+ const isRepo = (await run("git", ["rev-parse", "--git-dir"])).code === 0;
790
+ if (!isRepo) {
791
+ log.step("Initializing git\u2026");
792
+ if ((await run("git", ["init"])).code !== 0) {
793
+ log.warn("git init failed \u2014 the game was not pushed (use `genex publish --no-push` to silence).");
794
+ return false;
795
+ }
796
+ }
797
+ await run("git", ["add", "-A"]);
798
+ const commit = await run("git", ["commit", "-m", "build"]);
799
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.out + commit.err)) {
800
+ log.dim(" (no new commit to push)");
801
+ }
802
+ log.step("Pushing your game over SSH\u2026");
803
+ const push = await run("git", ["push", sshUrl, "+HEAD:main"], {
804
+ GIT_SSH_COMMAND: `ssh -i ./${KEY_NAME} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
805
+ });
806
+ if (push.code === 0) {
807
+ log.success("Pushed to main.");
808
+ return true;
809
+ }
810
+ log.warn("git push failed \u2014 your live game was NOT updated.");
811
+ const tail = push.err.trim().split("\n").slice(-2).join(" ");
812
+ if (tail) log.dim(` ${tail}`);
813
+ return false;
814
+ }
815
+
816
+ // src/index.ts
817
+ function getVersion() {
818
+ try {
819
+ const here = path8.dirname(fileURLToPath2(import.meta.url));
820
+ const pkg = JSON.parse(
821
+ readFileSync(path8.resolve(here, "..", "package.json"), "utf8")
822
+ );
823
+ return pkg.version ?? "0.0.0";
824
+ } catch {
825
+ return "0.0.0";
826
+ }
827
+ }
828
+ var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize, and publish 3D games.
829
+
830
+ ${c.bold("Usage")}
831
+ genex init [<name>] [options] Scaffold + authorize + create the draft project.
832
+ genex publish [options] Push the built game and list it in the gallery.
833
+
834
+ ${c.bold("Options for `init`")}
835
+ <name> Project name (positional; default: current directory name).
836
+ --name <name> Same as the positional name.
837
+ --dir <path> Destination workspace (default: ~/.claude).
838
+ --env <path> Token env file (default: ~/.genex/env).
839
+ --auth-url <url> Override the auth site (default: ${DEFAULT_AUTH_URL}).
840
+ --api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
841
+ --colyseus-url <url> Override the multiplayer URL (stored in project metadata).
842
+ --no-auth Only scaffold templates; skip authorization.
843
+ --force Overwrite existing files (default: never overwrite).
844
+ --timeout <seconds> How long to wait for the auth redirect (default: 300).
845
+
846
+ ${c.bold("Options for `publish`")}
847
+ --no-push Skip the git push; only flip the gallery flag.
848
+ --title <title> Gallery title.
849
+ --description <text> Gallery description.
850
+ --api-url <url> Override the API base URL.
851
+ --env <path> Token env file (default: ~/.genex/env).
852
+
853
+ ${c.bold("Global")}
854
+ --quiet Reduce output.
855
+ -h, --help Show this help.
856
+ -v, --version Show the version.
857
+
858
+ ${c.bold("Environment")}
859
+ GENEX_AUTH_URL Overrides the default auth site URL.
860
+ GENEX_API_URL Overrides the default API base URL.
861
+ GENEX_COLYSEUS_URL Overrides the default multiplayer URL.
862
+ GENEX_BROWSER Command used to open the browser (falls back to BROWSER).
863
+
864
+ ${c.bold("Examples")}
865
+ genex init my-game
866
+ genex init my-game --api-url http://localhost:3000 --auth-url http://localhost:5173
867
+ genex publish
868
+ genex publish --no-push --title "My Game"
869
+ `;
870
+ function parseArgs(argv) {
871
+ const parsed = {
872
+ command: void 0,
873
+ options: {},
874
+ help: false,
875
+ version: false
876
+ };
877
+ const needsValue = /* @__PURE__ */ new Set([
878
+ "--dir",
879
+ "--env",
880
+ "--auth-url",
881
+ "--api-url",
882
+ "--colyseus-url",
883
+ "--name",
884
+ "--title",
885
+ "--description",
886
+ "--timeout"
887
+ ]);
888
+ let i = 0;
889
+ while (i < argv.length) {
890
+ const arg = argv[i++];
891
+ if (arg === void 0) break;
892
+ switch (arg) {
893
+ case "-h":
894
+ case "--help":
895
+ parsed.help = true;
896
+ break;
897
+ case "-v":
898
+ case "--version":
899
+ parsed.version = true;
900
+ break;
901
+ case "--no-auth":
902
+ parsed.options.noAuth = true;
903
+ break;
904
+ case "--no-push":
905
+ parsed.options.noPush = true;
906
+ break;
907
+ case "--force":
908
+ parsed.options.force = true;
909
+ break;
910
+ case "--quiet":
911
+ parsed.options.quiet = true;
912
+ break;
913
+ default: {
914
+ if (needsValue.has(arg)) {
915
+ const value = argv[i++];
916
+ if (value === void 0) {
917
+ parsed.error = `Missing value for ${arg}`;
918
+ return parsed;
919
+ }
920
+ applyValueFlag(parsed.options, arg, value);
921
+ } else if (arg.startsWith("-")) {
922
+ parsed.error = `Unknown option: ${arg}`;
923
+ return parsed;
924
+ } else if (parsed.command === void 0) {
925
+ parsed.command = arg;
926
+ } else if (parsed.options.name === void 0) {
927
+ parsed.options.name = arg;
928
+ } else {
929
+ parsed.error = `Unexpected argument: ${arg}`;
930
+ return parsed;
931
+ }
932
+ break;
933
+ }
934
+ }
935
+ }
936
+ return parsed;
937
+ }
938
+ function applyValueFlag(options, flag, value) {
939
+ switch (flag) {
940
+ case "--dir":
941
+ options.dir = value;
942
+ break;
943
+ case "--env":
944
+ options.envPath = value;
945
+ break;
946
+ case "--auth-url":
947
+ options.authUrl = value;
948
+ break;
949
+ case "--api-url":
950
+ options.apiUrl = value;
951
+ break;
952
+ case "--colyseus-url":
953
+ options.colyseusUrl = value;
954
+ break;
955
+ case "--name":
956
+ options.name = value;
957
+ break;
958
+ case "--title":
959
+ options.title = value;
960
+ break;
961
+ case "--description":
962
+ options.description = value;
963
+ break;
964
+ case "--timeout": {
965
+ const n = Number(value);
966
+ if (!Number.isFinite(n) || n <= 0) {
967
+ throw new Error(`Invalid --timeout value: ${value}`);
968
+ }
969
+ options.timeoutSec = n;
970
+ break;
971
+ }
972
+ }
973
+ }
974
+ async function main() {
975
+ const log = createLogger();
976
+ let parsed;
977
+ try {
978
+ parsed = parseArgs(process.argv.slice(2));
979
+ } catch (err) {
980
+ log.error(err instanceof Error ? err.message : String(err));
981
+ process.exitCode = 1;
982
+ return;
983
+ }
984
+ if (parsed.error) {
985
+ log.error(parsed.error);
986
+ log.plain(`Run ${c.cyan("genex --help")} for usage.`);
987
+ process.exitCode = 1;
988
+ return;
989
+ }
990
+ if (parsed.version) {
991
+ log.plain(getVersion());
992
+ return;
993
+ }
994
+ if (parsed.help || parsed.command === void 0) {
995
+ log.plain(HELP);
996
+ return;
997
+ }
998
+ switch (parsed.command) {
999
+ case "init":
1000
+ await runInit(parsed.options);
1001
+ break;
1002
+ case "publish":
1003
+ await runPublish(parsed.options);
1004
+ break;
1005
+ default:
1006
+ log.error(`Unknown command: ${parsed.command}`);
1007
+ log.plain(`Run ${c.cyan("genex --help")} for usage.`);
1008
+ process.exitCode = 1;
1009
+ }
1010
+ }
1011
+ main().catch((err) => {
1012
+ const log = createLogger();
1013
+ log.error(err instanceof Error ? err.message : String(err));
1014
+ process.exitCode = 1;
1015
+ });