@genex-ai/cli-demo 0.25.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 +11 -11
- package/dist/index.js +50 -153
- package/package.json +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +5 -4
package/README.md
CHANGED
|
@@ -36,20 +36,20 @@ genex controller <type> # install a tuned character|car|drone controller →
|
|
|
36
36
|
browser can't open, it prints the URL to open manually.
|
|
37
37
|
3. **Saves your token** — writes `GENEX_TOKEN` to `~/.genex/env` (per-user;
|
|
38
38
|
reused across projects).
|
|
39
|
-
4. **Creates the draft project** —
|
|
40
|
-
(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
44
45
|
|
|
45
46
|
`genex link <slug>` is the recovery counterpart of step 4 for a game that
|
|
46
47
|
already exists: run it inside a fresh clone of the game's source repo (or any
|
|
47
|
-
folder that lost its link) and it re-authorizes if needed
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
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.
|
|
53
53
|
|
|
54
54
|
`genex preview` and `genex publish` share a build-aware deploy core: each runs
|
|
55
55
|
`npm run build` (when the project has a build script), then uploads the built
|
package/dist/index.js
CHANGED
|
@@ -585,7 +585,7 @@ async function apiFetch(url, init = {}) {
|
|
|
585
585
|
|
|
586
586
|
// src/lib/project.ts
|
|
587
587
|
async function createDraftProject(opts) {
|
|
588
|
-
const { apiUrl, token,
|
|
588
|
+
const { apiUrl, token, colyseusUrl, dashboardUrl, log } = opts;
|
|
589
589
|
log.step("Creating your project\u2026");
|
|
590
590
|
const names = [opts.name, `${opts.name}-${randomSuffix()}`];
|
|
591
591
|
for (let i = 0; i < names.length; i++) {
|
|
@@ -598,7 +598,7 @@ async function createDraftProject(opts) {
|
|
|
598
598
|
"Content-Type": "application/json",
|
|
599
599
|
Authorization: `Bearer ${token}`
|
|
600
600
|
},
|
|
601
|
-
body: JSON.stringify({ name
|
|
601
|
+
body: JSON.stringify({ name })
|
|
602
602
|
});
|
|
603
603
|
} catch (err) {
|
|
604
604
|
log.warn(`Couldn't reach the API at ${apiUrl} to create the project.`);
|
|
@@ -610,17 +610,13 @@ async function createDraftProject(opts) {
|
|
|
610
610
|
log.warn("Not authorized to create the project (token rejected).");
|
|
611
611
|
return null;
|
|
612
612
|
}
|
|
613
|
-
if (res.status === 400) {
|
|
614
|
-
log.warn("The API rejected the deploy key (must be an OpenSSH public key).");
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
613
|
if (!res.ok) {
|
|
618
614
|
log.warn(`Couldn't create the project (HTTP ${res.status}).`);
|
|
619
615
|
return null;
|
|
620
616
|
}
|
|
621
617
|
const data = await res.json().catch(() => null);
|
|
622
618
|
const project = data?.project;
|
|
623
|
-
if (!project || !
|
|
619
|
+
if (!project || !project.cloneUrl) {
|
|
624
620
|
log.warn("Project created, but the API response was unexpected.");
|
|
625
621
|
return null;
|
|
626
622
|
}
|
|
@@ -630,7 +626,7 @@ async function createDraftProject(opts) {
|
|
|
630
626
|
return {
|
|
631
627
|
id: project.id,
|
|
632
628
|
slug: project.slug,
|
|
633
|
-
|
|
629
|
+
cloneUrl: project.cloneUrl,
|
|
634
630
|
apiUrl,
|
|
635
631
|
colyseusUrl,
|
|
636
632
|
playUrl: project.playUrl ?? void 0,
|
|
@@ -650,57 +646,6 @@ function randomSuffix() {
|
|
|
650
646
|
// src/lib/ssh.ts
|
|
651
647
|
import fs4 from "fs/promises";
|
|
652
648
|
import path4 from "path";
|
|
653
|
-
import { spawn as spawn2 } from "child_process";
|
|
654
|
-
var KEY_NAME = "genex_key";
|
|
655
|
-
async function generateSshKeypair(dir, log) {
|
|
656
|
-
const keyPath = path4.join(dir, KEY_NAME);
|
|
657
|
-
const pubPath = `${keyPath}.pub`;
|
|
658
|
-
try {
|
|
659
|
-
const existing = (await fs4.readFile(pubPath, "utf8")).trim();
|
|
660
|
-
if (existing) {
|
|
661
|
-
log.dim(`Reusing existing deploy key (${KEY_NAME}).`);
|
|
662
|
-
return { publicKey: existing };
|
|
663
|
-
}
|
|
664
|
-
} catch {
|
|
665
|
-
}
|
|
666
|
-
log.step("Generating a deploy key\u2026");
|
|
667
|
-
const ok = await runSshKeygen(keyPath, log);
|
|
668
|
-
if (!ok) return null;
|
|
669
|
-
try {
|
|
670
|
-
const pub = (await fs4.readFile(pubPath, "utf8")).trim();
|
|
671
|
-
if (!pub) {
|
|
672
|
-
log.warn("ssh-keygen produced no public key.");
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
await fs4.chmod(keyPath, 384).catch(() => {
|
|
676
|
-
});
|
|
677
|
-
return { publicKey: pub };
|
|
678
|
-
} catch (err) {
|
|
679
|
-
log.warn(`Couldn't read the generated public key: ${String(err)}`);
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
function runSshKeygen(keyPath, log) {
|
|
684
|
-
return new Promise((resolve) => {
|
|
685
|
-
let child;
|
|
686
|
-
try {
|
|
687
|
-
child = spawn2(
|
|
688
|
-
"ssh-keygen",
|
|
689
|
-
["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "genex-agent"],
|
|
690
|
-
{ stdio: "ignore" }
|
|
691
|
-
);
|
|
692
|
-
} catch {
|
|
693
|
-
log.warn("ssh-keygen not found \u2014 install OpenSSH (ssh-keygen) and re-run.");
|
|
694
|
-
resolve(false);
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
child.on("error", () => {
|
|
698
|
-
log.warn("ssh-keygen not found \u2014 install OpenSSH (ssh-keygen) and re-run.");
|
|
699
|
-
resolve(false);
|
|
700
|
-
});
|
|
701
|
-
child.on("close", (code2) => resolve(code2 === 0));
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
649
|
async function writeGitignore(dir, log) {
|
|
705
650
|
const file = path4.join(dir, ".gitignore");
|
|
706
651
|
let content = "";
|
|
@@ -709,11 +654,11 @@ async function writeGitignore(dir, log) {
|
|
|
709
654
|
} catch {
|
|
710
655
|
}
|
|
711
656
|
const present = new Set(content.split("\n").map((l) => l.trim()));
|
|
712
|
-
const toAdd = [
|
|
657
|
+
const toAdd = [".genex/", ".env", ".env.*", "!.env.example"].filter((e) => !present.has(e));
|
|
713
658
|
if (toAdd.length === 0) return;
|
|
714
659
|
let next = content;
|
|
715
660
|
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
716
|
-
if (!content.trim()) next += "# genex
|
|
661
|
+
if (!content.trim()) next += "# genex local metadata + secrets \u2014 never publish\n";
|
|
717
662
|
next += toAdd.join("\n") + "\n";
|
|
718
663
|
await fs4.writeFile(file, next);
|
|
719
664
|
log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
|
|
@@ -726,7 +671,7 @@ import path6 from "path";
|
|
|
726
671
|
// src/lib/env.ts
|
|
727
672
|
import fs5 from "fs/promises";
|
|
728
673
|
import path5 from "path";
|
|
729
|
-
import { spawn as
|
|
674
|
+
import { spawn as spawn2 } from "child_process";
|
|
730
675
|
async function writeEnvVar(envPath, key, value) {
|
|
731
676
|
let content = "";
|
|
732
677
|
let existed = false;
|
|
@@ -766,7 +711,7 @@ async function restrictFilePermissions(filePath) {
|
|
|
766
711
|
if (!user) return;
|
|
767
712
|
await new Promise((resolve) => {
|
|
768
713
|
try {
|
|
769
|
-
const child =
|
|
714
|
+
const child = spawn2(
|
|
770
715
|
"icacls",
|
|
771
716
|
[filePath, "/inheritance:r", "/grant:r", `${user}:F`],
|
|
772
717
|
{ stdio: "ignore" }
|
|
@@ -971,16 +916,7 @@ async function runInit(opts) {
|
|
|
971
916
|
const { path: tokenPath } = await writeUserToken(token, opts.envPath);
|
|
972
917
|
log.success(`Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
|
|
973
918
|
log.plain("");
|
|
974
|
-
const key = await generateSshKeypair(process.cwd(), log);
|
|
975
919
|
await writeGitignore(process.cwd(), log);
|
|
976
|
-
if (!key) {
|
|
977
|
-
log.warn(
|
|
978
|
-
"Skipping project creation \u2014 no deploy key. Install ssh-keygen (OpenSSH) and re-run `genex init`."
|
|
979
|
-
);
|
|
980
|
-
log.plain("");
|
|
981
|
-
log.success("Workspace ready (no project created yet).");
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
920
|
const apiUrl = getApiUrl(opts.apiUrl);
|
|
985
921
|
const colyseusUrl = getColyseusUrl(opts.colyseusUrl);
|
|
986
922
|
const projectName = opts.name?.trim() || path8.basename(process.cwd());
|
|
@@ -988,7 +924,6 @@ async function runInit(opts) {
|
|
|
988
924
|
apiUrl,
|
|
989
925
|
token,
|
|
990
926
|
name: projectName,
|
|
991
|
-
deployKey: key.publicKey,
|
|
992
927
|
colyseusUrl,
|
|
993
928
|
dashboardUrl: authBaseUrl,
|
|
994
929
|
log
|
|
@@ -1041,23 +976,11 @@ async function runLink(opts) {
|
|
|
1041
976
|
process.exitCode = 1;
|
|
1042
977
|
return;
|
|
1043
978
|
}
|
|
1044
|
-
const key = await generateSshKeypair(process.cwd(), log);
|
|
1045
|
-
if (!key) {
|
|
1046
|
-
log.error("No deploy key \u2014 install ssh-keygen (OpenSSH) and re-run `genex link`.");
|
|
1047
|
-
process.exitCode = 1;
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
979
|
await writeGitignore(process.cwd(), log);
|
|
1051
|
-
log.step("Registering this folder's deploy key\u2026");
|
|
1052
|
-
const linked = await registerDeployKey(apiUrl, token, project.id, key.publicKey, log);
|
|
1053
|
-
if (!linked) {
|
|
1054
|
-
process.exitCode = 1;
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
980
|
const meta = {
|
|
1058
981
|
id: project.id,
|
|
1059
982
|
slug: project.slug,
|
|
1060
|
-
|
|
983
|
+
cloneUrl: project.cloneUrl ?? "",
|
|
1061
984
|
apiUrl,
|
|
1062
985
|
colyseusUrl: getColyseusUrl(opts.colyseusUrl),
|
|
1063
986
|
playUrl: project.playUrl ?? void 0,
|
|
@@ -1143,40 +1066,9 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
1143
1066
|
} catch {
|
|
1144
1067
|
}
|
|
1145
1068
|
}
|
|
1146
|
-
async function registerDeployKey(apiUrl, token, projectId, deployKey, log) {
|
|
1147
|
-
let res;
|
|
1148
|
-
try {
|
|
1149
|
-
res = await apiFetch(`${apiUrl}/api/projects/${projectId}/deploy-key`, {
|
|
1150
|
-
method: "POST",
|
|
1151
|
-
headers: {
|
|
1152
|
-
"Content-Type": "application/json",
|
|
1153
|
-
Authorization: `Bearer ${token}`
|
|
1154
|
-
},
|
|
1155
|
-
body: JSON.stringify({ deployKey })
|
|
1156
|
-
});
|
|
1157
|
-
} catch (err) {
|
|
1158
|
-
log.error(`Couldn't reach the API at ${apiUrl}.`);
|
|
1159
|
-
log.dim(` ${String(err)}`);
|
|
1160
|
-
return null;
|
|
1161
|
-
}
|
|
1162
|
-
if (res.status === 429) {
|
|
1163
|
-
log.error("Rate limited \u2014 too many link attempts. Try again in a bit.");
|
|
1164
|
-
return null;
|
|
1165
|
-
}
|
|
1166
|
-
if (!res.ok) {
|
|
1167
|
-
log.error(`Couldn't register the deploy key (HTTP ${res.status}).`);
|
|
1168
|
-
return null;
|
|
1169
|
-
}
|
|
1170
|
-
const data = await res.json().catch(() => null);
|
|
1171
|
-
if (!data?.sshUrl) {
|
|
1172
|
-
log.error("Unexpected API response while registering the deploy key.");
|
|
1173
|
-
return null;
|
|
1174
|
-
}
|
|
1175
|
-
return { sshUrl: data.sshUrl };
|
|
1176
|
-
}
|
|
1177
1069
|
|
|
1178
1070
|
// src/lib/deploy.ts
|
|
1179
|
-
import { spawn as
|
|
1071
|
+
import { spawn as spawn3 } from "child_process";
|
|
1180
1072
|
import crypto3 from "crypto";
|
|
1181
1073
|
import fs9 from "fs/promises";
|
|
1182
1074
|
import os2 from "os";
|
|
@@ -1185,7 +1077,7 @@ function run(cmd, args, env) {
|
|
|
1185
1077
|
return new Promise((resolve) => {
|
|
1186
1078
|
let child;
|
|
1187
1079
|
try {
|
|
1188
|
-
child =
|
|
1080
|
+
child = spawn3(cmd, args, { env: env ? { ...process.env, ...env } : process.env });
|
|
1189
1081
|
} catch {
|
|
1190
1082
|
resolve({ code: -1, out: "", err: `${cmd} not found` });
|
|
1191
1083
|
return;
|
|
@@ -1199,6 +1091,9 @@ function run(cmd, args, env) {
|
|
|
1199
1091
|
});
|
|
1200
1092
|
}
|
|
1201
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
|
+
}
|
|
1202
1097
|
async function deployGame(ctx, opts, log) {
|
|
1203
1098
|
const cwd = process.cwd();
|
|
1204
1099
|
if (!opts.noBuild && await hasBuildScript(cwd)) {
|
|
@@ -1217,10 +1112,6 @@ async function deployGame(ctx, opts, log) {
|
|
|
1217
1112
|
const rel = path10.relative(cwd, siteDir) || ".";
|
|
1218
1113
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
1219
1114
|
const files = await collectFiles(siteDir);
|
|
1220
|
-
if (files.some((f) => /(^|\/)genex_key(\.pub)?$/.test(f.relPath))) {
|
|
1221
|
-
log.error(`Refusing to deploy: ${KEY_NAME} is in ${rel}. Add it to .gitignore and retry.`);
|
|
1222
|
-
return false;
|
|
1223
|
-
}
|
|
1224
1115
|
if (files.length === 0) {
|
|
1225
1116
|
log.warn("Nothing to deploy \u2014 the build produced no files.");
|
|
1226
1117
|
return false;
|
|
@@ -1253,12 +1144,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
1253
1144
|
log.error("Couldn't upload your game \u2014 please try again.");
|
|
1254
1145
|
return false;
|
|
1255
1146
|
}
|
|
1256
|
-
|
|
1257
|
-
if (!await fileExists(keyPath)) {
|
|
1258
|
-
log.error(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
|
|
1259
|
-
return false;
|
|
1260
|
-
}
|
|
1261
|
-
if (!await pushSource(cwd, ctx.sshUrl, keyPath, log)) return false;
|
|
1147
|
+
if (!await pushSource(cwd, ctx, log)) return false;
|
|
1262
1148
|
log.step("Publishing\u2026");
|
|
1263
1149
|
if (!await callPublish(ctx, commit, opts, log)) return false;
|
|
1264
1150
|
const index = files.find((f) => f.relPath === "index.html");
|
|
@@ -1280,7 +1166,7 @@ async function collectFiles(root) {
|
|
|
1280
1166
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
1281
1167
|
if (e.isDirectory()) {
|
|
1282
1168
|
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path10.join(dir, e.name), relPath);
|
|
1283
|
-
} else if (e.isFile() && e.name
|
|
1169
|
+
} else if (e.isFile() && !isSecretEnvFile(e.name)) {
|
|
1284
1170
|
out.push({ relPath, bytes: await fs9.readFile(path10.join(dir, e.name)) });
|
|
1285
1171
|
}
|
|
1286
1172
|
}
|
|
@@ -1371,12 +1257,14 @@ async function callPublish(ctx, commit, opts, log) {
|
|
|
1371
1257
|
}
|
|
1372
1258
|
return true;
|
|
1373
1259
|
}
|
|
1374
|
-
async function pushSource(cwd,
|
|
1260
|
+
async function pushSource(cwd, ctx, log) {
|
|
1375
1261
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1376
1262
|
const failed = () => {
|
|
1377
1263
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
1378
1264
|
return false;
|
|
1379
1265
|
};
|
|
1266
|
+
const pushUrl = await fetchPushUrl(ctx, log);
|
|
1267
|
+
if (!pushUrl) return false;
|
|
1380
1268
|
const gitDir = await fs9.mkdtemp(path10.join(os2.tmpdir(), "genex-source-"));
|
|
1381
1269
|
const base = { GIT_DIR: gitDir };
|
|
1382
1270
|
const ident = {
|
|
@@ -1389,28 +1277,19 @@ async function pushSource(cwd, sshUrl, keyPath, log) {
|
|
|
1389
1277
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
1390
1278
|
await fs9.writeFile(
|
|
1391
1279
|
path10.join(gitDir, "info", "exclude"),
|
|
1392
|
-
|
|
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")
|
|
1393
1282
|
);
|
|
1394
1283
|
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path10.join(gitDir, "index-source") };
|
|
1395
1284
|
await run("git", ["add", "-A"], env);
|
|
1396
|
-
const
|
|
1397
|
-
if (/(^|\/)genex_key(\.pub)?$/m.test(tracked.out)) {
|
|
1398
|
-
log.error(`Refusing to publish: ${KEY_NAME} is in your project. Add it to .gitignore and retry.`);
|
|
1399
|
-
return false;
|
|
1400
|
-
}
|
|
1401
|
-
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() : "";
|
|
1402
1286
|
if (!tree || tree === EMPTY_TREE) {
|
|
1403
1287
|
log.error("Nothing to publish \u2014 the project has no files.");
|
|
1404
1288
|
return false;
|
|
1405
1289
|
}
|
|
1406
1290
|
const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
|
|
1407
1291
|
await run("git", ["update-ref", "refs/heads/main", commit], base);
|
|
1408
|
-
const push = await run("git", ["push", "-q",
|
|
1409
|
-
...base,
|
|
1410
|
-
// Quote the key path: git splits GIT_SSH_COMMAND shell-like, so a folder with a
|
|
1411
|
-
// space would otherwise break the `-i` argument.
|
|
1412
|
-
GIT_SSH_COMMAND: `ssh -i "${keyPath}" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
|
|
1413
|
-
});
|
|
1292
|
+
const push = await run("git", ["push", "-q", pushUrl, "+refs/heads/main:main"], base);
|
|
1414
1293
|
return push.code === 0 ? true : failed();
|
|
1415
1294
|
} catch {
|
|
1416
1295
|
return failed();
|
|
@@ -1419,17 +1298,35 @@ async function pushSource(cwd, sshUrl, keyPath, log) {
|
|
|
1419
1298
|
});
|
|
1420
1299
|
}
|
|
1421
1300
|
}
|
|
1422
|
-
async function
|
|
1301
|
+
async function fetchPushUrl(ctx, log) {
|
|
1302
|
+
let res;
|
|
1423
1303
|
try {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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;
|
|
1427
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;
|
|
1428
1326
|
}
|
|
1429
|
-
async function
|
|
1327
|
+
async function isDir2(p) {
|
|
1430
1328
|
try {
|
|
1431
|
-
await fs9.
|
|
1432
|
-
return true;
|
|
1329
|
+
return (await fs9.stat(p)).isDirectory();
|
|
1433
1330
|
} catch {
|
|
1434
1331
|
return false;
|
|
1435
1332
|
}
|
|
@@ -1571,7 +1468,7 @@ async function runPublish(opts) {
|
|
|
1571
1468
|
advisoryNudges(log, detections);
|
|
1572
1469
|
if (!opts.noPush) {
|
|
1573
1470
|
const ok = await deployGame(
|
|
1574
|
-
{ projectId: meta.id,
|
|
1471
|
+
{ projectId: meta.id, apiUrl, token },
|
|
1575
1472
|
{
|
|
1576
1473
|
noBuild: opts.noBuild,
|
|
1577
1474
|
matchmaking: detections.matchmaking,
|
|
@@ -1643,7 +1540,7 @@ async function runPreview(opts) {
|
|
|
1643
1540
|
advisoryNudges(log, detections);
|
|
1644
1541
|
const apiUrl = getApiUrl(meta.apiUrl);
|
|
1645
1542
|
const ok = await deployGame(
|
|
1646
|
-
{ projectId: meta.id,
|
|
1543
|
+
{ projectId: meta.id, apiUrl, token },
|
|
1647
1544
|
// Detections reach the server on every preview too: matchmaking so a draft's
|
|
1648
1545
|
// declared preset doesn't silently run the default (null clears a removed
|
|
1649
1546
|
// config), embedSdkVersion/multiplayer so the dashboard's Publish button can
|
package/package.json
CHANGED
|
@@ -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,8 +104,9 @@ 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`).
|