@genex-ai/cli-demo 0.24.0 → 0.26.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 +15 -13
- package/dist/index.js +374 -285
- package/package.json +1 -1
- package/templates/README.md +3 -3
- package/templates/skills/genex-getting-started/SKILL.md +15 -7
- package/templates/skills/genex-updates/SKILL.md +66 -0
package/README.md
CHANGED
|
@@ -28,26 +28,28 @@ 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;
|
|
36
38
|
reused across projects).
|
|
37
|
-
4. **Creates the draft project** —
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
4. **Creates the draft project** — `POST /api/projects` provisions a managed
|
|
40
|
+
Forgejo repo (one per user; no SSH key) and stores the project metadata (id,
|
|
41
|
+
slug, `cloneUrl`, urls) in `./.genex/project.json`. Source pushes to that repo
|
|
42
|
+
over HTTPS with a per-push token minted by the API (see below), so it works
|
|
43
|
+
from any device with a Genex login. The game shows up in your dashboard's
|
|
44
|
+
**My games** immediately.
|
|
42
45
|
|
|
43
46
|
`genex link <slug>` is the recovery counterpart of step 4 for a game that
|
|
44
47
|
already exists: run it inside a fresh clone of the game's source repo (or any
|
|
45
|
-
folder that lost its link) and it re-authorizes if needed
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
folder can't be found.
|
|
48
|
+
folder that lost its link) and it re-authorizes if needed and rewrites
|
|
49
|
+
`./.genex/project.json` — nothing to register, since source pushes authorize per
|
|
50
|
+
push over HTTPS. After it, `preview`/`publish` update the **same** live game. It
|
|
51
|
+
never creates a project; the dashboard's "Continue building" flow uses it when
|
|
52
|
+
the original folder can't be found.
|
|
51
53
|
|
|
52
54
|
`genex preview` and `genex publish` share a build-aware deploy core: each runs
|
|
53
55
|
`npm run build` (when the project has a build script), then uploads the built
|
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,22 +560,45 @@ 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
|
-
const { apiUrl, token,
|
|
588
|
+
const { apiUrl, token, colyseusUrl, dashboardUrl, log } = opts;
|
|
394
589
|
log.step("Creating your project\u2026");
|
|
395
590
|
const names = [opts.name, `${opts.name}-${randomSuffix()}`];
|
|
396
591
|
for (let i = 0; i < names.length; i++) {
|
|
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",
|
|
404
599
|
Authorization: `Bearer ${token}`
|
|
405
600
|
},
|
|
406
|
-
body: JSON.stringify({ name
|
|
601
|
+
body: JSON.stringify({ name })
|
|
407
602
|
});
|
|
408
603
|
} catch (err) {
|
|
409
604
|
log.warn(`Couldn't reach the API at ${apiUrl} to create the project.`);
|
|
@@ -415,17 +610,13 @@ async function createDraftProject(opts) {
|
|
|
415
610
|
log.warn("Not authorized to create the project (token rejected).");
|
|
416
611
|
return null;
|
|
417
612
|
}
|
|
418
|
-
if (res.status === 400) {
|
|
419
|
-
log.warn("The API rejected the deploy key (must be an OpenSSH public key).");
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
613
|
if (!res.ok) {
|
|
423
614
|
log.warn(`Couldn't create the project (HTTP ${res.status}).`);
|
|
424
615
|
return null;
|
|
425
616
|
}
|
|
426
617
|
const data = await res.json().catch(() => null);
|
|
427
618
|
const project = data?.project;
|
|
428
|
-
if (!project || !
|
|
619
|
+
if (!project || !project.cloneUrl) {
|
|
429
620
|
log.warn("Project created, but the API response was unexpected.");
|
|
430
621
|
return null;
|
|
431
622
|
}
|
|
@@ -435,7 +626,7 @@ async function createDraftProject(opts) {
|
|
|
435
626
|
return {
|
|
436
627
|
id: project.id,
|
|
437
628
|
slug: project.slug,
|
|
438
|
-
|
|
629
|
+
cloneUrl: project.cloneUrl,
|
|
439
630
|
apiUrl,
|
|
440
631
|
colyseusUrl,
|
|
441
632
|
playUrl: project.playUrl ?? void 0,
|
|
@@ -453,90 +644,39 @@ function randomSuffix() {
|
|
|
453
644
|
}
|
|
454
645
|
|
|
455
646
|
// src/lib/ssh.ts
|
|
456
|
-
import
|
|
457
|
-
import
|
|
458
|
-
import { spawn as spawn2 } from "child_process";
|
|
459
|
-
var KEY_NAME = "genex_key";
|
|
460
|
-
async function generateSshKeypair(dir, log) {
|
|
461
|
-
const keyPath = path3.join(dir, KEY_NAME);
|
|
462
|
-
const pubPath = `${keyPath}.pub`;
|
|
463
|
-
try {
|
|
464
|
-
const existing = (await fs3.readFile(pubPath, "utf8")).trim();
|
|
465
|
-
if (existing) {
|
|
466
|
-
log.dim(`Reusing existing deploy key (${KEY_NAME}).`);
|
|
467
|
-
return { publicKey: existing };
|
|
468
|
-
}
|
|
469
|
-
} catch {
|
|
470
|
-
}
|
|
471
|
-
log.step("Generating a deploy key\u2026");
|
|
472
|
-
const ok = await runSshKeygen(keyPath, log);
|
|
473
|
-
if (!ok) return null;
|
|
474
|
-
try {
|
|
475
|
-
const pub = (await fs3.readFile(pubPath, "utf8")).trim();
|
|
476
|
-
if (!pub) {
|
|
477
|
-
log.warn("ssh-keygen produced no public key.");
|
|
478
|
-
return null;
|
|
479
|
-
}
|
|
480
|
-
await fs3.chmod(keyPath, 384).catch(() => {
|
|
481
|
-
});
|
|
482
|
-
return { publicKey: pub };
|
|
483
|
-
} catch (err) {
|
|
484
|
-
log.warn(`Couldn't read the generated public key: ${String(err)}`);
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function runSshKeygen(keyPath, log) {
|
|
489
|
-
return new Promise((resolve) => {
|
|
490
|
-
let child;
|
|
491
|
-
try {
|
|
492
|
-
child = spawn2(
|
|
493
|
-
"ssh-keygen",
|
|
494
|
-
["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "genex-agent"],
|
|
495
|
-
{ stdio: "ignore" }
|
|
496
|
-
);
|
|
497
|
-
} catch {
|
|
498
|
-
log.warn("ssh-keygen not found \u2014 install OpenSSH (ssh-keygen) and re-run.");
|
|
499
|
-
resolve(false);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
child.on("error", () => {
|
|
503
|
-
log.warn("ssh-keygen not found \u2014 install OpenSSH (ssh-keygen) and re-run.");
|
|
504
|
-
resolve(false);
|
|
505
|
-
});
|
|
506
|
-
child.on("close", (code2) => resolve(code2 === 0));
|
|
507
|
-
});
|
|
508
|
-
}
|
|
647
|
+
import fs4 from "fs/promises";
|
|
648
|
+
import path4 from "path";
|
|
509
649
|
async function writeGitignore(dir, log) {
|
|
510
|
-
const file =
|
|
650
|
+
const file = path4.join(dir, ".gitignore");
|
|
511
651
|
let content = "";
|
|
512
652
|
try {
|
|
513
|
-
content = await
|
|
653
|
+
content = await fs4.readFile(file, "utf8");
|
|
514
654
|
} catch {
|
|
515
655
|
}
|
|
516
656
|
const present = new Set(content.split("\n").map((l) => l.trim()));
|
|
517
|
-
const toAdd = [
|
|
657
|
+
const toAdd = [".genex/", ".env", ".env.*", "!.env.example"].filter((e) => !present.has(e));
|
|
518
658
|
if (toAdd.length === 0) return;
|
|
519
659
|
let next = content;
|
|
520
660
|
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
521
|
-
if (!content.trim()) next += "# genex
|
|
661
|
+
if (!content.trim()) next += "# genex local metadata + secrets \u2014 never publish\n";
|
|
522
662
|
next += toAdd.join("\n") + "\n";
|
|
523
|
-
await
|
|
663
|
+
await fs4.writeFile(file, next);
|
|
524
664
|
log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
|
|
525
665
|
}
|
|
526
666
|
|
|
527
667
|
// src/lib/store.ts
|
|
528
|
-
import
|
|
529
|
-
import
|
|
668
|
+
import fs6 from "fs/promises";
|
|
669
|
+
import path6 from "path";
|
|
530
670
|
|
|
531
671
|
// src/lib/env.ts
|
|
532
|
-
import
|
|
533
|
-
import
|
|
534
|
-
import { spawn as
|
|
672
|
+
import fs5 from "fs/promises";
|
|
673
|
+
import path5 from "path";
|
|
674
|
+
import { spawn as spawn2 } from "child_process";
|
|
535
675
|
async function writeEnvVar(envPath, key, value) {
|
|
536
676
|
let content = "";
|
|
537
677
|
let existed = false;
|
|
538
678
|
try {
|
|
539
|
-
content = await
|
|
679
|
+
content = await fs5.readFile(envPath, "utf8");
|
|
540
680
|
existed = true;
|
|
541
681
|
} catch {
|
|
542
682
|
}
|
|
@@ -556,14 +696,14 @@ async function writeEnvVar(envPath, key, value) {
|
|
|
556
696
|
next = prefix + assignment + "\n";
|
|
557
697
|
mode = existed ? "appended" : "created";
|
|
558
698
|
}
|
|
559
|
-
await
|
|
560
|
-
await
|
|
699
|
+
await fs5.mkdir(path5.dirname(envPath), { recursive: true });
|
|
700
|
+
await fs5.writeFile(envPath, next, { mode: 384 });
|
|
561
701
|
await restrictFilePermissions(envPath);
|
|
562
702
|
return { mode, path: envPath };
|
|
563
703
|
}
|
|
564
704
|
async function restrictFilePermissions(filePath) {
|
|
565
705
|
if (process.platform !== "win32") {
|
|
566
|
-
await
|
|
706
|
+
await fs5.chmod(filePath, 384).catch(() => {
|
|
567
707
|
});
|
|
568
708
|
return;
|
|
569
709
|
}
|
|
@@ -571,7 +711,7 @@ async function restrictFilePermissions(filePath) {
|
|
|
571
711
|
if (!user) return;
|
|
572
712
|
await new Promise((resolve) => {
|
|
573
713
|
try {
|
|
574
|
-
const child =
|
|
714
|
+
const child = spawn2(
|
|
575
715
|
"icacls",
|
|
576
716
|
[filePath, "/inheritance:r", "/grant:r", `${user}:F`],
|
|
577
717
|
{ stdio: "ignore" }
|
|
@@ -595,7 +735,7 @@ function escapeRegExp(s) {
|
|
|
595
735
|
|
|
596
736
|
// src/lib/store.ts
|
|
597
737
|
function getProjectMetadataPath(cwd = process.cwd()) {
|
|
598
|
-
return
|
|
738
|
+
return path6.join(cwd, ".genex", "project.json");
|
|
599
739
|
}
|
|
600
740
|
async function writeUserToken(token, envPath) {
|
|
601
741
|
const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
|
|
@@ -605,14 +745,14 @@ async function readUserToken(envPath) {
|
|
|
605
745
|
const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
|
|
606
746
|
if (fromGenex) return fromGenex;
|
|
607
747
|
if (!envPath) {
|
|
608
|
-
return readTokenFromFile(
|
|
748
|
+
return readTokenFromFile(path6.join(process.cwd(), ".env"));
|
|
609
749
|
}
|
|
610
750
|
return null;
|
|
611
751
|
}
|
|
612
752
|
async function readTokenFromFile(file) {
|
|
613
753
|
let content;
|
|
614
754
|
try {
|
|
615
|
-
content = await
|
|
755
|
+
content = await fs6.readFile(file, "utf8");
|
|
616
756
|
} catch {
|
|
617
757
|
return null;
|
|
618
758
|
}
|
|
@@ -628,7 +768,7 @@ function stripQuotes(v) {
|
|
|
628
768
|
}
|
|
629
769
|
async function readProject(cwd = process.cwd()) {
|
|
630
770
|
try {
|
|
631
|
-
const raw = await
|
|
771
|
+
const raw = await fs6.readFile(getProjectMetadataPath(cwd), "utf8");
|
|
632
772
|
return JSON.parse(raw);
|
|
633
773
|
} catch {
|
|
634
774
|
return null;
|
|
@@ -636,16 +776,16 @@ async function readProject(cwd = process.cwd()) {
|
|
|
636
776
|
}
|
|
637
777
|
async function writeProject(meta, cwd = process.cwd()) {
|
|
638
778
|
const file = getProjectMetadataPath(cwd);
|
|
639
|
-
await
|
|
640
|
-
await
|
|
641
|
-
await
|
|
779
|
+
await fs6.mkdir(path6.dirname(file), { recursive: true });
|
|
780
|
+
await fs6.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
|
|
781
|
+
await fs6.chmod(file, 384).catch(() => {
|
|
642
782
|
});
|
|
643
783
|
return { path: file };
|
|
644
784
|
}
|
|
645
785
|
|
|
646
786
|
// src/lib/game-config.ts
|
|
647
|
-
import
|
|
648
|
-
import
|
|
787
|
+
import fs7 from "fs/promises";
|
|
788
|
+
import path7 from "path";
|
|
649
789
|
var DEFAULT_DASHBOARD_ORIGIN = new URL(DEFAULT_AUTH_URL).origin;
|
|
650
790
|
function renderGenexConfig() {
|
|
651
791
|
return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
|
|
@@ -686,7 +826,7 @@ ${overrides.join("\n")}
|
|
|
686
826
|
}
|
|
687
827
|
async function writeIfAbsent(file, content, log) {
|
|
688
828
|
try {
|
|
689
|
-
await
|
|
829
|
+
await fs7.writeFile(file, content, { flag: "wx" });
|
|
690
830
|
log.dim(` wrote ${c.cyan(file)}`);
|
|
691
831
|
return true;
|
|
692
832
|
} catch (err) {
|
|
@@ -698,12 +838,12 @@ async function writeIfAbsent(file, content, log) {
|
|
|
698
838
|
}
|
|
699
839
|
}
|
|
700
840
|
async function writeGameConfigFiles(meta, log, cwd = process.cwd()) {
|
|
701
|
-
await
|
|
702
|
-
await writeIfAbsent(
|
|
703
|
-
await writeIfAbsent(
|
|
841
|
+
await fs7.mkdir(path7.join(cwd, "src"), { recursive: true });
|
|
842
|
+
await writeIfAbsent(path7.join(cwd, "src", "genex.config.ts"), renderGenexConfig(), log);
|
|
843
|
+
await writeIfAbsent(path7.join(cwd, ".env"), renderSlugEnv(meta.slug), log);
|
|
704
844
|
const overrides = renderDevOverrides(meta);
|
|
705
845
|
if (overrides) {
|
|
706
|
-
await writeIfAbsent(
|
|
846
|
+
await writeIfAbsent(path7.join(cwd, ".env.development.local"), overrides, log);
|
|
707
847
|
}
|
|
708
848
|
}
|
|
709
849
|
|
|
@@ -739,12 +879,13 @@ async function runInit(opts) {
|
|
|
739
879
|
let totalNew = 0;
|
|
740
880
|
let totalUpdated = 0;
|
|
741
881
|
for (const t of targets) {
|
|
742
|
-
const src = t.full ? templatesDir :
|
|
743
|
-
const dest = t.full ? t.baseDir :
|
|
882
|
+
const src = t.full ? templatesDir : path8.join(templatesDir, "skills");
|
|
883
|
+
const dest = t.full ? t.baseDir : path8.join(t.baseDir, "skills");
|
|
744
884
|
const { copied, updated } = await copyTemplates(src, dest, {
|
|
745
885
|
force: opts.force,
|
|
746
886
|
exclude: ["controllers"]
|
|
747
887
|
});
|
|
888
|
+
await writeSkillsMarker(path8.join(t.baseDir, "skills"));
|
|
748
889
|
const added = copied.length - updated.length;
|
|
749
890
|
totalNew += added;
|
|
750
891
|
totalUpdated += updated.length;
|
|
@@ -775,24 +916,14 @@ async function runInit(opts) {
|
|
|
775
916
|
const { path: tokenPath } = await writeUserToken(token, opts.envPath);
|
|
776
917
|
log.success(`Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
|
|
777
918
|
log.plain("");
|
|
778
|
-
const key = await generateSshKeypair(process.cwd(), log);
|
|
779
919
|
await writeGitignore(process.cwd(), log);
|
|
780
|
-
if (!key) {
|
|
781
|
-
log.warn(
|
|
782
|
-
"Skipping project creation \u2014 no deploy key. Install ssh-keygen (OpenSSH) and re-run `genex init`."
|
|
783
|
-
);
|
|
784
|
-
log.plain("");
|
|
785
|
-
log.success("Workspace ready (no project created yet).");
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
920
|
const apiUrl = getApiUrl(opts.apiUrl);
|
|
789
921
|
const colyseusUrl = getColyseusUrl(opts.colyseusUrl);
|
|
790
|
-
const projectName = opts.name?.trim() ||
|
|
922
|
+
const projectName = opts.name?.trim() || path8.basename(process.cwd());
|
|
791
923
|
const meta = await createDraftProject({
|
|
792
924
|
apiUrl,
|
|
793
925
|
token,
|
|
794
926
|
name: projectName,
|
|
795
|
-
deployKey: key.publicKey,
|
|
796
927
|
colyseusUrl,
|
|
797
928
|
dashboardUrl: authBaseUrl,
|
|
798
929
|
log
|
|
@@ -807,8 +938,8 @@ async function runInit(opts) {
|
|
|
807
938
|
}
|
|
808
939
|
|
|
809
940
|
// src/commands/link.ts
|
|
810
|
-
import
|
|
811
|
-
import
|
|
941
|
+
import fs8 from "fs/promises";
|
|
942
|
+
import path9 from "path";
|
|
812
943
|
async function runLink(opts) {
|
|
813
944
|
const log = createLogger({ quiet: opts.quiet });
|
|
814
945
|
log.plain(c.bold("genex link"));
|
|
@@ -845,23 +976,11 @@ async function runLink(opts) {
|
|
|
845
976
|
process.exitCode = 1;
|
|
846
977
|
return;
|
|
847
978
|
}
|
|
848
|
-
const key = await generateSshKeypair(process.cwd(), log);
|
|
849
|
-
if (!key) {
|
|
850
|
-
log.error("No deploy key \u2014 install ssh-keygen (OpenSSH) and re-run `genex link`.");
|
|
851
|
-
process.exitCode = 1;
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
979
|
await writeGitignore(process.cwd(), log);
|
|
855
|
-
log.step("Registering this folder's deploy key\u2026");
|
|
856
|
-
const linked = await registerDeployKey(apiUrl, token, project.id, key.publicKey, log);
|
|
857
|
-
if (!linked) {
|
|
858
|
-
process.exitCode = 1;
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
980
|
const meta = {
|
|
862
981
|
id: project.id,
|
|
863
982
|
slug: project.slug,
|
|
864
|
-
|
|
983
|
+
cloneUrl: project.cloneUrl ?? "",
|
|
865
984
|
apiUrl,
|
|
866
985
|
colyseusUrl: getColyseusUrl(opts.colyseusUrl),
|
|
867
986
|
playUrl: project.playUrl ?? void 0,
|
|
@@ -879,29 +998,29 @@ async function runLink(opts) {
|
|
|
879
998
|
if (project.playUrl) log.dim(` play URL: ${project.playUrl}`);
|
|
880
999
|
}
|
|
881
1000
|
async function ensureSlugEnv(slug, log, cwd = process.cwd()) {
|
|
882
|
-
const file =
|
|
1001
|
+
const file = path9.join(cwd, ".env");
|
|
883
1002
|
let content;
|
|
884
1003
|
try {
|
|
885
|
-
content = await
|
|
1004
|
+
content = await fs8.readFile(file, "utf8");
|
|
886
1005
|
} catch {
|
|
887
1006
|
return;
|
|
888
1007
|
}
|
|
889
1008
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
890
1009
|
const m = content.match(re);
|
|
891
1010
|
if (!m) {
|
|
892
|
-
await
|
|
1011
|
+
await fs8.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
|
|
893
1012
|
`);
|
|
894
1013
|
log.dim(` added VITE_GENEX_SLUG=${slug} to .env`);
|
|
895
1014
|
return;
|
|
896
1015
|
}
|
|
897
1016
|
if (m[2].trim() === slug) return;
|
|
898
|
-
await
|
|
1017
|
+
await fs8.writeFile(file, content.replace(re, `$1${slug}`));
|
|
899
1018
|
log.dim(` updated .env: VITE_GENEX_SLUG=${slug} (was ${m[2].trim()})`);
|
|
900
1019
|
}
|
|
901
1020
|
async function fetchOwnProject(apiUrl, token, slug, log) {
|
|
902
1021
|
let res;
|
|
903
1022
|
try {
|
|
904
|
-
res = await
|
|
1023
|
+
res = await apiFetch(`${apiUrl}/api/projects/by-slug/${encodeURIComponent(slug)}`, {
|
|
905
1024
|
headers: { Authorization: `Bearer ${token}` }
|
|
906
1025
|
});
|
|
907
1026
|
} catch (err) {
|
|
@@ -933,7 +1052,7 @@ async function fetchOwnProject(apiUrl, token, slug, log) {
|
|
|
933
1052
|
}
|
|
934
1053
|
async function listOwnSlugs(apiUrl, token, log) {
|
|
935
1054
|
try {
|
|
936
|
-
const res = await
|
|
1055
|
+
const res = await apiFetch(`${apiUrl}/api/projects`, {
|
|
937
1056
|
headers: { Authorization: `Bearer ${token}` }
|
|
938
1057
|
});
|
|
939
1058
|
if (!res.ok) return;
|
|
@@ -947,49 +1066,18 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
947
1066
|
} catch {
|
|
948
1067
|
}
|
|
949
1068
|
}
|
|
950
|
-
async function registerDeployKey(apiUrl, token, projectId, deployKey, log) {
|
|
951
|
-
let res;
|
|
952
|
-
try {
|
|
953
|
-
res = await fetch(`${apiUrl}/api/projects/${projectId}/deploy-key`, {
|
|
954
|
-
method: "POST",
|
|
955
|
-
headers: {
|
|
956
|
-
"Content-Type": "application/json",
|
|
957
|
-
Authorization: `Bearer ${token}`
|
|
958
|
-
},
|
|
959
|
-
body: JSON.stringify({ deployKey })
|
|
960
|
-
});
|
|
961
|
-
} catch (err) {
|
|
962
|
-
log.error(`Couldn't reach the API at ${apiUrl}.`);
|
|
963
|
-
log.dim(` ${String(err)}`);
|
|
964
|
-
return null;
|
|
965
|
-
}
|
|
966
|
-
if (res.status === 429) {
|
|
967
|
-
log.error("Rate limited \u2014 too many link attempts. Try again in a bit.");
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
if (!res.ok) {
|
|
971
|
-
log.error(`Couldn't register the deploy key (HTTP ${res.status}).`);
|
|
972
|
-
return null;
|
|
973
|
-
}
|
|
974
|
-
const data = await res.json().catch(() => null);
|
|
975
|
-
if (!data?.sshUrl) {
|
|
976
|
-
log.error("Unexpected API response while registering the deploy key.");
|
|
977
|
-
return null;
|
|
978
|
-
}
|
|
979
|
-
return { sshUrl: data.sshUrl };
|
|
980
|
-
}
|
|
981
1069
|
|
|
982
1070
|
// src/lib/deploy.ts
|
|
983
|
-
import { spawn as
|
|
1071
|
+
import { spawn as spawn3 } from "child_process";
|
|
984
1072
|
import crypto3 from "crypto";
|
|
985
|
-
import
|
|
1073
|
+
import fs9 from "fs/promises";
|
|
986
1074
|
import os2 from "os";
|
|
987
|
-
import
|
|
1075
|
+
import path10 from "path";
|
|
988
1076
|
function run(cmd, args, env) {
|
|
989
1077
|
return new Promise((resolve) => {
|
|
990
1078
|
let child;
|
|
991
1079
|
try {
|
|
992
|
-
child =
|
|
1080
|
+
child = spawn3(cmd, args, { env: env ? { ...process.env, ...env } : process.env });
|
|
993
1081
|
} catch {
|
|
994
1082
|
resolve({ code: -1, out: "", err: `${cmd} not found` });
|
|
995
1083
|
return;
|
|
@@ -1003,6 +1091,9 @@ function run(cmd, args, env) {
|
|
|
1003
1091
|
});
|
|
1004
1092
|
}
|
|
1005
1093
|
var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".genex", "dist"]);
|
|
1094
|
+
function isSecretEnvFile(name) {
|
|
1095
|
+
return name === ".env" || name.startsWith(".env.") && name !== ".env.example";
|
|
1096
|
+
}
|
|
1006
1097
|
async function deployGame(ctx, opts, log) {
|
|
1007
1098
|
const cwd = process.cwd();
|
|
1008
1099
|
if (!opts.noBuild && await hasBuildScript(cwd)) {
|
|
@@ -1016,15 +1107,11 @@ async function deployGame(ctx, opts, log) {
|
|
|
1016
1107
|
}
|
|
1017
1108
|
log.success("Built.");
|
|
1018
1109
|
}
|
|
1019
|
-
const distDir =
|
|
1110
|
+
const distDir = path10.join(cwd, "dist");
|
|
1020
1111
|
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
1021
|
-
const rel =
|
|
1112
|
+
const rel = path10.relative(cwd, siteDir) || ".";
|
|
1022
1113
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
1023
1114
|
const files = await collectFiles(siteDir);
|
|
1024
|
-
if (files.some((f) => /(^|\/)genex_key(\.pub)?$/.test(f.relPath))) {
|
|
1025
|
-
log.error(`Refusing to deploy: ${KEY_NAME} is in ${rel}. Add it to .gitignore and retry.`);
|
|
1026
|
-
return false;
|
|
1027
|
-
}
|
|
1028
1115
|
if (files.length === 0) {
|
|
1029
1116
|
log.warn("Nothing to deploy \u2014 the build produced no files.");
|
|
1030
1117
|
return false;
|
|
@@ -1057,12 +1144,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
1057
1144
|
log.error("Couldn't upload your game \u2014 please try again.");
|
|
1058
1145
|
return false;
|
|
1059
1146
|
}
|
|
1060
|
-
|
|
1061
|
-
if (!await fileExists(keyPath)) {
|
|
1062
|
-
log.error(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
|
|
1063
|
-
return false;
|
|
1064
|
-
}
|
|
1065
|
-
if (!await pushSource(cwd, ctx.sshUrl, keyPath, log)) return false;
|
|
1147
|
+
if (!await pushSource(cwd, ctx, log)) return false;
|
|
1066
1148
|
log.step("Publishing\u2026");
|
|
1067
1149
|
if (!await callPublish(ctx, commit, opts, log)) return false;
|
|
1068
1150
|
const index = files.find((f) => f.relPath === "index.html");
|
|
@@ -1071,7 +1153,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
1071
1153
|
}
|
|
1072
1154
|
async function hasBuildScript(cwd) {
|
|
1073
1155
|
try {
|
|
1074
|
-
const pkg = JSON.parse(await
|
|
1156
|
+
const pkg = JSON.parse(await fs9.readFile(path10.join(cwd, "package.json"), "utf8"));
|
|
1075
1157
|
return Boolean(pkg.scripts?.build);
|
|
1076
1158
|
} catch {
|
|
1077
1159
|
return false;
|
|
@@ -1080,12 +1162,12 @@ async function hasBuildScript(cwd) {
|
|
|
1080
1162
|
async function collectFiles(root) {
|
|
1081
1163
|
const out = [];
|
|
1082
1164
|
const walk2 = async (dir, prefix) => {
|
|
1083
|
-
for (const e of await
|
|
1165
|
+
for (const e of await fs9.readdir(dir, { withFileTypes: true })) {
|
|
1084
1166
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
1085
1167
|
if (e.isDirectory()) {
|
|
1086
|
-
if (!EXCLUDE_DIRS.has(e.name)) await walk2(
|
|
1087
|
-
} else if (e.isFile() && e.name
|
|
1088
|
-
out.push({ relPath, bytes: await
|
|
1168
|
+
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path10.join(dir, e.name), relPath);
|
|
1169
|
+
} else if (e.isFile() && !isSecretEnvFile(e.name)) {
|
|
1170
|
+
out.push({ relPath, bytes: await fs9.readFile(path10.join(dir, e.name)) });
|
|
1089
1171
|
}
|
|
1090
1172
|
}
|
|
1091
1173
|
};
|
|
@@ -1104,7 +1186,7 @@ function contentCommit(files) {
|
|
|
1104
1186
|
async function getUploadToken(ctx, commit, log) {
|
|
1105
1187
|
let res;
|
|
1106
1188
|
try {
|
|
1107
|
-
res = await
|
|
1189
|
+
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/upload-token`, {
|
|
1108
1190
|
method: "POST",
|
|
1109
1191
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
|
|
1110
1192
|
body: JSON.stringify({ commit })
|
|
@@ -1151,7 +1233,7 @@ async function uploadAll(uploadUrl, uploadToken, files, log) {
|
|
|
1151
1233
|
async function callPublish(ctx, commit, opts, log) {
|
|
1152
1234
|
let res;
|
|
1153
1235
|
try {
|
|
1154
|
-
res = await
|
|
1236
|
+
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/publish`, {
|
|
1155
1237
|
method: "POST",
|
|
1156
1238
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
|
|
1157
1239
|
// Detections ride every go-live (preview AND publish): matchmaking is
|
|
@@ -1175,13 +1257,15 @@ async function callPublish(ctx, commit, opts, log) {
|
|
|
1175
1257
|
}
|
|
1176
1258
|
return true;
|
|
1177
1259
|
}
|
|
1178
|
-
async function pushSource(cwd,
|
|
1260
|
+
async function pushSource(cwd, ctx, log) {
|
|
1179
1261
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1180
1262
|
const failed = () => {
|
|
1181
1263
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
1182
1264
|
return false;
|
|
1183
1265
|
};
|
|
1184
|
-
const
|
|
1266
|
+
const pushUrl = await fetchPushUrl(ctx, log);
|
|
1267
|
+
if (!pushUrl) return false;
|
|
1268
|
+
const gitDir = await fs9.mkdtemp(path10.join(os2.tmpdir(), "genex-source-"));
|
|
1185
1269
|
const base = { GIT_DIR: gitDir };
|
|
1186
1270
|
const ident = {
|
|
1187
1271
|
GIT_AUTHOR_NAME: "genex",
|
|
@@ -1191,49 +1275,58 @@ async function pushSource(cwd, sshUrl, keyPath, log) {
|
|
|
1191
1275
|
};
|
|
1192
1276
|
try {
|
|
1193
1277
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
1194
|
-
await
|
|
1195
|
-
|
|
1196
|
-
|
|
1278
|
+
await fs9.writeFile(
|
|
1279
|
+
path10.join(gitDir, "info", "exclude"),
|
|
1280
|
+
// .env* are secrets — the managed repo is public. `!` keeps the non-secret template.
|
|
1281
|
+
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
1197
1282
|
);
|
|
1198
|
-
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE:
|
|
1283
|
+
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path10.join(gitDir, "index-source") };
|
|
1199
1284
|
await run("git", ["add", "-A"], env);
|
|
1200
|
-
const
|
|
1201
|
-
if (/(^|\/)genex_key(\.pub)?$/m.test(tracked.out)) {
|
|
1202
|
-
log.error(`Refusing to publish: ${KEY_NAME} is in your project. Add it to .gitignore and retry.`);
|
|
1203
|
-
return false;
|
|
1204
|
-
}
|
|
1205
|
-
const tree = tracked.out.trim() ? (await run("git", ["write-tree"], env)).out.trim() : "";
|
|
1285
|
+
const tree = (await run("git", ["ls-files"], env)).out.trim() ? (await run("git", ["write-tree"], env)).out.trim() : "";
|
|
1206
1286
|
if (!tree || tree === EMPTY_TREE) {
|
|
1207
1287
|
log.error("Nothing to publish \u2014 the project has no files.");
|
|
1208
1288
|
return false;
|
|
1209
1289
|
}
|
|
1210
1290
|
const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
|
|
1211
1291
|
await run("git", ["update-ref", "refs/heads/main", commit], base);
|
|
1212
|
-
const push = await run("git", ["push", "-q",
|
|
1213
|
-
...base,
|
|
1214
|
-
// Quote the key path: git splits GIT_SSH_COMMAND shell-like, so a folder with a
|
|
1215
|
-
// space would otherwise break the `-i` argument.
|
|
1216
|
-
GIT_SSH_COMMAND: `ssh -i "${keyPath}" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
|
|
1217
|
-
});
|
|
1292
|
+
const push = await run("git", ["push", "-q", pushUrl, "+refs/heads/main:main"], base);
|
|
1218
1293
|
return push.code === 0 ? true : failed();
|
|
1219
1294
|
} catch {
|
|
1220
1295
|
return failed();
|
|
1221
1296
|
} finally {
|
|
1222
|
-
await
|
|
1297
|
+
await fs9.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
1223
1298
|
});
|
|
1224
1299
|
}
|
|
1225
1300
|
}
|
|
1226
|
-
async function
|
|
1301
|
+
async function fetchPushUrl(ctx, log) {
|
|
1302
|
+
let res;
|
|
1227
1303
|
try {
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1304
|
+
res = await apiFetch(`${ctx.apiUrl}/api/projects/${ctx.projectId}/push-token`, {
|
|
1305
|
+
method: "POST",
|
|
1306
|
+
headers: { Authorization: `Bearer ${ctx.token}` }
|
|
1307
|
+
});
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
log.error(`Couldn't reach the API to authorize the source push: ${String(err)}`);
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
if (res.status === 401) {
|
|
1313
|
+
log.error("Not authorized \u2014 your token may have expired. Re-run `genex init`.");
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
if (!res.ok) {
|
|
1317
|
+
log.error(`Couldn't authorize the source push (HTTP ${res.status}).`);
|
|
1318
|
+
return null;
|
|
1231
1319
|
}
|
|
1320
|
+
const data = await res.json().catch(() => null);
|
|
1321
|
+
if (!data?.pushUrl) {
|
|
1322
|
+
log.error("The API didn't return a push URL.");
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
return data.pushUrl;
|
|
1232
1326
|
}
|
|
1233
|
-
async function
|
|
1327
|
+
async function isDir2(p) {
|
|
1234
1328
|
try {
|
|
1235
|
-
await
|
|
1236
|
-
return true;
|
|
1329
|
+
return (await fs9.stat(p)).isDirectory();
|
|
1237
1330
|
} catch {
|
|
1238
1331
|
return false;
|
|
1239
1332
|
}
|
|
@@ -1266,11 +1359,11 @@ async function waitUntilLive(playUrl, fingerprint, timeoutMs, log) {
|
|
|
1266
1359
|
}
|
|
1267
1360
|
|
|
1268
1361
|
// src/lib/detect-features.ts
|
|
1269
|
-
import
|
|
1270
|
-
import
|
|
1362
|
+
import fs10 from "fs/promises";
|
|
1363
|
+
import path11 from "path";
|
|
1271
1364
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
1272
1365
|
try {
|
|
1273
|
-
const raw = await
|
|
1366
|
+
const raw = await fs10.readFile(path11.join(cwd, "package.json"), "utf8");
|
|
1274
1367
|
const pkg = JSON.parse(raw);
|
|
1275
1368
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
1276
1369
|
return typeof version === "string" && version ? version : null;
|
|
@@ -1280,7 +1373,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
1280
1373
|
}
|
|
1281
1374
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
1282
1375
|
try {
|
|
1283
|
-
const raw = await
|
|
1376
|
+
const raw = await fs10.readFile(path11.join(cwd, "package.json"), "utf8");
|
|
1284
1377
|
const pkg = JSON.parse(raw);
|
|
1285
1378
|
return Boolean(
|
|
1286
1379
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -1292,7 +1385,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
1292
1385
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
1293
1386
|
let pkg;
|
|
1294
1387
|
try {
|
|
1295
|
-
pkg = JSON.parse(await
|
|
1388
|
+
pkg = JSON.parse(await fs10.readFile(path11.join(cwd, "package.json"), "utf8"));
|
|
1296
1389
|
} catch (err) {
|
|
1297
1390
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
1298
1391
|
return null;
|
|
@@ -1309,10 +1402,10 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
1309
1402
|
}
|
|
1310
1403
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
1311
1404
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
1312
|
-
const srcDir =
|
|
1405
|
+
const srcDir = path11.join(cwd, "src");
|
|
1313
1406
|
let entries;
|
|
1314
1407
|
try {
|
|
1315
|
-
entries = await
|
|
1408
|
+
entries = await fs10.readdir(srcDir, { recursive: true });
|
|
1316
1409
|
} catch {
|
|
1317
1410
|
return false;
|
|
1318
1411
|
}
|
|
@@ -1320,7 +1413,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1320
1413
|
if (rel.includes("node_modules")) continue;
|
|
1321
1414
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
1322
1415
|
try {
|
|
1323
|
-
const content = await
|
|
1416
|
+
const content = await fs10.readFile(path11.join(srcDir, rel), "utf8");
|
|
1324
1417
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
1325
1418
|
} catch {
|
|
1326
1419
|
}
|
|
@@ -1375,7 +1468,7 @@ async function runPublish(opts) {
|
|
|
1375
1468
|
advisoryNudges(log, detections);
|
|
1376
1469
|
if (!opts.noPush) {
|
|
1377
1470
|
const ok = await deployGame(
|
|
1378
|
-
{ projectId: meta.id,
|
|
1471
|
+
{ projectId: meta.id, apiUrl, token },
|
|
1379
1472
|
{
|
|
1380
1473
|
noBuild: opts.noBuild,
|
|
1381
1474
|
matchmaking: detections.matchmaking,
|
|
@@ -1406,7 +1499,7 @@ async function runPublish(opts) {
|
|
|
1406
1499
|
body.multiplayer = detections.multiplayer;
|
|
1407
1500
|
body.matchmaking = detections.matchmaking ?? null;
|
|
1408
1501
|
body.gameStateUsed = detections.gameStateUsed;
|
|
1409
|
-
res = await
|
|
1502
|
+
res = await apiFetch(`${apiUrl}/api/projects/${meta.id}/publish`, {
|
|
1410
1503
|
method: "POST",
|
|
1411
1504
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
1412
1505
|
body: JSON.stringify(body)
|
|
@@ -1447,7 +1540,7 @@ async function runPreview(opts) {
|
|
|
1447
1540
|
advisoryNudges(log, detections);
|
|
1448
1541
|
const apiUrl = getApiUrl(meta.apiUrl);
|
|
1449
1542
|
const ok = await deployGame(
|
|
1450
|
-
{ projectId: meta.id,
|
|
1543
|
+
{ projectId: meta.id, apiUrl, token },
|
|
1451
1544
|
// Detections reach the server on every preview too: matchmaking so a draft's
|
|
1452
1545
|
// declared preset doesn't silently run the default (null clears a removed
|
|
1453
1546
|
// config), embedSdkVersion/multiplayer so the dashboard's Publish button can
|
|
@@ -1533,7 +1626,7 @@ async function runGenerate(kind, opts) {
|
|
|
1533
1626
|
log.plain("");
|
|
1534
1627
|
let id;
|
|
1535
1628
|
try {
|
|
1536
|
-
const res = await
|
|
1629
|
+
const res = await apiFetch(`${apiUrl}/api/generations`, {
|
|
1537
1630
|
method: "POST",
|
|
1538
1631
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
1539
1632
|
body: JSON.stringify({ kind, prompt, options })
|
|
@@ -1599,7 +1692,7 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
|
|
|
1599
1692
|
try {
|
|
1600
1693
|
let res;
|
|
1601
1694
|
try {
|
|
1602
|
-
res = await
|
|
1695
|
+
res = await apiFetch(`${apiUrl}/api/generations/${id}/events`, {
|
|
1603
1696
|
headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
|
|
1604
1697
|
signal: abort.signal
|
|
1605
1698
|
});
|
|
@@ -1646,7 +1739,7 @@ async function poll(apiUrl, token, id, onProgress) {
|
|
|
1646
1739
|
let last = -1;
|
|
1647
1740
|
while (Date.now() < deadline) {
|
|
1648
1741
|
try {
|
|
1649
|
-
const res = await
|
|
1742
|
+
const res = await apiFetch(`${apiUrl}/api/generations/${id}`, {
|
|
1650
1743
|
headers: { Authorization: `Bearer ${token}` }
|
|
1651
1744
|
});
|
|
1652
1745
|
if (res.ok) {
|
|
@@ -1678,8 +1771,8 @@ function printHint(kind, files, log) {
|
|
|
1678
1771
|
}
|
|
1679
1772
|
|
|
1680
1773
|
// src/commands/controller.ts
|
|
1681
|
-
import
|
|
1682
|
-
import
|
|
1774
|
+
import fs11 from "fs/promises";
|
|
1775
|
+
import path12 from "path";
|
|
1683
1776
|
var CONTROLLER_KINDS = ["character", "car", "drone"];
|
|
1684
1777
|
var SHARED = [
|
|
1685
1778
|
"shared/math.ts",
|
|
@@ -1758,8 +1851,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1758
1851
|
]
|
|
1759
1852
|
}
|
|
1760
1853
|
};
|
|
1761
|
-
var CODE_DEST =
|
|
1762
|
-
var ASSETS_DEST =
|
|
1854
|
+
var CODE_DEST = path12.join("src", "controllers");
|
|
1855
|
+
var ASSETS_DEST = path12.join("public", "assets");
|
|
1763
1856
|
async function runController(opts) {
|
|
1764
1857
|
const log = createLogger({ quiet: opts.quiet });
|
|
1765
1858
|
const kind = opts.kind?.trim();
|
|
@@ -1772,31 +1865,31 @@ async function runController(opts) {
|
|
|
1772
1865
|
process.exitCode = 1;
|
|
1773
1866
|
return;
|
|
1774
1867
|
}
|
|
1775
|
-
const srcDir =
|
|
1868
|
+
const srcDir = path12.join(getTemplatesDir(), "controllers");
|
|
1776
1869
|
const root = opts.cwd ?? process.cwd();
|
|
1777
1870
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
1778
1871
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
1779
1872
|
log.plain("");
|
|
1780
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
1873
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path12.sep)}`);
|
|
1781
1874
|
const plan = [
|
|
1782
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
1875
|
+
...set.code.map((rel) => ({ from: rel, rel: path12.join(CODE_DEST, rel) })),
|
|
1783
1876
|
...set.assets.map((rel) => ({
|
|
1784
1877
|
from: rel,
|
|
1785
|
-
rel:
|
|
1878
|
+
rel: path12.join(ASSETS_DEST, path12.basename(rel))
|
|
1786
1879
|
}))
|
|
1787
1880
|
];
|
|
1788
1881
|
let copied = 0;
|
|
1789
1882
|
let skipped = 0;
|
|
1790
1883
|
try {
|
|
1791
1884
|
for (const file of plan) {
|
|
1792
|
-
const dest =
|
|
1885
|
+
const dest = path12.join(root, file.rel);
|
|
1793
1886
|
if (!opts.force && await exists2(dest)) {
|
|
1794
1887
|
skipped++;
|
|
1795
1888
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
1796
1889
|
continue;
|
|
1797
1890
|
}
|
|
1798
|
-
await
|
|
1799
|
-
await
|
|
1891
|
+
await fs11.mkdir(path12.dirname(dest), { recursive: true });
|
|
1892
|
+
await fs11.copyFile(path12.join(srcDir, file.from), dest);
|
|
1800
1893
|
copied++;
|
|
1801
1894
|
log.dim(` ${file.rel}`);
|
|
1802
1895
|
}
|
|
@@ -1828,11 +1921,11 @@ async function runController(opts) {
|
|
|
1828
1921
|
}
|
|
1829
1922
|
async function installOwnerAvatar(args) {
|
|
1830
1923
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
1831
|
-
const dest =
|
|
1832
|
-
await
|
|
1924
|
+
const dest = path12.join(root, ASSETS_DEST, "avatar.vrm");
|
|
1925
|
+
await fs11.mkdir(path12.dirname(dest), { recursive: true });
|
|
1833
1926
|
if (token) {
|
|
1834
1927
|
try {
|
|
1835
|
-
const res = await
|
|
1928
|
+
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
1836
1929
|
headers: { Authorization: `Bearer ${token}` }
|
|
1837
1930
|
});
|
|
1838
1931
|
if (res.ok) {
|
|
@@ -1841,7 +1934,7 @@ async function installOwnerAvatar(args) {
|
|
|
1841
1934
|
const vrmRes = await fetch(me.vrmUrl);
|
|
1842
1935
|
if (vrmRes.ok) {
|
|
1843
1936
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
1844
|
-
await
|
|
1937
|
+
await fs11.writeFile(dest, buf);
|
|
1845
1938
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
1846
1939
|
return;
|
|
1847
1940
|
}
|
|
@@ -1852,14 +1945,14 @@ async function installOwnerAvatar(args) {
|
|
|
1852
1945
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
1853
1946
|
}
|
|
1854
1947
|
}
|
|
1855
|
-
await
|
|
1948
|
+
await fs11.copyFile(path12.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
1856
1949
|
log.dim(
|
|
1857
1950
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
1858
1951
|
);
|
|
1859
1952
|
}
|
|
1860
1953
|
async function exists2(p) {
|
|
1861
1954
|
try {
|
|
1862
|
-
await
|
|
1955
|
+
await fs11.access(p);
|
|
1863
1956
|
return true;
|
|
1864
1957
|
} catch {
|
|
1865
1958
|
return false;
|
|
@@ -1873,7 +1966,7 @@ async function runExplore(opts) {
|
|
|
1873
1966
|
const apiUrl = getApiUrl(opts.apiUrl);
|
|
1874
1967
|
let res;
|
|
1875
1968
|
try {
|
|
1876
|
-
res = await
|
|
1969
|
+
res = await apiFetch(`${apiUrl}/api/gallery/search-corpus?curated=1`);
|
|
1877
1970
|
} catch {
|
|
1878
1971
|
log.error("Couldn't reach Genex \u2014 please try again.");
|
|
1879
1972
|
process.exitCode = 1;
|
|
@@ -1933,17 +2026,6 @@ function rank(items, query) {
|
|
|
1933
2026
|
|
|
1934
2027
|
// src/index.ts
|
|
1935
2028
|
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
2029
|
var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize, and publish 3D games.
|
|
1948
2030
|
|
|
1949
2031
|
${c.bold("Usage")}
|
|
@@ -2206,43 +2288,50 @@ async function main() {
|
|
|
2206
2288
|
return;
|
|
2207
2289
|
}
|
|
2208
2290
|
if (parsed.version) {
|
|
2209
|
-
log.plain(
|
|
2291
|
+
log.plain(getCliVersion());
|
|
2210
2292
|
return;
|
|
2211
2293
|
}
|
|
2212
2294
|
if (parsed.help || parsed.command === void 0) {
|
|
2213
2295
|
log.plain(HELP);
|
|
2214
2296
|
return;
|
|
2215
2297
|
}
|
|
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
|
-
|
|
2298
|
+
const updateLog = createLogger({ quiet: parsed.options.quiet });
|
|
2299
|
+
if (parsed.command !== "init") await syncSkills(updateLog);
|
|
2300
|
+
const updateCheck = startUpdateCheck();
|
|
2301
|
+
try {
|
|
2302
|
+
if (GEN_KINDS.has(parsed.command)) {
|
|
2303
|
+
await runGenerate(parsed.command, {
|
|
2304
|
+
...parsed.options,
|
|
2305
|
+
prompt: parsed.options.name
|
|
2306
|
+
});
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
switch (parsed.command) {
|
|
2310
|
+
case "init":
|
|
2311
|
+
await runInit(parsed.options);
|
|
2312
|
+
break;
|
|
2313
|
+
case "link":
|
|
2314
|
+
await runLink(parsed.options);
|
|
2315
|
+
break;
|
|
2316
|
+
case "controller":
|
|
2317
|
+
await runController({ ...parsed.options, kind: parsed.options.name });
|
|
2318
|
+
break;
|
|
2319
|
+
case "preview":
|
|
2320
|
+
await runPreview(parsed.options);
|
|
2321
|
+
break;
|
|
2322
|
+
case "publish":
|
|
2323
|
+
await runPublish(parsed.options);
|
|
2324
|
+
break;
|
|
2325
|
+
case "explore":
|
|
2326
|
+
await runExplore({ ...parsed.options, query: parsed.options.name });
|
|
2327
|
+
break;
|
|
2328
|
+
default:
|
|
2329
|
+
log.error(`Unknown command: ${parsed.command}`);
|
|
2330
|
+
log.plain(`Run ${c.cyan("genex --help")} for usage.`);
|
|
2331
|
+
process.exitCode = 1;
|
|
2332
|
+
}
|
|
2333
|
+
} finally {
|
|
2334
|
+
await reportUpdateNudges(updateCheck, updateLog);
|
|
2246
2335
|
}
|
|
2247
2336
|
}
|
|
2248
2337
|
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).
|
|
@@ -92,8 +92,8 @@ refreshes the genex-owned ones.
|
|
|
92
92
|
|
|
93
93
|
## Reconnecting an existing game (`genex link`)
|
|
94
94
|
|
|
95
|
-
Each game's connection to its live page is folder-local (`.genex/project.json`
|
|
96
|
-
|
|
95
|
+
Each game's connection to its live page is folder-local (`.genex/project.json`).
|
|
96
|
+
If that folder is gone — deleted, or the game
|
|
97
97
|
was built on another machine — **don't run `init` to "recover" it**: that
|
|
98
98
|
creates a brand-new game at a new URL. Instead, clone the game's source repo
|
|
99
99
|
and re-link the clone to the same live game:
|
|
@@ -104,12 +104,20 @@ npm install
|
|
|
104
104
|
npx @genex-ai/cli-demo@latest link <slug> # slug = the name in the play URL
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
`link` never creates a project: it signs in if needed (the browser opens once)
|
|
108
|
-
|
|
107
|
+
`link` never creates a project: it signs in if needed (the browser opens once)
|
|
108
|
+
and rewrites the local link (source pushes authorize over HTTPS, so there's no
|
|
109
|
+
key to set up — it works from any machine).
|
|
109
110
|
After it, `npx genex preview` / `npx genex publish` update the **same live
|
|
110
111
|
game** — plays, likes, and comments stay. It also fixes a folder whose
|
|
111
112
|
authorization went stale ("Not authorized" from `preview`/`publish`).
|
|
112
113
|
|
|
114
|
+
## Staying up to date
|
|
115
|
+
|
|
116
|
+
Genex commands print an update nudge when a newer package exists
|
|
117
|
+
(`⬆ Genex … available — run: …`). Follow `$genex-updates`: apply the printed
|
|
118
|
+
command at a safe moment (never mid-task), then tell the user what changed.
|
|
119
|
+
Skills need no action — they sync automatically on every `genex` command.
|
|
120
|
+
|
|
113
121
|
## Re-running setup
|
|
114
122
|
|
|
115
123
|
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.
|