@genex-ai/cli-demo 1.6.4-dev.433 → 1.6.5-dev.434
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 +566 -249
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1829,8 +1829,158 @@ async function runInit(opts) {
|
|
|
1829
1829
|
}
|
|
1830
1830
|
|
|
1831
1831
|
// 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
|
|
1832
1837
|
import fs11 from "fs/promises";
|
|
1833
1838
|
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(` Run ${c.cyan("npx genex pull")} to get that work, then run this again.`);
|
|
1890
|
+
}
|
|
1891
|
+
async function sourceTreeHash(cwd) {
|
|
1892
|
+
const gitDir = await fs11.mkdtemp(path11.join(os6.tmpdir(), "genex-tree-"));
|
|
1893
|
+
const base = { GIT_DIR: gitDir };
|
|
1894
|
+
try {
|
|
1895
|
+
if ((await run("git", ["init", "-q"], base)).code !== 0) return null;
|
|
1896
|
+
await fs11.writeFile(
|
|
1897
|
+
path11.join(gitDir, "info", "exclude"),
|
|
1898
|
+
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
1899
|
+
);
|
|
1900
|
+
if ((await run("git", ["lfs", "version"], base)).code === 0) {
|
|
1901
|
+
const filters = [
|
|
1902
|
+
["filter.lfs.clean", "git-lfs clean -- %f"],
|
|
1903
|
+
["filter.lfs.smudge", "git-lfs smudge -- %f"],
|
|
1904
|
+
["filter.lfs.process", "git-lfs filter-process"],
|
|
1905
|
+
["filter.lfs.required", "true"]
|
|
1906
|
+
];
|
|
1907
|
+
for (const [key, value] of filters) await run("git", ["config", key, value], base);
|
|
1908
|
+
}
|
|
1909
|
+
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path11.join(gitDir, "index-tree") };
|
|
1910
|
+
if ((await run("git", ["add", "-A"], env)).code !== 0) return null;
|
|
1911
|
+
const tree = (await run("git", ["write-tree"], env)).out.trim();
|
|
1912
|
+
return /^[0-9a-f]{40}$/.test(tree) ? tree : null;
|
|
1913
|
+
} catch {
|
|
1914
|
+
return null;
|
|
1915
|
+
} finally {
|
|
1916
|
+
await fs11.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
function urlHasEmbeddedCredentials(url) {
|
|
1921
|
+
return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(url);
|
|
1922
|
+
}
|
|
1923
|
+
function credentialHelperOff(url) {
|
|
1924
|
+
if (!urlHasEmbeddedCredentials(url)) return {};
|
|
1925
|
+
return { GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "credential.helper", GIT_CONFIG_VALUE_0: "" };
|
|
1926
|
+
}
|
|
1927
|
+
async function fetchCloneGrant(apiUrl, token, projectId, log) {
|
|
1928
|
+
let res;
|
|
1929
|
+
try {
|
|
1930
|
+
res = await apiFetch(`${apiUrl}/api/projects/${projectId}/push-token`, {
|
|
1931
|
+
method: "POST",
|
|
1932
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1933
|
+
});
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
log.error(`Couldn't reach the API to authorize the source read: ${String(err)}`);
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
if (res.status === 401) {
|
|
1939
|
+
log.error("Not authorized \u2014 your token may have expired. Re-run `genex auth`.");
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
if (!res.ok) {
|
|
1943
|
+
log.error(`Couldn't authorize the source read (HTTP ${res.status}).`);
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
const data = await res.json().catch(() => null);
|
|
1947
|
+
const url = data?.pushUrl ?? data?.cloneUrl;
|
|
1948
|
+
if (!url) {
|
|
1949
|
+
log.error("The API didn't return a source URL.");
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
return { cloneUrl: url, sourceRef: data?.sourceRef ?? null };
|
|
1953
|
+
}
|
|
1954
|
+
async function cloneSource(grant, dest, log) {
|
|
1955
|
+
const args = grant.sourceRef ? ["clone", "--branch", grant.sourceRef, grant.cloneUrl, dest] : ["clone", grant.cloneUrl, dest];
|
|
1956
|
+
const env = {
|
|
1957
|
+
GIT_LFS_SKIP_SMUDGE: "1",
|
|
1958
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1959
|
+
...credentialHelperOff(grant.cloneUrl)
|
|
1960
|
+
};
|
|
1961
|
+
const cloned = await run("git", args, env);
|
|
1962
|
+
if (cloned.code !== 0) {
|
|
1963
|
+
log.error("Couldn't download the game's source.");
|
|
1964
|
+
log.dim(` git clone exited ${cloned.code}`);
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
const repo = { ...env, GIT_DIR: path11.join(dest, ".git"), GIT_WORK_TREE: dest };
|
|
1968
|
+
const filters = [
|
|
1969
|
+
["filter.lfs.clean", "git-lfs clean -- %f"],
|
|
1970
|
+
["filter.lfs.smudge", "git-lfs smudge -- %f"],
|
|
1971
|
+
["filter.lfs.process", "git-lfs filter-process"],
|
|
1972
|
+
["filter.lfs.required", "true"]
|
|
1973
|
+
];
|
|
1974
|
+
for (const [key, value] of filters) await run("git", ["config", key, value], repo);
|
|
1975
|
+
const pulled = await run("git", ["lfs", "pull"], repo);
|
|
1976
|
+
if (pulled.code !== 0) {
|
|
1977
|
+
log.warn("Binary assets (models, textures, audio) are still pointer files \u2014 `git lfs pull` failed.");
|
|
1978
|
+
log.dim(" The source is there; run `git lfs pull` in this folder once git-lfs works.");
|
|
1979
|
+
}
|
|
1980
|
+
return true;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/commands/link.ts
|
|
1834
1984
|
async function runLink(opts) {
|
|
1835
1985
|
const log = createLogger({ quiet: opts.quiet });
|
|
1836
1986
|
log.plain(c.bold("genex link"));
|
|
@@ -1878,7 +2028,9 @@ async function runLink(opts) {
|
|
|
1878
2028
|
process.exitCode = 1;
|
|
1879
2029
|
return;
|
|
1880
2030
|
}
|
|
2031
|
+
const wasEmpty = await isEmptyDir(process.cwd());
|
|
1881
2032
|
await writeGitignore(process.cwd(), log);
|
|
2033
|
+
const cloned = wasEmpty ? await downloadSource(apiUrl, token, project, log) : false;
|
|
1882
2034
|
const meta = {
|
|
1883
2035
|
id: project.id,
|
|
1884
2036
|
slug: project.slug,
|
|
@@ -1892,30 +2044,72 @@ async function runLink(opts) {
|
|
|
1892
2044
|
log.dim(` saved ${c.cyan(metaPath)}`);
|
|
1893
2045
|
await writeGameConfigFiles(meta, log);
|
|
1894
2046
|
await ensureSlugEnv(project.slug, log);
|
|
2047
|
+
if (cloned) {
|
|
2048
|
+
const remote = await readRemoteSource(apiUrl, token, project.id);
|
|
2049
|
+
const tree = await sourceTreeHash(process.cwd());
|
|
2050
|
+
if (remote?.stagingCommit) meta.stagingCommit = remote.stagingCommit;
|
|
2051
|
+
if (tree) meta.sourceTree = tree;
|
|
2052
|
+
await writeProject(meta);
|
|
2053
|
+
}
|
|
1895
2054
|
log.plain("");
|
|
1896
2055
|
log.success(
|
|
1897
2056
|
`Linked. This folder now updates ${c.cyan(project.slug)} \u2014 \`npx genex preview\` / \`publish\` ship to the same live game.`
|
|
1898
2057
|
);
|
|
2058
|
+
if (cloned) log.dim(` Run ${c.cyan("npm install")}, then keep building.`);
|
|
1899
2059
|
if (project.playUrl) log.dim(` play URL: ${project.playUrl}`);
|
|
1900
2060
|
}
|
|
2061
|
+
async function isEmptyDir(cwd) {
|
|
2062
|
+
try {
|
|
2063
|
+
const entries = await fs12.readdir(cwd);
|
|
2064
|
+
return entries.every((e) => e === ".git" || e === ".genex" || e === ".DS_Store");
|
|
2065
|
+
} catch {
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
async function downloadSource(apiUrl, token, project, log) {
|
|
2070
|
+
const grant = await fetchCloneGrant(apiUrl, token, project.id, log);
|
|
2071
|
+
if (!grant) return false;
|
|
2072
|
+
if (!grant.sourceRef) {
|
|
2073
|
+
log.dim(" (no source saved for this game yet \u2014 nothing to download)");
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
log.step(`Downloading the ${grant.sourceRef === "preview" ? "draft" : "published"} source\u2026`);
|
|
2077
|
+
const staging = await fs12.mkdtemp(path12.join(os7.tmpdir(), "genex-link-"));
|
|
2078
|
+
const fresh = path12.join(staging, "source");
|
|
2079
|
+
try {
|
|
2080
|
+
if (!await cloneSource(grant, fresh, log)) return false;
|
|
2081
|
+
await fs12.rm(path12.join(fresh, ".git"), { recursive: true, force: true });
|
|
2082
|
+
for (const entry of await fs12.readdir(fresh)) {
|
|
2083
|
+
await fs12.cp(path12.join(fresh, entry), path12.join(process.cwd(), entry), {
|
|
2084
|
+
recursive: true,
|
|
2085
|
+
force: true
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
log.success("Downloaded.");
|
|
2089
|
+
return true;
|
|
2090
|
+
} finally {
|
|
2091
|
+
await fs12.rm(staging, { recursive: true, force: true }).catch(() => {
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
1901
2095
|
async function ensureSlugEnv(slug, log, cwd = process.cwd()) {
|
|
1902
|
-
const file =
|
|
2096
|
+
const file = path12.join(cwd, ".env");
|
|
1903
2097
|
let content;
|
|
1904
2098
|
try {
|
|
1905
|
-
content = await
|
|
2099
|
+
content = await fs12.readFile(file, "utf8");
|
|
1906
2100
|
} catch {
|
|
1907
2101
|
return;
|
|
1908
2102
|
}
|
|
1909
2103
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
1910
2104
|
const m = content.match(re);
|
|
1911
2105
|
if (!m) {
|
|
1912
|
-
await
|
|
2106
|
+
await fs12.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
|
|
1913
2107
|
`);
|
|
1914
2108
|
log.dim(` added VITE_GENEX_SLUG=${slug} to .env`);
|
|
1915
2109
|
return;
|
|
1916
2110
|
}
|
|
1917
2111
|
if (m[2].trim() === slug) return;
|
|
1918
|
-
await
|
|
2112
|
+
await fs12.writeFile(file, content.replace(re, `$1${slug}`));
|
|
1919
2113
|
log.dim(` updated .env: VITE_GENEX_SLUG=${slug} (was ${m[2].trim()})`);
|
|
1920
2114
|
}
|
|
1921
2115
|
async function fetchOwnProject(apiUrl, token, slug, log) {
|
|
@@ -1968,9 +2162,108 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
1968
2162
|
}
|
|
1969
2163
|
}
|
|
1970
2164
|
|
|
2165
|
+
// src/commands/pull.ts
|
|
2166
|
+
import fs13 from "fs/promises";
|
|
2167
|
+
import path13 from "path";
|
|
2168
|
+
import os8 from "os";
|
|
2169
|
+
function isMachineLocal(entry) {
|
|
2170
|
+
if (entry === ".genex" || entry === "node_modules" || entry === ".git") return true;
|
|
2171
|
+
if (entry === ".env.example") return false;
|
|
2172
|
+
return entry === ".env" || entry.startsWith(".env.");
|
|
2173
|
+
}
|
|
2174
|
+
async function runPull(opts) {
|
|
2175
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
2176
|
+
const cwd = process.cwd();
|
|
2177
|
+
log.plain(c.bold("genex pull"));
|
|
2178
|
+
log.plain("");
|
|
2179
|
+
const meta = await readProject(cwd);
|
|
2180
|
+
if (!meta) {
|
|
2181
|
+
log.error("This folder isn't linked to a game.");
|
|
2182
|
+
log.dim(` Run ${c.cyan("npx genex link <slug>")} here, or in an empty folder to download it.`);
|
|
2183
|
+
process.exitCode = 1;
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const token = await readUserToken(opts.envPath);
|
|
2187
|
+
if (!token) {
|
|
2188
|
+
log.error(`Not signed in. Run ${c.cyan("npx genex auth")} first.`);
|
|
2189
|
+
process.exitCode = 1;
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
2193
|
+
if (!opts.force) {
|
|
2194
|
+
const tree2 = await sourceTreeHash(cwd);
|
|
2195
|
+
if (!meta.sourceTree) {
|
|
2196
|
+
log.error("This folder has never deployed, so there's no way to tell what's unsaved here.");
|
|
2197
|
+
log.dim(` ${c.cyan("npx genex preview")} first to save it, or ${c.cyan("npx genex pull --force")} to discard it.`);
|
|
2198
|
+
process.exitCode = 1;
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (tree2 === null) {
|
|
2202
|
+
log.error("Couldn't check this folder for unsaved changes (git is unavailable).");
|
|
2203
|
+
log.dim(` ${c.cyan("npx genex pull --force")} replaces it anyway.`);
|
|
2204
|
+
process.exitCode = 1;
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
if (tree2 !== meta.sourceTree) {
|
|
2208
|
+
log.error("This folder has changes that were never deployed.");
|
|
2209
|
+
log.dim(` ${c.cyan("npx genex preview")} saves them first, or ${c.cyan("npx genex pull --force")} discards them.`);
|
|
2210
|
+
process.exitCode = 1;
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
log.step(`Fetching ${c.cyan(meta.slug)}\u2026`);
|
|
2215
|
+
const grant = await fetchCloneGrant(apiUrl, token, meta.id, log);
|
|
2216
|
+
if (!grant) {
|
|
2217
|
+
process.exitCode = 1;
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (!grant.sourceRef) {
|
|
2221
|
+
log.error(`${c.cyan(meta.slug)} has no source saved yet \u2014 nothing to pull.`);
|
|
2222
|
+
log.dim(` Run ${c.cyan("npx genex preview")} on the machine that has the game.`);
|
|
2223
|
+
process.exitCode = 1;
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const staging = await fs13.mkdtemp(path13.join(os8.tmpdir(), "genex-pull-"));
|
|
2227
|
+
const fresh = path13.join(staging, "source");
|
|
2228
|
+
try {
|
|
2229
|
+
if (!await cloneSource(grant, fresh, log)) {
|
|
2230
|
+
process.exitCode = 1;
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
await fs13.rm(path13.join(fresh, ".git"), { recursive: true, force: true });
|
|
2234
|
+
await replaceTree(cwd, fresh);
|
|
2235
|
+
} finally {
|
|
2236
|
+
await fs13.rm(staging, { recursive: true, force: true }).catch(() => {
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
const remote = await readRemoteSource(apiUrl, token, meta.id);
|
|
2240
|
+
const tree = await sourceTreeHash(cwd);
|
|
2241
|
+
await writeProject(
|
|
2242
|
+
{
|
|
2243
|
+
...meta,
|
|
2244
|
+
...remote?.stagingCommit ? { stagingCommit: remote.stagingCommit } : {},
|
|
2245
|
+
...tree ? { sourceTree: tree } : {}
|
|
2246
|
+
},
|
|
2247
|
+
cwd
|
|
2248
|
+
);
|
|
2249
|
+
log.plain("");
|
|
2250
|
+
log.success(`Pulled ${c.cyan(meta.slug)}${grant.sourceRef === "preview" ? " (draft)" : ""}.`);
|
|
2251
|
+
log.dim(` Run ${c.cyan("npm install")} if dependencies changed, then keep building.`);
|
|
2252
|
+
}
|
|
2253
|
+
async function replaceTree(dest, src) {
|
|
2254
|
+
for (const entry of await fs13.readdir(dest)) {
|
|
2255
|
+
if (isMachineLocal(entry)) continue;
|
|
2256
|
+
await fs13.rm(path13.join(dest, entry), { recursive: true, force: true });
|
|
2257
|
+
}
|
|
2258
|
+
for (const entry of await fs13.readdir(src)) {
|
|
2259
|
+
if (isMachineLocal(entry)) continue;
|
|
2260
|
+
await fs13.cp(path13.join(src, entry), path13.join(dest, entry), { recursive: true });
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1971
2264
|
// src/commands/rename.ts
|
|
1972
|
-
import
|
|
1973
|
-
import
|
|
2265
|
+
import fs14 from "fs/promises";
|
|
2266
|
+
import path14 from "path";
|
|
1974
2267
|
async function runRename(opts) {
|
|
1975
2268
|
const log = createLogger({ quiet: opts.quiet });
|
|
1976
2269
|
log.plain(c.bold("genex rename"));
|
|
@@ -2058,23 +2351,23 @@ async function runRename(opts) {
|
|
|
2058
2351
|
log.info("Run `genex preview` (or `publish`) to rebuild \u2014 the new slug is baked into the bundle.");
|
|
2059
2352
|
}
|
|
2060
2353
|
async function rewriteSlugEnv(from, to, log, cwd = process.cwd()) {
|
|
2061
|
-
const file =
|
|
2354
|
+
const file = path14.join(cwd, ".env");
|
|
2062
2355
|
let content;
|
|
2063
2356
|
try {
|
|
2064
|
-
content = await
|
|
2357
|
+
content = await fs14.readFile(file, "utf8");
|
|
2065
2358
|
} catch {
|
|
2066
2359
|
return;
|
|
2067
2360
|
}
|
|
2068
2361
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
2069
2362
|
if (!re.test(content)) return;
|
|
2070
|
-
await
|
|
2363
|
+
await fs14.writeFile(file, content.replace(re, `$1${to}`));
|
|
2071
2364
|
log.dim(` .env: VITE_GENEX_SLUG=${to} (was ${from})`);
|
|
2072
2365
|
}
|
|
2073
2366
|
async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
2074
|
-
const file =
|
|
2367
|
+
const file = path14.join(cwd, "src", "genex.config.ts");
|
|
2075
2368
|
let content;
|
|
2076
2369
|
try {
|
|
2077
|
-
content = await
|
|
2370
|
+
content = await fs14.readFile(file, "utf8");
|
|
2078
2371
|
} catch {
|
|
2079
2372
|
return;
|
|
2080
2373
|
}
|
|
@@ -2084,7 +2377,7 @@ async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
|
2084
2377
|
log.warn(` src/genex.config.ts has no "${from}" literal \u2014 check its slug by hand.`);
|
|
2085
2378
|
return;
|
|
2086
2379
|
}
|
|
2087
|
-
await
|
|
2380
|
+
await fs14.writeFile(file, content.replace(quoted, `"${to}"`));
|
|
2088
2381
|
log.dim(` src/genex.config.ts: baked slug -> ${to}`);
|
|
2089
2382
|
}
|
|
2090
2383
|
|
|
@@ -2212,11 +2505,11 @@ function relTime(iso) {
|
|
|
2212
2505
|
}
|
|
2213
2506
|
|
|
2214
2507
|
// src/lib/deploy.ts
|
|
2215
|
-
import
|
|
2508
|
+
import "child_process";
|
|
2216
2509
|
import crypto3 from "crypto";
|
|
2217
|
-
import
|
|
2218
|
-
import
|
|
2219
|
-
import
|
|
2510
|
+
import fs17 from "fs/promises";
|
|
2511
|
+
import os9 from "os";
|
|
2512
|
+
import path16 from "path";
|
|
2220
2513
|
|
|
2221
2514
|
// ../../packages/mobile-scan/src/image-dims.ts
|
|
2222
2515
|
function u32be(b, o) {
|
|
@@ -2480,12 +2773,12 @@ function tierFor(estVramMb) {
|
|
|
2480
2773
|
}
|
|
2481
2774
|
|
|
2482
2775
|
// src/commands/ui.ts
|
|
2483
|
-
import
|
|
2484
|
-
import
|
|
2776
|
+
import fs16 from "fs/promises";
|
|
2777
|
+
import path15 from "path";
|
|
2485
2778
|
import { PNG as PNG2 } from "pngjs";
|
|
2486
2779
|
|
|
2487
2780
|
// src/lib/png-tools.ts
|
|
2488
|
-
import
|
|
2781
|
+
import fs15 from "fs/promises";
|
|
2489
2782
|
import { PNG } from "pngjs";
|
|
2490
2783
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
2491
2784
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -2496,12 +2789,12 @@ async function loadPng(input) {
|
|
|
2496
2789
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
2497
2790
|
buf = Buffer.from(await res.arrayBuffer());
|
|
2498
2791
|
} else {
|
|
2499
|
-
buf = await
|
|
2792
|
+
buf = await fs15.readFile(input);
|
|
2500
2793
|
}
|
|
2501
2794
|
return PNG.sync.read(buf);
|
|
2502
2795
|
}
|
|
2503
2796
|
async function writePng(file, png) {
|
|
2504
|
-
await
|
|
2797
|
+
await fs15.writeFile(file, PNG.sync.write(png));
|
|
2505
2798
|
}
|
|
2506
2799
|
function cropPng(image, box) {
|
|
2507
2800
|
const out = new PNG({ width: box.w, height: box.h });
|
|
@@ -2801,7 +3094,7 @@ async function uiExtract(opts, log) {
|
|
|
2801
3094
|
const dilatePx = opts.dilate ?? 0;
|
|
2802
3095
|
const sheet = await loadPng(input);
|
|
2803
3096
|
const { width: W, height: H, data } = sheet;
|
|
2804
|
-
await
|
|
3097
|
+
await fs16.mkdir(outDir, { recursive: true });
|
|
2805
3098
|
log.plain(c.bold("genex ui extract"));
|
|
2806
3099
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
|
|
2807
3100
|
let hasTransparency = false;
|
|
@@ -3030,7 +3323,7 @@ async function uiExtract(opts, log) {
|
|
|
3030
3323
|
rimPixels: speckle.sampled
|
|
3031
3324
|
});
|
|
3032
3325
|
}
|
|
3033
|
-
const outPath =
|
|
3326
|
+
const outPath = path15.join(outDir, `${name}.png`);
|
|
3034
3327
|
await writePng(outPath, out);
|
|
3035
3328
|
const sidecar = {
|
|
3036
3329
|
name,
|
|
@@ -3049,7 +3342,7 @@ async function uiExtract(opts, log) {
|
|
|
3049
3342
|
defringed
|
|
3050
3343
|
};
|
|
3051
3344
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
3052
|
-
await
|
|
3345
|
+
await fs16.writeFile(
|
|
3053
3346
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
3054
3347
|
JSON.stringify(sidecarBody, null, 2)
|
|
3055
3348
|
);
|
|
@@ -3058,8 +3351,8 @@ async function uiExtract(opts, log) {
|
|
|
3058
3351
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
3059
3352
|
);
|
|
3060
3353
|
}
|
|
3061
|
-
const debugPath =
|
|
3062
|
-
await
|
|
3354
|
+
const debugPath = path15.join(outDir, "extract-debug.json");
|
|
3355
|
+
await fs16.writeFile(
|
|
3063
3356
|
debugPath,
|
|
3064
3357
|
JSON.stringify(
|
|
3065
3358
|
{
|
|
@@ -3521,7 +3814,7 @@ async function uiMasks(opts, log) {
|
|
|
3521
3814
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
3522
3815
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
3523
3816
|
const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
|
|
3524
|
-
await
|
|
3817
|
+
await fs16.mkdir(outDir, { recursive: true });
|
|
3525
3818
|
log.plain(c.bold("genex ui masks"));
|
|
3526
3819
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
3527
3820
|
const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
|
|
@@ -3574,11 +3867,11 @@ async function uiMasks(opts, log) {
|
|
|
3574
3867
|
});
|
|
3575
3868
|
}
|
|
3576
3869
|
const overlay = makeOverlay(clean2, converted.png);
|
|
3577
|
-
const framePath =
|
|
3578
|
-
const maskPath =
|
|
3579
|
-
const annotatedPath =
|
|
3580
|
-
const overlayPath =
|
|
3581
|
-
const metaPath =
|
|
3870
|
+
const framePath = path15.join(outDir, `${pair.name}-frame.png`);
|
|
3871
|
+
const maskPath = path15.join(outDir, `${pair.name}-mask.png`);
|
|
3872
|
+
const annotatedPath = path15.join(outDir, `${pair.name}-annotated-source.png`);
|
|
3873
|
+
const overlayPath = path15.join(outDir, `${pair.name}-overlay.png`);
|
|
3874
|
+
const metaPath = path15.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
3582
3875
|
await writePng(framePath, clean2);
|
|
3583
3876
|
await writePng(maskPath, converted.png);
|
|
3584
3877
|
await writePng(annotatedPath, annotated);
|
|
@@ -3625,7 +3918,7 @@ async function uiMasks(opts, log) {
|
|
|
3625
3918
|
},
|
|
3626
3919
|
overlay: overlayPath
|
|
3627
3920
|
};
|
|
3628
|
-
await
|
|
3921
|
+
await fs16.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
3629
3922
|
`);
|
|
3630
3923
|
results.push(meta);
|
|
3631
3924
|
const fb = converted.bbox;
|
|
@@ -3641,8 +3934,8 @@ async function uiMasks(opts, log) {
|
|
|
3641
3934
|
);
|
|
3642
3935
|
}
|
|
3643
3936
|
}
|
|
3644
|
-
const indexPath =
|
|
3645
|
-
await
|
|
3937
|
+
const indexPath = path15.join(outDir, "annotated-progress.json");
|
|
3938
|
+
await fs16.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
|
|
3646
3939
|
`);
|
|
3647
3940
|
log.plain("");
|
|
3648
3941
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -3768,7 +4061,7 @@ async function uiTextColor(opts, log) {
|
|
|
3768
4061
|
};
|
|
3769
4062
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3770
4063
|
`);
|
|
3771
|
-
if (opts.out) await
|
|
4064
|
+
if (opts.out) await fs16.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
3772
4065
|
`);
|
|
3773
4066
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
3774
4067
|
}
|
|
@@ -3800,7 +4093,7 @@ async function uiTrim(opts, log) {
|
|
|
3800
4093
|
const sidecar = computeBBoxes(trimmed);
|
|
3801
4094
|
const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
|
|
3802
4095
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
3803
|
-
await
|
|
4096
|
+
await fs16.writeFile(
|
|
3804
4097
|
sidecarPath,
|
|
3805
4098
|
JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
|
|
3806
4099
|
);
|
|
@@ -3907,7 +4200,7 @@ async function uiPlate(opts, log) {
|
|
|
3907
4200
|
fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
|
|
3908
4201
|
}
|
|
3909
4202
|
await writePng(outPath, out);
|
|
3910
|
-
const name =
|
|
4203
|
+
const name = path15.basename(outPath);
|
|
3911
4204
|
log.plain(c.bold("genex ui plate"));
|
|
3912
4205
|
log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
|
|
3913
4206
|
log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
|
|
@@ -4072,13 +4365,13 @@ async function walkFiles(dir) {
|
|
|
4072
4365
|
const out = [];
|
|
4073
4366
|
let entries;
|
|
4074
4367
|
try {
|
|
4075
|
-
entries = await
|
|
4368
|
+
entries = await fs16.readdir(dir, { withFileTypes: true });
|
|
4076
4369
|
} catch {
|
|
4077
4370
|
return out;
|
|
4078
4371
|
}
|
|
4079
4372
|
for (const entry of entries) {
|
|
4080
4373
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
4081
|
-
const p =
|
|
4374
|
+
const p = path15.join(dir, entry.name);
|
|
4082
4375
|
if (entry.isDirectory()) out.push(...await walkFiles(p));
|
|
4083
4376
|
else out.push(p);
|
|
4084
4377
|
}
|
|
@@ -4087,7 +4380,7 @@ async function walkFiles(dir) {
|
|
|
4087
4380
|
async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
4088
4381
|
const viewportFindings = [];
|
|
4089
4382
|
try {
|
|
4090
|
-
const indexHtml = await
|
|
4383
|
+
const indexHtml = await fs16.readFile(path15.join(cwd, "index.html"), "utf8");
|
|
4091
4384
|
if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
|
|
4092
4385
|
viewportFindings.push({
|
|
4093
4386
|
kind: "viewport-meta",
|
|
@@ -4101,28 +4394,28 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4101
4394
|
}
|
|
4102
4395
|
} catch {
|
|
4103
4396
|
}
|
|
4104
|
-
const absAssets =
|
|
4397
|
+
const absAssets = path15.resolve(cwd, assetDir);
|
|
4105
4398
|
try {
|
|
4106
|
-
if (!(await
|
|
4399
|
+
if (!(await fs16.stat(absAssets)).isDirectory()) {
|
|
4107
4400
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
4108
4401
|
}
|
|
4109
4402
|
} catch {
|
|
4110
4403
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
4111
4404
|
}
|
|
4112
|
-
const srcFiles = (await walkFiles(
|
|
4113
|
-
(p) => AUDIT_SRC_EXTS.has(
|
|
4405
|
+
const srcFiles = (await walkFiles(path15.resolve(cwd, srcDir))).filter(
|
|
4406
|
+
(p) => AUDIT_SRC_EXTS.has(path15.extname(p).toLowerCase())
|
|
4114
4407
|
);
|
|
4115
4408
|
try {
|
|
4116
|
-
for (const name of await
|
|
4117
|
-
const ext =
|
|
4118
|
-
if (ext === ".html" || ext === ".css") srcFiles.push(
|
|
4409
|
+
for (const name of await fs16.readdir(cwd)) {
|
|
4410
|
+
const ext = path15.extname(name).toLowerCase();
|
|
4411
|
+
if (ext === ".html" || ext === ".css") srcFiles.push(path15.join(cwd, name));
|
|
4119
4412
|
}
|
|
4120
4413
|
} catch {
|
|
4121
4414
|
}
|
|
4122
4415
|
const sources = [];
|
|
4123
4416
|
for (const p of srcFiles) {
|
|
4124
4417
|
try {
|
|
4125
|
-
sources.push({ rel:
|
|
4418
|
+
sources.push({ rel: path15.relative(cwd, p), text: await fs16.readFile(p, "utf8") });
|
|
4126
4419
|
} catch {
|
|
4127
4420
|
}
|
|
4128
4421
|
}
|
|
@@ -4132,11 +4425,11 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4132
4425
|
const metaByName = /* @__PURE__ */ new Map();
|
|
4133
4426
|
const bboxByPng = /* @__PURE__ */ new Map();
|
|
4134
4427
|
for (const p of assetFiles) {
|
|
4135
|
-
const base =
|
|
4428
|
+
const base = path15.basename(p);
|
|
4136
4429
|
const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
|
|
4137
4430
|
if (metaMatch) {
|
|
4138
4431
|
try {
|
|
4139
|
-
const meta = JSON.parse(await
|
|
4432
|
+
const meta = JSON.parse(await fs16.readFile(p, "utf8"));
|
|
4140
4433
|
metaByName.set(metaMatch[1], {
|
|
4141
4434
|
cleanCrop: meta.clean?.crop ?? null,
|
|
4142
4435
|
loosened: meta.loosened === true
|
|
@@ -4147,7 +4440,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4147
4440
|
}
|
|
4148
4441
|
if (base.endsWith(".bbox.json")) {
|
|
4149
4442
|
try {
|
|
4150
|
-
const sidecar = JSON.parse(await
|
|
4443
|
+
const sidecar = JSON.parse(await fs16.readFile(p, "utf8"));
|
|
4151
4444
|
if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
|
|
4152
4445
|
} catch {
|
|
4153
4446
|
}
|
|
@@ -4156,7 +4449,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4156
4449
|
for (const [name, meta] of metaByName) {
|
|
4157
4450
|
if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
|
|
4158
4451
|
for (const p of assetFiles) {
|
|
4159
|
-
const base =
|
|
4452
|
+
const base = path15.basename(p);
|
|
4160
4453
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4161
4454
|
if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
|
|
4162
4455
|
if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
|
|
@@ -4180,7 +4473,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4180
4473
|
}
|
|
4181
4474
|
const pngByBase = /* @__PURE__ */ new Map();
|
|
4182
4475
|
for (const p of assetFiles) {
|
|
4183
|
-
const base =
|
|
4476
|
+
const base = path15.basename(p);
|
|
4184
4477
|
if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
|
|
4185
4478
|
}
|
|
4186
4479
|
for (const [maskBase, maskPath] of pngByBase) {
|
|
@@ -4193,8 +4486,8 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4193
4486
|
let frame;
|
|
4194
4487
|
let mask;
|
|
4195
4488
|
try {
|
|
4196
|
-
frame = PNG2.sync.read(await
|
|
4197
|
-
mask = PNG2.sync.read(await
|
|
4489
|
+
frame = PNG2.sync.read(await fs16.readFile(framePath));
|
|
4490
|
+
mask = PNG2.sync.read(await fs16.readFile(maskPath));
|
|
4198
4491
|
} catch {
|
|
4199
4492
|
continue;
|
|
4200
4493
|
}
|
|
@@ -4213,7 +4506,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4213
4506
|
if (!referenced(base)) continue;
|
|
4214
4507
|
let png;
|
|
4215
4508
|
try {
|
|
4216
|
-
png = PNG2.sync.read(await
|
|
4509
|
+
png = PNG2.sync.read(await fs16.readFile(p));
|
|
4217
4510
|
} catch {
|
|
4218
4511
|
continue;
|
|
4219
4512
|
}
|
|
@@ -4233,7 +4526,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4233
4526
|
}
|
|
4234
4527
|
const maskReported = /* @__PURE__ */ new Set();
|
|
4235
4528
|
for (const p of assetFiles) {
|
|
4236
|
-
const m = /^(.+)\.annotated-progress\.json$/.exec(
|
|
4529
|
+
const m = /^(.+)\.annotated-progress\.json$/.exec(path15.basename(p));
|
|
4237
4530
|
if (!m) continue;
|
|
4238
4531
|
const maskBase = `${m[1]}-mask.png`;
|
|
4239
4532
|
if (!referenced(maskBase)) {
|
|
@@ -4245,14 +4538,14 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4245
4538
|
}
|
|
4246
4539
|
}
|
|
4247
4540
|
for (const p of assetFiles) {
|
|
4248
|
-
const base =
|
|
4541
|
+
const base = path15.basename(p);
|
|
4249
4542
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4250
4543
|
if (/-annotated-source\.png$|-overlay\.png$/.test(base)) continue;
|
|
4251
4544
|
if (maskReported.has(base)) continue;
|
|
4252
4545
|
if (!referenced(base)) {
|
|
4253
4546
|
findings.push({
|
|
4254
4547
|
kind: "unwired-sprite",
|
|
4255
|
-
message: `${
|
|
4548
|
+
message: `${path15.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.)`
|
|
4256
4549
|
});
|
|
4257
4550
|
}
|
|
4258
4551
|
}
|
|
@@ -4305,25 +4598,6 @@ async function uiAudit(opts, log) {
|
|
|
4305
4598
|
}
|
|
4306
4599
|
|
|
4307
4600
|
// src/lib/deploy.ts
|
|
4308
|
-
var WIN_SHELL_COMMANDS = /* @__PURE__ */ new Set(["npm", "npx"]);
|
|
4309
|
-
function run(cmd, args, env) {
|
|
4310
|
-
const shell = process.platform === "win32" && WIN_SHELL_COMMANDS.has(cmd);
|
|
4311
|
-
return new Promise((resolve) => {
|
|
4312
|
-
let child;
|
|
4313
|
-
try {
|
|
4314
|
-
child = spawn3(cmd, args, { env: env ? { ...process.env, ...env } : process.env, shell });
|
|
4315
|
-
} catch {
|
|
4316
|
-
resolve({ code: -1, out: "", err: `${cmd} not found` });
|
|
4317
|
-
return;
|
|
4318
|
-
}
|
|
4319
|
-
let out = "";
|
|
4320
|
-
let err = "";
|
|
4321
|
-
child.stdout?.on("data", (d) => out += String(d));
|
|
4322
|
-
child.stderr?.on("data", (d) => err += String(d));
|
|
4323
|
-
child.on("error", () => resolve({ code: -1, out, err: `${cmd} not found` }));
|
|
4324
|
-
child.on("close", (code2) => resolve({ code: code2 ?? -1, out, err }));
|
|
4325
|
-
});
|
|
4326
|
-
}
|
|
4327
4601
|
function printMobilePreflight(files, log) {
|
|
4328
4602
|
try {
|
|
4329
4603
|
const scan = scanBundle(files.map((f) => ({ relPath: f.relPath, bytes: f.bytes })));
|
|
@@ -4386,7 +4660,7 @@ async function printUiAuditPreflight(log) {
|
|
|
4386
4660
|
}
|
|
4387
4661
|
async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
4388
4662
|
try {
|
|
4389
|
-
const design = await
|
|
4663
|
+
const design = await fs17.readFile(path16.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
|
|
4390
4664
|
const warnings = [];
|
|
4391
4665
|
if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
|
|
4392
4666
|
warnings.push(
|
|
@@ -4394,7 +4668,7 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4394
4668
|
);
|
|
4395
4669
|
}
|
|
4396
4670
|
if (!/player character:/i.test(design)) {
|
|
4397
|
-
const hasCharacter = await
|
|
4671
|
+
const hasCharacter = await fs17.access(path16.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
|
|
4398
4672
|
if (!hasCharacter && await loadsPlayerBody(cwd)) {
|
|
4399
4673
|
warnings.push(
|
|
4400
4674
|
`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).`
|
|
@@ -4408,12 +4682,12 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4408
4682
|
async function loadsPlayerBody(cwd) {
|
|
4409
4683
|
const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
|
|
4410
4684
|
try {
|
|
4411
|
-
const entries = await
|
|
4685
|
+
const entries = await fs17.readdir(path16.join(cwd, "src"), { recursive: true });
|
|
4412
4686
|
for (const rel of entries) {
|
|
4413
4687
|
if (rel.includes("node_modules")) continue;
|
|
4414
|
-
if (rel.split(
|
|
4688
|
+
if (rel.split(path16.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
|
|
4415
4689
|
if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
|
|
4416
|
-
const text = await
|
|
4690
|
+
const text = await fs17.readFile(path16.join(cwd, "src", rel), "utf8").catch(() => "");
|
|
4417
4691
|
if (BODY_LOADERS.test(text)) return true;
|
|
4418
4692
|
}
|
|
4419
4693
|
} catch {
|
|
@@ -4426,6 +4700,15 @@ function isSecretEnvFile(name) {
|
|
|
4426
4700
|
}
|
|
4427
4701
|
async function deployGame(ctx, opts, log) {
|
|
4428
4702
|
const cwd = process.cwd();
|
|
4703
|
+
const meta = await readProject(cwd);
|
|
4704
|
+
const expectStagingCommit = meta?.stagingCommit;
|
|
4705
|
+
if (expectStagingCommit) {
|
|
4706
|
+
const remote = await readRemoteSource(ctx.apiUrl, ctx.token, ctx.projectId);
|
|
4707
|
+
if (remote && isStale(expectStagingCommit, remote.stagingCommit)) {
|
|
4708
|
+
reportStale(log, ctx.slug ?? meta?.slug ?? "this game", expectStagingCommit, remote.stagingCommit);
|
|
4709
|
+
return false;
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4429
4712
|
if (!opts.noBuild && await hasBuildScript(cwd)) {
|
|
4430
4713
|
log.step("Building the production bundle\u2026");
|
|
4431
4714
|
const built = await run("npm", ["run", "build"]);
|
|
@@ -4437,9 +4720,9 @@ async function deployGame(ctx, opts, log) {
|
|
|
4437
4720
|
}
|
|
4438
4721
|
log.success("Built.");
|
|
4439
4722
|
}
|
|
4440
|
-
const distDir =
|
|
4723
|
+
const distDir = path16.join(cwd, "dist");
|
|
4441
4724
|
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
4442
|
-
const rel =
|
|
4725
|
+
const rel = path16.relative(cwd, siteDir) || ".";
|
|
4443
4726
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
4444
4727
|
const files = await collectFiles(siteDir);
|
|
4445
4728
|
if (files.length === 0) {
|
|
@@ -4495,10 +4778,28 @@ async function deployGame(ctx, opts, log) {
|
|
|
4495
4778
|
log.error("Couldn't upload your game \u2014 please try again.");
|
|
4496
4779
|
return false;
|
|
4497
4780
|
}
|
|
4498
|
-
|
|
4781
|
+
const pushed = await pushSource(
|
|
4782
|
+
cwd,
|
|
4783
|
+
ctx,
|
|
4784
|
+
log,
|
|
4785
|
+
opts.channel === "staging" ? "preview" : "main",
|
|
4786
|
+
expectStagingCommit
|
|
4787
|
+
);
|
|
4788
|
+
if (pushed !== true) return false;
|
|
4499
4789
|
log.step("Publishing\u2026");
|
|
4500
4790
|
const published = await callPublish(ctx, commit, opts, log);
|
|
4501
4791
|
if (!published) return false;
|
|
4792
|
+
if (opts.channel === "staging") {
|
|
4793
|
+
const tree = await sourceTreeHash(cwd);
|
|
4794
|
+
const current = await readProject(cwd);
|
|
4795
|
+
if (current) {
|
|
4796
|
+
await writeProject(
|
|
4797
|
+
{ ...current, stagingCommit: commit, ...tree ? { sourceTree: tree } : {} },
|
|
4798
|
+
cwd
|
|
4799
|
+
).catch(() => {
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4502
4803
|
const index = files.find((f) => f.relPath === "index.html");
|
|
4503
4804
|
const liveUrl = published.url || grant.playUrl;
|
|
4504
4805
|
await waitUntilLive(liveUrl, fingerprintOf(index.bytes.toString("utf8")), opts.liveTimeoutMs ?? 2e4, log);
|
|
@@ -4506,7 +4807,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
4506
4807
|
}
|
|
4507
4808
|
async function hasBuildScript(cwd) {
|
|
4508
4809
|
try {
|
|
4509
|
-
const pkg = JSON.parse(await
|
|
4810
|
+
const pkg = JSON.parse(await fs17.readFile(path16.join(cwd, "package.json"), "utf8"));
|
|
4510
4811
|
return Boolean(pkg.scripts?.build);
|
|
4511
4812
|
} catch {
|
|
4512
4813
|
return false;
|
|
@@ -4515,12 +4816,12 @@ async function hasBuildScript(cwd) {
|
|
|
4515
4816
|
async function collectFiles(root) {
|
|
4516
4817
|
const out = [];
|
|
4517
4818
|
const walk2 = async (dir, prefix) => {
|
|
4518
|
-
for (const e of await
|
|
4819
|
+
for (const e of await fs17.readdir(dir, { withFileTypes: true })) {
|
|
4519
4820
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
4520
4821
|
if (e.isDirectory()) {
|
|
4521
|
-
if (!EXCLUDE_DIRS.has(e.name)) await walk2(
|
|
4822
|
+
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path16.join(dir, e.name), relPath);
|
|
4522
4823
|
} else if (e.isFile() && !isSecretEnvFile(e.name)) {
|
|
4523
|
-
out.push({ relPath, bytes: await
|
|
4824
|
+
out.push({ relPath, bytes: await fs17.readFile(path16.join(dir, e.name)) });
|
|
4524
4825
|
}
|
|
4525
4826
|
}
|
|
4526
4827
|
};
|
|
@@ -4737,28 +5038,27 @@ async function callPublish(ctx, commit, opts, log) {
|
|
|
4737
5038
|
}
|
|
4738
5039
|
return { url: body?.url ?? "" };
|
|
4739
5040
|
}
|
|
4740
|
-
async function pushSource(cwd, ctx, log, branch = "main") {
|
|
4741
|
-
const target = await fetchPushUrl(ctx, log);
|
|
5041
|
+
async function pushSource(cwd, ctx, log, branch = "main", expectStagingCommit) {
|
|
5042
|
+
const target = await fetchPushUrl(ctx, log, expectStagingCommit);
|
|
5043
|
+
if (target === "stale") return "stale";
|
|
4742
5044
|
if (!target) return false;
|
|
4743
5045
|
const ref = target.managed ? branch : "main";
|
|
4744
5046
|
if (await pushWorktree(cwd, target.pushUrl, target.managed, log, ref)) return true;
|
|
4745
5047
|
if (!target.managed) return false;
|
|
4746
5048
|
log.info("Retrying the source push\u2026");
|
|
4747
5049
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
4748
|
-
const fresh = await fetchPushUrl(ctx, log);
|
|
5050
|
+
const fresh = await fetchPushUrl(ctx, log, expectStagingCommit);
|
|
5051
|
+
if (fresh === "stale") return "stale";
|
|
4749
5052
|
if (!fresh) return false;
|
|
4750
5053
|
return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log, ref);
|
|
4751
5054
|
}
|
|
4752
|
-
function urlHasEmbeddedCredentials(pushUrl) {
|
|
4753
|
-
return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(pushUrl);
|
|
4754
|
-
}
|
|
4755
5055
|
async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
4756
5056
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
4757
5057
|
const failed = () => {
|
|
4758
5058
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
4759
5059
|
return false;
|
|
4760
5060
|
};
|
|
4761
|
-
const gitDir = await
|
|
5061
|
+
const gitDir = await fs17.mkdtemp(path16.join(os9.tmpdir(), "genex-source-"));
|
|
4762
5062
|
const base = { GIT_DIR: gitDir };
|
|
4763
5063
|
if (urlHasEmbeddedCredentials(pushUrl)) {
|
|
4764
5064
|
base.GIT_CONFIG_COUNT = "1";
|
|
@@ -4773,12 +5073,12 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
4773
5073
|
};
|
|
4774
5074
|
try {
|
|
4775
5075
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
4776
|
-
await
|
|
4777
|
-
|
|
5076
|
+
await fs17.writeFile(
|
|
5077
|
+
path16.join(gitDir, "info", "exclude"),
|
|
4778
5078
|
// .env* are secrets — never publish them; `!` keeps the non-secret template.
|
|
4779
5079
|
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
4780
5080
|
);
|
|
4781
|
-
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE:
|
|
5081
|
+
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path16.join(gitDir, "index-source") };
|
|
4782
5082
|
let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
|
|
4783
5083
|
if (!lfs) {
|
|
4784
5084
|
log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
|
|
@@ -4829,16 +5129,19 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
4829
5129
|
} catch {
|
|
4830
5130
|
return failed();
|
|
4831
5131
|
} finally {
|
|
4832
|
-
await
|
|
5132
|
+
await fs17.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
4833
5133
|
});
|
|
4834
5134
|
}
|
|
4835
5135
|
}
|
|
4836
|
-
async function fetchPushUrl(ctx, log) {
|
|
5136
|
+
async function fetchPushUrl(ctx, log, expectStagingCommit) {
|
|
4837
5137
|
let res;
|
|
4838
5138
|
try {
|
|
4839
5139
|
res = await apiFetch(`${ctx.apiUrl}/api/projects/${ctx.projectId}/push-token`, {
|
|
4840
5140
|
method: "POST",
|
|
4841
|
-
headers: {
|
|
5141
|
+
headers: {
|
|
5142
|
+
Authorization: `Bearer ${ctx.token}`,
|
|
5143
|
+
...expectStagingCommit ? { "If-Match": expectStagingCommit } : {}
|
|
5144
|
+
}
|
|
4842
5145
|
});
|
|
4843
5146
|
} catch (err) {
|
|
4844
5147
|
log.error(`Couldn't reach the API to authorize the source push: ${String(err)}`);
|
|
@@ -4848,6 +5151,11 @@ async function fetchPushUrl(ctx, log) {
|
|
|
4848
5151
|
log.error("Not authorized \u2014 your token may have expired. Re-run `genex init`.");
|
|
4849
5152
|
return null;
|
|
4850
5153
|
}
|
|
5154
|
+
if (res.status === 409) {
|
|
5155
|
+
const body = await res.json().catch(() => null);
|
|
5156
|
+
reportStale(log, ctx.slug ?? "this game", expectStagingCommit, body?.stagingCommit ?? null);
|
|
5157
|
+
return "stale";
|
|
5158
|
+
}
|
|
4851
5159
|
if (res.status === 429) {
|
|
4852
5160
|
log.error(`Rate limited authorizing the source push (HTTP 429).${retryAfterHint(res)}`);
|
|
4853
5161
|
return null;
|
|
@@ -4861,11 +5169,11 @@ async function fetchPushUrl(ctx, log) {
|
|
|
4861
5169
|
log.error("The API didn't return a push URL.");
|
|
4862
5170
|
return null;
|
|
4863
5171
|
}
|
|
4864
|
-
return { pushUrl: data.pushUrl, managed: data.managed !== false };
|
|
5172
|
+
return { pushUrl: data.pushUrl, managed: data.managed !== false, sourceRef: data.sourceRef ?? null };
|
|
4865
5173
|
}
|
|
4866
5174
|
async function isDir2(p) {
|
|
4867
5175
|
try {
|
|
4868
|
-
return (await
|
|
5176
|
+
return (await fs17.stat(p)).isDirectory();
|
|
4869
5177
|
} catch {
|
|
4870
5178
|
return false;
|
|
4871
5179
|
}
|
|
@@ -5312,17 +5620,17 @@ async function promoteBuild(apiUrl, projectId, token, log) {
|
|
|
5312
5620
|
}
|
|
5313
5621
|
|
|
5314
5622
|
// src/lib/detect-features.ts
|
|
5315
|
-
import
|
|
5316
|
-
import
|
|
5623
|
+
import fs19 from "fs/promises";
|
|
5624
|
+
import path18 from "path";
|
|
5317
5625
|
|
|
5318
5626
|
// src/lib/generation-ledger.ts
|
|
5319
|
-
import
|
|
5320
|
-
import
|
|
5321
|
-
var ledgerPath = (cwd) =>
|
|
5627
|
+
import fs18 from "fs/promises";
|
|
5628
|
+
import path17 from "path";
|
|
5629
|
+
var ledgerPath = (cwd) => path17.join(cwd, ".genex", "generations.ndjson");
|
|
5322
5630
|
async function append(cwd, event) {
|
|
5323
5631
|
try {
|
|
5324
|
-
await
|
|
5325
|
-
await
|
|
5632
|
+
await fs18.access(path17.join(cwd, ".genex"));
|
|
5633
|
+
await fs18.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
5326
5634
|
`, "utf8");
|
|
5327
5635
|
} catch {
|
|
5328
5636
|
}
|
|
@@ -5330,7 +5638,7 @@ async function append(cwd, event) {
|
|
|
5330
5638
|
async function readLedger(cwd = process.cwd()) {
|
|
5331
5639
|
let raw;
|
|
5332
5640
|
try {
|
|
5333
|
-
raw = await
|
|
5641
|
+
raw = await fs18.readFile(ledgerPath(cwd), "utf8");
|
|
5334
5642
|
} catch {
|
|
5335
5643
|
return [];
|
|
5336
5644
|
}
|
|
@@ -5384,7 +5692,7 @@ async function countFailed(kind, cwd = process.cwd()) {
|
|
|
5384
5692
|
// src/lib/detect-features.ts
|
|
5385
5693
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
5386
5694
|
try {
|
|
5387
|
-
const raw = await
|
|
5695
|
+
const raw = await fs19.readFile(path18.join(cwd, "package.json"), "utf8");
|
|
5388
5696
|
const pkg = JSON.parse(raw);
|
|
5389
5697
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
5390
5698
|
return typeof version === "string" && version ? version : null;
|
|
@@ -5394,7 +5702,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
5394
5702
|
}
|
|
5395
5703
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
5396
5704
|
try {
|
|
5397
|
-
const raw = await
|
|
5705
|
+
const raw = await fs19.readFile(path18.join(cwd, "package.json"), "utf8");
|
|
5398
5706
|
const pkg = JSON.parse(raw);
|
|
5399
5707
|
return Boolean(
|
|
5400
5708
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -5406,7 +5714,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
5406
5714
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
5407
5715
|
let pkg;
|
|
5408
5716
|
try {
|
|
5409
|
-
pkg = JSON.parse(await
|
|
5717
|
+
pkg = JSON.parse(await fs19.readFile(path18.join(cwd, "package.json"), "utf8"));
|
|
5410
5718
|
} catch (err) {
|
|
5411
5719
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
5412
5720
|
return null;
|
|
@@ -5424,15 +5732,15 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
5424
5732
|
var TOUCH_KIT_MARKERS = /controllers\/touch\/|controllers\/character\/touch-joystick|TouchJoystick|VirtualButton|DragZone|RotateOverlay/;
|
|
5425
5733
|
async function detectMobileControls(cwd = process.cwd()) {
|
|
5426
5734
|
try {
|
|
5427
|
-
const raw = await
|
|
5735
|
+
const raw = await fs19.readFile(path18.join(cwd, "package.json"), "utf8");
|
|
5428
5736
|
const pkg = JSON.parse(raw);
|
|
5429
5737
|
if (pkg.genex?.mobileControls === true) return true;
|
|
5430
5738
|
} catch {
|
|
5431
5739
|
}
|
|
5432
|
-
const srcDir =
|
|
5740
|
+
const srcDir = path18.join(cwd, "src");
|
|
5433
5741
|
let entries;
|
|
5434
5742
|
try {
|
|
5435
|
-
entries = await
|
|
5743
|
+
entries = await fs19.readdir(srcDir, { recursive: true });
|
|
5436
5744
|
} catch {
|
|
5437
5745
|
return false;
|
|
5438
5746
|
}
|
|
@@ -5440,7 +5748,7 @@ async function detectMobileControls(cwd = process.cwd()) {
|
|
|
5440
5748
|
if (rel.includes("node_modules")) continue;
|
|
5441
5749
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
5442
5750
|
try {
|
|
5443
|
-
const content = await
|
|
5751
|
+
const content = await fs19.readFile(path18.join(srcDir, rel), "utf8");
|
|
5444
5752
|
if (TOUCH_KIT_MARKERS.test(content)) return true;
|
|
5445
5753
|
} catch {
|
|
5446
5754
|
}
|
|
@@ -5449,10 +5757,10 @@ async function detectMobileControls(cwd = process.cwd()) {
|
|
|
5449
5757
|
}
|
|
5450
5758
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
5451
5759
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
5452
|
-
const srcDir =
|
|
5760
|
+
const srcDir = path18.join(cwd, "src");
|
|
5453
5761
|
let entries;
|
|
5454
5762
|
try {
|
|
5455
|
-
entries = await
|
|
5763
|
+
entries = await fs19.readdir(srcDir, { recursive: true });
|
|
5456
5764
|
} catch {
|
|
5457
5765
|
return false;
|
|
5458
5766
|
}
|
|
@@ -5460,7 +5768,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
5460
5768
|
if (rel.includes("node_modules")) continue;
|
|
5461
5769
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
5462
5770
|
try {
|
|
5463
|
-
const content = await
|
|
5771
|
+
const content = await fs19.readFile(path18.join(srcDir, rel), "utf8");
|
|
5464
5772
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
5465
5773
|
} catch {
|
|
5466
5774
|
}
|
|
@@ -5509,19 +5817,19 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
5509
5817
|
depthRange: [],
|
|
5510
5818
|
mediaElementAudio: []
|
|
5511
5819
|
};
|
|
5512
|
-
const srcDir =
|
|
5820
|
+
const srcDir = path18.join(cwd, "src");
|
|
5513
5821
|
let entries;
|
|
5514
5822
|
try {
|
|
5515
|
-
entries = await
|
|
5823
|
+
entries = await fs19.readdir(srcDir, { recursive: true });
|
|
5516
5824
|
} catch {
|
|
5517
5825
|
return found;
|
|
5518
5826
|
}
|
|
5519
5827
|
for (const nativeRel of entries) {
|
|
5520
5828
|
if (nativeRel.includes("node_modules")) continue;
|
|
5521
5829
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
|
|
5522
|
-
const raw = await
|
|
5830
|
+
const raw = await fs19.readFile(path18.join(srcDir, nativeRel), "utf8").catch(() => "");
|
|
5523
5831
|
if (!raw) continue;
|
|
5524
|
-
const rel = nativeRel.split(
|
|
5832
|
+
const rel = nativeRel.split(path18.sep).join("/");
|
|
5525
5833
|
const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
|
|
5526
5834
|
const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
5527
5835
|
let m;
|
|
@@ -5579,25 +5887,25 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
5579
5887
|
let haystack = "";
|
|
5580
5888
|
const read = async (file) => {
|
|
5581
5889
|
try {
|
|
5582
|
-
haystack += await
|
|
5890
|
+
haystack += await fs19.readFile(file, "utf8");
|
|
5583
5891
|
} catch {
|
|
5584
5892
|
}
|
|
5585
5893
|
};
|
|
5586
5894
|
try {
|
|
5587
|
-
for (const entry of await
|
|
5895
|
+
for (const entry of await fs19.readdir(cwd, { withFileTypes: true })) {
|
|
5588
5896
|
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
|
|
5589
|
-
await read(
|
|
5897
|
+
await read(path18.join(cwd, entry.name));
|
|
5590
5898
|
}
|
|
5591
5899
|
}
|
|
5592
5900
|
} catch {
|
|
5593
5901
|
}
|
|
5594
5902
|
for (const sub of ["src", "public"]) {
|
|
5595
5903
|
try {
|
|
5596
|
-
const entries = await
|
|
5904
|
+
const entries = await fs19.readdir(path18.join(cwd, sub), { recursive: true });
|
|
5597
5905
|
for (const rel of entries) {
|
|
5598
5906
|
if (rel.includes("node_modules")) continue;
|
|
5599
5907
|
if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
|
|
5600
|
-
await read(
|
|
5908
|
+
await read(path18.join(cwd, sub, rel));
|
|
5601
5909
|
}
|
|
5602
5910
|
} catch {
|
|
5603
5911
|
}
|
|
@@ -5729,7 +6037,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
5729
6037
|
} catch {
|
|
5730
6038
|
return true;
|
|
5731
6039
|
}
|
|
5732
|
-
const gitConfig = await
|
|
6040
|
+
const gitConfig = await fs19.readFile(path18.join(cwd, ".git", "config"), "utf8").catch(() => "");
|
|
5733
6041
|
for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
|
|
5734
6042
|
try {
|
|
5735
6043
|
const u = new URL(m[1]);
|
|
@@ -5737,7 +6045,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
5737
6045
|
} catch {
|
|
5738
6046
|
}
|
|
5739
6047
|
}
|
|
5740
|
-
const readme = await
|
|
6048
|
+
const readme = await fs19.readFile(path18.join(cwd, "README.md"), "utf8").catch(() => "");
|
|
5741
6049
|
return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
|
|
5742
6050
|
}
|
|
5743
6051
|
|
|
@@ -5803,7 +6111,7 @@ async function runPublish(opts) {
|
|
|
5803
6111
|
advisoryNudges(log, detections);
|
|
5804
6112
|
if (!opts.noPush) {
|
|
5805
6113
|
const ok = await deployGame(
|
|
5806
|
-
{ projectId: meta.id, apiUrl, token },
|
|
6114
|
+
{ projectId: meta.id, apiUrl, token, slug: meta.slug },
|
|
5807
6115
|
{
|
|
5808
6116
|
// Publish deploys to staging and then promotes, so both channels end up
|
|
5809
6117
|
// naming the same build. Deploying straight to production instead would
|
|
@@ -5904,7 +6212,7 @@ async function runPreview(opts) {
|
|
|
5904
6212
|
await exploreReuseNudge(log, meta);
|
|
5905
6213
|
const apiUrl = getApiUrl(meta.apiUrl);
|
|
5906
6214
|
const ok = await deployGame(
|
|
5907
|
-
{ projectId: meta.id, apiUrl, token },
|
|
6215
|
+
{ projectId: meta.id, apiUrl, token, slug: meta.slug },
|
|
5908
6216
|
// Detections reach the server on every preview too: matchmaking so a draft's
|
|
5909
6217
|
// declared preset doesn't silently run the default (null clears a removed
|
|
5910
6218
|
// config), embedSdkVersion/multiplayer so the dashboard's Publish button can
|
|
@@ -5987,8 +6295,8 @@ async function runPromote(opts) {
|
|
|
5987
6295
|
}
|
|
5988
6296
|
|
|
5989
6297
|
// src/commands/generate.ts
|
|
5990
|
-
import
|
|
5991
|
-
import
|
|
6298
|
+
import fs20 from "fs/promises";
|
|
6299
|
+
import path19 from "path";
|
|
5992
6300
|
import { PNG as PNG4 } from "pngjs";
|
|
5993
6301
|
|
|
5994
6302
|
// src/lib/glass.ts
|
|
@@ -6069,7 +6377,7 @@ function solveMagentaGlass(source) {
|
|
|
6069
6377
|
}
|
|
6070
6378
|
|
|
6071
6379
|
// src/lib/open.ts
|
|
6072
|
-
import { spawn as
|
|
6380
|
+
import { spawn as spawn5 } from "child_process";
|
|
6073
6381
|
function tokenize(cmd) {
|
|
6074
6382
|
return cmd.trim().split(/\s+/).filter(Boolean);
|
|
6075
6383
|
}
|
|
@@ -6098,7 +6406,7 @@ function openUrl(url) {
|
|
|
6098
6406
|
}
|
|
6099
6407
|
}
|
|
6100
6408
|
try {
|
|
6101
|
-
const child =
|
|
6409
|
+
const child = spawn5(command, args, { stdio: "ignore", detached: true });
|
|
6102
6410
|
child.on("error", () => {
|
|
6103
6411
|
});
|
|
6104
6412
|
child.unref();
|
|
@@ -6158,7 +6466,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
6158
6466
|
async function inlineLocalImage(filePath, flag) {
|
|
6159
6467
|
let bytes;
|
|
6160
6468
|
try {
|
|
6161
|
-
bytes = await
|
|
6469
|
+
bytes = await fs20.readFile(filePath);
|
|
6162
6470
|
} catch {
|
|
6163
6471
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
6164
6472
|
}
|
|
@@ -6168,7 +6476,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
6168
6476
|
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.`
|
|
6169
6477
|
};
|
|
6170
6478
|
}
|
|
6171
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
6479
|
+
const mime = IMAGE_MIME_BY_EXT[path19.extname(filePath).toLowerCase()] ?? "image/png";
|
|
6172
6480
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
6173
6481
|
}
|
|
6174
6482
|
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.";
|
|
@@ -6364,7 +6672,7 @@ async function runGenerate(kind, opts) {
|
|
|
6364
6672
|
return;
|
|
6365
6673
|
}
|
|
6366
6674
|
try {
|
|
6367
|
-
const bytes = await
|
|
6675
|
+
const bytes = await fs20.readFile(opts.inpaintUrl);
|
|
6368
6676
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
6369
6677
|
} catch {
|
|
6370
6678
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -6488,7 +6796,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
6488
6796
|
return;
|
|
6489
6797
|
}
|
|
6490
6798
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
6491
|
-
await
|
|
6799
|
+
await fs20.mkdir(outDir, { recursive: true });
|
|
6492
6800
|
const solved = [];
|
|
6493
6801
|
for (let i = 0; i < files.length; i++) {
|
|
6494
6802
|
const f = files[i];
|
|
@@ -6520,8 +6828,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
6520
6828
|
});
|
|
6521
6829
|
continue;
|
|
6522
6830
|
}
|
|
6523
|
-
const outPath =
|
|
6524
|
-
await
|
|
6831
|
+
const outPath = path19.join(outDir, `glass-${i + 1}.png`);
|
|
6832
|
+
await fs20.writeFile(outPath, PNG4.sync.write(r.png));
|
|
6525
6833
|
solved.push({
|
|
6526
6834
|
path: outPath,
|
|
6527
6835
|
url: f.url,
|
|
@@ -7118,8 +7426,8 @@ async function toRow(e, v, cwd) {
|
|
|
7118
7426
|
}
|
|
7119
7427
|
|
|
7120
7428
|
// src/commands/controller.ts
|
|
7121
|
-
import
|
|
7122
|
-
import
|
|
7429
|
+
import fs22 from "fs/promises";
|
|
7430
|
+
import path21 from "path";
|
|
7123
7431
|
|
|
7124
7432
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
7125
7433
|
import { createHash } from "crypto";
|
|
@@ -16249,9 +16557,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
16249
16557
|
}
|
|
16250
16558
|
|
|
16251
16559
|
// src/lib/anims.ts
|
|
16252
|
-
import
|
|
16253
|
-
import
|
|
16254
|
-
var ANIMS_DEST =
|
|
16560
|
+
import fs21 from "fs/promises";
|
|
16561
|
+
import path20 from "path";
|
|
16562
|
+
var ANIMS_DEST = path20.join("public", "assets", "anims");
|
|
16255
16563
|
var HIDDEN_TAG = "reference";
|
|
16256
16564
|
async function runAnims(opts) {
|
|
16257
16565
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -16267,7 +16575,7 @@ async function runAnims(opts) {
|
|
|
16267
16575
|
printCatalog(log, manifest, selectors);
|
|
16268
16576
|
return;
|
|
16269
16577
|
}
|
|
16270
|
-
const controllerMarker =
|
|
16578
|
+
const controllerMarker = path20.join(root, "src", "controllers", "character");
|
|
16271
16579
|
if (!await exists2(controllerMarker)) {
|
|
16272
16580
|
log.error(
|
|
16273
16581
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -16276,11 +16584,11 @@ async function runAnims(opts) {
|
|
|
16276
16584
|
process.exitCode = 1;
|
|
16277
16585
|
return;
|
|
16278
16586
|
}
|
|
16279
|
-
const destDir =
|
|
16280
|
-
const gameManifestPath =
|
|
16587
|
+
const destDir = path20.join(root, ANIMS_DEST);
|
|
16588
|
+
const gameManifestPath = path20.join(destDir, "manifest.json");
|
|
16281
16589
|
if (opts.reset) {
|
|
16282
|
-
await
|
|
16283
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
16590
|
+
await fs21.rm(destDir, { recursive: true, force: true });
|
|
16591
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path20.sep)} (--reset)`);
|
|
16284
16592
|
}
|
|
16285
16593
|
if (selectors.length === 0) {
|
|
16286
16594
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -16318,35 +16626,35 @@ async function runAnims(opts) {
|
|
|
16318
16626
|
}
|
|
16319
16627
|
}
|
|
16320
16628
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
16321
|
-
const cacheDir =
|
|
16629
|
+
const cacheDir = path20.join(
|
|
16322
16630
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
16323
16631
|
`${manifest.library}-v${manifest.version}`
|
|
16324
16632
|
);
|
|
16325
|
-
await
|
|
16326
|
-
await
|
|
16633
|
+
await fs21.mkdir(cacheDir, { recursive: true });
|
|
16634
|
+
await fs21.mkdir(destDir, { recursive: true });
|
|
16327
16635
|
const base = getAnimsBase(opts.animsBase);
|
|
16328
16636
|
let installedCount = 0;
|
|
16329
16637
|
let presentCount = 0;
|
|
16330
16638
|
let addedBytes = 0;
|
|
16331
16639
|
const failures = [];
|
|
16332
16640
|
for (const entry of wanted) {
|
|
16333
|
-
const dest =
|
|
16641
|
+
const dest = path20.join(destDir, entry.file);
|
|
16334
16642
|
if (await hasSize(dest, entry.bytes)) {
|
|
16335
16643
|
presentCount++;
|
|
16336
16644
|
continue;
|
|
16337
16645
|
}
|
|
16338
16646
|
try {
|
|
16339
|
-
const cached =
|
|
16647
|
+
const cached = path20.join(cacheDir, entry.file);
|
|
16340
16648
|
if (!await hasSize(cached, entry.bytes)) {
|
|
16341
16649
|
const res = await fetch(base + entry.file);
|
|
16342
16650
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
16343
16651
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
16344
|
-
await
|
|
16652
|
+
await fs21.writeFile(cached, buf);
|
|
16345
16653
|
}
|
|
16346
|
-
await
|
|
16654
|
+
await fs21.copyFile(cached, dest);
|
|
16347
16655
|
installedCount++;
|
|
16348
16656
|
addedBytes += entry.bytes;
|
|
16349
|
-
log.dim(` ${
|
|
16657
|
+
log.dim(` ${path20.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
16350
16658
|
} catch (err) {
|
|
16351
16659
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
16352
16660
|
}
|
|
@@ -16362,13 +16670,13 @@ async function runAnims(opts) {
|
|
|
16362
16670
|
version: manifest.version,
|
|
16363
16671
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
16364
16672
|
};
|
|
16365
|
-
await
|
|
16673
|
+
await fs21.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
16366
16674
|
log.plain("");
|
|
16367
16675
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
16368
16676
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
16369
16677
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
16370
16678
|
log.success(
|
|
16371
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
16679
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path20.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
16372
16680
|
);
|
|
16373
16681
|
for (const [selector, entries] of resolved) {
|
|
16374
16682
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -16400,8 +16708,8 @@ async function loadManifest(baseOverride) {
|
|
|
16400
16708
|
}
|
|
16401
16709
|
} catch {
|
|
16402
16710
|
}
|
|
16403
|
-
const snapshotPath =
|
|
16404
|
-
const manifest = JSON.parse(await
|
|
16711
|
+
const snapshotPath = path20.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
16712
|
+
const manifest = JSON.parse(await fs21.readFile(snapshotPath, "utf8"));
|
|
16405
16713
|
return { manifest, source: "snapshot" };
|
|
16406
16714
|
}
|
|
16407
16715
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -16519,21 +16827,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
16519
16827
|
}
|
|
16520
16828
|
async function readGameManifest(file) {
|
|
16521
16829
|
try {
|
|
16522
|
-
return JSON.parse(await
|
|
16830
|
+
return JSON.parse(await fs21.readFile(file, "utf8"));
|
|
16523
16831
|
} catch {
|
|
16524
16832
|
return null;
|
|
16525
16833
|
}
|
|
16526
16834
|
}
|
|
16527
16835
|
async function hasSize(file, bytes) {
|
|
16528
16836
|
try {
|
|
16529
|
-
return (await
|
|
16837
|
+
return (await fs21.stat(file)).size === bytes;
|
|
16530
16838
|
} catch {
|
|
16531
16839
|
return false;
|
|
16532
16840
|
}
|
|
16533
16841
|
}
|
|
16534
16842
|
async function exists2(p) {
|
|
16535
16843
|
try {
|
|
16536
|
-
await
|
|
16844
|
+
await fs21.access(p);
|
|
16537
16845
|
return true;
|
|
16538
16846
|
} catch {
|
|
16539
16847
|
return false;
|
|
@@ -16727,8 +17035,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
16727
17035
|
]
|
|
16728
17036
|
}
|
|
16729
17037
|
};
|
|
16730
|
-
var CODE_DEST =
|
|
16731
|
-
var ASSETS_DEST =
|
|
17038
|
+
var CODE_DEST = path21.join("src", "controllers");
|
|
17039
|
+
var ASSETS_DEST = path21.join("public", "assets");
|
|
16732
17040
|
async function runController(opts) {
|
|
16733
17041
|
const log = createLogger({ quiet: opts.quiet });
|
|
16734
17042
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -16745,31 +17053,31 @@ async function runController(opts) {
|
|
|
16745
17053
|
process.exitCode = 1;
|
|
16746
17054
|
return;
|
|
16747
17055
|
}
|
|
16748
|
-
const srcDir =
|
|
17056
|
+
const srcDir = path21.join(getTemplatesDir(), "controllers");
|
|
16749
17057
|
const root = opts.cwd ?? process.cwd();
|
|
16750
17058
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
16751
17059
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
16752
17060
|
log.plain("");
|
|
16753
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
17061
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path21.sep)}`);
|
|
16754
17062
|
const plan = [
|
|
16755
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
17063
|
+
...set.code.map((rel) => ({ from: rel, rel: path21.join(CODE_DEST, rel) })),
|
|
16756
17064
|
...set.assets.map((rel) => ({
|
|
16757
17065
|
from: rel,
|
|
16758
|
-
rel:
|
|
17066
|
+
rel: path21.join(ASSETS_DEST, path21.basename(rel))
|
|
16759
17067
|
}))
|
|
16760
17068
|
];
|
|
16761
17069
|
let copied = 0;
|
|
16762
17070
|
let skipped = 0;
|
|
16763
17071
|
try {
|
|
16764
17072
|
for (const file of plan) {
|
|
16765
|
-
const dest =
|
|
17073
|
+
const dest = path21.join(root, file.rel);
|
|
16766
17074
|
if (!opts.force && await exists3(dest)) {
|
|
16767
17075
|
skipped++;
|
|
16768
17076
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
16769
17077
|
continue;
|
|
16770
17078
|
}
|
|
16771
|
-
await
|
|
16772
|
-
await
|
|
17079
|
+
await fs22.mkdir(path21.dirname(dest), { recursive: true });
|
|
17080
|
+
await fs22.copyFile(path21.join(srcDir, file.from), dest);
|
|
16773
17081
|
copied++;
|
|
16774
17082
|
log.dim(` ${file.rel}`);
|
|
16775
17083
|
}
|
|
@@ -16822,7 +17130,7 @@ async function runController(opts) {
|
|
|
16822
17130
|
for (const line of set.sketch) {
|
|
16823
17131
|
log.dim(` ${line}`);
|
|
16824
17132
|
}
|
|
16825
|
-
if (kind === "character" && !await exists3(
|
|
17133
|
+
if (kind === "character" && !await exists3(path21.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
16826
17134
|
log.plain("");
|
|
16827
17135
|
log.plain(
|
|
16828
17136
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -16854,9 +17162,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
16854
17162
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
16855
17163
|
}
|
|
16856
17164
|
assertCompleteMeshyControllerPack(manifest);
|
|
16857
|
-
const destination =
|
|
16858
|
-
await
|
|
16859
|
-
await
|
|
17165
|
+
const destination = path21.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
17166
|
+
await fs22.mkdir(path21.dirname(destination), { recursive: true });
|
|
17167
|
+
await fs22.writeFile(
|
|
16860
17168
|
destination,
|
|
16861
17169
|
`${JSON.stringify(manifest, null, 2)}
|
|
16862
17170
|
`
|
|
@@ -16994,14 +17302,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
16994
17302
|
}
|
|
16995
17303
|
async function installFallbackAvatar(args) {
|
|
16996
17304
|
const { root, srcDir, log } = args;
|
|
16997
|
-
const dest =
|
|
16998
|
-
await
|
|
16999
|
-
await
|
|
17305
|
+
const dest = path21.join(root, ASSETS_DEST, "avatar.vrm");
|
|
17306
|
+
await fs22.mkdir(path21.dirname(dest), { recursive: true });
|
|
17307
|
+
await fs22.copyFile(path21.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
17000
17308
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
17001
17309
|
}
|
|
17002
17310
|
async function exists3(p) {
|
|
17003
17311
|
try {
|
|
17004
|
-
await
|
|
17312
|
+
await fs22.access(p);
|
|
17005
17313
|
return true;
|
|
17006
17314
|
} catch {
|
|
17007
17315
|
return false;
|
|
@@ -17009,8 +17317,8 @@ async function exists3(p) {
|
|
|
17009
17317
|
}
|
|
17010
17318
|
|
|
17011
17319
|
// src/commands/character.ts
|
|
17012
|
-
import
|
|
17013
|
-
import
|
|
17320
|
+
import fs23 from "fs/promises";
|
|
17321
|
+
import path22 from "path";
|
|
17014
17322
|
function exactAnimation(selector) {
|
|
17015
17323
|
const trimmed = selector.trim();
|
|
17016
17324
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -17090,7 +17398,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
17090
17398
|
}
|
|
17091
17399
|
process.exitCode = 1;
|
|
17092
17400
|
}
|
|
17093
|
-
var INSTALLED_MANIFEST =
|
|
17401
|
+
var INSTALLED_MANIFEST = path22.join("public", "assets", "meshy-character.json");
|
|
17094
17402
|
async function resolveAdoptTarget(selector) {
|
|
17095
17403
|
const trimmed = selector?.trim();
|
|
17096
17404
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -17099,7 +17407,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
17099
17407
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
17100
17408
|
let raw;
|
|
17101
17409
|
try {
|
|
17102
|
-
raw = await
|
|
17410
|
+
raw = await fs23.readFile(file, "utf8");
|
|
17103
17411
|
} catch {
|
|
17104
17412
|
return {
|
|
17105
17413
|
ok: false,
|
|
@@ -17577,22 +17885,22 @@ async function context2(opts) {
|
|
|
17577
17885
|
const project = await readProject();
|
|
17578
17886
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
17579
17887
|
}
|
|
17580
|
-
async function readVideo(
|
|
17888
|
+
async function readVideo(path25, log) {
|
|
17581
17889
|
let bytes;
|
|
17582
17890
|
try {
|
|
17583
|
-
bytes = await readFile(
|
|
17891
|
+
bytes = await readFile(path25);
|
|
17584
17892
|
} catch {
|
|
17585
|
-
log.error(`Can't read ${
|
|
17893
|
+
log.error(`Can't read ${path25}.`);
|
|
17586
17894
|
return null;
|
|
17587
17895
|
}
|
|
17588
17896
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
17589
|
-
log.error(`${basename(
|
|
17897
|
+
log.error(`${basename(path25)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
17590
17898
|
return null;
|
|
17591
17899
|
}
|
|
17592
17900
|
return bytes;
|
|
17593
17901
|
}
|
|
17594
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
17595
|
-
const contentType = /\.mov$/i.test(
|
|
17902
|
+
async function uploadVideo(apiUrl, token, characterId, path25, bytes, log) {
|
|
17903
|
+
const contentType = /\.mov$/i.test(path25) ? "video/quicktime" : "video/mp4";
|
|
17596
17904
|
const minted = await apiFetch(
|
|
17597
17905
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
17598
17906
|
{
|
|
@@ -17607,7 +17915,7 @@ async function uploadVideo(apiUrl, token, characterId, path23, bytes, log) {
|
|
|
17607
17915
|
return null;
|
|
17608
17916
|
}
|
|
17609
17917
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
17610
|
-
log.dim(` uploading ${basename(
|
|
17918
|
+
log.dim(` uploading ${basename(path25)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
17611
17919
|
const put = await fetch(uploadUrl, {
|
|
17612
17920
|
method: "PUT",
|
|
17613
17921
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -17923,8 +18231,8 @@ function rank(items, query) {
|
|
|
17923
18231
|
}
|
|
17924
18232
|
|
|
17925
18233
|
// src/commands/motion.ts
|
|
17926
|
-
import
|
|
17927
|
-
import
|
|
18234
|
+
import fs24 from "fs/promises";
|
|
18235
|
+
import path23 from "path";
|
|
17928
18236
|
|
|
17929
18237
|
// src/lib/motion/npz.ts
|
|
17930
18238
|
import zlib from "zlib";
|
|
@@ -19175,7 +19483,7 @@ async function motionGen(opts, log) {
|
|
|
19175
19483
|
}
|
|
19176
19484
|
if (opts.constraintsPath !== void 0) {
|
|
19177
19485
|
try {
|
|
19178
|
-
const raw = await
|
|
19486
|
+
const raw = await fs24.readFile(opts.constraintsPath, "utf8");
|
|
19179
19487
|
generationOptions.constraints = JSON.parse(raw);
|
|
19180
19488
|
} catch {
|
|
19181
19489
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -19199,10 +19507,10 @@ async function motionGen(opts, log) {
|
|
|
19199
19507
|
async function expandTakes(selectors) {
|
|
19200
19508
|
const out = [];
|
|
19201
19509
|
for (const sel of selectors) {
|
|
19202
|
-
const st = await
|
|
19510
|
+
const st = await fs24.stat(sel).catch(() => null);
|
|
19203
19511
|
if (st?.isDirectory()) {
|
|
19204
|
-
const names = await
|
|
19205
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
19512
|
+
const names = await fs24.readdir(sel);
|
|
19513
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path23.join(sel, n));
|
|
19206
19514
|
} else if (st?.isFile()) {
|
|
19207
19515
|
out.push(sel);
|
|
19208
19516
|
} else {
|
|
@@ -19237,7 +19545,7 @@ async function motionVerify(opts, log) {
|
|
|
19237
19545
|
let gates = DEFAULT_GATES;
|
|
19238
19546
|
if (opts.gatesPath) {
|
|
19239
19547
|
try {
|
|
19240
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
19548
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs24.readFile(opts.gatesPath, "utf8")));
|
|
19241
19549
|
} catch {
|
|
19242
19550
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
19243
19551
|
process.exitCode = 1;
|
|
@@ -19259,9 +19567,9 @@ async function motionVerify(opts, log) {
|
|
|
19259
19567
|
}
|
|
19260
19568
|
const reports = [];
|
|
19261
19569
|
for (const file of files) {
|
|
19262
|
-
const stem =
|
|
19570
|
+
const stem = path23.basename(file).replace(/\.npz$/, "");
|
|
19263
19571
|
try {
|
|
19264
|
-
reports.push(analyzeTake(stem, await
|
|
19572
|
+
reports.push(analyzeTake(stem, await fs24.readFile(file), gates));
|
|
19265
19573
|
} catch (err) {
|
|
19266
19574
|
reports.push({
|
|
19267
19575
|
take: stem,
|
|
@@ -19299,7 +19607,7 @@ async function motionCompile(opts, log) {
|
|
|
19299
19607
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
19300
19608
|
if (opts.configPath) {
|
|
19301
19609
|
try {
|
|
19302
|
-
const patch = JSON.parse(await
|
|
19610
|
+
const patch = JSON.parse(await fs24.readFile(opts.configPath, "utf8"));
|
|
19303
19611
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
19304
19612
|
} catch {
|
|
19305
19613
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -19317,16 +19625,16 @@ async function motionCompile(opts, log) {
|
|
|
19317
19625
|
}
|
|
19318
19626
|
const inputs = [];
|
|
19319
19627
|
for (const file of files) {
|
|
19320
|
-
const stem =
|
|
19628
|
+
const stem = path23.basename(file).replace(/\.npz$/, "");
|
|
19321
19629
|
try {
|
|
19322
|
-
inputs.push({ stem, take: loadTake(await
|
|
19630
|
+
inputs.push({ stem, take: loadTake(await fs24.readFile(file)) });
|
|
19323
19631
|
} catch (err) {
|
|
19324
19632
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
19325
19633
|
process.exitCode = 1;
|
|
19326
19634
|
return;
|
|
19327
19635
|
}
|
|
19328
19636
|
}
|
|
19329
|
-
const setName = opts.set ??
|
|
19637
|
+
const setName = opts.set ?? path23.basename(opts.out).replace(/\.json$/, "");
|
|
19330
19638
|
let result;
|
|
19331
19639
|
try {
|
|
19332
19640
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -19341,9 +19649,9 @@ async function motionCompile(opts, log) {
|
|
|
19341
19649
|
process.exitCode = 1;
|
|
19342
19650
|
return;
|
|
19343
19651
|
}
|
|
19344
|
-
await
|
|
19652
|
+
await fs24.mkdir(path23.dirname(path23.resolve(opts.out)), { recursive: true });
|
|
19345
19653
|
const json = JSON.stringify(result.data);
|
|
19346
|
-
await
|
|
19654
|
+
await fs24.writeFile(opts.out, json);
|
|
19347
19655
|
if (opts.json) {
|
|
19348
19656
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
19349
19657
|
return;
|
|
@@ -19361,9 +19669,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
19361
19669
|
var MOTION_PRESETS = {
|
|
19362
19670
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
19363
19671
|
};
|
|
19364
|
-
var MOTION_DEST =
|
|
19672
|
+
var MOTION_DEST = path23.join("src", "motion");
|
|
19365
19673
|
async function motionInstall(opts, log) {
|
|
19366
|
-
const srcDir =
|
|
19674
|
+
const srcDir = path23.join(getTemplatesDir(), "motion");
|
|
19367
19675
|
const root = opts.cwd ?? process.cwd();
|
|
19368
19676
|
const preset = opts.set;
|
|
19369
19677
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -19374,21 +19682,21 @@ async function motionInstall(opts, log) {
|
|
|
19374
19682
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
19375
19683
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
19376
19684
|
log.plain("");
|
|
19377
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
19685
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path23.sep)}`);
|
|
19378
19686
|
let copied = 0, skipped = 0;
|
|
19379
19687
|
try {
|
|
19380
19688
|
for (const rel of files) {
|
|
19381
|
-
const dest =
|
|
19382
|
-
const exists4 = await
|
|
19689
|
+
const dest = path23.join(root, MOTION_DEST, rel);
|
|
19690
|
+
const exists4 = await fs24.access(dest).then(() => true, () => false);
|
|
19383
19691
|
if (!opts.force && exists4) {
|
|
19384
19692
|
skipped++;
|
|
19385
|
-
log.dim(` skipped ${
|
|
19693
|
+
log.dim(` skipped ${path23.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
19386
19694
|
continue;
|
|
19387
19695
|
}
|
|
19388
|
-
await
|
|
19389
|
-
await
|
|
19696
|
+
await fs24.mkdir(path23.dirname(dest), { recursive: true });
|
|
19697
|
+
await fs24.copyFile(path23.join(srcDir, rel), dest);
|
|
19390
19698
|
copied++;
|
|
19391
|
-
log.dim(` ${
|
|
19699
|
+
log.dim(` ${path23.join(MOTION_DEST, rel)}`);
|
|
19392
19700
|
}
|
|
19393
19701
|
} catch (err) {
|
|
19394
19702
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -19429,7 +19737,7 @@ async function motionConstraints(opts, log) {
|
|
|
19429
19737
|
}
|
|
19430
19738
|
const doc = directionConstraint(dir, speed, duration);
|
|
19431
19739
|
const out = opts.out ?? "constraints.json";
|
|
19432
|
-
await
|
|
19740
|
+
await fs24.writeFile(out, JSON.stringify(doc));
|
|
19433
19741
|
if (opts.json) {
|
|
19434
19742
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
19435
19743
|
return;
|
|
@@ -19466,9 +19774,9 @@ async function runMotion(opts) {
|
|
|
19466
19774
|
}
|
|
19467
19775
|
|
|
19468
19776
|
// src/commands/asset-new.ts
|
|
19469
|
-
import
|
|
19777
|
+
import fs25 from "fs";
|
|
19470
19778
|
import fsp from "fs/promises";
|
|
19471
|
-
import
|
|
19779
|
+
import path24 from "path";
|
|
19472
19780
|
import { pathToFileURL } from "url";
|
|
19473
19781
|
var EXTRA_FILES = [
|
|
19474
19782
|
"genex-asset.example.json",
|
|
@@ -19561,7 +19869,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
19561
19869
|
}
|
|
19562
19870
|
async function runAssetNew(options) {
|
|
19563
19871
|
const log = createLogger();
|
|
19564
|
-
const cwd = options.dir ?
|
|
19872
|
+
const cwd = options.dir ? path24.resolve(options.dir) : process.cwd();
|
|
19565
19873
|
const slug = options.assetSlug;
|
|
19566
19874
|
if (!slug) {
|
|
19567
19875
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -19571,14 +19879,14 @@ async function runAssetNew(options) {
|
|
|
19571
19879
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
19572
19880
|
return 1;
|
|
19573
19881
|
}
|
|
19574
|
-
const templateDir =
|
|
19575
|
-
if (!
|
|
19882
|
+
const templateDir = path24.join(getTemplatesDir(), "asset-viewer");
|
|
19883
|
+
if (!fs25.existsSync(templateDir)) {
|
|
19576
19884
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
19577
19885
|
return 1;
|
|
19578
19886
|
}
|
|
19579
|
-
const manifestTools = await import(pathToFileURL(
|
|
19887
|
+
const manifestTools = await import(pathToFileURL(path24.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
19580
19888
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
19581
|
-
const lockPath =
|
|
19889
|
+
const lockPath = path24.join(templateDir, "shared-files.sha256.json");
|
|
19582
19890
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
19583
19891
|
const actual = hashSharedFiles(templateDir);
|
|
19584
19892
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -19591,8 +19899,8 @@ async function runAssetNew(options) {
|
|
|
19591
19899
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
19592
19900
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
19593
19901
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
19594
|
-
const outDir =
|
|
19595
|
-
if (
|
|
19902
|
+
const outDir = path24.resolve(cwd, options.out ?? slug);
|
|
19903
|
+
if (fs25.existsSync(outDir) && fs25.readdirSync(outDir).length > 0 && !options.force) {
|
|
19596
19904
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
19597
19905
|
return 1;
|
|
19598
19906
|
}
|
|
@@ -19620,24 +19928,24 @@ async function runAssetNew(options) {
|
|
|
19620
19928
|
};
|
|
19621
19929
|
await fsp.mkdir(outDir, { recursive: true });
|
|
19622
19930
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
19623
|
-
const to =
|
|
19624
|
-
await fsp.mkdir(
|
|
19625
|
-
await fsp.copyFile(
|
|
19931
|
+
const to = path24.join(outDir, rel);
|
|
19932
|
+
await fsp.mkdir(path24.dirname(to), { recursive: true });
|
|
19933
|
+
await fsp.copyFile(path24.join(templateDir, rel), to);
|
|
19626
19934
|
}
|
|
19627
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
19935
|
+
const pkg = fillTemplate(await fsp.readFile(path24.join(templateDir, "package.json"), "utf8"), {
|
|
19628
19936
|
slug,
|
|
19629
19937
|
name,
|
|
19630
19938
|
version
|
|
19631
19939
|
});
|
|
19632
|
-
await fsp.writeFile(
|
|
19633
|
-
await fsp.writeFile(
|
|
19634
|
-
await fsp.writeFile(
|
|
19940
|
+
await fsp.writeFile(path24.join(outDir, "package.json"), pkg, "utf8");
|
|
19941
|
+
await fsp.writeFile(path24.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
19942
|
+
await fsp.writeFile(path24.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
19635
19943
|
await fsp.writeFile(
|
|
19636
|
-
|
|
19944
|
+
path24.join(outDir, "DESIGN.md"),
|
|
19637
19945
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
19638
19946
|
"utf8"
|
|
19639
19947
|
);
|
|
19640
|
-
const placeholder = await fsp.readFile(
|
|
19948
|
+
const placeholder = await fsp.readFile(path24.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
19641
19949
|
const seeded = seedAssetSource(placeholder, {
|
|
19642
19950
|
slug,
|
|
19643
19951
|
name,
|
|
@@ -19648,8 +19956,8 @@ async function runAssetNew(options) {
|
|
|
19648
19956
|
pascalCase
|
|
19649
19957
|
});
|
|
19650
19958
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
19651
|
-
await fsp.mkdir(
|
|
19652
|
-
await fsp.writeFile(
|
|
19959
|
+
await fsp.mkdir(path24.join(outDir, "src", "asset"), { recursive: true });
|
|
19960
|
+
await fsp.writeFile(path24.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
19653
19961
|
const copied = hashSharedFiles(outDir);
|
|
19654
19962
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
19655
19963
|
if (mismatched.length) {
|
|
@@ -19657,7 +19965,7 @@ async function runAssetNew(options) {
|
|
|
19657
19965
|
return 1;
|
|
19658
19966
|
}
|
|
19659
19967
|
await fsp.writeFile(
|
|
19660
|
-
|
|
19968
|
+
path24.join(outDir, PARITY_FILENAME),
|
|
19661
19969
|
JSON.stringify(
|
|
19662
19970
|
{
|
|
19663
19971
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -19711,9 +20019,15 @@ ${c.bold("Usage")}
|
|
|
19711
20019
|
resumes the same code, so an interrupted
|
|
19712
20020
|
'genex init' costs nothing. --force switches
|
|
19713
20021
|
accounts.
|
|
19714
|
-
genex link <slug> [options]
|
|
19715
|
-
|
|
19716
|
-
|
|
20022
|
+
genex link <slug> [options] Point THIS folder at an existing game of yours;
|
|
20023
|
+
preview/publish then update the same live game.
|
|
20024
|
+
In an EMPTY folder it downloads the game too \u2014
|
|
20025
|
+
that's how you pick a game up on a new machine.
|
|
20026
|
+
Never creates a project.
|
|
20027
|
+
genex pull [options] Update THIS folder with the game's latest draft,
|
|
20028
|
+
for when it was built from somewhere else. Refuses
|
|
20029
|
+
if this folder has changes you never deployed;
|
|
20030
|
+
--force replaces them.
|
|
19717
20031
|
genex rename <name> [options] Rename THIS game \u2014 its play address, its source repo,
|
|
19718
20032
|
and its gallery name. Keeps the game (plays, likes,
|
|
19719
20033
|
comments); the OLD address stops working, so a
|
|
@@ -20660,6 +20974,9 @@ async function main() {
|
|
|
20660
20974
|
case "link":
|
|
20661
20975
|
await runLink(parsed.options);
|
|
20662
20976
|
break;
|
|
20977
|
+
case "pull":
|
|
20978
|
+
await runPull(parsed.options);
|
|
20979
|
+
break;
|
|
20663
20980
|
case "rename":
|
|
20664
20981
|
await runRename(parsed.options);
|
|
20665
20982
|
break;
|