@genex-ai/cli-demo 1.38.0-dev.1096 → 1.38.1-dev.1097
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 +935 -551
- package/package.json +1 -1
- package/templates/skills/genex-authoritative-multiplayer/SKILL.md +208 -0
- package/templates/skills/genex-authoritative-multiplayer/references/module-api.md +172 -0
- package/templates/skills/genex-authoritative-multiplayer/references/physics.md +150 -0
- package/templates/skills/genex-authoritative-multiplayer/references/presets.md +394 -0
- package/templates/skills/genex-authoritative-multiplayer/references/projectiles.md +195 -0
- package/templates/skills/genex-authoritative-multiplayer/references/room-template.md +1189 -0
- package/templates/skills/genex-authoritative-multiplayer/references/settings.md +122 -0
- package/templates/skills/genex-authoritative-multiplayer/references/testing.md +118 -0
- package/templates/skills/genex-game-director/SKILL.md +1 -0
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +4 -0
package/dist/index.js
CHANGED
|
@@ -810,9 +810,9 @@ async function runAuth(opts) {
|
|
|
810
810
|
}
|
|
811
811
|
|
|
812
812
|
// src/commands/remix.ts
|
|
813
|
-
import
|
|
814
|
-
import
|
|
815
|
-
import
|
|
813
|
+
import fs20 from "fs/promises";
|
|
814
|
+
import path20 from "path";
|
|
815
|
+
import crypto6 from "crypto";
|
|
816
816
|
import { spawn as spawn4 } from "child_process";
|
|
817
817
|
|
|
818
818
|
// src/lib/game-config.ts
|
|
@@ -2852,6 +2852,98 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
2852
2852
|
}
|
|
2853
2853
|
return null;
|
|
2854
2854
|
}
|
|
2855
|
+
var AUTHORITY_TICK_RATES = [20, 30, 60, 120];
|
|
2856
|
+
var AUTHORITY_PHYSICS_ONLY_TICK_HZ = 120;
|
|
2857
|
+
var authorityTickHzOf = (value) => AUTHORITY_TICK_RATES.find((hz) => hz === value);
|
|
2858
|
+
var AUTHORITY_TICK_RATES_TEXT = `${AUTHORITY_TICK_RATES.slice(0, -1).join(", ")} or ${AUTHORITY_TICK_RATES.at(-1)}`;
|
|
2859
|
+
var AUTHORITY_MAX_PLAYERS = 64;
|
|
2860
|
+
var MAX_AUTHORITY_MANIFEST_CHARS = 1024;
|
|
2861
|
+
var AUTHORITY_SERVER_DIR = "server";
|
|
2862
|
+
var ROOM_MODULE_FILE = /\.(js|ts)$/;
|
|
2863
|
+
var PRESET_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
2864
|
+
var TEMPLATE_GENEX_SETTINGS = "genex.json";
|
|
2865
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2866
|
+
function modulePathProblem(module) {
|
|
2867
|
+
if (typeof module !== "string" || module === "") return `needs "module", the room module's path (e.g. "server/room.ts")`;
|
|
2868
|
+
if (!ROOM_MODULE_FILE.test(module)) return `module "${module.slice(0, 80)}" is not a .js or .ts file`;
|
|
2869
|
+
const unsafe = module.includes("\\") || /[\u0000-\u001f]/.test(module) || module.startsWith("/") || /^[A-Za-z]:/.test(module);
|
|
2870
|
+
const segments = module.split("/");
|
|
2871
|
+
if (unsafe || segments.some((s) => s === "" || s === "." || s === "..")) {
|
|
2872
|
+
return `module "${module.slice(0, 80)}" must be a plain path inside the game folder, like "server/room.ts"`;
|
|
2873
|
+
}
|
|
2874
|
+
if (segments[0] !== AUTHORITY_SERVER_DIR || segments.length < 2) {
|
|
2875
|
+
return `module "${module.slice(0, 80)}" must be in the game's ${AUTHORITY_SERVER_DIR}/ folder, like "server/room.ts"`;
|
|
2876
|
+
}
|
|
2877
|
+
return null;
|
|
2878
|
+
}
|
|
2879
|
+
function knobProblem(block) {
|
|
2880
|
+
const { tickHz, maxPlayers, preset } = block;
|
|
2881
|
+
if (tickHz !== void 0 && authorityTickHzOf(tickHz) === void 0) {
|
|
2882
|
+
return `tickHz must be ${AUTHORITY_TICK_RATES_TEXT}`;
|
|
2883
|
+
}
|
|
2884
|
+
const players = typeof maxPlayers === "number" && Number.isSafeInteger(maxPlayers) && maxPlayers >= 1 && maxPlayers <= AUTHORITY_MAX_PLAYERS;
|
|
2885
|
+
if (maxPlayers !== void 0 && !players) return `maxPlayers must be a whole number from 1 to ${AUTHORITY_MAX_PLAYERS}`;
|
|
2886
|
+
if (preset !== void 0 && !(typeof preset === "string" && PRESET_NAME.test(preset))) {
|
|
2887
|
+
return 'preset must be a preset name, like "topdown"';
|
|
2888
|
+
}
|
|
2889
|
+
return null;
|
|
2890
|
+
}
|
|
2891
|
+
function parseAuthorityManifest(value) {
|
|
2892
|
+
if (!isRecord(value)) return { problem: 'must be an object like { "module": "server/room.ts" }' };
|
|
2893
|
+
if (JSON.stringify(value).length > MAX_AUTHORITY_MANIFEST_CHARS) {
|
|
2894
|
+
return { problem: `is over ${MAX_AUTHORITY_MANIFEST_CHARS} characters` };
|
|
2895
|
+
}
|
|
2896
|
+
const problem = modulePathProblem(value.module) ?? knobProblem(value);
|
|
2897
|
+
if (problem) return { problem };
|
|
2898
|
+
const manifest = { module: value.module };
|
|
2899
|
+
const tickHz = authorityTickHzOf(value.tickHz);
|
|
2900
|
+
if (tickHz !== void 0) manifest.tickHz = tickHz;
|
|
2901
|
+
if (value.maxPlayers !== void 0) manifest.maxPlayers = value.maxPlayers;
|
|
2902
|
+
if (value.preset !== void 0) manifest.preset = value.preset;
|
|
2903
|
+
return { manifest };
|
|
2904
|
+
}
|
|
2905
|
+
function within(child, parent) {
|
|
2906
|
+
const rel = path14.relative(parent, child);
|
|
2907
|
+
return rel === "" || !rel.startsWith("..") && !path14.isAbsolute(rel);
|
|
2908
|
+
}
|
|
2909
|
+
async function moduleFileProblem(cwd, module) {
|
|
2910
|
+
try {
|
|
2911
|
+
const root = await fs13.realpath(cwd);
|
|
2912
|
+
const real = await fs13.realpath(path14.join(root, module));
|
|
2913
|
+
if (!within(real, root)) return `module "${module}" leads outside the game folder`;
|
|
2914
|
+
if (!(await fs13.stat(real)).isFile()) return `module "${module}" is not a file`;
|
|
2915
|
+
return null;
|
|
2916
|
+
} catch {
|
|
2917
|
+
return `module "${module}" does not exist`;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
async function readJsonFile(cwd, rel) {
|
|
2921
|
+
try {
|
|
2922
|
+
return JSON.parse(await fs13.readFile(path14.join(cwd, rel), "utf8"));
|
|
2923
|
+
} catch {
|
|
2924
|
+
return void 0;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
async function detectAuthority(cwd = process.cwd()) {
|
|
2928
|
+
const pkg = await readJsonFile(cwd, "package.json");
|
|
2929
|
+
const fromPkg = isRecord(pkg) && isRecord(pkg.genex) && Object.hasOwn(pkg.genex, "authority");
|
|
2930
|
+
let block;
|
|
2931
|
+
let source;
|
|
2932
|
+
if (fromPkg) {
|
|
2933
|
+
block = pkg.genex.authority;
|
|
2934
|
+
source = "package.json";
|
|
2935
|
+
} else {
|
|
2936
|
+
const settings = await readJsonFile(cwd, TEMPLATE_GENEX_SETTINGS);
|
|
2937
|
+
if (!isRecord(settings) || !Object.hasOwn(settings, "authority")) return { state: "none" };
|
|
2938
|
+
block = settings.authority;
|
|
2939
|
+
source = "genex.json";
|
|
2940
|
+
}
|
|
2941
|
+
const parsed = parseAuthorityManifest(block);
|
|
2942
|
+
if ("problem" in parsed) return { state: "invalid", source, problem: parsed.problem };
|
|
2943
|
+
const onDisk = await moduleFileProblem(cwd, parsed.manifest.module);
|
|
2944
|
+
if (onDisk) return { state: "invalid", source, problem: onDisk };
|
|
2945
|
+
return { state: "declared", source, manifest: parsed.manifest };
|
|
2946
|
+
}
|
|
2855
2947
|
var TOUCH_KIT_MARKERS = /controllers\/touch\/|controllers\/character\/touch-joystick|TouchJoystick|VirtualButton|DragZone|RotateOverlay/;
|
|
2856
2948
|
async function detectMobileControls(cwd = process.cwd()) {
|
|
2857
2949
|
try {
|
|
@@ -3459,7 +3551,8 @@ async function detectFeatures(log, cwd = process.cwd()) {
|
|
|
3459
3551
|
generations: await detectGenerationAudit(cwd),
|
|
3460
3552
|
embedBoot: await detectEmbedBoot(cwd),
|
|
3461
3553
|
capabilities: await detectCapabilities(cwd),
|
|
3462
|
-
shellApis: await detectShellApis(cwd)
|
|
3554
|
+
shellApis: await detectShellApis(cwd),
|
|
3555
|
+
authority: await detectAuthority(cwd)
|
|
3463
3556
|
};
|
|
3464
3557
|
}
|
|
3465
3558
|
function advisoryNudges(log, d) {
|
|
@@ -3478,6 +3571,16 @@ function advisoryNudges(log, d) {
|
|
|
3478
3571
|
"package.json declares genex.matchmaking but @genex-ai/multiplayer is not installed \u2014 matchmaking cannot work. Install the SDK and use matchmake() (see genex-threejs-multiplayer)."
|
|
3479
3572
|
);
|
|
3480
3573
|
}
|
|
3574
|
+
if (d.authority.state === "invalid") {
|
|
3575
|
+
log.warn(
|
|
3576
|
+
`${d.authority.source} declares genex.authority but ${d.authority.problem} \u2014 the deploy will refuse it until the room module is named correctly (see genex-authoritative-multiplayer).`
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
if (d.authority.state === "declared" && !d.multiplayer) {
|
|
3580
|
+
log.warn(
|
|
3581
|
+
"genex.authority names a room module but @genex-ai/multiplayer is not installed \u2014 the game cannot join its server. Install the SDK and connect with connectAuthority (see genex-authoritative-multiplayer)."
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3481
3584
|
embedBootNudges(log, d);
|
|
3482
3585
|
shellNudges(log, d);
|
|
3483
3586
|
surfaceNudges(log, d.surfaces);
|
|
@@ -3685,6 +3788,223 @@ async function borrowEvidence(meta, cwd) {
|
|
|
3685
3788
|
return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
|
|
3686
3789
|
}
|
|
3687
3790
|
|
|
3791
|
+
// src/lib/authority-publish.ts
|
|
3792
|
+
import crypto2 from "crypto";
|
|
3793
|
+
import fs14 from "fs/promises";
|
|
3794
|
+
import { createRequire } from "module";
|
|
3795
|
+
import path15 from "path";
|
|
3796
|
+
import { pathToFileURL } from "url";
|
|
3797
|
+
var AUTHORITY_DEV_PACKAGE = "@genex-ai/authority-dev";
|
|
3798
|
+
var MODULE_CONTRACT = 1;
|
|
3799
|
+
var MODULE_MAX_BYTES = 1048576;
|
|
3800
|
+
var REPORT_MAX_BYTES = 64 * 1024;
|
|
3801
|
+
var REPORT_COPY = path15.join(".genex", "authority", "report.json");
|
|
3802
|
+
var MAX_FAILURES_SHOWN = 6;
|
|
3803
|
+
var AuthorityStop = {
|
|
3804
|
+
/** The manifest's `genex.authority` is unusable (detect-features said why). */
|
|
3805
|
+
ManifestInvalid: "manifest_invalid",
|
|
3806
|
+
/** The game has no @genex-ai/authority-dev to bundle and validate with. */
|
|
3807
|
+
ToolchainMissing: "toolchain_missing",
|
|
3808
|
+
/** It has one, but it does not export the library calls this CLI uses. */
|
|
3809
|
+
ToolchainUnusable: "toolchain_unusable",
|
|
3810
|
+
/** The module did not bundle (the bundler's reason is printed beside it). */
|
|
3811
|
+
BundleFailed: "bundle_failed",
|
|
3812
|
+
/** The module failed validation (each typed reason is printed). */
|
|
3813
|
+
ValidateFailed: "validate_failed",
|
|
3814
|
+
/** The report passed a different module than the one bundled for upload. */
|
|
3815
|
+
ShaDisagrees: "sha_disagrees",
|
|
3816
|
+
/** The module or its report is larger than the API stores. */
|
|
3817
|
+
TooLarge: "too_large",
|
|
3818
|
+
/** The resolved tick rate, player count or preset is one the API refuses. */
|
|
3819
|
+
ConfigOutOfRange: "config_out_of_range",
|
|
3820
|
+
/** The owner is not on a plan with Genex servers (403 authority_requires_pro). */
|
|
3821
|
+
RequiresPro: "requires_pro",
|
|
3822
|
+
/** The presign or a PUT failed. */
|
|
3823
|
+
UploadFailed: "upload_failed"
|
|
3824
|
+
};
|
|
3825
|
+
var sha256 = (bytes) => crypto2.createHash("sha256").update(bytes).digest("hex");
|
|
3826
|
+
var localTestCommand = (module) => `npx genex-authority-dev start --module ${module} --bots 3`;
|
|
3827
|
+
async function loadAuthorityToolchain(gameDir) {
|
|
3828
|
+
let manifestPath;
|
|
3829
|
+
try {
|
|
3830
|
+
manifestPath = createRequire(path15.join(gameDir, "package.json")).resolve(`${AUTHORITY_DEV_PACKAGE}/package.json`);
|
|
3831
|
+
} catch {
|
|
3832
|
+
return { ok: false, stop: AuthorityStop.ToolchainMissing };
|
|
3833
|
+
}
|
|
3834
|
+
try {
|
|
3835
|
+
const pkg = JSON.parse(await fs14.readFile(manifestPath, "utf8"));
|
|
3836
|
+
const root = pkg.exports?.["."];
|
|
3837
|
+
const entry = typeof root === "string" ? root : root?.import;
|
|
3838
|
+
if (typeof entry !== "string") return { ok: false, stop: AuthorityStop.ToolchainUnusable };
|
|
3839
|
+
const lib = await import(pathToFileURL(path15.resolve(path15.dirname(manifestPath), entry)).href);
|
|
3840
|
+
if (typeof lib.bundleModule !== "function" || typeof lib.validateModule !== "function") {
|
|
3841
|
+
return { ok: false, stop: AuthorityStop.ToolchainUnusable };
|
|
3842
|
+
}
|
|
3843
|
+
return { ok: true, toolchain: lib, version: typeof pkg.version === "string" ? pkg.version : "unknown" };
|
|
3844
|
+
} catch {
|
|
3845
|
+
return { ok: false, stop: AuthorityStop.ToolchainUnusable };
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
function stopped(log, stop2, sentence, ...details) {
|
|
3849
|
+
log.error(`Room module refused (${stop2}): ${sentence} Nothing was deployed.`);
|
|
3850
|
+
for (const line of details) log.dim(` ${line}`);
|
|
3851
|
+
return null;
|
|
3852
|
+
}
|
|
3853
|
+
var failureLine = (f) => `${f.code ? `${f.reason}:${f.code}` : f.reason} \u2014 ${f.detail}${f.tick === void 0 ? "" : ` (tick ${f.tick})`}`;
|
|
3854
|
+
async function keepReport(gameDir, bytes) {
|
|
3855
|
+
const file = path15.join(gameDir, REPORT_COPY);
|
|
3856
|
+
await fs14.mkdir(path15.dirname(file), { recursive: true }).catch(() => {
|
|
3857
|
+
});
|
|
3858
|
+
await fs14.writeFile(file, bytes).catch(() => {
|
|
3859
|
+
});
|
|
3860
|
+
return REPORT_COPY.split(path15.sep).join("/");
|
|
3861
|
+
}
|
|
3862
|
+
function goLiveBlock(manifest, report, code, reportBytes) {
|
|
3863
|
+
const preset = manifest.preset ?? report.config?.preset?.name;
|
|
3864
|
+
return {
|
|
3865
|
+
contract: MODULE_CONTRACT,
|
|
3866
|
+
moduleSha256: sha256(code),
|
|
3867
|
+
bytes: code.length,
|
|
3868
|
+
tickHz: manifest.tickHz ?? report.config?.tickHz ?? 0,
|
|
3869
|
+
maxPlayers: manifest.maxPlayers ?? report.config?.maxPlayers ?? 0,
|
|
3870
|
+
...preset ? { preset } : {},
|
|
3871
|
+
reportSha256: sha256(reportBytes)
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
var asksPhysics = (report) => typeof report.config?.physics === "object" && report.config.physics !== null;
|
|
3875
|
+
function blockProblem(block, report) {
|
|
3876
|
+
if (authorityTickHzOf(block.tickHz) === void 0) return `tick rate ${block.tickHz} is not ${AUTHORITY_TICK_RATES_TEXT}`;
|
|
3877
|
+
if (block.tickHz === AUTHORITY_PHYSICS_ONLY_TICK_HZ && !asksPhysics(report)) {
|
|
3878
|
+
return `tick rate ${block.tickHz} is only for a module that asks for physics`;
|
|
3879
|
+
}
|
|
3880
|
+
if (!Number.isSafeInteger(block.maxPlayers) || block.maxPlayers < 1 || block.maxPlayers > AUTHORITY_MAX_PLAYERS) {
|
|
3881
|
+
return `${block.maxPlayers} players is outside 1\u2013${AUTHORITY_MAX_PLAYERS}`;
|
|
3882
|
+
}
|
|
3883
|
+
return null;
|
|
3884
|
+
}
|
|
3885
|
+
async function prepareAuthorityModule(gameDir, detection, log, injected) {
|
|
3886
|
+
if (detection === void 0 || detection.state === "none") return null;
|
|
3887
|
+
if (detection.state === "invalid") {
|
|
3888
|
+
stopped(log, AuthorityStop.ManifestInvalid, `${detection.source} genex.authority ${detection.problem}.`);
|
|
3889
|
+
return false;
|
|
3890
|
+
}
|
|
3891
|
+
const { manifest } = detection;
|
|
3892
|
+
let toolchain = injected;
|
|
3893
|
+
if (!toolchain) {
|
|
3894
|
+
const loaded = await loadAuthorityToolchain(gameDir);
|
|
3895
|
+
if (!loaded.ok) {
|
|
3896
|
+
const verb = loaded.stop === AuthorityStop.ToolchainMissing ? "is not installed in this game" : "in this game is too old";
|
|
3897
|
+
stopped(log, loaded.stop, `${AUTHORITY_DEV_PACKAGE} ${verb}; it bundles and checks the room module.`, `Run \`npm i -D ${AUTHORITY_DEV_PACKAGE}@latest\`, then deploy again.`);
|
|
3898
|
+
return false;
|
|
3899
|
+
}
|
|
3900
|
+
toolchain = loaded.toolchain;
|
|
3901
|
+
}
|
|
3902
|
+
const entry = path15.join(gameDir, manifest.module);
|
|
3903
|
+
log.step(`Checking the room module (${manifest.module})\u2026`);
|
|
3904
|
+
const bundled = await toolchain.bundleModule(entry, gameDir);
|
|
3905
|
+
if (!bundled.ok) {
|
|
3906
|
+
stopped(log, AuthorityStop.BundleFailed, `${manifest.module} does not bundle.`, failureLine(bundled.failure));
|
|
3907
|
+
return false;
|
|
3908
|
+
}
|
|
3909
|
+
const report = await toolchain.validateModule({ module: entry, root: gameDir });
|
|
3910
|
+
const reportBytes = Buffer.from(`${JSON.stringify(report, null, 2)}
|
|
3911
|
+
`, "utf8");
|
|
3912
|
+
const kept = await keepReport(gameDir, reportBytes);
|
|
3913
|
+
if (!report.passed) {
|
|
3914
|
+
const shown = report.failures.slice(0, MAX_FAILURES_SHOWN).map(failureLine);
|
|
3915
|
+
const more = report.failures.length > MAX_FAILURES_SHOWN ? [`\u2026and ${report.failures.length - MAX_FAILURES_SHOWN} more`] : [];
|
|
3916
|
+
stopped(log, AuthorityStop.ValidateFailed, `${manifest.module} did not pass validation.`, ...shown, ...more, `Full report: ${kept}`, `Reproduce: npx genex-authority-dev validate --module ${manifest.module}`);
|
|
3917
|
+
return false;
|
|
3918
|
+
}
|
|
3919
|
+
const code = Buffer.from(bundled.value.code, "utf8");
|
|
3920
|
+
if (report.moduleSha256 !== sha256(code) || bundled.value.sha256 !== sha256(code)) {
|
|
3921
|
+
stopped(log, AuthorityStop.ShaDisagrees, "the report passed a different bundle than the one about to be uploaded.", `Update ${AUTHORITY_DEV_PACKAGE} and deploy again.`);
|
|
3922
|
+
return false;
|
|
3923
|
+
}
|
|
3924
|
+
if (code.length > MODULE_MAX_BYTES || reportBytes.length > REPORT_MAX_BYTES) {
|
|
3925
|
+
stopped(log, AuthorityStop.TooLarge, `the module (${code.length} bytes) or its report (${reportBytes.length} bytes) is over what Genex stores.`);
|
|
3926
|
+
return false;
|
|
3927
|
+
}
|
|
3928
|
+
const goLive = goLiveBlock(manifest, report, code, reportBytes);
|
|
3929
|
+
const problem = blockProblem(goLive, report);
|
|
3930
|
+
if (problem) {
|
|
3931
|
+
stopped(log, AuthorityStop.ConfigOutOfRange, `${problem}.`, "Set genex.authority's tickHz / maxPlayers, or the module's config, to a value Genex runs.");
|
|
3932
|
+
return false;
|
|
3933
|
+
}
|
|
3934
|
+
log.success(`Room module passed (${goLive.moduleSha256.slice(0, 12)}, ${code.length} bytes).`);
|
|
3935
|
+
return { manifest, code, report: reportBytes, goLive };
|
|
3936
|
+
}
|
|
3937
|
+
function isServerFile(relPath) {
|
|
3938
|
+
const [top, ...rest] = relPath.split("/");
|
|
3939
|
+
return rest.length > 0 && top?.toLowerCase() === AUTHORITY_SERVER_DIR;
|
|
3940
|
+
}
|
|
3941
|
+
var isPresigned = (value) => {
|
|
3942
|
+
const v = value;
|
|
3943
|
+
return typeof v?.url === "string" && typeof v.headers === "object" && v.headers !== null;
|
|
3944
|
+
};
|
|
3945
|
+
async function putSigned(target, body, what, log) {
|
|
3946
|
+
if (target.headers["Content-Length"] !== String(body.length)) {
|
|
3947
|
+
log.error(`The server signed the ${what} for ${target.headers["Content-Length"]} bytes, not ${body.length}. Nothing was deployed.`);
|
|
3948
|
+
return false;
|
|
3949
|
+
}
|
|
3950
|
+
try {
|
|
3951
|
+
const res = await fetch(target.url, { method: "PUT", headers: target.headers, body });
|
|
3952
|
+
if (res.ok) return true;
|
|
3953
|
+
log.error(`Couldn't upload the ${what} (HTTP ${res.status}). Nothing was deployed.`);
|
|
3954
|
+
} catch (err) {
|
|
3955
|
+
log.error(`Couldn't upload the ${what}: ${String(err)}. Nothing was deployed.`);
|
|
3956
|
+
}
|
|
3957
|
+
return false;
|
|
3958
|
+
}
|
|
3959
|
+
async function reportPresignRefusal(res, module, log) {
|
|
3960
|
+
const body = await res.clone().json().catch(() => null);
|
|
3961
|
+
if (res.status === 403 && body?.error === "authority_requires_pro") {
|
|
3962
|
+
log.error(`Room module refused (${AuthorityStop.RequiresPro}): server-authoritative multiplayer on Genex servers needs Genex Pro. Nothing was deployed.`);
|
|
3963
|
+
log.dim(` Test it on this machine without an account: ${localTestCommand(module)}`);
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
if (res.status === 401) {
|
|
3967
|
+
log.error("Not authorized (HTTP 401) \u2014 your sign-in expired or was revoked. Run `genex link <your-game>` and sign in again.");
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
if (res.status === 404) {
|
|
3971
|
+
log.error("This account doesn't own this game (HTTP 404). Run `genex link <your-game>` as the owner.");
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
const said = typeof body?.message === "string" ? body.message : typeof body?.error === "string" ? body.error : "";
|
|
3975
|
+
log.error(`Couldn't store the room module (HTTP ${res.status})${said ? `: ${said}` : ""}. Nothing was deployed.`);
|
|
3976
|
+
}
|
|
3977
|
+
async function uploadAuthorityModule(api, prepared, log) {
|
|
3978
|
+
log.step("Uploading the room module\u2026");
|
|
3979
|
+
let res;
|
|
3980
|
+
try {
|
|
3981
|
+
res = await apiFetch(`${api.apiUrl}/api/games/${api.projectId}/authority-module`, {
|
|
3982
|
+
method: "POST",
|
|
3983
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${api.token}` },
|
|
3984
|
+
body: JSON.stringify({
|
|
3985
|
+
sha256: prepared.goLive.moduleSha256,
|
|
3986
|
+
bytes: prepared.code.length,
|
|
3987
|
+
reportSha256: prepared.goLive.reportSha256,
|
|
3988
|
+
reportBytes: prepared.report.length
|
|
3989
|
+
})
|
|
3990
|
+
});
|
|
3991
|
+
} catch (err) {
|
|
3992
|
+
log.error(`Couldn't reach the API at ${api.apiUrl}: ${String(err)}`);
|
|
3993
|
+
return false;
|
|
3994
|
+
}
|
|
3995
|
+
if (!res.ok) {
|
|
3996
|
+
await reportPresignRefusal(res, prepared.manifest.module, log);
|
|
3997
|
+
return false;
|
|
3998
|
+
}
|
|
3999
|
+
const grant = await res.json().catch(() => null);
|
|
4000
|
+
if (!isPresigned(grant?.module) || !isPresigned(grant?.report)) {
|
|
4001
|
+
log.error("The API answered the room module upload without upload addresses. Nothing was deployed.");
|
|
4002
|
+
return false;
|
|
4003
|
+
}
|
|
4004
|
+
if (!await putSigned(grant.module, prepared.code, "room module", log)) return false;
|
|
4005
|
+
return putSigned(grant.report, prepared.report, "validation report", log);
|
|
4006
|
+
}
|
|
4007
|
+
|
|
3688
4008
|
// src/lib/generation-plan.ts
|
|
3689
4009
|
var LANE_OF_KIND = {
|
|
3690
4010
|
image: "image",
|
|
@@ -3863,14 +4183,14 @@ function auditGenerationPlan(input) {
|
|
|
3863
4183
|
}
|
|
3864
4184
|
|
|
3865
4185
|
// src/lib/deploy.ts
|
|
3866
|
-
import
|
|
3867
|
-
import
|
|
4186
|
+
import crypto3 from "crypto";
|
|
4187
|
+
import fs18 from "fs/promises";
|
|
3868
4188
|
import os5 from "os";
|
|
3869
|
-
import
|
|
4189
|
+
import path18 from "path";
|
|
3870
4190
|
|
|
3871
4191
|
// src/lib/source-size.ts
|
|
3872
|
-
import
|
|
3873
|
-
import
|
|
4192
|
+
import fs15 from "fs/promises";
|
|
4193
|
+
import path16 from "path";
|
|
3874
4194
|
var FALLBACK_SOURCE_LIMITS = {
|
|
3875
4195
|
maxTotalBytes: 5 * 1024 * 1024 * 1024,
|
|
3876
4196
|
maxFiles: 5e3
|
|
@@ -3912,7 +4232,7 @@ async function measurePayload(cwd, staged) {
|
|
|
3912
4232
|
for (const file of staged) {
|
|
3913
4233
|
let size = 0;
|
|
3914
4234
|
try {
|
|
3915
|
-
size = (await
|
|
4235
|
+
size = (await fs15.stat(path16.join(cwd, file))).size;
|
|
3916
4236
|
} catch {
|
|
3917
4237
|
size = 0;
|
|
3918
4238
|
}
|
|
@@ -3923,12 +4243,12 @@ async function measurePayload(cwd, staged) {
|
|
|
3923
4243
|
}
|
|
3924
4244
|
|
|
3925
4245
|
// src/commands/ui.ts
|
|
3926
|
-
import
|
|
3927
|
-
import
|
|
4246
|
+
import fs17 from "fs/promises";
|
|
4247
|
+
import path17 from "path";
|
|
3928
4248
|
import { PNG as PNG2 } from "pngjs";
|
|
3929
4249
|
|
|
3930
4250
|
// src/lib/png-tools.ts
|
|
3931
|
-
import
|
|
4251
|
+
import fs16 from "fs/promises";
|
|
3932
4252
|
import { PNG } from "pngjs";
|
|
3933
4253
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
3934
4254
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -3939,12 +4259,12 @@ async function loadPng(input) {
|
|
|
3939
4259
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
3940
4260
|
buf = Buffer.from(await res.arrayBuffer());
|
|
3941
4261
|
} else {
|
|
3942
|
-
buf = await
|
|
4262
|
+
buf = await fs16.readFile(input);
|
|
3943
4263
|
}
|
|
3944
4264
|
return PNG.sync.read(buf);
|
|
3945
4265
|
}
|
|
3946
4266
|
async function writePng(file, png) {
|
|
3947
|
-
await
|
|
4267
|
+
await fs16.writeFile(file, PNG.sync.write(png));
|
|
3948
4268
|
}
|
|
3949
4269
|
function cropPng(image, box) {
|
|
3950
4270
|
const out = new PNG({ width: box.w, height: box.h });
|
|
@@ -4244,7 +4564,7 @@ async function uiExtract(opts, log) {
|
|
|
4244
4564
|
const dilatePx = opts.dilate ?? 0;
|
|
4245
4565
|
const sheet = await loadPng(input);
|
|
4246
4566
|
const { width: W, height: H, data } = sheet;
|
|
4247
|
-
await
|
|
4567
|
+
await fs17.mkdir(outDir, { recursive: true });
|
|
4248
4568
|
log.plain(c.bold("genex ui extract"));
|
|
4249
4569
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names2.length} names`);
|
|
4250
4570
|
let hasTransparency = false;
|
|
@@ -4473,7 +4793,7 @@ async function uiExtract(opts, log) {
|
|
|
4473
4793
|
rimPixels: speckle.sampled
|
|
4474
4794
|
});
|
|
4475
4795
|
}
|
|
4476
|
-
const outPath =
|
|
4796
|
+
const outPath = path17.join(outDir, `${name}.png`);
|
|
4477
4797
|
await writePng(outPath, out);
|
|
4478
4798
|
const sidecar = {
|
|
4479
4799
|
name,
|
|
@@ -4492,7 +4812,7 @@ async function uiExtract(opts, log) {
|
|
|
4492
4812
|
defringed
|
|
4493
4813
|
};
|
|
4494
4814
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
4495
|
-
await
|
|
4815
|
+
await fs17.writeFile(
|
|
4496
4816
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
4497
4817
|
JSON.stringify(sidecarBody, null, 2)
|
|
4498
4818
|
);
|
|
@@ -4501,8 +4821,8 @@ async function uiExtract(opts, log) {
|
|
|
4501
4821
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
4502
4822
|
);
|
|
4503
4823
|
}
|
|
4504
|
-
const debugPath =
|
|
4505
|
-
await
|
|
4824
|
+
const debugPath = path17.join(outDir, "extract-debug.json");
|
|
4825
|
+
await fs17.writeFile(
|
|
4506
4826
|
debugPath,
|
|
4507
4827
|
JSON.stringify(
|
|
4508
4828
|
{
|
|
@@ -4964,7 +5284,7 @@ async function uiMasks(opts, log) {
|
|
|
4964
5284
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
4965
5285
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
4966
5286
|
const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
|
|
4967
|
-
await
|
|
5287
|
+
await fs17.mkdir(outDir, { recursive: true });
|
|
4968
5288
|
log.plain(c.bold("genex ui masks"));
|
|
4969
5289
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
4970
5290
|
const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
|
|
@@ -5017,11 +5337,11 @@ async function uiMasks(opts, log) {
|
|
|
5017
5337
|
});
|
|
5018
5338
|
}
|
|
5019
5339
|
const overlay = makeOverlay(clean2, converted.png);
|
|
5020
|
-
const framePath =
|
|
5021
|
-
const maskPath =
|
|
5022
|
-
const annotatedPath =
|
|
5023
|
-
const overlayPath =
|
|
5024
|
-
const metaPath =
|
|
5340
|
+
const framePath = path17.join(outDir, `${pair.name}-frame.png`);
|
|
5341
|
+
const maskPath = path17.join(outDir, `${pair.name}-mask.png`);
|
|
5342
|
+
const annotatedPath = path17.join(outDir, `${pair.name}-annotated-source.png`);
|
|
5343
|
+
const overlayPath = path17.join(outDir, `${pair.name}-overlay.png`);
|
|
5344
|
+
const metaPath = path17.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
5025
5345
|
await writePng(framePath, clean2);
|
|
5026
5346
|
await writePng(maskPath, converted.png);
|
|
5027
5347
|
await writePng(annotatedPath, annotated);
|
|
@@ -5068,7 +5388,7 @@ async function uiMasks(opts, log) {
|
|
|
5068
5388
|
},
|
|
5069
5389
|
overlay: overlayPath
|
|
5070
5390
|
};
|
|
5071
|
-
await
|
|
5391
|
+
await fs17.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
5072
5392
|
`);
|
|
5073
5393
|
results.push(meta);
|
|
5074
5394
|
const fb = converted.bbox;
|
|
@@ -5084,8 +5404,8 @@ async function uiMasks(opts, log) {
|
|
|
5084
5404
|
);
|
|
5085
5405
|
}
|
|
5086
5406
|
}
|
|
5087
|
-
const indexPath =
|
|
5088
|
-
await
|
|
5407
|
+
const indexPath = path17.join(outDir, "annotated-progress.json");
|
|
5408
|
+
await fs17.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
|
|
5089
5409
|
`);
|
|
5090
5410
|
log.plain("");
|
|
5091
5411
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -5211,7 +5531,7 @@ async function uiTextColor(opts, log) {
|
|
|
5211
5531
|
};
|
|
5212
5532
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
5213
5533
|
`);
|
|
5214
|
-
if (opts.out) await
|
|
5534
|
+
if (opts.out) await fs17.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
5215
5535
|
`);
|
|
5216
5536
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
5217
5537
|
}
|
|
@@ -5243,7 +5563,7 @@ async function uiTrim(opts, log) {
|
|
|
5243
5563
|
const sidecar = computeBBoxes(trimmed);
|
|
5244
5564
|
const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
|
|
5245
5565
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
5246
|
-
await
|
|
5566
|
+
await fs17.writeFile(
|
|
5247
5567
|
sidecarPath,
|
|
5248
5568
|
JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
|
|
5249
5569
|
);
|
|
@@ -5350,7 +5670,7 @@ async function uiPlate(opts, log) {
|
|
|
5350
5670
|
fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
|
|
5351
5671
|
}
|
|
5352
5672
|
await writePng(outPath, out);
|
|
5353
|
-
const name =
|
|
5673
|
+
const name = path17.basename(outPath);
|
|
5354
5674
|
log.plain(c.bold("genex ui plate"));
|
|
5355
5675
|
log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
|
|
5356
5676
|
log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
|
|
@@ -5515,13 +5835,13 @@ async function walkFiles(dir) {
|
|
|
5515
5835
|
const out = [];
|
|
5516
5836
|
let entries;
|
|
5517
5837
|
try {
|
|
5518
|
-
entries = await
|
|
5838
|
+
entries = await fs17.readdir(dir, { withFileTypes: true });
|
|
5519
5839
|
} catch {
|
|
5520
5840
|
return out;
|
|
5521
5841
|
}
|
|
5522
5842
|
for (const entry of entries) {
|
|
5523
5843
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
5524
|
-
const p =
|
|
5844
|
+
const p = path17.join(dir, entry.name);
|
|
5525
5845
|
if (entry.isDirectory()) out.push(...await walkFiles(p));
|
|
5526
5846
|
else out.push(p);
|
|
5527
5847
|
}
|
|
@@ -5530,7 +5850,7 @@ async function walkFiles(dir) {
|
|
|
5530
5850
|
async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
5531
5851
|
const viewportFindings = [];
|
|
5532
5852
|
try {
|
|
5533
|
-
const indexHtml = await
|
|
5853
|
+
const indexHtml = await fs17.readFile(path17.join(cwd, "index.html"), "utf8");
|
|
5534
5854
|
if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
|
|
5535
5855
|
viewportFindings.push({
|
|
5536
5856
|
kind: "viewport-meta",
|
|
@@ -5544,28 +5864,28 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5544
5864
|
}
|
|
5545
5865
|
} catch {
|
|
5546
5866
|
}
|
|
5547
|
-
const absAssets =
|
|
5867
|
+
const absAssets = path17.resolve(cwd, assetDir);
|
|
5548
5868
|
try {
|
|
5549
|
-
if (!(await
|
|
5869
|
+
if (!(await fs17.stat(absAssets)).isDirectory()) {
|
|
5550
5870
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
5551
5871
|
}
|
|
5552
5872
|
} catch {
|
|
5553
5873
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
5554
5874
|
}
|
|
5555
|
-
const srcFiles = (await walkFiles(
|
|
5556
|
-
(p) => AUDIT_SRC_EXTS.has(
|
|
5875
|
+
const srcFiles = (await walkFiles(path17.resolve(cwd, srcDir))).filter(
|
|
5876
|
+
(p) => AUDIT_SRC_EXTS.has(path17.extname(p).toLowerCase())
|
|
5557
5877
|
);
|
|
5558
5878
|
try {
|
|
5559
|
-
for (const name of await
|
|
5560
|
-
const ext =
|
|
5561
|
-
if (ext === ".html" || ext === ".css") srcFiles.push(
|
|
5879
|
+
for (const name of await fs17.readdir(cwd)) {
|
|
5880
|
+
const ext = path17.extname(name).toLowerCase();
|
|
5881
|
+
if (ext === ".html" || ext === ".css") srcFiles.push(path17.join(cwd, name));
|
|
5562
5882
|
}
|
|
5563
5883
|
} catch {
|
|
5564
5884
|
}
|
|
5565
5885
|
const sources = [];
|
|
5566
5886
|
for (const p of srcFiles) {
|
|
5567
5887
|
try {
|
|
5568
|
-
sources.push({ rel:
|
|
5888
|
+
sources.push({ rel: path17.relative(cwd, p), text: await fs17.readFile(p, "utf8") });
|
|
5569
5889
|
} catch {
|
|
5570
5890
|
}
|
|
5571
5891
|
}
|
|
@@ -5575,11 +5895,11 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5575
5895
|
const metaByName = /* @__PURE__ */ new Map();
|
|
5576
5896
|
const bboxByPng = /* @__PURE__ */ new Map();
|
|
5577
5897
|
for (const p of assetFiles) {
|
|
5578
|
-
const base =
|
|
5898
|
+
const base = path17.basename(p);
|
|
5579
5899
|
const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
|
|
5580
5900
|
if (metaMatch) {
|
|
5581
5901
|
try {
|
|
5582
|
-
const meta = JSON.parse(await
|
|
5902
|
+
const meta = JSON.parse(await fs17.readFile(p, "utf8"));
|
|
5583
5903
|
metaByName.set(metaMatch[1], {
|
|
5584
5904
|
cleanCrop: meta.clean?.crop ?? null,
|
|
5585
5905
|
loosened: meta.loosened === true
|
|
@@ -5590,7 +5910,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5590
5910
|
}
|
|
5591
5911
|
if (base.endsWith(".bbox.json")) {
|
|
5592
5912
|
try {
|
|
5593
|
-
const sidecar = JSON.parse(await
|
|
5913
|
+
const sidecar = JSON.parse(await fs17.readFile(p, "utf8"));
|
|
5594
5914
|
if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
|
|
5595
5915
|
} catch {
|
|
5596
5916
|
}
|
|
@@ -5599,7 +5919,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5599
5919
|
for (const [name, meta] of metaByName) {
|
|
5600
5920
|
if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
|
|
5601
5921
|
for (const p of assetFiles) {
|
|
5602
|
-
const base =
|
|
5922
|
+
const base = path17.basename(p);
|
|
5603
5923
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
5604
5924
|
if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
|
|
5605
5925
|
if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
|
|
@@ -5623,7 +5943,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5623
5943
|
}
|
|
5624
5944
|
const pngByBase = /* @__PURE__ */ new Map();
|
|
5625
5945
|
for (const p of assetFiles) {
|
|
5626
|
-
const base =
|
|
5946
|
+
const base = path17.basename(p);
|
|
5627
5947
|
if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
|
|
5628
5948
|
}
|
|
5629
5949
|
for (const [maskBase, maskPath] of pngByBase) {
|
|
@@ -5636,8 +5956,8 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5636
5956
|
let frame;
|
|
5637
5957
|
let mask;
|
|
5638
5958
|
try {
|
|
5639
|
-
frame = PNG2.sync.read(await
|
|
5640
|
-
mask = PNG2.sync.read(await
|
|
5959
|
+
frame = PNG2.sync.read(await fs17.readFile(framePath));
|
|
5960
|
+
mask = PNG2.sync.read(await fs17.readFile(maskPath));
|
|
5641
5961
|
} catch {
|
|
5642
5962
|
continue;
|
|
5643
5963
|
}
|
|
@@ -5656,7 +5976,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5656
5976
|
if (!referenced(base)) continue;
|
|
5657
5977
|
let png;
|
|
5658
5978
|
try {
|
|
5659
|
-
png = PNG2.sync.read(await
|
|
5979
|
+
png = PNG2.sync.read(await fs17.readFile(p));
|
|
5660
5980
|
} catch {
|
|
5661
5981
|
continue;
|
|
5662
5982
|
}
|
|
@@ -5676,7 +5996,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5676
5996
|
}
|
|
5677
5997
|
const maskReported = /* @__PURE__ */ new Set();
|
|
5678
5998
|
for (const p of assetFiles) {
|
|
5679
|
-
const m = /^(.+)\.annotated-progress\.json$/.exec(
|
|
5999
|
+
const m = /^(.+)\.annotated-progress\.json$/.exec(path17.basename(p));
|
|
5680
6000
|
if (!m) continue;
|
|
5681
6001
|
const maskBase = `${m[1]}-mask.png`;
|
|
5682
6002
|
if (!referenced(maskBase)) {
|
|
@@ -5688,14 +6008,14 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
5688
6008
|
}
|
|
5689
6009
|
}
|
|
5690
6010
|
for (const p of assetFiles) {
|
|
5691
|
-
const base =
|
|
6011
|
+
const base = path17.basename(p);
|
|
5692
6012
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
5693
6013
|
if (/-annotated-source\.png$|-overlay\.png$/.test(base)) continue;
|
|
5694
6014
|
if (maskReported.has(base)) continue;
|
|
5695
6015
|
if (!referenced(base)) {
|
|
5696
6016
|
findings.push({
|
|
5697
6017
|
kind: "unwired-sprite",
|
|
5698
|
-
message: `${
|
|
6018
|
+
message: `${path17.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.)`
|
|
5699
6019
|
});
|
|
5700
6020
|
}
|
|
5701
6021
|
}
|
|
@@ -5827,7 +6147,7 @@ async function printUiAuditPreflight(log) {
|
|
|
5827
6147
|
}
|
|
5828
6148
|
async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
5829
6149
|
try {
|
|
5830
|
-
const design = await
|
|
6150
|
+
const design = await fs18.readFile(path18.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
|
|
5831
6151
|
const warnings = [];
|
|
5832
6152
|
if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
|
|
5833
6153
|
warnings.push(
|
|
@@ -5835,7 +6155,7 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
5835
6155
|
);
|
|
5836
6156
|
}
|
|
5837
6157
|
if (!/player character:/i.test(design)) {
|
|
5838
|
-
const hasCharacter = await
|
|
6158
|
+
const hasCharacter = await fs18.access(path18.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
|
|
5839
6159
|
if (!hasCharacter && await loadsPlayerBody(cwd)) {
|
|
5840
6160
|
warnings.push(
|
|
5841
6161
|
`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).`
|
|
@@ -5852,12 +6172,12 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
5852
6172
|
async function loadsPlayerBody(cwd) {
|
|
5853
6173
|
const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
|
|
5854
6174
|
try {
|
|
5855
|
-
const entries = await
|
|
6175
|
+
const entries = await fs18.readdir(path18.join(cwd, "src"), { recursive: true });
|
|
5856
6176
|
for (const rel of entries) {
|
|
5857
6177
|
if (rel.includes("node_modules")) continue;
|
|
5858
|
-
if (rel.split(
|
|
6178
|
+
if (rel.split(path18.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
|
|
5859
6179
|
if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
|
|
5860
|
-
const text = await
|
|
6180
|
+
const text = await fs18.readFile(path18.join(cwd, "src", rel), "utf8").catch(() => "");
|
|
5861
6181
|
if (BODY_LOADERS.test(text)) return true;
|
|
5862
6182
|
}
|
|
5863
6183
|
} catch {
|
|
@@ -5900,6 +6220,8 @@ async function deployGame(ctx, opts, log) {
|
|
|
5900
6220
|
);
|
|
5901
6221
|
return false;
|
|
5902
6222
|
}
|
|
6223
|
+
const authority = await prepareAuthorityModule(cwd, opts.authority, log, opts.authorityToolchain);
|
|
6224
|
+
if (authority === false) return false;
|
|
5903
6225
|
if (!opts.noBuild && await hasBuildScript(cwd)) {
|
|
5904
6226
|
log.step("Building the production bundle\u2026");
|
|
5905
6227
|
const built = await run("npm", ["run", "build"]);
|
|
@@ -5911,11 +6233,15 @@ async function deployGame(ctx, opts, log) {
|
|
|
5911
6233
|
}
|
|
5912
6234
|
log.success("Built.");
|
|
5913
6235
|
}
|
|
5914
|
-
const distDir =
|
|
6236
|
+
const distDir = path18.join(cwd, "dist");
|
|
5915
6237
|
const siteDir = await isDir(distDir) ? distDir : cwd;
|
|
5916
|
-
const rel =
|
|
6238
|
+
const rel = path18.relative(cwd, siteDir) || ".";
|
|
5917
6239
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
5918
|
-
const
|
|
6240
|
+
const collected = await collectFiles(siteDir);
|
|
6241
|
+
const files = authority ? collected.filter((f) => !isServerFile(f.relPath)) : collected;
|
|
6242
|
+
if (authority && files.length < collected.length) {
|
|
6243
|
+
log.dim(` Kept ${collected.length - files.length} server file(s) out of the game's public build.`);
|
|
6244
|
+
}
|
|
5919
6245
|
if (files.length === 0) {
|
|
5920
6246
|
log.warn("Nothing to deploy \u2014 the build produced no files.");
|
|
5921
6247
|
return false;
|
|
@@ -5966,6 +6292,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
5966
6292
|
log.error(`Build is ${mb2(totalBytes)} MB \u2014 over the ${mb2(grant.limits.maxTotalBytes)} MB limit.`);
|
|
5967
6293
|
return false;
|
|
5968
6294
|
}
|
|
6295
|
+
if (authority && !await uploadAuthorityModule(ctx, authority, log)) return false;
|
|
5969
6296
|
log.step(`Uploading ${files.length} files\u2026`);
|
|
5970
6297
|
if (!await uploadAll(grant, files, log)) {
|
|
5971
6298
|
log.error("Couldn't upload your game \u2014 please try again.");
|
|
@@ -5985,7 +6312,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
5985
6312
|
);
|
|
5986
6313
|
if (pushed !== true) return false;
|
|
5987
6314
|
log.step("Publishing\u2026");
|
|
5988
|
-
const published = await callPublish(ctx, commit, opts, log, sourceCommitSha, capabilities);
|
|
6315
|
+
const published = await callPublish(ctx, commit, opts, log, sourceCommitSha, capabilities, authority?.goLive);
|
|
5989
6316
|
if (!published) return false;
|
|
5990
6317
|
if (opts.channel === "staging") {
|
|
5991
6318
|
const pushedHere = opts.sourceDir === void 0;
|
|
@@ -6010,7 +6337,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
6010
6337
|
}
|
|
6011
6338
|
async function hasBuildScript(cwd) {
|
|
6012
6339
|
try {
|
|
6013
|
-
const pkg = JSON.parse(await
|
|
6340
|
+
const pkg = JSON.parse(await fs18.readFile(path18.join(cwd, "package.json"), "utf8"));
|
|
6014
6341
|
return Boolean(pkg.scripts?.build);
|
|
6015
6342
|
} catch {
|
|
6016
6343
|
return false;
|
|
@@ -6019,12 +6346,12 @@ async function hasBuildScript(cwd) {
|
|
|
6019
6346
|
async function collectFiles(root) {
|
|
6020
6347
|
const out = [];
|
|
6021
6348
|
const walk2 = async (dir, prefix) => {
|
|
6022
|
-
for (const e of await
|
|
6349
|
+
for (const e of await fs18.readdir(dir, { withFileTypes: true })) {
|
|
6023
6350
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
6024
6351
|
if (e.isDirectory()) {
|
|
6025
|
-
if (!EXCLUDE_DIRS.has(e.name)) await walk2(
|
|
6352
|
+
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path18.join(dir, e.name), relPath);
|
|
6026
6353
|
} else if (e.isFile() && !isSecretEnvFile(e.name)) {
|
|
6027
|
-
out.push({ relPath, bytes: await
|
|
6354
|
+
out.push({ relPath, bytes: await fs18.readFile(path18.join(dir, e.name)) });
|
|
6028
6355
|
}
|
|
6029
6356
|
}
|
|
6030
6357
|
};
|
|
@@ -6032,7 +6359,7 @@ async function collectFiles(root) {
|
|
|
6032
6359
|
return out;
|
|
6033
6360
|
}
|
|
6034
6361
|
function contentCommit(files) {
|
|
6035
|
-
const h =
|
|
6362
|
+
const h = crypto3.createHash("sha1");
|
|
6036
6363
|
for (const f of [...files].sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0)) {
|
|
6037
6364
|
h.update(f.relPath, "utf8");
|
|
6038
6365
|
h.update("\0");
|
|
@@ -6210,7 +6537,7 @@ async function uploadPartWithRetry(base, encoded, auth, uploadId, partNumber, by
|
|
|
6210
6537
|
await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 500 : 2e3));
|
|
6211
6538
|
}
|
|
6212
6539
|
}
|
|
6213
|
-
async function callPublish(ctx, commit, opts, log, sourceCommitSha, capabilities) {
|
|
6540
|
+
async function callPublish(ctx, commit, opts, log, sourceCommitSha, capabilities, authority) {
|
|
6214
6541
|
let res;
|
|
6215
6542
|
try {
|
|
6216
6543
|
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}/publish`, {
|
|
@@ -6234,7 +6561,10 @@ async function callPublish(ctx, commit, opts, log, sourceCommitSha, capabilities
|
|
|
6234
6561
|
// Recorded into the release history and acted on by nothing. Absent when
|
|
6235
6562
|
// the source push produced no commit (a user's own repo, or a failure
|
|
6236
6563
|
// that already stopped the deploy), which the server accepts as null.
|
|
6237
|
-
...sourceCommitSha ? { sourceCommitSha } : {}
|
|
6564
|
+
...sourceCommitSha ? { sourceCommitSha } : {},
|
|
6565
|
+
// Last, and only when there is one: a game without a room module sends the body it always
|
|
6566
|
+
// sent, so the channel keeps whatever module it had (absent = keep, on the API).
|
|
6567
|
+
...authority ? { authority } : {}
|
|
6238
6568
|
})
|
|
6239
6569
|
});
|
|
6240
6570
|
} catch (err) {
|
|
@@ -6259,6 +6589,16 @@ async function callPublish(ctx, commit, opts, log, sourceCommitSha, capabilities
|
|
|
6259
6589
|
log.dim(" Re-run once the API finishes deploying; the build you meant to keep is still in R2.");
|
|
6260
6590
|
return null;
|
|
6261
6591
|
}
|
|
6592
|
+
if (authority && body?.authority?.moduleSha256 !== authority.moduleSha256) {
|
|
6593
|
+
log.error("The server did not record this build's room module, so its multiplayer rooms would not start.");
|
|
6594
|
+
log.dim(` Your ${asked} build may already be serving${body?.url ? ` at ${body.url}` : ""}. Deploy again once the API is updated.`);
|
|
6595
|
+
return null;
|
|
6596
|
+
}
|
|
6597
|
+
const kept = authority ? void 0 : body?.authority?.moduleSha256;
|
|
6598
|
+
if (typeof kept === "string") {
|
|
6599
|
+
log.warn(`This ${asked} channel still runs the room module from an earlier build (${kept.slice(0, 12)}); this build declares none.`);
|
|
6600
|
+
log.dim(" Its rooms keep that module until a build declares another in genex.authority.");
|
|
6601
|
+
}
|
|
6262
6602
|
return { url: body?.url ?? "" };
|
|
6263
6603
|
}
|
|
6264
6604
|
async function pushSource(sourceDir, ctx, log, branch = "main", expectStagingCommit, onCommit) {
|
|
@@ -6296,17 +6636,17 @@ async function pushSource(sourceDir, ctx, log, branch = "main", expectStagingCom
|
|
|
6296
6636
|
) === true;
|
|
6297
6637
|
}
|
|
6298
6638
|
async function resolveSourceDir(dir, cwd = process.cwd()) {
|
|
6299
|
-
const abs =
|
|
6300
|
-
const stat = await
|
|
6639
|
+
const abs = path18.resolve(cwd, dir);
|
|
6640
|
+
const stat = await fs18.lstat(abs).catch(() => null);
|
|
6301
6641
|
if (!stat) return { refused: `--source ${abs} does not exist.` };
|
|
6302
6642
|
if (stat.isSymbolicLink()) {
|
|
6303
6643
|
return { refused: `--source ${abs} is a symbolic link. Pass the folder itself.` };
|
|
6304
6644
|
}
|
|
6305
6645
|
if (!stat.isDirectory()) return { refused: `--source ${abs} is not a folder.` };
|
|
6306
|
-
const real = await
|
|
6307
|
-
const project = await
|
|
6308
|
-
const genex =
|
|
6309
|
-
const genexTarget = await
|
|
6646
|
+
const real = await fs18.realpath(abs);
|
|
6647
|
+
const project = await fs18.realpath(cwd).catch(() => path18.resolve(cwd));
|
|
6648
|
+
const genex = path18.join(project, ".genex");
|
|
6649
|
+
const genexTarget = await fs18.realpath(genex).catch(() => genex);
|
|
6310
6650
|
for (const meta of /* @__PURE__ */ new Set([genex, genexTarget])) {
|
|
6311
6651
|
if (real === meta) {
|
|
6312
6652
|
return { refused: `--source ${abs} is this project's .genex folder, not the game's source.` };
|
|
@@ -6320,8 +6660,8 @@ async function resolveSourceDir(dir, cwd = process.cwd()) {
|
|
|
6320
6660
|
return { dir: real };
|
|
6321
6661
|
}
|
|
6322
6662
|
function isInside(child, parent) {
|
|
6323
|
-
const rel =
|
|
6324
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${
|
|
6663
|
+
const rel = path18.relative(parent, child);
|
|
6664
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${path18.sep}`) && !path18.isAbsolute(rel);
|
|
6325
6665
|
}
|
|
6326
6666
|
var WORKSPACE_SOURCE_REF = "workspace";
|
|
6327
6667
|
function gitLfsInstallCommand(platform) {
|
|
@@ -6340,7 +6680,7 @@ async function pushWorktree(workTree, pushUrl, managed, log, branch = "main", on
|
|
|
6340
6680
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
6341
6681
|
return "failed";
|
|
6342
6682
|
};
|
|
6343
|
-
const gitDir = await
|
|
6683
|
+
const gitDir = await fs18.mkdtemp(path18.join(os5.tmpdir(), "genex-source-"));
|
|
6344
6684
|
const base = { GIT_DIR: gitDir };
|
|
6345
6685
|
if (urlHasEmbeddedCredentials(pushUrl)) {
|
|
6346
6686
|
base.GIT_CONFIG_COUNT = "1";
|
|
@@ -6355,8 +6695,8 @@ async function pushWorktree(workTree, pushUrl, managed, log, branch = "main", on
|
|
|
6355
6695
|
};
|
|
6356
6696
|
try {
|
|
6357
6697
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
6358
|
-
await
|
|
6359
|
-
const env = { ...base, GIT_WORK_TREE: workTree, GIT_INDEX_FILE:
|
|
6698
|
+
await fs18.writeFile(path18.join(gitDir, "info", "exclude"), excludeFile());
|
|
6699
|
+
const env = { ...base, GIT_WORK_TREE: workTree, GIT_INDEX_FILE: path18.join(gitDir, "index-source") };
|
|
6360
6700
|
let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
|
|
6361
6701
|
if (!lfs) {
|
|
6362
6702
|
log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
|
|
@@ -6422,7 +6762,7 @@ async function pushWorktree(workTree, pushUrl, managed, log, branch = "main", on
|
|
|
6422
6762
|
} catch {
|
|
6423
6763
|
return failed();
|
|
6424
6764
|
} finally {
|
|
6425
|
-
await
|
|
6765
|
+
await fs18.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
6426
6766
|
});
|
|
6427
6767
|
}
|
|
6428
6768
|
}
|
|
@@ -6483,7 +6823,7 @@ async function fetchPushUrl(ctx, log, expectStagingCommit) {
|
|
|
6483
6823
|
}
|
|
6484
6824
|
async function isDir(p) {
|
|
6485
6825
|
try {
|
|
6486
|
-
return (await
|
|
6826
|
+
return (await fs18.stat(p)).isDirectory();
|
|
6487
6827
|
} catch {
|
|
6488
6828
|
return false;
|
|
6489
6829
|
}
|
|
@@ -6516,7 +6856,7 @@ async function waitUntilLive(playUrl, fingerprint, timeoutMs, log) {
|
|
|
6516
6856
|
}
|
|
6517
6857
|
|
|
6518
6858
|
// src/lib/project.ts
|
|
6519
|
-
import
|
|
6859
|
+
import crypto4 from "crypto";
|
|
6520
6860
|
async function createDraftProject(opts) {
|
|
6521
6861
|
const { apiUrl, token, dashboardUrl, log } = opts;
|
|
6522
6862
|
log.step("Creating your project\u2026");
|
|
@@ -6582,7 +6922,7 @@ async function createDraftProject(opts) {
|
|
|
6582
6922
|
return { meta: null };
|
|
6583
6923
|
}
|
|
6584
6924
|
function randomSuffix() {
|
|
6585
|
-
return
|
|
6925
|
+
return crypto4.randomBytes(3).toString("hex");
|
|
6586
6926
|
}
|
|
6587
6927
|
async function fetchOwnerProject(apiUrl, token, slug) {
|
|
6588
6928
|
try {
|
|
@@ -6606,9 +6946,9 @@ async function fetchOwnerProject(apiUrl, token, slug) {
|
|
|
6606
6946
|
}
|
|
6607
6947
|
|
|
6608
6948
|
// src/lib/cover.ts
|
|
6609
|
-
import
|
|
6610
|
-
import
|
|
6611
|
-
import
|
|
6949
|
+
import crypto5 from "crypto";
|
|
6950
|
+
import fs19 from "fs/promises";
|
|
6951
|
+
import path19 from "path";
|
|
6612
6952
|
var COVER_MAX_BYTES = 8 * 1024 * 1024;
|
|
6613
6953
|
var COVER_MIN_WIDTH = 320;
|
|
6614
6954
|
var COVER_MIN_HEIGHT = 180;
|
|
@@ -6936,12 +7276,12 @@ function reportCoverOutcome(log, outcome2, opts) {
|
|
|
6936
7276
|
return;
|
|
6937
7277
|
}
|
|
6938
7278
|
}
|
|
6939
|
-
function
|
|
6940
|
-
return
|
|
7279
|
+
function sha2562(bytes) {
|
|
7280
|
+
return crypto5.createHash("sha256").update(bytes).digest("hex");
|
|
6941
7281
|
}
|
|
6942
7282
|
async function readCoverMemo(cwd = process.cwd()) {
|
|
6943
7283
|
try {
|
|
6944
|
-
const memo = JSON.parse(await
|
|
7284
|
+
const memo = JSON.parse(await fs19.readFile(path19.join(cwd, COVER_MEMO), "utf8"));
|
|
6945
7285
|
return typeof memo.sha256 === "string" ? memo : null;
|
|
6946
7286
|
} catch {
|
|
6947
7287
|
return null;
|
|
@@ -6949,10 +7289,10 @@ async function readCoverMemo(cwd = process.cwd()) {
|
|
|
6949
7289
|
}
|
|
6950
7290
|
async function rememberCoverUpload(bytes, file, outcome2, cwd = process.cwd()) {
|
|
6951
7291
|
if (!isFinalAnswer(outcome2)) return;
|
|
6952
|
-
const memo = { sha256:
|
|
7292
|
+
const memo = { sha256: sha2562(bytes), file, outcome: outcome2.kind, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
6953
7293
|
try {
|
|
6954
|
-
await
|
|
6955
|
-
await
|
|
7294
|
+
await fs19.mkdir(path19.join(cwd, ".genex"), { recursive: true });
|
|
7295
|
+
await fs19.writeFile(path19.join(cwd, COVER_MEMO), JSON.stringify(memo, null, 2) + "\n");
|
|
6956
7296
|
} catch {
|
|
6957
7297
|
}
|
|
6958
7298
|
}
|
|
@@ -6961,7 +7301,7 @@ async function findScratchCover(cwd = process.cwd()) {
|
|
|
6961
7301
|
for (const name of SCRATCH_COVER_NAMES) {
|
|
6962
7302
|
const rel = `${SCRATCH_DIR}/${name}`;
|
|
6963
7303
|
try {
|
|
6964
|
-
const stat = await
|
|
7304
|
+
const stat = await fs19.stat(path19.join(cwd, rel));
|
|
6965
7305
|
if (stat.isFile() && (!best || stat.mtimeMs > best.mtime)) best = { rel, mtime: stat.mtimeMs };
|
|
6966
7306
|
} catch {
|
|
6967
7307
|
}
|
|
@@ -6973,9 +7313,9 @@ async function pickUpScratchCover(args) {
|
|
|
6973
7313
|
try {
|
|
6974
7314
|
const rel = await findScratchCover(cwd);
|
|
6975
7315
|
if (!rel) return null;
|
|
6976
|
-
const bytes = await
|
|
7316
|
+
const bytes = await fs19.readFile(path19.join(cwd, rel));
|
|
6977
7317
|
const memo = await readCoverMemo(cwd);
|
|
6978
|
-
if (memo?.sha256 ===
|
|
7318
|
+
if (memo?.sha256 === sha2562(bytes)) return null;
|
|
6979
7319
|
const outcome2 = await uploadCover({ apiUrl: args.apiUrl, token: args.token, projectId: args.projectId, bytes, label: rel });
|
|
6980
7320
|
reportCoverOutcome(args.log, outcome2, {
|
|
6981
7321
|
label: rel,
|
|
@@ -7036,7 +7376,10 @@ async function runPreview(opts) {
|
|
|
7036
7376
|
embedSdkVersion: detections.embedSdkVersion,
|
|
7037
7377
|
multiplayer: detections.multiplayer,
|
|
7038
7378
|
mobileControls: detections.mobileControls,
|
|
7039
|
-
capabilities: detections.capabilities
|
|
7379
|
+
capabilities: detections.capabilities,
|
|
7380
|
+
// The room module, when the game declares one: bundled, validated and uploaded with this
|
|
7381
|
+
// staging build (promote carries it to production with the build).
|
|
7382
|
+
authority: detections.authority
|
|
7040
7383
|
},
|
|
7041
7384
|
log
|
|
7042
7385
|
);
|
|
@@ -7109,28 +7452,28 @@ function loggerFor(opts) {
|
|
|
7109
7452
|
}
|
|
7110
7453
|
async function readState(directory) {
|
|
7111
7454
|
try {
|
|
7112
|
-
const marker =
|
|
7113
|
-
if ((await
|
|
7114
|
-
const value = JSON.parse(await
|
|
7455
|
+
const marker = path20.join(directory, ".genex", "remix-operation.json");
|
|
7456
|
+
if ((await fs20.lstat(marker)).isSymbolicLink()) return null;
|
|
7457
|
+
const value = JSON.parse(await fs20.readFile(marker, "utf8"));
|
|
7115
7458
|
return value.schema === 1 && value.directory === directory && typeof value.operationId === "string" && /^[a-zA-Z0-9_-]{8,128}$/.test(value.operationId) && value.source && /^[a-f0-9]{40}$/.test(value.source.sourceCommitSha) ? value : null;
|
|
7116
7459
|
} catch {
|
|
7117
7460
|
return null;
|
|
7118
7461
|
}
|
|
7119
7462
|
}
|
|
7120
7463
|
async function saveState(state) {
|
|
7121
|
-
const dir =
|
|
7122
|
-
await
|
|
7123
|
-
const temp =
|
|
7124
|
-
await
|
|
7125
|
-
await
|
|
7464
|
+
const dir = path20.join(state.directory, ".genex");
|
|
7465
|
+
await fs20.mkdir(dir, { recursive: true, mode: 448 });
|
|
7466
|
+
const temp = path20.join(dir, "remix-operation.next.json");
|
|
7467
|
+
await fs20.writeFile(temp, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
7468
|
+
await fs20.rename(temp, path20.join(dir, "remix-operation.json"));
|
|
7126
7469
|
}
|
|
7127
7470
|
async function checkDestination(directory) {
|
|
7128
7471
|
try {
|
|
7129
|
-
const stat = await
|
|
7472
|
+
const stat = await fs20.lstat(directory);
|
|
7130
7473
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new RemixError("destination_occupied", "The destination is not a fresh directory. Choose a new folder.");
|
|
7131
|
-
const entries = await
|
|
7474
|
+
const entries = await fs20.readdir(directory);
|
|
7132
7475
|
if (!entries.length) return null;
|
|
7133
|
-
const genex = await
|
|
7476
|
+
const genex = await fs20.lstat(path20.join(directory, ".genex")).catch(() => null);
|
|
7134
7477
|
const state = genex?.isDirectory() && !genex.isSymbolicLink() ? await readState(directory) : null;
|
|
7135
7478
|
if (state) return state;
|
|
7136
7479
|
throw new RemixError("destination_occupied", "The destination already contains files. Choose a fresh folder; remix never clears an existing project.");
|
|
@@ -7141,7 +7484,7 @@ async function checkDestination(directory) {
|
|
|
7141
7484
|
}
|
|
7142
7485
|
async function assertReferenceParents(cwd) {
|
|
7143
7486
|
for (const rel of [".genex", ".genex/refs"]) {
|
|
7144
|
-
const stat = await
|
|
7487
|
+
const stat = await fs20.lstat(path20.join(cwd, rel)).catch((err) => {
|
|
7145
7488
|
if (err.code === "ENOENT") return null;
|
|
7146
7489
|
throw err;
|
|
7147
7490
|
});
|
|
@@ -7149,41 +7492,41 @@ async function assertReferenceParents(cwd) {
|
|
|
7149
7492
|
}
|
|
7150
7493
|
}
|
|
7151
7494
|
async function archiveSourceInstructions(directory) {
|
|
7152
|
-
const archive =
|
|
7495
|
+
const archive = path20.join(directory, ".genex", "source-instructions");
|
|
7153
7496
|
const rootArtifacts = /* @__PURE__ */ new Set([".claude", ".codex", ".cursor", ".agents", ".mcp.json", ".cursorrules", "DESIGN.md"]);
|
|
7154
7497
|
const instructionNames = /* @__PURE__ */ new Set(["AGENTS.md", "CLAUDE.md", "GEMINI.md"]);
|
|
7155
7498
|
const visit = async (dir) => {
|
|
7156
|
-
for (const entry of await
|
|
7499
|
+
for (const entry of await fs20.readdir(dir, { withFileTypes: true })) {
|
|
7157
7500
|
if (entry.name === ".genex" || entry.name === ".git" || entry.name === "node_modules") continue;
|
|
7158
|
-
const from =
|
|
7501
|
+
const from = path20.join(dir, entry.name), rel = path20.relative(directory, from);
|
|
7159
7502
|
if (instructionNames.has(entry.name) || dir === directory && rootArtifacts.has(entry.name)) {
|
|
7160
|
-
const to =
|
|
7161
|
-
await
|
|
7162
|
-
if (await
|
|
7163
|
-
await
|
|
7503
|
+
const to = path20.join(archive, rel);
|
|
7504
|
+
await fs20.mkdir(path20.dirname(to), { recursive: true });
|
|
7505
|
+
if (await fs20.lstat(to).catch(() => null)) throw new RemixError("destination_changed", `Source instructions at ${rel} changed during preparation; both copies were retained.`);
|
|
7506
|
+
await fs20.rename(from, to);
|
|
7164
7507
|
} else if (entry.isDirectory()) await visit(from);
|
|
7165
7508
|
}
|
|
7166
7509
|
};
|
|
7167
7510
|
await visit(directory);
|
|
7168
7511
|
}
|
|
7169
7512
|
async function acquireLock(directory) {
|
|
7170
|
-
const lock =
|
|
7513
|
+
const lock = path20.join(directory, ".genex", "remix.lock");
|
|
7171
7514
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
7172
7515
|
try {
|
|
7173
|
-
const handle = await
|
|
7516
|
+
const handle = await fs20.open(lock, "wx", 384);
|
|
7174
7517
|
await handle.writeFile(String(process.pid));
|
|
7175
7518
|
await handle.close();
|
|
7176
|
-
return () =>
|
|
7519
|
+
return () => fs20.unlink(lock).catch(() => {
|
|
7177
7520
|
});
|
|
7178
7521
|
} catch (err) {
|
|
7179
7522
|
if (err.code !== "EEXIST") throw err;
|
|
7180
|
-
const pid = Number(await
|
|
7523
|
+
const pid = Number(await fs20.readFile(lock, "utf8").catch(() => ""));
|
|
7181
7524
|
if (!Number.isInteger(pid) || pid <= 0) throw new RemixError("remix_in_progress", "Another remix owns this destination. Retry after it finishes.");
|
|
7182
7525
|
try {
|
|
7183
7526
|
process.kill(pid, 0);
|
|
7184
7527
|
} catch (probe) {
|
|
7185
7528
|
if (probe.code === "ESRCH") {
|
|
7186
|
-
await
|
|
7529
|
+
await fs20.unlink(lock);
|
|
7187
7530
|
continue;
|
|
7188
7531
|
}
|
|
7189
7532
|
}
|
|
@@ -7269,13 +7612,13 @@ function pinRemixDependencies(pkg, version) {
|
|
|
7269
7612
|
return warnings;
|
|
7270
7613
|
}
|
|
7271
7614
|
async function installRemixDependencies(cwd, pm, version, log, runner = childRun) {
|
|
7272
|
-
const pkgPath =
|
|
7273
|
-
const pkg = JSON.parse(await
|
|
7615
|
+
const pkgPath = path20.join(cwd, "package.json");
|
|
7616
|
+
const pkg = JSON.parse(await fs20.readFile(pkgPath, "utf8"));
|
|
7274
7617
|
const warnings = pinRemixDependencies(pkg, version);
|
|
7275
|
-
await
|
|
7618
|
+
await fs20.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
7276
7619
|
log.step(`Restoring dependencies with ${pm}; pinning ${CLI_PACKAGE}@${version} (${CLI_CHANNEL}).`);
|
|
7277
7620
|
const args = pm === "npm" ? ["install", "--workspaces=false", "--no-audit", "--no-fund"] : pm === "pnpm" ? ["install", "--ignore-workspace", "--no-frozen-lockfile"] : ["install"];
|
|
7278
|
-
if (pm === "yarn") await
|
|
7621
|
+
if (pm === "yarn") await fs20.writeFile(path20.join(cwd, "yarn.lock"), "", { flag: "wx" }).catch((err) => {
|
|
7279
7622
|
if (err.code !== "EEXIST") throw err;
|
|
7280
7623
|
});
|
|
7281
7624
|
await runner(cwd, pm, args);
|
|
@@ -7309,27 +7652,27 @@ function rebindRemixConfig(content, slug) {
|
|
|
7309
7652
|
async function configureIdentity(state, log) {
|
|
7310
7653
|
const cwd = state.directory, meta = state.project;
|
|
7311
7654
|
await writeProject(meta, cwd);
|
|
7312
|
-
const configFile =
|
|
7313
|
-
const config = await
|
|
7655
|
+
const configFile = path20.join(cwd, "src", "genex.config.ts");
|
|
7656
|
+
const config = await fs20.readFile(configFile, "utf8").catch((err) => {
|
|
7314
7657
|
if (err.code === "ENOENT") return null;
|
|
7315
7658
|
throw err;
|
|
7316
7659
|
});
|
|
7317
|
-
if (config !== null) await
|
|
7660
|
+
if (config !== null) await fs20.writeFile(configFile, rebindRemixConfig(config, meta.slug));
|
|
7318
7661
|
await writeGameConfigFiles(meta, log, cwd);
|
|
7319
7662
|
await writeGitignore(cwd, log);
|
|
7320
7663
|
await installRemixProfile(cwd, state.apiUrl, log);
|
|
7321
|
-
const manifestFile =
|
|
7322
|
-
const manifest = JSON.parse(await
|
|
7664
|
+
const manifestFile = path20.join(cwd, "package.json");
|
|
7665
|
+
const manifest = JSON.parse(await fs20.readFile(manifestFile, "utf8"));
|
|
7323
7666
|
manifest.genex.remixSource = { projectId: state.source.projectId, slug: state.source.slug, sourceCommitSha: state.source.sourceCommitSha, version: state.source.version, versionCertainty: state.source.versionCertainty };
|
|
7324
|
-
await
|
|
7667
|
+
await fs20.writeFile(manifestFile, JSON.stringify(manifest, null, 2) + "\n");
|
|
7325
7668
|
const possible = [];
|
|
7326
7669
|
const quoted = new RegExp(`(["'\`])(?:${state.source.slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${state.source.projectId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})\\1`);
|
|
7327
7670
|
const scan = async (dir) => {
|
|
7328
|
-
for (const entry of await
|
|
7671
|
+
for (const entry of await fs20.readdir(dir, { withFileTypes: true })) {
|
|
7329
7672
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
7330
|
-
const file =
|
|
7673
|
+
const file = path20.join(dir, entry.name);
|
|
7331
7674
|
if (entry.isDirectory()) await scan(file);
|
|
7332
|
-
else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|html)$/.test(entry.name) && (await
|
|
7675
|
+
else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|html)$/.test(entry.name) && (await fs20.stat(file)).size < 1024 * 1024 && quoted.test(await fs20.readFile(file, "utf8"))) possible.push(path20.relative(cwd, file));
|
|
7333
7676
|
}
|
|
7334
7677
|
};
|
|
7335
7678
|
await scan(cwd);
|
|
@@ -7354,7 +7697,7 @@ async function runRemix(opts, deps = {}) {
|
|
|
7354
7697
|
const apiUrl = getApiUrl(opts.apiUrl), dashboardUrl = new URL(getAuthUrl(opts.authUrl)).origin;
|
|
7355
7698
|
if (opts.sourceOnly && !opts.destination) await assertReferenceParents(originalCwd);
|
|
7356
7699
|
const selected = parseRemixSource(opts.remixSource, apiUrl, dashboardUrl);
|
|
7357
|
-
let directory = opts.destination ?
|
|
7700
|
+
let directory = opts.destination ? path20.resolve(originalCwd, opts.destination) : !opts.sourceOnly ? path20.resolve(originalCwd, `${selected.slug}-remix`) : void 0;
|
|
7358
7701
|
if (directory) state = await checkDestination(directory);
|
|
7359
7702
|
resumed = state !== null;
|
|
7360
7703
|
if (state && (state.apiUrl !== apiUrl || state.sourceOnly !== Boolean(opts.sourceOnly) || ![state.requestedSlug, state.source.slug].includes(selected.slug) || opts.operationId && state.operationId !== opts.operationId || opts.name && state.name !== opts.name || selected.version && selected.version !== state.source.version)) throw new RemixError("operation_mismatch", "This folder belongs to a different remix operation. Resume with its original source/options or choose a fresh folder.");
|
|
@@ -7372,29 +7715,29 @@ async function runRemix(opts, deps = {}) {
|
|
|
7372
7715
|
log.step(`Resolving source for ${selected.slug}\u2026`);
|
|
7373
7716
|
const body = await responseJson(await request(`${apiUrl}/api/projects/${encodeURIComponent(selected.slug)}/source?${query}`, { headers, redirect: "error" }));
|
|
7374
7717
|
validateSource(body, Boolean(opts.sourceOnly));
|
|
7375
|
-
directory ??=
|
|
7718
|
+
directory ??= path20.resolve(originalCwd, ".genex", "refs", `${body.source.slug}-${body.source.sourceCommitSha.slice(0, 12)}`);
|
|
7376
7719
|
const existing = await checkDestination(directory);
|
|
7377
7720
|
if (existing) {
|
|
7378
7721
|
if (!existing.sourceOnly || !opts.sourceOnly || existing.apiUrl !== apiUrl || existing.source.sourceCommitSha !== body.source.sourceCommitSha || existing.source.projectId !== body.source.projectId) throw new RemixError("destination_occupied", "This reference destination already belongs to another operation. Choose a fresh folder.");
|
|
7379
7722
|
state = existing;
|
|
7380
7723
|
resumed = true;
|
|
7381
7724
|
} else {
|
|
7382
|
-
state = { schema: 1, operationId: opts.operationId ??
|
|
7383
|
-
await
|
|
7384
|
-
if ((await
|
|
7385
|
-
await
|
|
7725
|
+
state = { schema: 1, operationId: opts.operationId ?? crypto6.randomUUID(), directory, apiUrl, dashboardUrl, requestedSlug: selected.slug, source: body.source, sourceToken: body.sourceToken, sourceOnly: Boolean(opts.sourceOnly), cliVersion: getCliVersion(), name: opts.name?.trim() || `${body.source.title.slice(0, 74)} remix`, stage: "download", warnings: [...body.source.warnings] };
|
|
7726
|
+
await fs20.mkdir(directory, { recursive: true });
|
|
7727
|
+
if ((await fs20.readdir(directory)).length) throw new RemixError("destination_occupied", "The destination changed during setup. Choose a fresh folder.");
|
|
7728
|
+
await fs20.mkdir(path20.join(directory, ".genex"), { mode: 448 });
|
|
7386
7729
|
await saveState(state);
|
|
7387
7730
|
}
|
|
7388
7731
|
}
|
|
7389
7732
|
unlock = await acquireLock(state.directory);
|
|
7390
7733
|
state = await readState(state.directory) ?? state;
|
|
7391
7734
|
if (resumed && state.stage === "download") await renewSource(state, request, headers);
|
|
7392
|
-
const staging =
|
|
7735
|
+
const staging = path20.join(state.directory, ".genex", "remix-stage");
|
|
7393
7736
|
if (state.stage === "download") {
|
|
7394
|
-
await
|
|
7395
|
-
await
|
|
7396
|
-
const archive =
|
|
7397
|
-
await
|
|
7737
|
+
await fs20.rm(staging, { recursive: true, force: true });
|
|
7738
|
+
await fs20.mkdir(staging, { recursive: true });
|
|
7739
|
+
const archive = path20.join(staging, "source.zip"), sourceDir = path20.join(staging, "source");
|
|
7740
|
+
await fs20.mkdir(sourceDir);
|
|
7398
7741
|
const archiveUrl = new URL(state.source.archiveUrl, state.apiUrl);
|
|
7399
7742
|
if (archiveUrl.origin !== new URL(state.apiUrl).origin || !archiveUrl.pathname.startsWith("/api/projects/")) throw new RemixError("invalid_source_response", "Source archive must be served by the selected Genex API.");
|
|
7400
7743
|
log.step(`Downloading ${state.source.slug} at ${state.source.sourceCommitSha.slice(0, 12)}\u2026`);
|
|
@@ -7409,26 +7752,26 @@ async function runRemix(opts, deps = {}) {
|
|
|
7409
7752
|
if (!state.sourceOnly) {
|
|
7410
7753
|
let pkg;
|
|
7411
7754
|
try {
|
|
7412
|
-
pkg = JSON.parse(await
|
|
7755
|
+
pkg = JSON.parse(await fs20.readFile(path20.join(sourceDir, "package.json"), "utf8"));
|
|
7413
7756
|
} catch (error) {
|
|
7414
|
-
const staticEntry = await
|
|
7757
|
+
const staticEntry = await fs20.stat(path20.join(sourceDir, "index.html")).catch(() => null);
|
|
7415
7758
|
if (error.code !== "ENOENT" || !staticEntry?.isFile()) throw new RemixError("source_unavailable", "Source has neither a readable package.json nor a static index.html. Use --source-only to inspect it.");
|
|
7416
7759
|
pkg = { name: manifestName(state.directory), private: true };
|
|
7417
|
-
await
|
|
7760
|
+
await fs20.writeFile(path20.join(sourceDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
7418
7761
|
state.warnings.push("This static game had no package.json; added a minimal private manifest for the local CLI. Its source and index.html remain unchanged.");
|
|
7419
7762
|
}
|
|
7420
7763
|
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) throw new RemixError("source_unavailable", "Source package.json is not a project manifest.");
|
|
7421
7764
|
}
|
|
7422
|
-
state.transferEntries = await
|
|
7765
|
+
state.transferEntries = await fs20.readdir(sourceDir);
|
|
7423
7766
|
state.stage = "transfer";
|
|
7424
7767
|
await saveState(state);
|
|
7425
7768
|
}
|
|
7426
7769
|
if (state.stage === "transfer") {
|
|
7427
7770
|
for (const entry of state.transferEntries ?? []) {
|
|
7428
|
-
const from =
|
|
7429
|
-
if (await
|
|
7430
|
-
if (await
|
|
7431
|
-
await
|
|
7771
|
+
const from = path20.join(staging, "source", entry), to = path20.join(state.directory, entry);
|
|
7772
|
+
if (await fs20.lstat(from).catch(() => null)) {
|
|
7773
|
+
if (await fs20.lstat(to).catch(() => null)) throw new RemixError("destination_changed", `A file appeared at ${entry} during source preparation. It was left untouched.`);
|
|
7774
|
+
await fs20.rename(from, to);
|
|
7432
7775
|
}
|
|
7433
7776
|
}
|
|
7434
7777
|
if (!state.sourceOnly) {
|
|
@@ -7437,7 +7780,7 @@ async function runRemix(opts, deps = {}) {
|
|
|
7437
7780
|
}
|
|
7438
7781
|
state.stage = state.sourceOnly ? "prepared" : "configure";
|
|
7439
7782
|
await saveState(state);
|
|
7440
|
-
await
|
|
7783
|
+
await fs20.rm(staging, { recursive: true, force: true });
|
|
7441
7784
|
}
|
|
7442
7785
|
process.chdir(state.directory);
|
|
7443
7786
|
if (state.stage === "configure") {
|
|
@@ -7508,18 +7851,18 @@ async function runRemix(opts, deps = {}) {
|
|
|
7508
7851
|
}
|
|
7509
7852
|
|
|
7510
7853
|
// src/commands/init.ts
|
|
7511
|
-
import
|
|
7512
|
-
import
|
|
7854
|
+
import fs24 from "fs/promises";
|
|
7855
|
+
import path23 from "path";
|
|
7513
7856
|
|
|
7514
7857
|
// src/lib/item-drafts.ts
|
|
7515
7858
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
7516
7859
|
import { constants as fsConstants2 } from "fs";
|
|
7517
|
-
import
|
|
7860
|
+
import fs23 from "fs/promises";
|
|
7518
7861
|
import os6 from "os";
|
|
7519
|
-
import
|
|
7862
|
+
import path22 from "path";
|
|
7520
7863
|
|
|
7521
7864
|
// src/lib/item-media.ts
|
|
7522
|
-
import
|
|
7865
|
+
import fs21 from "fs/promises";
|
|
7523
7866
|
var MIB = 1024 * 1024;
|
|
7524
7867
|
var MediaKind = { Picture: "picture", Model: "model" };
|
|
7525
7868
|
var MediaContentType = {
|
|
@@ -7553,7 +7896,7 @@ async function readItemMedia(kind, file) {
|
|
|
7553
7896
|
}
|
|
7554
7897
|
let size;
|
|
7555
7898
|
try {
|
|
7556
|
-
const stat = await
|
|
7899
|
+
const stat = await fs21.stat(file);
|
|
7557
7900
|
if (!stat.isFile()) throw new Error("not a file");
|
|
7558
7901
|
size = stat.size;
|
|
7559
7902
|
} catch {
|
|
@@ -7563,7 +7906,7 @@ async function readItemMedia(kind, file) {
|
|
|
7563
7906
|
if (size > max) {
|
|
7564
7907
|
throw new ItemMediaError(`${file} is ${Math.ceil(size / MIB * 10) / 10} MB; ${kind === MediaKind.Picture ? "a picture" : "a model"} may be at most ${max / MIB} MB.`);
|
|
7565
7908
|
}
|
|
7566
|
-
const bytes = await
|
|
7909
|
+
const bytes = await fs21.readFile(file);
|
|
7567
7910
|
const contentType = mediaContentType(kind, bytes);
|
|
7568
7911
|
if (!contentType) {
|
|
7569
7912
|
throw new ItemMediaError(
|
|
@@ -7576,8 +7919,8 @@ async function readItemMedia(kind, file) {
|
|
|
7576
7919
|
// src/lib/item-package.ts
|
|
7577
7920
|
import { createHash } from "crypto";
|
|
7578
7921
|
import { constants as fsConstants } from "fs";
|
|
7579
|
-
import
|
|
7580
|
-
import
|
|
7922
|
+
import fs22 from "fs/promises";
|
|
7923
|
+
import path21 from "path";
|
|
7581
7924
|
import zlib from "zlib";
|
|
7582
7925
|
var MIB2 = 1024 * 1024;
|
|
7583
7926
|
var ITEM_PACKAGE_BYTES_MAX = 50 * MIB2;
|
|
@@ -7656,16 +7999,16 @@ async function packItemDirectory(dir) {
|
|
|
7656
7999
|
}
|
|
7657
8000
|
async function realDirectory(dir) {
|
|
7658
8001
|
try {
|
|
7659
|
-
const root = await
|
|
7660
|
-
if ((await
|
|
8002
|
+
const root = await fs22.realpath(dir);
|
|
8003
|
+
if ((await fs22.stat(root)).isDirectory()) return root;
|
|
7661
8004
|
} catch {
|
|
7662
8005
|
}
|
|
7663
8006
|
throw new ItemPackageError(PackageErrorCode.DirMissing, `${dir} is not a folder.`);
|
|
7664
8007
|
}
|
|
7665
8008
|
async function insideRoot(root, candidate) {
|
|
7666
8009
|
try {
|
|
7667
|
-
const real = await
|
|
7668
|
-
return real === root || real.startsWith(root +
|
|
8010
|
+
const real = await fs22.realpath(candidate);
|
|
8011
|
+
return real === root || real.startsWith(root + path21.sep);
|
|
7669
8012
|
} catch {
|
|
7670
8013
|
return false;
|
|
7671
8014
|
}
|
|
@@ -7681,10 +8024,10 @@ function portableName(relPath) {
|
|
|
7681
8024
|
}
|
|
7682
8025
|
var byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
7683
8026
|
async function collect(root, abs, rel, out) {
|
|
7684
|
-
const entries = (await
|
|
8027
|
+
const entries = (await fs22.readdir(abs, { withFileTypes: true })).sort(byName);
|
|
7685
8028
|
for (const entry of entries) {
|
|
7686
8029
|
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
7687
|
-
const full =
|
|
8030
|
+
const full = path21.join(abs, entry.name);
|
|
7688
8031
|
const excluded = packExclusion(relPath, entry.name);
|
|
7689
8032
|
if (excluded) {
|
|
7690
8033
|
out.skipped.push({ path: relPath, reason: excluded });
|
|
@@ -7722,10 +8065,10 @@ async function collect(root, abs, rel, out) {
|
|
|
7722
8065
|
}
|
|
7723
8066
|
}
|
|
7724
8067
|
async function readRegularFile(root, full) {
|
|
7725
|
-
if (!await insideRoot(root,
|
|
8068
|
+
if (!await insideRoot(root, path21.dirname(full))) return PackSkip.Symlink;
|
|
7726
8069
|
let handle;
|
|
7727
8070
|
try {
|
|
7728
|
-
handle = await
|
|
8071
|
+
handle = await fs22.open(full, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
7729
8072
|
} catch {
|
|
7730
8073
|
return PackSkip.Symlink;
|
|
7731
8074
|
}
|
|
@@ -7933,14 +8276,14 @@ function entryBytes(zip, entry) {
|
|
|
7933
8276
|
if (sourceCrc32(data) !== entry.crc) invalid(`${entry.path} fails its checksum.`, entry.path);
|
|
7934
8277
|
return data;
|
|
7935
8278
|
}
|
|
7936
|
-
var
|
|
8279
|
+
var sha2563 = (data) => createHash("sha256").update(data).digest("hex");
|
|
7937
8280
|
function checkAgainstManifest(zip, files, manifest) {
|
|
7938
8281
|
const expected = new Map(manifest.files.map((f) => [f.path, f]));
|
|
7939
8282
|
if (expected.size !== manifest.files.length) throw new ItemPackageError(PackageErrorCode.Mismatch, "The package's manifest lists a file twice.");
|
|
7940
8283
|
for (const file of files) {
|
|
7941
8284
|
const want = expected.get(file.path);
|
|
7942
8285
|
if (!want) throw new ItemPackageError(PackageErrorCode.Mismatch, `${file.path} is in the package but not in its manifest.`, file.path);
|
|
7943
|
-
if (want.bytes !== file.size || want.sha256.toLowerCase() !==
|
|
8286
|
+
if (want.bytes !== file.size || want.sha256.toLowerCase() !== sha2563(entryBytes(zip, file))) {
|
|
7944
8287
|
throw new ItemPackageError(PackageErrorCode.Mismatch, `${file.path} is not the file the manifest recorded.`, file.path);
|
|
7945
8288
|
}
|
|
7946
8289
|
expected.delete(file.path);
|
|
@@ -7959,7 +8302,7 @@ async function extractItemPackage(zip, dest, manifest) {
|
|
|
7959
8302
|
for (const file of files) entryBytes(zip, file);
|
|
7960
8303
|
if (manifest) checkAgainstManifest(zip, files, manifest);
|
|
7961
8304
|
try {
|
|
7962
|
-
await
|
|
8305
|
+
await fs22.mkdir(dest);
|
|
7963
8306
|
} catch (err) {
|
|
7964
8307
|
if (err.code === "EEXIST") {
|
|
7965
8308
|
throw new ItemPackageError(PackageErrorCode.DestinationExists, `${dest} already exists.`);
|
|
@@ -7967,13 +8310,13 @@ async function extractItemPackage(zip, dest, manifest) {
|
|
|
7967
8310
|
throw err;
|
|
7968
8311
|
}
|
|
7969
8312
|
for (const entry of entries) {
|
|
7970
|
-
const target =
|
|
8313
|
+
const target = path21.join(dest, ...entry.path.split("/"));
|
|
7971
8314
|
if (entry.dir) {
|
|
7972
|
-
await
|
|
8315
|
+
await fs22.mkdir(target, { recursive: true });
|
|
7973
8316
|
continue;
|
|
7974
8317
|
}
|
|
7975
|
-
await
|
|
7976
|
-
await
|
|
8318
|
+
await fs22.mkdir(path21.dirname(target), { recursive: true });
|
|
8319
|
+
await fs22.writeFile(target, entryBytes(zip, entry), { flag: "wx", mode: FILE_MODE });
|
|
7977
8320
|
}
|
|
7978
8321
|
return files.map((f) => f.path);
|
|
7979
8322
|
}
|
|
@@ -8200,7 +8543,7 @@ function isLocalId(value) {
|
|
|
8200
8543
|
function newLocalId() {
|
|
8201
8544
|
return `${LOCAL_ID_PREFIX}${randomBytes(6).toString("hex")}`;
|
|
8202
8545
|
}
|
|
8203
|
-
var
|
|
8546
|
+
var sha2564 = (bytes) => createHash2("sha256").update(bytes).digest("hex");
|
|
8204
8547
|
function contentHash(fields, files) {
|
|
8205
8548
|
const canonical = JSON.stringify({
|
|
8206
8549
|
description: fields.description ?? null,
|
|
@@ -8213,9 +8556,9 @@ function contentHash(fields, files) {
|
|
|
8213
8556
|
const parts = [canonical];
|
|
8214
8557
|
for (const kind of FILE_ORDER) {
|
|
8215
8558
|
const bytes = files[kind];
|
|
8216
|
-
if (bytes) parts.push(`${kind}:${
|
|
8559
|
+
if (bytes) parts.push(`${kind}:${sha2564(bytes)}`);
|
|
8217
8560
|
}
|
|
8218
|
-
return
|
|
8561
|
+
return sha2564(parts.join("\n"));
|
|
8219
8562
|
}
|
|
8220
8563
|
function draftLine(draft) {
|
|
8221
8564
|
const { record } = draft;
|
|
@@ -8245,13 +8588,13 @@ var machineFolder = () => new LocalDraftError(
|
|
|
8245
8588
|
"This is the home folder: its .genex is this machine's Genex folder (sign-in and caches), not a game's. Run it in the game's folder; nothing was read or written."
|
|
8246
8589
|
);
|
|
8247
8590
|
async function isHomeFolder(cwd) {
|
|
8248
|
-
const [here, home] = await Promise.all([
|
|
8591
|
+
const [here, home] = await Promise.all([fs23.realpath(cwd), fs23.realpath(os6.homedir()).catch(() => path22.resolve(os6.homedir()))]);
|
|
8249
8592
|
return here === home;
|
|
8250
8593
|
}
|
|
8251
|
-
var
|
|
8594
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8252
8595
|
async function lstatOrNull(full) {
|
|
8253
8596
|
try {
|
|
8254
|
-
return await
|
|
8597
|
+
return await fs23.lstat(full);
|
|
8255
8598
|
} catch (err) {
|
|
8256
8599
|
if (err.code === "ENOENT") return null;
|
|
8257
8600
|
throw err;
|
|
@@ -8259,8 +8602,8 @@ async function lstatOrNull(full) {
|
|
|
8259
8602
|
}
|
|
8260
8603
|
var isRealFolder = (stat) => stat !== null && stat.isDirectory() && !stat.isSymbolicLink();
|
|
8261
8604
|
async function itemsFolder(cwd, create) {
|
|
8262
|
-
const genex =
|
|
8263
|
-
const items =
|
|
8605
|
+
const genex = path22.join(cwd, ".genex");
|
|
8606
|
+
const items = path22.join(genex, "items");
|
|
8264
8607
|
if (await isHomeFolder(cwd)) {
|
|
8265
8608
|
if (!create && !await lstatOrNull(items)) return null;
|
|
8266
8609
|
throw machineFolder();
|
|
@@ -8269,22 +8612,22 @@ async function itemsFolder(cwd, create) {
|
|
|
8269
8612
|
let stat = await lstatOrNull(dir);
|
|
8270
8613
|
if (!stat && !create) return null;
|
|
8271
8614
|
if (!stat) {
|
|
8272
|
-
await
|
|
8615
|
+
await fs23.mkdir(dir).catch((err) => {
|
|
8273
8616
|
if (err.code !== "EEXIST") throw err;
|
|
8274
8617
|
});
|
|
8275
8618
|
stat = await lstatOrNull(dir);
|
|
8276
8619
|
}
|
|
8277
8620
|
if (!isRealFolder(stat)) throw folderRefused(rel);
|
|
8278
8621
|
}
|
|
8279
|
-
const [root, real] = await Promise.all([
|
|
8280
|
-
if (real !==
|
|
8622
|
+
const [root, real] = await Promise.all([fs23.realpath(cwd), fs23.realpath(items)]);
|
|
8623
|
+
if (real !== path22.join(root, ".genex", "items")) throw folderRefused(LOCAL_ITEMS_DIR);
|
|
8281
8624
|
return items;
|
|
8282
8625
|
}
|
|
8283
8626
|
async function draftFolder(items, localId, create) {
|
|
8284
|
-
const dir =
|
|
8627
|
+
const dir = path22.join(items, localId);
|
|
8285
8628
|
let stat = await lstatOrNull(dir);
|
|
8286
8629
|
if (!stat && create) {
|
|
8287
|
-
await
|
|
8630
|
+
await fs23.mkdir(dir).catch((err) => {
|
|
8288
8631
|
if (err.code !== "EEXIST") throw err;
|
|
8289
8632
|
});
|
|
8290
8633
|
stat = await lstatOrNull(dir);
|
|
@@ -8307,7 +8650,7 @@ async function readStored(full, max) {
|
|
|
8307
8650
|
if (stat.isSymbolicLink()) throw new StoredFileError(ReadRefusal.Link);
|
|
8308
8651
|
let handle;
|
|
8309
8652
|
try {
|
|
8310
|
-
handle = await
|
|
8653
|
+
handle = await fs23.open(full, fsConstants2.O_RDONLY | (fsConstants2.O_NOFOLLOW ?? 0));
|
|
8311
8654
|
} catch (err) {
|
|
8312
8655
|
if (err.code === "ENOENT") throw new StoredFileError(ReadRefusal.Missing);
|
|
8313
8656
|
throw new StoredFileError(ReadRefusal.Link);
|
|
@@ -8362,7 +8705,7 @@ function checkFields(raw, label) {
|
|
|
8362
8705
|
}
|
|
8363
8706
|
var RECORD_KEYS = /* @__PURE__ */ new Set(["localId", "name", "description", "priceCoins", "type", "tags", "shareable", "files", "createdAt", "updatedAt"]);
|
|
8364
8707
|
function checkFileNames(files, label) {
|
|
8365
|
-
if (!
|
|
8708
|
+
if (!isRecord2(files)) throw invalid2(`${label} is not a draft this CLI wrote: its files are not a list of names.`);
|
|
8366
8709
|
const out = {};
|
|
8367
8710
|
for (const [kind, name] of Object.entries(files)) {
|
|
8368
8711
|
if (!FILE_ORDER.includes(kind)) throw invalid2(`${label} names a file kind no draft has (${JSON.stringify(kind.slice(0, 40))}).`);
|
|
@@ -8376,7 +8719,7 @@ function checkFileNames(files, label) {
|
|
|
8376
8719
|
}
|
|
8377
8720
|
function parseDraftRecord(raw, fileId) {
|
|
8378
8721
|
const label = `${LOCAL_ITEMS_DIR}/${fileId}.json`;
|
|
8379
|
-
if (!
|
|
8722
|
+
if (!isRecord2(raw)) throw invalid2(`${label} is not a draft this CLI wrote: it is not an object.`);
|
|
8380
8723
|
const extra = Object.keys(raw).find((key) => !RECORD_KEYS.has(key));
|
|
8381
8724
|
if (extra !== void 0) throw invalid2(`${label} is not a draft this CLI wrote: it has a field no draft has (${JSON.stringify(extra.slice(0, 40))}).`);
|
|
8382
8725
|
if (raw.localId !== fileId) throw invalid2(`${label} is not a draft this CLI wrote: its localId is not its file name.`);
|
|
@@ -8409,7 +8752,7 @@ async function readRecord2(items, localId) {
|
|
|
8409
8752
|
const rel = `${LOCAL_ITEMS_DIR}/${localId}.json`;
|
|
8410
8753
|
let text;
|
|
8411
8754
|
try {
|
|
8412
|
-
text = await readStored(
|
|
8755
|
+
text = await readStored(path22.join(items, `${localId}.json`), DRAFT_FILE_BYTES_MAX);
|
|
8413
8756
|
} catch (err) {
|
|
8414
8757
|
if (!(err instanceof StoredFileError)) throw err;
|
|
8415
8758
|
if (err.reason === ReadRefusal.Missing) throw notFound(localId);
|
|
@@ -8434,7 +8777,7 @@ async function readDraft(items, localId) {
|
|
|
8434
8777
|
const rel = `${LOCAL_ITEMS_DIR}/${localId}/${name}`;
|
|
8435
8778
|
let bytes;
|
|
8436
8779
|
try {
|
|
8437
|
-
bytes = await readStored(
|
|
8780
|
+
bytes = await readStored(path22.join(dir, name), FILE_BYTES_MAX[kind]);
|
|
8438
8781
|
} catch (err) {
|
|
8439
8782
|
if (err instanceof StoredFileError) throw unreadable(rel, err.reason, FILE_BYTES_MAX[kind]);
|
|
8440
8783
|
throw err;
|
|
@@ -8453,7 +8796,7 @@ async function loadLocalDraft(cwd, localId) {
|
|
|
8453
8796
|
}
|
|
8454
8797
|
var isDraftFileName = (name) => name.endsWith(".json") && name !== LAST_SYNC_FILE;
|
|
8455
8798
|
async function draftFileNames(items) {
|
|
8456
|
-
return (await
|
|
8799
|
+
return (await fs23.readdir(items)).filter(isDraftFileName).sort();
|
|
8457
8800
|
}
|
|
8458
8801
|
async function holdsDraftFiles(items) {
|
|
8459
8802
|
return (await draftFileNames(items)).length > 0;
|
|
@@ -8489,12 +8832,12 @@ async function listLocalDrafts(cwd) {
|
|
|
8489
8832
|
return { drafts, refused };
|
|
8490
8833
|
}
|
|
8491
8834
|
async function writeReplacing(target, bytes) {
|
|
8492
|
-
const temporary =
|
|
8493
|
-
await
|
|
8835
|
+
const temporary = path22.join(path22.dirname(target), `.${path22.basename(target)}.${randomBytes(4).toString("hex")}.tmp`);
|
|
8836
|
+
await fs23.writeFile(temporary, bytes, { flag: "wx", mode: 420 });
|
|
8494
8837
|
try {
|
|
8495
|
-
await
|
|
8838
|
+
await fs23.rename(temporary, target);
|
|
8496
8839
|
} catch (err) {
|
|
8497
|
-
await
|
|
8840
|
+
await fs23.rm(temporary, { force: true });
|
|
8498
8841
|
throw err;
|
|
8499
8842
|
}
|
|
8500
8843
|
}
|
|
@@ -8507,13 +8850,13 @@ async function storeDraft(items, record, newFiles) {
|
|
|
8507
8850
|
if (newFiles.package) incoming.push([DraftFileKind.Package, STORED_NAMES.package[0] ?? "", newFiles.package]);
|
|
8508
8851
|
for (const [kind, name, bytes] of incoming) {
|
|
8509
8852
|
if (!STORED_NAMES[kind].includes(name)) throw invalid2(`A ${kind} of that type cannot be stored with a draft.`);
|
|
8510
|
-
await writeReplacing(
|
|
8853
|
+
await writeReplacing(path22.join(dir, name), bytes);
|
|
8511
8854
|
const previous = files[kind];
|
|
8512
|
-
if (previous && previous !== name) await
|
|
8855
|
+
if (previous && previous !== name) await fs23.rm(path22.join(dir, previous), { force: true });
|
|
8513
8856
|
files[kind] = name;
|
|
8514
8857
|
}
|
|
8515
8858
|
const stored = { ...record, files };
|
|
8516
|
-
await writeReplacing(
|
|
8859
|
+
await writeReplacing(path22.join(items, `${record.localId}.json`), `${JSON.stringify(orderedRecord(stored), null, 2)}
|
|
8517
8860
|
`);
|
|
8518
8861
|
return readDraft(items, record.localId);
|
|
8519
8862
|
}
|
|
@@ -8536,7 +8879,7 @@ async function createLocalDraft(cwd, fields, files, now = /* @__PURE__ */ new Da
|
|
|
8536
8879
|
if (!items) throw folderRefused(LOCAL_ITEMS_DIR);
|
|
8537
8880
|
for (let attempt = 0; attempt < NEW_ID_ATTEMPTS; attempt++) {
|
|
8538
8881
|
const localId = newLocalId();
|
|
8539
|
-
const taken = await lstatOrNull(
|
|
8882
|
+
const taken = await lstatOrNull(path22.join(items, `${localId}.json`)) ?? await lstatOrNull(path22.join(items, localId));
|
|
8540
8883
|
if (taken) continue;
|
|
8541
8884
|
const stamp = now.toISOString();
|
|
8542
8885
|
return storeDraft(items, { localId, ...fields, files: {}, createdAt: stamp, updatedAt: stamp }, files);
|
|
@@ -8558,26 +8901,26 @@ async function removeLocalDraft(cwd, localId) {
|
|
|
8558
8901
|
}
|
|
8559
8902
|
async function removeFrom(items, localId) {
|
|
8560
8903
|
const record = await readRecord2(items, localId);
|
|
8561
|
-
const dir =
|
|
8904
|
+
const dir = path22.join(items, localId);
|
|
8562
8905
|
const stat = await lstatOrNull(dir);
|
|
8563
8906
|
if (stat && !isRealFolder(stat)) throw folderRefused(`${LOCAL_ITEMS_DIR}/${localId}`);
|
|
8564
|
-
await
|
|
8565
|
-
if (stat) await
|
|
8907
|
+
await fs23.rm(path22.join(items, `${localId}.json`), { force: true });
|
|
8908
|
+
if (stat) await fs23.rm(dir, { recursive: true, force: true });
|
|
8566
8909
|
return record;
|
|
8567
8910
|
}
|
|
8568
8911
|
async function removeSyncedDraft(items, localId) {
|
|
8569
8912
|
await removeFrom(items, localId);
|
|
8570
8913
|
}
|
|
8571
8914
|
async function writeLastSync(items, document) {
|
|
8572
|
-
await writeReplacing(
|
|
8915
|
+
await writeReplacing(path22.join(items, LAST_SYNC_FILE), `${JSON.stringify(document)}
|
|
8573
8916
|
`);
|
|
8574
8917
|
}
|
|
8575
8918
|
async function takeSyncLock(items, now = Date.now()) {
|
|
8576
|
-
const lock =
|
|
8919
|
+
const lock = path22.join(items, SYNC_LOCK_FILE);
|
|
8577
8920
|
const body = JSON.stringify({ pid: process.pid, at: new Date(now).toISOString() });
|
|
8578
8921
|
const create = async () => {
|
|
8579
8922
|
try {
|
|
8580
|
-
await
|
|
8923
|
+
await fs23.writeFile(lock, body, { flag: "wx", mode: 420 });
|
|
8581
8924
|
return true;
|
|
8582
8925
|
} catch (err) {
|
|
8583
8926
|
if (err.code === "EEXIST") return false;
|
|
@@ -8588,11 +8931,11 @@ async function takeSyncLock(items, now = Date.now()) {
|
|
|
8588
8931
|
const held = await lstatOrNull(lock);
|
|
8589
8932
|
const fresh = held !== null && held.isFile() && !held.isSymbolicLink() && now - held.mtimeMs < SYNC_LOCK_STALE_MS;
|
|
8590
8933
|
if (fresh) return null;
|
|
8591
|
-
await
|
|
8934
|
+
await fs23.rm(lock, { force: true });
|
|
8592
8935
|
if (!await create()) return null;
|
|
8593
8936
|
}
|
|
8594
8937
|
return async () => {
|
|
8595
|
-
await
|
|
8938
|
+
await fs23.rm(lock, { force: true });
|
|
8596
8939
|
};
|
|
8597
8940
|
}
|
|
8598
8941
|
|
|
@@ -8611,7 +8954,7 @@ function isTransferUrl(url) {
|
|
|
8611
8954
|
if (parsed.protocol === "https:") return true;
|
|
8612
8955
|
return parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname);
|
|
8613
8956
|
}
|
|
8614
|
-
async function
|
|
8957
|
+
async function putSigned2(uploadUrl, bytes, contentType) {
|
|
8615
8958
|
if (!isTransferUrl(uploadUrl)) throw new Error("The upload link Genex returned isn't an https URL.");
|
|
8616
8959
|
const res = await fetch(uploadUrl, {
|
|
8617
8960
|
method: "PUT",
|
|
@@ -8666,7 +9009,7 @@ async function uploadItemPart(call3, skuUrl2, part) {
|
|
|
8666
9009
|
if (!urlRes.ok) return { ok: false, refused: urlRes, what: `upload the ${part.kind}` };
|
|
8667
9010
|
const { uploadUrl, key } = await urlRes.json();
|
|
8668
9011
|
try {
|
|
8669
|
-
await
|
|
9012
|
+
await putSigned2(uploadUrl, part.bytes, part.contentType);
|
|
8670
9013
|
} catch (err) {
|
|
8671
9014
|
return { ok: false, putFailed: err instanceof Error ? err.message : String(err) };
|
|
8672
9015
|
}
|
|
@@ -8970,7 +9313,7 @@ var TOOLING_ENTRIES = /* @__PURE__ */ new Set([
|
|
|
8970
9313
|
]);
|
|
8971
9314
|
async function listPreexistingEntries(dir) {
|
|
8972
9315
|
try {
|
|
8973
|
-
return (await
|
|
9316
|
+
return (await fs24.readdir(dir)).filter((name) => !TOOLING_ENTRIES.has(name)).sort();
|
|
8974
9317
|
} catch {
|
|
8975
9318
|
return [];
|
|
8976
9319
|
}
|
|
@@ -9038,8 +9381,8 @@ async function runInit(opts) {
|
|
|
9038
9381
|
let totalUpdated = 0;
|
|
9039
9382
|
const refused = [];
|
|
9040
9383
|
for (const t of targets) {
|
|
9041
|
-
const src = t.full ? templatesDir :
|
|
9042
|
-
const dest = t.full ? t.baseDir :
|
|
9384
|
+
const src = t.full ? templatesDir : path23.join(templatesDir, "skills");
|
|
9385
|
+
const dest = t.full ? t.baseDir : path23.join(t.baseDir, "skills");
|
|
9043
9386
|
const family = remixing ? skillFamilyFilter("remix") : converting ? skillFamilyFilter("tools", { hosted: true }) : skillFamilyFilter("game");
|
|
9044
9387
|
let copied;
|
|
9045
9388
|
let updated;
|
|
@@ -9050,8 +9393,8 @@ async function runInit(opts) {
|
|
|
9050
9393
|
exclude: ["controllers", "motion", "asset-viewer", "blender-service"],
|
|
9051
9394
|
filter: t.full ? family : (rel) => family(`skills/${rel}`)
|
|
9052
9395
|
}));
|
|
9053
|
-
await pruneRemovedSkills(
|
|
9054
|
-
await writeSkillsMarker(
|
|
9396
|
+
await pruneRemovedSkills(path23.join(t.baseDir, "skills"), log);
|
|
9397
|
+
await writeSkillsMarker(path23.join(t.baseDir, "skills"));
|
|
9055
9398
|
} catch (err) {
|
|
9056
9399
|
const refusal2 = explainFsPermission(err, dest);
|
|
9057
9400
|
if (!refusal2) throw err;
|
|
@@ -9121,7 +9464,7 @@ async function runInit(opts) {
|
|
|
9121
9464
|
if (email) log.plain(` signed in as ${c.cyan(email)}`);
|
|
9122
9465
|
};
|
|
9123
9466
|
await echoIdentity();
|
|
9124
|
-
const projectName = opts.name?.trim() ||
|
|
9467
|
+
const projectName = opts.name?.trim() || path23.basename(process.cwd());
|
|
9125
9468
|
const create = (bearer) => createDraftProject({
|
|
9126
9469
|
apiUrl,
|
|
9127
9470
|
token: bearer,
|
|
@@ -9173,7 +9516,7 @@ async function runInit(opts) {
|
|
|
9173
9516
|
if (await writeToolsContract(process.cwd(), { hosted: true })) {
|
|
9174
9517
|
log.dim(" wrote the Genex Tools publishing rules into AGENTS.md (managed block)");
|
|
9175
9518
|
}
|
|
9176
|
-
await ensureEnvVar(
|
|
9519
|
+
await ensureEnvVar(path23.join(process.cwd(), ".env"), "VITE_GENEX_SLUG", meta.slug, log);
|
|
9177
9520
|
await reportCliEvent(apiUrl, token, "tools_converted");
|
|
9178
9521
|
}
|
|
9179
9522
|
if (!remixing) warnPreexisting(log, preexisting);
|
|
@@ -9201,9 +9544,9 @@ async function runInit(opts) {
|
|
|
9201
9544
|
}
|
|
9202
9545
|
|
|
9203
9546
|
// src/commands/link.ts
|
|
9204
|
-
import
|
|
9547
|
+
import fs25 from "fs/promises";
|
|
9205
9548
|
import os7 from "os";
|
|
9206
|
-
import
|
|
9549
|
+
import path24 from "path";
|
|
9207
9550
|
async function runLink(opts) {
|
|
9208
9551
|
const log = createLogger({ quiet: opts.quiet });
|
|
9209
9552
|
log.plain(c.bold("genex link"));
|
|
@@ -9286,7 +9629,7 @@ async function runLink(opts) {
|
|
|
9286
9629
|
}
|
|
9287
9630
|
async function isEmptyDir(cwd) {
|
|
9288
9631
|
try {
|
|
9289
|
-
const entries = await
|
|
9632
|
+
const entries = await fs25.readdir(cwd);
|
|
9290
9633
|
return entries.every((e) => e === ".git" || e === ".genex" || e === ".DS_Store");
|
|
9291
9634
|
} catch {
|
|
9292
9635
|
return false;
|
|
@@ -9300,13 +9643,13 @@ async function downloadSource(apiUrl, token, project, log) {
|
|
|
9300
9643
|
return false;
|
|
9301
9644
|
}
|
|
9302
9645
|
log.step(`Downloading the ${grant.sourceRef === "preview" ? "draft" : "published"} source\u2026`);
|
|
9303
|
-
const staging = await
|
|
9304
|
-
const fresh =
|
|
9646
|
+
const staging = await fs25.mkdtemp(path24.join(os7.tmpdir(), "genex-link-"));
|
|
9647
|
+
const fresh = path24.join(staging, "source");
|
|
9305
9648
|
try {
|
|
9306
9649
|
if (!await cloneSource(grant, fresh, log)) return false;
|
|
9307
|
-
await
|
|
9308
|
-
for (const entry of await
|
|
9309
|
-
await
|
|
9650
|
+
await fs25.rm(path24.join(fresh, ".git"), { recursive: true, force: true });
|
|
9651
|
+
for (const entry of await fs25.readdir(fresh)) {
|
|
9652
|
+
await fs25.cp(path24.join(fresh, entry), path24.join(process.cwd(), entry), {
|
|
9310
9653
|
recursive: true,
|
|
9311
9654
|
force: true
|
|
9312
9655
|
});
|
|
@@ -9314,28 +9657,28 @@ async function downloadSource(apiUrl, token, project, log) {
|
|
|
9314
9657
|
log.success("Downloaded.");
|
|
9315
9658
|
return true;
|
|
9316
9659
|
} finally {
|
|
9317
|
-
await
|
|
9660
|
+
await fs25.rm(staging, { recursive: true, force: true }).catch(() => {
|
|
9318
9661
|
});
|
|
9319
9662
|
}
|
|
9320
9663
|
}
|
|
9321
9664
|
async function ensureSlugEnv(slug, log, cwd = process.cwd()) {
|
|
9322
|
-
const file =
|
|
9665
|
+
const file = path24.join(cwd, ".env");
|
|
9323
9666
|
let content;
|
|
9324
9667
|
try {
|
|
9325
|
-
content = await
|
|
9668
|
+
content = await fs25.readFile(file, "utf8");
|
|
9326
9669
|
} catch {
|
|
9327
9670
|
return;
|
|
9328
9671
|
}
|
|
9329
9672
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
9330
9673
|
const m = content.match(re);
|
|
9331
9674
|
if (!m) {
|
|
9332
|
-
await
|
|
9675
|
+
await fs25.appendFile(file, `${content.endsWith("\n") ? "" : "\n"}VITE_GENEX_SLUG=${slug}
|
|
9333
9676
|
`);
|
|
9334
9677
|
log.dim(` added VITE_GENEX_SLUG=${slug} to .env`);
|
|
9335
9678
|
return;
|
|
9336
9679
|
}
|
|
9337
9680
|
if (m[2].trim() === slug) return;
|
|
9338
|
-
await
|
|
9681
|
+
await fs25.writeFile(file, content.replace(re, `$1${slug}`));
|
|
9339
9682
|
log.dim(` updated .env: VITE_GENEX_SLUG=${slug} (was ${m[2].trim()})`);
|
|
9340
9683
|
}
|
|
9341
9684
|
async function fetchOwnProject(apiUrl, token, slug, log) {
|
|
@@ -9389,8 +9732,8 @@ async function listOwnSlugs(apiUrl, token, log) {
|
|
|
9389
9732
|
}
|
|
9390
9733
|
|
|
9391
9734
|
// src/commands/pull.ts
|
|
9392
|
-
import
|
|
9393
|
-
import
|
|
9735
|
+
import fs26 from "fs/promises";
|
|
9736
|
+
import path25 from "path";
|
|
9394
9737
|
import os8 from "os";
|
|
9395
9738
|
function isMachineLocal(entry) {
|
|
9396
9739
|
if (entry === ".genex" || entry === "node_modules" || entry === ".git") return true;
|
|
@@ -9450,23 +9793,23 @@ async function runPull(opts) {
|
|
|
9450
9793
|
process.exitCode = 1;
|
|
9451
9794
|
return;
|
|
9452
9795
|
}
|
|
9453
|
-
const staging = await
|
|
9454
|
-
const fresh =
|
|
9796
|
+
const staging = await fs26.mkdtemp(path25.join(os8.tmpdir(), "genex-pull-"));
|
|
9797
|
+
const fresh = path25.join(staging, "source");
|
|
9455
9798
|
try {
|
|
9456
9799
|
if (!await cloneSource(grant, fresh, log)) {
|
|
9457
9800
|
process.exitCode = 1;
|
|
9458
9801
|
return;
|
|
9459
9802
|
}
|
|
9460
|
-
await
|
|
9803
|
+
await fs26.rm(path25.join(fresh, ".git"), { recursive: true, force: true });
|
|
9461
9804
|
const kept = await keepReplaced(cwd, log);
|
|
9462
9805
|
await replaceTree(cwd, fresh);
|
|
9463
9806
|
if (kept) {
|
|
9464
9807
|
log.plain("");
|
|
9465
|
-
log.info(`What was here is kept at ${c.cyan(
|
|
9808
|
+
log.info(`What was here is kept at ${c.cyan(path25.relative(cwd, kept) || kept)}`);
|
|
9466
9809
|
log.dim(" Nothing was thrown away \u2014 re-apply from there, or delete it when you are done.");
|
|
9467
9810
|
}
|
|
9468
9811
|
} finally {
|
|
9469
|
-
await
|
|
9812
|
+
await fs26.rm(staging, { recursive: true, force: true }).catch(() => {
|
|
9470
9813
|
});
|
|
9471
9814
|
}
|
|
9472
9815
|
if (await hasRemixProfile(cwd)) await installRemixProfile(cwd, apiUrl, log);
|
|
@@ -9486,13 +9829,13 @@ async function runPull(opts) {
|
|
|
9486
9829
|
}
|
|
9487
9830
|
async function keepReplaced(cwd, log) {
|
|
9488
9831
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
9489
|
-
const dest =
|
|
9832
|
+
const dest = path25.join(cwd, ".genex", `replaced-${stamp}`);
|
|
9490
9833
|
try {
|
|
9491
|
-
const entries = (await
|
|
9834
|
+
const entries = (await fs26.readdir(cwd)).filter((e) => !isMachineLocal(e));
|
|
9492
9835
|
if (entries.length === 0) return null;
|
|
9493
|
-
await
|
|
9836
|
+
await fs26.mkdir(dest, { recursive: true });
|
|
9494
9837
|
for (const entry of entries) {
|
|
9495
|
-
await
|
|
9838
|
+
await fs26.cp(path25.join(cwd, entry), path25.join(dest, entry), { recursive: true });
|
|
9496
9839
|
}
|
|
9497
9840
|
return dest;
|
|
9498
9841
|
} catch (err) {
|
|
@@ -9502,19 +9845,19 @@ async function keepReplaced(cwd, log) {
|
|
|
9502
9845
|
}
|
|
9503
9846
|
}
|
|
9504
9847
|
async function replaceTree(dest, src) {
|
|
9505
|
-
for (const entry of await
|
|
9848
|
+
for (const entry of await fs26.readdir(dest)) {
|
|
9506
9849
|
if (isMachineLocal(entry)) continue;
|
|
9507
|
-
await
|
|
9850
|
+
await fs26.rm(path25.join(dest, entry), { recursive: true, force: true });
|
|
9508
9851
|
}
|
|
9509
|
-
for (const entry of await
|
|
9852
|
+
for (const entry of await fs26.readdir(src)) {
|
|
9510
9853
|
if (isMachineLocal(entry)) continue;
|
|
9511
|
-
await
|
|
9854
|
+
await fs26.cp(path25.join(src, entry), path25.join(dest, entry), { recursive: true });
|
|
9512
9855
|
}
|
|
9513
9856
|
}
|
|
9514
9857
|
|
|
9515
9858
|
// src/commands/rename.ts
|
|
9516
|
-
import
|
|
9517
|
-
import
|
|
9859
|
+
import fs27 from "fs/promises";
|
|
9860
|
+
import path26 from "path";
|
|
9518
9861
|
async function runRename(opts) {
|
|
9519
9862
|
const log = createLogger({ quiet: opts.quiet });
|
|
9520
9863
|
log.plain(c.bold("genex rename"));
|
|
@@ -9602,23 +9945,23 @@ async function runRename(opts) {
|
|
|
9602
9945
|
log.info("Run `genex preview` (or `publish`) to rebuild \u2014 the new slug is baked into the bundle.");
|
|
9603
9946
|
}
|
|
9604
9947
|
async function rewriteSlugEnv(from, to, log, cwd = process.cwd()) {
|
|
9605
|
-
const file =
|
|
9948
|
+
const file = path26.join(cwd, ".env");
|
|
9606
9949
|
let content;
|
|
9607
9950
|
try {
|
|
9608
|
-
content = await
|
|
9951
|
+
content = await fs27.readFile(file, "utf8");
|
|
9609
9952
|
} catch {
|
|
9610
9953
|
return;
|
|
9611
9954
|
}
|
|
9612
9955
|
const re = /^(\s*VITE_GENEX_SLUG=)(.*)$/m;
|
|
9613
9956
|
if (!re.test(content)) return;
|
|
9614
|
-
await
|
|
9957
|
+
await fs27.writeFile(file, content.replace(re, `$1${to}`));
|
|
9615
9958
|
log.dim(` .env: VITE_GENEX_SLUG=${to} (was ${from})`);
|
|
9616
9959
|
}
|
|
9617
9960
|
async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
9618
|
-
const file =
|
|
9961
|
+
const file = path26.join(cwd, "src", "genex.config.ts");
|
|
9619
9962
|
let content;
|
|
9620
9963
|
try {
|
|
9621
|
-
content = await
|
|
9964
|
+
content = await fs27.readFile(file, "utf8");
|
|
9622
9965
|
} catch {
|
|
9623
9966
|
return;
|
|
9624
9967
|
}
|
|
@@ -9628,7 +9971,7 @@ async function rewriteBakedSlug(from, to, log, cwd = process.cwd()) {
|
|
|
9628
9971
|
log.warn(` src/genex.config.ts has no "${from}" literal \u2014 check its slug by hand.`);
|
|
9629
9972
|
return;
|
|
9630
9973
|
}
|
|
9631
|
-
await
|
|
9974
|
+
await fs27.writeFile(file, content.replace(quoted, `"${to}"`));
|
|
9632
9975
|
log.dim(` src/genex.config.ts: baked slug -> ${to}`);
|
|
9633
9976
|
}
|
|
9634
9977
|
|
|
@@ -10558,9 +10901,9 @@ var ShopSession = class {
|
|
|
10558
10901
|
};
|
|
10559
10902
|
|
|
10560
10903
|
// src/commands/items.ts
|
|
10561
|
-
import
|
|
10562
|
-
import
|
|
10563
|
-
import
|
|
10904
|
+
import crypto7 from "crypto";
|
|
10905
|
+
import fs28 from "fs/promises";
|
|
10906
|
+
import path27 from "path";
|
|
10564
10907
|
var SUBS2 = ["search", "add", "remove", "list"];
|
|
10565
10908
|
var ITEMS_DIR = SHARED_ITEMS_DIR;
|
|
10566
10909
|
var REPLACED_DIR = ".genex/replaced-items";
|
|
@@ -10756,26 +11099,26 @@ var ItemsSession = class _ItemsSession {
|
|
|
10756
11099
|
*/
|
|
10757
11100
|
async writeItemFolder(skuId, itemFile, pkg) {
|
|
10758
11101
|
const root = process.cwd();
|
|
10759
|
-
const itemsDir =
|
|
11102
|
+
const itemsDir = path27.join(root, ITEMS_DIR);
|
|
10760
11103
|
await ensureRealDir(itemsDir, ITEMS_DIR);
|
|
10761
|
-
const final =
|
|
11104
|
+
const final = path27.join(itemsDir, skuId);
|
|
10762
11105
|
const existing = await lstatOrNull2(final);
|
|
10763
11106
|
if (existing && !existing.isDirectory()) {
|
|
10764
11107
|
throw new FolderRefused(`${ITEMS_DIR}/${skuId} is a link or a file, not a folder. Move it aside first; nothing was written through it.`);
|
|
10765
11108
|
}
|
|
10766
11109
|
const keep = existing ? await replacedItemsDir(root) : null;
|
|
10767
11110
|
const zip = pkg ? await downloadItemPackage(pkg.url) : null;
|
|
10768
|
-
const staging =
|
|
11111
|
+
const staging = path27.join(itemsDir, `.${skuId}.incoming-${crypto7.randomBytes(4).toString("hex")}`);
|
|
10769
11112
|
try {
|
|
10770
11113
|
const files = zip && pkg ? await extractItemPackage(zip, staging, pkg.manifest) : [];
|
|
10771
|
-
if (!zip) await
|
|
10772
|
-
await
|
|
11114
|
+
if (!zip) await fs28.mkdir(staging);
|
|
11115
|
+
await fs28.writeFile(path27.join(staging, ITEM_FILE), `${JSON.stringify(itemFile, null, 2)}
|
|
10773
11116
|
`, { flag: "wx" });
|
|
10774
|
-
if (keep) await
|
|
10775
|
-
await
|
|
11117
|
+
if (keep) await fs28.rename(final, path27.join(keep, `${skuId}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`));
|
|
11118
|
+
await fs28.rename(staging, final);
|
|
10776
11119
|
return files;
|
|
10777
11120
|
} finally {
|
|
10778
|
-
await
|
|
11121
|
+
await fs28.rm(staging, { recursive: true, force: true });
|
|
10779
11122
|
}
|
|
10780
11123
|
}
|
|
10781
11124
|
async remove(skuId) {
|
|
@@ -10830,22 +11173,22 @@ function folderRefusal(skuId, name, err) {
|
|
|
10830
11173
|
}
|
|
10831
11174
|
async function lstatOrNull2(p) {
|
|
10832
11175
|
try {
|
|
10833
|
-
return await
|
|
11176
|
+
return await fs28.lstat(p);
|
|
10834
11177
|
} catch (err) {
|
|
10835
11178
|
if (err.code === "ENOENT") return null;
|
|
10836
11179
|
throw err;
|
|
10837
11180
|
}
|
|
10838
11181
|
}
|
|
10839
11182
|
async function replacedItemsDir(root) {
|
|
10840
|
-
await
|
|
10841
|
-
const keep =
|
|
11183
|
+
await fs28.mkdir(path27.join(root, ".genex"), { recursive: true });
|
|
11184
|
+
const keep = path27.join(root, ...REPLACED_DIR.split("/"));
|
|
10842
11185
|
await ensureRealDir(keep, REPLACED_DIR);
|
|
10843
11186
|
return keep;
|
|
10844
11187
|
}
|
|
10845
11188
|
async function ensureRealDir(dir, label) {
|
|
10846
11189
|
const stat = await lstatOrNull2(dir);
|
|
10847
11190
|
if (!stat) {
|
|
10848
|
-
await
|
|
11191
|
+
await fs28.mkdir(dir);
|
|
10849
11192
|
return;
|
|
10850
11193
|
}
|
|
10851
11194
|
if (!stat.isDirectory()) {
|
|
@@ -11059,7 +11402,10 @@ async function runPublish(opts) {
|
|
|
11059
11402
|
mobileControls: detections.mobileControls,
|
|
11060
11403
|
// Declared on the staging go-live; the promotion below copies that
|
|
11061
11404
|
// build's own list to production with it.
|
|
11062
|
-
capabilities: detections.capabilities
|
|
11405
|
+
capabilities: detections.capabilities,
|
|
11406
|
+
// The room module, when the game declares one: bundled, validated and uploaded with this
|
|
11407
|
+
// staging build (the promotion below carries it to production).
|
|
11408
|
+
authority: detections.authority
|
|
11063
11409
|
},
|
|
11064
11410
|
log
|
|
11065
11411
|
);
|
|
@@ -11295,23 +11641,36 @@ async function readPreviousVersion(apiUrl, projectId, token, log) {
|
|
|
11295
11641
|
log.dim(" Nothing was changed. Try again shortly, or update the server.");
|
|
11296
11642
|
return null;
|
|
11297
11643
|
}
|
|
11298
|
-
|
|
11644
|
+
const available = typeof body.releaseAvailable === "boolean" ? body.releaseAvailable : body.available;
|
|
11645
|
+
if (!available || !body.commit || !body.currentCommit) {
|
|
11299
11646
|
log.error("This game has no previous version to go back to.");
|
|
11300
11647
|
log.dim(" Only versions published from this account since the feature shipped are recorded.");
|
|
11301
11648
|
return null;
|
|
11302
11649
|
}
|
|
11650
|
+
const release = typeof body.release === "string" ? body.release : null;
|
|
11651
|
+
const currentRelease = typeof body.currentRelease === "string" ? body.currentRelease : null;
|
|
11303
11652
|
return {
|
|
11304
11653
|
commit: body.commit,
|
|
11305
11654
|
deployedAt: body.deployedAt ?? null,
|
|
11306
|
-
currentCommit: body.currentCommit
|
|
11655
|
+
currentCommit: body.currentCommit,
|
|
11656
|
+
release,
|
|
11657
|
+
currentRelease,
|
|
11658
|
+
expectLive: currentRelease ?? body.currentCommit
|
|
11307
11659
|
};
|
|
11308
11660
|
}
|
|
11309
|
-
|
|
11661
|
+
function releaseModule(tag) {
|
|
11662
|
+
if (tag === null) return void 0;
|
|
11663
|
+
const at = tag.indexOf("@");
|
|
11664
|
+
if (at < 0) return void 0;
|
|
11665
|
+
const module = tag.slice(at + 1);
|
|
11666
|
+
return module === "none" ? null : module;
|
|
11667
|
+
}
|
|
11668
|
+
async function restorePreviousVersion(apiUrl, projectId, token, expectLive, log) {
|
|
11310
11669
|
let res;
|
|
11311
11670
|
try {
|
|
11312
11671
|
res = await apiFetch(`${apiUrl}/api/games/${projectId}/rollback`, {
|
|
11313
11672
|
method: "POST",
|
|
11314
|
-
headers: { Authorization: `Bearer ${token}`, "If-Match":
|
|
11673
|
+
headers: { Authorization: `Bearer ${token}`, "If-Match": expectLive }
|
|
11315
11674
|
});
|
|
11316
11675
|
} catch (err) {
|
|
11317
11676
|
log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
|
|
@@ -11335,6 +11694,12 @@ async function restorePreviousVersion(apiUrl, projectId, token, expectLiveCommit
|
|
|
11335
11694
|
return null;
|
|
11336
11695
|
}
|
|
11337
11696
|
if (res.status === 428) {
|
|
11697
|
+
const body2 = await res.json().catch(() => null);
|
|
11698
|
+
if (body2?.error === "release_match_required") {
|
|
11699
|
+
log.error("The previous version differs from the live one only in its room module, and the server needs that named exactly \u2014 nothing was changed.");
|
|
11700
|
+
log.dim(" Run the command again; if it repeats, update the CLI.");
|
|
11701
|
+
return null;
|
|
11702
|
+
}
|
|
11338
11703
|
log.error("This CLI is out of date for this command \u2014 upgrade and try again.");
|
|
11339
11704
|
return null;
|
|
11340
11705
|
}
|
|
@@ -11368,6 +11733,26 @@ function when(iso) {
|
|
|
11368
11733
|
if (hours < 48) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
11369
11734
|
return `${Math.round(hours / 24)} days ago`;
|
|
11370
11735
|
}
|
|
11736
|
+
var moduleLabel = (module) => module === null ? "no room module" : `room module ${module.slice(0, 7)}`;
|
|
11737
|
+
function restoreLines(previous) {
|
|
11738
|
+
const now = releaseModule(previous.currentRelease);
|
|
11739
|
+
const back = releaseModule(previous.release);
|
|
11740
|
+
const hasModule = now !== void 0 && now !== null || back !== void 0 && back !== null;
|
|
11741
|
+
if (!hasModule || now === void 0 || back === void 0) {
|
|
11742
|
+
return [`live now: ${previous.currentCommit.slice(0, 7)}`, `goes back to: ${previous.commit.slice(0, 7)}`];
|
|
11743
|
+
}
|
|
11744
|
+
if (previous.commit === previous.currentCommit) {
|
|
11745
|
+
return [
|
|
11746
|
+
`same game build: ${previous.commit.slice(0, 7)}`,
|
|
11747
|
+
`live now: ${moduleLabel(now)}`,
|
|
11748
|
+
`goes back to: ${moduleLabel(back)}`
|
|
11749
|
+
];
|
|
11750
|
+
}
|
|
11751
|
+
return [
|
|
11752
|
+
`live now: ${previous.currentCommit.slice(0, 7)} with ${moduleLabel(now)}`,
|
|
11753
|
+
`goes back to: ${previous.commit.slice(0, 7)} with ${moduleLabel(back)}`
|
|
11754
|
+
];
|
|
11755
|
+
}
|
|
11371
11756
|
async function runRollback(opts) {
|
|
11372
11757
|
const log = createLogger({ quiet: opts.quiet });
|
|
11373
11758
|
log.plain(c.bold("genex rollback"));
|
|
@@ -11401,15 +11786,14 @@ async function runRollback(opts) {
|
|
|
11401
11786
|
}
|
|
11402
11787
|
if (!opts.yes) {
|
|
11403
11788
|
log.warn(`This puts players back on the version from ${when(previous.deployedAt)}.`);
|
|
11404
|
-
log.dim(`
|
|
11405
|
-
log.dim(` goes back to: ${previous.commit.slice(0, 7)}`);
|
|
11789
|
+
for (const line of restoreLines(previous)) log.dim(` ${line}`);
|
|
11406
11790
|
log.dim(" Your code is NOT changed \u2014 this folder, and what a Remix copies, stay as they are.");
|
|
11407
11791
|
log.dim(" Re-run with --yes to do it.");
|
|
11408
11792
|
process.exitCode = 1;
|
|
11409
11793
|
return;
|
|
11410
11794
|
}
|
|
11411
11795
|
log.step("Putting the previous version back\u2026");
|
|
11412
|
-
const result = await restorePreviousVersion(apiUrl, meta.id, token, previous.
|
|
11796
|
+
const result = await restorePreviousVersion(apiUrl, meta.id, token, previous.expectLive, log);
|
|
11413
11797
|
if (!result) {
|
|
11414
11798
|
process.exitCode = 1;
|
|
11415
11799
|
return;
|
|
@@ -11423,8 +11807,8 @@ async function runRollback(opts) {
|
|
|
11423
11807
|
}
|
|
11424
11808
|
|
|
11425
11809
|
// src/commands/cover.ts
|
|
11426
|
-
import
|
|
11427
|
-
import
|
|
11810
|
+
import fs29 from "fs/promises";
|
|
11811
|
+
import path28 from "path";
|
|
11428
11812
|
var WHAT_A_COVER_IS = "one real frame of the game: its signature moment, real gameplay, well lit, no menus, popups or busy HUD, landscape 16:9, no text";
|
|
11429
11813
|
async function runCover(opts) {
|
|
11430
11814
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
@@ -11449,7 +11833,7 @@ async function runCover(opts) {
|
|
|
11449
11833
|
}
|
|
11450
11834
|
let bytes;
|
|
11451
11835
|
try {
|
|
11452
|
-
const stat = await
|
|
11836
|
+
const stat = await fs29.stat(file);
|
|
11453
11837
|
if (!stat.isFile()) throw new Error("not a file");
|
|
11454
11838
|
if (stat.size > COVER_MAX_BYTES) {
|
|
11455
11839
|
return finish(
|
|
@@ -11459,23 +11843,23 @@ async function runCover(opts) {
|
|
|
11459
11843
|
log
|
|
11460
11844
|
);
|
|
11461
11845
|
}
|
|
11462
|
-
bytes = await
|
|
11846
|
+
bytes = await fs29.readFile(file);
|
|
11463
11847
|
} catch {
|
|
11464
11848
|
return finish({ kind: "invalid", message: `Can't read ${file} \u2014 pass the path of a PNG, JPEG or WebP image.` }, file, opts, log);
|
|
11465
11849
|
}
|
|
11466
|
-
log.step(`Uploading ${
|
|
11850
|
+
log.step(`Uploading ${path28.basename(file)} as the cover\u2026`);
|
|
11467
11851
|
const outcome2 = await uploadCover({ apiUrl, token, projectId: meta.id, bytes, label: file });
|
|
11468
11852
|
await rememberNomination(bytes, file, outcome2);
|
|
11469
11853
|
finish(outcome2, file, opts, log);
|
|
11470
11854
|
}
|
|
11471
11855
|
async function rememberNomination(bytes, file, outcome2) {
|
|
11472
11856
|
const scratch = await findScratchCover();
|
|
11473
|
-
if (!scratch ||
|
|
11857
|
+
if (!scratch || path28.resolve(scratch) === path28.resolve(file)) {
|
|
11474
11858
|
await rememberCoverUpload(bytes, file, outcome2);
|
|
11475
11859
|
return;
|
|
11476
11860
|
}
|
|
11477
11861
|
if (outcome2.kind !== "applied" && outcome2.kind !== "outranked") return;
|
|
11478
|
-
const stale = await
|
|
11862
|
+
const stale = await fs29.readFile(scratch).catch(() => null);
|
|
11479
11863
|
if (stale) await rememberCoverUpload(stale, scratch, outcome2);
|
|
11480
11864
|
}
|
|
11481
11865
|
async function sendClip(apiUrl, token, projectId, clip, opts, log) {
|
|
@@ -11486,16 +11870,16 @@ async function sendClip(apiUrl, token, projectId, clip, opts, log) {
|
|
|
11486
11870
|
if (!opts.commit?.trim()) return invalid3("--clip needs --commit <sha>: the git commit of the build the recording shows.");
|
|
11487
11871
|
let bytes;
|
|
11488
11872
|
try {
|
|
11489
|
-
const stat = await
|
|
11873
|
+
const stat = await fs29.stat(clip);
|
|
11490
11874
|
if (!stat.isFile()) throw new Error("not a file");
|
|
11491
11875
|
if (stat.size > CLIP_MAX_BYTES) {
|
|
11492
11876
|
return invalid3(`${clip} is ${(stat.size / 1024 / 1024).toFixed(1)} MB; a cover clip recording can be at most ${CLIP_MAX_BYTES / 1024 / 1024} MB.`);
|
|
11493
11877
|
}
|
|
11494
|
-
bytes = await
|
|
11878
|
+
bytes = await fs29.readFile(clip);
|
|
11495
11879
|
} catch {
|
|
11496
11880
|
return invalid3(`Can't read ${clip} \u2014 pass the path of an MP4 or WebM recording.`);
|
|
11497
11881
|
}
|
|
11498
|
-
log.step(`Uploading ${
|
|
11882
|
+
log.step(`Uploading ${path28.basename(clip)} as the cover clip\u2026`);
|
|
11499
11883
|
const outcome2 = await uploadClip({ apiUrl, token, projectId, bytes, label: clip, buildCommit: opts.commit });
|
|
11500
11884
|
finishClip(outcome2, clip, opts, log);
|
|
11501
11885
|
}
|
|
@@ -11553,8 +11937,8 @@ async function showCover(apiUrl, token, slug, opts, log) {
|
|
|
11553
11937
|
}
|
|
11554
11938
|
|
|
11555
11939
|
// src/commands/generate.ts
|
|
11556
|
-
import
|
|
11557
|
-
import
|
|
11940
|
+
import fs32 from "fs/promises";
|
|
11941
|
+
import path31 from "path";
|
|
11558
11942
|
import { PNG as PNG4 } from "pngjs";
|
|
11559
11943
|
|
|
11560
11944
|
// src/lib/glass.ts
|
|
@@ -11742,13 +12126,13 @@ function ceilingVerdict(input) {
|
|
|
11742
12126
|
}
|
|
11743
12127
|
|
|
11744
12128
|
// src/lib/generation-admission.ts
|
|
11745
|
-
import
|
|
11746
|
-
import
|
|
12129
|
+
import fs31 from "fs/promises";
|
|
12130
|
+
import path30 from "path";
|
|
11747
12131
|
import { createHash as createHash3, randomUUID } from "crypto";
|
|
11748
12132
|
|
|
11749
12133
|
// src/lib/asset-budget.ts
|
|
11750
|
-
import
|
|
11751
|
-
import
|
|
12134
|
+
import fs30 from "fs/promises";
|
|
12135
|
+
import path29 from "path";
|
|
11752
12136
|
var DEFAULT_ASSET_BUDGET_SHARE = 0.5;
|
|
11753
12137
|
var DEFAULT_ASSET_BUDGET_CAP = 500;
|
|
11754
12138
|
function envShare(raw, fallback) {
|
|
@@ -11775,10 +12159,10 @@ function allowanceFor(input) {
|
|
|
11775
12159
|
if (!Number.isFinite(spendable) || spendable <= 0) return 0;
|
|
11776
12160
|
return Math.max(0, Math.min(Math.floor(spendable * config.share), config.cap));
|
|
11777
12161
|
}
|
|
11778
|
-
var storePath = (cwd) =>
|
|
12162
|
+
var storePath = (cwd) => path29.join(cwd, ".genex", "asset-budget.json");
|
|
11779
12163
|
async function readBudgetStore(cwd = process.cwd()) {
|
|
11780
12164
|
try {
|
|
11781
|
-
const raw = await
|
|
12165
|
+
const raw = await fs30.readFile(storePath(cwd), "utf8");
|
|
11782
12166
|
const parsed = JSON.parse(raw);
|
|
11783
12167
|
if (!parsed || parsed.v !== 1 || typeof parsed.allowanceCredits !== "number" || !Number.isFinite(parsed.allowanceCredits) || parsed.setBy !== "default" && parsed.setBy !== "user") {
|
|
11784
12168
|
return null;
|
|
@@ -11797,8 +12181,8 @@ async function readBudgetStore(cwd = process.cwd()) {
|
|
|
11797
12181
|
}
|
|
11798
12182
|
async function writeBudgetStore(cwd, store) {
|
|
11799
12183
|
try {
|
|
11800
|
-
await
|
|
11801
|
-
await
|
|
12184
|
+
await fs30.access(path29.join(cwd, ".genex"));
|
|
12185
|
+
await fs30.writeFile(storePath(cwd), `${JSON.stringify(store, null, 2)}
|
|
11802
12186
|
`, "utf8");
|
|
11803
12187
|
return true;
|
|
11804
12188
|
} catch {
|
|
@@ -11809,7 +12193,7 @@ async function ensureBudgetStore(cwd, input) {
|
|
|
11809
12193
|
const existing = await readBudgetStore(cwd);
|
|
11810
12194
|
if (existing) return existing;
|
|
11811
12195
|
try {
|
|
11812
|
-
await
|
|
12196
|
+
await fs30.stat(storePath(cwd));
|
|
11813
12197
|
throw new Error("Stored asset allowance is unreadable or invalid; reconcile it before paid generation.");
|
|
11814
12198
|
} catch (error) {
|
|
11815
12199
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -11913,13 +12297,13 @@ async function assetBudgetGate(input) {
|
|
|
11913
12297
|
|
|
11914
12298
|
// src/lib/generation-admission.ts
|
|
11915
12299
|
async function withAdmissionLock(cwd, work, waitMs = 3e4) {
|
|
11916
|
-
const lock =
|
|
11917
|
-
await
|
|
12300
|
+
const lock = path30.join(cwd, ".genex", "generation-admission.lock");
|
|
12301
|
+
await fs31.mkdir(path30.dirname(lock), { recursive: true });
|
|
11918
12302
|
const deadline = Date.now() + waitMs;
|
|
11919
12303
|
let file;
|
|
11920
12304
|
while (!file) {
|
|
11921
12305
|
try {
|
|
11922
|
-
file = await
|
|
12306
|
+
file = await fs31.open(lock, "wx");
|
|
11923
12307
|
} catch (error) {
|
|
11924
12308
|
if (error.code !== "EEXIST") throw error;
|
|
11925
12309
|
if (Date.now() >= deadline) throw new Error("Another generation admission holds .genex/generation-admission.lock. If its process died, reconcile pending reservations before removing that lock.");
|
|
@@ -11931,7 +12315,7 @@ async function withAdmissionLock(cwd, work, waitMs = 3e4) {
|
|
|
11931
12315
|
return await work();
|
|
11932
12316
|
} finally {
|
|
11933
12317
|
await file.close();
|
|
11934
|
-
await
|
|
12318
|
+
await fs31.unlink(lock);
|
|
11935
12319
|
}
|
|
11936
12320
|
}
|
|
11937
12321
|
async function enqueueWithAllowance(input) {
|
|
@@ -12180,7 +12564,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
|
|
|
12180
12564
|
async function inlineLocalImage(filePath, flag) {
|
|
12181
12565
|
let bytes;
|
|
12182
12566
|
try {
|
|
12183
|
-
bytes = await
|
|
12567
|
+
bytes = await fs32.readFile(filePath);
|
|
12184
12568
|
} catch {
|
|
12185
12569
|
return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
|
|
12186
12570
|
}
|
|
@@ -12190,7 +12574,7 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
12190
12574
|
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.`
|
|
12191
12575
|
};
|
|
12192
12576
|
}
|
|
12193
|
-
const mime = IMAGE_MIME_BY_EXT[
|
|
12577
|
+
const mime = IMAGE_MIME_BY_EXT[path31.extname(filePath).toLowerCase()] ?? "image/png";
|
|
12194
12578
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
12195
12579
|
}
|
|
12196
12580
|
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.";
|
|
@@ -12385,7 +12769,7 @@ async function runGenerate(kind, opts) {
|
|
|
12385
12769
|
let typedPrompt = opts.prompt?.trim();
|
|
12386
12770
|
if (!typedPrompt && kind === "model" && opts.imageUrl) {
|
|
12387
12771
|
const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
|
|
12388
|
-
typedPrompt = `from image: ${
|
|
12772
|
+
typedPrompt = `from image: ${path31.basename(ref).slice(0, 120)}`;
|
|
12389
12773
|
}
|
|
12390
12774
|
if (kind === "model" && opts.texture !== void 0 && !MODEL_TEXTURE_TIERS.includes(opts.texture)) {
|
|
12391
12775
|
log.error(`--texture ${opts.texture} is a character texture size. \`genex model\` takes a texture TIER: ${MODEL_TEXTURE_TIERS.join("|")} (default detailed).`);
|
|
@@ -12434,7 +12818,7 @@ async function runGenerate(kind, opts) {
|
|
|
12434
12818
|
return;
|
|
12435
12819
|
}
|
|
12436
12820
|
try {
|
|
12437
|
-
const bytes = await
|
|
12821
|
+
const bytes = await fs32.readFile(opts.inpaintUrl);
|
|
12438
12822
|
opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
|
|
12439
12823
|
} catch {
|
|
12440
12824
|
log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
|
|
@@ -12603,7 +12987,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
12603
12987
|
return;
|
|
12604
12988
|
}
|
|
12605
12989
|
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
12606
|
-
await
|
|
12990
|
+
await fs32.mkdir(outDir, { recursive: true });
|
|
12607
12991
|
const solved = [];
|
|
12608
12992
|
for (let i = 0; i < files.length; i++) {
|
|
12609
12993
|
const f = files[i];
|
|
@@ -12635,8 +13019,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
|
|
|
12635
13019
|
});
|
|
12636
13020
|
continue;
|
|
12637
13021
|
}
|
|
12638
|
-
const outPath =
|
|
12639
|
-
await
|
|
13022
|
+
const outPath = path31.join(outDir, `glass-${i + 1}.png`);
|
|
13023
|
+
await fs32.writeFile(outPath, PNG4.sync.write(r.png));
|
|
12640
13024
|
solved.push({
|
|
12641
13025
|
path: outPath,
|
|
12642
13026
|
url: f.url,
|
|
@@ -13266,8 +13650,8 @@ function writeJson(value) {
|
|
|
13266
13650
|
}
|
|
13267
13651
|
|
|
13268
13652
|
// src/commands/model-sub.ts
|
|
13269
|
-
import
|
|
13270
|
-
import
|
|
13653
|
+
import fs33 from "fs/promises";
|
|
13654
|
+
import path32 from "path";
|
|
13271
13655
|
var MODEL_SUBCOMMANDS = ["segment", "rig", "animate", "import"];
|
|
13272
13656
|
function apiErrorMessage(data, fallback) {
|
|
13273
13657
|
if (typeof data !== "object" || data === null) return fallback;
|
|
@@ -13282,29 +13666,29 @@ async function importModelFile(args) {
|
|
|
13282
13666
|
const { log } = args;
|
|
13283
13667
|
const filePath = args.filePath.trim();
|
|
13284
13668
|
if (!/\.glb$/i.test(filePath)) {
|
|
13285
|
-
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${
|
|
13669
|
+
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path32.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
|
|
13286
13670
|
return null;
|
|
13287
13671
|
}
|
|
13288
13672
|
let bytes;
|
|
13289
13673
|
try {
|
|
13290
|
-
bytes = await
|
|
13674
|
+
bytes = await fs33.readFile(filePath);
|
|
13291
13675
|
} catch {
|
|
13292
13676
|
log.error(`Couldn't read ${filePath}.`);
|
|
13293
13677
|
return null;
|
|
13294
13678
|
}
|
|
13295
13679
|
if (bytes.byteLength > MODEL_IMPORT_MAX_BYTES) {
|
|
13296
|
-
log.error(`${
|
|
13680
|
+
log.error(`${path32.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
|
|
13297
13681
|
return null;
|
|
13298
13682
|
}
|
|
13299
13683
|
if (bytes.byteLength < 12 || bytes.readUInt32LE(0) !== GLB_MAGIC) {
|
|
13300
|
-
log.error(`${
|
|
13684
|
+
log.error(`${path32.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
|
|
13301
13685
|
return null;
|
|
13302
13686
|
}
|
|
13303
13687
|
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${args.token}` };
|
|
13304
13688
|
const minted = await apiFetch(`${args.apiUrl}/api/generations/import`, {
|
|
13305
13689
|
method: "POST",
|
|
13306
13690
|
headers,
|
|
13307
|
-
body: JSON.stringify({ filename:
|
|
13691
|
+
body: JSON.stringify({ filename: path32.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
|
|
13308
13692
|
});
|
|
13309
13693
|
if (printedStructuredError(minted)) return null;
|
|
13310
13694
|
if (!minted.ok) {
|
|
@@ -13313,7 +13697,7 @@ async function importModelFile(args) {
|
|
|
13313
13697
|
return null;
|
|
13314
13698
|
}
|
|
13315
13699
|
const { id, uploadUrl, url } = await minted.json();
|
|
13316
|
-
log.dim(` uploading ${
|
|
13700
|
+
log.dim(` uploading ${path32.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
13317
13701
|
const put = await fetch(uploadUrl, {
|
|
13318
13702
|
method: "PUT",
|
|
13319
13703
|
headers: { "Content-Type": "model/gltf-binary", "Content-Length": String(bytes.byteLength) },
|
|
@@ -13461,7 +13845,7 @@ async function runModelAnimate(opts) {
|
|
|
13461
13845
|
}
|
|
13462
13846
|
|
|
13463
13847
|
// src/commands/wait.ts
|
|
13464
|
-
import
|
|
13848
|
+
import path33 from "path";
|
|
13465
13849
|
var MAX_TIMEOUT_SEC = 2e6;
|
|
13466
13850
|
var STILL_RUNNING_EXIT = 2;
|
|
13467
13851
|
var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
@@ -13668,7 +14052,7 @@ async function runWaitAll(opts) {
|
|
|
13668
14052
|
if (v?.status !== "completed" || !v.files?.length) continue;
|
|
13669
14053
|
const local = localDeliveryFor("tools", { ...opts, outDir: opts.outDir ?? e.outDir }, e.prompt || e.kind);
|
|
13670
14054
|
if (!local) continue;
|
|
13671
|
-
const outDir =
|
|
14055
|
+
const outDir = path33.join(path33.relative(process.cwd(), cwd) || ".", local.outDir);
|
|
13672
14056
|
const target = { kind: e.kind, prompt: local.prompt, id: e.id, outDir };
|
|
13673
14057
|
const missing = await undeliveredFiles(v.files, target);
|
|
13674
14058
|
if (missing.length === 0) continue;
|
|
@@ -13780,8 +14164,8 @@ async function toRow(e, v, cwd) {
|
|
|
13780
14164
|
}
|
|
13781
14165
|
|
|
13782
14166
|
// src/commands/controller.ts
|
|
13783
|
-
import
|
|
13784
|
-
import
|
|
14167
|
+
import fs35 from "fs/promises";
|
|
14168
|
+
import path35 from "path";
|
|
13785
14169
|
|
|
13786
14170
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
13787
14171
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -22625,9 +23009,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
22625
23009
|
}
|
|
22626
23010
|
|
|
22627
23011
|
// src/lib/anims.ts
|
|
22628
|
-
import
|
|
22629
|
-
import
|
|
22630
|
-
var ANIMS_DEST =
|
|
23012
|
+
import fs34 from "fs/promises";
|
|
23013
|
+
import path34 from "path";
|
|
23014
|
+
var ANIMS_DEST = path34.join("public", "assets", "anims");
|
|
22631
23015
|
var HIDDEN_TAG = "reference";
|
|
22632
23016
|
async function runAnims(opts) {
|
|
22633
23017
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -22643,7 +23027,7 @@ async function runAnims(opts) {
|
|
|
22643
23027
|
printCatalog(log, manifest, selectors);
|
|
22644
23028
|
return;
|
|
22645
23029
|
}
|
|
22646
|
-
const controllerMarker =
|
|
23030
|
+
const controllerMarker = path34.join(root, "src", "controllers", "character");
|
|
22647
23031
|
if (!await exists3(controllerMarker)) {
|
|
22648
23032
|
log.error(
|
|
22649
23033
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -22652,11 +23036,11 @@ async function runAnims(opts) {
|
|
|
22652
23036
|
process.exitCode = 1;
|
|
22653
23037
|
return;
|
|
22654
23038
|
}
|
|
22655
|
-
const destDir =
|
|
22656
|
-
const gameManifestPath =
|
|
23039
|
+
const destDir = path34.join(root, ANIMS_DEST);
|
|
23040
|
+
const gameManifestPath = path34.join(destDir, "manifest.json");
|
|
22657
23041
|
if (opts.reset) {
|
|
22658
|
-
await
|
|
22659
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
23042
|
+
await fs34.rm(destDir, { recursive: true, force: true });
|
|
23043
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path34.sep)} (--reset)`);
|
|
22660
23044
|
}
|
|
22661
23045
|
if (selectors.length === 0) {
|
|
22662
23046
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -22694,35 +23078,35 @@ async function runAnims(opts) {
|
|
|
22694
23078
|
}
|
|
22695
23079
|
}
|
|
22696
23080
|
const wanted = [...byName2.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
22697
|
-
const cacheDir =
|
|
23081
|
+
const cacheDir = path34.join(
|
|
22698
23082
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
22699
23083
|
`${manifest.library}-v${manifest.version}`
|
|
22700
23084
|
);
|
|
22701
|
-
await
|
|
22702
|
-
await
|
|
23085
|
+
await fs34.mkdir(cacheDir, { recursive: true });
|
|
23086
|
+
await fs34.mkdir(destDir, { recursive: true });
|
|
22703
23087
|
const base = getAnimsBase(opts.animsBase);
|
|
22704
23088
|
let installedCount = 0;
|
|
22705
23089
|
let presentCount = 0;
|
|
22706
23090
|
let addedBytes = 0;
|
|
22707
23091
|
const failures = [];
|
|
22708
23092
|
for (const entry of wanted) {
|
|
22709
|
-
const dest =
|
|
23093
|
+
const dest = path34.join(destDir, entry.file);
|
|
22710
23094
|
if (await hasSize(dest, entry.bytes)) {
|
|
22711
23095
|
presentCount++;
|
|
22712
23096
|
continue;
|
|
22713
23097
|
}
|
|
22714
23098
|
try {
|
|
22715
|
-
const cached =
|
|
23099
|
+
const cached = path34.join(cacheDir, entry.file);
|
|
22716
23100
|
if (!await hasSize(cached, entry.bytes)) {
|
|
22717
23101
|
const res = await fetch(base + entry.file);
|
|
22718
23102
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
22719
23103
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
22720
|
-
await
|
|
23104
|
+
await fs34.writeFile(cached, buf);
|
|
22721
23105
|
}
|
|
22722
|
-
await
|
|
23106
|
+
await fs34.copyFile(cached, dest);
|
|
22723
23107
|
installedCount++;
|
|
22724
23108
|
addedBytes += entry.bytes;
|
|
22725
|
-
log.dim(` ${
|
|
23109
|
+
log.dim(` ${path34.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
22726
23110
|
} catch (err) {
|
|
22727
23111
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
22728
23112
|
}
|
|
@@ -22738,13 +23122,13 @@ async function runAnims(opts) {
|
|
|
22738
23122
|
version: manifest.version,
|
|
22739
23123
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
22740
23124
|
};
|
|
22741
|
-
await
|
|
23125
|
+
await fs34.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
22742
23126
|
log.plain("");
|
|
22743
23127
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
22744
23128
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
22745
23129
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
22746
23130
|
log.success(
|
|
22747
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
23131
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path34.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
22748
23132
|
);
|
|
22749
23133
|
for (const [selector, entries] of resolved) {
|
|
22750
23134
|
const names2 = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -22776,8 +23160,8 @@ async function loadManifest(baseOverride) {
|
|
|
22776
23160
|
}
|
|
22777
23161
|
} catch {
|
|
22778
23162
|
}
|
|
22779
|
-
const snapshotPath =
|
|
22780
|
-
const manifest = JSON.parse(await
|
|
23163
|
+
const snapshotPath = path34.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
23164
|
+
const manifest = JSON.parse(await fs34.readFile(snapshotPath, "utf8"));
|
|
22781
23165
|
return { manifest, source: "snapshot" };
|
|
22782
23166
|
}
|
|
22783
23167
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -22895,21 +23279,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
22895
23279
|
}
|
|
22896
23280
|
async function readGameManifest(file) {
|
|
22897
23281
|
try {
|
|
22898
|
-
return JSON.parse(await
|
|
23282
|
+
return JSON.parse(await fs34.readFile(file, "utf8"));
|
|
22899
23283
|
} catch {
|
|
22900
23284
|
return null;
|
|
22901
23285
|
}
|
|
22902
23286
|
}
|
|
22903
23287
|
async function hasSize(file, bytes) {
|
|
22904
23288
|
try {
|
|
22905
|
-
return (await
|
|
23289
|
+
return (await fs34.stat(file)).size === bytes;
|
|
22906
23290
|
} catch {
|
|
22907
23291
|
return false;
|
|
22908
23292
|
}
|
|
22909
23293
|
}
|
|
22910
23294
|
async function exists3(p) {
|
|
22911
23295
|
try {
|
|
22912
|
-
await
|
|
23296
|
+
await fs34.access(p);
|
|
22913
23297
|
return true;
|
|
22914
23298
|
} catch {
|
|
22915
23299
|
return false;
|
|
@@ -23110,8 +23494,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
23110
23494
|
]
|
|
23111
23495
|
}
|
|
23112
23496
|
};
|
|
23113
|
-
var CODE_DEST =
|
|
23114
|
-
var ASSETS_DEST =
|
|
23497
|
+
var CODE_DEST = path35.join("src", "controllers");
|
|
23498
|
+
var ASSETS_DEST = path35.join("public", "assets");
|
|
23115
23499
|
async function runController(opts) {
|
|
23116
23500
|
const log = createLogger({ quiet: opts.quiet });
|
|
23117
23501
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -23128,31 +23512,31 @@ async function runController(opts) {
|
|
|
23128
23512
|
process.exitCode = 1;
|
|
23129
23513
|
return;
|
|
23130
23514
|
}
|
|
23131
|
-
const srcDir =
|
|
23515
|
+
const srcDir = path35.join(getTemplatesDir(), "controllers");
|
|
23132
23516
|
const root = opts.cwd ?? process.cwd();
|
|
23133
23517
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
23134
23518
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
23135
23519
|
log.plain("");
|
|
23136
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
23520
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path35.sep)}`);
|
|
23137
23521
|
const plan2 = [
|
|
23138
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
23522
|
+
...set.code.map((rel) => ({ from: rel, rel: path35.join(CODE_DEST, rel) })),
|
|
23139
23523
|
...set.assets.map((rel) => ({
|
|
23140
23524
|
from: rel,
|
|
23141
|
-
rel:
|
|
23525
|
+
rel: path35.join(ASSETS_DEST, path35.basename(rel))
|
|
23142
23526
|
}))
|
|
23143
23527
|
];
|
|
23144
23528
|
let copied = 0;
|
|
23145
23529
|
let skipped = 0;
|
|
23146
23530
|
try {
|
|
23147
23531
|
for (const file of plan2) {
|
|
23148
|
-
const dest =
|
|
23532
|
+
const dest = path35.join(root, file.rel);
|
|
23149
23533
|
if (!opts.force && await exists4(dest)) {
|
|
23150
23534
|
skipped++;
|
|
23151
23535
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
23152
23536
|
continue;
|
|
23153
23537
|
}
|
|
23154
|
-
await
|
|
23155
|
-
await
|
|
23538
|
+
await fs35.mkdir(path35.dirname(dest), { recursive: true });
|
|
23539
|
+
await fs35.copyFile(path35.join(srcDir, file.from), dest);
|
|
23156
23540
|
copied++;
|
|
23157
23541
|
log.dim(` ${file.rel}`);
|
|
23158
23542
|
}
|
|
@@ -23205,7 +23589,7 @@ async function runController(opts) {
|
|
|
23205
23589
|
for (const line of set.sketch) {
|
|
23206
23590
|
log.dim(` ${line}`);
|
|
23207
23591
|
}
|
|
23208
|
-
if (kind === "character" && !await exists4(
|
|
23592
|
+
if (kind === "character" && !await exists4(path35.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
23209
23593
|
log.plain("");
|
|
23210
23594
|
log.plain(
|
|
23211
23595
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -23237,9 +23621,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
23237
23621
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
23238
23622
|
}
|
|
23239
23623
|
assertCompleteMeshyControllerPack(manifest);
|
|
23240
|
-
const destination =
|
|
23241
|
-
await
|
|
23242
|
-
await
|
|
23624
|
+
const destination = path35.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
23625
|
+
await fs35.mkdir(path35.dirname(destination), { recursive: true });
|
|
23626
|
+
await fs35.writeFile(
|
|
23243
23627
|
destination,
|
|
23244
23628
|
`${JSON.stringify(manifest, null, 2)}
|
|
23245
23629
|
`
|
|
@@ -23384,14 +23768,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
23384
23768
|
}
|
|
23385
23769
|
async function installFallbackAvatar(args) {
|
|
23386
23770
|
const { root, srcDir, log } = args;
|
|
23387
|
-
const dest =
|
|
23388
|
-
await
|
|
23389
|
-
await
|
|
23771
|
+
const dest = path35.join(root, ASSETS_DEST, "avatar.vrm");
|
|
23772
|
+
await fs35.mkdir(path35.dirname(dest), { recursive: true });
|
|
23773
|
+
await fs35.copyFile(path35.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
23390
23774
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
23391
23775
|
}
|
|
23392
23776
|
async function exists4(p) {
|
|
23393
23777
|
try {
|
|
23394
|
-
await
|
|
23778
|
+
await fs35.access(p);
|
|
23395
23779
|
return true;
|
|
23396
23780
|
} catch {
|
|
23397
23781
|
return false;
|
|
@@ -23399,8 +23783,8 @@ async function exists4(p) {
|
|
|
23399
23783
|
}
|
|
23400
23784
|
|
|
23401
23785
|
// src/commands/character.ts
|
|
23402
|
-
import
|
|
23403
|
-
import
|
|
23786
|
+
import fs36 from "fs/promises";
|
|
23787
|
+
import path36 from "path";
|
|
23404
23788
|
function exactAnimation(selector) {
|
|
23405
23789
|
const trimmed = selector.trim();
|
|
23406
23790
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -23485,7 +23869,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
23485
23869
|
}
|
|
23486
23870
|
process.exitCode = 1;
|
|
23487
23871
|
}
|
|
23488
|
-
var INSTALLED_MANIFEST =
|
|
23872
|
+
var INSTALLED_MANIFEST = path36.join("public", "assets", "meshy-character.json");
|
|
23489
23873
|
async function resolveAdoptTarget(selector) {
|
|
23490
23874
|
const trimmed = selector?.trim();
|
|
23491
23875
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -23494,7 +23878,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
23494
23878
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
23495
23879
|
let raw;
|
|
23496
23880
|
try {
|
|
23497
|
-
raw = await
|
|
23881
|
+
raw = await fs36.readFile(file, "utf8");
|
|
23498
23882
|
} catch {
|
|
23499
23883
|
return {
|
|
23500
23884
|
ok: false,
|
|
@@ -23619,7 +24003,7 @@ async function runCharacterImport(opts) {
|
|
|
23619
24003
|
opts,
|
|
23620
24004
|
ctx,
|
|
23621
24005
|
kind: "character",
|
|
23622
|
-
prompt: `Import ${
|
|
24006
|
+
prompt: `Import ${path36.basename(filePath)} as a rigged character`,
|
|
23623
24007
|
createPath: "/api/characters/import",
|
|
23624
24008
|
body,
|
|
23625
24009
|
quote: price,
|
|
@@ -24164,22 +24548,22 @@ async function context2(opts) {
|
|
|
24164
24548
|
if (!token) return null;
|
|
24165
24549
|
return { token, apiUrl };
|
|
24166
24550
|
}
|
|
24167
|
-
async function readVideo(
|
|
24551
|
+
async function readVideo(path53, log) {
|
|
24168
24552
|
let bytes;
|
|
24169
24553
|
try {
|
|
24170
|
-
bytes = await readFile(
|
|
24554
|
+
bytes = await readFile(path53);
|
|
24171
24555
|
} catch {
|
|
24172
|
-
log.error(`Can't read ${
|
|
24556
|
+
log.error(`Can't read ${path53}.`);
|
|
24173
24557
|
return null;
|
|
24174
24558
|
}
|
|
24175
24559
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
24176
|
-
log.error(`${basename(
|
|
24560
|
+
log.error(`${basename(path53)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
24177
24561
|
return null;
|
|
24178
24562
|
}
|
|
24179
24563
|
return bytes;
|
|
24180
24564
|
}
|
|
24181
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
24182
|
-
const contentType = /\.mov$/i.test(
|
|
24565
|
+
async function uploadVideo(apiUrl, token, characterId, path53, bytes, log) {
|
|
24566
|
+
const contentType = /\.mov$/i.test(path53) ? "video/quicktime" : "video/mp4";
|
|
24183
24567
|
const minted = await apiFetch(
|
|
24184
24568
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
24185
24569
|
{
|
|
@@ -24194,7 +24578,7 @@ async function uploadVideo(apiUrl, token, characterId, path52, bytes, log) {
|
|
|
24194
24578
|
return null;
|
|
24195
24579
|
}
|
|
24196
24580
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
24197
|
-
log.dim(` uploading ${basename(
|
|
24581
|
+
log.dim(` uploading ${basename(path53)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
24198
24582
|
const put = await fetch(uploadUrl, {
|
|
24199
24583
|
method: "PUT",
|
|
24200
24584
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -24526,8 +24910,8 @@ function rank(items, query) {
|
|
|
24526
24910
|
}
|
|
24527
24911
|
|
|
24528
24912
|
// src/commands/motion.ts
|
|
24529
|
-
import
|
|
24530
|
-
import
|
|
24913
|
+
import fs37 from "fs/promises";
|
|
24914
|
+
import path37 from "path";
|
|
24531
24915
|
|
|
24532
24916
|
// src/lib/motion/npz.ts
|
|
24533
24917
|
import zlib2 from "zlib";
|
|
@@ -25778,7 +26162,7 @@ async function motionGen(opts, log) {
|
|
|
25778
26162
|
}
|
|
25779
26163
|
if (opts.constraintsPath !== void 0) {
|
|
25780
26164
|
try {
|
|
25781
|
-
const raw = await
|
|
26165
|
+
const raw = await fs37.readFile(opts.constraintsPath, "utf8");
|
|
25782
26166
|
generationOptions.constraints = JSON.parse(raw);
|
|
25783
26167
|
} catch {
|
|
25784
26168
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -25802,10 +26186,10 @@ async function motionGen(opts, log) {
|
|
|
25802
26186
|
async function expandTakes(selectors) {
|
|
25803
26187
|
const out = [];
|
|
25804
26188
|
for (const sel of selectors) {
|
|
25805
|
-
const st = await
|
|
26189
|
+
const st = await fs37.stat(sel).catch(() => null);
|
|
25806
26190
|
if (st?.isDirectory()) {
|
|
25807
|
-
const names2 = await
|
|
25808
|
-
for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(
|
|
26191
|
+
const names2 = await fs37.readdir(sel);
|
|
26192
|
+
for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(path37.join(sel, n));
|
|
25809
26193
|
} else if (st?.isFile()) {
|
|
25810
26194
|
out.push(sel);
|
|
25811
26195
|
} else {
|
|
@@ -25840,7 +26224,7 @@ async function motionVerify(opts, log) {
|
|
|
25840
26224
|
let gates = DEFAULT_GATES;
|
|
25841
26225
|
if (opts.gatesPath) {
|
|
25842
26226
|
try {
|
|
25843
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
26227
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs37.readFile(opts.gatesPath, "utf8")));
|
|
25844
26228
|
} catch {
|
|
25845
26229
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
25846
26230
|
process.exitCode = 1;
|
|
@@ -25862,9 +26246,9 @@ async function motionVerify(opts, log) {
|
|
|
25862
26246
|
}
|
|
25863
26247
|
const reports = [];
|
|
25864
26248
|
for (const file of files) {
|
|
25865
|
-
const stem =
|
|
26249
|
+
const stem = path37.basename(file).replace(/\.npz$/, "");
|
|
25866
26250
|
try {
|
|
25867
|
-
reports.push(analyzeTake(stem, await
|
|
26251
|
+
reports.push(analyzeTake(stem, await fs37.readFile(file), gates));
|
|
25868
26252
|
} catch (err) {
|
|
25869
26253
|
reports.push({
|
|
25870
26254
|
take: stem,
|
|
@@ -25902,7 +26286,7 @@ async function motionCompile(opts, log) {
|
|
|
25902
26286
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
25903
26287
|
if (opts.configPath) {
|
|
25904
26288
|
try {
|
|
25905
|
-
const patch = JSON.parse(await
|
|
26289
|
+
const patch = JSON.parse(await fs37.readFile(opts.configPath, "utf8"));
|
|
25906
26290
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
25907
26291
|
} catch {
|
|
25908
26292
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -25920,16 +26304,16 @@ async function motionCompile(opts, log) {
|
|
|
25920
26304
|
}
|
|
25921
26305
|
const inputs = [];
|
|
25922
26306
|
for (const file of files) {
|
|
25923
|
-
const stem =
|
|
26307
|
+
const stem = path37.basename(file).replace(/\.npz$/, "");
|
|
25924
26308
|
try {
|
|
25925
|
-
inputs.push({ stem, take: loadTake(await
|
|
26309
|
+
inputs.push({ stem, take: loadTake(await fs37.readFile(file)) });
|
|
25926
26310
|
} catch (err) {
|
|
25927
26311
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
25928
26312
|
process.exitCode = 1;
|
|
25929
26313
|
return;
|
|
25930
26314
|
}
|
|
25931
26315
|
}
|
|
25932
|
-
const setName = opts.set ??
|
|
26316
|
+
const setName = opts.set ?? path37.basename(opts.out).replace(/\.json$/, "");
|
|
25933
26317
|
let result;
|
|
25934
26318
|
try {
|
|
25935
26319
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -25944,9 +26328,9 @@ async function motionCompile(opts, log) {
|
|
|
25944
26328
|
process.exitCode = 1;
|
|
25945
26329
|
return;
|
|
25946
26330
|
}
|
|
25947
|
-
await
|
|
26331
|
+
await fs37.mkdir(path37.dirname(path37.resolve(opts.out)), { recursive: true });
|
|
25948
26332
|
const json = JSON.stringify(result.data);
|
|
25949
|
-
await
|
|
26333
|
+
await fs37.writeFile(opts.out, json);
|
|
25950
26334
|
if (opts.json) {
|
|
25951
26335
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
25952
26336
|
return;
|
|
@@ -25964,9 +26348,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
25964
26348
|
var MOTION_PRESETS = {
|
|
25965
26349
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
25966
26350
|
};
|
|
25967
|
-
var MOTION_DEST =
|
|
26351
|
+
var MOTION_DEST = path37.join("src", "motion");
|
|
25968
26352
|
async function motionInstall(opts, log) {
|
|
25969
|
-
const srcDir =
|
|
26353
|
+
const srcDir = path37.join(getTemplatesDir(), "motion");
|
|
25970
26354
|
const root = opts.cwd ?? process.cwd();
|
|
25971
26355
|
const preset = opts.set;
|
|
25972
26356
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -25977,21 +26361,21 @@ async function motionInstall(opts, log) {
|
|
|
25977
26361
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
25978
26362
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
25979
26363
|
log.plain("");
|
|
25980
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
26364
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path37.sep)}`);
|
|
25981
26365
|
let copied = 0, skipped = 0;
|
|
25982
26366
|
try {
|
|
25983
26367
|
for (const rel of files) {
|
|
25984
|
-
const dest =
|
|
25985
|
-
const exists5 = await
|
|
26368
|
+
const dest = path37.join(root, MOTION_DEST, rel);
|
|
26369
|
+
const exists5 = await fs37.access(dest).then(() => true, () => false);
|
|
25986
26370
|
if (!opts.force && exists5) {
|
|
25987
26371
|
skipped++;
|
|
25988
|
-
log.dim(` skipped ${
|
|
26372
|
+
log.dim(` skipped ${path37.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
25989
26373
|
continue;
|
|
25990
26374
|
}
|
|
25991
|
-
await
|
|
25992
|
-
await
|
|
26375
|
+
await fs37.mkdir(path37.dirname(dest), { recursive: true });
|
|
26376
|
+
await fs37.copyFile(path37.join(srcDir, rel), dest);
|
|
25993
26377
|
copied++;
|
|
25994
|
-
log.dim(` ${
|
|
26378
|
+
log.dim(` ${path37.join(MOTION_DEST, rel)}`);
|
|
25995
26379
|
}
|
|
25996
26380
|
} catch (err) {
|
|
25997
26381
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -26032,7 +26416,7 @@ async function motionConstraints(opts, log) {
|
|
|
26032
26416
|
}
|
|
26033
26417
|
const doc = directionConstraint(dir, speed, duration);
|
|
26034
26418
|
const out = opts.out ?? "constraints.json";
|
|
26035
|
-
await
|
|
26419
|
+
await fs37.writeFile(out, JSON.stringify(doc));
|
|
26036
26420
|
if (opts.json) {
|
|
26037
26421
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
26038
26422
|
return;
|
|
@@ -26069,14 +26453,14 @@ async function runMotion(opts) {
|
|
|
26069
26453
|
}
|
|
26070
26454
|
|
|
26071
26455
|
// src/commands/blender.ts
|
|
26072
|
-
import
|
|
26073
|
-
import
|
|
26456
|
+
import fs38 from "fs/promises";
|
|
26457
|
+
import path38 from "path";
|
|
26074
26458
|
var SUBS3 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
|
|
26075
26459
|
var DEFAULT_OUT_DIR = "assets/blender";
|
|
26076
26460
|
async function writeB64(dir, name, b64) {
|
|
26077
|
-
await
|
|
26078
|
-
const p =
|
|
26079
|
-
await
|
|
26461
|
+
await fs38.mkdir(dir, { recursive: true });
|
|
26462
|
+
const p = path38.join(dir, name);
|
|
26463
|
+
await fs38.writeFile(p, Buffer.from(b64, "base64"));
|
|
26080
26464
|
return p;
|
|
26081
26465
|
}
|
|
26082
26466
|
function reportScene(log, s) {
|
|
@@ -26160,7 +26544,7 @@ async function runBlender(opts) {
|
|
|
26160
26544
|
log.plain(rest.join("\n"));
|
|
26161
26545
|
return 1;
|
|
26162
26546
|
}
|
|
26163
|
-
const outDir =
|
|
26547
|
+
const outDir = path38.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
|
|
26164
26548
|
const mode = opts.mode;
|
|
26165
26549
|
if (mode !== void 0 && !isRenderMode(mode)) {
|
|
26166
26550
|
log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
|
|
@@ -26200,14 +26584,14 @@ async function runBlender(opts) {
|
|
|
26200
26584
|
return 0;
|
|
26201
26585
|
}
|
|
26202
26586
|
case "export": {
|
|
26203
|
-
const target = opts.out ??
|
|
26587
|
+
const target = opts.out ?? path38.join(outDir, "scene.glb");
|
|
26204
26588
|
const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
|
|
26205
26589
|
if (!r.glbBase64) {
|
|
26206
26590
|
log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
|
|
26207
26591
|
return 1;
|
|
26208
26592
|
}
|
|
26209
|
-
await
|
|
26210
|
-
await
|
|
26593
|
+
await fs38.mkdir(path38.dirname(target), { recursive: true });
|
|
26594
|
+
await fs38.writeFile(target, Buffer.from(r.glbBase64, "base64"));
|
|
26211
26595
|
log.success(`Exported ${r.bytes ?? 0} bytes`);
|
|
26212
26596
|
log.plain(` ${c.cyan(target)}`);
|
|
26213
26597
|
return 0;
|
|
@@ -26240,12 +26624,12 @@ async function runBlender(opts) {
|
|
|
26240
26624
|
return 1;
|
|
26241
26625
|
}
|
|
26242
26626
|
try {
|
|
26243
|
-
script = await
|
|
26627
|
+
script = await fs38.readFile(opts.input, "utf8");
|
|
26244
26628
|
} catch {
|
|
26245
26629
|
log.error(`Can't read ${opts.input}`);
|
|
26246
26630
|
return 1;
|
|
26247
26631
|
}
|
|
26248
|
-
label =
|
|
26632
|
+
label = path38.basename(opts.input);
|
|
26249
26633
|
}
|
|
26250
26634
|
const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
|
|
26251
26635
|
if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
|
|
@@ -26364,10 +26748,10 @@ print(f"castle: {n} objects")
|
|
|
26364
26748
|
`;
|
|
26365
26749
|
|
|
26366
26750
|
// src/commands/asset-new.ts
|
|
26367
|
-
import
|
|
26751
|
+
import fs39 from "fs";
|
|
26368
26752
|
import fsp from "fs/promises";
|
|
26369
|
-
import
|
|
26370
|
-
import { pathToFileURL } from "url";
|
|
26753
|
+
import path39 from "path";
|
|
26754
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
26371
26755
|
var EXTRA_FILES = [
|
|
26372
26756
|
"genex-asset.example.json",
|
|
26373
26757
|
"public/fonts/ZalandoSansSemiExpanded-variable.woff2",
|
|
@@ -26460,7 +26844,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
26460
26844
|
}
|
|
26461
26845
|
async function runAssetNew(options) {
|
|
26462
26846
|
const log = createLogger();
|
|
26463
|
-
const cwd = options.dir ?
|
|
26847
|
+
const cwd = options.dir ? path39.resolve(options.dir) : process.cwd();
|
|
26464
26848
|
const slug = options.assetSlug;
|
|
26465
26849
|
if (!slug) {
|
|
26466
26850
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -26470,14 +26854,14 @@ async function runAssetNew(options) {
|
|
|
26470
26854
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
26471
26855
|
return 1;
|
|
26472
26856
|
}
|
|
26473
|
-
const templateDir =
|
|
26474
|
-
if (!
|
|
26857
|
+
const templateDir = path39.join(getTemplatesDir(), "asset-viewer");
|
|
26858
|
+
if (!fs39.existsSync(templateDir)) {
|
|
26475
26859
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
26476
26860
|
return 1;
|
|
26477
26861
|
}
|
|
26478
|
-
const manifestTools = await import(
|
|
26862
|
+
const manifestTools = await import(pathToFileURL2(path39.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
26479
26863
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
26480
|
-
const lockPath =
|
|
26864
|
+
const lockPath = path39.join(templateDir, "shared-files.sha256.json");
|
|
26481
26865
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
26482
26866
|
const actual = hashSharedFiles(templateDir);
|
|
26483
26867
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -26490,8 +26874,8 @@ async function runAssetNew(options) {
|
|
|
26490
26874
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
26491
26875
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
26492
26876
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
26493
|
-
const outDir =
|
|
26494
|
-
if (
|
|
26877
|
+
const outDir = path39.resolve(cwd, options.out ?? slug);
|
|
26878
|
+
if (fs39.existsSync(outDir) && fs39.readdirSync(outDir).length > 0 && !options.force) {
|
|
26495
26879
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
26496
26880
|
return 1;
|
|
26497
26881
|
}
|
|
@@ -26519,24 +26903,24 @@ async function runAssetNew(options) {
|
|
|
26519
26903
|
};
|
|
26520
26904
|
await fsp.mkdir(outDir, { recursive: true });
|
|
26521
26905
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
26522
|
-
const to =
|
|
26523
|
-
await fsp.mkdir(
|
|
26524
|
-
await fsp.copyFile(
|
|
26906
|
+
const to = path39.join(outDir, rel);
|
|
26907
|
+
await fsp.mkdir(path39.dirname(to), { recursive: true });
|
|
26908
|
+
await fsp.copyFile(path39.join(templateDir, rel), to);
|
|
26525
26909
|
}
|
|
26526
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
26910
|
+
const pkg = fillTemplate(await fsp.readFile(path39.join(templateDir, "package.json"), "utf8"), {
|
|
26527
26911
|
slug,
|
|
26528
26912
|
name,
|
|
26529
26913
|
version
|
|
26530
26914
|
});
|
|
26531
|
-
await fsp.writeFile(
|
|
26532
|
-
await fsp.writeFile(
|
|
26533
|
-
await fsp.writeFile(
|
|
26915
|
+
await fsp.writeFile(path39.join(outDir, "package.json"), pkg, "utf8");
|
|
26916
|
+
await fsp.writeFile(path39.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
26917
|
+
await fsp.writeFile(path39.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
26534
26918
|
await fsp.writeFile(
|
|
26535
|
-
|
|
26919
|
+
path39.join(outDir, "DESIGN.md"),
|
|
26536
26920
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
26537
26921
|
"utf8"
|
|
26538
26922
|
);
|
|
26539
|
-
const placeholder = await fsp.readFile(
|
|
26923
|
+
const placeholder = await fsp.readFile(path39.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
26540
26924
|
const seeded = seedAssetSource(placeholder, {
|
|
26541
26925
|
slug,
|
|
26542
26926
|
name,
|
|
@@ -26547,8 +26931,8 @@ async function runAssetNew(options) {
|
|
|
26547
26931
|
pascalCase
|
|
26548
26932
|
});
|
|
26549
26933
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
26550
|
-
await fsp.mkdir(
|
|
26551
|
-
await fsp.writeFile(
|
|
26934
|
+
await fsp.mkdir(path39.join(outDir, "src", "asset"), { recursive: true });
|
|
26935
|
+
await fsp.writeFile(path39.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
26552
26936
|
const copied = hashSharedFiles(outDir);
|
|
26553
26937
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
26554
26938
|
if (mismatched.length) {
|
|
@@ -26556,7 +26940,7 @@ async function runAssetNew(options) {
|
|
|
26556
26940
|
return 1;
|
|
26557
26941
|
}
|
|
26558
26942
|
await fsp.writeFile(
|
|
26559
|
-
|
|
26943
|
+
path39.join(outDir, PARITY_FILENAME),
|
|
26560
26944
|
JSON.stringify(
|
|
26561
26945
|
{
|
|
26562
26946
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -26600,14 +26984,14 @@ async function runAssetNew(options) {
|
|
|
26600
26984
|
}
|
|
26601
26985
|
|
|
26602
26986
|
// src/commands/tools.ts
|
|
26603
|
-
import
|
|
26987
|
+
import path42 from "path";
|
|
26604
26988
|
|
|
26605
26989
|
// src/commands/doctor.ts
|
|
26606
|
-
import
|
|
26990
|
+
import path41 from "path";
|
|
26607
26991
|
|
|
26608
26992
|
// src/commands/llm.ts
|
|
26609
|
-
import
|
|
26610
|
-
import
|
|
26993
|
+
import fs40 from "fs/promises";
|
|
26994
|
+
import path40 from "path";
|
|
26611
26995
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
26612
26996
|
var SUBS4 = ["models", "bench", "price", "status", "cancel"];
|
|
26613
26997
|
var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends YOUR OWN credits \u2014 your balance, not a player's, and a spend this build's asset allowance does not count. Re-run with --max-credits <n> --user-approved only after the person at the keyboard agreed to the number.";
|
|
@@ -27129,7 +27513,7 @@ async function runBench(args) {
|
|
|
27129
27513
|
let schema;
|
|
27130
27514
|
if (opts.schemaPath) {
|
|
27131
27515
|
try {
|
|
27132
|
-
schema = JSON.parse(await
|
|
27516
|
+
schema = JSON.parse(await fs40.readFile(path40.resolve(cwd, opts.schemaPath), "utf8"));
|
|
27133
27517
|
} catch (err) {
|
|
27134
27518
|
benchFailed(opts, log, `Couldn't read --schema ${opts.schemaPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
27135
27519
|
return;
|
|
@@ -27511,9 +27895,9 @@ async function pollSettled(call3, url, timeoutSec) {
|
|
|
27511
27895
|
}
|
|
27512
27896
|
async function saveBench(cwd, record) {
|
|
27513
27897
|
try {
|
|
27514
|
-
const file =
|
|
27515
|
-
await
|
|
27516
|
-
await
|
|
27898
|
+
const file = path40.join(cwd, LLM_BENCH_FILE);
|
|
27899
|
+
await fs40.mkdir(path40.dirname(file), { recursive: true });
|
|
27900
|
+
await fs40.writeFile(file, JSON.stringify(record, null, 2) + "\n");
|
|
27517
27901
|
return LLM_BENCH_FILE;
|
|
27518
27902
|
} catch {
|
|
27519
27903
|
return null;
|
|
@@ -27726,7 +28110,7 @@ async function cancelCall(args) {
|
|
|
27726
28110
|
async function readSavedBench(cwd) {
|
|
27727
28111
|
let parsed;
|
|
27728
28112
|
try {
|
|
27729
|
-
parsed = JSON.parse(await
|
|
28113
|
+
parsed = JSON.parse(await fs40.readFile(path40.join(cwd, LLM_BENCH_FILE), "utf8"));
|
|
27730
28114
|
} catch {
|
|
27731
28115
|
return null;
|
|
27732
28116
|
}
|
|
@@ -28158,7 +28542,7 @@ async function fetchLegalStatus(apiUrl, token) {
|
|
|
28158
28542
|
}
|
|
28159
28543
|
async function firstSkillsMarker() {
|
|
28160
28544
|
for (const target of resolveAgentTargets()) {
|
|
28161
|
-
const marker = await readSkillsMarker(
|
|
28545
|
+
const marker = await readSkillsMarker(path41.join(target.baseDir, "skills"));
|
|
28162
28546
|
if (marker) return marker;
|
|
28163
28547
|
}
|
|
28164
28548
|
return null;
|
|
@@ -28210,11 +28594,11 @@ async function runTools(opts) {
|
|
|
28210
28594
|
let totalUpdated = 0;
|
|
28211
28595
|
const refused = [];
|
|
28212
28596
|
for (const t of targets) {
|
|
28213
|
-
const dest =
|
|
28597
|
+
const dest = path42.join(t.baseDir, "skills");
|
|
28214
28598
|
let copied;
|
|
28215
28599
|
let updated;
|
|
28216
28600
|
try {
|
|
28217
|
-
({ copied, updated } = await copyTemplates(
|
|
28601
|
+
({ copied, updated } = await copyTemplates(path42.join(templatesDir, "skills"), dest, {
|
|
28218
28602
|
filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
|
|
28219
28603
|
}));
|
|
28220
28604
|
await pruneRemovedSkills(dest, log);
|
|
@@ -28286,8 +28670,8 @@ async function runTools(opts) {
|
|
|
28286
28670
|
}
|
|
28287
28671
|
|
|
28288
28672
|
// src/commands/budget.ts
|
|
28289
|
-
import
|
|
28290
|
-
import
|
|
28673
|
+
import fs41 from "fs/promises";
|
|
28674
|
+
import path43 from "path";
|
|
28291
28675
|
var BUDGET_SOURCE_LINE = "Live from your account; prices can change without a deploy.";
|
|
28292
28676
|
var BUDGET_APPROVAL_REQUIRED = "STOP: raising the asset allowance requires the player's explicit approval. Re-run with --user-approved only after they agreed to the number.";
|
|
28293
28677
|
function internalKind(kind) {
|
|
@@ -28295,7 +28679,7 @@ function internalKind(kind) {
|
|
|
28295
28679
|
}
|
|
28296
28680
|
async function isGenexProject(cwd) {
|
|
28297
28681
|
try {
|
|
28298
|
-
await
|
|
28682
|
+
await fs41.access(path43.join(cwd, ".genex"));
|
|
28299
28683
|
return true;
|
|
28300
28684
|
} catch {
|
|
28301
28685
|
return false;
|
|
@@ -28444,39 +28828,39 @@ async function runBudget(opts = {}) {
|
|
|
28444
28828
|
}
|
|
28445
28829
|
|
|
28446
28830
|
// src/commands/player.ts
|
|
28447
|
-
import
|
|
28831
|
+
import fs46 from "fs";
|
|
28448
28832
|
import fsp2 from "fs/promises";
|
|
28449
28833
|
import os13 from "os";
|
|
28450
|
-
import
|
|
28834
|
+
import path49 from "path";
|
|
28451
28835
|
import { spawn as spawn8 } from "child_process";
|
|
28452
28836
|
|
|
28453
28837
|
// src/lib/player/paths.ts
|
|
28454
28838
|
import os9 from "os";
|
|
28455
|
-
import
|
|
28839
|
+
import path44 from "path";
|
|
28456
28840
|
var PLAYER_DRY_RUN_ENV = "GENEX_PLAYER_DRY_RUN";
|
|
28457
28841
|
function playerDryRun() {
|
|
28458
28842
|
const raw = process.env[PLAYER_DRY_RUN_ENV];
|
|
28459
28843
|
return raw === "1" || raw === "true";
|
|
28460
28844
|
}
|
|
28461
28845
|
function playerPaths(home = os9.homedir()) {
|
|
28462
|
-
const dir = home === os9.homedir() ? getGenexDir() :
|
|
28463
|
-
const logDir =
|
|
28846
|
+
const dir = home === os9.homedir() ? getGenexDir() : path44.join(home, ".genex");
|
|
28847
|
+
const logDir = path44.join(dir, "logs");
|
|
28464
28848
|
return {
|
|
28465
|
-
credential:
|
|
28466
|
-
state:
|
|
28467
|
-
relaunch:
|
|
28849
|
+
credential: path44.join(dir, "player.json"),
|
|
28850
|
+
state: path44.join(dir, "player-state.json"),
|
|
28851
|
+
relaunch: path44.join(dir, "player-relaunch.json"),
|
|
28468
28852
|
logDir,
|
|
28469
|
-
log:
|
|
28470
|
-
errorLog:
|
|
28853
|
+
log: path44.join(logDir, "player.log"),
|
|
28854
|
+
errorLog: path44.join(logDir, "player.error.log")
|
|
28471
28855
|
};
|
|
28472
28856
|
}
|
|
28473
28857
|
|
|
28474
28858
|
// src/lib/player/store.ts
|
|
28475
|
-
import
|
|
28476
|
-
import
|
|
28859
|
+
import fs42 from "fs/promises";
|
|
28860
|
+
import path45 from "path";
|
|
28477
28861
|
async function readPlayerInstall(home) {
|
|
28478
28862
|
try {
|
|
28479
|
-
const raw = await
|
|
28863
|
+
const raw = await fs42.readFile(playerPaths(home).credential, "utf8");
|
|
28480
28864
|
const parsed = JSON.parse(raw);
|
|
28481
28865
|
if (!parsed || typeof parsed.token !== "string" || typeof parsed.apiUrl !== "string") return null;
|
|
28482
28866
|
return parsed;
|
|
@@ -28506,7 +28890,7 @@ function describeInstall(install2) {
|
|
|
28506
28890
|
}
|
|
28507
28891
|
async function readPlayerState(home) {
|
|
28508
28892
|
try {
|
|
28509
|
-
return JSON.parse(await
|
|
28893
|
+
return JSON.parse(await fs42.readFile(playerPaths(home).state, "utf8"));
|
|
28510
28894
|
} catch {
|
|
28511
28895
|
return null;
|
|
28512
28896
|
}
|
|
@@ -28514,8 +28898,8 @@ async function readPlayerState(home) {
|
|
|
28514
28898
|
async function writePlayerState(state, home) {
|
|
28515
28899
|
const file = playerPaths(home).state;
|
|
28516
28900
|
try {
|
|
28517
|
-
await
|
|
28518
|
-
await
|
|
28901
|
+
await fs42.mkdir(path45.dirname(file), { recursive: true, mode: 448 });
|
|
28902
|
+
await fs42.writeFile(file, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
28519
28903
|
} catch {
|
|
28520
28904
|
}
|
|
28521
28905
|
}
|
|
@@ -28523,8 +28907,8 @@ async function removePlayerFiles(home) {
|
|
|
28523
28907
|
const paths = playerPaths(home);
|
|
28524
28908
|
const removed = [];
|
|
28525
28909
|
for (const file of [paths.credential, paths.state, paths.relaunch]) {
|
|
28526
|
-
const existed = await
|
|
28527
|
-
await
|
|
28910
|
+
const existed = await fs42.stat(file).then(() => true, () => false);
|
|
28911
|
+
await fs42.rm(file, { force: true }).catch(() => {
|
|
28528
28912
|
});
|
|
28529
28913
|
if (existed) removed.push(file);
|
|
28530
28914
|
}
|
|
@@ -28658,14 +29042,14 @@ var PlayerClient = class {
|
|
|
28658
29042
|
this.version = options.version ?? getCliVersion();
|
|
28659
29043
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
28660
29044
|
}
|
|
28661
|
-
async call(
|
|
29045
|
+
async call(path53, init = {}, provider) {
|
|
28662
29046
|
const headers = {
|
|
28663
29047
|
Authorization: `Bearer ${this.token}`,
|
|
28664
29048
|
[PLAYER_VERSION_HEADER]: this.version,
|
|
28665
29049
|
...provider ? { [EXTERNAL_PROVIDER_HEADER]: provider } : {},
|
|
28666
29050
|
...init.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
28667
29051
|
};
|
|
28668
|
-
const res = await apiFetch(`${this.apiUrl}${
|
|
29052
|
+
const res = await apiFetch(`${this.apiUrl}${path53}`, {
|
|
28669
29053
|
...init,
|
|
28670
29054
|
headers,
|
|
28671
29055
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
@@ -28734,10 +29118,10 @@ var PlayerClient = class {
|
|
|
28734
29118
|
|
|
28735
29119
|
// src/lib/player/runners.ts
|
|
28736
29120
|
import { spawn as spawn6, spawnSync } from "child_process";
|
|
28737
|
-
import
|
|
29121
|
+
import fs43 from "fs/promises";
|
|
28738
29122
|
import fsSync from "fs";
|
|
28739
29123
|
import os10 from "os";
|
|
28740
|
-
import
|
|
29124
|
+
import path46 from "path";
|
|
28741
29125
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
28742
29126
|
var WATCHER_RUNNERS = ["claude", "codex"];
|
|
28743
29127
|
var RUNNER_FOR_FAMILY = { claude: "claude", chatgpt: "codex" };
|
|
@@ -28795,7 +29179,7 @@ function resolveOnPath(name, env = process.env, platform = process.platform) {
|
|
|
28795
29179
|
const candidates = win ? [`${name}.cmd`, `${name}.exe`, `${name}.bat`, name] : [name];
|
|
28796
29180
|
for (const dir of dirs) {
|
|
28797
29181
|
for (const candidate of candidates) {
|
|
28798
|
-
const file =
|
|
29182
|
+
const file = path46.join(dir, candidate);
|
|
28799
29183
|
try {
|
|
28800
29184
|
const stat = fsSync.statSync(file);
|
|
28801
29185
|
if (!stat.isFile()) continue;
|
|
@@ -28959,17 +29343,17 @@ async function runJob(job, options = {}) {
|
|
|
28959
29343
|
if (!resolved) {
|
|
28960
29344
|
return { ok: false, release: "runner_unavailable", detail: `missing:${runner}`, durationMs: Date.now() - started };
|
|
28961
29345
|
}
|
|
28962
|
-
const cwd =
|
|
28963
|
-
await
|
|
28964
|
-
const outFile =
|
|
29346
|
+
const cwd = path46.join(os10.tmpdir(), `genex-player-${randomUUID3()}`);
|
|
29347
|
+
await fs43.mkdir(cwd, { recursive: true, mode: 448 });
|
|
29348
|
+
const outFile = path46.join(cwd, "result.out");
|
|
28965
29349
|
let argv;
|
|
28966
29350
|
if (runner === "claude") {
|
|
28967
29351
|
argv = claudeArgv(job, policy.claude ?? FALLBACK_POLICY.claude ?? {});
|
|
28968
29352
|
} else {
|
|
28969
29353
|
let schemaFile;
|
|
28970
29354
|
if (job.outputFormat === "json" && job.schema !== void 0) {
|
|
28971
|
-
schemaFile =
|
|
28972
|
-
await
|
|
29355
|
+
schemaFile = path46.join(cwd, "schema.json");
|
|
29356
|
+
await fs43.writeFile(schemaFile, JSON.stringify(job.schema), { mode: 384 });
|
|
28973
29357
|
}
|
|
28974
29358
|
argv = codexArgv(job, policy.codex ?? FALLBACK_POLICY.codex ?? {}, { cwd, outFile, ...schemaFile ? { schemaFile } : {} });
|
|
28975
29359
|
}
|
|
@@ -29033,7 +29417,7 @@ async function runJob(job, options = {}) {
|
|
|
29033
29417
|
if (!read2.ok) return { ok: false, release: "failed", detail: read2.detail, exitCode: code, durationMs };
|
|
29034
29418
|
return { ok: true, output: read2.output, exitCode: code, durationMs };
|
|
29035
29419
|
}
|
|
29036
|
-
const text = await
|
|
29420
|
+
const text = await fs43.readFile(outFile, "utf8").catch(() => "");
|
|
29037
29421
|
if (code !== 0 && !text.trim()) {
|
|
29038
29422
|
const classified = classifyFailure(`${stderrText} exit ${code}`);
|
|
29039
29423
|
return { ok: false, ...classified, detail: `exit_${code}`, exitCode: code, durationMs };
|
|
@@ -29042,15 +29426,15 @@ async function runJob(job, options = {}) {
|
|
|
29042
29426
|
if (!read.ok) return { ok: false, release: "failed", detail: read.detail, exitCode: code, durationMs };
|
|
29043
29427
|
return { ok: true, output: read.output, exitCode: code, durationMs };
|
|
29044
29428
|
} finally {
|
|
29045
|
-
await
|
|
29429
|
+
await fs43.rm(cwd, { recursive: true, force: true }).catch(() => {
|
|
29046
29430
|
});
|
|
29047
29431
|
}
|
|
29048
29432
|
}
|
|
29049
29433
|
|
|
29050
29434
|
// src/lib/player/service.ts
|
|
29051
|
-
import
|
|
29435
|
+
import fs44 from "fs/promises";
|
|
29052
29436
|
import os11 from "os";
|
|
29053
|
-
import
|
|
29437
|
+
import path47 from "path";
|
|
29054
29438
|
import { execFile } from "child_process";
|
|
29055
29439
|
import { promisify } from "util";
|
|
29056
29440
|
var run2 = promisify(execFile);
|
|
@@ -29067,9 +29451,9 @@ function servicePlatform(platform = process.platform) {
|
|
|
29067
29451
|
}
|
|
29068
29452
|
function servicePaths(platform, home = os11.homedir()) {
|
|
29069
29453
|
const { log, errorLog } = playerPaths(home);
|
|
29070
|
-
if (platform === "darwin") return { log, errorLog, unit:
|
|
29071
|
-
if (platform === "linux") return { log, errorLog, unit:
|
|
29072
|
-
return { log, errorLog, unit:
|
|
29454
|
+
if (platform === "darwin") return { log, errorLog, unit: path47.join(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) };
|
|
29455
|
+
if (platform === "linux") return { log, errorLog, unit: path47.join(home, ".config", "systemd", "user", SYSTEMD_UNIT) };
|
|
29456
|
+
return { log, errorLog, unit: path47.join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "genex-player.cmd") };
|
|
29073
29457
|
}
|
|
29074
29458
|
function xmlEscape(value) {
|
|
29075
29459
|
return value.replace(/[<>&'"]/g, (ch) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ })[ch]);
|
|
@@ -29148,9 +29532,9 @@ function autostartCommand(opts) {
|
|
|
29148
29532
|
}
|
|
29149
29533
|
async function installService(input, platform = servicePlatform()) {
|
|
29150
29534
|
const paths = servicePaths(platform, input.home);
|
|
29151
|
-
await
|
|
29152
|
-
await
|
|
29153
|
-
await
|
|
29535
|
+
await fs44.mkdir(path47.dirname(paths.unit), { recursive: true });
|
|
29536
|
+
await fs44.mkdir(path47.dirname(paths.log), { recursive: true, mode: 448 });
|
|
29537
|
+
await fs44.writeFile(paths.unit, unitFile(platform, input, paths), { mode: platform === "win32" ? 420 : 384 });
|
|
29154
29538
|
if (playerDryRun()) return { unit: paths.unit, loaded: false, detail: "dry run \u2014 the unit was written, the system was not asked" };
|
|
29155
29539
|
if (platform === "darwin") {
|
|
29156
29540
|
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
@@ -29179,7 +29563,7 @@ async function installService(input, platform = servicePlatform()) {
|
|
|
29179
29563
|
}
|
|
29180
29564
|
async function serviceInstalled(home = os11.homedir(), platform = servicePlatform()) {
|
|
29181
29565
|
const paths = servicePaths(platform, home);
|
|
29182
|
-
return await
|
|
29566
|
+
return await fs44.stat(paths.unit).then(() => paths.unit, () => null);
|
|
29183
29567
|
}
|
|
29184
29568
|
async function stopService(home = os11.homedir(), platform = servicePlatform()) {
|
|
29185
29569
|
const paths = servicePaths(platform, home);
|
|
@@ -29209,15 +29593,15 @@ async function uninstallService(home = os11.homedir(), platform = servicePlatfor
|
|
|
29209
29593
|
});
|
|
29210
29594
|
}
|
|
29211
29595
|
}
|
|
29212
|
-
await
|
|
29596
|
+
await fs44.rm(paths.unit, { force: true }).catch(() => {
|
|
29213
29597
|
});
|
|
29214
29598
|
return { unit: paths.unit, loaded: false, detail: present ? "removed" : "nothing installed" };
|
|
29215
29599
|
}
|
|
29216
29600
|
|
|
29217
29601
|
// src/lib/player/relaunch.ts
|
|
29218
|
-
import
|
|
29602
|
+
import fs45 from "fs/promises";
|
|
29219
29603
|
import fsSync2 from "fs";
|
|
29220
|
-
import
|
|
29604
|
+
import path48 from "path";
|
|
29221
29605
|
import { spawn as spawn7 } from "child_process";
|
|
29222
29606
|
var RELAUNCH_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
29223
29607
|
function relaunchPlan(input) {
|
|
@@ -29245,7 +29629,7 @@ function relaunchPossible() {
|
|
|
29245
29629
|
}
|
|
29246
29630
|
async function relaunchOnCooldown(home, now = Date.now()) {
|
|
29247
29631
|
try {
|
|
29248
|
-
const raw = await
|
|
29632
|
+
const raw = await fs45.readFile(playerPaths(home).relaunch, "utf8");
|
|
29249
29633
|
const at = Date.parse(JSON.parse(raw).at ?? "");
|
|
29250
29634
|
return Number.isFinite(at) && now - at < RELAUNCH_COOLDOWN_MS;
|
|
29251
29635
|
} catch {
|
|
@@ -29255,8 +29639,8 @@ async function relaunchOnCooldown(home, now = Date.now()) {
|
|
|
29255
29639
|
async function markRelaunch(version, home) {
|
|
29256
29640
|
const file = playerPaths(home).relaunch;
|
|
29257
29641
|
try {
|
|
29258
|
-
await
|
|
29259
|
-
await
|
|
29642
|
+
await fs45.mkdir(path48.dirname(file), { recursive: true, mode: 448 });
|
|
29643
|
+
await fs45.writeFile(file, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), version }) + "\n", { mode: 384 });
|
|
29260
29644
|
} catch {
|
|
29261
29645
|
}
|
|
29262
29646
|
}
|
|
@@ -29280,7 +29664,7 @@ async function relaunchWatcher(input) {
|
|
|
29280
29664
|
let err;
|
|
29281
29665
|
const paths = playerPaths(input.home);
|
|
29282
29666
|
try {
|
|
29283
|
-
await
|
|
29667
|
+
await fs45.mkdir(paths.logDir, { recursive: true, mode: 448 });
|
|
29284
29668
|
out = fsSync2.openSync(paths.log, "a", 384);
|
|
29285
29669
|
err = fsSync2.openSync(paths.errorLog, "a", 384);
|
|
29286
29670
|
} catch {
|
|
@@ -29761,8 +30145,8 @@ async function spawnDetachedRun(apiUrl, opts) {
|
|
|
29761
30145
|
try {
|
|
29762
30146
|
const paths = playerPaths();
|
|
29763
30147
|
await fsp2.mkdir(paths.logDir, { recursive: true, mode: 448 });
|
|
29764
|
-
out =
|
|
29765
|
-
err =
|
|
30148
|
+
out = fs46.openSync(paths.log, "a", 384);
|
|
30149
|
+
err = fs46.openSync(paths.errorLog, "a", 384);
|
|
29766
30150
|
const args = [process.argv[1] ?? "", "player", "run", ...opts.apiUrl ? ["--api-url", apiUrl] : []];
|
|
29767
30151
|
const child = spawn8(process.execPath, args, { detached: true, stdio: ["ignore", out, err] });
|
|
29768
30152
|
child.on("error", () => {
|
|
@@ -29773,7 +30157,7 @@ async function spawnDetachedRun(apiUrl, opts) {
|
|
|
29773
30157
|
return false;
|
|
29774
30158
|
} finally {
|
|
29775
30159
|
for (const fd of [out, err]) if (fd !== void 0) try {
|
|
29776
|
-
|
|
30160
|
+
fs46.closeSync(fd);
|
|
29777
30161
|
} catch {
|
|
29778
30162
|
}
|
|
29779
30163
|
}
|
|
@@ -29972,7 +30356,7 @@ async function uninstall(opts, log) {
|
|
|
29972
30356
|
writeJsonLine2({ command: "player uninstall", status: "ok", error: null, unit: service.unit, detail: service.detail, removed });
|
|
29973
30357
|
return;
|
|
29974
30358
|
}
|
|
29975
|
-
log.success(`Removed: ${service.detail} (${
|
|
30359
|
+
log.success(`Removed: ${service.detail} (${path49.basename(service.unit)})`);
|
|
29976
30360
|
for (const file of removed) log.dim(` deleted ${file}`);
|
|
29977
30361
|
if (removed.length === 0) log.dim(" no credential was stored here");
|
|
29978
30362
|
log.plain("");
|
|
@@ -29990,21 +30374,21 @@ function writeJsonLine2(value) {
|
|
|
29990
30374
|
}
|
|
29991
30375
|
|
|
29992
30376
|
// src/commands/native.ts
|
|
29993
|
-
import
|
|
30377
|
+
import fs52 from "fs/promises";
|
|
29994
30378
|
import os15 from "os";
|
|
29995
|
-
import
|
|
30379
|
+
import path52 from "path";
|
|
29996
30380
|
|
|
29997
30381
|
// src/lib/native-build.ts
|
|
29998
30382
|
import { createHash as createHash5 } from "crypto";
|
|
29999
30383
|
import { createReadStream } from "fs";
|
|
30000
|
-
import
|
|
30384
|
+
import fs48 from "fs/promises";
|
|
30001
30385
|
import os14 from "os";
|
|
30002
|
-
import
|
|
30386
|
+
import path50 from "path";
|
|
30003
30387
|
import { execFile as execFile2 } from "child_process";
|
|
30004
30388
|
import { promisify as promisify2 } from "util";
|
|
30005
30389
|
|
|
30006
30390
|
// src/lib/native-zip.ts
|
|
30007
|
-
import
|
|
30391
|
+
import fs47 from "fs/promises";
|
|
30008
30392
|
import zlib3 from "zlib";
|
|
30009
30393
|
var EOCD_SIGNATURE2 = 101010256;
|
|
30010
30394
|
var EOCD64_LOCATOR_SIGNATURE = 117853008;
|
|
@@ -30106,7 +30490,7 @@ function entryAt(data, at) {
|
|
|
30106
30490
|
return { entry, madeBy, external, next: at + 46 + nameLength + extraLength + commentLength };
|
|
30107
30491
|
}
|
|
30108
30492
|
async function readZipEntries(filePath) {
|
|
30109
|
-
const file = await
|
|
30493
|
+
const file = await fs47.open(filePath, "r");
|
|
30110
30494
|
try {
|
|
30111
30495
|
const { size } = await file.stat();
|
|
30112
30496
|
const directory = await findDirectory2(file, size);
|
|
@@ -30125,7 +30509,7 @@ async function readZipEntries(filePath) {
|
|
|
30125
30509
|
}
|
|
30126
30510
|
}
|
|
30127
30511
|
async function readZipEntryHead(filePath, entry, maxBytes) {
|
|
30128
|
-
const file = await
|
|
30512
|
+
const file = await fs47.open(filePath, "r");
|
|
30129
30513
|
try {
|
|
30130
30514
|
return await entryHead(file, entry, maxBytes);
|
|
30131
30515
|
} finally {
|
|
@@ -30154,7 +30538,7 @@ function wantedMode(mode, directory, program) {
|
|
|
30154
30538
|
return (mode | S_IFREG2 | EXECUTE_BITS) >>> 0;
|
|
30155
30539
|
}
|
|
30156
30540
|
async function markPrograms(filePath) {
|
|
30157
|
-
const file = await
|
|
30541
|
+
const file = await fs47.open(filePath, "r+");
|
|
30158
30542
|
try {
|
|
30159
30543
|
const { size } = await file.stat();
|
|
30160
30544
|
const directory = await findDirectory2(file, size);
|
|
@@ -30234,7 +30618,7 @@ function rootApps(entries) {
|
|
|
30234
30618
|
}
|
|
30235
30619
|
function launchableExes(entries) {
|
|
30236
30620
|
const exes = entries.filter((e) => !isDirectory(e) && e.name.toLowerCase().endsWith(".exe"));
|
|
30237
|
-
const games = exes.filter((e) => e.name.split("/").length <= ENTRY_DEPTH_MAX && !HELPER_EXE.test(
|
|
30621
|
+
const games = exes.filter((e) => e.name.split("/").length <= ENTRY_DEPTH_MAX && !HELPER_EXE.test(path50.posix.basename(e.name)));
|
|
30238
30622
|
const depth = (name) => name.split("/").length;
|
|
30239
30623
|
return games.map((e) => e.name).sort((a, b) => depth(a) - depth(b) || a.localeCompare(b));
|
|
30240
30624
|
}
|
|
@@ -30316,7 +30700,7 @@ function exeArch(head) {
|
|
|
30316
30700
|
}
|
|
30317
30701
|
function engineOf(entries) {
|
|
30318
30702
|
const names2 = entries.map((e) => e.name.toLowerCase());
|
|
30319
|
-
const base = (name) =>
|
|
30703
|
+
const base = (name) => path50.posix.basename(name.replace(/\/$/, ""));
|
|
30320
30704
|
if (names2.some((n) => /^unityplayer\.(dll|dylib|so)$/.test(base(n)) || base(n) === "globalgamemanagers")) return "unity";
|
|
30321
30705
|
if (names2.some((n) => n.startsWith("engine/binaries/") || /\.app\/contents\/ue[45]?\//.test(n) || n.includes("/engine/binaries/")))
|
|
30322
30706
|
return "unreal";
|
|
@@ -30360,7 +30744,7 @@ async function describeNativeArchive(zipPath, asked = {}) {
|
|
|
30360
30744
|
try {
|
|
30361
30745
|
entries = await readZipEntries(zipPath);
|
|
30362
30746
|
} catch (err) {
|
|
30363
|
-
throw new NativeBuildError(`Couldn't read ${
|
|
30747
|
+
throw new NativeBuildError(`Couldn't read ${path50.basename(zipPath)} as a zip: ${err.message}`);
|
|
30364
30748
|
}
|
|
30365
30749
|
if (entries.length === 0) throw new NativeBuildError("The archive is empty.");
|
|
30366
30750
|
const platform = platformOf(entries, asked.platform);
|
|
@@ -30382,14 +30766,14 @@ async function describeNativeArchive(zipPath, asked = {}) {
|
|
|
30382
30766
|
...mac?.appVersion ? { appVersion: mac.appVersion } : {}
|
|
30383
30767
|
};
|
|
30384
30768
|
}
|
|
30385
|
-
function buildVersion(asked, appVersion,
|
|
30769
|
+
function buildVersion(asked, appVersion, sha2565) {
|
|
30386
30770
|
if (asked !== void 0) {
|
|
30387
30771
|
if (asked.length > VERSION_MAX || !VERSION2.test(asked)) {
|
|
30388
30772
|
throw new NativeBuildError(`--build-version "${asked}" may hold only letters, digits and . _ + - (at most ${VERSION_MAX}).`);
|
|
30389
30773
|
}
|
|
30390
30774
|
return asked;
|
|
30391
30775
|
}
|
|
30392
|
-
const hash =
|
|
30776
|
+
const hash = sha2565.slice(0, VERSION_SHA_CHARS);
|
|
30393
30777
|
const app = (appVersion ?? "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, VERSION_MAX - hash.length - 1);
|
|
30394
30778
|
return app ? `${app}+${hash}` : hash;
|
|
30395
30779
|
}
|
|
@@ -30405,28 +30789,28 @@ async function hashFile(filePath, onProgress) {
|
|
|
30405
30789
|
}
|
|
30406
30790
|
async function packTarget(dir) {
|
|
30407
30791
|
if (dir.toLowerCase().endsWith(".app")) return { target: dir, keepParent: true };
|
|
30408
|
-
const children = await
|
|
30792
|
+
const children = await fs48.readdir(dir, { withFileTypes: true });
|
|
30409
30793
|
const apps = children.filter((d) => d.isDirectory() && d.name.toLowerCase().endsWith(".app"));
|
|
30410
|
-
if (apps.length === 1) return { target:
|
|
30794
|
+
if (apps.length === 1) return { target: path50.join(dir, apps[0].name), keepParent: true };
|
|
30411
30795
|
return { target: dir, keepParent: false };
|
|
30412
30796
|
}
|
|
30413
|
-
var windowsTar = () =>
|
|
30797
|
+
var windowsTar = () => path50.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
|
|
30414
30798
|
async function zipFolder(target, keepParent, zipPath, opts) {
|
|
30415
30799
|
const platform = opts.platform ?? process.platform;
|
|
30416
30800
|
if (platform === "darwin") {
|
|
30417
30801
|
await run4("ditto", ["-c", "-k", "--sequesterRsrc", ...keepParent ? ["--keepParent"] : [], target, zipPath]);
|
|
30418
30802
|
return;
|
|
30419
30803
|
}
|
|
30420
|
-
const cwd = keepParent ?
|
|
30804
|
+
const cwd = keepParent ? path50.dirname(target) : target;
|
|
30421
30805
|
if (platform === "win32") {
|
|
30422
|
-
const names2 = keepParent ? [
|
|
30806
|
+
const names2 = keepParent ? [path50.basename(target)] : await fs48.readdir(target);
|
|
30423
30807
|
await run4(opts.tar ?? windowsTar(), ["-a", "-c", "-f", zipPath, ...names2], { cwd });
|
|
30424
30808
|
return;
|
|
30425
30809
|
}
|
|
30426
|
-
await run4("zip", ["-q", "-r", "-y", zipPath, keepParent ?
|
|
30810
|
+
await run4("zip", ["-q", "-r", "-y", zipPath, keepParent ? path50.basename(target) : "."], { cwd });
|
|
30427
30811
|
}
|
|
30428
30812
|
async function packNativeBuild(input, opts = {}) {
|
|
30429
|
-
const stat = await
|
|
30813
|
+
const stat = await fs48.stat(input).catch(() => null);
|
|
30430
30814
|
if (!stat) throw new NativeBuildError(`${input} does not exist.`);
|
|
30431
30815
|
if (stat.isFile()) {
|
|
30432
30816
|
if (!input.toLowerCase().endsWith(".zip")) throw new NativeBuildError("Pass a .zip, a .app, or the build's folder.");
|
|
@@ -30434,9 +30818,9 @@ async function packNativeBuild(input, opts = {}) {
|
|
|
30434
30818
|
} };
|
|
30435
30819
|
}
|
|
30436
30820
|
const { target, keepParent } = await packTarget(input);
|
|
30437
|
-
const dir = await
|
|
30438
|
-
const zipPath =
|
|
30439
|
-
const cleanup = () =>
|
|
30821
|
+
const dir = await fs48.mkdtemp(path50.join(os14.tmpdir(), "genex-native-"));
|
|
30822
|
+
const zipPath = path50.join(dir, `${path50.basename(target).replace(/[^A-Za-z0-9._-]+/g, "-") || "build"}.zip`);
|
|
30823
|
+
const cleanup = () => fs48.rm(dir, { recursive: true, force: true });
|
|
30440
30824
|
try {
|
|
30441
30825
|
await zipFolder(target, keepParent, zipPath, opts);
|
|
30442
30826
|
await markPrograms(zipPath);
|
|
@@ -30452,7 +30836,7 @@ async function packNativeBuild(input, opts = {}) {
|
|
|
30452
30836
|
}
|
|
30453
30837
|
|
|
30454
30838
|
// src/lib/native-upload.ts
|
|
30455
|
-
import
|
|
30839
|
+
import fs49 from "fs/promises";
|
|
30456
30840
|
var PART_CONCURRENCY = 2;
|
|
30457
30841
|
async function call(ctx, route, init, what, log) {
|
|
30458
30842
|
let res;
|
|
@@ -30498,7 +30882,7 @@ async function uploadNativeArchive(grant, zipPath, sizeBytes, log, onProgress) {
|
|
|
30498
30882
|
const base = grant.uploadUrl.replace(/\/+$/, "");
|
|
30499
30883
|
const encoded = grant.uploadPath.split("/").map(encodeURIComponent).join("/");
|
|
30500
30884
|
const auth = { Authorization: `Bearer ${grant.token}` };
|
|
30501
|
-
const file = await
|
|
30885
|
+
const file = await fs49.open(zipPath, "r");
|
|
30502
30886
|
try {
|
|
30503
30887
|
if (sizeBytes <= grant.limits.singlePutMaxBytes) {
|
|
30504
30888
|
const body = await readPart(file, 0, sizeBytes);
|
|
@@ -30601,7 +30985,7 @@ async function listInGallery(ctx, body, log) {
|
|
|
30601
30985
|
}
|
|
30602
30986
|
|
|
30603
30987
|
// src/lib/cloud-build.ts
|
|
30604
|
-
import
|
|
30988
|
+
import fs50 from "fs/promises";
|
|
30605
30989
|
var CLOUD_BUILD_PLATFORMS = ["windows", "linux", "mac"];
|
|
30606
30990
|
var isCloudBuildPlatform = (value) => CLOUD_BUILD_PLATFORMS.includes(value);
|
|
30607
30991
|
var CLOUD_BUILD_JOB_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
@@ -30718,7 +31102,7 @@ async function uploadCloudBuildSource(upload, zipPath, sizeBytes, log, onProgres
|
|
|
30718
31102
|
log.error(`The API sent ${parts.length} upload URLs for a ${expected}-part project. Run this again.`);
|
|
30719
31103
|
return null;
|
|
30720
31104
|
}
|
|
30721
|
-
const file = await
|
|
31105
|
+
const file = await fs50.open(zipPath, "r");
|
|
30722
31106
|
const done = [];
|
|
30723
31107
|
let sent = 0;
|
|
30724
31108
|
let next = 0;
|
|
@@ -30752,8 +31136,8 @@ async function uploadCloudBuildSource(upload, zipPath, sizeBytes, log, onProgres
|
|
|
30752
31136
|
}
|
|
30753
31137
|
|
|
30754
31138
|
// src/lib/source-zip.ts
|
|
30755
|
-
import
|
|
30756
|
-
import
|
|
31139
|
+
import fs51 from "fs/promises";
|
|
31140
|
+
import path51 from "path";
|
|
30757
31141
|
import zlib4 from "zlib";
|
|
30758
31142
|
var SOURCE_ZIP_ROOT_EXCLUDES = [".genex", "Intermediate", "Saved", "DerivedDataCache", "Binaries"];
|
|
30759
31143
|
var SOURCE_ZIP_EXCLUDES_ANYWHERE = [".git", ".DS_Store", ".env"];
|
|
@@ -30771,23 +31155,23 @@ var U32_MAX2 = 4294967295;
|
|
|
30771
31155
|
var CHUNK = 1024 * 1024;
|
|
30772
31156
|
var isExcludedAnywhere = (name) => SOURCE_ZIP_EXCLUDES_ANYWHERE.includes(name) || ENV_VARIANT.test(name) && name !== ENV_TEMPLATE;
|
|
30773
31157
|
function inside(root, target) {
|
|
30774
|
-
const rel =
|
|
30775
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
31158
|
+
const rel = path51.relative(root, target);
|
|
31159
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${path51.sep}`) && !path51.isAbsolute(rel);
|
|
30776
31160
|
}
|
|
30777
31161
|
async function plan(root) {
|
|
30778
31162
|
const entries = [];
|
|
30779
31163
|
const skippedLinks = [];
|
|
30780
31164
|
const walk2 = async (dir, rel) => {
|
|
30781
|
-
const children = await
|
|
31165
|
+
const children = await fs51.readdir(dir, { withFileTypes: true });
|
|
30782
31166
|
let kept = 0;
|
|
30783
31167
|
for (const child of children) {
|
|
30784
31168
|
const name = child.name;
|
|
30785
31169
|
const childRel = rel ? `${rel}/${name}` : name;
|
|
30786
|
-
const abs =
|
|
31170
|
+
const abs = path51.join(dir, name);
|
|
30787
31171
|
if (isExcludedAnywhere(name)) continue;
|
|
30788
31172
|
if (!rel && SOURCE_ZIP_ROOT_EXCLUDES.includes(name)) continue;
|
|
30789
31173
|
if (child.isSymbolicLink()) {
|
|
30790
|
-
const target = await
|
|
31174
|
+
const target = await fs51.realpath(abs).catch(() => null);
|
|
30791
31175
|
if (target !== null && !inside(root, target)) {
|
|
30792
31176
|
throw new SourceZipError(
|
|
30793
31177
|
`${childRel} is a link to ${target}, outside the folder being built. Copy what it points to into the project, or remove the link.`
|
|
@@ -30801,7 +31185,7 @@ async function plan(root) {
|
|
|
30801
31185
|
await walk2(abs, childRel);
|
|
30802
31186
|
} else if (child.isFile()) {
|
|
30803
31187
|
kept++;
|
|
30804
|
-
const { size } = await
|
|
31188
|
+
const { size } = await fs51.stat(abs);
|
|
30805
31189
|
entries.push({ name: childRel, abs, size });
|
|
30806
31190
|
}
|
|
30807
31191
|
}
|
|
@@ -30847,8 +31231,8 @@ var Out = class {
|
|
|
30847
31231
|
}
|
|
30848
31232
|
};
|
|
30849
31233
|
async function writeSourceZip(root, zipPath, opts = {}) {
|
|
30850
|
-
const { entries, skippedLinks } = await plan(await
|
|
30851
|
-
const file = await
|
|
31234
|
+
const { entries, skippedLinks } = await plan(await fs51.realpath(root));
|
|
31235
|
+
const file = await fs51.open(zipPath, "w");
|
|
30852
31236
|
const out = new Out(file);
|
|
30853
31237
|
const central = [];
|
|
30854
31238
|
let contentBytes = 0;
|
|
@@ -30875,7 +31259,7 @@ async function writeSourceZip(root, zipPath, opts = {}) {
|
|
|
30875
31259
|
await out.write(Buffer.concat([local, name, localExtra]));
|
|
30876
31260
|
let crc = 0;
|
|
30877
31261
|
if (entry.abs) {
|
|
30878
|
-
const source = await
|
|
31262
|
+
const source = await fs51.open(entry.abs, "r");
|
|
30879
31263
|
try {
|
|
30880
31264
|
let read = 0;
|
|
30881
31265
|
for (; ; ) {
|
|
@@ -30966,12 +31350,12 @@ function zip64Extra(values) {
|
|
|
30966
31350
|
return extra;
|
|
30967
31351
|
}
|
|
30968
31352
|
async function resolveZipSource(dir, cwd = process.cwd()) {
|
|
30969
|
-
const abs =
|
|
30970
|
-
const stat = await
|
|
31353
|
+
const abs = path51.resolve(cwd, dir);
|
|
31354
|
+
const stat = await fs51.lstat(abs).catch(() => null);
|
|
30971
31355
|
if (!stat) return { refused: `--source ${abs} does not exist.` };
|
|
30972
31356
|
if (stat.isSymbolicLink()) return { refused: `--source ${abs} is a symbolic link. Pass the folder itself.` };
|
|
30973
31357
|
if (!stat.isDirectory()) return { refused: `--source ${abs} is not a folder.` };
|
|
30974
|
-
return { dir: await
|
|
31358
|
+
return { dir: await fs51.realpath(abs) };
|
|
30975
31359
|
}
|
|
30976
31360
|
|
|
30977
31361
|
// src/commands/native.ts
|
|
@@ -31063,10 +31447,10 @@ async function runPublish2(opts, log) {
|
|
|
31063
31447
|
const description = await describeNativeArchive(packed.zipPath, opts);
|
|
31064
31448
|
log.step(describeLine(description));
|
|
31065
31449
|
log.step("Hashing the archive\u2026");
|
|
31066
|
-
const { sha256:
|
|
31067
|
-
const version = buildVersion(opts.buildVersion, description.appVersion,
|
|
31450
|
+
const { sha256: sha2565, sizeBytes } = await hashFile(packed.zipPath);
|
|
31451
|
+
const version = buildVersion(opts.buildVersion, description.appVersion, sha2565);
|
|
31068
31452
|
const { platform, arch, entry, installedBytes, minOs, engine } = description;
|
|
31069
|
-
const grant = await requestNativeUpload(ctx, { platform, arch, sha256:
|
|
31453
|
+
const grant = await requestNativeUpload(ctx, { platform, arch, sha256: sha2565, sizeBytes }, log);
|
|
31070
31454
|
if (!grant) return false;
|
|
31071
31455
|
if (grant.uploaded) {
|
|
31072
31456
|
log.step(`Already uploaded (${sizeLine(sizeBytes)}).`);
|
|
@@ -31077,7 +31461,7 @@ async function runPublish2(opts, log) {
|
|
|
31077
31461
|
}
|
|
31078
31462
|
const build = await publishNativeBuild(
|
|
31079
31463
|
ctx,
|
|
31080
|
-
{ platform, arch, sha256:
|
|
31464
|
+
{ platform, arch, sha256: sha2565, sizeBytes, installedBytes, version, entry, ...minOs ? { minOs } : {}, ...engine ? { engine } : {} },
|
|
31081
31465
|
log
|
|
31082
31466
|
);
|
|
31083
31467
|
if (!build) return false;
|
|
@@ -31224,8 +31608,8 @@ async function runBuild(opts, log) {
|
|
|
31224
31608
|
}
|
|
31225
31609
|
const ctx = await context3(opts, log);
|
|
31226
31610
|
if (!ctx) return false;
|
|
31227
|
-
const tmp = await
|
|
31228
|
-
const zipPath =
|
|
31611
|
+
const tmp = await fs52.mkdtemp(path52.join(os15.tmpdir(), "genex-cloud-build-"));
|
|
31612
|
+
const zipPath = path52.join(tmp, "source.zip");
|
|
31229
31613
|
try {
|
|
31230
31614
|
log.step(`Zipping ${source.dir}\u2026`);
|
|
31231
31615
|
let zipped;
|
|
@@ -31245,11 +31629,11 @@ async function runBuild(opts, log) {
|
|
|
31245
31629
|
const more = zipped.skippedLinks.length > 5 ? ` and ${zipped.skippedLinks.length - 5} more` : "";
|
|
31246
31630
|
log.warn(`Left out ${zipped.skippedLinks.length} link(s) inside the folder: ${shown}${more}.`);
|
|
31247
31631
|
}
|
|
31248
|
-
const { sha256:
|
|
31632
|
+
const { sha256: sha2565, sizeBytes } = await hashFile(zipPath);
|
|
31249
31633
|
log.step(`${zipped.entries} files, ${sizeLine(sizeBytes)} zipped.`);
|
|
31250
31634
|
const body = {
|
|
31251
31635
|
platform: opts.platform,
|
|
31252
|
-
sourceSha256:
|
|
31636
|
+
sourceSha256: sha2565,
|
|
31253
31637
|
sourceBytes: sizeBytes,
|
|
31254
31638
|
...opts.buildVersion !== void 0 ? { version: opts.buildVersion } : {}
|
|
31255
31639
|
};
|
|
@@ -31294,7 +31678,7 @@ async function runBuild(opts, log) {
|
|
|
31294
31678
|
if (opts.json) console.log(JSON.stringify({ build }));
|
|
31295
31679
|
return true;
|
|
31296
31680
|
} finally {
|
|
31297
|
-
await
|
|
31681
|
+
await fs52.rm(tmp, { recursive: true, force: true });
|
|
31298
31682
|
}
|
|
31299
31683
|
}
|
|
31300
31684
|
async function runBuildStatus(opts, log) {
|