@genex-ai/cli-demo 1.6.4-dev.433 → 1.6.5-dev.434

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