@genex-ai/cli-demo 1.6.4-dev.433 → 1.6.6-dev.435
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 +570 -249
- package/package.json +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +28 -10
package/dist/index.js
CHANGED
|
@@ -1829,8 +1829,159 @@ 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(` ${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
|
|
1834
1985
|
async function runLink(opts) {
|
|
1835
1986
|
const log = createLogger({ quiet: opts.quiet });
|
|
1836
1987
|
log.plain(c.bold("genex link"));
|
|
@@ -1878,7 +2029,9 @@ async function runLink(opts) {
|
|
|
1878
2029
|
process.exitCode = 1;
|
|
1879
2030
|
return;
|
|
1880
2031
|
}
|
|
2032
|
+
const wasEmpty = await isEmptyDir(process.cwd());
|
|
1881
2033
|
await writeGitignore(process.cwd(), log);
|
|
2034
|
+
const cloned = wasEmpty ? await downloadSource(apiUrl, token, project, log) : false;
|
|
1882
2035
|
const meta = {
|
|
1883
2036
|
id: project.id,
|
|
1884
2037
|
slug: project.slug,
|
|
@@ -1892,30 +2045,72 @@ async function runLink(opts) {
|
|
|
1892
2045
|
log.dim(` saved ${c.cyan(metaPath)}`);
|
|
1893
2046
|
await writeGameConfigFiles(meta, log);
|
|
1894
2047
|
await ensureSlugEnv(project.slug, log);
|
|
2048
|
+
if (cloned) {
|
|
2049
|
+
const remote = await readRemoteSource(apiUrl, token, project.id);
|
|
2050
|
+
const tree = await sourceTreeHash(process.cwd());
|
|
2051
|
+
if (remote?.stagingCommit) meta.stagingCommit = remote.stagingCommit;
|
|
2052
|
+
if (tree) meta.sourceTree = tree;
|
|
2053
|
+
await writeProject(meta);
|
|
2054
|
+
}
|
|
1895
2055
|
log.plain("");
|
|
1896
2056
|
log.success(
|
|
1897
2057
|
`Linked. This folder now updates ${c.cyan(project.slug)} \u2014 \`npx genex preview\` / \`publish\` ship to the same live game.`
|
|
1898
2058
|
);
|
|
2059
|
+
if (cloned) log.dim(` Run ${c.cyan("npm install")}, then keep building.`);
|
|
1899
2060
|
if (project.playUrl) log.dim(` play URL: ${project.playUrl}`);
|
|
1900
2061
|
}
|
|
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
|
+
}
|
|
1901
2096
|
async function ensureSlugEnv(slug, log, cwd = process.cwd()) {
|
|
1902
|
-
const file =
|
|
2097
|
+
const file = path12.join(cwd, ".env");
|
|
1903
2098
|
let content;
|
|
1904
2099
|
try {
|
|
1905
|
-
content = await
|
|
2100
|
+
content = await fs12.readFile(file, "utf8");
|
|
1906
2101
|
} catch {
|
|
1907
2102
|
return;
|
|
1908
2103
|
}
|
|
1909
2104
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
1910
2105
|
const m = content.match(re);
|
|
1911
2106
|
if (!m) {
|
|
1912
|
-
await
|
|
2107
|
+
await fs12.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
|
|
1913
2108
|
`);
|
|
1914
2109
|
log.dim(` added VITE_GENEX_SLUG=${slug} to .env`);
|
|
1915
2110
|
return;
|
|
1916
2111
|
}
|
|
1917
2112
|
if (m[2].trim() === slug) return;
|
|
1918
|
-
await
|
|
2113
|
+
await fs12.writeFile(file, content.replace(re, `$1${slug}`));
|
|
1919
2114
|
log.dim(` updated .env: VITE_GENEX_SLUG=${slug} (was ${m[2].trim()})`);
|
|
1920
2115
|
}
|
|
1921
2116
|
async function fetchOwnProject(apiUrl, token, slug, log) {
|
|
@@ -1968,9 +2163,109 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
1968
2163
|
}
|
|
1969
2164
|
}
|
|
1970
2165
|
|
|
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")} throws them away and takes the draft`);
|
|
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
|
+
await replaceTree(cwd, fresh);
|
|
2237
|
+
} finally {
|
|
2238
|
+
await fs13.rm(staging, { recursive: true, force: true }).catch(() => {
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
const remote = await readRemoteSource(apiUrl, token, meta.id);
|
|
2242
|
+
const tree = await sourceTreeHash(cwd);
|
|
2243
|
+
await writeProject(
|
|
2244
|
+
{
|
|
2245
|
+
...meta,
|
|
2246
|
+
...remote?.stagingCommit ? { stagingCommit: remote.stagingCommit } : {},
|
|
2247
|
+
...tree ? { sourceTree: tree } : {}
|
|
2248
|
+
},
|
|
2249
|
+
cwd
|
|
2250
|
+
);
|
|
2251
|
+
log.plain("");
|
|
2252
|
+
log.success(`Pulled ${c.cyan(meta.slug)}${grant.sourceRef === "preview" ? " (draft)" : ""}.`);
|
|
2253
|
+
log.dim(` Run ${c.cyan("npm install")} if dependencies changed, then keep building.`);
|
|
2254
|
+
}
|
|
2255
|
+
async function replaceTree(dest, src) {
|
|
2256
|
+
for (const entry of await fs13.readdir(dest)) {
|
|
2257
|
+
if (isMachineLocal(entry)) continue;
|
|
2258
|
+
await fs13.rm(path13.join(dest, entry), { recursive: true, force: true });
|
|
2259
|
+
}
|
|
2260
|
+
for (const entry of await fs13.readdir(src)) {
|
|
2261
|
+
if (isMachineLocal(entry)) continue;
|
|
2262
|
+
await fs13.cp(path13.join(src, entry), path13.join(dest, entry), { recursive: true });
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
1971
2266
|
// src/commands/rename.ts
|
|
1972
|
-
import
|
|
1973
|
-
import
|
|
2267
|
+
import fs14 from "fs/promises";
|
|
2268
|
+
import path14 from "path";
|
|
1974
2269
|
async function runRename(opts) {
|
|
1975
2270
|
const log = createLogger({ quiet: opts.quiet });
|
|
1976
2271
|
log.plain(c.bold("genex rename"));
|
|
@@ -2058,23 +2353,23 @@ async function runRename(opts) {
|
|
|
2058
2353
|
log.info("Run `genex preview` (or `publish`) to rebuild \u2014 the new slug is baked into the bundle.");
|
|
2059
2354
|
}
|
|
2060
2355
|
async function rewriteSlugEnv(from, to, log, cwd = process.cwd()) {
|
|
2061
|
-
const file =
|
|
2356
|
+
const file = path14.join(cwd, ".env");
|
|
2062
2357
|
let content;
|
|
2063
2358
|
try {
|
|
2064
|
-
content = await
|
|
2359
|
+
content = await fs14.readFile(file, "utf8");
|
|
2065
2360
|
} catch {
|
|
2066
2361
|
return;
|
|
2067
2362
|
}
|
|
2068
2363
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
2069
2364
|
if (!re.test(content)) return;
|
|
2070
|
-
await
|
|
2365
|
+
await fs14.writeFile(file, content.replace(re, `$1${to}`));
|
|
2071
2366
|
log.dim(` .env: VITE_GENEX_SLUG=${to} (was ${from})`);
|
|
2072
2367
|
}
|
|
2073
2368
|
async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
2074
|
-
const file =
|
|
2369
|
+
const file = path14.join(cwd, "src", "genex.config.ts");
|
|
2075
2370
|
let content;
|
|
2076
2371
|
try {
|
|
2077
|
-
content = await
|
|
2372
|
+
content = await fs14.readFile(file, "utf8");
|
|
2078
2373
|
} catch {
|
|
2079
2374
|
return;
|
|
2080
2375
|
}
|
|
@@ -2084,7 +2379,7 @@ async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
|
2084
2379
|
log.warn(` src/genex.config.ts has no "${from}" literal \u2014 check its slug by hand.`);
|
|
2085
2380
|
return;
|
|
2086
2381
|
}
|
|
2087
|
-
await
|
|
2382
|
+
await fs14.writeFile(file, content.replace(quoted, `"${to}"`));
|
|
2088
2383
|
log.dim(` src/genex.config.ts: baked slug -> ${to}`);
|
|
2089
2384
|
}
|
|
2090
2385
|
|
|
@@ -2212,11 +2507,11 @@ function relTime(iso) {
|
|
|
2212
2507
|
}
|
|
2213
2508
|
|
|
2214
2509
|
// src/lib/deploy.ts
|
|
2215
|
-
import
|
|
2510
|
+
import "child_process";
|
|
2216
2511
|
import crypto3 from "crypto";
|
|
2217
|
-
import
|
|
2218
|
-
import
|
|
2219
|
-
import
|
|
2512
|
+
import fs17 from "fs/promises";
|
|
2513
|
+
import os9 from "os";
|
|
2514
|
+
import path16 from "path";
|
|
2220
2515
|
|
|
2221
2516
|
// ../../packages/mobile-scan/src/image-dims.ts
|
|
2222
2517
|
function u32be(b, o) {
|
|
@@ -2480,12 +2775,12 @@ function tierFor(estVramMb) {
|
|
|
2480
2775
|
}
|
|
2481
2776
|
|
|
2482
2777
|
// src/commands/ui.ts
|
|
2483
|
-
import
|
|
2484
|
-
import
|
|
2778
|
+
import fs16 from "fs/promises";
|
|
2779
|
+
import path15 from "path";
|
|
2485
2780
|
import { PNG as PNG2 } from "pngjs";
|
|
2486
2781
|
|
|
2487
2782
|
// src/lib/png-tools.ts
|
|
2488
|
-
import
|
|
2783
|
+
import fs15 from "fs/promises";
|
|
2489
2784
|
import { PNG } from "pngjs";
|
|
2490
2785
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
2491
2786
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -2496,12 +2791,12 @@ async function loadPng(input) {
|
|
|
2496
2791
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
2497
2792
|
buf = Buffer.from(await res.arrayBuffer());
|
|
2498
2793
|
} else {
|
|
2499
|
-
buf = await
|
|
2794
|
+
buf = await fs15.readFile(input);
|
|
2500
2795
|
}
|
|
2501
2796
|
return PNG.sync.read(buf);
|
|
2502
2797
|
}
|
|
2503
2798
|
async function writePng(file, png) {
|
|
2504
|
-
await
|
|
2799
|
+
await fs15.writeFile(file, PNG.sync.write(png));
|
|
2505
2800
|
}
|
|
2506
2801
|
function cropPng(image, box) {
|
|
2507
2802
|
const out = new PNG({ width: box.w, height: box.h });
|
|
@@ -2801,7 +3096,7 @@ async function uiExtract(opts, log) {
|
|
|
2801
3096
|
const dilatePx = opts.dilate ?? 0;
|
|
2802
3097
|
const sheet = await loadPng(input);
|
|
2803
3098
|
const { width: W, height: H, data } = sheet;
|
|
2804
|
-
await
|
|
3099
|
+
await fs16.mkdir(outDir, { recursive: true });
|
|
2805
3100
|
log.plain(c.bold("genex ui extract"));
|
|
2806
3101
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
|
|
2807
3102
|
let hasTransparency = false;
|
|
@@ -3030,7 +3325,7 @@ async function uiExtract(opts, log) {
|
|
|
3030
3325
|
rimPixels: speckle.sampled
|
|
3031
3326
|
});
|
|
3032
3327
|
}
|
|
3033
|
-
const outPath =
|
|
3328
|
+
const outPath = path15.join(outDir, `${name}.png`);
|
|
3034
3329
|
await writePng(outPath, out);
|
|
3035
3330
|
const sidecar = {
|
|
3036
3331
|
name,
|
|
@@ -3049,7 +3344,7 @@ async function uiExtract(opts, log) {
|
|
|
3049
3344
|
defringed
|
|
3050
3345
|
};
|
|
3051
3346
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
3052
|
-
await
|
|
3347
|
+
await fs16.writeFile(
|
|
3053
3348
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
3054
3349
|
JSON.stringify(sidecarBody, null, 2)
|
|
3055
3350
|
);
|
|
@@ -3058,8 +3353,8 @@ async function uiExtract(opts, log) {
|
|
|
3058
3353
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
3059
3354
|
);
|
|
3060
3355
|
}
|
|
3061
|
-
const debugPath =
|
|
3062
|
-
await
|
|
3356
|
+
const debugPath = path15.join(outDir, "extract-debug.json");
|
|
3357
|
+
await fs16.writeFile(
|
|
3063
3358
|
debugPath,
|
|
3064
3359
|
JSON.stringify(
|
|
3065
3360
|
{
|
|
@@ -3521,7 +3816,7 @@ async function uiMasks(opts, log) {
|
|
|
3521
3816
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
3522
3817
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
3523
3818
|
const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
|
|
3524
|
-
await
|
|
3819
|
+
await fs16.mkdir(outDir, { recursive: true });
|
|
3525
3820
|
log.plain(c.bold("genex ui masks"));
|
|
3526
3821
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
3527
3822
|
const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
|
|
@@ -3574,11 +3869,11 @@ async function uiMasks(opts, log) {
|
|
|
3574
3869
|
});
|
|
3575
3870
|
}
|
|
3576
3871
|
const overlay = makeOverlay(clean2, converted.png);
|
|
3577
|
-
const framePath =
|
|
3578
|
-
const maskPath =
|
|
3579
|
-
const annotatedPath =
|
|
3580
|
-
const overlayPath =
|
|
3581
|
-
const metaPath =
|
|
3872
|
+
const framePath = path15.join(outDir, `${pair.name}-frame.png`);
|
|
3873
|
+
const maskPath = path15.join(outDir, `${pair.name}-mask.png`);
|
|
3874
|
+
const annotatedPath = path15.join(outDir, `${pair.name}-annotated-source.png`);
|
|
3875
|
+
const overlayPath = path15.join(outDir, `${pair.name}-overlay.png`);
|
|
3876
|
+
const metaPath = path15.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
3582
3877
|
await writePng(framePath, clean2);
|
|
3583
3878
|
await writePng(maskPath, converted.png);
|
|
3584
3879
|
await writePng(annotatedPath, annotated);
|
|
@@ -3625,7 +3920,7 @@ async function uiMasks(opts, log) {
|
|
|
3625
3920
|
},
|
|
3626
3921
|
overlay: overlayPath
|
|
3627
3922
|
};
|
|
3628
|
-
await
|
|
3923
|
+
await fs16.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
3629
3924
|
`);
|
|
3630
3925
|
results.push(meta);
|
|
3631
3926
|
const fb = converted.bbox;
|
|
@@ -3641,8 +3936,8 @@ async function uiMasks(opts, log) {
|
|
|
3641
3936
|
);
|
|
3642
3937
|
}
|
|
3643
3938
|
}
|
|
3644
|
-
const indexPath =
|
|
3645
|
-
await
|
|
3939
|
+
const indexPath = path15.join(outDir, "annotated-progress.json");
|
|
3940
|
+
await fs16.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
|
|
3646
3941
|
`);
|
|
3647
3942
|
log.plain("");
|
|
3648
3943
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -3768,7 +4063,7 @@ async function uiTextColor(opts, log) {
|
|
|
3768
4063
|
};
|
|
3769
4064
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3770
4065
|
`);
|
|
3771
|
-
if (opts.out) await
|
|
4066
|
+
if (opts.out) await fs16.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
3772
4067
|
`);
|
|
3773
4068
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
3774
4069
|
}
|
|
@@ -3800,7 +4095,7 @@ async function uiTrim(opts, log) {
|
|
|
3800
4095
|
const sidecar = computeBBoxes(trimmed);
|
|
3801
4096
|
const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
|
|
3802
4097
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
3803
|
-
await
|
|
4098
|
+
await fs16.writeFile(
|
|
3804
4099
|
sidecarPath,
|
|
3805
4100
|
JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
|
|
3806
4101
|
);
|
|
@@ -3907,7 +4202,7 @@ async function uiPlate(opts, log) {
|
|
|
3907
4202
|
fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
|
|
3908
4203
|
}
|
|
3909
4204
|
await writePng(outPath, out);
|
|
3910
|
-
const name =
|
|
4205
|
+
const name = path15.basename(outPath);
|
|
3911
4206
|
log.plain(c.bold("genex ui plate"));
|
|
3912
4207
|
log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
|
|
3913
4208
|
log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
|
|
@@ -4072,13 +4367,13 @@ async function walkFiles(dir) {
|
|
|
4072
4367
|
const out = [];
|
|
4073
4368
|
let entries;
|
|
4074
4369
|
try {
|
|
4075
|
-
entries = await
|
|
4370
|
+
entries = await fs16.readdir(dir, { withFileTypes: true });
|
|
4076
4371
|
} catch {
|
|
4077
4372
|
return out;
|
|
4078
4373
|
}
|
|
4079
4374
|
for (const entry of entries) {
|
|
4080
4375
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
4081
|
-
const p =
|
|
4376
|
+
const p = path15.join(dir, entry.name);
|
|
4082
4377
|
if (entry.isDirectory()) out.push(...await walkFiles(p));
|
|
4083
4378
|
else out.push(p);
|
|
4084
4379
|
}
|
|
@@ -4087,7 +4382,7 @@ async function walkFiles(dir) {
|
|
|
4087
4382
|
async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
4088
4383
|
const viewportFindings = [];
|
|
4089
4384
|
try {
|
|
4090
|
-
const indexHtml = await
|
|
4385
|
+
const indexHtml = await fs16.readFile(path15.join(cwd, "index.html"), "utf8");
|
|
4091
4386
|
if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
|
|
4092
4387
|
viewportFindings.push({
|
|
4093
4388
|
kind: "viewport-meta",
|
|
@@ -4101,28 +4396,28 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4101
4396
|
}
|
|
4102
4397
|
} catch {
|
|
4103
4398
|
}
|
|
4104
|
-
const absAssets =
|
|
4399
|
+
const absAssets = path15.resolve(cwd, assetDir);
|
|
4105
4400
|
try {
|
|
4106
|
-
if (!(await
|
|
4401
|
+
if (!(await fs16.stat(absAssets)).isDirectory()) {
|
|
4107
4402
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
4108
4403
|
}
|
|
4109
4404
|
} catch {
|
|
4110
4405
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
4111
4406
|
}
|
|
4112
|
-
const srcFiles = (await walkFiles(
|
|
4113
|
-
(p) => AUDIT_SRC_EXTS.has(
|
|
4407
|
+
const srcFiles = (await walkFiles(path15.resolve(cwd, srcDir))).filter(
|
|
4408
|
+
(p) => AUDIT_SRC_EXTS.has(path15.extname(p).toLowerCase())
|
|
4114
4409
|
);
|
|
4115
4410
|
try {
|
|
4116
|
-
for (const name of await
|
|
4117
|
-
const ext =
|
|
4118
|
-
if (ext === ".html" || ext === ".css") srcFiles.push(
|
|
4411
|
+
for (const name of await fs16.readdir(cwd)) {
|
|
4412
|
+
const ext = path15.extname(name).toLowerCase();
|
|
4413
|
+
if (ext === ".html" || ext === ".css") srcFiles.push(path15.join(cwd, name));
|
|
4119
4414
|
}
|
|
4120
4415
|
} catch {
|
|
4121
4416
|
}
|
|
4122
4417
|
const sources = [];
|
|
4123
4418
|
for (const p of srcFiles) {
|
|
4124
4419
|
try {
|
|
4125
|
-
sources.push({ rel:
|
|
4420
|
+
sources.push({ rel: path15.relative(cwd, p), text: await fs16.readFile(p, "utf8") });
|
|
4126
4421
|
} catch {
|
|
4127
4422
|
}
|
|
4128
4423
|
}
|
|
@@ -4132,11 +4427,11 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4132
4427
|
const metaByName = /* @__PURE__ */ new Map();
|
|
4133
4428
|
const bboxByPng = /* @__PURE__ */ new Map();
|
|
4134
4429
|
for (const p of assetFiles) {
|
|
4135
|
-
const base =
|
|
4430
|
+
const base = path15.basename(p);
|
|
4136
4431
|
const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
|
|
4137
4432
|
if (metaMatch) {
|
|
4138
4433
|
try {
|
|
4139
|
-
const meta = JSON.parse(await
|
|
4434
|
+
const meta = JSON.parse(await fs16.readFile(p, "utf8"));
|
|
4140
4435
|
metaByName.set(metaMatch[1], {
|
|
4141
4436
|
cleanCrop: meta.clean?.crop ?? null,
|
|
4142
4437
|
loosened: meta.loosened === true
|
|
@@ -4147,7 +4442,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4147
4442
|
}
|
|
4148
4443
|
if (base.endsWith(".bbox.json")) {
|
|
4149
4444
|
try {
|
|
4150
|
-
const sidecar = JSON.parse(await
|
|
4445
|
+
const sidecar = JSON.parse(await fs16.readFile(p, "utf8"));
|
|
4151
4446
|
if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
|
|
4152
4447
|
} catch {
|
|
4153
4448
|
}
|
|
@@ -4156,7 +4451,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4156
4451
|
for (const [name, meta] of metaByName) {
|
|
4157
4452
|
if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
|
|
4158
4453
|
for (const p of assetFiles) {
|
|
4159
|
-
const base =
|
|
4454
|
+
const base = path15.basename(p);
|
|
4160
4455
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4161
4456
|
if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
|
|
4162
4457
|
if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
|
|
@@ -4180,7 +4475,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4180
4475
|
}
|
|
4181
4476
|
const pngByBase = /* @__PURE__ */ new Map();
|
|
4182
4477
|
for (const p of assetFiles) {
|
|
4183
|
-
const base =
|
|
4478
|
+
const base = path15.basename(p);
|
|
4184
4479
|
if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
|
|
4185
4480
|
}
|
|
4186
4481
|
for (const [maskBase, maskPath] of pngByBase) {
|
|
@@ -4193,8 +4488,8 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4193
4488
|
let frame;
|
|
4194
4489
|
let mask;
|
|
4195
4490
|
try {
|
|
4196
|
-
frame = PNG2.sync.read(await
|
|
4197
|
-
mask = PNG2.sync.read(await
|
|
4491
|
+
frame = PNG2.sync.read(await fs16.readFile(framePath));
|
|
4492
|
+
mask = PNG2.sync.read(await fs16.readFile(maskPath));
|
|
4198
4493
|
} catch {
|
|
4199
4494
|
continue;
|
|
4200
4495
|
}
|
|
@@ -4213,7 +4508,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4213
4508
|
if (!referenced(base)) continue;
|
|
4214
4509
|
let png;
|
|
4215
4510
|
try {
|
|
4216
|
-
png = PNG2.sync.read(await
|
|
4511
|
+
png = PNG2.sync.read(await fs16.readFile(p));
|
|
4217
4512
|
} catch {
|
|
4218
4513
|
continue;
|
|
4219
4514
|
}
|
|
@@ -4233,7 +4528,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4233
4528
|
}
|
|
4234
4529
|
const maskReported = /* @__PURE__ */ new Set();
|
|
4235
4530
|
for (const p of assetFiles) {
|
|
4236
|
-
const m = /^(.+)\.annotated-progress\.json$/.exec(
|
|
4531
|
+
const m = /^(.+)\.annotated-progress\.json$/.exec(path15.basename(p));
|
|
4237
4532
|
if (!m) continue;
|
|
4238
4533
|
const maskBase = `${m[1]}-mask.png`;
|
|
4239
4534
|
if (!referenced(maskBase)) {
|
|
@@ -4245,14 +4540,14 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4245
4540
|
}
|
|
4246
4541
|
}
|
|
4247
4542
|
for (const p of assetFiles) {
|
|
4248
|
-
const base =
|
|
4543
|
+
const base = path15.basename(p);
|
|
4249
4544
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4250
4545
|
if (/-annotated-source\.png$|-overlay\.png$/.test(base)) continue;
|
|
4251
4546
|
if (maskReported.has(base)) continue;
|
|
4252
4547
|
if (!referenced(base)) {
|
|
4253
4548
|
findings.push({
|
|
4254
4549
|
kind: "unwired-sprite",
|
|
4255
|
-
message: `${
|
|
4550
|
+
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
4551
|
});
|
|
4257
4552
|
}
|
|
4258
4553
|
}
|
|
@@ -4305,25 +4600,6 @@ async function uiAudit(opts, log) {
|
|
|
4305
4600
|
}
|
|
4306
4601
|
|
|
4307
4602
|
// 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
4603
|
function printMobilePreflight(files, log) {
|
|
4328
4604
|
try {
|
|
4329
4605
|
const scan = scanBundle(files.map((f) => ({ relPath: f.relPath, bytes: f.bytes })));
|
|
@@ -4386,7 +4662,7 @@ async function printUiAuditPreflight(log) {
|
|
|
4386
4662
|
}
|
|
4387
4663
|
async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
4388
4664
|
try {
|
|
4389
|
-
const design = await
|
|
4665
|
+
const design = await fs17.readFile(path16.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
|
|
4390
4666
|
const warnings = [];
|
|
4391
4667
|
if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
|
|
4392
4668
|
warnings.push(
|
|
@@ -4394,7 +4670,7 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4394
4670
|
);
|
|
4395
4671
|
}
|
|
4396
4672
|
if (!/player character:/i.test(design)) {
|
|
4397
|
-
const hasCharacter = await
|
|
4673
|
+
const hasCharacter = await fs17.access(path16.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
|
|
4398
4674
|
if (!hasCharacter && await loadsPlayerBody(cwd)) {
|
|
4399
4675
|
warnings.push(
|
|
4400
4676
|
`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 +4684,12 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4408
4684
|
async function loadsPlayerBody(cwd) {
|
|
4409
4685
|
const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
|
|
4410
4686
|
try {
|
|
4411
|
-
const entries = await
|
|
4687
|
+
const entries = await fs17.readdir(path16.join(cwd, "src"), { recursive: true });
|
|
4412
4688
|
for (const rel of entries) {
|
|
4413
4689
|
if (rel.includes("node_modules")) continue;
|
|
4414
|
-
if (rel.split(
|
|
4690
|
+
if (rel.split(path16.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
|
|
4415
4691
|
if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
|
|
4416
|
-
const text = await
|
|
4692
|
+
const text = await fs17.readFile(path16.join(cwd, "src", rel), "utf8").catch(() => "");
|
|
4417
4693
|
if (BODY_LOADERS.test(text)) return true;
|
|
4418
4694
|
}
|
|
4419
4695
|
} catch {
|
|
@@ -4426,6 +4702,15 @@ function isSecretEnvFile(name) {
|
|
|
4426
4702
|
}
|
|
4427
4703
|
async function deployGame(ctx, opts, log) {
|
|
4428
4704
|
const cwd = process.cwd();
|
|
4705
|
+
const meta = await readProject(cwd);
|
|
4706
|
+
const expectStagingCommit = opts.force ? void 0 : meta?.stagingCommit;
|
|
4707
|
+
if (expectStagingCommit) {
|
|
4708
|
+
const remote = await readRemoteSource(ctx.apiUrl, ctx.token, ctx.projectId);
|
|
4709
|
+
if (remote && isStale(expectStagingCommit, remote.stagingCommit)) {
|
|
4710
|
+
reportStale(log, ctx.slug ?? meta?.slug ?? "this game", expectStagingCommit, remote.stagingCommit);
|
|
4711
|
+
return false;
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4429
4714
|
if (!opts.noBuild && await hasBuildScript(cwd)) {
|
|
4430
4715
|
log.step("Building the production bundle\u2026");
|
|
4431
4716
|
const built = await run("npm", ["run", "build"]);
|
|
@@ -4437,9 +4722,9 @@ async function deployGame(ctx, opts, log) {
|
|
|
4437
4722
|
}
|
|
4438
4723
|
log.success("Built.");
|
|
4439
4724
|
}
|
|
4440
|
-
const distDir =
|
|
4725
|
+
const distDir = path16.join(cwd, "dist");
|
|
4441
4726
|
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
4442
|
-
const rel =
|
|
4727
|
+
const rel = path16.relative(cwd, siteDir) || ".";
|
|
4443
4728
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
4444
4729
|
const files = await collectFiles(siteDir);
|
|
4445
4730
|
if (files.length === 0) {
|
|
@@ -4495,10 +4780,28 @@ async function deployGame(ctx, opts, log) {
|
|
|
4495
4780
|
log.error("Couldn't upload your game \u2014 please try again.");
|
|
4496
4781
|
return false;
|
|
4497
4782
|
}
|
|
4498
|
-
|
|
4783
|
+
const pushed = await pushSource(
|
|
4784
|
+
cwd,
|
|
4785
|
+
ctx,
|
|
4786
|
+
log,
|
|
4787
|
+
opts.channel === "staging" ? "preview" : "main",
|
|
4788
|
+
expectStagingCommit
|
|
4789
|
+
);
|
|
4790
|
+
if (pushed !== true) return false;
|
|
4499
4791
|
log.step("Publishing\u2026");
|
|
4500
4792
|
const published = await callPublish(ctx, commit, opts, log);
|
|
4501
4793
|
if (!published) return false;
|
|
4794
|
+
if (opts.channel === "staging") {
|
|
4795
|
+
const tree = await sourceTreeHash(cwd);
|
|
4796
|
+
const current = await readProject(cwd);
|
|
4797
|
+
if (current) {
|
|
4798
|
+
await writeProject(
|
|
4799
|
+
{ ...current, stagingCommit: commit, ...tree ? { sourceTree: tree } : {} },
|
|
4800
|
+
cwd
|
|
4801
|
+
).catch(() => {
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4502
4805
|
const index = files.find((f) => f.relPath === "index.html");
|
|
4503
4806
|
const liveUrl = published.url || grant.playUrl;
|
|
4504
4807
|
await waitUntilLive(liveUrl, fingerprintOf(index.bytes.toString("utf8")), opts.liveTimeoutMs ?? 2e4, log);
|
|
@@ -4506,7 +4809,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
4506
4809
|
}
|
|
4507
4810
|
async function hasBuildScript(cwd) {
|
|
4508
4811
|
try {
|
|
4509
|
-
const pkg = JSON.parse(await
|
|
4812
|
+
const pkg = JSON.parse(await fs17.readFile(path16.join(cwd, "package.json"), "utf8"));
|
|
4510
4813
|
return Boolean(pkg.scripts?.build);
|
|
4511
4814
|
} catch {
|
|
4512
4815
|
return false;
|
|
@@ -4515,12 +4818,12 @@ async function hasBuildScript(cwd) {
|
|
|
4515
4818
|
async function collectFiles(root) {
|
|
4516
4819
|
const out = [];
|
|
4517
4820
|
const walk2 = async (dir, prefix) => {
|
|
4518
|
-
for (const e of await
|
|
4821
|
+
for (const e of await fs17.readdir(dir, { withFileTypes: true })) {
|
|
4519
4822
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
4520
4823
|
if (e.isDirectory()) {
|
|
4521
|
-
if (!EXCLUDE_DIRS.has(e.name)) await walk2(
|
|
4824
|
+
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path16.join(dir, e.name), relPath);
|
|
4522
4825
|
} else if (e.isFile() && !isSecretEnvFile(e.name)) {
|
|
4523
|
-
out.push({ relPath, bytes: await
|
|
4826
|
+
out.push({ relPath, bytes: await fs17.readFile(path16.join(dir, e.name)) });
|
|
4524
4827
|
}
|
|
4525
4828
|
}
|
|
4526
4829
|
};
|
|
@@ -4737,28 +5040,27 @@ async function callPublish(ctx, commit, opts, log) {
|
|
|
4737
5040
|
}
|
|
4738
5041
|
return { url: body?.url ?? "" };
|
|
4739
5042
|
}
|
|
4740
|
-
async function pushSource(cwd, ctx, log, branch = "main") {
|
|
4741
|
-
const target = await fetchPushUrl(ctx, log);
|
|
5043
|
+
async function pushSource(cwd, ctx, log, branch = "main", expectStagingCommit) {
|
|
5044
|
+
const target = await fetchPushUrl(ctx, log, expectStagingCommit);
|
|
5045
|
+
if (target === "stale") return "stale";
|
|
4742
5046
|
if (!target) return false;
|
|
4743
5047
|
const ref = target.managed ? branch : "main";
|
|
4744
5048
|
if (await pushWorktree(cwd, target.pushUrl, target.managed, log, ref)) return true;
|
|
4745
5049
|
if (!target.managed) return false;
|
|
4746
5050
|
log.info("Retrying the source push\u2026");
|
|
4747
5051
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
4748
|
-
const fresh = await fetchPushUrl(ctx, log);
|
|
5052
|
+
const fresh = await fetchPushUrl(ctx, log, expectStagingCommit);
|
|
5053
|
+
if (fresh === "stale") return "stale";
|
|
4749
5054
|
if (!fresh) return false;
|
|
4750
5055
|
return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log, ref);
|
|
4751
5056
|
}
|
|
4752
|
-
function urlHasEmbeddedCredentials(pushUrl) {
|
|
4753
|
-
return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(pushUrl);
|
|
4754
|
-
}
|
|
4755
5057
|
async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
4756
5058
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
4757
5059
|
const failed = () => {
|
|
4758
5060
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
4759
5061
|
return false;
|
|
4760
5062
|
};
|
|
4761
|
-
const gitDir = await
|
|
5063
|
+
const gitDir = await fs17.mkdtemp(path16.join(os9.tmpdir(), "genex-source-"));
|
|
4762
5064
|
const base = { GIT_DIR: gitDir };
|
|
4763
5065
|
if (urlHasEmbeddedCredentials(pushUrl)) {
|
|
4764
5066
|
base.GIT_CONFIG_COUNT = "1";
|
|
@@ -4773,12 +5075,12 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
4773
5075
|
};
|
|
4774
5076
|
try {
|
|
4775
5077
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
4776
|
-
await
|
|
4777
|
-
|
|
5078
|
+
await fs17.writeFile(
|
|
5079
|
+
path16.join(gitDir, "info", "exclude"),
|
|
4778
5080
|
// .env* are secrets — never publish them; `!` keeps the non-secret template.
|
|
4779
5081
|
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
4780
5082
|
);
|
|
4781
|
-
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE:
|
|
5083
|
+
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path16.join(gitDir, "index-source") };
|
|
4782
5084
|
let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
|
|
4783
5085
|
if (!lfs) {
|
|
4784
5086
|
log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
|
|
@@ -4829,16 +5131,19 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main") {
|
|
|
4829
5131
|
} catch {
|
|
4830
5132
|
return failed();
|
|
4831
5133
|
} finally {
|
|
4832
|
-
await
|
|
5134
|
+
await fs17.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
4833
5135
|
});
|
|
4834
5136
|
}
|
|
4835
5137
|
}
|
|
4836
|
-
async function fetchPushUrl(ctx, log) {
|
|
5138
|
+
async function fetchPushUrl(ctx, log, expectStagingCommit) {
|
|
4837
5139
|
let res;
|
|
4838
5140
|
try {
|
|
4839
5141
|
res = await apiFetch(`${ctx.apiUrl}/api/projects/${ctx.projectId}/push-token`, {
|
|
4840
5142
|
method: "POST",
|
|
4841
|
-
headers: {
|
|
5143
|
+
headers: {
|
|
5144
|
+
Authorization: `Bearer ${ctx.token}`,
|
|
5145
|
+
...expectStagingCommit ? { "If-Match": expectStagingCommit } : {}
|
|
5146
|
+
}
|
|
4842
5147
|
});
|
|
4843
5148
|
} catch (err) {
|
|
4844
5149
|
log.error(`Couldn't reach the API to authorize the source push: ${String(err)}`);
|
|
@@ -4848,6 +5153,11 @@ async function fetchPushUrl(ctx, log) {
|
|
|
4848
5153
|
log.error("Not authorized \u2014 your token may have expired. Re-run `genex init`.");
|
|
4849
5154
|
return null;
|
|
4850
5155
|
}
|
|
5156
|
+
if (res.status === 409) {
|
|
5157
|
+
const body = await res.json().catch(() => null);
|
|
5158
|
+
reportStale(log, ctx.slug ?? "this game", expectStagingCommit, body?.stagingCommit ?? null);
|
|
5159
|
+
return "stale";
|
|
5160
|
+
}
|
|
4851
5161
|
if (res.status === 429) {
|
|
4852
5162
|
log.error(`Rate limited authorizing the source push (HTTP 429).${retryAfterHint(res)}`);
|
|
4853
5163
|
return null;
|
|
@@ -4861,11 +5171,11 @@ async function fetchPushUrl(ctx, log) {
|
|
|
4861
5171
|
log.error("The API didn't return a push URL.");
|
|
4862
5172
|
return null;
|
|
4863
5173
|
}
|
|
4864
|
-
return { pushUrl: data.pushUrl, managed: data.managed !== false };
|
|
5174
|
+
return { pushUrl: data.pushUrl, managed: data.managed !== false, sourceRef: data.sourceRef ?? null };
|
|
4865
5175
|
}
|
|
4866
5176
|
async function isDir2(p) {
|
|
4867
5177
|
try {
|
|
4868
|
-
return (await
|
|
5178
|
+
return (await fs17.stat(p)).isDirectory();
|
|
4869
5179
|
} catch {
|
|
4870
5180
|
return false;
|
|
4871
5181
|
}
|
|
@@ -5312,17 +5622,17 @@ async function promoteBuild(apiUrl, projectId, token, log) {
|
|
|
5312
5622
|
}
|
|
5313
5623
|
|
|
5314
5624
|
// src/lib/detect-features.ts
|
|
5315
|
-
import
|
|
5316
|
-
import
|
|
5625
|
+
import fs19 from "fs/promises";
|
|
5626
|
+
import path18 from "path";
|
|
5317
5627
|
|
|
5318
5628
|
// src/lib/generation-ledger.ts
|
|
5319
|
-
import
|
|
5320
|
-
import
|
|
5321
|
-
var ledgerPath = (cwd) =>
|
|
5629
|
+
import fs18 from "fs/promises";
|
|
5630
|
+
import path17 from "path";
|
|
5631
|
+
var ledgerPath = (cwd) => path17.join(cwd, ".genex", "generations.ndjson");
|
|
5322
5632
|
async function append(cwd, event) {
|
|
5323
5633
|
try {
|
|
5324
|
-
await
|
|
5325
|
-
await
|
|
5634
|
+
await fs18.access(path17.join(cwd, ".genex"));
|
|
5635
|
+
await fs18.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
5326
5636
|
`, "utf8");
|
|
5327
5637
|
} catch {
|
|
5328
5638
|
}
|
|
@@ -5330,7 +5640,7 @@ async function append(cwd, event) {
|
|
|
5330
5640
|
async function readLedger(cwd = process.cwd()) {
|
|
5331
5641
|
let raw;
|
|
5332
5642
|
try {
|
|
5333
|
-
raw = await
|
|
5643
|
+
raw = await fs18.readFile(ledgerPath(cwd), "utf8");
|
|
5334
5644
|
} catch {
|
|
5335
5645
|
return [];
|
|
5336
5646
|
}
|
|
@@ -5384,7 +5694,7 @@ async function countFailed(kind, cwd = process.cwd()) {
|
|
|
5384
5694
|
// src/lib/detect-features.ts
|
|
5385
5695
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
5386
5696
|
try {
|
|
5387
|
-
const raw = await
|
|
5697
|
+
const raw = await fs19.readFile(path18.join(cwd, "package.json"), "utf8");
|
|
5388
5698
|
const pkg = JSON.parse(raw);
|
|
5389
5699
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
5390
5700
|
return typeof version === "string" && version ? version : null;
|
|
@@ -5394,7 +5704,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
5394
5704
|
}
|
|
5395
5705
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
5396
5706
|
try {
|
|
5397
|
-
const raw = await
|
|
5707
|
+
const raw = await fs19.readFile(path18.join(cwd, "package.json"), "utf8");
|
|
5398
5708
|
const pkg = JSON.parse(raw);
|
|
5399
5709
|
return Boolean(
|
|
5400
5710
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -5406,7 +5716,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
5406
5716
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
5407
5717
|
let pkg;
|
|
5408
5718
|
try {
|
|
5409
|
-
pkg = JSON.parse(await
|
|
5719
|
+
pkg = JSON.parse(await fs19.readFile(path18.join(cwd, "package.json"), "utf8"));
|
|
5410
5720
|
} catch (err) {
|
|
5411
5721
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
5412
5722
|
return null;
|
|
@@ -5424,15 +5734,15 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
5424
5734
|
var TOUCH_KIT_MARKERS = /controllers\/touch\/|controllers\/character\/touch-joystick|TouchJoystick|VirtualButton|DragZone|RotateOverlay/;
|
|
5425
5735
|
async function detectMobileControls(cwd = process.cwd()) {
|
|
5426
5736
|
try {
|
|
5427
|
-
const raw = await
|
|
5737
|
+
const raw = await fs19.readFile(path18.join(cwd, "package.json"), "utf8");
|
|
5428
5738
|
const pkg = JSON.parse(raw);
|
|
5429
5739
|
if (pkg.genex?.mobileControls === true) return true;
|
|
5430
5740
|
} catch {
|
|
5431
5741
|
}
|
|
5432
|
-
const srcDir =
|
|
5742
|
+
const srcDir = path18.join(cwd, "src");
|
|
5433
5743
|
let entries;
|
|
5434
5744
|
try {
|
|
5435
|
-
entries = await
|
|
5745
|
+
entries = await fs19.readdir(srcDir, { recursive: true });
|
|
5436
5746
|
} catch {
|
|
5437
5747
|
return false;
|
|
5438
5748
|
}
|
|
@@ -5440,7 +5750,7 @@ async function detectMobileControls(cwd = process.cwd()) {
|
|
|
5440
5750
|
if (rel.includes("node_modules")) continue;
|
|
5441
5751
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
5442
5752
|
try {
|
|
5443
|
-
const content = await
|
|
5753
|
+
const content = await fs19.readFile(path18.join(srcDir, rel), "utf8");
|
|
5444
5754
|
if (TOUCH_KIT_MARKERS.test(content)) return true;
|
|
5445
5755
|
} catch {
|
|
5446
5756
|
}
|
|
@@ -5449,10 +5759,10 @@ async function detectMobileControls(cwd = process.cwd()) {
|
|
|
5449
5759
|
}
|
|
5450
5760
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
5451
5761
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
5452
|
-
const srcDir =
|
|
5762
|
+
const srcDir = path18.join(cwd, "src");
|
|
5453
5763
|
let entries;
|
|
5454
5764
|
try {
|
|
5455
|
-
entries = await
|
|
5765
|
+
entries = await fs19.readdir(srcDir, { recursive: true });
|
|
5456
5766
|
} catch {
|
|
5457
5767
|
return false;
|
|
5458
5768
|
}
|
|
@@ -5460,7 +5770,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
5460
5770
|
if (rel.includes("node_modules")) continue;
|
|
5461
5771
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
5462
5772
|
try {
|
|
5463
|
-
const content = await
|
|
5773
|
+
const content = await fs19.readFile(path18.join(srcDir, rel), "utf8");
|
|
5464
5774
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
5465
5775
|
} catch {
|
|
5466
5776
|
}
|
|
@@ -5509,19 +5819,19 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
5509
5819
|
depthRange: [],
|
|
5510
5820
|
mediaElementAudio: []
|
|
5511
5821
|
};
|
|
5512
|
-
const srcDir =
|
|
5822
|
+
const srcDir = path18.join(cwd, "src");
|
|
5513
5823
|
let entries;
|
|
5514
5824
|
try {
|
|
5515
|
-
entries = await
|
|
5825
|
+
entries = await fs19.readdir(srcDir, { recursive: true });
|
|
5516
5826
|
} catch {
|
|
5517
5827
|
return found;
|
|
5518
5828
|
}
|
|
5519
5829
|
for (const nativeRel of entries) {
|
|
5520
5830
|
if (nativeRel.includes("node_modules")) continue;
|
|
5521
5831
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
|
|
5522
|
-
const raw = await
|
|
5832
|
+
const raw = await fs19.readFile(path18.join(srcDir, nativeRel), "utf8").catch(() => "");
|
|
5523
5833
|
if (!raw) continue;
|
|
5524
|
-
const rel = nativeRel.split(
|
|
5834
|
+
const rel = nativeRel.split(path18.sep).join("/");
|
|
5525
5835
|
const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
|
|
5526
5836
|
const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
5527
5837
|
let m;
|
|
@@ -5579,25 +5889,25 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
5579
5889
|
let haystack = "";
|
|
5580
5890
|
const read = async (file) => {
|
|
5581
5891
|
try {
|
|
5582
|
-
haystack += await
|
|
5892
|
+
haystack += await fs19.readFile(file, "utf8");
|
|
5583
5893
|
} catch {
|
|
5584
5894
|
}
|
|
5585
5895
|
};
|
|
5586
5896
|
try {
|
|
5587
|
-
for (const entry of await
|
|
5897
|
+
for (const entry of await fs19.readdir(cwd, { withFileTypes: true })) {
|
|
5588
5898
|
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
|
|
5589
|
-
await read(
|
|
5899
|
+
await read(path18.join(cwd, entry.name));
|
|
5590
5900
|
}
|
|
5591
5901
|
}
|
|
5592
5902
|
} catch {
|
|
5593
5903
|
}
|
|
5594
5904
|
for (const sub of ["src", "public"]) {
|
|
5595
5905
|
try {
|
|
5596
|
-
const entries = await
|
|
5906
|
+
const entries = await fs19.readdir(path18.join(cwd, sub), { recursive: true });
|
|
5597
5907
|
for (const rel of entries) {
|
|
5598
5908
|
if (rel.includes("node_modules")) continue;
|
|
5599
5909
|
if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
|
|
5600
|
-
await read(
|
|
5910
|
+
await read(path18.join(cwd, sub, rel));
|
|
5601
5911
|
}
|
|
5602
5912
|
} catch {
|
|
5603
5913
|
}
|
|
@@ -5729,7 +6039,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
5729
6039
|
} catch {
|
|
5730
6040
|
return true;
|
|
5731
6041
|
}
|
|
5732
|
-
const gitConfig = await
|
|
6042
|
+
const gitConfig = await fs19.readFile(path18.join(cwd, ".git", "config"), "utf8").catch(() => "");
|
|
5733
6043
|
for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
|
|
5734
6044
|
try {
|
|
5735
6045
|
const u = new URL(m[1]);
|
|
@@ -5737,7 +6047,7 @@ async function borrowEvidence(meta, cwd) {
|
|
|
5737
6047
|
} catch {
|
|
5738
6048
|
}
|
|
5739
6049
|
}
|
|
5740
|
-
const readme = await
|
|
6050
|
+
const readme = await fs19.readFile(path18.join(cwd, "README.md"), "utf8").catch(() => "");
|
|
5741
6051
|
return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
|
|
5742
6052
|
}
|
|
5743
6053
|
|
|
@@ -5803,7 +6113,7 @@ async function runPublish(opts) {
|
|
|
5803
6113
|
advisoryNudges(log, detections);
|
|
5804
6114
|
if (!opts.noPush) {
|
|
5805
6115
|
const ok = await deployGame(
|
|
5806
|
-
{ projectId: meta.id, apiUrl, token },
|
|
6116
|
+
{ projectId: meta.id, apiUrl, token, slug: meta.slug },
|
|
5807
6117
|
{
|
|
5808
6118
|
// Publish deploys to staging and then promotes, so both channels end up
|
|
5809
6119
|
// naming the same build. Deploying straight to production instead would
|
|
@@ -5811,6 +6121,7 @@ async function runPublish(opts) {
|
|
|
5811
6121
|
// staging — the owner would publish and then see a stale game.
|
|
5812
6122
|
channel: "staging",
|
|
5813
6123
|
noBuild: opts.noBuild,
|
|
6124
|
+
force: opts.force,
|
|
5814
6125
|
matchmaking: detections.matchmaking,
|
|
5815
6126
|
embedSdkVersion: detections.embedSdkVersion,
|
|
5816
6127
|
multiplayer: detections.multiplayer,
|
|
@@ -5904,7 +6215,7 @@ async function runPreview(opts) {
|
|
|
5904
6215
|
await exploreReuseNudge(log, meta);
|
|
5905
6216
|
const apiUrl = getApiUrl(meta.apiUrl);
|
|
5906
6217
|
const ok = await deployGame(
|
|
5907
|
-
{ projectId: meta.id, apiUrl, token },
|
|
6218
|
+
{ projectId: meta.id, apiUrl, token, slug: meta.slug },
|
|
5908
6219
|
// Detections reach the server on every preview too: matchmaking so a draft's
|
|
5909
6220
|
// declared preset doesn't silently run the default (null clears a removed
|
|
5910
6221
|
// config), embedSdkVersion/multiplayer so the dashboard's Publish button can
|
|
@@ -5915,6 +6226,7 @@ async function runPreview(opts) {
|
|
|
5915
6226
|
// was inside, and the only protection was a warning line below.
|
|
5916
6227
|
channel: "staging",
|
|
5917
6228
|
noBuild: opts.noBuild,
|
|
6229
|
+
force: opts.force,
|
|
5918
6230
|
matchmaking: detections.matchmaking,
|
|
5919
6231
|
embedSdkVersion: detections.embedSdkVersion,
|
|
5920
6232
|
multiplayer: detections.multiplayer,
|
|
@@ -5987,8 +6299,8 @@ async function runPromote(opts) {
|
|
|
5987
6299
|
}
|
|
5988
6300
|
|
|
5989
6301
|
// src/commands/generate.ts
|
|
5990
|
-
import
|
|
5991
|
-
import
|
|
6302
|
+
import fs20 from "fs/promises";
|
|
6303
|
+
import path19 from "path";
|
|
5992
6304
|
import { PNG as PNG4 } from "pngjs";
|
|
5993
6305
|
|
|
5994
6306
|
// src/lib/glass.ts
|
|
@@ -6069,7 +6381,7 @@ function solveMagentaGlass(source) {
|
|
|
6069
6381
|
}
|
|
6070
6382
|
|
|
6071
6383
|
// src/lib/open.ts
|
|
6072
|
-
import { spawn as
|
|
6384
|
+
import { spawn as spawn5 } from "child_process";
|
|
6073
6385
|
function tokenize(cmd) {
|
|
6074
6386
|
return cmd.trim().split(/\s+/).filter(Boolean);
|
|
6075
6387
|
}
|
|
@@ -6098,7 +6410,7 @@ function openUrl(url) {
|
|
|
6098
6410
|
}
|
|
6099
6411
|
}
|
|
6100
6412
|
try {
|
|
6101
|
-
const child =
|
|
6413
|
+
const child = spawn5(command, args, { stdio: "ignore", detached: true });
|
|
6102
6414
|
child.on("error", () => {
|
|
6103
6415
|
});
|
|
6104
6416
|
child.unref();
|
|
@@ -6158,7 +6470,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
6158
6470
|
async function inlineLocalImage(filePath, flag) {
|
|
6159
6471
|
let bytes;
|
|
6160
6472
|
try {
|
|
6161
|
-
bytes = await
|
|
6473
|
+
bytes = await fs20.readFile(filePath);
|
|
6162
6474
|
} catch {
|
|
6163
6475
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
6164
6476
|
}
|
|
@@ -6168,7 +6480,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
6168
6480
|
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
6481
|
};
|
|
6170
6482
|
}
|
|
6171
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
6483
|
+
const mime = IMAGE_MIME_BY_EXT[path19.extname(filePath).toLowerCase()] ?? "image/png";
|
|
6172
6484
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
6173
6485
|
}
|
|
6174
6486
|
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 +6676,7 @@ async function runGenerate(kind, opts) {
|
|
|
6364
6676
|
return;
|
|
6365
6677
|
}
|
|
6366
6678
|
try {
|
|
6367
|
-
const bytes = await
|
|
6679
|
+
const bytes = await fs20.readFile(opts.inpaintUrl);
|
|
6368
6680
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
6369
6681
|
} catch {
|
|
6370
6682
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -6488,7 +6800,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
6488
6800
|
return;
|
|
6489
6801
|
}
|
|
6490
6802
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
6491
|
-
await
|
|
6803
|
+
await fs20.mkdir(outDir, { recursive: true });
|
|
6492
6804
|
const solved = [];
|
|
6493
6805
|
for (let i = 0; i < files.length; i++) {
|
|
6494
6806
|
const f = files[i];
|
|
@@ -6520,8 +6832,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
6520
6832
|
});
|
|
6521
6833
|
continue;
|
|
6522
6834
|
}
|
|
6523
|
-
const outPath =
|
|
6524
|
-
await
|
|
6835
|
+
const outPath = path19.join(outDir, `glass-${i + 1}.png`);
|
|
6836
|
+
await fs20.writeFile(outPath, PNG4.sync.write(r.png));
|
|
6525
6837
|
solved.push({
|
|
6526
6838
|
path: outPath,
|
|
6527
6839
|
url: f.url,
|
|
@@ -7118,8 +7430,8 @@ async function toRow(e, v, cwd) {
|
|
|
7118
7430
|
}
|
|
7119
7431
|
|
|
7120
7432
|
// src/commands/controller.ts
|
|
7121
|
-
import
|
|
7122
|
-
import
|
|
7433
|
+
import fs22 from "fs/promises";
|
|
7434
|
+
import path21 from "path";
|
|
7123
7435
|
|
|
7124
7436
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
7125
7437
|
import { createHash } from "crypto";
|
|
@@ -16249,9 +16561,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
16249
16561
|
}
|
|
16250
16562
|
|
|
16251
16563
|
// src/lib/anims.ts
|
|
16252
|
-
import
|
|
16253
|
-
import
|
|
16254
|
-
var ANIMS_DEST =
|
|
16564
|
+
import fs21 from "fs/promises";
|
|
16565
|
+
import path20 from "path";
|
|
16566
|
+
var ANIMS_DEST = path20.join("public", "assets", "anims");
|
|
16255
16567
|
var HIDDEN_TAG = "reference";
|
|
16256
16568
|
async function runAnims(opts) {
|
|
16257
16569
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -16267,7 +16579,7 @@ async function runAnims(opts) {
|
|
|
16267
16579
|
printCatalog(log, manifest, selectors);
|
|
16268
16580
|
return;
|
|
16269
16581
|
}
|
|
16270
|
-
const controllerMarker =
|
|
16582
|
+
const controllerMarker = path20.join(root, "src", "controllers", "character");
|
|
16271
16583
|
if (!await exists2(controllerMarker)) {
|
|
16272
16584
|
log.error(
|
|
16273
16585
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -16276,11 +16588,11 @@ async function runAnims(opts) {
|
|
|
16276
16588
|
process.exitCode = 1;
|
|
16277
16589
|
return;
|
|
16278
16590
|
}
|
|
16279
|
-
const destDir =
|
|
16280
|
-
const gameManifestPath =
|
|
16591
|
+
const destDir = path20.join(root, ANIMS_DEST);
|
|
16592
|
+
const gameManifestPath = path20.join(destDir, "manifest.json");
|
|
16281
16593
|
if (opts.reset) {
|
|
16282
|
-
await
|
|
16283
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
16594
|
+
await fs21.rm(destDir, { recursive: true, force: true });
|
|
16595
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path20.sep)} (--reset)`);
|
|
16284
16596
|
}
|
|
16285
16597
|
if (selectors.length === 0) {
|
|
16286
16598
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -16318,35 +16630,35 @@ async function runAnims(opts) {
|
|
|
16318
16630
|
}
|
|
16319
16631
|
}
|
|
16320
16632
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
16321
|
-
const cacheDir =
|
|
16633
|
+
const cacheDir = path20.join(
|
|
16322
16634
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
16323
16635
|
`${manifest.library}-v${manifest.version}`
|
|
16324
16636
|
);
|
|
16325
|
-
await
|
|
16326
|
-
await
|
|
16637
|
+
await fs21.mkdir(cacheDir, { recursive: true });
|
|
16638
|
+
await fs21.mkdir(destDir, { recursive: true });
|
|
16327
16639
|
const base = getAnimsBase(opts.animsBase);
|
|
16328
16640
|
let installedCount = 0;
|
|
16329
16641
|
let presentCount = 0;
|
|
16330
16642
|
let addedBytes = 0;
|
|
16331
16643
|
const failures = [];
|
|
16332
16644
|
for (const entry of wanted) {
|
|
16333
|
-
const dest =
|
|
16645
|
+
const dest = path20.join(destDir, entry.file);
|
|
16334
16646
|
if (await hasSize(dest, entry.bytes)) {
|
|
16335
16647
|
presentCount++;
|
|
16336
16648
|
continue;
|
|
16337
16649
|
}
|
|
16338
16650
|
try {
|
|
16339
|
-
const cached =
|
|
16651
|
+
const cached = path20.join(cacheDir, entry.file);
|
|
16340
16652
|
if (!await hasSize(cached, entry.bytes)) {
|
|
16341
16653
|
const res = await fetch(base + entry.file);
|
|
16342
16654
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
16343
16655
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
16344
|
-
await
|
|
16656
|
+
await fs21.writeFile(cached, buf);
|
|
16345
16657
|
}
|
|
16346
|
-
await
|
|
16658
|
+
await fs21.copyFile(cached, dest);
|
|
16347
16659
|
installedCount++;
|
|
16348
16660
|
addedBytes += entry.bytes;
|
|
16349
|
-
log.dim(` ${
|
|
16661
|
+
log.dim(` ${path20.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
16350
16662
|
} catch (err) {
|
|
16351
16663
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
16352
16664
|
}
|
|
@@ -16362,13 +16674,13 @@ async function runAnims(opts) {
|
|
|
16362
16674
|
version: manifest.version,
|
|
16363
16675
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
16364
16676
|
};
|
|
16365
|
-
await
|
|
16677
|
+
await fs21.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
16366
16678
|
log.plain("");
|
|
16367
16679
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
16368
16680
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
16369
16681
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
16370
16682
|
log.success(
|
|
16371
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
16683
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path20.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
16372
16684
|
);
|
|
16373
16685
|
for (const [selector, entries] of resolved) {
|
|
16374
16686
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -16400,8 +16712,8 @@ async function loadManifest(baseOverride) {
|
|
|
16400
16712
|
}
|
|
16401
16713
|
} catch {
|
|
16402
16714
|
}
|
|
16403
|
-
const snapshotPath =
|
|
16404
|
-
const manifest = JSON.parse(await
|
|
16715
|
+
const snapshotPath = path20.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
16716
|
+
const manifest = JSON.parse(await fs21.readFile(snapshotPath, "utf8"));
|
|
16405
16717
|
return { manifest, source: "snapshot" };
|
|
16406
16718
|
}
|
|
16407
16719
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -16519,21 +16831,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
16519
16831
|
}
|
|
16520
16832
|
async function readGameManifest(file) {
|
|
16521
16833
|
try {
|
|
16522
|
-
return JSON.parse(await
|
|
16834
|
+
return JSON.parse(await fs21.readFile(file, "utf8"));
|
|
16523
16835
|
} catch {
|
|
16524
16836
|
return null;
|
|
16525
16837
|
}
|
|
16526
16838
|
}
|
|
16527
16839
|
async function hasSize(file, bytes) {
|
|
16528
16840
|
try {
|
|
16529
|
-
return (await
|
|
16841
|
+
return (await fs21.stat(file)).size === bytes;
|
|
16530
16842
|
} catch {
|
|
16531
16843
|
return false;
|
|
16532
16844
|
}
|
|
16533
16845
|
}
|
|
16534
16846
|
async function exists2(p) {
|
|
16535
16847
|
try {
|
|
16536
|
-
await
|
|
16848
|
+
await fs21.access(p);
|
|
16537
16849
|
return true;
|
|
16538
16850
|
} catch {
|
|
16539
16851
|
return false;
|
|
@@ -16727,8 +17039,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
16727
17039
|
]
|
|
16728
17040
|
}
|
|
16729
17041
|
};
|
|
16730
|
-
var CODE_DEST =
|
|
16731
|
-
var ASSETS_DEST =
|
|
17042
|
+
var CODE_DEST = path21.join("src", "controllers");
|
|
17043
|
+
var ASSETS_DEST = path21.join("public", "assets");
|
|
16732
17044
|
async function runController(opts) {
|
|
16733
17045
|
const log = createLogger({ quiet: opts.quiet });
|
|
16734
17046
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -16745,31 +17057,31 @@ async function runController(opts) {
|
|
|
16745
17057
|
process.exitCode = 1;
|
|
16746
17058
|
return;
|
|
16747
17059
|
}
|
|
16748
|
-
const srcDir =
|
|
17060
|
+
const srcDir = path21.join(getTemplatesDir(), "controllers");
|
|
16749
17061
|
const root = opts.cwd ?? process.cwd();
|
|
16750
17062
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
16751
17063
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
16752
17064
|
log.plain("");
|
|
16753
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
17065
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path21.sep)}`);
|
|
16754
17066
|
const plan = [
|
|
16755
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
17067
|
+
...set.code.map((rel) => ({ from: rel, rel: path21.join(CODE_DEST, rel) })),
|
|
16756
17068
|
...set.assets.map((rel) => ({
|
|
16757
17069
|
from: rel,
|
|
16758
|
-
rel:
|
|
17070
|
+
rel: path21.join(ASSETS_DEST, path21.basename(rel))
|
|
16759
17071
|
}))
|
|
16760
17072
|
];
|
|
16761
17073
|
let copied = 0;
|
|
16762
17074
|
let skipped = 0;
|
|
16763
17075
|
try {
|
|
16764
17076
|
for (const file of plan) {
|
|
16765
|
-
const dest =
|
|
17077
|
+
const dest = path21.join(root, file.rel);
|
|
16766
17078
|
if (!opts.force && await exists3(dest)) {
|
|
16767
17079
|
skipped++;
|
|
16768
17080
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
16769
17081
|
continue;
|
|
16770
17082
|
}
|
|
16771
|
-
await
|
|
16772
|
-
await
|
|
17083
|
+
await fs22.mkdir(path21.dirname(dest), { recursive: true });
|
|
17084
|
+
await fs22.copyFile(path21.join(srcDir, file.from), dest);
|
|
16773
17085
|
copied++;
|
|
16774
17086
|
log.dim(` ${file.rel}`);
|
|
16775
17087
|
}
|
|
@@ -16822,7 +17134,7 @@ async function runController(opts) {
|
|
|
16822
17134
|
for (const line of set.sketch) {
|
|
16823
17135
|
log.dim(` ${line}`);
|
|
16824
17136
|
}
|
|
16825
|
-
if (kind === "character" && !await exists3(
|
|
17137
|
+
if (kind === "character" && !await exists3(path21.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
16826
17138
|
log.plain("");
|
|
16827
17139
|
log.plain(
|
|
16828
17140
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -16854,9 +17166,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
16854
17166
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
16855
17167
|
}
|
|
16856
17168
|
assertCompleteMeshyControllerPack(manifest);
|
|
16857
|
-
const destination =
|
|
16858
|
-
await
|
|
16859
|
-
await
|
|
17169
|
+
const destination = path21.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
17170
|
+
await fs22.mkdir(path21.dirname(destination), { recursive: true });
|
|
17171
|
+
await fs22.writeFile(
|
|
16860
17172
|
destination,
|
|
16861
17173
|
`${JSON.stringify(manifest, null, 2)}
|
|
16862
17174
|
`
|
|
@@ -16994,14 +17306,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
16994
17306
|
}
|
|
16995
17307
|
async function installFallbackAvatar(args) {
|
|
16996
17308
|
const { root, srcDir, log } = args;
|
|
16997
|
-
const dest =
|
|
16998
|
-
await
|
|
16999
|
-
await
|
|
17309
|
+
const dest = path21.join(root, ASSETS_DEST, "avatar.vrm");
|
|
17310
|
+
await fs22.mkdir(path21.dirname(dest), { recursive: true });
|
|
17311
|
+
await fs22.copyFile(path21.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
17000
17312
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
17001
17313
|
}
|
|
17002
17314
|
async function exists3(p) {
|
|
17003
17315
|
try {
|
|
17004
|
-
await
|
|
17316
|
+
await fs22.access(p);
|
|
17005
17317
|
return true;
|
|
17006
17318
|
} catch {
|
|
17007
17319
|
return false;
|
|
@@ -17009,8 +17321,8 @@ async function exists3(p) {
|
|
|
17009
17321
|
}
|
|
17010
17322
|
|
|
17011
17323
|
// src/commands/character.ts
|
|
17012
|
-
import
|
|
17013
|
-
import
|
|
17324
|
+
import fs23 from "fs/promises";
|
|
17325
|
+
import path22 from "path";
|
|
17014
17326
|
function exactAnimation(selector) {
|
|
17015
17327
|
const trimmed = selector.trim();
|
|
17016
17328
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -17090,7 +17402,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
17090
17402
|
}
|
|
17091
17403
|
process.exitCode = 1;
|
|
17092
17404
|
}
|
|
17093
|
-
var INSTALLED_MANIFEST =
|
|
17405
|
+
var INSTALLED_MANIFEST = path22.join("public", "assets", "meshy-character.json");
|
|
17094
17406
|
async function resolveAdoptTarget(selector) {
|
|
17095
17407
|
const trimmed = selector?.trim();
|
|
17096
17408
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -17099,7 +17411,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
17099
17411
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
17100
17412
|
let raw;
|
|
17101
17413
|
try {
|
|
17102
|
-
raw = await
|
|
17414
|
+
raw = await fs23.readFile(file, "utf8");
|
|
17103
17415
|
} catch {
|
|
17104
17416
|
return {
|
|
17105
17417
|
ok: false,
|
|
@@ -17577,22 +17889,22 @@ async function context2(opts) {
|
|
|
17577
17889
|
const project = await readProject();
|
|
17578
17890
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
17579
17891
|
}
|
|
17580
|
-
async function readVideo(
|
|
17892
|
+
async function readVideo(path25, log) {
|
|
17581
17893
|
let bytes;
|
|
17582
17894
|
try {
|
|
17583
|
-
bytes = await readFile(
|
|
17895
|
+
bytes = await readFile(path25);
|
|
17584
17896
|
} catch {
|
|
17585
|
-
log.error(`Can't read ${
|
|
17897
|
+
log.error(`Can't read ${path25}.`);
|
|
17586
17898
|
return null;
|
|
17587
17899
|
}
|
|
17588
17900
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
17589
|
-
log.error(`${basename(
|
|
17901
|
+
log.error(`${basename(path25)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
17590
17902
|
return null;
|
|
17591
17903
|
}
|
|
17592
17904
|
return bytes;
|
|
17593
17905
|
}
|
|
17594
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
17595
|
-
const contentType = /\.mov$/i.test(
|
|
17906
|
+
async function uploadVideo(apiUrl, token, characterId, path25, bytes, log) {
|
|
17907
|
+
const contentType = /\.mov$/i.test(path25) ? "video/quicktime" : "video/mp4";
|
|
17596
17908
|
const minted = await apiFetch(
|
|
17597
17909
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
17598
17910
|
{
|
|
@@ -17607,7 +17919,7 @@ async function uploadVideo(apiUrl, token, characterId, path23, bytes, log) {
|
|
|
17607
17919
|
return null;
|
|
17608
17920
|
}
|
|
17609
17921
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
17610
|
-
log.dim(` uploading ${basename(
|
|
17922
|
+
log.dim(` uploading ${basename(path25)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
17611
17923
|
const put = await fetch(uploadUrl, {
|
|
17612
17924
|
method: "PUT",
|
|
17613
17925
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -17923,8 +18235,8 @@ function rank(items, query) {
|
|
|
17923
18235
|
}
|
|
17924
18236
|
|
|
17925
18237
|
// src/commands/motion.ts
|
|
17926
|
-
import
|
|
17927
|
-
import
|
|
18238
|
+
import fs24 from "fs/promises";
|
|
18239
|
+
import path23 from "path";
|
|
17928
18240
|
|
|
17929
18241
|
// src/lib/motion/npz.ts
|
|
17930
18242
|
import zlib from "zlib";
|
|
@@ -19175,7 +19487,7 @@ async function motionGen(opts, log) {
|
|
|
19175
19487
|
}
|
|
19176
19488
|
if (opts.constraintsPath !== void 0) {
|
|
19177
19489
|
try {
|
|
19178
|
-
const raw = await
|
|
19490
|
+
const raw = await fs24.readFile(opts.constraintsPath, "utf8");
|
|
19179
19491
|
generationOptions.constraints = JSON.parse(raw);
|
|
19180
19492
|
} catch {
|
|
19181
19493
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -19199,10 +19511,10 @@ async function motionGen(opts, log) {
|
|
|
19199
19511
|
async function expandTakes(selectors) {
|
|
19200
19512
|
const out = [];
|
|
19201
19513
|
for (const sel of selectors) {
|
|
19202
|
-
const st = await
|
|
19514
|
+
const st = await fs24.stat(sel).catch(() => null);
|
|
19203
19515
|
if (st?.isDirectory()) {
|
|
19204
|
-
const names = await
|
|
19205
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
19516
|
+
const names = await fs24.readdir(sel);
|
|
19517
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path23.join(sel, n));
|
|
19206
19518
|
} else if (st?.isFile()) {
|
|
19207
19519
|
out.push(sel);
|
|
19208
19520
|
} else {
|
|
@@ -19237,7 +19549,7 @@ async function motionVerify(opts, log) {
|
|
|
19237
19549
|
let gates = DEFAULT_GATES;
|
|
19238
19550
|
if (opts.gatesPath) {
|
|
19239
19551
|
try {
|
|
19240
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
19552
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs24.readFile(opts.gatesPath, "utf8")));
|
|
19241
19553
|
} catch {
|
|
19242
19554
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
19243
19555
|
process.exitCode = 1;
|
|
@@ -19259,9 +19571,9 @@ async function motionVerify(opts, log) {
|
|
|
19259
19571
|
}
|
|
19260
19572
|
const reports = [];
|
|
19261
19573
|
for (const file of files) {
|
|
19262
|
-
const stem =
|
|
19574
|
+
const stem = path23.basename(file).replace(/\.npz$/, "");
|
|
19263
19575
|
try {
|
|
19264
|
-
reports.push(analyzeTake(stem, await
|
|
19576
|
+
reports.push(analyzeTake(stem, await fs24.readFile(file), gates));
|
|
19265
19577
|
} catch (err) {
|
|
19266
19578
|
reports.push({
|
|
19267
19579
|
take: stem,
|
|
@@ -19299,7 +19611,7 @@ async function motionCompile(opts, log) {
|
|
|
19299
19611
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
19300
19612
|
if (opts.configPath) {
|
|
19301
19613
|
try {
|
|
19302
|
-
const patch = JSON.parse(await
|
|
19614
|
+
const patch = JSON.parse(await fs24.readFile(opts.configPath, "utf8"));
|
|
19303
19615
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
19304
19616
|
} catch {
|
|
19305
19617
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -19317,16 +19629,16 @@ async function motionCompile(opts, log) {
|
|
|
19317
19629
|
}
|
|
19318
19630
|
const inputs = [];
|
|
19319
19631
|
for (const file of files) {
|
|
19320
|
-
const stem =
|
|
19632
|
+
const stem = path23.basename(file).replace(/\.npz$/, "");
|
|
19321
19633
|
try {
|
|
19322
|
-
inputs.push({ stem, take: loadTake(await
|
|
19634
|
+
inputs.push({ stem, take: loadTake(await fs24.readFile(file)) });
|
|
19323
19635
|
} catch (err) {
|
|
19324
19636
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
19325
19637
|
process.exitCode = 1;
|
|
19326
19638
|
return;
|
|
19327
19639
|
}
|
|
19328
19640
|
}
|
|
19329
|
-
const setName = opts.set ??
|
|
19641
|
+
const setName = opts.set ?? path23.basename(opts.out).replace(/\.json$/, "");
|
|
19330
19642
|
let result;
|
|
19331
19643
|
try {
|
|
19332
19644
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -19341,9 +19653,9 @@ async function motionCompile(opts, log) {
|
|
|
19341
19653
|
process.exitCode = 1;
|
|
19342
19654
|
return;
|
|
19343
19655
|
}
|
|
19344
|
-
await
|
|
19656
|
+
await fs24.mkdir(path23.dirname(path23.resolve(opts.out)), { recursive: true });
|
|
19345
19657
|
const json = JSON.stringify(result.data);
|
|
19346
|
-
await
|
|
19658
|
+
await fs24.writeFile(opts.out, json);
|
|
19347
19659
|
if (opts.json) {
|
|
19348
19660
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
19349
19661
|
return;
|
|
@@ -19361,9 +19673,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
19361
19673
|
var MOTION_PRESETS = {
|
|
19362
19674
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
19363
19675
|
};
|
|
19364
|
-
var MOTION_DEST =
|
|
19676
|
+
var MOTION_DEST = path23.join("src", "motion");
|
|
19365
19677
|
async function motionInstall(opts, log) {
|
|
19366
|
-
const srcDir =
|
|
19678
|
+
const srcDir = path23.join(getTemplatesDir(), "motion");
|
|
19367
19679
|
const root = opts.cwd ?? process.cwd();
|
|
19368
19680
|
const preset = opts.set;
|
|
19369
19681
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -19374,21 +19686,21 @@ async function motionInstall(opts, log) {
|
|
|
19374
19686
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
19375
19687
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
19376
19688
|
log.plain("");
|
|
19377
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
19689
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path23.sep)}`);
|
|
19378
19690
|
let copied = 0, skipped = 0;
|
|
19379
19691
|
try {
|
|
19380
19692
|
for (const rel of files) {
|
|
19381
|
-
const dest =
|
|
19382
|
-
const exists4 = await
|
|
19693
|
+
const dest = path23.join(root, MOTION_DEST, rel);
|
|
19694
|
+
const exists4 = await fs24.access(dest).then(() => true, () => false);
|
|
19383
19695
|
if (!opts.force && exists4) {
|
|
19384
19696
|
skipped++;
|
|
19385
|
-
log.dim(` skipped ${
|
|
19697
|
+
log.dim(` skipped ${path23.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
19386
19698
|
continue;
|
|
19387
19699
|
}
|
|
19388
|
-
await
|
|
19389
|
-
await
|
|
19700
|
+
await fs24.mkdir(path23.dirname(dest), { recursive: true });
|
|
19701
|
+
await fs24.copyFile(path23.join(srcDir, rel), dest);
|
|
19390
19702
|
copied++;
|
|
19391
|
-
log.dim(` ${
|
|
19703
|
+
log.dim(` ${path23.join(MOTION_DEST, rel)}`);
|
|
19392
19704
|
}
|
|
19393
19705
|
} catch (err) {
|
|
19394
19706
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -19429,7 +19741,7 @@ async function motionConstraints(opts, log) {
|
|
|
19429
19741
|
}
|
|
19430
19742
|
const doc = directionConstraint(dir, speed, duration);
|
|
19431
19743
|
const out = opts.out ?? "constraints.json";
|
|
19432
|
-
await
|
|
19744
|
+
await fs24.writeFile(out, JSON.stringify(doc));
|
|
19433
19745
|
if (opts.json) {
|
|
19434
19746
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
19435
19747
|
return;
|
|
@@ -19466,9 +19778,9 @@ async function runMotion(opts) {
|
|
|
19466
19778
|
}
|
|
19467
19779
|
|
|
19468
19780
|
// src/commands/asset-new.ts
|
|
19469
|
-
import
|
|
19781
|
+
import fs25 from "fs";
|
|
19470
19782
|
import fsp from "fs/promises";
|
|
19471
|
-
import
|
|
19783
|
+
import path24 from "path";
|
|
19472
19784
|
import { pathToFileURL } from "url";
|
|
19473
19785
|
var EXTRA_FILES = [
|
|
19474
19786
|
"genex-asset.example.json",
|
|
@@ -19561,7 +19873,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
19561
19873
|
}
|
|
19562
19874
|
async function runAssetNew(options) {
|
|
19563
19875
|
const log = createLogger();
|
|
19564
|
-
const cwd = options.dir ?
|
|
19876
|
+
const cwd = options.dir ? path24.resolve(options.dir) : process.cwd();
|
|
19565
19877
|
const slug = options.assetSlug;
|
|
19566
19878
|
if (!slug) {
|
|
19567
19879
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -19571,14 +19883,14 @@ async function runAssetNew(options) {
|
|
|
19571
19883
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
19572
19884
|
return 1;
|
|
19573
19885
|
}
|
|
19574
|
-
const templateDir =
|
|
19575
|
-
if (!
|
|
19886
|
+
const templateDir = path24.join(getTemplatesDir(), "asset-viewer");
|
|
19887
|
+
if (!fs25.existsSync(templateDir)) {
|
|
19576
19888
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
19577
19889
|
return 1;
|
|
19578
19890
|
}
|
|
19579
|
-
const manifestTools = await import(pathToFileURL(
|
|
19891
|
+
const manifestTools = await import(pathToFileURL(path24.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
19580
19892
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
19581
|
-
const lockPath =
|
|
19893
|
+
const lockPath = path24.join(templateDir, "shared-files.sha256.json");
|
|
19582
19894
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
19583
19895
|
const actual = hashSharedFiles(templateDir);
|
|
19584
19896
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -19591,8 +19903,8 @@ async function runAssetNew(options) {
|
|
|
19591
19903
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
19592
19904
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
19593
19905
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
19594
|
-
const outDir =
|
|
19595
|
-
if (
|
|
19906
|
+
const outDir = path24.resolve(cwd, options.out ?? slug);
|
|
19907
|
+
if (fs25.existsSync(outDir) && fs25.readdirSync(outDir).length > 0 && !options.force) {
|
|
19596
19908
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
19597
19909
|
return 1;
|
|
19598
19910
|
}
|
|
@@ -19620,24 +19932,24 @@ async function runAssetNew(options) {
|
|
|
19620
19932
|
};
|
|
19621
19933
|
await fsp.mkdir(outDir, { recursive: true });
|
|
19622
19934
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
19623
|
-
const to =
|
|
19624
|
-
await fsp.mkdir(
|
|
19625
|
-
await fsp.copyFile(
|
|
19935
|
+
const to = path24.join(outDir, rel);
|
|
19936
|
+
await fsp.mkdir(path24.dirname(to), { recursive: true });
|
|
19937
|
+
await fsp.copyFile(path24.join(templateDir, rel), to);
|
|
19626
19938
|
}
|
|
19627
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
19939
|
+
const pkg = fillTemplate(await fsp.readFile(path24.join(templateDir, "package.json"), "utf8"), {
|
|
19628
19940
|
slug,
|
|
19629
19941
|
name,
|
|
19630
19942
|
version
|
|
19631
19943
|
});
|
|
19632
|
-
await fsp.writeFile(
|
|
19633
|
-
await fsp.writeFile(
|
|
19634
|
-
await fsp.writeFile(
|
|
19944
|
+
await fsp.writeFile(path24.join(outDir, "package.json"), pkg, "utf8");
|
|
19945
|
+
await fsp.writeFile(path24.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
19946
|
+
await fsp.writeFile(path24.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
19635
19947
|
await fsp.writeFile(
|
|
19636
|
-
|
|
19948
|
+
path24.join(outDir, "DESIGN.md"),
|
|
19637
19949
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
19638
19950
|
"utf8"
|
|
19639
19951
|
);
|
|
19640
|
-
const placeholder = await fsp.readFile(
|
|
19952
|
+
const placeholder = await fsp.readFile(path24.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
19641
19953
|
const seeded = seedAssetSource(placeholder, {
|
|
19642
19954
|
slug,
|
|
19643
19955
|
name,
|
|
@@ -19648,8 +19960,8 @@ async function runAssetNew(options) {
|
|
|
19648
19960
|
pascalCase
|
|
19649
19961
|
});
|
|
19650
19962
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
19651
|
-
await fsp.mkdir(
|
|
19652
|
-
await fsp.writeFile(
|
|
19963
|
+
await fsp.mkdir(path24.join(outDir, "src", "asset"), { recursive: true });
|
|
19964
|
+
await fsp.writeFile(path24.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
19653
19965
|
const copied = hashSharedFiles(outDir);
|
|
19654
19966
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
19655
19967
|
if (mismatched.length) {
|
|
@@ -19657,7 +19969,7 @@ async function runAssetNew(options) {
|
|
|
19657
19969
|
return 1;
|
|
19658
19970
|
}
|
|
19659
19971
|
await fsp.writeFile(
|
|
19660
|
-
|
|
19972
|
+
path24.join(outDir, PARITY_FILENAME),
|
|
19661
19973
|
JSON.stringify(
|
|
19662
19974
|
{
|
|
19663
19975
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -19711,9 +20023,15 @@ ${c.bold("Usage")}
|
|
|
19711
20023
|
resumes the same code, so an interrupted
|
|
19712
20024
|
'genex init' costs nothing. --force switches
|
|
19713
20025
|
accounts.
|
|
19714
|
-
genex link <slug> [options]
|
|
19715
|
-
|
|
19716
|
-
|
|
20026
|
+
genex link <slug> [options] Point THIS folder at an existing game of yours;
|
|
20027
|
+
preview/publish then update the same live game.
|
|
20028
|
+
In an EMPTY folder it downloads the game too \u2014
|
|
20029
|
+
that's how you pick a game up on a new machine.
|
|
20030
|
+
Never creates a project.
|
|
20031
|
+
genex pull [options] Update THIS folder with the game's latest draft,
|
|
20032
|
+
for when it was built from somewhere else. Refuses
|
|
20033
|
+
if this folder has changes you never deployed;
|
|
20034
|
+
--force replaces them.
|
|
19717
20035
|
genex rename <name> [options] Rename THIS game \u2014 its play address, its source repo,
|
|
19718
20036
|
and its gallery name. Keeps the game (plays, likes,
|
|
19719
20037
|
comments); the OLD address stops working, so a
|
|
@@ -20660,6 +20978,9 @@ async function main() {
|
|
|
20660
20978
|
case "link":
|
|
20661
20979
|
await runLink(parsed.options);
|
|
20662
20980
|
break;
|
|
20981
|
+
case "pull":
|
|
20982
|
+
await runPull(parsed.options);
|
|
20983
|
+
break;
|
|
20663
20984
|
case "rename":
|
|
20664
20985
|
await runRename(parsed.options);
|
|
20665
20986
|
break;
|