@genex-ai/cli-demo 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/index.js +332 -140
- package/package.json +1 -1
- package/templates/README.md +3 -3
- package/templates/skills/genex-getting-started/SKILL.md +10 -3
- package/templates/skills/genex-updates/SKILL.md +66 -0
package/README.md
CHANGED
|
@@ -28,8 +28,10 @@ genex controller <type> # install a tuned character|car|drone controller →
|
|
|
28
28
|
agents/ + commands/), Codex (`~/.codex/skills`), and Cursor
|
|
29
29
|
(`~/.cursor/skills`). genex-owned skills (`genex-*`) are **always refreshed**
|
|
30
30
|
to the package version so a stale copy can't linger; your own files are never
|
|
31
|
-
overwritten.
|
|
32
|
-
|
|
31
|
+
overwritten. After init, **every** `genex` command re-syncs them to the
|
|
32
|
+
installed CLI version automatically (printing `🔄 Genex skills updated to
|
|
33
|
+
X.Y.Z` when it does). Pick agents explicitly with `--agents
|
|
34
|
+
claude,codex,cursor`, or a single custom dir with `--dir`.
|
|
33
35
|
2. **Authorizes you** — opens the Genex auth site (web) in your browser. If the
|
|
34
36
|
browser can't open, it prints the URL to open manually.
|
|
35
37
|
3. **Saves your token** — writes `GENEX_TOKEN` to `~/.genex/env` (per-user;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/index.ts
|
|
4
|
-
import { readFileSync } from "fs";
|
|
5
|
-
import path12 from "path";
|
|
6
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
-
|
|
8
3
|
// src/commands/init.ts
|
|
9
|
-
import
|
|
4
|
+
import path8 from "path";
|
|
10
5
|
|
|
11
6
|
// src/config.ts
|
|
12
7
|
import fs from "fs";
|
|
@@ -43,6 +38,17 @@ function getTemplatesDir() {
|
|
|
43
38
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
44
39
|
return path.resolve(here, "..", "templates");
|
|
45
40
|
}
|
|
41
|
+
function getCliVersion() {
|
|
42
|
+
try {
|
|
43
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const pkg = JSON.parse(
|
|
45
|
+
fs.readFileSync(path.resolve(here, "..", "package.json"), "utf8")
|
|
46
|
+
);
|
|
47
|
+
return pkg.version ?? "0.0.0";
|
|
48
|
+
} catch {
|
|
49
|
+
return "0.0.0";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
46
52
|
var KNOWN_AGENTS = {
|
|
47
53
|
claude: { label: "Claude Code", dirName: ".claude", full: true },
|
|
48
54
|
codex: { label: "Codex", dirName: ".codex", full: false },
|
|
@@ -121,6 +127,172 @@ async function exists(p) {
|
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
|
|
130
|
+
// src/lib/updates.ts
|
|
131
|
+
import fs3 from "fs/promises";
|
|
132
|
+
import path3 from "path";
|
|
133
|
+
function parseSemver(v) {
|
|
134
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
135
|
+
if (!m) return null;
|
|
136
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
137
|
+
}
|
|
138
|
+
function isNewerVersion(a, b) {
|
|
139
|
+
const pa = parseSemver(a);
|
|
140
|
+
const pb = parseSemver(b);
|
|
141
|
+
if (!pa || !pb) return false;
|
|
142
|
+
for (let i = 0; i < 3; i++) {
|
|
143
|
+
if (pa[i] !== pb[i]) return pa[i] > pb[i];
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
var SKILLS_VERSION_MARKER = "genex-skills-version.json";
|
|
148
|
+
async function readSkillsMarker(skillsDir) {
|
|
149
|
+
try {
|
|
150
|
+
const raw = await fs3.readFile(path3.join(skillsDir, SKILLS_VERSION_MARKER), "utf8");
|
|
151
|
+
const parsed = JSON.parse(raw);
|
|
152
|
+
return typeof parsed.version === "string" ? parsed.version : null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function writeSkillsMarker(skillsDir, version = getCliVersion()) {
|
|
158
|
+
await fs3.mkdir(skillsDir, { recursive: true });
|
|
159
|
+
await fs3.writeFile(
|
|
160
|
+
path3.join(skillsDir, SKILLS_VERSION_MARKER),
|
|
161
|
+
JSON.stringify({ version, syncedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
async function hasGenexSkills(skillsDir) {
|
|
165
|
+
try {
|
|
166
|
+
const entries = await fs3.readdir(skillsDir);
|
|
167
|
+
return entries.some((name) => name.startsWith("genex-"));
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion()) {
|
|
173
|
+
const skillsDir = path3.join(target.baseDir, "skills");
|
|
174
|
+
if (!await hasGenexSkills(skillsDir)) return false;
|
|
175
|
+
if (await readSkillsMarker(skillsDir) === version) return false;
|
|
176
|
+
const src = target.full ? templatesDir : path3.join(templatesDir, "skills");
|
|
177
|
+
const dest = target.full ? target.baseDir : skillsDir;
|
|
178
|
+
await copyTemplates(src, dest, { exclude: ["controllers"] });
|
|
179
|
+
await writeSkillsMarker(skillsDir, version);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
async function syncSkills(log) {
|
|
183
|
+
try {
|
|
184
|
+
const version = getCliVersion();
|
|
185
|
+
const templatesDir = getTemplatesDir();
|
|
186
|
+
let refreshed = false;
|
|
187
|
+
for (const target of resolveAgentTargets()) {
|
|
188
|
+
try {
|
|
189
|
+
refreshed = await syncSkillsForTarget(target, templatesDir, version) || refreshed;
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (refreshed) log.plain(`\u{1F504} Genex skills updated to ${version}`);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
var PUBLISHED_PACKAGES = [
|
|
198
|
+
{ name: "@genex-ai/cli-demo", label: "CLI", install: "npm i -D @genex-ai/cli-demo@latest" },
|
|
199
|
+
{ name: "@genex-ai/embed-sdk", label: "embed SDK", install: "npm i @genex-ai/embed-sdk@latest" },
|
|
200
|
+
{
|
|
201
|
+
name: "@genex-ai/multiplayer",
|
|
202
|
+
label: "multiplayer SDK",
|
|
203
|
+
install: "npm i @genex-ai/multiplayer@latest"
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
207
|
+
var REGISTRY_TIMEOUT_MS = 1500;
|
|
208
|
+
function getUpdateCachePath() {
|
|
209
|
+
return path3.join(getGenexDir(), "update-check.json");
|
|
210
|
+
}
|
|
211
|
+
function isCacheFresh(cache, nowMs) {
|
|
212
|
+
const at = Date.parse(cache.checkedAt);
|
|
213
|
+
return Number.isFinite(at) && nowMs - at < CHECK_INTERVAL_MS;
|
|
214
|
+
}
|
|
215
|
+
async function readUpdateCache() {
|
|
216
|
+
try {
|
|
217
|
+
const raw = await fs3.readFile(getUpdateCachePath(), "utf8");
|
|
218
|
+
const parsed = JSON.parse(raw);
|
|
219
|
+
if (typeof parsed.checkedAt !== "string" || typeof parsed.latest !== "object") return null;
|
|
220
|
+
return parsed;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function fetchLatestFromRegistry(name) {
|
|
226
|
+
try {
|
|
227
|
+
const res = await fetch(
|
|
228
|
+
`https://registry.npmjs.org/-/package/${encodeURIComponent(name)}/dist-tags`,
|
|
229
|
+
{ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) }
|
|
230
|
+
);
|
|
231
|
+
if (!res.ok) return null;
|
|
232
|
+
const tags = await res.json();
|
|
233
|
+
return typeof tags.latest === "string" && parseSemver(tags.latest) ? tags.latest : null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function startUpdateCheck() {
|
|
239
|
+
return (async () => {
|
|
240
|
+
try {
|
|
241
|
+
const cached = await readUpdateCache();
|
|
242
|
+
if (cached && isCacheFresh(cached, Date.now())) return cached;
|
|
243
|
+
const latest = { ...cached?.latest ?? {} };
|
|
244
|
+
const results = await Promise.allSettled(
|
|
245
|
+
PUBLISHED_PACKAGES.map((p) => fetchLatestFromRegistry(p.name))
|
|
246
|
+
);
|
|
247
|
+
results.forEach((r, i) => {
|
|
248
|
+
if (r.status === "fulfilled" && r.value) latest[PUBLISHED_PACKAGES[i].name] = r.value;
|
|
249
|
+
});
|
|
250
|
+
const next = { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latest };
|
|
251
|
+
await fs3.mkdir(getGenexDir(), { recursive: true });
|
|
252
|
+
await fs3.writeFile(getUpdateCachePath(), JSON.stringify(next, null, 2) + "\n");
|
|
253
|
+
return next;
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
}
|
|
259
|
+
async function installedPackageVersion(cwd, name) {
|
|
260
|
+
try {
|
|
261
|
+
const raw = await fs3.readFile(path3.join(cwd, "node_modules", name, "package.json"), "utf8");
|
|
262
|
+
const pkg = JSON.parse(raw);
|
|
263
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function formatNudge(label, latest, installed, install) {
|
|
269
|
+
return `\u2B06 Genex ${label} ${latest} available (installed ${installed}) \u2014 run: ${install}`;
|
|
270
|
+
}
|
|
271
|
+
async function reportUpdateNudges(check, log, cwd = process.cwd()) {
|
|
272
|
+
try {
|
|
273
|
+
const cache = await check;
|
|
274
|
+
if (!cache) return;
|
|
275
|
+
const lines = [];
|
|
276
|
+
let whatsNew = null;
|
|
277
|
+
for (const pkg of PUBLISHED_PACKAGES) {
|
|
278
|
+
const latest = cache.latest[pkg.name];
|
|
279
|
+
if (!latest) continue;
|
|
280
|
+
const installed = pkg.name === "@genex-ai/cli-demo" ? getCliVersion() : await installedPackageVersion(cwd, pkg.name);
|
|
281
|
+
if (!installed) continue;
|
|
282
|
+
if (isNewerVersion(latest, installed)) {
|
|
283
|
+
lines.push(formatNudge(pkg.label, latest, installed, pkg.install));
|
|
284
|
+
whatsNew ??= `https://www.npmjs.com/package/${pkg.name}`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (lines.length === 0) return;
|
|
288
|
+
for (const line of lines) log.plain(line);
|
|
289
|
+
log.dim(
|
|
290
|
+
` Apply at a safe moment (never mid-task) \u2014 see the genex-updates skill. What's new: ${whatsNew}`
|
|
291
|
+
);
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
124
296
|
// src/lib/auth.ts
|
|
125
297
|
import http from "http";
|
|
126
298
|
import crypto from "crypto";
|
|
@@ -388,6 +560,29 @@ var c = {
|
|
|
388
560
|
gray: code(90, 39)
|
|
389
561
|
};
|
|
390
562
|
|
|
563
|
+
// src/lib/api.ts
|
|
564
|
+
var CLI_VERSION_HEADER = "x-genex-cli-version";
|
|
565
|
+
function formatUpdateRequired(body) {
|
|
566
|
+
const action = body.action ?? "npm i -D @genex-ai/cli-demo@latest";
|
|
567
|
+
const message = body.message ?? `Genex CLI ${body.clientVersion ?? getCliVersion()} is below the minimum supported version${body.minVersion ? ` ${body.minVersion}` : ""}.`;
|
|
568
|
+
return [`${c.red("\u2717")} ${message}`, ` Update now \u2014 run: ${action} (then re-run this command)`];
|
|
569
|
+
}
|
|
570
|
+
async function apiFetch(url, init = {}) {
|
|
571
|
+
const headers = new Headers(init.headers);
|
|
572
|
+
if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
|
|
573
|
+
const res = await fetch(url, { ...init, headers });
|
|
574
|
+
if (res.status === 426) {
|
|
575
|
+
try {
|
|
576
|
+
const body = await res.clone().json();
|
|
577
|
+
if (body?.error === "cli_update_required") {
|
|
578
|
+
for (const line of formatUpdateRequired(body)) process.stderr.write(line + "\n");
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return res;
|
|
584
|
+
}
|
|
585
|
+
|
|
391
586
|
// src/lib/project.ts
|
|
392
587
|
async function createDraftProject(opts) {
|
|
393
588
|
const { apiUrl, token, deployKey, colyseusUrl, dashboardUrl, log } = opts;
|
|
@@ -397,7 +592,7 @@ async function createDraftProject(opts) {
|
|
|
397
592
|
const name = names[i];
|
|
398
593
|
let res;
|
|
399
594
|
try {
|
|
400
|
-
res = await
|
|
595
|
+
res = await apiFetch(`${apiUrl}/api/projects`, {
|
|
401
596
|
method: "POST",
|
|
402
597
|
headers: {
|
|
403
598
|
"Content-Type": "application/json",
|
|
@@ -453,15 +648,15 @@ function randomSuffix() {
|
|
|
453
648
|
}
|
|
454
649
|
|
|
455
650
|
// src/lib/ssh.ts
|
|
456
|
-
import
|
|
457
|
-
import
|
|
651
|
+
import fs4 from "fs/promises";
|
|
652
|
+
import path4 from "path";
|
|
458
653
|
import { spawn as spawn2 } from "child_process";
|
|
459
654
|
var KEY_NAME = "genex_key";
|
|
460
655
|
async function generateSshKeypair(dir, log) {
|
|
461
|
-
const keyPath =
|
|
656
|
+
const keyPath = path4.join(dir, KEY_NAME);
|
|
462
657
|
const pubPath = `${keyPath}.pub`;
|
|
463
658
|
try {
|
|
464
|
-
const existing = (await
|
|
659
|
+
const existing = (await fs4.readFile(pubPath, "utf8")).trim();
|
|
465
660
|
if (existing) {
|
|
466
661
|
log.dim(`Reusing existing deploy key (${KEY_NAME}).`);
|
|
467
662
|
return { publicKey: existing };
|
|
@@ -472,12 +667,12 @@ async function generateSshKeypair(dir, log) {
|
|
|
472
667
|
const ok = await runSshKeygen(keyPath, log);
|
|
473
668
|
if (!ok) return null;
|
|
474
669
|
try {
|
|
475
|
-
const pub = (await
|
|
670
|
+
const pub = (await fs4.readFile(pubPath, "utf8")).trim();
|
|
476
671
|
if (!pub) {
|
|
477
672
|
log.warn("ssh-keygen produced no public key.");
|
|
478
673
|
return null;
|
|
479
674
|
}
|
|
480
|
-
await
|
|
675
|
+
await fs4.chmod(keyPath, 384).catch(() => {
|
|
481
676
|
});
|
|
482
677
|
return { publicKey: pub };
|
|
483
678
|
} catch (err) {
|
|
@@ -507,10 +702,10 @@ function runSshKeygen(keyPath, log) {
|
|
|
507
702
|
});
|
|
508
703
|
}
|
|
509
704
|
async function writeGitignore(dir, log) {
|
|
510
|
-
const file =
|
|
705
|
+
const file = path4.join(dir, ".gitignore");
|
|
511
706
|
let content = "";
|
|
512
707
|
try {
|
|
513
|
-
content = await
|
|
708
|
+
content = await fs4.readFile(file, "utf8");
|
|
514
709
|
} catch {
|
|
515
710
|
}
|
|
516
711
|
const present = new Set(content.split("\n").map((l) => l.trim()));
|
|
@@ -520,23 +715,23 @@ async function writeGitignore(dir, log) {
|
|
|
520
715
|
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
521
716
|
if (!content.trim()) next += "# genex (deploy key + local metadata \u2014 never publish)\n";
|
|
522
717
|
next += toAdd.join("\n") + "\n";
|
|
523
|
-
await
|
|
718
|
+
await fs4.writeFile(file, next);
|
|
524
719
|
log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
|
|
525
720
|
}
|
|
526
721
|
|
|
527
722
|
// src/lib/store.ts
|
|
528
|
-
import
|
|
529
|
-
import
|
|
723
|
+
import fs6 from "fs/promises";
|
|
724
|
+
import path6 from "path";
|
|
530
725
|
|
|
531
726
|
// src/lib/env.ts
|
|
532
|
-
import
|
|
533
|
-
import
|
|
727
|
+
import fs5 from "fs/promises";
|
|
728
|
+
import path5 from "path";
|
|
534
729
|
import { spawn as spawn3 } from "child_process";
|
|
535
730
|
async function writeEnvVar(envPath, key, value) {
|
|
536
731
|
let content = "";
|
|
537
732
|
let existed = false;
|
|
538
733
|
try {
|
|
539
|
-
content = await
|
|
734
|
+
content = await fs5.readFile(envPath, "utf8");
|
|
540
735
|
existed = true;
|
|
541
736
|
} catch {
|
|
542
737
|
}
|
|
@@ -556,14 +751,14 @@ async function writeEnvVar(envPath, key, value) {
|
|
|
556
751
|
next = prefix + assignment + "\n";
|
|
557
752
|
mode = existed ? "appended" : "created";
|
|
558
753
|
}
|
|
559
|
-
await
|
|
560
|
-
await
|
|
754
|
+
await fs5.mkdir(path5.dirname(envPath), { recursive: true });
|
|
755
|
+
await fs5.writeFile(envPath, next, { mode: 384 });
|
|
561
756
|
await restrictFilePermissions(envPath);
|
|
562
757
|
return { mode, path: envPath };
|
|
563
758
|
}
|
|
564
759
|
async function restrictFilePermissions(filePath) {
|
|
565
760
|
if (process.platform !== "win32") {
|
|
566
|
-
await
|
|
761
|
+
await fs5.chmod(filePath, 384).catch(() => {
|
|
567
762
|
});
|
|
568
763
|
return;
|
|
569
764
|
}
|
|
@@ -595,7 +790,7 @@ function escapeRegExp(s) {
|
|
|
595
790
|
|
|
596
791
|
// src/lib/store.ts
|
|
597
792
|
function getProjectMetadataPath(cwd = process.cwd()) {
|
|
598
|
-
return
|
|
793
|
+
return path6.join(cwd, ".genex", "project.json");
|
|
599
794
|
}
|
|
600
795
|
async function writeUserToken(token, envPath) {
|
|
601
796
|
const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
|
|
@@ -605,14 +800,14 @@ async function readUserToken(envPath) {
|
|
|
605
800
|
const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
|
|
606
801
|
if (fromGenex) return fromGenex;
|
|
607
802
|
if (!envPath) {
|
|
608
|
-
return readTokenFromFile(
|
|
803
|
+
return readTokenFromFile(path6.join(process.cwd(), ".env"));
|
|
609
804
|
}
|
|
610
805
|
return null;
|
|
611
806
|
}
|
|
612
807
|
async function readTokenFromFile(file) {
|
|
613
808
|
let content;
|
|
614
809
|
try {
|
|
615
|
-
content = await
|
|
810
|
+
content = await fs6.readFile(file, "utf8");
|
|
616
811
|
} catch {
|
|
617
812
|
return null;
|
|
618
813
|
}
|
|
@@ -628,7 +823,7 @@ function stripQuotes(v) {
|
|
|
628
823
|
}
|
|
629
824
|
async function readProject(cwd = process.cwd()) {
|
|
630
825
|
try {
|
|
631
|
-
const raw = await
|
|
826
|
+
const raw = await fs6.readFile(getProjectMetadataPath(cwd), "utf8");
|
|
632
827
|
return JSON.parse(raw);
|
|
633
828
|
} catch {
|
|
634
829
|
return null;
|
|
@@ -636,16 +831,16 @@ async function readProject(cwd = process.cwd()) {
|
|
|
636
831
|
}
|
|
637
832
|
async function writeProject(meta, cwd = process.cwd()) {
|
|
638
833
|
const file = getProjectMetadataPath(cwd);
|
|
639
|
-
await
|
|
640
|
-
await
|
|
641
|
-
await
|
|
834
|
+
await fs6.mkdir(path6.dirname(file), { recursive: true });
|
|
835
|
+
await fs6.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
|
|
836
|
+
await fs6.chmod(file, 384).catch(() => {
|
|
642
837
|
});
|
|
643
838
|
return { path: file };
|
|
644
839
|
}
|
|
645
840
|
|
|
646
841
|
// src/lib/game-config.ts
|
|
647
|
-
import
|
|
648
|
-
import
|
|
842
|
+
import fs7 from "fs/promises";
|
|
843
|
+
import path7 from "path";
|
|
649
844
|
var DEFAULT_DASHBOARD_ORIGIN = new URL(DEFAULT_AUTH_URL).origin;
|
|
650
845
|
function renderGenexConfig() {
|
|
651
846
|
return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
|
|
@@ -686,7 +881,7 @@ ${overrides.join("\n")}
|
|
|
686
881
|
}
|
|
687
882
|
async function writeIfAbsent(file, content, log) {
|
|
688
883
|
try {
|
|
689
|
-
await
|
|
884
|
+
await fs7.writeFile(file, content, { flag: "wx" });
|
|
690
885
|
log.dim(` wrote ${c.cyan(file)}`);
|
|
691
886
|
return true;
|
|
692
887
|
} catch (err) {
|
|
@@ -698,12 +893,12 @@ async function writeIfAbsent(file, content, log) {
|
|
|
698
893
|
}
|
|
699
894
|
}
|
|
700
895
|
async function writeGameConfigFiles(meta, log, cwd = process.cwd()) {
|
|
701
|
-
await
|
|
702
|
-
await writeIfAbsent(
|
|
703
|
-
await writeIfAbsent(
|
|
896
|
+
await fs7.mkdir(path7.join(cwd, "src"), { recursive: true });
|
|
897
|
+
await writeIfAbsent(path7.join(cwd, "src", "genex.config.ts"), renderGenexConfig(), log);
|
|
898
|
+
await writeIfAbsent(path7.join(cwd, ".env"), renderSlugEnv(meta.slug), log);
|
|
704
899
|
const overrides = renderDevOverrides(meta);
|
|
705
900
|
if (overrides) {
|
|
706
|
-
await writeIfAbsent(
|
|
901
|
+
await writeIfAbsent(path7.join(cwd, ".env.development.local"), overrides, log);
|
|
707
902
|
}
|
|
708
903
|
}
|
|
709
904
|
|
|
@@ -739,12 +934,13 @@ async function runInit(opts) {
|
|
|
739
934
|
let totalNew = 0;
|
|
740
935
|
let totalUpdated = 0;
|
|
741
936
|
for (const t of targets) {
|
|
742
|
-
const src = t.full ? templatesDir :
|
|
743
|
-
const dest = t.full ? t.baseDir :
|
|
937
|
+
const src = t.full ? templatesDir : path8.join(templatesDir, "skills");
|
|
938
|
+
const dest = t.full ? t.baseDir : path8.join(t.baseDir, "skills");
|
|
744
939
|
const { copied, updated } = await copyTemplates(src, dest, {
|
|
745
940
|
force: opts.force,
|
|
746
941
|
exclude: ["controllers"]
|
|
747
942
|
});
|
|
943
|
+
await writeSkillsMarker(path8.join(t.baseDir, "skills"));
|
|
748
944
|
const added = copied.length - updated.length;
|
|
749
945
|
totalNew += added;
|
|
750
946
|
totalUpdated += updated.length;
|
|
@@ -787,7 +983,7 @@ async function runInit(opts) {
|
|
|
787
983
|
}
|
|
788
984
|
const apiUrl = getApiUrl(opts.apiUrl);
|
|
789
985
|
const colyseusUrl = getColyseusUrl(opts.colyseusUrl);
|
|
790
|
-
const projectName = opts.name?.trim() ||
|
|
986
|
+
const projectName = opts.name?.trim() || path8.basename(process.cwd());
|
|
791
987
|
const meta = await createDraftProject({
|
|
792
988
|
apiUrl,
|
|
793
989
|
token,
|
|
@@ -807,8 +1003,8 @@ async function runInit(opts) {
|
|
|
807
1003
|
}
|
|
808
1004
|
|
|
809
1005
|
// src/commands/link.ts
|
|
810
|
-
import
|
|
811
|
-
import
|
|
1006
|
+
import fs8 from "fs/promises";
|
|
1007
|
+
import path9 from "path";
|
|
812
1008
|
async function runLink(opts) {
|
|
813
1009
|
const log = createLogger({ quiet: opts.quiet });
|
|
814
1010
|
log.plain(c.bold("genex link"));
|
|
@@ -879,29 +1075,29 @@ async function runLink(opts) {
|
|
|
879
1075
|
if (project.playUrl) log.dim(` play URL: ${project.playUrl}`);
|
|
880
1076
|
}
|
|
881
1077
|
async function ensureSlugEnv(slug, log, cwd = process.cwd()) {
|
|
882
|
-
const file =
|
|
1078
|
+
const file = path9.join(cwd, ".env");
|
|
883
1079
|
let content;
|
|
884
1080
|
try {
|
|
885
|
-
content = await
|
|
1081
|
+
content = await fs8.readFile(file, "utf8");
|
|
886
1082
|
} catch {
|
|
887
1083
|
return;
|
|
888
1084
|
}
|
|
889
1085
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
890
1086
|
const m = content.match(re);
|
|
891
1087
|
if (!m) {
|
|
892
|
-
await
|
|
1088
|
+
await fs8.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
|
|
893
1089
|
`);
|
|
894
1090
|
log.dim(` added VITE_GENEX_SLUG=${slug} to .env`);
|
|
895
1091
|
return;
|
|
896
1092
|
}
|
|
897
1093
|
if (m[2].trim() === slug) return;
|
|
898
|
-
await
|
|
1094
|
+
await fs8.writeFile(file, content.replace(re, `$1${slug}`));
|
|
899
1095
|
log.dim(` updated .env: VITE_GENEX_SLUG=${slug} (was ${m[2].trim()})`);
|
|
900
1096
|
}
|
|
901
1097
|
async function fetchOwnProject(apiUrl, token, slug, log) {
|
|
902
1098
|
let res;
|
|
903
1099
|
try {
|
|
904
|
-
res = await
|
|
1100
|
+
res = await apiFetch(`${apiUrl}/api/projects/by-slug/${encodeURIComponent(slug)}`, {
|
|
905
1101
|
headers: { Authorization: `Bearer ${token}` }
|
|
906
1102
|
});
|
|
907
1103
|
} catch (err) {
|
|
@@ -933,7 +1129,7 @@ async function fetchOwnProject(apiUrl, token, slug, log) {
|
|
|
933
1129
|
}
|
|
934
1130
|
async function listOwnSlugs(apiUrl, token, log) {
|
|
935
1131
|
try {
|
|
936
|
-
const res = await
|
|
1132
|
+
const res = await apiFetch(`${apiUrl}/api/projects`, {
|
|
937
1133
|
headers: { Authorization: `Bearer ${token}` }
|
|
938
1134
|
});
|
|
939
1135
|
if (!res.ok) return;
|
|
@@ -950,7 +1146,7 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
950
1146
|
async function registerDeployKey(apiUrl, token, projectId, deployKey, log) {
|
|
951
1147
|
let res;
|
|
952
1148
|
try {
|
|
953
|
-
res = await
|
|
1149
|
+
res = await apiFetch(`${apiUrl}/api/projects/${projectId}/deploy-key`, {
|
|
954
1150
|
method: "POST",
|
|
955
1151
|
headers: {
|
|
956
1152
|
"Content-Type": "application/json",
|
|
@@ -982,9 +1178,9 @@ async function registerDeployKey(apiUrl, token, projectId, deployKey, log) {
|
|
|
982
1178
|
// src/lib/deploy.ts
|
|
983
1179
|
import { spawn as spawn4 } from "child_process";
|
|
984
1180
|
import crypto3 from "crypto";
|
|
985
|
-
import
|
|
1181
|
+
import fs9 from "fs/promises";
|
|
986
1182
|
import os2 from "os";
|
|
987
|
-
import
|
|
1183
|
+
import path10 from "path";
|
|
988
1184
|
function run(cmd, args, env) {
|
|
989
1185
|
return new Promise((resolve) => {
|
|
990
1186
|
let child;
|
|
@@ -1016,9 +1212,9 @@ async function deployGame(ctx, opts, log) {
|
|
|
1016
1212
|
}
|
|
1017
1213
|
log.success("Built.");
|
|
1018
1214
|
}
|
|
1019
|
-
const distDir =
|
|
1215
|
+
const distDir = path10.join(cwd, "dist");
|
|
1020
1216
|
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
1021
|
-
const rel =
|
|
1217
|
+
const rel = path10.relative(cwd, siteDir) || ".";
|
|
1022
1218
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
1023
1219
|
const files = await collectFiles(siteDir);
|
|
1024
1220
|
if (files.some((f) => /(^|\/)genex_key(\.pub)?$/.test(f.relPath))) {
|
|
@@ -1057,7 +1253,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
1057
1253
|
log.error("Couldn't upload your game \u2014 please try again.");
|
|
1058
1254
|
return false;
|
|
1059
1255
|
}
|
|
1060
|
-
const keyPath =
|
|
1256
|
+
const keyPath = path10.resolve(cwd, KEY_NAME);
|
|
1061
1257
|
if (!await fileExists(keyPath)) {
|
|
1062
1258
|
log.error(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
|
|
1063
1259
|
return false;
|
|
@@ -1071,7 +1267,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
1071
1267
|
}
|
|
1072
1268
|
async function hasBuildScript(cwd) {
|
|
1073
1269
|
try {
|
|
1074
|
-
const pkg = JSON.parse(await
|
|
1270
|
+
const pkg = JSON.parse(await fs9.readFile(path10.join(cwd, "package.json"), "utf8"));
|
|
1075
1271
|
return Boolean(pkg.scripts?.build);
|
|
1076
1272
|
} catch {
|
|
1077
1273
|
return false;
|
|
@@ -1080,12 +1276,12 @@ async function hasBuildScript(cwd) {
|
|
|
1080
1276
|
async function collectFiles(root) {
|
|
1081
1277
|
const out = [];
|
|
1082
1278
|
const walk2 = async (dir, prefix) => {
|
|
1083
|
-
for (const e of await
|
|
1279
|
+
for (const e of await fs9.readdir(dir, { withFileTypes: true })) {
|
|
1084
1280
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
1085
1281
|
if (e.isDirectory()) {
|
|
1086
|
-
if (!EXCLUDE_DIRS.has(e.name)) await walk2(
|
|
1282
|
+
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path10.join(dir, e.name), relPath);
|
|
1087
1283
|
} else if (e.isFile() && e.name !== KEY_NAME && e.name !== `${KEY_NAME}.pub`) {
|
|
1088
|
-
out.push({ relPath, bytes: await
|
|
1284
|
+
out.push({ relPath, bytes: await fs9.readFile(path10.join(dir, e.name)) });
|
|
1089
1285
|
}
|
|
1090
1286
|
}
|
|
1091
1287
|
};
|
|
@@ -1104,7 +1300,7 @@ function contentCommit(files) {
|
|
|
1104
1300
|
async function getUploadToken(ctx, commit, log) {
|
|
1105
1301
|
let res;
|
|
1106
1302
|
try {
|
|
1107
|
-
res = await
|
|
1303
|
+
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/upload-token`, {
|
|
1108
1304
|
method: "POST",
|
|
1109
1305
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
|
|
1110
1306
|
body: JSON.stringify({ commit })
|
|
@@ -1151,7 +1347,7 @@ async function uploadAll(uploadUrl, uploadToken, files, log) {
|
|
|
1151
1347
|
async function callPublish(ctx, commit, opts, log) {
|
|
1152
1348
|
let res;
|
|
1153
1349
|
try {
|
|
1154
|
-
res = await
|
|
1350
|
+
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/publish`, {
|
|
1155
1351
|
method: "POST",
|
|
1156
1352
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
|
|
1157
1353
|
// Detections ride every go-live (preview AND publish): matchmaking is
|
|
@@ -1181,7 +1377,7 @@ async function pushSource(cwd, sshUrl, keyPath, log) {
|
|
|
1181
1377
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
1182
1378
|
return false;
|
|
1183
1379
|
};
|
|
1184
|
-
const gitDir = await
|
|
1380
|
+
const gitDir = await fs9.mkdtemp(path10.join(os2.tmpdir(), "genex-source-"));
|
|
1185
1381
|
const base = { GIT_DIR: gitDir };
|
|
1186
1382
|
const ident = {
|
|
1187
1383
|
GIT_AUTHOR_NAME: "genex",
|
|
@@ -1191,11 +1387,11 @@ async function pushSource(cwd, sshUrl, keyPath, log) {
|
|
|
1191
1387
|
};
|
|
1192
1388
|
try {
|
|
1193
1389
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
1194
|
-
await
|
|
1195
|
-
|
|
1390
|
+
await fs9.writeFile(
|
|
1391
|
+
path10.join(gitDir, "info", "exclude"),
|
|
1196
1392
|
["node_modules/", "dist/", ".git/", KEY_NAME, `${KEY_NAME}.pub`, ".genex/", ""].join("\n")
|
|
1197
1393
|
);
|
|
1198
|
-
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE:
|
|
1394
|
+
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path10.join(gitDir, "index-source") };
|
|
1199
1395
|
await run("git", ["add", "-A"], env);
|
|
1200
1396
|
const tracked = await run("git", ["ls-files"], env);
|
|
1201
1397
|
if (/(^|\/)genex_key(\.pub)?$/m.test(tracked.out)) {
|
|
@@ -1219,20 +1415,20 @@ async function pushSource(cwd, sshUrl, keyPath, log) {
|
|
|
1219
1415
|
} catch {
|
|
1220
1416
|
return failed();
|
|
1221
1417
|
} finally {
|
|
1222
|
-
await
|
|
1418
|
+
await fs9.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
1223
1419
|
});
|
|
1224
1420
|
}
|
|
1225
1421
|
}
|
|
1226
1422
|
async function isDir2(p) {
|
|
1227
1423
|
try {
|
|
1228
|
-
return (await
|
|
1424
|
+
return (await fs9.stat(p)).isDirectory();
|
|
1229
1425
|
} catch {
|
|
1230
1426
|
return false;
|
|
1231
1427
|
}
|
|
1232
1428
|
}
|
|
1233
1429
|
async function fileExists(p) {
|
|
1234
1430
|
try {
|
|
1235
|
-
await
|
|
1431
|
+
await fs9.access(p);
|
|
1236
1432
|
return true;
|
|
1237
1433
|
} catch {
|
|
1238
1434
|
return false;
|
|
@@ -1266,11 +1462,11 @@ async function waitUntilLive(playUrl, fingerprint, timeoutMs, log) {
|
|
|
1266
1462
|
}
|
|
1267
1463
|
|
|
1268
1464
|
// src/lib/detect-features.ts
|
|
1269
|
-
import
|
|
1270
|
-
import
|
|
1465
|
+
import fs10 from "fs/promises";
|
|
1466
|
+
import path11 from "path";
|
|
1271
1467
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
1272
1468
|
try {
|
|
1273
|
-
const raw = await
|
|
1469
|
+
const raw = await fs10.readFile(path11.join(cwd, "package.json"), "utf8");
|
|
1274
1470
|
const pkg = JSON.parse(raw);
|
|
1275
1471
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
1276
1472
|
return typeof version === "string" && version ? version : null;
|
|
@@ -1280,7 +1476,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
1280
1476
|
}
|
|
1281
1477
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
1282
1478
|
try {
|
|
1283
|
-
const raw = await
|
|
1479
|
+
const raw = await fs10.readFile(path11.join(cwd, "package.json"), "utf8");
|
|
1284
1480
|
const pkg = JSON.parse(raw);
|
|
1285
1481
|
return Boolean(
|
|
1286
1482
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -1292,7 +1488,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
1292
1488
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
1293
1489
|
let pkg;
|
|
1294
1490
|
try {
|
|
1295
|
-
pkg = JSON.parse(await
|
|
1491
|
+
pkg = JSON.parse(await fs10.readFile(path11.join(cwd, "package.json"), "utf8"));
|
|
1296
1492
|
} catch (err) {
|
|
1297
1493
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
1298
1494
|
return null;
|
|
@@ -1309,10 +1505,10 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
1309
1505
|
}
|
|
1310
1506
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
1311
1507
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
1312
|
-
const srcDir =
|
|
1508
|
+
const srcDir = path11.join(cwd, "src");
|
|
1313
1509
|
let entries;
|
|
1314
1510
|
try {
|
|
1315
|
-
entries = await
|
|
1511
|
+
entries = await fs10.readdir(srcDir, { recursive: true });
|
|
1316
1512
|
} catch {
|
|
1317
1513
|
return false;
|
|
1318
1514
|
}
|
|
@@ -1320,7 +1516,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1320
1516
|
if (rel.includes("node_modules")) continue;
|
|
1321
1517
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
1322
1518
|
try {
|
|
1323
|
-
const content = await
|
|
1519
|
+
const content = await fs10.readFile(path11.join(srcDir, rel), "utf8");
|
|
1324
1520
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
1325
1521
|
} catch {
|
|
1326
1522
|
}
|
|
@@ -1406,7 +1602,7 @@ async function runPublish(opts) {
|
|
|
1406
1602
|
body.multiplayer = detections.multiplayer;
|
|
1407
1603
|
body.matchmaking = detections.matchmaking ?? null;
|
|
1408
1604
|
body.gameStateUsed = detections.gameStateUsed;
|
|
1409
|
-
res = await
|
|
1605
|
+
res = await apiFetch(`${apiUrl}/api/projects/${meta.id}/publish`, {
|
|
1410
1606
|
method: "POST",
|
|
1411
1607
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
1412
1608
|
body: JSON.stringify(body)
|
|
@@ -1533,7 +1729,7 @@ async function runGenerate(kind, opts) {
|
|
|
1533
1729
|
log.plain("");
|
|
1534
1730
|
let id;
|
|
1535
1731
|
try {
|
|
1536
|
-
const res = await
|
|
1732
|
+
const res = await apiFetch(`${apiUrl}/api/generations`, {
|
|
1537
1733
|
method: "POST",
|
|
1538
1734
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
1539
1735
|
body: JSON.stringify({ kind, prompt, options })
|
|
@@ -1599,7 +1795,7 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
|
|
|
1599
1795
|
try {
|
|
1600
1796
|
let res;
|
|
1601
1797
|
try {
|
|
1602
|
-
res = await
|
|
1798
|
+
res = await apiFetch(`${apiUrl}/api/generations/${id}/events`, {
|
|
1603
1799
|
headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
|
|
1604
1800
|
signal: abort.signal
|
|
1605
1801
|
});
|
|
@@ -1646,7 +1842,7 @@ async function poll(apiUrl, token, id, onProgress) {
|
|
|
1646
1842
|
let last = -1;
|
|
1647
1843
|
while (Date.now() < deadline) {
|
|
1648
1844
|
try {
|
|
1649
|
-
const res = await
|
|
1845
|
+
const res = await apiFetch(`${apiUrl}/api/generations/${id}`, {
|
|
1650
1846
|
headers: { Authorization: `Bearer ${token}` }
|
|
1651
1847
|
});
|
|
1652
1848
|
if (res.ok) {
|
|
@@ -1678,8 +1874,8 @@ function printHint(kind, files, log) {
|
|
|
1678
1874
|
}
|
|
1679
1875
|
|
|
1680
1876
|
// src/commands/controller.ts
|
|
1681
|
-
import
|
|
1682
|
-
import
|
|
1877
|
+
import fs11 from "fs/promises";
|
|
1878
|
+
import path12 from "path";
|
|
1683
1879
|
var CONTROLLER_KINDS = ["character", "car", "drone"];
|
|
1684
1880
|
var SHARED = [
|
|
1685
1881
|
"shared/math.ts",
|
|
@@ -1758,8 +1954,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1758
1954
|
]
|
|
1759
1955
|
}
|
|
1760
1956
|
};
|
|
1761
|
-
var CODE_DEST =
|
|
1762
|
-
var ASSETS_DEST =
|
|
1957
|
+
var CODE_DEST = path12.join("src", "controllers");
|
|
1958
|
+
var ASSETS_DEST = path12.join("public", "assets");
|
|
1763
1959
|
async function runController(opts) {
|
|
1764
1960
|
const log = createLogger({ quiet: opts.quiet });
|
|
1765
1961
|
const kind = opts.kind?.trim();
|
|
@@ -1772,31 +1968,31 @@ async function runController(opts) {
|
|
|
1772
1968
|
process.exitCode = 1;
|
|
1773
1969
|
return;
|
|
1774
1970
|
}
|
|
1775
|
-
const srcDir =
|
|
1971
|
+
const srcDir = path12.join(getTemplatesDir(), "controllers");
|
|
1776
1972
|
const root = opts.cwd ?? process.cwd();
|
|
1777
1973
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
1778
1974
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
1779
1975
|
log.plain("");
|
|
1780
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
1976
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path12.sep)}`);
|
|
1781
1977
|
const plan = [
|
|
1782
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
1978
|
+
...set.code.map((rel) => ({ from: rel, rel: path12.join(CODE_DEST, rel) })),
|
|
1783
1979
|
...set.assets.map((rel) => ({
|
|
1784
1980
|
from: rel,
|
|
1785
|
-
rel:
|
|
1981
|
+
rel: path12.join(ASSETS_DEST, path12.basename(rel))
|
|
1786
1982
|
}))
|
|
1787
1983
|
];
|
|
1788
1984
|
let copied = 0;
|
|
1789
1985
|
let skipped = 0;
|
|
1790
1986
|
try {
|
|
1791
1987
|
for (const file of plan) {
|
|
1792
|
-
const dest =
|
|
1988
|
+
const dest = path12.join(root, file.rel);
|
|
1793
1989
|
if (!opts.force && await exists2(dest)) {
|
|
1794
1990
|
skipped++;
|
|
1795
1991
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
1796
1992
|
continue;
|
|
1797
1993
|
}
|
|
1798
|
-
await
|
|
1799
|
-
await
|
|
1994
|
+
await fs11.mkdir(path12.dirname(dest), { recursive: true });
|
|
1995
|
+
await fs11.copyFile(path12.join(srcDir, file.from), dest);
|
|
1800
1996
|
copied++;
|
|
1801
1997
|
log.dim(` ${file.rel}`);
|
|
1802
1998
|
}
|
|
@@ -1828,11 +2024,11 @@ async function runController(opts) {
|
|
|
1828
2024
|
}
|
|
1829
2025
|
async function installOwnerAvatar(args) {
|
|
1830
2026
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
1831
|
-
const dest =
|
|
1832
|
-
await
|
|
2027
|
+
const dest = path12.join(root, ASSETS_DEST, "avatar.vrm");
|
|
2028
|
+
await fs11.mkdir(path12.dirname(dest), { recursive: true });
|
|
1833
2029
|
if (token) {
|
|
1834
2030
|
try {
|
|
1835
|
-
const res = await
|
|
2031
|
+
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
1836
2032
|
headers: { Authorization: `Bearer ${token}` }
|
|
1837
2033
|
});
|
|
1838
2034
|
if (res.ok) {
|
|
@@ -1841,7 +2037,7 @@ async function installOwnerAvatar(args) {
|
|
|
1841
2037
|
const vrmRes = await fetch(me.vrmUrl);
|
|
1842
2038
|
if (vrmRes.ok) {
|
|
1843
2039
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
1844
|
-
await
|
|
2040
|
+
await fs11.writeFile(dest, buf);
|
|
1845
2041
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
1846
2042
|
return;
|
|
1847
2043
|
}
|
|
@@ -1852,14 +2048,14 @@ async function installOwnerAvatar(args) {
|
|
|
1852
2048
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
1853
2049
|
}
|
|
1854
2050
|
}
|
|
1855
|
-
await
|
|
2051
|
+
await fs11.copyFile(path12.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
1856
2052
|
log.dim(
|
|
1857
2053
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
1858
2054
|
);
|
|
1859
2055
|
}
|
|
1860
2056
|
async function exists2(p) {
|
|
1861
2057
|
try {
|
|
1862
|
-
await
|
|
2058
|
+
await fs11.access(p);
|
|
1863
2059
|
return true;
|
|
1864
2060
|
} catch {
|
|
1865
2061
|
return false;
|
|
@@ -1873,7 +2069,7 @@ async function runExplore(opts) {
|
|
|
1873
2069
|
const apiUrl = getApiUrl(opts.apiUrl);
|
|
1874
2070
|
let res;
|
|
1875
2071
|
try {
|
|
1876
|
-
res = await
|
|
2072
|
+
res = await apiFetch(`${apiUrl}/api/gallery/search-corpus?curated=1`);
|
|
1877
2073
|
} catch {
|
|
1878
2074
|
log.error("Couldn't reach Genex \u2014 please try again.");
|
|
1879
2075
|
process.exitCode = 1;
|
|
@@ -1933,17 +2129,6 @@ function rank(items, query) {
|
|
|
1933
2129
|
|
|
1934
2130
|
// src/index.ts
|
|
1935
2131
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
|
|
1936
|
-
function getVersion() {
|
|
1937
|
-
try {
|
|
1938
|
-
const here = path12.dirname(fileURLToPath2(import.meta.url));
|
|
1939
|
-
const pkg = JSON.parse(
|
|
1940
|
-
readFileSync(path12.resolve(here, "..", "package.json"), "utf8")
|
|
1941
|
-
);
|
|
1942
|
-
return pkg.version ?? "0.0.0";
|
|
1943
|
-
} catch {
|
|
1944
|
-
return "0.0.0";
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
2132
|
var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize, and publish 3D games.
|
|
1948
2133
|
|
|
1949
2134
|
${c.bold("Usage")}
|
|
@@ -2206,43 +2391,50 @@ async function main() {
|
|
|
2206
2391
|
return;
|
|
2207
2392
|
}
|
|
2208
2393
|
if (parsed.version) {
|
|
2209
|
-
log.plain(
|
|
2394
|
+
log.plain(getCliVersion());
|
|
2210
2395
|
return;
|
|
2211
2396
|
}
|
|
2212
2397
|
if (parsed.help || parsed.command === void 0) {
|
|
2213
2398
|
log.plain(HELP);
|
|
2214
2399
|
return;
|
|
2215
2400
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2401
|
+
const updateLog = createLogger({ quiet: parsed.options.quiet });
|
|
2402
|
+
if (parsed.command !== "init") await syncSkills(updateLog);
|
|
2403
|
+
const updateCheck = startUpdateCheck();
|
|
2404
|
+
try {
|
|
2405
|
+
if (GEN_KINDS.has(parsed.command)) {
|
|
2406
|
+
await runGenerate(parsed.command, {
|
|
2407
|
+
...parsed.options,
|
|
2408
|
+
prompt: parsed.options.name
|
|
2409
|
+
});
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
switch (parsed.command) {
|
|
2413
|
+
case "init":
|
|
2414
|
+
await runInit(parsed.options);
|
|
2415
|
+
break;
|
|
2416
|
+
case "link":
|
|
2417
|
+
await runLink(parsed.options);
|
|
2418
|
+
break;
|
|
2419
|
+
case "controller":
|
|
2420
|
+
await runController({ ...parsed.options, kind: parsed.options.name });
|
|
2421
|
+
break;
|
|
2422
|
+
case "preview":
|
|
2423
|
+
await runPreview(parsed.options);
|
|
2424
|
+
break;
|
|
2425
|
+
case "publish":
|
|
2426
|
+
await runPublish(parsed.options);
|
|
2427
|
+
break;
|
|
2428
|
+
case "explore":
|
|
2429
|
+
await runExplore({ ...parsed.options, query: parsed.options.name });
|
|
2430
|
+
break;
|
|
2431
|
+
default:
|
|
2432
|
+
log.error(`Unknown command: ${parsed.command}`);
|
|
2433
|
+
log.plain(`Run ${c.cyan("genex --help")} for usage.`);
|
|
2434
|
+
process.exitCode = 1;
|
|
2435
|
+
}
|
|
2436
|
+
} finally {
|
|
2437
|
+
await reportUpdateNudges(updateCheck, updateLog);
|
|
2246
2438
|
}
|
|
2247
2439
|
}
|
|
2248
2440
|
main().catch((err) => {
|
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -8,9 +8,9 @@ for making 3D games in the browser.
|
|
|
8
8
|
Genex is built around agent superpowers for browser games: Three.js skills,
|
|
9
9
|
one-click publishing, multiplayer-ready architecture, and team workflows.
|
|
10
10
|
|
|
11
|
-
Files whose names start with `genex-` are managed by Genex:
|
|
12
|
-
|
|
13
|
-
never touched.
|
|
11
|
+
Files whose names start with `genex-` are managed by Genex: every `genex`
|
|
12
|
+
command keeps them in sync with the installed CLI version automatically
|
|
13
|
+
(re-running `genex init` does too). Anything you add yourself is never touched.
|
|
14
14
|
|
|
15
15
|
- `skills/` - reusable Genex skills for 3D game creation.
|
|
16
16
|
- `agents/` - example subagent definitions.
|
|
@@ -13,9 +13,9 @@ architecture, and team-ready workflows.
|
|
|
13
13
|
|
|
14
14
|
`genex init` installs the Genex skills into **every coding agent it detects** —
|
|
15
15
|
Claude Code (`~/.claude`), Codex (`~/.codex`), and Cursor (`~/.cursor`) — so the
|
|
16
|
-
same skills are available whichever agent you build with.
|
|
17
|
-
|
|
18
|
-
are never touched
|
|
16
|
+
same skills are available whichever agent you build with. After that, every
|
|
17
|
+
`genex` command keeps the genex-owned skills in sync with the installed CLI
|
|
18
|
+
automatically (re-running `init` does too); your own files are never touched.
|
|
19
19
|
|
|
20
20
|
- **skills/** - reusable Genex skills for 3D browser-game work (all agents).
|
|
21
21
|
- **agents/** - example subagent definitions (Claude Code).
|
|
@@ -110,6 +110,13 @@ After it, `npx genex preview` / `npx genex publish` update the **same live
|
|
|
110
110
|
game** — plays, likes, and comments stay. It also fixes a folder whose
|
|
111
111
|
authorization went stale ("Not authorized" from `preview`/`publish`).
|
|
112
112
|
|
|
113
|
+
## Staying up to date
|
|
114
|
+
|
|
115
|
+
Genex commands print an update nudge when a newer package exists
|
|
116
|
+
(`⬆ Genex … available — run: …`). Follow `$genex-updates`: apply the printed
|
|
117
|
+
command at a safe moment (never mid-task), then tell the user what changed.
|
|
118
|
+
Skills need no action — they sync automatically on every `genex` command.
|
|
119
|
+
|
|
113
120
|
## Re-running setup
|
|
114
121
|
|
|
115
122
|
Safe to run any time — genex-owned skills are refreshed to the latest version,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-updates
|
|
3
|
+
description: Apply Genex platform updates safely. Use when a genex command prints an update nudge ("⬆ Genex … available — run: …"), a skills-refresh line ("🔄 Genex skills updated"), or an update-required refusal (HTTP 426, cli_update_required), or when the user asks about updating Genex packages.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Updates — apply platform updates at safe moments
|
|
7
|
+
|
|
8
|
+
Genex ships improvements continuously. The `genex` CLI tells you when something
|
|
9
|
+
is stale; you (the agent) apply the update at the right moment and tell the
|
|
10
|
+
user what happened. The user never manages versions themselves.
|
|
11
|
+
|
|
12
|
+
## The three signals in `genex` output
|
|
13
|
+
|
|
14
|
+
| Line | Meaning | What you do |
|
|
15
|
+
| -- | -- | -- |
|
|
16
|
+
| `🔄 Genex skills updated to X.Y.Z` | Skills were auto-refreshed to match the installed CLI. | Nothing — informational. Skills stay in sync on their own. |
|
|
17
|
+
| `⬆ Genex <package> X.Y.Z available (installed A.B.C) — run: <command>` | A newer npm package exists. | Run the printed command at a **safe moment** (below), then tell the user. |
|
|
18
|
+
| `✗ … below the minimum supported version … Update now — run: <command>` | The API refuses this CLI version (HTTP 426, `cli_update_required`). | Run the printed command **now**, then re-run the refused genex command. |
|
|
19
|
+
|
|
20
|
+
## Safe moments — when to apply a nudge
|
|
21
|
+
|
|
22
|
+
Update when nothing is in flight:
|
|
23
|
+
|
|
24
|
+
- at the start of a session, before you begin the user's task;
|
|
25
|
+
- between tasks, after the current change is finished and working;
|
|
26
|
+
- right before a `genex preview`/`publish` you have **not** started yet.
|
|
27
|
+
|
|
28
|
+
Never mid-task: not during a build or deploy, not halfway through a code
|
|
29
|
+
change, not while the user is waiting for something else. A nudge is a
|
|
30
|
+
suggestion — if now is not a safe moment, finish first; the nudge reappears on
|
|
31
|
+
the next command. (The 426 refusal is the exception: nothing works until you
|
|
32
|
+
update, so update immediately.)
|
|
33
|
+
|
|
34
|
+
## How to apply
|
|
35
|
+
|
|
36
|
+
Run exactly the command the nudge printed, from the game project root:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm i -D @genex-ai/cli-demo@latest # the genex CLI (a dev dependency)
|
|
40
|
+
npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
|
|
41
|
+
npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Skills refresh automatically on the next `genex` command after a CLI update —
|
|
45
|
+
no extra step. Then tell the user in one short line what changed and why it
|
|
46
|
+
matters, e.g. "I also updated Genex to 0.22 — skills and SDK refreshed." For
|
|
47
|
+
what's new, check the package page: https://www.npmjs.com/package/@genex-ai/cli-demo
|
|
48
|
+
|
|
49
|
+
## Hard rules
|
|
50
|
+
|
|
51
|
+
- **Never touch game code as part of an update.** Everything in the game's
|
|
52
|
+
`src/` — including vendored controller code in `src/controllers/` — is
|
|
53
|
+
user-owned and may carry customizations. Updates never overwrite it. If a
|
|
54
|
+
newer controller exists you may *mention* that `genex controller <type>
|
|
55
|
+
--force` re-vendors it, but run that only when the user explicitly opts in
|
|
56
|
+
(it overwrites their changes).
|
|
57
|
+
- **Never update silently.** `npm i …@latest` changes `package.json` — that is
|
|
58
|
+
the sanctioned path, but always say in chat that you did it.
|
|
59
|
+
- **Never downgrade**, and never pin to an old version to "fix" an error —
|
|
60
|
+
report the error instead.
|
|
61
|
+
- **Verify after updating**: the game should still build/run. If it does not,
|
|
62
|
+
say exactly what broke rather than quietly reverting.
|
|
63
|
+
- If `npm i` fails (offline, registry down), say so and continue on the
|
|
64
|
+
current version — everything keeps working, and the nudge will reappear.
|
|
65
|
+
- A published game is never affected by any of this: its live bundle is frozen
|
|
66
|
+
and keeps working regardless of local package versions.
|