@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 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 = path11.join(cwd, ".env");
2097
+ const file = path12.join(cwd, ".env");
1903
2098
  let content;
1904
2099
  try {
1905
- content = await fs11.readFile(file, "utf8");
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 fs11.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
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 fs11.writeFile(file, content.replace(re, `$1${slug}`));
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 fs12 from "fs/promises";
1973
- import path12 from "path";
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 = path12.join(cwd, ".env");
2356
+ const file = path14.join(cwd, ".env");
2062
2357
  let content;
2063
2358
  try {
2064
- content = await fs12.readFile(file, "utf8");
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 fs12.writeFile(file, content.replace(re, `$1${to}`));
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 = path12.join(cwd, "src", "genex.config.ts");
2369
+ const file = path14.join(cwd, "src", "genex.config.ts");
2075
2370
  let content;
2076
2371
  try {
2077
- content = await fs12.readFile(file, "utf8");
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 fs12.writeFile(file, content.replace(quoted, `"${to}"`));
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 { spawn as spawn3 } from "child_process";
2510
+ import "child_process";
2216
2511
  import crypto3 from "crypto";
2217
- import fs15 from "fs/promises";
2218
- import os6 from "os";
2219
- import path14 from "path";
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 fs14 from "fs/promises";
2484
- import path13 from "path";
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 fs13 from "fs/promises";
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 fs13.readFile(input);
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 fs13.writeFile(file, PNG.sync.write(png));
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 fs14.mkdir(outDir, { recursive: true });
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 = path13.join(outDir, `${name}.png`);
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 fs14.writeFile(
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 = path13.join(outDir, "extract-debug.json");
3062
- await fs14.writeFile(
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 fs14.mkdir(outDir, { recursive: true });
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 = path13.join(outDir, `${pair.name}-frame.png`);
3578
- const maskPath = path13.join(outDir, `${pair.name}-mask.png`);
3579
- const annotatedPath = path13.join(outDir, `${pair.name}-annotated-source.png`);
3580
- const overlayPath = path13.join(outDir, `${pair.name}-overlay.png`);
3581
- const metaPath = path13.join(outDir, `${pair.name}.annotated-progress.json`);
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 fs14.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
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 = path13.join(outDir, "annotated-progress.json");
3645
- await fs14.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
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 fs14.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
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 fs14.writeFile(
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 = path13.basename(outPath);
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 fs14.readdir(dir, { withFileTypes: true });
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 = path13.join(dir, entry.name);
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 fs14.readFile(path13.join(cwd, "index.html"), "utf8");
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 = path13.resolve(cwd, assetDir);
4399
+ const absAssets = path15.resolve(cwd, assetDir);
4105
4400
  try {
4106
- if (!(await fs14.stat(absAssets)).isDirectory()) {
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(path13.resolve(cwd, srcDir))).filter(
4113
- (p) => AUDIT_SRC_EXTS.has(path13.extname(p).toLowerCase())
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 fs14.readdir(cwd)) {
4117
- const ext = path13.extname(name).toLowerCase();
4118
- if (ext === ".html" || ext === ".css") srcFiles.push(path13.join(cwd, name));
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: path13.relative(cwd, p), text: await fs14.readFile(p, "utf8") });
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 = path13.basename(p);
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 fs14.readFile(p, "utf8"));
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 fs14.readFile(p, "utf8"));
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 = path13.basename(p);
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 = path13.basename(p);
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 fs14.readFile(framePath));
4197
- mask = PNG2.sync.read(await fs14.readFile(maskPath));
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 fs14.readFile(p));
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(path13.basename(p));
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 = path13.basename(p);
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: `${path13.relative(cwd, p)} is on disk but never referenced in ${srcDir}/index.html/CSS \u2014 wire it, or record the one-line reason it was cut. (Computed-string references are invisible here \u2014 check.)`
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 fs15.readFile(path14.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
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 fs15.access(path14.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
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 fs15.readdir(path14.join(cwd, "src"), { recursive: true });
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(path14.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
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 fs15.readFile(path14.join(cwd, "src", rel), "utf8").catch(() => "");
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 = path14.join(cwd, "dist");
4725
+ const distDir = path16.join(cwd, "dist");
4441
4726
  const siteDir = await isDir2(distDir) ? distDir : cwd;
4442
- const rel = path14.relative(cwd, siteDir) || ".";
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
- if (!await pushSource(cwd, ctx, log, opts.channel === "staging" ? "preview" : "main")) return false;
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 fs15.readFile(path14.join(cwd, "package.json"), "utf8"));
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 fs15.readdir(dir, { withFileTypes: true })) {
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(path14.join(dir, e.name), relPath);
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 fs15.readFile(path14.join(dir, e.name)) });
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 fs15.mkdtemp(path14.join(os6.tmpdir(), "genex-source-"));
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 fs15.writeFile(
4777
- path14.join(gitDir, "info", "exclude"),
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: path14.join(gitDir, "index-source") };
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 fs15.rm(gitDir, { recursive: true, force: true }).catch(() => {
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: { Authorization: `Bearer ${ctx.token}` }
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 fs15.stat(p)).isDirectory();
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 fs17 from "fs/promises";
5316
- import path16 from "path";
5625
+ import fs19 from "fs/promises";
5626
+ import path18 from "path";
5317
5627
 
5318
5628
  // src/lib/generation-ledger.ts
5319
- import fs16 from "fs/promises";
5320
- import path15 from "path";
5321
- var ledgerPath = (cwd) => path15.join(cwd, ".genex", "generations.ndjson");
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 fs16.access(path15.join(cwd, ".genex"));
5325
- await fs16.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
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 fs16.readFile(ledgerPath(cwd), "utf8");
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 fs17.readFile(path16.join(cwd, "package.json"), "utf8");
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 fs17.readFile(path16.join(cwd, "package.json"), "utf8");
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 fs17.readFile(path16.join(cwd, "package.json"), "utf8"));
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 fs17.readFile(path16.join(cwd, "package.json"), "utf8");
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 = path16.join(cwd, "src");
5742
+ const srcDir = path18.join(cwd, "src");
5433
5743
  let entries;
5434
5744
  try {
5435
- entries = await fs17.readdir(srcDir, { recursive: true });
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 fs17.readFile(path16.join(srcDir, rel), "utf8");
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 = path16.join(cwd, "src");
5762
+ const srcDir = path18.join(cwd, "src");
5453
5763
  let entries;
5454
5764
  try {
5455
- entries = await fs17.readdir(srcDir, { recursive: true });
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 fs17.readFile(path16.join(srcDir, rel), "utf8");
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 = path16.join(cwd, "src");
5822
+ const srcDir = path18.join(cwd, "src");
5513
5823
  let entries;
5514
5824
  try {
5515
- entries = await fs17.readdir(srcDir, { recursive: true });
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 fs17.readFile(path16.join(srcDir, nativeRel), "utf8").catch(() => "");
5832
+ const raw = await fs19.readFile(path18.join(srcDir, nativeRel), "utf8").catch(() => "");
5523
5833
  if (!raw) continue;
5524
- const rel = nativeRel.split(path16.sep).join("/");
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 fs17.readFile(file, "utf8");
5892
+ haystack += await fs19.readFile(file, "utf8");
5583
5893
  } catch {
5584
5894
  }
5585
5895
  };
5586
5896
  try {
5587
- for (const entry of await fs17.readdir(cwd, { withFileTypes: true })) {
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(path16.join(cwd, entry.name));
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 fs17.readdir(path16.join(cwd, sub), { recursive: true });
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(path16.join(cwd, sub, rel));
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 fs17.readFile(path16.join(cwd, ".git", "config"), "utf8").catch(() => "");
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 fs17.readFile(path16.join(cwd, "README.md"), "utf8").catch(() => "");
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 fs18 from "fs/promises";
5991
- import path17 from "path";
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 spawn4 } from "child_process";
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 = spawn4(command, args, { stdio: "ignore", detached: true });
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 fs18.readFile(filePath);
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[path17.extname(filePath).toLowerCase()] ?? "image/png";
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 fs18.readFile(opts.inpaintUrl);
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 fs18.mkdir(outDir, { recursive: true });
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 = path17.join(outDir, `glass-${i + 1}.png`);
6524
- await fs18.writeFile(outPath, PNG4.sync.write(r.png));
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 fs20 from "fs/promises";
7122
- import path19 from "path";
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 fs19 from "fs/promises";
16253
- import path18 from "path";
16254
- var ANIMS_DEST = path18.join("public", "assets", "anims");
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 = path18.join(root, "src", "controllers", "character");
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 = path18.join(root, ANIMS_DEST);
16280
- const gameManifestPath = path18.join(destDir, "manifest.json");
16591
+ const destDir = path20.join(root, ANIMS_DEST);
16592
+ const gameManifestPath = path20.join(destDir, "manifest.json");
16281
16593
  if (opts.reset) {
16282
- await fs19.rm(destDir, { recursive: true, force: true });
16283
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path18.sep)} (--reset)`);
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 = path18.join(
16633
+ const cacheDir = path20.join(
16322
16634
  opts.cacheDir ?? getAnimsCacheDir(),
16323
16635
  `${manifest.library}-v${manifest.version}`
16324
16636
  );
16325
- await fs19.mkdir(cacheDir, { recursive: true });
16326
- await fs19.mkdir(destDir, { recursive: true });
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 = path18.join(destDir, entry.file);
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 = path18.join(cacheDir, entry.file);
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 fs19.writeFile(cached, buf);
16656
+ await fs21.writeFile(cached, buf);
16345
16657
  }
16346
- await fs19.copyFile(cached, dest);
16658
+ await fs21.copyFile(cached, dest);
16347
16659
  installedCount++;
16348
16660
  addedBytes += entry.bytes;
16349
- log.dim(` ${path18.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
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 fs19.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
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 + path18.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
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 = path18.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
16404
- const manifest = JSON.parse(await fs19.readFile(snapshotPath, "utf8"));
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 fs19.readFile(file, "utf8"));
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 fs19.stat(file)).size === bytes;
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 fs19.access(p);
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 = path19.join("src", "controllers");
16731
- var ASSETS_DEST = path19.join("public", "assets");
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 = path19.join(getTemplatesDir(), "controllers");
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 + path19.sep)}`);
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: path19.join(CODE_DEST, 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: path19.join(ASSETS_DEST, path19.basename(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 = path19.join(root, file.rel);
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 fs20.mkdir(path19.dirname(dest), { recursive: true });
16772
- await fs20.copyFile(path19.join(srcDir, file.from), dest);
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(path19.join(root, ASSETS_DEST, "meshy-character.json"))) {
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 = path19.join(args.root, ASSETS_DEST, "meshy-character.json");
16858
- await fs20.mkdir(path19.dirname(destination), { recursive: true });
16859
- await fs20.writeFile(
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 = path19.join(root, ASSETS_DEST, "avatar.vrm");
16998
- await fs20.mkdir(path19.dirname(dest), { recursive: true });
16999
- await fs20.copyFile(path19.join(srcDir, "assets", "default-avatar.vrm"), dest);
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 fs20.access(p);
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 fs21 from "fs/promises";
17013
- import path20 from "path";
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 = path20.join("public", "assets", "meshy-character.json");
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 fs21.readFile(file, "utf8");
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(path23, log) {
17892
+ async function readVideo(path25, log) {
17581
17893
  let bytes;
17582
17894
  try {
17583
- bytes = await readFile(path23);
17895
+ bytes = await readFile(path25);
17584
17896
  } catch {
17585
- log.error(`Can't read ${path23}.`);
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(path23)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
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, path23, bytes, log) {
17595
- const contentType = /\.mov$/i.test(path23) ? "video/quicktime" : "video/mp4";
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(path23)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
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 fs22 from "fs/promises";
17927
- import path21 from "path";
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 fs22.readFile(opts.constraintsPath, "utf8");
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 fs22.stat(sel).catch(() => null);
19514
+ const st = await fs24.stat(sel).catch(() => null);
19203
19515
  if (st?.isDirectory()) {
19204
- const names = await fs22.readdir(sel);
19205
- for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path21.join(sel, n));
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 fs22.readFile(opts.gatesPath, "utf8")));
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 = path21.basename(file).replace(/\.npz$/, "");
19574
+ const stem = path23.basename(file).replace(/\.npz$/, "");
19263
19575
  try {
19264
- reports.push(analyzeTake(stem, await fs22.readFile(file), gates));
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 fs22.readFile(opts.configPath, "utf8"));
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 = path21.basename(file).replace(/\.npz$/, "");
19632
+ const stem = path23.basename(file).replace(/\.npz$/, "");
19321
19633
  try {
19322
- inputs.push({ stem, take: loadTake(await fs22.readFile(file)) });
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 ?? path21.basename(opts.out).replace(/\.json$/, "");
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 fs22.mkdir(path21.dirname(path21.resolve(opts.out)), { recursive: true });
19656
+ await fs24.mkdir(path23.dirname(path23.resolve(opts.out)), { recursive: true });
19345
19657
  const json = JSON.stringify(result.data);
19346
- await fs22.writeFile(opts.out, json);
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 = path21.join("src", "motion");
19676
+ var MOTION_DEST = path23.join("src", "motion");
19365
19677
  async function motionInstall(opts, log) {
19366
- const srcDir = path21.join(getTemplatesDir(), "motion");
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 + path21.sep)}`);
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 = path21.join(root, MOTION_DEST, rel);
19382
- const exists4 = await fs22.access(dest).then(() => true, () => false);
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 ${path21.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
19697
+ log.dim(` skipped ${path23.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
19386
19698
  continue;
19387
19699
  }
19388
- await fs22.mkdir(path21.dirname(dest), { recursive: true });
19389
- await fs22.copyFile(path21.join(srcDir, rel), dest);
19700
+ await fs24.mkdir(path23.dirname(dest), { recursive: true });
19701
+ await fs24.copyFile(path23.join(srcDir, rel), dest);
19390
19702
  copied++;
19391
- log.dim(` ${path21.join(MOTION_DEST, rel)}`);
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 fs22.writeFile(out, JSON.stringify(doc));
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 fs23 from "fs";
19781
+ import fs25 from "fs";
19470
19782
  import fsp from "fs/promises";
19471
- import path22 from "path";
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 ? path22.resolve(options.dir) : process.cwd();
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 = path22.join(getTemplatesDir(), "asset-viewer");
19575
- if (!fs23.existsSync(templateDir)) {
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(path22.join(templateDir, "tools", "emit-manifest.mjs")).href);
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 = path22.join(templateDir, "shared-files.sha256.json");
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 = path22.resolve(cwd, options.out ?? slug);
19595
- if (fs23.existsSync(outDir) && fs23.readdirSync(outDir).length > 0 && !options.force) {
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 = path22.join(outDir, rel);
19624
- await fsp.mkdir(path22.dirname(to), { recursive: true });
19625
- await fsp.copyFile(path22.join(templateDir, rel), to);
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(path22.join(templateDir, "package.json"), "utf8"), {
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(path22.join(outDir, "package.json"), pkg, "utf8");
19633
- await fsp.writeFile(path22.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
19634
- await fsp.writeFile(path22.join(outDir, ".gitignore"), GITIGNORE, "utf8");
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
- path22.join(outDir, "DESIGN.md"),
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(path22.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
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(path22.join(outDir, "src", "asset"), { recursive: true });
19652
- await fsp.writeFile(path22.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
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
- path22.join(outDir, PARITY_FILENAME),
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] Re-link THIS folder to an existing game of yours
19715
- (lost folder / new machine); preview/publish then
19716
- update the same live game. Never creates a project.
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;