@genex-ai/cli-demo 1.7.0-dev.449 → 1.7.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/dist/index.js +255 -979
- package/package.json +1 -1
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +1 -1
- package/templates/skills/genex-game-director/references/routing-map.md +0 -1
- package/templates/skills/genex-getting-started/SKILL.md +11 -43
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +11 -8
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +3 -7
- package/templates/skills/genex-updates/SKILL.md +1 -1
- package/templates/skills/genex-monetization/SKILL.md +0 -292
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs from "fs";
|
|
|
8
8
|
import os from "os";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
|
-
var RAW_CHANNEL = "
|
|
11
|
+
var RAW_CHANNEL = "latest";
|
|
12
12
|
var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
|
|
13
13
|
var STANDS = {
|
|
14
14
|
prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
|
|
@@ -1014,7 +1014,6 @@ Important note: put soul into your creations, with many details and love. Aim to
|
|
|
1014
1014
|
16. Never add debug-only code to the game to check your own work \u2014 no hidden test modes, no special URL parameters, no forced-visible flags, no auth mocks, no pixel-sampling hooks. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
|
|
1015
1015
|
17. Input directions match their labels: A/\u2190 moves or turns the player screen-LEFT, D/\u2192 screen-RIGHT, mouse-up looks up, and drag-pan axes share ONE convention. The cursor is either the gameplay tool (RTS, card, builder) or locked away during play \u2014 keyboard-only games included. Check it in every milestone's smoke pass.
|
|
1016
1016
|
18. NEVER delete, empty, move, rename, or overwrite anything you did not create yourself. This folder may hold the player's own reference images, notes, sketches, or an earlier attempt \u2014 files that exist nowhere else and have no undo, no trash, no backup. A non-empty folder is normal and is NEVER something to clean up, and "start clean" is never a reason. That rules out \`rm\`/\`rm -rf\`, \`git clean\`, \`git checkout -- .\`, \`git reset --hard\` over their work, deleting to resolve a conflict or a stuck interactive prompt, and every setup tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files") \u2014 scaffold into a fresh subfolder and copy in instead. You may add files and edit the ones you wrote. If a step genuinely cannot continue without removing something of theirs, STOP and ask, naming the exact files, and wait for a yes \u2014 "it looks like junk" is never that yes. This binds hardest during setup, where it runs fast and automatically before the player has asked for anything at all.
|
|
1017
|
-
19. If the game sells anything, it sells it for coin at a fixed, visible price, and NEVER sells chance. Run this test on any purchasable thing before building it: does the player pay (with coin, or with anything coin bought, directly or indirectly), is the outcome uncertain when they pay, and is there a prize they wanted \u2014 all three yes means it is paid randomness, and you build the deterministic version instead. That rules out loot boxes, gacha, mystery boxes, crates, card packs, prize wheels and raffles; wagering, staking, betting, coinflips and casino or slot mechanics denominated in coin; and donation prompts, tip jars or any player-to-player coin transfer, because coin buys goods and never just moves. Randomness the player EARNS by playing is gameplay, not commerce \u2014 an enemy dropping a random item, a chest found in the level, a procedural layout, a crit roll \u2014 and is completely fine. Every coin price renders its real-money equivalent beside it (the server sends one with every item), item prices sit on the platform's price grid so no player is left holding change they cannot spend, and nothing in a shop carries a countdown, "limited time", or a stock counter. When a request crosses one of these lines, name the mechanic, give the one-sentence reason, propose a specific compliant alternative, and build that \u2014 never the banned version "as an option", never a partial one, and never after asking the player to confirm they want it. Load \`$genex-monetization\` before building a shop.
|
|
1018
1017
|
${CONTRACT_END}
|
|
1019
1018
|
`;
|
|
1020
1019
|
var CLAUDE_IMPORT_LINE = "@AGENTS.md";
|
|
@@ -1476,7 +1475,7 @@ async function createDraftProject(opts) {
|
|
|
1476
1475
|
log.dim(` ${String(err)}`);
|
|
1477
1476
|
return { meta: null };
|
|
1478
1477
|
}
|
|
1479
|
-
if (
|
|
1478
|
+
if (res.status === 409 && i === 0) continue;
|
|
1480
1479
|
if (res.status === 401) {
|
|
1481
1480
|
log.warn("Not authorized to create the project (token rejected).");
|
|
1482
1481
|
return { meta: null, unauthorized: true };
|
|
@@ -1829,159 +1828,8 @@ async function runInit(opts) {
|
|
|
1829
1828
|
}
|
|
1830
1829
|
|
|
1831
1830
|
// src/commands/link.ts
|
|
1832
|
-
import fs12 from "fs/promises";
|
|
1833
|
-
import os7 from "os";
|
|
1834
|
-
import path12 from "path";
|
|
1835
|
-
|
|
1836
|
-
// src/lib/source-sync.ts
|
|
1837
1831
|
import fs11 from "fs/promises";
|
|
1838
1832
|
import path11 from "path";
|
|
1839
|
-
import os6 from "os";
|
|
1840
|
-
|
|
1841
|
-
// src/utils/run.ts
|
|
1842
|
-
import { spawn as spawn3 } from "child_process";
|
|
1843
|
-
var WIN_SHELL_COMMANDS = /* @__PURE__ */ new Set(["npm", "npx"]);
|
|
1844
|
-
function run(cmd, args, env) {
|
|
1845
|
-
const shell = process.platform === "win32" && WIN_SHELL_COMMANDS.has(cmd);
|
|
1846
|
-
return new Promise((resolve) => {
|
|
1847
|
-
let child;
|
|
1848
|
-
try {
|
|
1849
|
-
child = spawn3(cmd, args, { env: env ? { ...process.env, ...env } : process.env, shell });
|
|
1850
|
-
} catch {
|
|
1851
|
-
resolve({ code: -1, out: "", err: `${cmd} not found` });
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
let out = "";
|
|
1855
|
-
let err = "";
|
|
1856
|
-
child.stdout?.on("data", (d) => out += String(d));
|
|
1857
|
-
child.stderr?.on("data", (d) => err += String(d));
|
|
1858
|
-
child.on("error", () => resolve({ code: -1, out, err: `${cmd} not found` }));
|
|
1859
|
-
child.on("close", (code2) => resolve({ code: code2 ?? -1, out, err }));
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// src/lib/source-sync.ts
|
|
1864
|
-
async function readRemoteSource(apiUrl, token, projectId) {
|
|
1865
|
-
try {
|
|
1866
|
-
const res = await apiFetch(`${apiUrl}/api/projects/${projectId}`, {
|
|
1867
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1868
|
-
});
|
|
1869
|
-
if (!res.ok) return null;
|
|
1870
|
-
const data = await res.json().catch(() => null);
|
|
1871
|
-
if (!data?.project) return null;
|
|
1872
|
-
return {
|
|
1873
|
-
stagingCommit: data.project.stagingCommitSha ?? null,
|
|
1874
|
-
commit: data.project.commitSha ?? null
|
|
1875
|
-
};
|
|
1876
|
-
} catch {
|
|
1877
|
-
return null;
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
function isStale(local, remote) {
|
|
1881
|
-
if (!local || !remote) return false;
|
|
1882
|
-
return local !== remote;
|
|
1883
|
-
}
|
|
1884
|
-
function reportStale(log, slug, local, remote) {
|
|
1885
|
-
log.error(`${c.cyan(slug)} was updated from another device \u2014 nothing was deployed.`);
|
|
1886
|
-
if (remote && local) {
|
|
1887
|
-
log.dim(` the draft is on ${remote.slice(0, 7)}, this folder last shipped ${local.slice(0, 7)}`);
|
|
1888
|
-
}
|
|
1889
|
-
log.dim(` ${c.cyan("npx genex pull")} \u2014 take the other device's work (refuses if you have unshipped changes)`);
|
|
1890
|
-
log.dim(` ${c.cyan("npx genex preview --force")} \u2014 keep yours and replace theirs`);
|
|
1891
|
-
}
|
|
1892
|
-
async function sourceTreeHash(cwd) {
|
|
1893
|
-
const gitDir = await fs11.mkdtemp(path11.join(os6.tmpdir(), "genex-tree-"));
|
|
1894
|
-
const base = { GIT_DIR: gitDir };
|
|
1895
|
-
try {
|
|
1896
|
-
if ((await run("git", ["init", "-q"], base)).code !== 0) return null;
|
|
1897
|
-
await fs11.writeFile(
|
|
1898
|
-
path11.join(gitDir, "info", "exclude"),
|
|
1899
|
-
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
1900
|
-
);
|
|
1901
|
-
if ((await run("git", ["lfs", "version"], base)).code === 0) {
|
|
1902
|
-
const filters = [
|
|
1903
|
-
["filter.lfs.clean", "git-lfs clean -- %f"],
|
|
1904
|
-
["filter.lfs.smudge", "git-lfs smudge -- %f"],
|
|
1905
|
-
["filter.lfs.process", "git-lfs filter-process"],
|
|
1906
|
-
["filter.lfs.required", "true"]
|
|
1907
|
-
];
|
|
1908
|
-
for (const [key, value] of filters) await run("git", ["config", key, value], base);
|
|
1909
|
-
}
|
|
1910
|
-
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path11.join(gitDir, "index-tree") };
|
|
1911
|
-
if ((await run("git", ["add", "-A"], env)).code !== 0) return null;
|
|
1912
|
-
const tree = (await run("git", ["write-tree"], env)).out.trim();
|
|
1913
|
-
return /^[0-9a-f]{40}$/.test(tree) ? tree : null;
|
|
1914
|
-
} catch {
|
|
1915
|
-
return null;
|
|
1916
|
-
} finally {
|
|
1917
|
-
await fs11.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
function urlHasEmbeddedCredentials(url) {
|
|
1922
|
-
return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(url);
|
|
1923
|
-
}
|
|
1924
|
-
function credentialHelperOff(url) {
|
|
1925
|
-
if (!urlHasEmbeddedCredentials(url)) return {};
|
|
1926
|
-
return { GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "credential.helper", GIT_CONFIG_VALUE_0: "" };
|
|
1927
|
-
}
|
|
1928
|
-
async function fetchCloneGrant(apiUrl, token, projectId, log) {
|
|
1929
|
-
let res;
|
|
1930
|
-
try {
|
|
1931
|
-
res = await apiFetch(`${apiUrl}/api/projects/${projectId}/push-token`, {
|
|
1932
|
-
method: "POST",
|
|
1933
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1934
|
-
});
|
|
1935
|
-
} catch (err) {
|
|
1936
|
-
log.error(`Couldn't reach the API to authorize the source read: ${String(err)}`);
|
|
1937
|
-
return null;
|
|
1938
|
-
}
|
|
1939
|
-
if (res.status === 401) {
|
|
1940
|
-
log.error("Not authorized \u2014 your token may have expired. Re-run `genex auth`.");
|
|
1941
|
-
return null;
|
|
1942
|
-
}
|
|
1943
|
-
if (!res.ok) {
|
|
1944
|
-
log.error(`Couldn't authorize the source read (HTTP ${res.status}).`);
|
|
1945
|
-
return null;
|
|
1946
|
-
}
|
|
1947
|
-
const data = await res.json().catch(() => null);
|
|
1948
|
-
const url = data?.pushUrl ?? data?.cloneUrl;
|
|
1949
|
-
if (!url) {
|
|
1950
|
-
log.error("The API didn't return a source URL.");
|
|
1951
|
-
return null;
|
|
1952
|
-
}
|
|
1953
|
-
return { cloneUrl: url, sourceRef: data?.sourceRef ?? null };
|
|
1954
|
-
}
|
|
1955
|
-
async function cloneSource(grant, dest, log) {
|
|
1956
|
-
const args = grant.sourceRef ? ["clone", "--branch", grant.sourceRef, grant.cloneUrl, dest] : ["clone", grant.cloneUrl, dest];
|
|
1957
|
-
const env = {
|
|
1958
|
-
GIT_LFS_SKIP_SMUDGE: "1",
|
|
1959
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
1960
|
-
...credentialHelperOff(grant.cloneUrl)
|
|
1961
|
-
};
|
|
1962
|
-
const cloned = await run("git", args, env);
|
|
1963
|
-
if (cloned.code !== 0) {
|
|
1964
|
-
log.error("Couldn't download the game's source.");
|
|
1965
|
-
log.dim(` git clone exited ${cloned.code}`);
|
|
1966
|
-
return false;
|
|
1967
|
-
}
|
|
1968
|
-
const repo = { ...env, GIT_DIR: path11.join(dest, ".git"), GIT_WORK_TREE: dest };
|
|
1969
|
-
const filters = [
|
|
1970
|
-
["filter.lfs.clean", "git-lfs clean -- %f"],
|
|
1971
|
-
["filter.lfs.smudge", "git-lfs smudge -- %f"],
|
|
1972
|
-
["filter.lfs.process", "git-lfs filter-process"],
|
|
1973
|
-
["filter.lfs.required", "true"]
|
|
1974
|
-
];
|
|
1975
|
-
for (const [key, value] of filters) await run("git", ["config", key, value], repo);
|
|
1976
|
-
const pulled = await run("git", ["lfs", "pull"], repo);
|
|
1977
|
-
if (pulled.code !== 0) {
|
|
1978
|
-
log.warn("Binary assets (models, textures, audio) are still pointer files \u2014 `git lfs pull` failed.");
|
|
1979
|
-
log.dim(" The source is there; run `git lfs pull` in this folder once git-lfs works.");
|
|
1980
|
-
}
|
|
1981
|
-
return true;
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
// src/commands/link.ts
|
|
1985
1833
|
async function runLink(opts) {
|
|
1986
1834
|
const log = createLogger({ quiet: opts.quiet });
|
|
1987
1835
|
log.plain(c.bold("genex link"));
|
|
@@ -2029,9 +1877,7 @@ async function runLink(opts) {
|
|
|
2029
1877
|
process.exitCode = 1;
|
|
2030
1878
|
return;
|
|
2031
1879
|
}
|
|
2032
|
-
const wasEmpty = await isEmptyDir(process.cwd());
|
|
2033
1880
|
await writeGitignore(process.cwd(), log);
|
|
2034
|
-
const cloned = wasEmpty ? await downloadSource(apiUrl, token, project, log) : false;
|
|
2035
1881
|
const meta = {
|
|
2036
1882
|
id: project.id,
|
|
2037
1883
|
slug: project.slug,
|
|
@@ -2045,72 +1891,30 @@ async function runLink(opts) {
|
|
|
2045
1891
|
log.dim(` saved ${c.cyan(metaPath)}`);
|
|
2046
1892
|
await writeGameConfigFiles(meta, log);
|
|
2047
1893
|
await ensureSlugEnv(project.slug, log);
|
|
2048
|
-
const remote = await readRemoteSource(apiUrl, token, project.id);
|
|
2049
|
-
if (remote?.stagingCommit) meta.stagingCommit = remote.stagingCommit;
|
|
2050
|
-
if (cloned) {
|
|
2051
|
-
const tree = await sourceTreeHash(process.cwd());
|
|
2052
|
-
if (tree) meta.sourceTree = tree;
|
|
2053
|
-
}
|
|
2054
|
-
await writeProject(meta);
|
|
2055
1894
|
log.plain("");
|
|
2056
1895
|
log.success(
|
|
2057
1896
|
`Linked. This folder now updates ${c.cyan(project.slug)} \u2014 \`npx genex preview\` / \`publish\` ship to the same live game.`
|
|
2058
1897
|
);
|
|
2059
|
-
if (cloned) log.dim(` Run ${c.cyan("npm install")}, then keep building.`);
|
|
2060
1898
|
if (project.playUrl) log.dim(` play URL: ${project.playUrl}`);
|
|
2061
1899
|
}
|
|
2062
|
-
async function isEmptyDir(cwd) {
|
|
2063
|
-
try {
|
|
2064
|
-
const entries = await fs12.readdir(cwd);
|
|
2065
|
-
return entries.every((e) => e === ".git" || e === ".genex" || e === ".DS_Store");
|
|
2066
|
-
} catch {
|
|
2067
|
-
return false;
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
async function downloadSource(apiUrl, token, project, log) {
|
|
2071
|
-
const grant = await fetchCloneGrant(apiUrl, token, project.id, log);
|
|
2072
|
-
if (!grant) return false;
|
|
2073
|
-
if (!grant.sourceRef) {
|
|
2074
|
-
log.dim(" (no source saved for this game yet \u2014 nothing to download)");
|
|
2075
|
-
return false;
|
|
2076
|
-
}
|
|
2077
|
-
log.step(`Downloading the ${grant.sourceRef === "preview" ? "draft" : "published"} source\u2026`);
|
|
2078
|
-
const staging = await fs12.mkdtemp(path12.join(os7.tmpdir(), "genex-link-"));
|
|
2079
|
-
const fresh = path12.join(staging, "source");
|
|
2080
|
-
try {
|
|
2081
|
-
if (!await cloneSource(grant, fresh, log)) return false;
|
|
2082
|
-
await fs12.rm(path12.join(fresh, ".git"), { recursive: true, force: true });
|
|
2083
|
-
for (const entry of await fs12.readdir(fresh)) {
|
|
2084
|
-
await fs12.cp(path12.join(fresh, entry), path12.join(process.cwd(), entry), {
|
|
2085
|
-
recursive: true,
|
|
2086
|
-
force: true
|
|
2087
|
-
});
|
|
2088
|
-
}
|
|
2089
|
-
log.success("Downloaded.");
|
|
2090
|
-
return true;
|
|
2091
|
-
} finally {
|
|
2092
|
-
await fs12.rm(staging, { recursive: true, force: true }).catch(() => {
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
1900
|
async function ensureSlugEnv(slug, log, cwd = process.cwd()) {
|
|
2097
|
-
const file =
|
|
1901
|
+
const file = path11.join(cwd, ".env");
|
|
2098
1902
|
let content;
|
|
2099
1903
|
try {
|
|
2100
|
-
content = await
|
|
1904
|
+
content = await fs11.readFile(file, "utf8");
|
|
2101
1905
|
} catch {
|
|
2102
1906
|
return;
|
|
2103
1907
|
}
|
|
2104
1908
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
2105
1909
|
const m = content.match(re);
|
|
2106
1910
|
if (!m) {
|
|
2107
|
-
await
|
|
1911
|
+
await fs11.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
|
|
2108
1912
|
`);
|
|
2109
1913
|
log.dim(` added VITE_GENEX_SLUG=${slug} to .env`);
|
|
2110
1914
|
return;
|
|
2111
1915
|
}
|
|
2112
1916
|
if (m[2].trim() === slug) return;
|
|
2113
|
-
await
|
|
1917
|
+
await fs11.writeFile(file, content.replace(re, `$1${slug}`));
|
|
2114
1918
|
log.dim(` updated .env: VITE_GENEX_SLUG=${slug} (was ${m[2].trim()})`);
|
|
2115
1919
|
}
|
|
2116
1920
|
async function fetchOwnProject(apiUrl, token, slug, log) {
|
|
@@ -2163,132 +1967,9 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
2163
1967
|
}
|
|
2164
1968
|
}
|
|
2165
1969
|
|
|
2166
|
-
// src/commands/pull.ts
|
|
2167
|
-
import fs13 from "fs/promises";
|
|
2168
|
-
import path13 from "path";
|
|
2169
|
-
import os8 from "os";
|
|
2170
|
-
function isMachineLocal(entry) {
|
|
2171
|
-
if (entry === ".genex" || entry === "node_modules" || entry === ".git") return true;
|
|
2172
|
-
if (entry === ".env.example") return false;
|
|
2173
|
-
return entry === ".env" || entry.startsWith(".env.");
|
|
2174
|
-
}
|
|
2175
|
-
async function runPull(opts) {
|
|
2176
|
-
const log = createLogger({ quiet: opts.quiet });
|
|
2177
|
-
const cwd = process.cwd();
|
|
2178
|
-
log.plain(c.bold("genex pull"));
|
|
2179
|
-
log.plain("");
|
|
2180
|
-
const meta = await readProject(cwd);
|
|
2181
|
-
if (!meta) {
|
|
2182
|
-
log.error("This folder isn't linked to a game.");
|
|
2183
|
-
log.dim(` Run ${c.cyan("npx genex link <slug>")} here, or in an empty folder to download it.`);
|
|
2184
|
-
process.exitCode = 1;
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2187
|
-
const token = await readUserToken(opts.envPath);
|
|
2188
|
-
if (!token) {
|
|
2189
|
-
log.error(`Not signed in. Run ${c.cyan("npx genex auth")} first.`);
|
|
2190
|
-
process.exitCode = 1;
|
|
2191
|
-
return;
|
|
2192
|
-
}
|
|
2193
|
-
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
2194
|
-
if (!opts.force) {
|
|
2195
|
-
const tree2 = await sourceTreeHash(cwd);
|
|
2196
|
-
if (!meta.sourceTree) {
|
|
2197
|
-
log.error("This folder has never deployed, so there's no way to tell what's unsaved here.");
|
|
2198
|
-
log.dim(` ${c.cyan("npx genex preview")} first to save it, or ${c.cyan("npx genex pull --force")} to discard it.`);
|
|
2199
|
-
process.exitCode = 1;
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
if (tree2 === null) {
|
|
2203
|
-
log.error("Couldn't check this folder for unsaved changes (git is unavailable).");
|
|
2204
|
-
log.dim(` ${c.cyan("npx genex pull --force")} replaces it anyway.`);
|
|
2205
|
-
process.exitCode = 1;
|
|
2206
|
-
return;
|
|
2207
|
-
}
|
|
2208
|
-
if (tree2 !== meta.sourceTree) {
|
|
2209
|
-
log.error("This folder has changes that were never deployed.");
|
|
2210
|
-
log.dim(` ${c.cyan("npx genex preview")} ships them first (add ${c.cyan("--force")} if it says another device updated this)`);
|
|
2211
|
-
log.dim(` ${c.cyan("npx genex pull --force")} takes the draft and keeps a copy of what is here`);
|
|
2212
|
-
process.exitCode = 1;
|
|
2213
|
-
return;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
log.step(`Fetching ${c.cyan(meta.slug)}\u2026`);
|
|
2217
|
-
const grant = await fetchCloneGrant(apiUrl, token, meta.id, log);
|
|
2218
|
-
if (!grant) {
|
|
2219
|
-
process.exitCode = 1;
|
|
2220
|
-
return;
|
|
2221
|
-
}
|
|
2222
|
-
if (!grant.sourceRef) {
|
|
2223
|
-
log.error(`${c.cyan(meta.slug)} has no source saved yet \u2014 nothing to pull.`);
|
|
2224
|
-
log.dim(` Run ${c.cyan("npx genex preview")} on the machine that has the game.`);
|
|
2225
|
-
process.exitCode = 1;
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
const staging = await fs13.mkdtemp(path13.join(os8.tmpdir(), "genex-pull-"));
|
|
2229
|
-
const fresh = path13.join(staging, "source");
|
|
2230
|
-
try {
|
|
2231
|
-
if (!await cloneSource(grant, fresh, log)) {
|
|
2232
|
-
process.exitCode = 1;
|
|
2233
|
-
return;
|
|
2234
|
-
}
|
|
2235
|
-
await fs13.rm(path13.join(fresh, ".git"), { recursive: true, force: true });
|
|
2236
|
-
const kept = await keepReplaced(cwd, log);
|
|
2237
|
-
await replaceTree(cwd, fresh);
|
|
2238
|
-
if (kept) {
|
|
2239
|
-
log.plain("");
|
|
2240
|
-
log.info(`What was here is kept at ${c.cyan(path13.relative(cwd, kept) || kept)}`);
|
|
2241
|
-
log.dim(" Nothing was thrown away \u2014 re-apply from there, or delete it when you are done.");
|
|
2242
|
-
}
|
|
2243
|
-
} finally {
|
|
2244
|
-
await fs13.rm(staging, { recursive: true, force: true }).catch(() => {
|
|
2245
|
-
});
|
|
2246
|
-
}
|
|
2247
|
-
const remote = await readRemoteSource(apiUrl, token, meta.id);
|
|
2248
|
-
const tree = await sourceTreeHash(cwd);
|
|
2249
|
-
await writeProject(
|
|
2250
|
-
{
|
|
2251
|
-
...meta,
|
|
2252
|
-
...remote?.stagingCommit ? { stagingCommit: remote.stagingCommit } : {},
|
|
2253
|
-
...tree ? { sourceTree: tree } : {}
|
|
2254
|
-
},
|
|
2255
|
-
cwd
|
|
2256
|
-
);
|
|
2257
|
-
log.plain("");
|
|
2258
|
-
log.success(`Pulled ${c.cyan(meta.slug)}${grant.sourceRef === "preview" ? " (draft)" : ""}.`);
|
|
2259
|
-
log.dim(` Run ${c.cyan("npm install")} if dependencies changed, then keep building.`);
|
|
2260
|
-
}
|
|
2261
|
-
async function keepReplaced(cwd, log) {
|
|
2262
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2263
|
-
const dest = path13.join(cwd, ".genex", `replaced-${stamp}`);
|
|
2264
|
-
try {
|
|
2265
|
-
const entries = (await fs13.readdir(cwd)).filter((e) => !isMachineLocal(e));
|
|
2266
|
-
if (entries.length === 0) return null;
|
|
2267
|
-
await fs13.mkdir(dest, { recursive: true });
|
|
2268
|
-
for (const entry of entries) {
|
|
2269
|
-
await fs13.cp(path13.join(cwd, entry), path13.join(dest, entry), { recursive: true });
|
|
2270
|
-
}
|
|
2271
|
-
return dest;
|
|
2272
|
-
} catch (err) {
|
|
2273
|
-
log.warn("Couldn't keep a copy of what's being replaced \u2014 continuing with the pull.");
|
|
2274
|
-
log.dim(` ${String(err)}`);
|
|
2275
|
-
return null;
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
async function replaceTree(dest, src) {
|
|
2279
|
-
for (const entry of await fs13.readdir(dest)) {
|
|
2280
|
-
if (isMachineLocal(entry)) continue;
|
|
2281
|
-
await fs13.rm(path13.join(dest, entry), { recursive: true, force: true });
|
|
2282
|
-
}
|
|
2283
|
-
for (const entry of await fs13.readdir(src)) {
|
|
2284
|
-
if (isMachineLocal(entry)) continue;
|
|
2285
|
-
await fs13.cp(path13.join(src, entry), path13.join(dest, entry), { recursive: true });
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
1970
|
// src/commands/rename.ts
|
|
2290
|
-
import
|
|
2291
|
-
import
|
|
1971
|
+
import fs12 from "fs/promises";
|
|
1972
|
+
import path12 from "path";
|
|
2292
1973
|
async function runRename(opts) {
|
|
2293
1974
|
const log = createLogger({ quiet: opts.quiet });
|
|
2294
1975
|
log.plain(c.bold("genex rename"));
|
|
@@ -2314,9 +1995,8 @@ async function runRename(opts) {
|
|
|
2314
1995
|
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
2315
1996
|
const from = meta.slug;
|
|
2316
1997
|
if (meta.status === "published" && !opts.yes) {
|
|
2317
|
-
log.warn(`This retires ${c.cyan(from)}
|
|
2318
|
-
log.dim("
|
|
2319
|
-
log.dim(" Games get a limited number of renames. Re-run with --yes to spend one.");
|
|
1998
|
+
log.warn(`This retires ${c.cyan(`https://${from}.genex.technology/`)} \u2014 links already shared to it will stop working.`);
|
|
1999
|
+
log.dim(" Plays, likes, and comments move with the game. Re-run with --yes to go ahead.");
|
|
2320
2000
|
process.exitCode = 1;
|
|
2321
2001
|
return;
|
|
2322
2002
|
}
|
|
@@ -2366,33 +2046,32 @@ async function runRename(opts) {
|
|
|
2366
2046
|
await rewriteBakedSlug(from, project.slug, log);
|
|
2367
2047
|
log.plain("");
|
|
2368
2048
|
log.success(`Renamed to ${c.cyan(project.slug)}.`);
|
|
2369
|
-
log.dim(` ${from} still works \u2014 it redirects here.`);
|
|
2370
2049
|
if (project.playUrl) log.dim(` play: ${project.playUrl}`);
|
|
2371
2050
|
if (meta.status === "published") {
|
|
2372
2051
|
const dashboard = meta.dashboardOrigins?.[0];
|
|
2373
|
-
if (dashboard) log.dim(` page: ${dashboard}/${project.slug}`);
|
|
2052
|
+
if (dashboard) log.dim(` world page: ${dashboard}/world/${project.slug}`);
|
|
2374
2053
|
}
|
|
2375
2054
|
log.plain("");
|
|
2376
2055
|
log.info("Run `genex preview` (or `publish`) to rebuild \u2014 the new slug is baked into the bundle.");
|
|
2377
2056
|
}
|
|
2378
2057
|
async function rewriteSlugEnv(from, to, log, cwd = process.cwd()) {
|
|
2379
|
-
const file =
|
|
2058
|
+
const file = path12.join(cwd, ".env");
|
|
2380
2059
|
let content;
|
|
2381
2060
|
try {
|
|
2382
|
-
content = await
|
|
2061
|
+
content = await fs12.readFile(file, "utf8");
|
|
2383
2062
|
} catch {
|
|
2384
2063
|
return;
|
|
2385
2064
|
}
|
|
2386
2065
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
2387
2066
|
if (!re.test(content)) return;
|
|
2388
|
-
await
|
|
2067
|
+
await fs12.writeFile(file, content.replace(re, `$1${to}`));
|
|
2389
2068
|
log.dim(` .env: VITE_GENEX_SLUG=${to} (was ${from})`);
|
|
2390
2069
|
}
|
|
2391
2070
|
async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
2392
|
-
const file =
|
|
2071
|
+
const file = path12.join(cwd, "src", "genex.config.ts");
|
|
2393
2072
|
let content;
|
|
2394
2073
|
try {
|
|
2395
|
-
content = await
|
|
2074
|
+
content = await fs12.readFile(file, "utf8");
|
|
2396
2075
|
} catch {
|
|
2397
2076
|
return;
|
|
2398
2077
|
}
|
|
@@ -2402,7 +2081,7 @@ async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
|
2402
2081
|
log.warn(` src/genex.config.ts has no "${from}" literal \u2014 check its slug by hand.`);
|
|
2403
2082
|
return;
|
|
2404
2083
|
}
|
|
2405
|
-
await
|
|
2084
|
+
await fs12.writeFile(file, content.replace(quoted, `"${to}"`));
|
|
2406
2085
|
log.dim(` src/genex.config.ts: baked slug -> ${to}`);
|
|
2407
2086
|
}
|
|
2408
2087
|
|
|
@@ -2530,11 +2209,11 @@ function relTime(iso) {
|
|
|
2530
2209
|
}
|
|
2531
2210
|
|
|
2532
2211
|
// src/lib/deploy.ts
|
|
2533
|
-
import "child_process";
|
|
2212
|
+
import { spawn as spawn3 } from "child_process";
|
|
2534
2213
|
import crypto3 from "crypto";
|
|
2535
|
-
import
|
|
2536
|
-
import
|
|
2537
|
-
import
|
|
2214
|
+
import fs15 from "fs/promises";
|
|
2215
|
+
import os6 from "os";
|
|
2216
|
+
import path14 from "path";
|
|
2538
2217
|
|
|
2539
2218
|
// ../../packages/mobile-scan/src/image-dims.ts
|
|
2540
2219
|
function u32be(b, o) {
|
|
@@ -2798,12 +2477,12 @@ function tierFor(estVramMb) {
|
|
|
2798
2477
|
}
|
|
2799
2478
|
|
|
2800
2479
|
// src/commands/ui.ts
|
|
2801
|
-
import
|
|
2802
|
-
import
|
|
2480
|
+
import fs14 from "fs/promises";
|
|
2481
|
+
import path13 from "path";
|
|
2803
2482
|
import { PNG as PNG2 } from "pngjs";
|
|
2804
2483
|
|
|
2805
2484
|
// src/lib/png-tools.ts
|
|
2806
|
-
import
|
|
2485
|
+
import fs13 from "fs/promises";
|
|
2807
2486
|
import { PNG } from "pngjs";
|
|
2808
2487
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
2809
2488
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -2814,12 +2493,12 @@ async function loadPng(input) {
|
|
|
2814
2493
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
2815
2494
|
buf = Buffer.from(await res.arrayBuffer());
|
|
2816
2495
|
} else {
|
|
2817
|
-
buf = await
|
|
2496
|
+
buf = await fs13.readFile(input);
|
|
2818
2497
|
}
|
|
2819
2498
|
return PNG.sync.read(buf);
|
|
2820
2499
|
}
|
|
2821
2500
|
async function writePng(file, png) {
|
|
2822
|
-
await
|
|
2501
|
+
await fs13.writeFile(file, PNG.sync.write(png));
|
|
2823
2502
|
}
|
|
2824
2503
|
function cropPng(image, box) {
|
|
2825
2504
|
const out = new PNG({ width: box.w, height: box.h });
|
|
@@ -3119,7 +2798,7 @@ async function uiExtract(opts, log) {
|
|
|
3119
2798
|
const dilatePx = opts.dilate ?? 0;
|
|
3120
2799
|
const sheet = await loadPng(input);
|
|
3121
2800
|
const { width: W, height: H, data } = sheet;
|
|
3122
|
-
await
|
|
2801
|
+
await fs14.mkdir(outDir, { recursive: true });
|
|
3123
2802
|
log.plain(c.bold("genex ui extract"));
|
|
3124
2803
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
|
|
3125
2804
|
let hasTransparency = false;
|
|
@@ -3348,7 +3027,7 @@ async function uiExtract(opts, log) {
|
|
|
3348
3027
|
rimPixels: speckle.sampled
|
|
3349
3028
|
});
|
|
3350
3029
|
}
|
|
3351
|
-
const outPath =
|
|
3030
|
+
const outPath = path13.join(outDir, `${name}.png`);
|
|
3352
3031
|
await writePng(outPath, out);
|
|
3353
3032
|
const sidecar = {
|
|
3354
3033
|
name,
|
|
@@ -3367,7 +3046,7 @@ async function uiExtract(opts, log) {
|
|
|
3367
3046
|
defringed
|
|
3368
3047
|
};
|
|
3369
3048
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
3370
|
-
await
|
|
3049
|
+
await fs14.writeFile(
|
|
3371
3050
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
3372
3051
|
JSON.stringify(sidecarBody, null, 2)
|
|
3373
3052
|
);
|
|
@@ -3376,8 +3055,8 @@ async function uiExtract(opts, log) {
|
|
|
3376
3055
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
3377
3056
|
);
|
|
3378
3057
|
}
|
|
3379
|
-
const debugPath =
|
|
3380
|
-
await
|
|
3058
|
+
const debugPath = path13.join(outDir, "extract-debug.json");
|
|
3059
|
+
await fs14.writeFile(
|
|
3381
3060
|
debugPath,
|
|
3382
3061
|
JSON.stringify(
|
|
3383
3062
|
{
|
|
@@ -3839,7 +3518,7 @@ async function uiMasks(opts, log) {
|
|
|
3839
3518
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
3840
3519
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
3841
3520
|
const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
|
|
3842
|
-
await
|
|
3521
|
+
await fs14.mkdir(outDir, { recursive: true });
|
|
3843
3522
|
log.plain(c.bold("genex ui masks"));
|
|
3844
3523
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
3845
3524
|
const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
|
|
@@ -3892,11 +3571,11 @@ async function uiMasks(opts, log) {
|
|
|
3892
3571
|
});
|
|
3893
3572
|
}
|
|
3894
3573
|
const overlay = makeOverlay(clean2, converted.png);
|
|
3895
|
-
const framePath =
|
|
3896
|
-
const maskPath =
|
|
3897
|
-
const annotatedPath =
|
|
3898
|
-
const overlayPath =
|
|
3899
|
-
const metaPath =
|
|
3574
|
+
const framePath = path13.join(outDir, `${pair.name}-frame.png`);
|
|
3575
|
+
const maskPath = path13.join(outDir, `${pair.name}-mask.png`);
|
|
3576
|
+
const annotatedPath = path13.join(outDir, `${pair.name}-annotated-source.png`);
|
|
3577
|
+
const overlayPath = path13.join(outDir, `${pair.name}-overlay.png`);
|
|
3578
|
+
const metaPath = path13.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
3900
3579
|
await writePng(framePath, clean2);
|
|
3901
3580
|
await writePng(maskPath, converted.png);
|
|
3902
3581
|
await writePng(annotatedPath, annotated);
|
|
@@ -3943,7 +3622,7 @@ async function uiMasks(opts, log) {
|
|
|
3943
3622
|
},
|
|
3944
3623
|
overlay: overlayPath
|
|
3945
3624
|
};
|
|
3946
|
-
await
|
|
3625
|
+
await fs14.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
3947
3626
|
`);
|
|
3948
3627
|
results.push(meta);
|
|
3949
3628
|
const fb = converted.bbox;
|
|
@@ -3959,8 +3638,8 @@ async function uiMasks(opts, log) {
|
|
|
3959
3638
|
);
|
|
3960
3639
|
}
|
|
3961
3640
|
}
|
|
3962
|
-
const indexPath =
|
|
3963
|
-
await
|
|
3641
|
+
const indexPath = path13.join(outDir, "annotated-progress.json");
|
|
3642
|
+
await fs14.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
|
|
3964
3643
|
`);
|
|
3965
3644
|
log.plain("");
|
|
3966
3645
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -4086,7 +3765,7 @@ async function uiTextColor(opts, log) {
|
|
|
4086
3765
|
};
|
|
4087
3766
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4088
3767
|
`);
|
|
4089
|
-
if (opts.out) await
|
|
3768
|
+
if (opts.out) await fs14.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
4090
3769
|
`);
|
|
4091
3770
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
4092
3771
|
}
|
|
@@ -4118,7 +3797,7 @@ async function uiTrim(opts, log) {
|
|
|
4118
3797
|
const sidecar = computeBBoxes(trimmed);
|
|
4119
3798
|
const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
|
|
4120
3799
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
4121
|
-
await
|
|
3800
|
+
await fs14.writeFile(
|
|
4122
3801
|
sidecarPath,
|
|
4123
3802
|
JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
|
|
4124
3803
|
);
|
|
@@ -4225,7 +3904,7 @@ async function uiPlate(opts, log) {
|
|
|
4225
3904
|
fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
|
|
4226
3905
|
}
|
|
4227
3906
|
await writePng(outPath, out);
|
|
4228
|
-
const name =
|
|
3907
|
+
const name = path13.basename(outPath);
|
|
4229
3908
|
log.plain(c.bold("genex ui plate"));
|
|
4230
3909
|
log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
|
|
4231
3910
|
log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
|
|
@@ -4390,13 +4069,13 @@ async function walkFiles(dir) {
|
|
|
4390
4069
|
const out = [];
|
|
4391
4070
|
let entries;
|
|
4392
4071
|
try {
|
|
4393
|
-
entries = await
|
|
4072
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
4394
4073
|
} catch {
|
|
4395
4074
|
return out;
|
|
4396
4075
|
}
|
|
4397
4076
|
for (const entry of entries) {
|
|
4398
4077
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
4399
|
-
const p =
|
|
4078
|
+
const p = path13.join(dir, entry.name);
|
|
4400
4079
|
if (entry.isDirectory()) out.push(...await walkFiles(p));
|
|
4401
4080
|
else out.push(p);
|
|
4402
4081
|
}
|
|
@@ -4405,7 +4084,7 @@ async function walkFiles(dir) {
|
|
|
4405
4084
|
async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
4406
4085
|
const viewportFindings = [];
|
|
4407
4086
|
try {
|
|
4408
|
-
const indexHtml = await
|
|
4087
|
+
const indexHtml = await fs14.readFile(path13.join(cwd, "index.html"), "utf8");
|
|
4409
4088
|
if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
|
|
4410
4089
|
viewportFindings.push({
|
|
4411
4090
|
kind: "viewport-meta",
|
|
@@ -4419,28 +4098,28 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4419
4098
|
}
|
|
4420
4099
|
} catch {
|
|
4421
4100
|
}
|
|
4422
|
-
const absAssets =
|
|
4101
|
+
const absAssets = path13.resolve(cwd, assetDir);
|
|
4423
4102
|
try {
|
|
4424
|
-
if (!(await
|
|
4103
|
+
if (!(await fs14.stat(absAssets)).isDirectory()) {
|
|
4425
4104
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
4426
4105
|
}
|
|
4427
4106
|
} catch {
|
|
4428
4107
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
4429
4108
|
}
|
|
4430
|
-
const srcFiles = (await walkFiles(
|
|
4431
|
-
(p) => AUDIT_SRC_EXTS.has(
|
|
4109
|
+
const srcFiles = (await walkFiles(path13.resolve(cwd, srcDir))).filter(
|
|
4110
|
+
(p) => AUDIT_SRC_EXTS.has(path13.extname(p).toLowerCase())
|
|
4432
4111
|
);
|
|
4433
4112
|
try {
|
|
4434
|
-
for (const name of await
|
|
4435
|
-
const ext =
|
|
4436
|
-
if (ext === ".html" || ext === ".css") srcFiles.push(
|
|
4113
|
+
for (const name of await fs14.readdir(cwd)) {
|
|
4114
|
+
const ext = path13.extname(name).toLowerCase();
|
|
4115
|
+
if (ext === ".html" || ext === ".css") srcFiles.push(path13.join(cwd, name));
|
|
4437
4116
|
}
|
|
4438
4117
|
} catch {
|
|
4439
4118
|
}
|
|
4440
4119
|
const sources = [];
|
|
4441
4120
|
for (const p of srcFiles) {
|
|
4442
4121
|
try {
|
|
4443
|
-
sources.push({ rel:
|
|
4122
|
+
sources.push({ rel: path13.relative(cwd, p), text: await fs14.readFile(p, "utf8") });
|
|
4444
4123
|
} catch {
|
|
4445
4124
|
}
|
|
4446
4125
|
}
|
|
@@ -4450,11 +4129,11 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4450
4129
|
const metaByName = /* @__PURE__ */ new Map();
|
|
4451
4130
|
const bboxByPng = /* @__PURE__ */ new Map();
|
|
4452
4131
|
for (const p of assetFiles) {
|
|
4453
|
-
const base =
|
|
4132
|
+
const base = path13.basename(p);
|
|
4454
4133
|
const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
|
|
4455
4134
|
if (metaMatch) {
|
|
4456
4135
|
try {
|
|
4457
|
-
const meta = JSON.parse(await
|
|
4136
|
+
const meta = JSON.parse(await fs14.readFile(p, "utf8"));
|
|
4458
4137
|
metaByName.set(metaMatch[1], {
|
|
4459
4138
|
cleanCrop: meta.clean?.crop ?? null,
|
|
4460
4139
|
loosened: meta.loosened === true
|
|
@@ -4465,7 +4144,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4465
4144
|
}
|
|
4466
4145
|
if (base.endsWith(".bbox.json")) {
|
|
4467
4146
|
try {
|
|
4468
|
-
const sidecar = JSON.parse(await
|
|
4147
|
+
const sidecar = JSON.parse(await fs14.readFile(p, "utf8"));
|
|
4469
4148
|
if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
|
|
4470
4149
|
} catch {
|
|
4471
4150
|
}
|
|
@@ -4474,7 +4153,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4474
4153
|
for (const [name, meta] of metaByName) {
|
|
4475
4154
|
if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
|
|
4476
4155
|
for (const p of assetFiles) {
|
|
4477
|
-
const base =
|
|
4156
|
+
const base = path13.basename(p);
|
|
4478
4157
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4479
4158
|
if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
|
|
4480
4159
|
if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
|
|
@@ -4498,7 +4177,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4498
4177
|
}
|
|
4499
4178
|
const pngByBase = /* @__PURE__ */ new Map();
|
|
4500
4179
|
for (const p of assetFiles) {
|
|
4501
|
-
const base =
|
|
4180
|
+
const base = path13.basename(p);
|
|
4502
4181
|
if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
|
|
4503
4182
|
}
|
|
4504
4183
|
for (const [maskBase, maskPath] of pngByBase) {
|
|
@@ -4511,8 +4190,8 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4511
4190
|
let frame;
|
|
4512
4191
|
let mask;
|
|
4513
4192
|
try {
|
|
4514
|
-
frame = PNG2.sync.read(await
|
|
4515
|
-
mask = PNG2.sync.read(await
|
|
4193
|
+
frame = PNG2.sync.read(await fs14.readFile(framePath));
|
|
4194
|
+
mask = PNG2.sync.read(await fs14.readFile(maskPath));
|
|
4516
4195
|
} catch {
|
|
4517
4196
|
continue;
|
|
4518
4197
|
}
|
|
@@ -4531,7 +4210,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4531
4210
|
if (!referenced(base)) continue;
|
|
4532
4211
|
let png;
|
|
4533
4212
|
try {
|
|
4534
|
-
png = PNG2.sync.read(await
|
|
4213
|
+
png = PNG2.sync.read(await fs14.readFile(p));
|
|
4535
4214
|
} catch {
|
|
4536
4215
|
continue;
|
|
4537
4216
|
}
|
|
@@ -4551,7 +4230,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4551
4230
|
}
|
|
4552
4231
|
const maskReported = /* @__PURE__ */ new Set();
|
|
4553
4232
|
for (const p of assetFiles) {
|
|
4554
|
-
const m = /^(.+)\.annotated-progress\.json$/.exec(
|
|
4233
|
+
const m = /^(.+)\.annotated-progress\.json$/.exec(path13.basename(p));
|
|
4555
4234
|
if (!m) continue;
|
|
4556
4235
|
const maskBase = `${m[1]}-mask.png`;
|
|
4557
4236
|
if (!referenced(maskBase)) {
|
|
@@ -4563,14 +4242,14 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4563
4242
|
}
|
|
4564
4243
|
}
|
|
4565
4244
|
for (const p of assetFiles) {
|
|
4566
|
-
const base =
|
|
4245
|
+
const base = path13.basename(p);
|
|
4567
4246
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4568
4247
|
if (/-annotated-source\.png$|-overlay\.png$/.test(base)) continue;
|
|
4569
4248
|
if (maskReported.has(base)) continue;
|
|
4570
4249
|
if (!referenced(base)) {
|
|
4571
4250
|
findings.push({
|
|
4572
4251
|
kind: "unwired-sprite",
|
|
4573
|
-
message: `${
|
|
4252
|
+
message: `${path13.relative(cwd, p)} is on disk but never referenced in ${srcDir}/index.html/CSS \u2014 wire it, or record the one-line reason it was cut. (Computed-string references are invisible here \u2014 check.)`
|
|
4574
4253
|
});
|
|
4575
4254
|
}
|
|
4576
4255
|
}
|
|
@@ -4623,6 +4302,25 @@ async function uiAudit(opts, log) {
|
|
|
4623
4302
|
}
|
|
4624
4303
|
|
|
4625
4304
|
// src/lib/deploy.ts
|
|
4305
|
+
var WIN_SHELL_COMMANDS = /* @__PURE__ */ new Set(["npm", "npx"]);
|
|
4306
|
+
function run(cmd, args, env) {
|
|
4307
|
+
const shell = process.platform === "win32" && WIN_SHELL_COMMANDS.has(cmd);
|
|
4308
|
+
return new Promise((resolve) => {
|
|
4309
|
+
let child;
|
|
4310
|
+
try {
|
|
4311
|
+
child = spawn3(cmd, args, { env: env ? { ...process.env, ...env } : process.env, shell });
|
|
4312
|
+
} catch {
|
|
4313
|
+
resolve({ code: -1, out: "", err: `${cmd} not found` });
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
let out = "";
|
|
4317
|
+
let err = "";
|
|
4318
|
+
child.stdout?.on("data", (d) => out += String(d));
|
|
4319
|
+
child.stderr?.on("data", (d) => err += String(d));
|
|
4320
|
+
child.on("error", () => resolve({ code: -1, out, err: `${cmd} not found` }));
|
|
4321
|
+
child.on("close", (code2) => resolve({ code: code2 ?? -1, out, err }));
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4626
4324
|
function printMobilePreflight(files, log) {
|
|
4627
4325
|
try {
|
|
4628
4326
|
const scan = scanBundle(files.map((f) => ({ relPath: f.relPath, bytes: f.bytes })));
|
|
@@ -4685,7 +4383,7 @@ async function printUiAuditPreflight(log) {
|
|
|
4685
4383
|
}
|
|
4686
4384
|
async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
4687
4385
|
try {
|
|
4688
|
-
const design = await
|
|
4386
|
+
const design = await fs15.readFile(path14.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
|
|
4689
4387
|
const warnings = [];
|
|
4690
4388
|
if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
|
|
4691
4389
|
warnings.push(
|
|
@@ -4693,7 +4391,7 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4693
4391
|
);
|
|
4694
4392
|
}
|
|
4695
4393
|
if (!/player character:/i.test(design)) {
|
|
4696
|
-
const hasCharacter = await
|
|
4394
|
+
const hasCharacter = await fs15.access(path14.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
|
|
4697
4395
|
if (!hasCharacter && await loadsPlayerBody(cwd)) {
|
|
4698
4396
|
warnings.push(
|
|
4699
4397
|
`Player character is the stock avatar \u2014 no generated character is wired. The game's own generated character is the player's body wherever a human body appears on screen, first-person included (genex-ai-character). Generate it, or record "Player character: VRM \u2014 <reason>" in DESIGN.md (no human body in this game / out of credits / player declined).`
|
|
@@ -4707,12 +4405,12 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4707
4405
|
async function loadsPlayerBody(cwd) {
|
|
4708
4406
|
const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
|
|
4709
4407
|
try {
|
|
4710
|
-
const entries = await
|
|
4408
|
+
const entries = await fs15.readdir(path14.join(cwd, "src"), { recursive: true });
|
|
4711
4409
|
for (const rel of entries) {
|
|
4712
4410
|
if (rel.includes("node_modules")) continue;
|
|
4713
|
-
if (rel.split(
|
|
4411
|
+
if (rel.split(path14.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
|
|
4714
4412
|
if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
|
|
4715
|
-
const text = await
|
|
4413
|
+
const text = await fs15.readFile(path14.join(cwd, "src", rel), "utf8").catch(() => "");
|
|
4716
4414
|
if (BODY_LOADERS.test(text)) return true;
|
|
4717
4415
|
}
|
|
4718
4416
|
} catch {
|
|
@@ -4725,15 +4423,6 @@ function isSecretEnvFile(name) {
|
|
|
4725
4423
|
}
|
|
4726
4424
|
async function deployGame(ctx, opts, log) {
|
|
4727
4425
|
const cwd = process.cwd();
|
|
4728
|
-
const meta = await readProject(cwd);
|
|
4729
|
-
const expectStagingCommit = opts.force ? void 0 : meta?.stagingCommit;
|
|
4730
|
-
if (expectStagingCommit) {
|
|
4731
|
-
const remote = await readRemoteSource(ctx.apiUrl, ctx.token, ctx.projectId);
|
|
4732
|
-
if (remote && isStale(expectStagingCommit, remote.stagingCommit)) {
|
|
4733
|
-
reportStale(log, ctx.slug ?? meta?.slug ?? "this game", expectStagingCommit, remote.stagingCommit);
|
|
4734
|
-
return false;
|
|
4735
|
-
}
|
|
4736
|
-
}
|
|
4737
4426
|
if (!opts.noBuild && await hasBuildScript(cwd)) {
|
|
4738
4427
|
log.step("Building the production bundle\u2026");
|
|
4739
4428
|
const built = await run("npm", ["run", "build"]);
|
|
@@ -4745,9 +4434,9 @@ async function deployGame(ctx, opts, log) {
|
|
|
4745
4434
|
}
|
|
4746
4435
|
log.success("Built.");
|
|
4747
4436
|
}
|
|
4748
|
-
const distDir =
|
|
4437
|
+
const distDir = path14.join(cwd, "dist");
|
|
4749
4438
|
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
4750
|
-
const rel =
|
|
4439
|
+
const rel = path14.relative(cwd, siteDir) || ".";
|
|
4751
4440
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
4752
4441
|
const files = await collectFiles(siteDir);
|
|
4753
4442
|
if (files.length === 0) {
|
|
@@ -4803,28 +4492,10 @@ async function deployGame(ctx, opts, log) {
|
|
|
4803
4492
|
log.error("Couldn't upload your game \u2014 please try again.");
|
|
4804
4493
|
return false;
|
|
4805
4494
|
}
|
|
4806
|
-
|
|
4807
|
-
cwd,
|
|
4808
|
-
ctx,
|
|
4809
|
-
log,
|
|
4810
|
-
opts.channel === "staging" ? "preview" : "main",
|
|
4811
|
-
expectStagingCommit
|
|
4812
|
-
);
|
|
4813
|
-
if (pushed !== true) return false;
|
|
4495
|
+
if (!await pushSource(cwd, ctx, log, opts.channel === "staging" ? "preview" : "main")) return false;
|
|
4814
4496
|
log.step("Publishing\u2026");
|
|
4815
4497
|
const published = await callPublish(ctx, commit, opts, log);
|
|
4816
4498
|
if (!published) return false;
|
|
4817
|
-
if (opts.channel === "staging") {
|
|
4818
|
-
const tree = await sourceTreeHash(cwd);
|
|
4819
|
-
const current = await readProject(cwd);
|
|
4820
|
-
if (current) {
|
|
4821
|
-
await writeProject(
|
|
4822
|
-
{ ...current, stagingCommit: commit, ...tree ? { sourceTree: tree } : {} },
|
|
4823
|
-
cwd
|
|
4824
|
-
).catch(() => {
|
|
4825
|
-
});
|
|
4826
|
-
}
|
|
4827
|
-
}
|
|
4828
4499
|
const index = files.find((f) => f.relPath === "index.html");
|
|
4829
4500
|
const liveUrl = published.url || grant.playUrl;
|
|
4830
4501
|
await waitUntilLive(liveUrl, fingerprintOf(index.bytes.toString("utf8")), opts.liveTimeoutMs ?? 2e4, log);
|
|
@@ -4832,7 +4503,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
4832
4503
|
}
|
|
4833
4504
|
async function hasBuildScript(cwd) {
|
|
4834
4505
|
try {
|
|
4835
|
-
const pkg = JSON.parse(await
|
|
4506
|
+
const pkg = JSON.parse(await fs15.readFile(path14.join(cwd, "package.json"), "utf8"));
|
|
4836
4507
|
return Boolean(pkg.scripts?.build);
|
|
4837
4508
|
} catch {
|
|
4838
4509
|
return false;
|
|
@@ -4841,12 +4512,12 @@ async function hasBuildScript(cwd) {
|
|
|
4841
4512
|
async function collectFiles(root) {
|
|
4842
4513
|
const out = [];
|
|
4843
4514
|
const walk2 = async (dir, prefix) => {
|
|
4844
|
-
for (const e of await
|
|
4515
|
+
for (const e of await fs15.readdir(dir, { withFileTypes: true })) {
|
|
4845
4516
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
4846
4517
|
if (e.isDirectory()) {
|
|
4847
|
-
if (!EXCLUDE_DIRS.has(e.name)) await walk2(
|
|
4518
|
+
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path14.join(dir, e.name), relPath);
|
|
4848
4519
|
} else if (e.isFile() && !isSecretEnvFile(e.name)) {
|
|
4849
|
-
out.push({ relPath, bytes: await
|
|
4520
|
+
out.push({ relPath, bytes: await fs15.readFile(path14.join(dir, e.name)) });
|
|
4850
4521
|
}
|
|
4851
4522
|
}
|
|
4852
4523
|
};
|
|
@@ -5063,27 +4734,28 @@ async function callPublish(ctx, commit, opts, log) {
|
|
|
5063
4734
|
}
|
|
5064
4735
|
return { url: body?.url ?? "" };
|
|
5065
4736
|
}
|
|
5066
|
-
async function pushSource(cwd, ctx, log, branch = "main"
|
|
5067
|
-
const target = await fetchPushUrl(ctx, log
|
|
5068
|
-
if (target === "stale") return "stale";
|
|
4737
|
+
async function pushSource(cwd, ctx, log, branch = "main") {
|
|
4738
|
+
const target = await fetchPushUrl(ctx, log);
|
|
5069
4739
|
if (!target) return false;
|
|
5070
4740
|
const ref = target.managed ? branch : "main";
|
|
5071
4741
|
if (await pushWorktree(cwd, target.pushUrl, target.managed, log, ref)) return true;
|
|
5072
4742
|
if (!target.managed) return false;
|
|
5073
4743
|
log.info("Retrying the source push\u2026");
|
|
5074
4744
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
5075
|
-
const fresh = await fetchPushUrl(ctx, log
|
|
5076
|
-
if (fresh === "stale") return "stale";
|
|
4745
|
+
const fresh = await fetchPushUrl(ctx, log);
|
|
5077
4746
|
if (!fresh) return false;
|
|
5078
4747
|
return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log, ref);
|
|
5079
4748
|
}
|
|
4749
|
+
function urlHasEmbeddedCredentials(pushUrl) {
|
|
4750
|
+
return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(pushUrl);
|
|
4751
|
+
}
|
|
5080
4752
|
async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
5081
4753
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
5082
4754
|
const failed = () => {
|
|
5083
4755
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
5084
4756
|
return false;
|
|
5085
4757
|
};
|
|
5086
|
-
const gitDir = await
|
|
4758
|
+
const gitDir = await fs15.mkdtemp(path14.join(os6.tmpdir(), "genex-source-"));
|
|
5087
4759
|
const base = { GIT_DIR: gitDir };
|
|
5088
4760
|
if (urlHasEmbeddedCredentials(pushUrl)) {
|
|
5089
4761
|
base.GIT_CONFIG_COUNT = "1";
|
|
@@ -5098,12 +4770,12 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
5098
4770
|
};
|
|
5099
4771
|
try {
|
|
5100
4772
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
5101
|
-
await
|
|
5102
|
-
|
|
4773
|
+
await fs15.writeFile(
|
|
4774
|
+
path14.join(gitDir, "info", "exclude"),
|
|
5103
4775
|
// .env* are secrets — never publish them; `!` keeps the non-secret template.
|
|
5104
4776
|
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
5105
4777
|
);
|
|
5106
|
-
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE:
|
|
4778
|
+
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path14.join(gitDir, "index-source") };
|
|
5107
4779
|
let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
|
|
5108
4780
|
if (!lfs) {
|
|
5109
4781
|
log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
|
|
@@ -5154,19 +4826,16 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
5154
4826
|
} catch {
|
|
5155
4827
|
return failed();
|
|
5156
4828
|
} finally {
|
|
5157
|
-
await
|
|
4829
|
+
await fs15.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
5158
4830
|
});
|
|
5159
4831
|
}
|
|
5160
4832
|
}
|
|
5161
|
-
async function fetchPushUrl(ctx, log
|
|
4833
|
+
async function fetchPushUrl(ctx, log) {
|
|
5162
4834
|
let res;
|
|
5163
4835
|
try {
|
|
5164
4836
|
res = await apiFetch(`${ctx.apiUrl}/api/projects/${ctx.projectId}/push-token`, {
|
|
5165
4837
|
method: "POST",
|
|
5166
|
-
headers: {
|
|
5167
|
-
Authorization: `Bearer ${ctx.token}`,
|
|
5168
|
-
...expectStagingCommit ? { "If-Match": expectStagingCommit } : {}
|
|
5169
|
-
}
|
|
4838
|
+
headers: { Authorization: `Bearer ${ctx.token}` }
|
|
5170
4839
|
});
|
|
5171
4840
|
} catch (err) {
|
|
5172
4841
|
log.error(`Couldn't reach the API to authorize the source push: ${String(err)}`);
|
|
@@ -5176,11 +4845,6 @@ async function fetchPushUrl(ctx, log, expectStagingCommit) {
|
|
|
5176
4845
|
log.error("Not authorized \u2014 your token may have expired. Re-run `genex init`.");
|
|
5177
4846
|
return null;
|
|
5178
4847
|
}
|
|
5179
|
-
if (res.status === 409) {
|
|
5180
|
-
const body = await res.json().catch(() => null);
|
|
5181
|
-
reportStale(log, ctx.slug ?? "this game", expectStagingCommit, body?.stagingCommit ?? null);
|
|
5182
|
-
return "stale";
|
|
5183
|
-
}
|
|
5184
4848
|
if (res.status === 429) {
|
|
5185
4849
|
log.error(`Rate limited authorizing the source push (HTTP 429).${retryAfterHint(res)}`);
|
|
5186
4850
|
return null;
|
|
@@ -5194,11 +4858,11 @@ async function fetchPushUrl(ctx, log, expectStagingCommit) {
|
|
|
5194
4858
|
log.error("The API didn't return a push URL.");
|
|
5195
4859
|
return null;
|
|
5196
4860
|
}
|
|
5197
|
-
return { pushUrl: data.pushUrl, managed: data.managed !== false
|
|
4861
|
+
return { pushUrl: data.pushUrl, managed: data.managed !== false };
|
|
5198
4862
|
}
|
|
5199
4863
|
async function isDir2(p) {
|
|
5200
4864
|
try {
|
|
5201
|
-
return (await
|
|
4865
|
+
return (await fs15.stat(p)).isDirectory();
|
|
5202
4866
|
} catch {
|
|
5203
4867
|
return false;
|
|
5204
4868
|
}
|
|
@@ -5299,318 +4963,6 @@ async function runMakeRemixable(opts) {
|
|
|
5299
4963
|
}
|
|
5300
4964
|
}
|
|
5301
4965
|
|
|
5302
|
-
// src/commands/domain.ts
|
|
5303
|
-
var SUBCOMMANDS = ["add", "list", "verify", "remove"];
|
|
5304
|
-
function statusWord(d) {
|
|
5305
|
-
if (d.status === "active") return c.green("live");
|
|
5306
|
-
if (d.status === "failed") return c.red("stopped");
|
|
5307
|
-
return c.dim("waiting for DNS");
|
|
5308
|
-
}
|
|
5309
|
-
async function runDomain(opts) {
|
|
5310
|
-
const log = createLogger({ quiet: opts.quiet });
|
|
5311
|
-
const sub = (opts.name ?? "list").trim();
|
|
5312
|
-
if (!SUBCOMMANDS.includes(sub)) {
|
|
5313
|
-
log.error(`Unknown subcommand \`${sub}\`. Use: ${SUBCOMMANDS.join(", ")}.`);
|
|
5314
|
-
process.exitCode = 1;
|
|
5315
|
-
return;
|
|
5316
|
-
}
|
|
5317
|
-
const needsHost = sub !== "list";
|
|
5318
|
-
const hostname = opts.hostname?.trim();
|
|
5319
|
-
if (needsHost && !hostname) {
|
|
5320
|
-
log.error(`\`genex domain ${sub}\` needs a hostname, e.g. ${c.cyan(`genex domain ${sub} play.yourdomain.com`)}.`);
|
|
5321
|
-
process.exitCode = 1;
|
|
5322
|
-
return;
|
|
5323
|
-
}
|
|
5324
|
-
const meta = await readProject();
|
|
5325
|
-
if (!meta?.id) {
|
|
5326
|
-
log.error("This folder isn't linked to a game.");
|
|
5327
|
-
log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
|
|
5328
|
-
process.exitCode = 1;
|
|
5329
|
-
return;
|
|
5330
|
-
}
|
|
5331
|
-
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
5332
|
-
let token = opts.token ?? await readUserToken(opts.envPath);
|
|
5333
|
-
if (!token) {
|
|
5334
|
-
if (opts.noAuth) {
|
|
5335
|
-
log.error("Not signed in. Re-run without --no-auth to connect.");
|
|
5336
|
-
process.exitCode = 1;
|
|
5337
|
-
return;
|
|
5338
|
-
}
|
|
5339
|
-
log.plain("Not signed in \u2014 connecting\u2026");
|
|
5340
|
-
try {
|
|
5341
|
-
token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
|
|
5342
|
-
log,
|
|
5343
|
-
inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
|
|
5344
|
-
});
|
|
5345
|
-
} catch (err) {
|
|
5346
|
-
if (err instanceof AuthPendingError) {
|
|
5347
|
-
printAuthHandoff(log, err);
|
|
5348
|
-
log.dim(`Then re-run ${c.cyan(`genex domain ${sub}`)}.`);
|
|
5349
|
-
return;
|
|
5350
|
-
}
|
|
5351
|
-
log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
5352
|
-
process.exitCode = 1;
|
|
5353
|
-
return;
|
|
5354
|
-
}
|
|
5355
|
-
await writeUserToken(token, opts.envPath);
|
|
5356
|
-
}
|
|
5357
|
-
const base = `${apiUrl}/api/projects/${encodeURIComponent(meta.id)}/domains`;
|
|
5358
|
-
const auth = { Authorization: `Bearer ${token}` };
|
|
5359
|
-
const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, "Content-Type": "application/json", ...init2?.headers ?? {} } });
|
|
5360
|
-
let res;
|
|
5361
|
-
try {
|
|
5362
|
-
if (sub === "list") res = await call(base);
|
|
5363
|
-
else if (sub === "add") res = await call(base, { method: "POST", body: JSON.stringify({ hostname }) });
|
|
5364
|
-
else if (sub === "verify")
|
|
5365
|
-
res = await call(`${base}/${encodeURIComponent(hostname)}/verify`, { method: "POST" });
|
|
5366
|
-
else res = await call(`${base}/${encodeURIComponent(hostname)}`, { method: "DELETE" });
|
|
5367
|
-
} catch (err) {
|
|
5368
|
-
log.error(`Couldn't reach the API at ${apiUrl}.`);
|
|
5369
|
-
log.dim(` ${String(err)}`);
|
|
5370
|
-
process.exitCode = 1;
|
|
5371
|
-
return;
|
|
5372
|
-
}
|
|
5373
|
-
if (!res.ok) {
|
|
5374
|
-
if (await printedStructuredError(res)) {
|
|
5375
|
-
process.exitCode = 1;
|
|
5376
|
-
return;
|
|
5377
|
-
}
|
|
5378
|
-
const body = await res.json().catch(() => null);
|
|
5379
|
-
if (res.status === 503) {
|
|
5380
|
-
log.error("Custom domains aren't available on this Genex environment yet.");
|
|
5381
|
-
} else if (res.status === 404) {
|
|
5382
|
-
log.error(sub === "list" || sub === "add" ? "That game wasn't found on this account." : `${hostname} isn't connected to this game.`);
|
|
5383
|
-
} else {
|
|
5384
|
-
log.error(body?.message ?? body?.error ?? `Request failed (${res.status}).`);
|
|
5385
|
-
}
|
|
5386
|
-
process.exitCode = 1;
|
|
5387
|
-
return;
|
|
5388
|
-
}
|
|
5389
|
-
const data = await res.json();
|
|
5390
|
-
if (opts.json) {
|
|
5391
|
-
log.plain(JSON.stringify(data, null, 2));
|
|
5392
|
-
return;
|
|
5393
|
-
}
|
|
5394
|
-
if (sub === "list") {
|
|
5395
|
-
const rows = data.domains ?? [];
|
|
5396
|
-
if (rows.length === 0) {
|
|
5397
|
-
log.plain("No domains connected to this game.");
|
|
5398
|
-
log.dim(` ${c.cyan("genex domain add play.yourdomain.com")} to connect one.`);
|
|
5399
|
-
return;
|
|
5400
|
-
}
|
|
5401
|
-
for (const d of rows) log.plain(` ${d.hostname.padEnd(34)} ${statusWord(d)}`);
|
|
5402
|
-
return;
|
|
5403
|
-
}
|
|
5404
|
-
if (sub === "add") {
|
|
5405
|
-
if (data.supported === false) {
|
|
5406
|
-
log.plain(String(data.message ?? "That domain's DNS host doesn't support one-click setup."));
|
|
5407
|
-
const manual = data.manual;
|
|
5408
|
-
const records = Array.isArray(manual?.records) ? manual.records : [];
|
|
5409
|
-
if (records.length > 0) {
|
|
5410
|
-
log.plain("");
|
|
5411
|
-
log.plain(" Add these two records at your DNS host:");
|
|
5412
|
-
log.plain("");
|
|
5413
|
-
for (const r of records) {
|
|
5414
|
-
log.plain(` ${c.cyan(String(r.type ?? ""))} ${String(r.name ?? "")}`);
|
|
5415
|
-
log.plain(` ${String(r.value ?? "")}`);
|
|
5416
|
-
}
|
|
5417
|
-
log.plain("");
|
|
5418
|
-
if (manual?.apexHint) {
|
|
5419
|
-
log.dim(" At a root domain your host may call the first one ALIAS or ANAME.");
|
|
5420
|
-
}
|
|
5421
|
-
log.dim(` Then run: genex domain verify ${String(data.hostname ?? "")}`);
|
|
5422
|
-
log.dim(" DNS can take a few minutes to spread.");
|
|
5423
|
-
return;
|
|
5424
|
-
}
|
|
5425
|
-
log.dim(" Your game stays reachable at its usual address.");
|
|
5426
|
-
return;
|
|
5427
|
-
}
|
|
5428
|
-
const applyUrl = String(data.applyUrl ?? "");
|
|
5429
|
-
const providerName = String(data.providerName ?? "your DNS host");
|
|
5430
|
-
log.success(`${data.hostname} can be connected through ${providerName}.`);
|
|
5431
|
-
log.plain("");
|
|
5432
|
-
log.plain(` Approve the DNS change here: ${c.cyan(applyUrl)}`);
|
|
5433
|
-
log.plain("");
|
|
5434
|
-
log.dim(" One click writes the records. Nothing to copy or paste.");
|
|
5435
|
-
if (!applyUrl.startsWith("https://")) {
|
|
5436
|
-
log.warn("That DNS host returned a setup link we could not verify \u2014 not opening it.");
|
|
5437
|
-
} else if (!opts.noOpen) {
|
|
5438
|
-
openBrowser(applyUrl, () => log.dim(" (Couldn't open a browser \u2014 use the link above.)"));
|
|
5439
|
-
}
|
|
5440
|
-
log.dim(` Then ${c.cyan(`genex domain verify ${String(data.hostname)}`)} once you have approved it.`);
|
|
5441
|
-
return;
|
|
5442
|
-
}
|
|
5443
|
-
if (sub === "verify") {
|
|
5444
|
-
const status = String(data.status ?? "");
|
|
5445
|
-
if (status === "active") log.success(`${hostname} is live.`);
|
|
5446
|
-
else if (status === "failed") log.error(`${hostname} is stopped \u2014 its verification record is missing.`);
|
|
5447
|
-
else log.plain(`${hostname} isn't verified yet \u2014 DNS changes can take a few minutes to spread.`);
|
|
5448
|
-
return;
|
|
5449
|
-
}
|
|
5450
|
-
log.success(`${hostname} disconnected.`);
|
|
5451
|
-
}
|
|
5452
|
-
|
|
5453
|
-
// src/commands/shop.ts
|
|
5454
|
-
var SUBS = ["list", "add", "set", "remove", "test"];
|
|
5455
|
-
function money(cents) {
|
|
5456
|
-
return cents === void 0 ? "" : ` ($${(cents / 100).toFixed(2)})`;
|
|
5457
|
-
}
|
|
5458
|
-
async function runShop(opts) {
|
|
5459
|
-
const log = createLogger({ quiet: opts.quiet });
|
|
5460
|
-
const sub = opts.name?.trim() || "list";
|
|
5461
|
-
if (!SUBS.includes(sub)) {
|
|
5462
|
-
log.error(`Unknown subcommand ${c.cyan(sub)}. Use: ${SUBS.join(", ")}.`);
|
|
5463
|
-
process.exitCode = 1;
|
|
5464
|
-
return;
|
|
5465
|
-
}
|
|
5466
|
-
const meta = await readProject();
|
|
5467
|
-
if (!meta?.id) {
|
|
5468
|
-
log.error("This folder isn't linked to a game.");
|
|
5469
|
-
log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
|
|
5470
|
-
process.exitCode = 1;
|
|
5471
|
-
return;
|
|
5472
|
-
}
|
|
5473
|
-
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
5474
|
-
let token = opts.token ?? await readUserToken(opts.envPath);
|
|
5475
|
-
if (!token) {
|
|
5476
|
-
if (opts.noAuth) {
|
|
5477
|
-
log.error("Not signed in. Re-run without --no-auth to connect.");
|
|
5478
|
-
process.exitCode = 1;
|
|
5479
|
-
return;
|
|
5480
|
-
}
|
|
5481
|
-
log.plain("Not signed in \u2014 connecting\u2026");
|
|
5482
|
-
try {
|
|
5483
|
-
token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
|
|
5484
|
-
log,
|
|
5485
|
-
inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
|
|
5486
|
-
});
|
|
5487
|
-
} catch (err) {
|
|
5488
|
-
if (err instanceof AuthPendingError) {
|
|
5489
|
-
printAuthHandoff(log, err);
|
|
5490
|
-
log.dim(`Then re-run ${c.cyan(`genex shop ${sub}`)}.`);
|
|
5491
|
-
return;
|
|
5492
|
-
}
|
|
5493
|
-
log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
5494
|
-
process.exitCode = 1;
|
|
5495
|
-
return;
|
|
5496
|
-
}
|
|
5497
|
-
await writeUserToken(token, opts.envPath);
|
|
5498
|
-
}
|
|
5499
|
-
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
5500
|
-
const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, ...init2?.headers ?? {} } });
|
|
5501
|
-
const projectSkus = `${apiUrl}/api/coin/projects/${encodeURIComponent(meta.id)}/skus`;
|
|
5502
|
-
async function fail3(res2, what) {
|
|
5503
|
-
if (printedStructuredError(res2)) {
|
|
5504
|
-
process.exitCode = 1;
|
|
5505
|
-
return;
|
|
5506
|
-
}
|
|
5507
|
-
process.exitCode = 1;
|
|
5508
|
-
const body = await res2.json().catch(() => ({}));
|
|
5509
|
-
if (res2.status === 404 && body.error === "not_found") {
|
|
5510
|
-
log.error("In-game purchases aren't enabled on this environment yet.");
|
|
5511
|
-
return;
|
|
5512
|
-
}
|
|
5513
|
-
log.error(body.message ?? `Couldn't ${what} (HTTP ${res2.status}).`);
|
|
5514
|
-
if (body.error === "price_off_grid") {
|
|
5515
|
-
log.dim(` See the full list any time: ${c.cyan("genex shop list")}`);
|
|
5516
|
-
}
|
|
5517
|
-
}
|
|
5518
|
-
if (sub === "list") {
|
|
5519
|
-
const res2 = await call(projectSkus);
|
|
5520
|
-
if (!res2.ok) return fail3(res2, "read your shop");
|
|
5521
|
-
const body = await res2.json();
|
|
5522
|
-
if (opts.json) {
|
|
5523
|
-
log.plain(JSON.stringify(body, null, 2));
|
|
5524
|
-
return;
|
|
5525
|
-
}
|
|
5526
|
-
if (body.items.length === 0) {
|
|
5527
|
-
log.plain("This game sells nothing yet.");
|
|
5528
|
-
log.dim(` Add something: ${c.cyan('genex shop add "Iron Key" --price 100')}`);
|
|
5529
|
-
log.dim(` Prices available: ${body.priceGrid.join(", ")} coin`);
|
|
5530
|
-
return;
|
|
5531
|
-
}
|
|
5532
|
-
log.plain(`${body.items.length} item${body.items.length === 1 ? "" : "s"}:`);
|
|
5533
|
-
for (const s of body.items) {
|
|
5534
|
-
const state = s.active ? "" : c.dim(" (retired)");
|
|
5535
|
-
log.plain(
|
|
5536
|
-
` ${c.cyan(s.id)} ${s.name} \u2014 ${s.priceCoins} coin${money(s.priceDisplayUsdCents)} ${c.dim(s.type)}${state}`
|
|
5537
|
-
);
|
|
5538
|
-
}
|
|
5539
|
-
log.dim(`Prices available: ${body.priceGrid.join(", ")} coin`);
|
|
5540
|
-
return;
|
|
5541
|
-
}
|
|
5542
|
-
if (sub === "add") {
|
|
5543
|
-
const name = opts.hostname?.trim();
|
|
5544
|
-
if (!name) {
|
|
5545
|
-
log.error(`\`genex shop add\` needs a name, e.g. ${c.cyan('genex shop add "Iron Key" --price 100')}.`);
|
|
5546
|
-
process.exitCode = 1;
|
|
5547
|
-
return;
|
|
5548
|
-
}
|
|
5549
|
-
if (!opts.price) {
|
|
5550
|
-
log.error(`\`genex shop add\` needs ${c.cyan("--price")}, in coin.`);
|
|
5551
|
-
process.exitCode = 1;
|
|
5552
|
-
return;
|
|
5553
|
-
}
|
|
5554
|
-
const res2 = await call(projectSkus, {
|
|
5555
|
-
method: "POST",
|
|
5556
|
-
body: JSON.stringify({
|
|
5557
|
-
name,
|
|
5558
|
-
priceCoins: opts.price,
|
|
5559
|
-
type: opts.type === "durable" ? "durable" : "consumable",
|
|
5560
|
-
...opts.icon ? { iconUrl: opts.icon } : {}
|
|
5561
|
-
})
|
|
5562
|
-
});
|
|
5563
|
-
if (!res2.ok) return fail3(res2, "add that item");
|
|
5564
|
-
const sku = await res2.json();
|
|
5565
|
-
if (opts.json) {
|
|
5566
|
-
log.plain(JSON.stringify(sku, null, 2));
|
|
5567
|
-
return;
|
|
5568
|
-
}
|
|
5569
|
-
log.success(`Added ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
|
|
5570
|
-
log.plain(` id: ${c.cyan(sku.id)}`);
|
|
5571
|
-
log.dim(` Use it in the game: buy({ skuId: "${sku.id}" })`);
|
|
5572
|
-
return;
|
|
5573
|
-
}
|
|
5574
|
-
const skuId = opts.hostname?.trim();
|
|
5575
|
-
if (!skuId) {
|
|
5576
|
-
log.error(`\`genex shop ${sub}\` needs an item id \u2014 see ${c.cyan("genex shop list")}.`);
|
|
5577
|
-
process.exitCode = 1;
|
|
5578
|
-
return;
|
|
5579
|
-
}
|
|
5580
|
-
const skuUrl = `${apiUrl}/api/coin/skus/${encodeURIComponent(skuId)}`;
|
|
5581
|
-
if (sub === "test") {
|
|
5582
|
-
const res2 = await call(`${skuUrl}/test-grant`, { method: "POST" });
|
|
5583
|
-
if (!res2.ok) return fail3(res2, "grant yourself a test copy");
|
|
5584
|
-
const got = await res2.json();
|
|
5585
|
-
log.success(`You now own ${c.cyan(got.name)} in this game.`);
|
|
5586
|
-
log.dim(" It cost nothing and no sale was recorded \u2014 it is a test copy.");
|
|
5587
|
-
log.dim(
|
|
5588
|
-
got.skuType === "durable" ? " Reload the game: it should appear in your inventory." : " Reload the game: it should be delivered and consumed once."
|
|
5589
|
-
);
|
|
5590
|
-
return;
|
|
5591
|
-
}
|
|
5592
|
-
if (sub === "set") {
|
|
5593
|
-
const patch = {};
|
|
5594
|
-
if (opts.rename) patch.name = opts.rename;
|
|
5595
|
-
if (opts.price) patch.priceCoins = opts.price;
|
|
5596
|
-
if (opts.icon) patch.iconUrl = opts.icon;
|
|
5597
|
-
if (Object.keys(patch).length === 0) {
|
|
5598
|
-
log.error(`Nothing to change. Pass ${c.cyan("--rename")}, ${c.cyan("--price")} or ${c.cyan("--icon")}.`);
|
|
5599
|
-
process.exitCode = 1;
|
|
5600
|
-
return;
|
|
5601
|
-
}
|
|
5602
|
-
const res2 = await call(skuUrl, { method: "PATCH", body: JSON.stringify(patch) });
|
|
5603
|
-
if (!res2.ok) return fail3(res2, "update that item");
|
|
5604
|
-
const sku = await res2.json();
|
|
5605
|
-
log.success(`Updated ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
|
|
5606
|
-
return;
|
|
5607
|
-
}
|
|
5608
|
-
const res = await call(skuUrl, { method: "DELETE" });
|
|
5609
|
-
if (!res.ok) return fail3(res, "remove that item");
|
|
5610
|
-
log.success("Removed from the shop.");
|
|
5611
|
-
log.dim(" Players who already bought it keep it.");
|
|
5612
|
-
}
|
|
5613
|
-
|
|
5614
4966
|
// src/lib/promote.ts
|
|
5615
4967
|
async function promoteBuild(apiUrl, projectId, token, log) {
|
|
5616
4968
|
let res;
|
|
@@ -5645,17 +4997,17 @@ async function promoteBuild(apiUrl, projectId, token, log) {
|
|
|
5645
4997
|
}
|
|
5646
4998
|
|
|
5647
4999
|
// src/lib/detect-features.ts
|
|
5648
|
-
import
|
|
5649
|
-
import
|
|
5000
|
+
import fs17 from "fs/promises";
|
|
5001
|
+
import path16 from "path";
|
|
5650
5002
|
|
|
5651
5003
|
// src/lib/generation-ledger.ts
|
|
5652
|
-
import
|
|
5653
|
-
import
|
|
5654
|
-
var ledgerPath = (cwd) =>
|
|
5004
|
+
import fs16 from "fs/promises";
|
|
5005
|
+
import path15 from "path";
|
|
5006
|
+
var ledgerPath = (cwd) => path15.join(cwd, ".genex", "generations.ndjson");
|
|
5655
5007
|
async function append(cwd, event) {
|
|
5656
5008
|
try {
|
|
5657
|
-
await
|
|
5658
|
-
await
|
|
5009
|
+
await fs16.access(path15.join(cwd, ".genex"));
|
|
5010
|
+
await fs16.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
5659
5011
|
`, "utf8");
|
|
5660
5012
|
} catch {
|
|
5661
5013
|
}
|
|
@@ -5663,7 +5015,7 @@ async function append(cwd, event) {
|
|
|
5663
5015
|
async function readLedger(cwd = process.cwd()) {
|
|
5664
5016
|
let raw;
|
|
5665
5017
|
try {
|
|
5666
|
-
raw = await
|
|
5018
|
+
raw = await fs16.readFile(ledgerPath(cwd), "utf8");
|
|
5667
5019
|
} catch {
|
|
5668
5020
|
return [];
|
|
5669
5021
|
}
|
|
@@ -5717,7 +5069,7 @@ async function countFailed(kind, cwd = process.cwd()) {
|
|
|
5717
5069
|
// src/lib/detect-features.ts
|
|
5718
5070
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
5719
5071
|
try {
|
|
5720
|
-
const raw = await
|
|
5072
|
+
const raw = await fs17.readFile(path16.join(cwd, "package.json"), "utf8");
|
|
5721
5073
|
const pkg = JSON.parse(raw);
|
|
5722
5074
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
5723
5075
|
return typeof version === "string" && version ? version : null;
|
|
@@ -5727,7 +5079,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
5727
5079
|
}
|
|
5728
5080
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
5729
5081
|
try {
|
|
5730
|
-
const raw = await
|
|
5082
|
+
const raw = await fs17.readFile(path16.join(cwd, "package.json"), "utf8");
|
|
5731
5083
|
const pkg = JSON.parse(raw);
|
|
5732
5084
|
return Boolean(
|
|
5733
5085
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -5739,7 +5091,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
5739
5091
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
5740
5092
|
let pkg;
|
|
5741
5093
|
try {
|
|
5742
|
-
pkg = JSON.parse(await
|
|
5094
|
+
pkg = JSON.parse(await fs17.readFile(path16.join(cwd, "package.json"), "utf8"));
|
|
5743
5095
|
} catch (err) {
|
|
5744
5096
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
5745
5097
|
return null;
|
|
@@ -5757,15 +5109,15 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
5757
5109
|
var TOUCH_KIT_MARKERS = /controllers\/touch\/|controllers\/character\/touch-joystick|TouchJoystick|VirtualButton|DragZone|RotateOverlay/;
|
|
5758
5110
|
async function detectMobileControls(cwd = process.cwd()) {
|
|
5759
5111
|
try {
|
|
5760
|
-
const raw = await
|
|
5112
|
+
const raw = await fs17.readFile(path16.join(cwd, "package.json"), "utf8");
|
|
5761
5113
|
const pkg = JSON.parse(raw);
|
|
5762
5114
|
if (pkg.genex?.mobileControls === true) return true;
|
|
5763
5115
|
} catch {
|
|
5764
5116
|
}
|
|
5765
|
-
const srcDir =
|
|
5117
|
+
const srcDir = path16.join(cwd, "src");
|
|
5766
5118
|
let entries;
|
|
5767
5119
|
try {
|
|
5768
|
-
entries = await
|
|
5120
|
+
entries = await fs17.readdir(srcDir, { recursive: true });
|
|
5769
5121
|
} catch {
|
|
5770
5122
|
return false;
|
|
5771
5123
|
}
|
|
@@ -5773,7 +5125,7 @@ async function detectMobileControls(cwd = process.cwd()) {
|
|
|
5773
5125
|
if (rel.includes("node_modules")) continue;
|
|
5774
5126
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
5775
5127
|
try {
|
|
5776
|
-
const content = await
|
|
5128
|
+
const content = await fs17.readFile(path16.join(srcDir, rel), "utf8");
|
|
5777
5129
|
if (TOUCH_KIT_MARKERS.test(content)) return true;
|
|
5778
5130
|
} catch {
|
|
5779
5131
|
}
|
|
@@ -5782,10 +5134,10 @@ async function detectMobileControls(cwd = process.cwd()) {
|
|
|
5782
5134
|
}
|
|
5783
5135
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
5784
5136
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
5785
|
-
const srcDir =
|
|
5137
|
+
const srcDir = path16.join(cwd, "src");
|
|
5786
5138
|
let entries;
|
|
5787
5139
|
try {
|
|
5788
|
-
entries = await
|
|
5140
|
+
entries = await fs17.readdir(srcDir, { recursive: true });
|
|
5789
5141
|
} catch {
|
|
5790
5142
|
return false;
|
|
5791
5143
|
}
|
|
@@ -5793,7 +5145,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
5793
5145
|
if (rel.includes("node_modules")) continue;
|
|
5794
5146
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
5795
5147
|
try {
|
|
5796
|
-
const content = await
|
|
5148
|
+
const content = await fs17.readFile(path16.join(srcDir, rel), "utf8");
|
|
5797
5149
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
5798
5150
|
} catch {
|
|
5799
5151
|
}
|
|
@@ -5842,19 +5194,19 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
5842
5194
|
depthRange: [],
|
|
5843
5195
|
mediaElementAudio: []
|
|
5844
5196
|
};
|
|
5845
|
-
const srcDir =
|
|
5197
|
+
const srcDir = path16.join(cwd, "src");
|
|
5846
5198
|
let entries;
|
|
5847
5199
|
try {
|
|
5848
|
-
entries = await
|
|
5200
|
+
entries = await fs17.readdir(srcDir, { recursive: true });
|
|
5849
5201
|
} catch {
|
|
5850
5202
|
return found;
|
|
5851
5203
|
}
|
|
5852
5204
|
for (const nativeRel of entries) {
|
|
5853
5205
|
if (nativeRel.includes("node_modules")) continue;
|
|
5854
5206
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
|
|
5855
|
-
const raw = await
|
|
5207
|
+
const raw = await fs17.readFile(path16.join(srcDir, nativeRel), "utf8").catch(() => "");
|
|
5856
5208
|
if (!raw) continue;
|
|
5857
|
-
const rel = nativeRel.split(
|
|
5209
|
+
const rel = nativeRel.split(path16.sep).join("/");
|
|
5858
5210
|
const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
|
|
5859
5211
|
const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
5860
5212
|
let m;
|
|
@@ -5912,25 +5264,25 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
5912
5264
|
let haystack = "";
|
|
5913
5265
|
const read = async (file) => {
|
|
5914
5266
|
try {
|
|
5915
|
-
haystack += await
|
|
5267
|
+
haystack += await fs17.readFile(file, "utf8");
|
|
5916
5268
|
} catch {
|
|
5917
5269
|
}
|
|
5918
5270
|
};
|
|
5919
5271
|
try {
|
|
5920
|
-
for (const entry of await
|
|
5272
|
+
for (const entry of await fs17.readdir(cwd, { withFileTypes: true })) {
|
|
5921
5273
|
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
|
|
5922
|
-
await read(
|
|
5274
|
+
await read(path16.join(cwd, entry.name));
|
|
5923
5275
|
}
|
|
5924
5276
|
}
|
|
5925
5277
|
} catch {
|
|
5926
5278
|
}
|
|
5927
5279
|
for (const sub of ["src", "public"]) {
|
|
5928
5280
|
try {
|
|
5929
|
-
const entries = await
|
|
5281
|
+
const entries = await fs17.readdir(path16.join(cwd, sub), { recursive: true });
|
|
5930
5282
|
for (const rel of entries) {
|
|
5931
5283
|
if (rel.includes("node_modules")) continue;
|
|
5932
5284
|
if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
|
|
5933
|
-
await read(
|
|
5285
|
+
await read(path16.join(cwd, sub, rel));
|
|
5934
5286
|
}
|
|
5935
5287
|
} catch {
|
|
5936
5288
|
}
|
|
@@ -6062,7 +5414,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
6062
5414
|
} catch {
|
|
6063
5415
|
return true;
|
|
6064
5416
|
}
|
|
6065
|
-
const gitConfig = await
|
|
5417
|
+
const gitConfig = await fs17.readFile(path16.join(cwd, ".git", "config"), "utf8").catch(() => "");
|
|
6066
5418
|
for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
|
|
6067
5419
|
try {
|
|
6068
5420
|
const u = new URL(m[1]);
|
|
@@ -6070,7 +5422,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
6070
5422
|
} catch {
|
|
6071
5423
|
}
|
|
6072
5424
|
}
|
|
6073
|
-
const readme = await
|
|
5425
|
+
const readme = await fs17.readFile(path16.join(cwd, "README.md"), "utf8").catch(() => "");
|
|
6074
5426
|
return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
|
|
6075
5427
|
}
|
|
6076
5428
|
|
|
@@ -6136,7 +5488,7 @@ async function runPublish(opts) {
|
|
|
6136
5488
|
advisoryNudges(log, detections);
|
|
6137
5489
|
if (!opts.noPush) {
|
|
6138
5490
|
const ok = await deployGame(
|
|
6139
|
-
{ projectId: meta.id, apiUrl, token
|
|
5491
|
+
{ projectId: meta.id, apiUrl, token },
|
|
6140
5492
|
{
|
|
6141
5493
|
// Publish deploys to staging and then promotes, so both channels end up
|
|
6142
5494
|
// naming the same build. Deploying straight to production instead would
|
|
@@ -6144,7 +5496,6 @@ async function runPublish(opts) {
|
|
|
6144
5496
|
// staging — the owner would publish and then see a stale game.
|
|
6145
5497
|
channel: "staging",
|
|
6146
5498
|
noBuild: opts.noBuild,
|
|
6147
|
-
force: opts.force,
|
|
6148
5499
|
matchmaking: detections.matchmaking,
|
|
6149
5500
|
embedSdkVersion: detections.embedSdkVersion,
|
|
6150
5501
|
multiplayer: detections.multiplayer,
|
|
@@ -6205,7 +5556,7 @@ async function runPublish(opts) {
|
|
|
6205
5556
|
log.plain("");
|
|
6206
5557
|
log.success("Published. \u{1F389}");
|
|
6207
5558
|
const dashboard = meta.dashboardOrigins?.[0];
|
|
6208
|
-
if (dashboard) log.plain(`
|
|
5559
|
+
if (dashboard) log.plain(` world page (share this link): ${c.cyan(`${dashboard}/world/${meta.slug}`)}`);
|
|
6209
5560
|
if (meta.playUrl) log.dim(` play: ${meta.playUrl}`);
|
|
6210
5561
|
await verifyGamePage({
|
|
6211
5562
|
apiUrl,
|
|
@@ -6238,7 +5589,7 @@ async function runPreview(opts) {
|
|
|
6238
5589
|
await exploreReuseNudge(log, meta);
|
|
6239
5590
|
const apiUrl = getApiUrl(meta.apiUrl);
|
|
6240
5591
|
const ok = await deployGame(
|
|
6241
|
-
{ projectId: meta.id, apiUrl, token
|
|
5592
|
+
{ projectId: meta.id, apiUrl, token },
|
|
6242
5593
|
// Detections reach the server on every preview too: matchmaking so a draft's
|
|
6243
5594
|
// declared preset doesn't silently run the default (null clears a removed
|
|
6244
5595
|
// config), embedSdkVersion/multiplayer so the dashboard's Publish button can
|
|
@@ -6249,7 +5600,6 @@ async function runPreview(opts) {
|
|
|
6249
5600
|
// was inside, and the only protection was a warning line below.
|
|
6250
5601
|
channel: "staging",
|
|
6251
5602
|
noBuild: opts.noBuild,
|
|
6252
|
-
force: opts.force,
|
|
6253
5603
|
matchmaking: detections.matchmaking,
|
|
6254
5604
|
embedSdkVersion: detections.embedSdkVersion,
|
|
6255
5605
|
multiplayer: detections.multiplayer,
|
|
@@ -6322,8 +5672,8 @@ async function runPromote(opts) {
|
|
|
6322
5672
|
}
|
|
6323
5673
|
|
|
6324
5674
|
// src/commands/generate.ts
|
|
6325
|
-
import
|
|
6326
|
-
import
|
|
5675
|
+
import fs18 from "fs/promises";
|
|
5676
|
+
import path17 from "path";
|
|
6327
5677
|
import { PNG as PNG4 } from "pngjs";
|
|
6328
5678
|
|
|
6329
5679
|
// src/lib/glass.ts
|
|
@@ -6404,7 +5754,7 @@ function solveMagentaGlass(source) {
|
|
|
6404
5754
|
}
|
|
6405
5755
|
|
|
6406
5756
|
// src/lib/open.ts
|
|
6407
|
-
import { spawn as
|
|
5757
|
+
import { spawn as spawn4 } from "child_process";
|
|
6408
5758
|
function tokenize(cmd) {
|
|
6409
5759
|
return cmd.trim().split(/\s+/).filter(Boolean);
|
|
6410
5760
|
}
|
|
@@ -6433,7 +5783,7 @@ function openUrl(url) {
|
|
|
6433
5783
|
}
|
|
6434
5784
|
}
|
|
6435
5785
|
try {
|
|
6436
|
-
const child =
|
|
5786
|
+
const child = spawn4(command, args, { stdio: "ignore", detached: true });
|
|
6437
5787
|
child.on("error", () => {
|
|
6438
5788
|
});
|
|
6439
5789
|
child.unref();
|
|
@@ -6493,7 +5843,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
6493
5843
|
async function inlineLocalImage(filePath, flag) {
|
|
6494
5844
|
let bytes;
|
|
6495
5845
|
try {
|
|
6496
|
-
bytes = await
|
|
5846
|
+
bytes = await fs18.readFile(filePath);
|
|
6497
5847
|
} catch {
|
|
6498
5848
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
6499
5849
|
}
|
|
@@ -6503,7 +5853,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
6503
5853
|
error: `${flag} file is ${(bytes.length / 1048576).toFixed(1)} MB \u2014 over the ~4 MB inline limit. Downscale/compress it first, or pass an asset URL instead.`
|
|
6504
5854
|
};
|
|
6505
5855
|
}
|
|
6506
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
5856
|
+
const mime = IMAGE_MIME_BY_EXT[path17.extname(filePath).toLowerCase()] ?? "image/png";
|
|
6507
5857
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
6508
5858
|
}
|
|
6509
5859
|
var SKYBOX_ENVIRONMENT_SUFFIX = ". The image contains ONLY sky: cloud, atmosphere, light, weather and distant haze at the horizon. Every structure, object, plant and ground surface is outside the frame.";
|
|
@@ -6699,7 +6049,7 @@ async function runGenerate(kind, opts) {
|
|
|
6699
6049
|
return;
|
|
6700
6050
|
}
|
|
6701
6051
|
try {
|
|
6702
|
-
const bytes = await
|
|
6052
|
+
const bytes = await fs18.readFile(opts.inpaintUrl);
|
|
6703
6053
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
6704
6054
|
} catch {
|
|
6705
6055
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -6823,7 +6173,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
6823
6173
|
return;
|
|
6824
6174
|
}
|
|
6825
6175
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
6826
|
-
await
|
|
6176
|
+
await fs18.mkdir(outDir, { recursive: true });
|
|
6827
6177
|
const solved = [];
|
|
6828
6178
|
for (let i = 0; i < files.length; i++) {
|
|
6829
6179
|
const f = files[i];
|
|
@@ -6855,8 +6205,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
6855
6205
|
});
|
|
6856
6206
|
continue;
|
|
6857
6207
|
}
|
|
6858
|
-
const outPath =
|
|
6859
|
-
await
|
|
6208
|
+
const outPath = path17.join(outDir, `glass-${i + 1}.png`);
|
|
6209
|
+
await fs18.writeFile(outPath, PNG4.sync.write(r.png));
|
|
6860
6210
|
solved.push({
|
|
6861
6211
|
path: outPath,
|
|
6862
6212
|
url: f.url,
|
|
@@ -7453,8 +6803,8 @@ async function toRow(e, v, cwd) {
|
|
|
7453
6803
|
}
|
|
7454
6804
|
|
|
7455
6805
|
// src/commands/controller.ts
|
|
7456
|
-
import
|
|
7457
|
-
import
|
|
6806
|
+
import fs20 from "fs/promises";
|
|
6807
|
+
import path19 from "path";
|
|
7458
6808
|
|
|
7459
6809
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
7460
6810
|
import { createHash } from "crypto";
|
|
@@ -16584,9 +15934,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
16584
15934
|
}
|
|
16585
15935
|
|
|
16586
15936
|
// src/lib/anims.ts
|
|
16587
|
-
import
|
|
16588
|
-
import
|
|
16589
|
-
var ANIMS_DEST =
|
|
15937
|
+
import fs19 from "fs/promises";
|
|
15938
|
+
import path18 from "path";
|
|
15939
|
+
var ANIMS_DEST = path18.join("public", "assets", "anims");
|
|
16590
15940
|
var HIDDEN_TAG = "reference";
|
|
16591
15941
|
async function runAnims(opts) {
|
|
16592
15942
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -16602,7 +15952,7 @@ async function runAnims(opts) {
|
|
|
16602
15952
|
printCatalog(log, manifest, selectors);
|
|
16603
15953
|
return;
|
|
16604
15954
|
}
|
|
16605
|
-
const controllerMarker =
|
|
15955
|
+
const controllerMarker = path18.join(root, "src", "controllers", "character");
|
|
16606
15956
|
if (!await exists2(controllerMarker)) {
|
|
16607
15957
|
log.error(
|
|
16608
15958
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -16611,11 +15961,11 @@ async function runAnims(opts) {
|
|
|
16611
15961
|
process.exitCode = 1;
|
|
16612
15962
|
return;
|
|
16613
15963
|
}
|
|
16614
|
-
const destDir =
|
|
16615
|
-
const gameManifestPath =
|
|
15964
|
+
const destDir = path18.join(root, ANIMS_DEST);
|
|
15965
|
+
const gameManifestPath = path18.join(destDir, "manifest.json");
|
|
16616
15966
|
if (opts.reset) {
|
|
16617
|
-
await
|
|
16618
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
15967
|
+
await fs19.rm(destDir, { recursive: true, force: true });
|
|
15968
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path18.sep)} (--reset)`);
|
|
16619
15969
|
}
|
|
16620
15970
|
if (selectors.length === 0) {
|
|
16621
15971
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -16653,35 +16003,35 @@ async function runAnims(opts) {
|
|
|
16653
16003
|
}
|
|
16654
16004
|
}
|
|
16655
16005
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
16656
|
-
const cacheDir =
|
|
16006
|
+
const cacheDir = path18.join(
|
|
16657
16007
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
16658
16008
|
`${manifest.library}-v${manifest.version}`
|
|
16659
16009
|
);
|
|
16660
|
-
await
|
|
16661
|
-
await
|
|
16010
|
+
await fs19.mkdir(cacheDir, { recursive: true });
|
|
16011
|
+
await fs19.mkdir(destDir, { recursive: true });
|
|
16662
16012
|
const base = getAnimsBase(opts.animsBase);
|
|
16663
16013
|
let installedCount = 0;
|
|
16664
16014
|
let presentCount = 0;
|
|
16665
16015
|
let addedBytes = 0;
|
|
16666
16016
|
const failures = [];
|
|
16667
16017
|
for (const entry of wanted) {
|
|
16668
|
-
const dest =
|
|
16018
|
+
const dest = path18.join(destDir, entry.file);
|
|
16669
16019
|
if (await hasSize(dest, entry.bytes)) {
|
|
16670
16020
|
presentCount++;
|
|
16671
16021
|
continue;
|
|
16672
16022
|
}
|
|
16673
16023
|
try {
|
|
16674
|
-
const cached =
|
|
16024
|
+
const cached = path18.join(cacheDir, entry.file);
|
|
16675
16025
|
if (!await hasSize(cached, entry.bytes)) {
|
|
16676
16026
|
const res = await fetch(base + entry.file);
|
|
16677
16027
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
16678
16028
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
16679
|
-
await
|
|
16029
|
+
await fs19.writeFile(cached, buf);
|
|
16680
16030
|
}
|
|
16681
|
-
await
|
|
16031
|
+
await fs19.copyFile(cached, dest);
|
|
16682
16032
|
installedCount++;
|
|
16683
16033
|
addedBytes += entry.bytes;
|
|
16684
|
-
log.dim(` ${
|
|
16034
|
+
log.dim(` ${path18.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
16685
16035
|
} catch (err) {
|
|
16686
16036
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
16687
16037
|
}
|
|
@@ -16697,13 +16047,13 @@ async function runAnims(opts) {
|
|
|
16697
16047
|
version: manifest.version,
|
|
16698
16048
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
16699
16049
|
};
|
|
16700
|
-
await
|
|
16050
|
+
await fs19.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
16701
16051
|
log.plain("");
|
|
16702
16052
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
16703
16053
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
16704
16054
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
16705
16055
|
log.success(
|
|
16706
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
16056
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path18.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
16707
16057
|
);
|
|
16708
16058
|
for (const [selector, entries] of resolved) {
|
|
16709
16059
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -16735,8 +16085,8 @@ async function loadManifest(baseOverride) {
|
|
|
16735
16085
|
}
|
|
16736
16086
|
} catch {
|
|
16737
16087
|
}
|
|
16738
|
-
const snapshotPath =
|
|
16739
|
-
const manifest = JSON.parse(await
|
|
16088
|
+
const snapshotPath = path18.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
16089
|
+
const manifest = JSON.parse(await fs19.readFile(snapshotPath, "utf8"));
|
|
16740
16090
|
return { manifest, source: "snapshot" };
|
|
16741
16091
|
}
|
|
16742
16092
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -16854,21 +16204,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
16854
16204
|
}
|
|
16855
16205
|
async function readGameManifest(file) {
|
|
16856
16206
|
try {
|
|
16857
|
-
return JSON.parse(await
|
|
16207
|
+
return JSON.parse(await fs19.readFile(file, "utf8"));
|
|
16858
16208
|
} catch {
|
|
16859
16209
|
return null;
|
|
16860
16210
|
}
|
|
16861
16211
|
}
|
|
16862
16212
|
async function hasSize(file, bytes) {
|
|
16863
16213
|
try {
|
|
16864
|
-
return (await
|
|
16214
|
+
return (await fs19.stat(file)).size === bytes;
|
|
16865
16215
|
} catch {
|
|
16866
16216
|
return false;
|
|
16867
16217
|
}
|
|
16868
16218
|
}
|
|
16869
16219
|
async function exists2(p) {
|
|
16870
16220
|
try {
|
|
16871
|
-
await
|
|
16221
|
+
await fs19.access(p);
|
|
16872
16222
|
return true;
|
|
16873
16223
|
} catch {
|
|
16874
16224
|
return false;
|
|
@@ -17062,8 +16412,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
17062
16412
|
]
|
|
17063
16413
|
}
|
|
17064
16414
|
};
|
|
17065
|
-
var CODE_DEST =
|
|
17066
|
-
var ASSETS_DEST =
|
|
16415
|
+
var CODE_DEST = path19.join("src", "controllers");
|
|
16416
|
+
var ASSETS_DEST = path19.join("public", "assets");
|
|
17067
16417
|
async function runController(opts) {
|
|
17068
16418
|
const log = createLogger({ quiet: opts.quiet });
|
|
17069
16419
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -17080,31 +16430,31 @@ async function runController(opts) {
|
|
|
17080
16430
|
process.exitCode = 1;
|
|
17081
16431
|
return;
|
|
17082
16432
|
}
|
|
17083
|
-
const srcDir =
|
|
16433
|
+
const srcDir = path19.join(getTemplatesDir(), "controllers");
|
|
17084
16434
|
const root = opts.cwd ?? process.cwd();
|
|
17085
16435
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
17086
16436
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
17087
16437
|
log.plain("");
|
|
17088
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
16438
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path19.sep)}`);
|
|
17089
16439
|
const plan = [
|
|
17090
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
16440
|
+
...set.code.map((rel) => ({ from: rel, rel: path19.join(CODE_DEST, rel) })),
|
|
17091
16441
|
...set.assets.map((rel) => ({
|
|
17092
16442
|
from: rel,
|
|
17093
|
-
rel:
|
|
16443
|
+
rel: path19.join(ASSETS_DEST, path19.basename(rel))
|
|
17094
16444
|
}))
|
|
17095
16445
|
];
|
|
17096
16446
|
let copied = 0;
|
|
17097
16447
|
let skipped = 0;
|
|
17098
16448
|
try {
|
|
17099
16449
|
for (const file of plan) {
|
|
17100
|
-
const dest =
|
|
16450
|
+
const dest = path19.join(root, file.rel);
|
|
17101
16451
|
if (!opts.force && await exists3(dest)) {
|
|
17102
16452
|
skipped++;
|
|
17103
16453
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
17104
16454
|
continue;
|
|
17105
16455
|
}
|
|
17106
|
-
await
|
|
17107
|
-
await
|
|
16456
|
+
await fs20.mkdir(path19.dirname(dest), { recursive: true });
|
|
16457
|
+
await fs20.copyFile(path19.join(srcDir, file.from), dest);
|
|
17108
16458
|
copied++;
|
|
17109
16459
|
log.dim(` ${file.rel}`);
|
|
17110
16460
|
}
|
|
@@ -17157,7 +16507,7 @@ async function runController(opts) {
|
|
|
17157
16507
|
for (const line of set.sketch) {
|
|
17158
16508
|
log.dim(` ${line}`);
|
|
17159
16509
|
}
|
|
17160
|
-
if (kind === "character" && !await exists3(
|
|
16510
|
+
if (kind === "character" && !await exists3(path19.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
17161
16511
|
log.plain("");
|
|
17162
16512
|
log.plain(
|
|
17163
16513
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -17189,9 +16539,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
17189
16539
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
17190
16540
|
}
|
|
17191
16541
|
assertCompleteMeshyControllerPack(manifest);
|
|
17192
|
-
const destination =
|
|
17193
|
-
await
|
|
17194
|
-
await
|
|
16542
|
+
const destination = path19.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
16543
|
+
await fs20.mkdir(path19.dirname(destination), { recursive: true });
|
|
16544
|
+
await fs20.writeFile(
|
|
17195
16545
|
destination,
|
|
17196
16546
|
`${JSON.stringify(manifest, null, 2)}
|
|
17197
16547
|
`
|
|
@@ -17329,14 +16679,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
17329
16679
|
}
|
|
17330
16680
|
async function installFallbackAvatar(args) {
|
|
17331
16681
|
const { root, srcDir, log } = args;
|
|
17332
|
-
const dest =
|
|
17333
|
-
await
|
|
17334
|
-
await
|
|
16682
|
+
const dest = path19.join(root, ASSETS_DEST, "avatar.vrm");
|
|
16683
|
+
await fs20.mkdir(path19.dirname(dest), { recursive: true });
|
|
16684
|
+
await fs20.copyFile(path19.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
17335
16685
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
17336
16686
|
}
|
|
17337
16687
|
async function exists3(p) {
|
|
17338
16688
|
try {
|
|
17339
|
-
await
|
|
16689
|
+
await fs20.access(p);
|
|
17340
16690
|
return true;
|
|
17341
16691
|
} catch {
|
|
17342
16692
|
return false;
|
|
@@ -17344,8 +16694,8 @@ async function exists3(p) {
|
|
|
17344
16694
|
}
|
|
17345
16695
|
|
|
17346
16696
|
// src/commands/character.ts
|
|
17347
|
-
import
|
|
17348
|
-
import
|
|
16697
|
+
import fs21 from "fs/promises";
|
|
16698
|
+
import path20 from "path";
|
|
17349
16699
|
function exactAnimation(selector) {
|
|
17350
16700
|
const trimmed = selector.trim();
|
|
17351
16701
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -17425,7 +16775,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
17425
16775
|
}
|
|
17426
16776
|
process.exitCode = 1;
|
|
17427
16777
|
}
|
|
17428
|
-
var INSTALLED_MANIFEST =
|
|
16778
|
+
var INSTALLED_MANIFEST = path20.join("public", "assets", "meshy-character.json");
|
|
17429
16779
|
async function resolveAdoptTarget(selector) {
|
|
17430
16780
|
const trimmed = selector?.trim();
|
|
17431
16781
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -17434,7 +16784,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
17434
16784
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
17435
16785
|
let raw;
|
|
17436
16786
|
try {
|
|
17437
|
-
raw = await
|
|
16787
|
+
raw = await fs21.readFile(file, "utf8");
|
|
17438
16788
|
} catch {
|
|
17439
16789
|
return {
|
|
17440
16790
|
ok: false,
|
|
@@ -17912,22 +17262,22 @@ async function context2(opts) {
|
|
|
17912
17262
|
const project = await readProject();
|
|
17913
17263
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
17914
17264
|
}
|
|
17915
|
-
async function readVideo(
|
|
17265
|
+
async function readVideo(path23, log) {
|
|
17916
17266
|
let bytes;
|
|
17917
17267
|
try {
|
|
17918
|
-
bytes = await readFile(
|
|
17268
|
+
bytes = await readFile(path23);
|
|
17919
17269
|
} catch {
|
|
17920
|
-
log.error(`Can't read ${
|
|
17270
|
+
log.error(`Can't read ${path23}.`);
|
|
17921
17271
|
return null;
|
|
17922
17272
|
}
|
|
17923
17273
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
17924
|
-
log.error(`${basename(
|
|
17274
|
+
log.error(`${basename(path23)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
17925
17275
|
return null;
|
|
17926
17276
|
}
|
|
17927
17277
|
return bytes;
|
|
17928
17278
|
}
|
|
17929
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
17930
|
-
const contentType = /\.mov$/i.test(
|
|
17279
|
+
async function uploadVideo(apiUrl, token, characterId, path23, bytes, log) {
|
|
17280
|
+
const contentType = /\.mov$/i.test(path23) ? "video/quicktime" : "video/mp4";
|
|
17931
17281
|
const minted = await apiFetch(
|
|
17932
17282
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
17933
17283
|
{
|
|
@@ -17942,7 +17292,7 @@ async function uploadVideo(apiUrl, token, characterId, path25, bytes, log) {
|
|
|
17942
17292
|
return null;
|
|
17943
17293
|
}
|
|
17944
17294
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
17945
|
-
log.dim(` uploading ${basename(
|
|
17295
|
+
log.dim(` uploading ${basename(path23)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
17946
17296
|
const put = await fetch(uploadUrl, {
|
|
17947
17297
|
method: "PUT",
|
|
17948
17298
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -18258,8 +17608,8 @@ function rank(items, query) {
|
|
|
18258
17608
|
}
|
|
18259
17609
|
|
|
18260
17610
|
// src/commands/motion.ts
|
|
18261
|
-
import
|
|
18262
|
-
import
|
|
17611
|
+
import fs22 from "fs/promises";
|
|
17612
|
+
import path21 from "path";
|
|
18263
17613
|
|
|
18264
17614
|
// src/lib/motion/npz.ts
|
|
18265
17615
|
import zlib from "zlib";
|
|
@@ -19510,7 +18860,7 @@ async function motionGen(opts, log) {
|
|
|
19510
18860
|
}
|
|
19511
18861
|
if (opts.constraintsPath !== void 0) {
|
|
19512
18862
|
try {
|
|
19513
|
-
const raw = await
|
|
18863
|
+
const raw = await fs22.readFile(opts.constraintsPath, "utf8");
|
|
19514
18864
|
generationOptions.constraints = JSON.parse(raw);
|
|
19515
18865
|
} catch {
|
|
19516
18866
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -19534,10 +18884,10 @@ async function motionGen(opts, log) {
|
|
|
19534
18884
|
async function expandTakes(selectors) {
|
|
19535
18885
|
const out = [];
|
|
19536
18886
|
for (const sel of selectors) {
|
|
19537
|
-
const st = await
|
|
18887
|
+
const st = await fs22.stat(sel).catch(() => null);
|
|
19538
18888
|
if (st?.isDirectory()) {
|
|
19539
|
-
const names = await
|
|
19540
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
18889
|
+
const names = await fs22.readdir(sel);
|
|
18890
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path21.join(sel, n));
|
|
19541
18891
|
} else if (st?.isFile()) {
|
|
19542
18892
|
out.push(sel);
|
|
19543
18893
|
} else {
|
|
@@ -19572,7 +18922,7 @@ async function motionVerify(opts, log) {
|
|
|
19572
18922
|
let gates = DEFAULT_GATES;
|
|
19573
18923
|
if (opts.gatesPath) {
|
|
19574
18924
|
try {
|
|
19575
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
18925
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs22.readFile(opts.gatesPath, "utf8")));
|
|
19576
18926
|
} catch {
|
|
19577
18927
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
19578
18928
|
process.exitCode = 1;
|
|
@@ -19594,9 +18944,9 @@ async function motionVerify(opts, log) {
|
|
|
19594
18944
|
}
|
|
19595
18945
|
const reports = [];
|
|
19596
18946
|
for (const file of files) {
|
|
19597
|
-
const stem =
|
|
18947
|
+
const stem = path21.basename(file).replace(/\.npz$/, "");
|
|
19598
18948
|
try {
|
|
19599
|
-
reports.push(analyzeTake(stem, await
|
|
18949
|
+
reports.push(analyzeTake(stem, await fs22.readFile(file), gates));
|
|
19600
18950
|
} catch (err) {
|
|
19601
18951
|
reports.push({
|
|
19602
18952
|
take: stem,
|
|
@@ -19634,7 +18984,7 @@ async function motionCompile(opts, log) {
|
|
|
19634
18984
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
19635
18985
|
if (opts.configPath) {
|
|
19636
18986
|
try {
|
|
19637
|
-
const patch = JSON.parse(await
|
|
18987
|
+
const patch = JSON.parse(await fs22.readFile(opts.configPath, "utf8"));
|
|
19638
18988
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
19639
18989
|
} catch {
|
|
19640
18990
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -19652,16 +19002,16 @@ async function motionCompile(opts, log) {
|
|
|
19652
19002
|
}
|
|
19653
19003
|
const inputs = [];
|
|
19654
19004
|
for (const file of files) {
|
|
19655
|
-
const stem =
|
|
19005
|
+
const stem = path21.basename(file).replace(/\.npz$/, "");
|
|
19656
19006
|
try {
|
|
19657
|
-
inputs.push({ stem, take: loadTake(await
|
|
19007
|
+
inputs.push({ stem, take: loadTake(await fs22.readFile(file)) });
|
|
19658
19008
|
} catch (err) {
|
|
19659
19009
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
19660
19010
|
process.exitCode = 1;
|
|
19661
19011
|
return;
|
|
19662
19012
|
}
|
|
19663
19013
|
}
|
|
19664
|
-
const setName = opts.set ??
|
|
19014
|
+
const setName = opts.set ?? path21.basename(opts.out).replace(/\.json$/, "");
|
|
19665
19015
|
let result;
|
|
19666
19016
|
try {
|
|
19667
19017
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -19676,9 +19026,9 @@ async function motionCompile(opts, log) {
|
|
|
19676
19026
|
process.exitCode = 1;
|
|
19677
19027
|
return;
|
|
19678
19028
|
}
|
|
19679
|
-
await
|
|
19029
|
+
await fs22.mkdir(path21.dirname(path21.resolve(opts.out)), { recursive: true });
|
|
19680
19030
|
const json = JSON.stringify(result.data);
|
|
19681
|
-
await
|
|
19031
|
+
await fs22.writeFile(opts.out, json);
|
|
19682
19032
|
if (opts.json) {
|
|
19683
19033
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
19684
19034
|
return;
|
|
@@ -19696,9 +19046,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
19696
19046
|
var MOTION_PRESETS = {
|
|
19697
19047
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
19698
19048
|
};
|
|
19699
|
-
var MOTION_DEST =
|
|
19049
|
+
var MOTION_DEST = path21.join("src", "motion");
|
|
19700
19050
|
async function motionInstall(opts, log) {
|
|
19701
|
-
const srcDir =
|
|
19051
|
+
const srcDir = path21.join(getTemplatesDir(), "motion");
|
|
19702
19052
|
const root = opts.cwd ?? process.cwd();
|
|
19703
19053
|
const preset = opts.set;
|
|
19704
19054
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -19709,21 +19059,21 @@ async function motionInstall(opts, log) {
|
|
|
19709
19059
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
19710
19060
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
19711
19061
|
log.plain("");
|
|
19712
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
19062
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path21.sep)}`);
|
|
19713
19063
|
let copied = 0, skipped = 0;
|
|
19714
19064
|
try {
|
|
19715
19065
|
for (const rel of files) {
|
|
19716
|
-
const dest =
|
|
19717
|
-
const exists4 = await
|
|
19066
|
+
const dest = path21.join(root, MOTION_DEST, rel);
|
|
19067
|
+
const exists4 = await fs22.access(dest).then(() => true, () => false);
|
|
19718
19068
|
if (!opts.force && exists4) {
|
|
19719
19069
|
skipped++;
|
|
19720
|
-
log.dim(` skipped ${
|
|
19070
|
+
log.dim(` skipped ${path21.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
19721
19071
|
continue;
|
|
19722
19072
|
}
|
|
19723
|
-
await
|
|
19724
|
-
await
|
|
19073
|
+
await fs22.mkdir(path21.dirname(dest), { recursive: true });
|
|
19074
|
+
await fs22.copyFile(path21.join(srcDir, rel), dest);
|
|
19725
19075
|
copied++;
|
|
19726
|
-
log.dim(` ${
|
|
19076
|
+
log.dim(` ${path21.join(MOTION_DEST, rel)}`);
|
|
19727
19077
|
}
|
|
19728
19078
|
} catch (err) {
|
|
19729
19079
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -19764,7 +19114,7 @@ async function motionConstraints(opts, log) {
|
|
|
19764
19114
|
}
|
|
19765
19115
|
const doc = directionConstraint(dir, speed, duration);
|
|
19766
19116
|
const out = opts.out ?? "constraints.json";
|
|
19767
|
-
await
|
|
19117
|
+
await fs22.writeFile(out, JSON.stringify(doc));
|
|
19768
19118
|
if (opts.json) {
|
|
19769
19119
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
19770
19120
|
return;
|
|
@@ -19801,9 +19151,9 @@ async function runMotion(opts) {
|
|
|
19801
19151
|
}
|
|
19802
19152
|
|
|
19803
19153
|
// src/commands/asset-new.ts
|
|
19804
|
-
import
|
|
19154
|
+
import fs23 from "fs";
|
|
19805
19155
|
import fsp from "fs/promises";
|
|
19806
|
-
import
|
|
19156
|
+
import path22 from "path";
|
|
19807
19157
|
import { pathToFileURL } from "url";
|
|
19808
19158
|
var EXTRA_FILES = [
|
|
19809
19159
|
"genex-asset.example.json",
|
|
@@ -19896,7 +19246,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
19896
19246
|
}
|
|
19897
19247
|
async function runAssetNew(options) {
|
|
19898
19248
|
const log = createLogger();
|
|
19899
|
-
const cwd = options.dir ?
|
|
19249
|
+
const cwd = options.dir ? path22.resolve(options.dir) : process.cwd();
|
|
19900
19250
|
const slug = options.assetSlug;
|
|
19901
19251
|
if (!slug) {
|
|
19902
19252
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -19906,14 +19256,14 @@ async function runAssetNew(options) {
|
|
|
19906
19256
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
19907
19257
|
return 1;
|
|
19908
19258
|
}
|
|
19909
|
-
const templateDir =
|
|
19910
|
-
if (!
|
|
19259
|
+
const templateDir = path22.join(getTemplatesDir(), "asset-viewer");
|
|
19260
|
+
if (!fs23.existsSync(templateDir)) {
|
|
19911
19261
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
19912
19262
|
return 1;
|
|
19913
19263
|
}
|
|
19914
|
-
const manifestTools = await import(pathToFileURL(
|
|
19264
|
+
const manifestTools = await import(pathToFileURL(path22.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
19915
19265
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
19916
|
-
const lockPath =
|
|
19266
|
+
const lockPath = path22.join(templateDir, "shared-files.sha256.json");
|
|
19917
19267
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
19918
19268
|
const actual = hashSharedFiles(templateDir);
|
|
19919
19269
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -19926,8 +19276,8 @@ async function runAssetNew(options) {
|
|
|
19926
19276
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
19927
19277
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
19928
19278
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
19929
|
-
const outDir =
|
|
19930
|
-
if (
|
|
19279
|
+
const outDir = path22.resolve(cwd, options.out ?? slug);
|
|
19280
|
+
if (fs23.existsSync(outDir) && fs23.readdirSync(outDir).length > 0 && !options.force) {
|
|
19931
19281
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
19932
19282
|
return 1;
|
|
19933
19283
|
}
|
|
@@ -19955,24 +19305,24 @@ async function runAssetNew(options) {
|
|
|
19955
19305
|
};
|
|
19956
19306
|
await fsp.mkdir(outDir, { recursive: true });
|
|
19957
19307
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
19958
|
-
const to =
|
|
19959
|
-
await fsp.mkdir(
|
|
19960
|
-
await fsp.copyFile(
|
|
19308
|
+
const to = path22.join(outDir, rel);
|
|
19309
|
+
await fsp.mkdir(path22.dirname(to), { recursive: true });
|
|
19310
|
+
await fsp.copyFile(path22.join(templateDir, rel), to);
|
|
19961
19311
|
}
|
|
19962
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
19312
|
+
const pkg = fillTemplate(await fsp.readFile(path22.join(templateDir, "package.json"), "utf8"), {
|
|
19963
19313
|
slug,
|
|
19964
19314
|
name,
|
|
19965
19315
|
version
|
|
19966
19316
|
});
|
|
19967
|
-
await fsp.writeFile(
|
|
19968
|
-
await fsp.writeFile(
|
|
19969
|
-
await fsp.writeFile(
|
|
19317
|
+
await fsp.writeFile(path22.join(outDir, "package.json"), pkg, "utf8");
|
|
19318
|
+
await fsp.writeFile(path22.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
19319
|
+
await fsp.writeFile(path22.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
19970
19320
|
await fsp.writeFile(
|
|
19971
|
-
|
|
19321
|
+
path22.join(outDir, "DESIGN.md"),
|
|
19972
19322
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
19973
19323
|
"utf8"
|
|
19974
19324
|
);
|
|
19975
|
-
const placeholder = await fsp.readFile(
|
|
19325
|
+
const placeholder = await fsp.readFile(path22.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
19976
19326
|
const seeded = seedAssetSource(placeholder, {
|
|
19977
19327
|
slug,
|
|
19978
19328
|
name,
|
|
@@ -19983,8 +19333,8 @@ async function runAssetNew(options) {
|
|
|
19983
19333
|
pascalCase
|
|
19984
19334
|
});
|
|
19985
19335
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
19986
|
-
await fsp.mkdir(
|
|
19987
|
-
await fsp.writeFile(
|
|
19336
|
+
await fsp.mkdir(path22.join(outDir, "src", "asset"), { recursive: true });
|
|
19337
|
+
await fsp.writeFile(path22.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
19988
19338
|
const copied = hashSharedFiles(outDir);
|
|
19989
19339
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
19990
19340
|
if (mismatched.length) {
|
|
@@ -19992,7 +19342,7 @@ async function runAssetNew(options) {
|
|
|
19992
19342
|
return 1;
|
|
19993
19343
|
}
|
|
19994
19344
|
await fsp.writeFile(
|
|
19995
|
-
|
|
19345
|
+
path22.join(outDir, PARITY_FILENAME),
|
|
19996
19346
|
JSON.stringify(
|
|
19997
19347
|
{
|
|
19998
19348
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -20046,15 +19396,9 @@ ${c.bold("Usage")}
|
|
|
20046
19396
|
resumes the same code, so an interrupted
|
|
20047
19397
|
'genex init' costs nothing. --force switches
|
|
20048
19398
|
accounts.
|
|
20049
|
-
genex link <slug> [options]
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
that's how you pick a game up on a new machine.
|
|
20053
|
-
Never creates a project.
|
|
20054
|
-
genex pull [options] Update THIS folder with the game's latest draft,
|
|
20055
|
-
for when it was built from somewhere else. Refuses
|
|
20056
|
-
if this folder has changes you never deployed;
|
|
20057
|
-
--force replaces them.
|
|
19399
|
+
genex link <slug> [options] Re-link THIS folder to an existing game of yours
|
|
19400
|
+
(lost folder / new machine); preview/publish then
|
|
19401
|
+
update the same live game. Never creates a project.
|
|
20058
19402
|
genex rename <name> [options] Rename THIS game \u2014 its play address, its source repo,
|
|
20059
19403
|
and its gallery name. Keeps the game (plays, likes,
|
|
20060
19404
|
comments); the OLD address stops working, so a
|
|
@@ -20070,18 +19414,6 @@ ${c.bold("Usage")}
|
|
|
20070
19414
|
genex publish [options] Build + push + make live, then list it in the gallery.
|
|
20071
19415
|
genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
|
|
20072
19416
|
private source onto a public managed genex repo.
|
|
20073
|
-
genex domain <sub> [host] Play this game on a domain you own:
|
|
20074
|
-
add | list | verify | remove. "add" opens a
|
|
20075
|
-
one-click approval at your DNS host \u2014 no
|
|
20076
|
-
records to copy or paste.
|
|
20077
|
-
genex shop <sub> [name|id] What this game sells: list | add | set | remove |
|
|
20078
|
-
test. "add" prints the item id your game passes
|
|
20079
|
-
to buy({ skuId }) \u2014 the platform owns the
|
|
20080
|
-
catalog, so this is the only way one exists.
|
|
20081
|
-
Prices come off a fixed grid, printed on any
|
|
20082
|
-
refusal. "test <id>" gives you a free copy of
|
|
20083
|
-
your own item: you can't buy from your own game,
|
|
20084
|
-
so this is how you check the shop works.
|
|
20085
19417
|
genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
|
|
20086
19418
|
genex sfx "<prompt>" [options] Generate a sound effect (mp3); prints a public asset URL.
|
|
20087
19419
|
genex music "<prompt>" [options] Generate an instrumental music track (mp3); prints a
|
|
@@ -20329,13 +19661,6 @@ ${c.bold("Examples")}
|
|
|
20329
19661
|
genex publish
|
|
20330
19662
|
genex publish --categories games,vfx
|
|
20331
19663
|
genex make-remixable
|
|
20332
|
-
genex domain add play.mygame.com
|
|
20333
|
-
genex domain list
|
|
20334
|
-
genex shop list
|
|
20335
|
-
genex shop add "Iron Key" --price 100 --type durable
|
|
20336
|
-
genex shop set sku_123 --price 200
|
|
20337
|
-
genex shop test sku_123
|
|
20338
|
-
genex shop remove sku_123
|
|
20339
19664
|
genex publish --no-push --title "My Game"
|
|
20340
19665
|
genex model "weathered wooden barrel with iron bands"
|
|
20341
19666
|
genex sfx "punchy laser zap" --duration 2
|
|
@@ -20387,10 +19712,6 @@ function parseArgs(argv) {
|
|
|
20387
19712
|
"--name",
|
|
20388
19713
|
"--repo",
|
|
20389
19714
|
"--remixed-from",
|
|
20390
|
-
"--price",
|
|
20391
|
-
"--icon",
|
|
20392
|
-
"--rename",
|
|
20393
|
-
"--type",
|
|
20394
19715
|
"--title",
|
|
20395
19716
|
"--description",
|
|
20396
19717
|
"--categories",
|
|
@@ -20468,9 +19789,6 @@ function parseArgs(argv) {
|
|
|
20468
19789
|
case "--no-auth":
|
|
20469
19790
|
parsed.options.noAuth = true;
|
|
20470
19791
|
break;
|
|
20471
|
-
case "--no-open":
|
|
20472
|
-
parsed.options.noOpen = true;
|
|
20473
|
-
break;
|
|
20474
19792
|
case "--no-push":
|
|
20475
19793
|
parsed.options.noPush = true;
|
|
20476
19794
|
break;
|
|
@@ -20605,18 +19923,6 @@ function parseArgs(argv) {
|
|
|
20605
19923
|
} else {
|
|
20606
19924
|
(parsed.options.selectors ??= []).push(arg);
|
|
20607
19925
|
}
|
|
20608
|
-
} else if (parsed.command === "shop") {
|
|
20609
|
-
if (!parsed.options.hostname) parsed.options.hostname = arg;
|
|
20610
|
-
else {
|
|
20611
|
-
parsed.error = `Unexpected argument: ${arg}`;
|
|
20612
|
-
return parsed;
|
|
20613
|
-
}
|
|
20614
|
-
} else if (parsed.command === "domain") {
|
|
20615
|
-
if (!parsed.options.hostname) parsed.options.hostname = arg;
|
|
20616
|
-
else {
|
|
20617
|
-
parsed.error = `Unexpected argument: ${arg}`;
|
|
20618
|
-
return parsed;
|
|
20619
|
-
}
|
|
20620
19926
|
} else if (parsed.command === "animations") {
|
|
20621
19927
|
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
20622
19928
|
} else if (parsed.command === "asset") {
|
|
@@ -20809,27 +20115,6 @@ function applyValueFlag(options, flag, value) {
|
|
|
20809
20115
|
options.duration = n;
|
|
20810
20116
|
break;
|
|
20811
20117
|
}
|
|
20812
|
-
case "--price": {
|
|
20813
|
-
const n = Number(value);
|
|
20814
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
20815
|
-
throw new Error(`Invalid --price value: ${value} (whole coin, e.g. 100)`);
|
|
20816
|
-
}
|
|
20817
|
-
options.price = n;
|
|
20818
|
-
break;
|
|
20819
|
-
}
|
|
20820
|
-
case "--icon":
|
|
20821
|
-
options.icon = value;
|
|
20822
|
-
break;
|
|
20823
|
-
case "--type": {
|
|
20824
|
-
if (value !== "consumable" && value !== "durable") {
|
|
20825
|
-
throw new Error(`Invalid --type value: ${value} (consumable or durable)`);
|
|
20826
|
-
}
|
|
20827
|
-
options.type = value;
|
|
20828
|
-
break;
|
|
20829
|
-
}
|
|
20830
|
-
case "--rename":
|
|
20831
|
-
options.rename = value;
|
|
20832
|
-
break;
|
|
20833
20118
|
case "--aspect":
|
|
20834
20119
|
options.aspect = value;
|
|
20835
20120
|
break;
|
|
@@ -21001,9 +20286,6 @@ async function main() {
|
|
|
21001
20286
|
case "link":
|
|
21002
20287
|
await runLink(parsed.options);
|
|
21003
20288
|
break;
|
|
21004
|
-
case "pull":
|
|
21005
|
-
await runPull(parsed.options);
|
|
21006
|
-
break;
|
|
21007
20289
|
case "rename":
|
|
21008
20290
|
await runRename(parsed.options);
|
|
21009
20291
|
break;
|
|
@@ -21013,12 +20295,6 @@ async function main() {
|
|
|
21013
20295
|
case "make-remixable":
|
|
21014
20296
|
await runMakeRemixable(parsed.options);
|
|
21015
20297
|
break;
|
|
21016
|
-
case "shop":
|
|
21017
|
-
await runShop(parsed.options);
|
|
21018
|
-
break;
|
|
21019
|
-
case "domain":
|
|
21020
|
-
await runDomain(parsed.options);
|
|
21021
|
-
break;
|
|
21022
20298
|
case "controller":
|
|
21023
20299
|
await runController({ ...parsed.options, kind: parsed.options.name });
|
|
21024
20300
|
break;
|