@downdraft/cli 0.0.0 → 0.1.1
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/README.md +11 -0
- package/package.json +36 -3
- package/scripts/default-icon.png +0 -0
- package/scripts/package-desktop.mjs +931 -0
- package/scripts/package-mobile.mjs +822 -0
- package/scripts/package-native.mjs +638 -0
- package/src/args.ts +333 -0
- package/src/assets.ts +523 -0
- package/src/blob-store-s3.ts +145 -0
- package/src/build-games.ts +45 -0
- package/src/build.ts +40 -0
- package/src/bun-env.d.ts +6 -0
- package/src/debug.ts +56 -0
- package/src/dev.ts +231 -0
- package/src/dist.ts +36 -0
- package/src/export.ts +38 -0
- package/src/index.ts +131 -0
- package/src/list-games.ts +37 -0
- package/src/mcp.ts +320 -0
- package/src/new.ts +63 -0
- package/src/paths.ts +101 -0
- package/src/plugin-command.ts +214 -0
- package/src/process-utils.ts +179 -0
- package/src/release.ts +432 -0
- package/src/scaffold-plugin.ts +280 -0
- package/src/scaffold.ts +207 -0
- package/src/test.ts +187 -0
- package/src/usage.ts +256 -0
- package/templates/_shared/ai-companion/devin-config.eta +11 -0
- package/templates/_shared/ai-companion/engine-prompt.eta +155 -0
- package/templates/_shared/ai-companion/logging-rule.eta +3 -0
- package/templates/_shared/ai-companion/loops-rule.eta +3 -0
- package/templates/_shared/partials/config-json-fields.eta +3 -0
- package/templates/_shared/partials/dev-deps.eta +8 -0
- package/templates/_shared/partials/package-json-fields.eta +12 -0
- package/templates/_shared/partials/tsconfig.eta +16 -0
- package/templates/_shared/partials/vscode-extensions.eta +11 -0
- package/templates/_shared/partials/vscode-settings.eta +29 -0
- package/templates/_shared/partials/vscode-tasks.eta +93 -0
- package/templates/full/.gitignore.eta +5 -0
- package/templates/full/.vscode/extensions.json.eta +1 -0
- package/templates/full/.vscode/settings.json.eta +1 -0
- package/templates/full/.vscode/tasks.json.eta +1 -0
- package/templates/full/README.md.eta +45 -0
- package/templates/full/assets/shaders/.eta +0 -0
- package/templates/full/downdraft.config.json.eta +10 -0
- package/templates/full/package.json.eta +9 -0
- package/templates/full/src/entities/.eta +0 -0
- package/templates/full/src/game-module.ts.eta +30 -0
- package/templates/full/src/native-entry.ts.eta +14 -0
- package/templates/full/src/sim-host.ts.eta +19 -0
- package/templates/full/src/sim-worker.ts.eta +99 -0
- package/templates/full/src/systems/.eta +0 -0
- package/templates/full/tsconfig.json.eta +1 -0
- package/templates/gamemodule/README.md.eta +32 -0
- package/templates/gamemodule/downdraft.config.json.eta +3 -0
- package/templates/gamemodule/package.json.eta +9 -0
- package/templates/gamemodule/src/game-module.ts.eta +63 -0
- package/templates/gamemodule/src/native-entry.ts.eta +14 -0
- package/templates/gamemodule/src/sim-host.ts.eta +25 -0
- package/templates/gamemodule/src/sim-worker.ts.eta +24 -0
- package/templates/gamemodule/tsconfig.json.eta +1 -0
- package/templates/minimal/.gitignore.eta +5 -0
- package/templates/minimal/.vscode/extensions.json.eta +1 -0
- package/templates/minimal/.vscode/settings.json.eta +1 -0
- package/templates/minimal/.vscode/tasks.json.eta +1 -0
- package/templates/minimal/README.md.eta +32 -0
- package/templates/minimal/downdraft.config.json.eta +3 -0
- package/templates/minimal/package.json.eta +9 -0
- package/templates/minimal/src/game-module.ts.eta +44 -0
- package/templates/minimal/src/native-entry.ts.eta +14 -0
- package/templates/minimal/tsconfig.json.eta +1 -0
- package/templates/physics/.gitignore.eta +5 -0
- package/templates/physics/.vscode/extensions.json.eta +1 -0
- package/templates/physics/.vscode/settings.json.eta +1 -0
- package/templates/physics/.vscode/tasks.json.eta +1 -0
- package/templates/physics/README.md.eta +36 -0
- package/templates/physics/downdraft.config.json.eta +6 -0
- package/templates/physics/package.json.eta +9 -0
- package/templates/physics/src/game-module.ts.eta +29 -0
- package/templates/physics/src/native-entry.ts.eta +14 -0
- package/templates/physics/src/sim-host.ts.eta +19 -0
- package/templates/physics/src/sim-worker.ts.eta +155 -0
- package/templates/physics/tsconfig.json.eta +1 -0
package/src/release.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// draft release — native build + package pipeline
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Replaces the separate `build`, `dist`, `export`, `mobile`, and
|
|
6
|
+
// `build-games` commands with a single pipeline:
|
|
7
|
+
//
|
|
8
|
+
// draft release [--game=<name>] [--target=<csv>]
|
|
9
|
+
// (auto-detects the game from cwd or games/ when --game is omitted)
|
|
10
|
+
// [--stage=<build|package|release>] [--games=<csv>]
|
|
11
|
+
// [--mode=<dev|debug|prod>] [--out=<dir>]
|
|
12
|
+
// [--mcp] [--verbose]
|
|
13
|
+
//
|
|
14
|
+
// Stages (all stages produce the same artifact — a compiled Bun binary plus
|
|
15
|
+
// the staged native/ + dd-assets/ tree — via scripts/package-native.mjs):
|
|
16
|
+
// build = compile only
|
|
17
|
+
// package = package an existing build (for native: same compile step)
|
|
18
|
+
// release = build + package + collect to release/ (default)
|
|
19
|
+
//
|
|
20
|
+
// Targets: win | linux | mac | all
|
|
21
|
+
//
|
|
22
|
+
// The old commands remain as backward-compat aliases that delegate here with
|
|
23
|
+
// a deprecation warning.
|
|
24
|
+
//
|
|
25
|
+
|
|
26
|
+
import { createLogger } from "@downdraft/engine";
|
|
27
|
+
import { spawnSync } from "node:child_process";
|
|
28
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
+
import { basename, resolve } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { parseArgs, print, renderHelp } from "./args";
|
|
32
|
+
import { formatGamesList, listGames } from "./list-games";
|
|
33
|
+
import { findGameDirUpward, findMonorepoRoot, resolveGameDir } from "./paths";
|
|
34
|
+
import { getCommand } from "./usage";
|
|
35
|
+
|
|
36
|
+
const log = createLogger();
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Types
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
type Stage = "build" | "package" | "release";
|
|
43
|
+
|
|
44
|
+
interface ReleaseArgs {
|
|
45
|
+
game: string | null;
|
|
46
|
+
games: string[];
|
|
47
|
+
target: string;
|
|
48
|
+
stage: Stage;
|
|
49
|
+
mode: string;
|
|
50
|
+
abi: string;
|
|
51
|
+
minSdk: string | null;
|
|
52
|
+
nodeFlavor: string | null;
|
|
53
|
+
libs: string | null;
|
|
54
|
+
excludeLibs: string | null;
|
|
55
|
+
noStrip: boolean;
|
|
56
|
+
out: string;
|
|
57
|
+
retainMcp: boolean;
|
|
58
|
+
verbose: boolean;
|
|
59
|
+
runtime: string | null;
|
|
60
|
+
format: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Arg parsing
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function parseReleaseArgs(args: string[]): ReleaseArgs {
|
|
68
|
+
const entry = getCommand("release")!;
|
|
69
|
+
const parsed = parseArgs(args, entry.schema);
|
|
70
|
+
if (parsed.help) {
|
|
71
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const game = (parsed.flags.game as string) || null;
|
|
76
|
+
const gamesFlag = (parsed.flags.games as string) || "";
|
|
77
|
+
const games = gamesFlag
|
|
78
|
+
? gamesFlag.split(",").map((s) => s.trim()).filter(Boolean)
|
|
79
|
+
: game ? [game] : [];
|
|
80
|
+
|
|
81
|
+
const stageFlag = (parsed.flags.stage as string) || "release";
|
|
82
|
+
// --skip-build and --build-only are convenience aliases for --stage.
|
|
83
|
+
const skipBuild = parsed.flags["skip-build"] as boolean;
|
|
84
|
+
const buildOnly = parsed.flags["build-only"] as boolean;
|
|
85
|
+
let stage: Stage = stageFlag as Stage;
|
|
86
|
+
if (buildOnly) stage = "build";
|
|
87
|
+
else if (skipBuild) stage = "package";
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
game,
|
|
91
|
+
games,
|
|
92
|
+
target: parsed.flags.target as string,
|
|
93
|
+
stage,
|
|
94
|
+
mode: (parsed.flags.mode as string) || "prod",
|
|
95
|
+
abi: (parsed.flags.abi as string) || "arm64-v8a",
|
|
96
|
+
minSdk: (parsed.flags["min-sdk"] as string) || null,
|
|
97
|
+
nodeFlavor: (parsed.flags["node-flavor"] as string) || null,
|
|
98
|
+
libs: (parsed.flags.libs as string) || null,
|
|
99
|
+
excludeLibs: (parsed.flags["exclude-libs"] as string) || null,
|
|
100
|
+
noStrip: parsed.flags["no-strip"] as boolean,
|
|
101
|
+
out: (parsed.flags.out as string) || "release",
|
|
102
|
+
retainMcp: parsed.flags.mcp as boolean,
|
|
103
|
+
verbose: parsed.flags.verbose as boolean,
|
|
104
|
+
runtime: (parsed.flags.runtime as string) || null,
|
|
105
|
+
format: (parsed.flags.format as string) || null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Game resolution
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the game(s) to release when neither --game nor --games is given.
|
|
115
|
+
*
|
|
116
|
+
* Resolution order:
|
|
117
|
+
* 1. Explicit flags (--game / --games) — handled by the caller before this.
|
|
118
|
+
* 2. cwd inference — walk up from `process.cwd()` looking for
|
|
119
|
+
* `downdraft.config.json` or `src/native-entry.ts`. Lets games run
|
|
120
|
+
* `draft release` from their own directory (the scaffolded
|
|
121
|
+
* `package.json` sets `"release": "draft release"`).
|
|
122
|
+
* 3. Single-game fallback — if `games/` contains exactly one game, use it.
|
|
123
|
+
*
|
|
124
|
+
* Returns the detected game name, or `null` if none could be determined.
|
|
125
|
+
*/
|
|
126
|
+
function detectGame(monorepoRoot: string | null): string | null {
|
|
127
|
+
// 2. Cwd inference: walk up looking for the game-dir markers.
|
|
128
|
+
const upward = findGameDirUpward();
|
|
129
|
+
if (upward) return basename(upward);
|
|
130
|
+
|
|
131
|
+
// 3. Single-game fallback (monorepo only).
|
|
132
|
+
if (monorepoRoot) {
|
|
133
|
+
const games = listGames(monorepoRoot);
|
|
134
|
+
if (games.length === 1) {
|
|
135
|
+
log.info("release", `No game specified — defaulting to the only game found: ${games[0]}`);
|
|
136
|
+
return games[0];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Target classification
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
const DESKTOP_TARGETS = new Set(["win", "linux", "mac"]);
|
|
148
|
+
const MOBILE_TARGETS = new Set(["android"]);
|
|
149
|
+
|
|
150
|
+
function classifyTargets(target: string): string[] {
|
|
151
|
+
if (target === "all") {
|
|
152
|
+
return ["win", "linux", "mac"];
|
|
153
|
+
}
|
|
154
|
+
const parts = target.split(",").map((s) => s.trim()).filter(Boolean);
|
|
155
|
+
parts.forEach((t) => {
|
|
156
|
+
if (t === "ios") {
|
|
157
|
+
log.error("release", `iOS is not implemented yet — the native mobile port currently supports android only.`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
if (!DESKTOP_TARGETS.has(t) && !MOBILE_TARGETS.has(t)) {
|
|
161
|
+
log.error("release", `Unknown target: ${t}. Use win, linux, mac, android, or all.`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
if (parts.some((t) => MOBILE_TARGETS.has(t)) && parts.some((t) => DESKTOP_TARGETS.has(t))) {
|
|
166
|
+
log.error("release", `Can't mix mobile and desktop targets in one release — run android separately.`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
return parts;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Game info resolution
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
interface GameInfo {
|
|
177
|
+
productName: string;
|
|
178
|
+
appId: string;
|
|
179
|
+
version: string;
|
|
180
|
+
/** The electron-builder-style `build` block from the game's package.json. */
|
|
181
|
+
build: Record<string, any>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getGameInfo(gameDir: string, game: string): GameInfo {
|
|
185
|
+
const pkgPath = resolve(gameDir, "package.json");
|
|
186
|
+
const fallback: GameInfo = { productName: game, appId: `com.downdraft.${game}`, version: "0.0.0", build: {} };
|
|
187
|
+
if (!existsSync(pkgPath)) return fallback;
|
|
188
|
+
try {
|
|
189
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
190
|
+
const productName = pkg.productName ?? pkg.build?.productName ??
|
|
191
|
+
String(pkg.name ?? game).replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
192
|
+
const appId = pkg.build?.appId ?? `com.downdraft.${game}`;
|
|
193
|
+
const version = pkg.version ?? "0.0.0";
|
|
194
|
+
return { productName, appId, version, build: pkg.build ?? {} };
|
|
195
|
+
} catch {
|
|
196
|
+
return fallback;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Native packaging — scripts/package-native.mjs
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Compile the game's native entry into a standalone Bun binary plus the
|
|
206
|
+
* staged runtime tree (<out>/<game>-<target> + sibling native/ + dd-assets/).
|
|
207
|
+
* One compile covers both the build and package stages — the binary IS the
|
|
208
|
+
* artifact.
|
|
209
|
+
*/
|
|
210
|
+
function packageNative(
|
|
211
|
+
game: string,
|
|
212
|
+
gameDir: string,
|
|
213
|
+
targets: string[],
|
|
214
|
+
opts: ReleaseArgs,
|
|
215
|
+
info: GameInfo,
|
|
216
|
+
): boolean {
|
|
217
|
+
const entry = resolve(gameDir, "src/native-entry.ts");
|
|
218
|
+
if (!existsSync(entry)) {
|
|
219
|
+
log.error("release:package", `No src/native-entry.ts in ${gameDir} — the game has no native entry.`);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
// The packaging scripts ship inside @downdraft/cli (files: ["scripts"]) so
|
|
223
|
+
// standalone game repos resolve them through the installed package, not
|
|
224
|
+
// the engine monorepo.
|
|
225
|
+
const scriptNative = fileURLToPath(new URL("../scripts/package-native.mjs", import.meta.url));
|
|
226
|
+
const scriptDesktop = fileURLToPath(new URL("../scripts/package-desktop.mjs", import.meta.url));
|
|
227
|
+
// cwd is the relativization root for staged assets — the monorepo root
|
|
228
|
+
// when present (engine sources relativize cleanly), else the game dir.
|
|
229
|
+
const cwd = findMonorepoRoot(gameDir) ?? gameDir;
|
|
230
|
+
|
|
231
|
+
// Runtime matrix: --runtime flag → build.runtime in package.json → bun.
|
|
232
|
+
// "all" expands to the full matrix (one appdir per runtime).
|
|
233
|
+
const RUNTIMES = ["bun", "node", "deno"];
|
|
234
|
+
const requested = opts.runtime ?? info.build.runtime ?? "bun";
|
|
235
|
+
const runtimes = requested === "all" ? RUNTIMES : [requested];
|
|
236
|
+
for (const r of runtimes) {
|
|
237
|
+
if (!RUNTIMES.includes(r)) {
|
|
238
|
+
log.error("release:package", `Unknown runtime "${r}" — expected bun, node, deno, or all.`);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Non-bun runtimes are linux-only today — the emit-tree packaging hasn't
|
|
243
|
+
// been wired for win/mac.
|
|
244
|
+
const nonBun = runtimes.filter((r) => r !== "bun");
|
|
245
|
+
if (nonBun.length && targets.some((t) => t !== "linux")) {
|
|
246
|
+
log.warn("release:package", `--runtime=${nonBun.join(",")} is linux-only today — win/mac targets still embed bun.`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let ok = true;
|
|
250
|
+
targets.forEach((target) => {
|
|
251
|
+
for (const runtime of runtimes) {
|
|
252
|
+
if (target === "linux") {
|
|
253
|
+
// Linux: package-desktop.mjs owns the format matrix (dir, deb,
|
|
254
|
+
// appimage, flatpak). Formats come from --format → build.linux.target
|
|
255
|
+
// → "dir".
|
|
256
|
+
const format = opts.format
|
|
257
|
+
?? (Array.isArray(info.build.linux?.target) ? info.build.linux.target.join(",") : info.build.linux?.target)
|
|
258
|
+
?? "dir";
|
|
259
|
+
const argv = [
|
|
260
|
+
scriptDesktop,
|
|
261
|
+
`--runtime=${runtime}`,
|
|
262
|
+
`--format=${format}`,
|
|
263
|
+
`--mode=${opts.mode}`,
|
|
264
|
+
`--product-name=${info.productName}`,
|
|
265
|
+
`--product-version=${info.version}`,
|
|
266
|
+
`--app-id=${info.appId}`,
|
|
267
|
+
...(opts.retainMcp ? ["--mcp"] : []),
|
|
268
|
+
entry,
|
|
269
|
+
resolve(opts.out),
|
|
270
|
+
];
|
|
271
|
+
log.info("release:package", `bun package-desktop.mjs --runtime=${runtime} --format=${format} ${basename(entry)}`);
|
|
272
|
+
const result = spawnSync("bun", argv, { cwd, stdio: "inherit", env: process.env });
|
|
273
|
+
if (result.status !== 0) {
|
|
274
|
+
log.error("release:package", `Packaging failed for ${game} (${target}/${runtime})`);
|
|
275
|
+
ok = false;
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// win/mac: single-binary bun compile via package-native.mjs.
|
|
280
|
+
if (runtime !== "bun") continue;
|
|
281
|
+
const outfile = resolve(opts.out, `${game}-${target}`);
|
|
282
|
+
const argv = [
|
|
283
|
+
scriptNative,
|
|
284
|
+
`--target=${target}`,
|
|
285
|
+
`--mode=${opts.mode}`,
|
|
286
|
+
`--product-name=${info.productName}`,
|
|
287
|
+
`--product-version=${info.version}`,
|
|
288
|
+
...(opts.retainMcp ? ["--mcp"] : []),
|
|
289
|
+
entry,
|
|
290
|
+
outfile,
|
|
291
|
+
];
|
|
292
|
+
log.info("release:package", `bun ${argv.map((a) => basename(a)).join(" ")}`);
|
|
293
|
+
const result = spawnSync("bun", argv, {
|
|
294
|
+
cwd,
|
|
295
|
+
stdio: "inherit",
|
|
296
|
+
env: process.env,
|
|
297
|
+
});
|
|
298
|
+
if (result.status !== 0) {
|
|
299
|
+
log.error("release:package", `Native packaging failed for ${game} (${target})`);
|
|
300
|
+
ok = false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return ok;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Android packaging — scripts/package-mobile.mjs
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Bundle the game + native libs + embedded libnode into an APK. Single
|
|
313
|
+
* target ("android") produces lib/<abi>/ entries for --abi (default
|
|
314
|
+
* arm64-v8a). Unlike the desktop path the artifact is the APK itself.
|
|
315
|
+
*/
|
|
316
|
+
function packageAndroid(
|
|
317
|
+
game: string,
|
|
318
|
+
gameDir: string,
|
|
319
|
+
opts: ReleaseArgs,
|
|
320
|
+
info: GameInfo,
|
|
321
|
+
): boolean {
|
|
322
|
+
const entry = resolve(gameDir, "src/native-entry.ts");
|
|
323
|
+
if (!existsSync(entry)) {
|
|
324
|
+
log.error("release:package", `No src/native-entry.ts in ${gameDir} — the game has no native entry.`);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
const script = fileURLToPath(new URL("../scripts/package-mobile.mjs", import.meta.url));
|
|
328
|
+
const cwd = findMonorepoRoot(gameDir) ?? gameDir;
|
|
329
|
+
const outfile = resolve(opts.out, `${game}-android.apk`);
|
|
330
|
+
const argv = [
|
|
331
|
+
script,
|
|
332
|
+
`--mode=${opts.mode}`,
|
|
333
|
+
`--abi=${opts.abi}`,
|
|
334
|
+
`--app-id=${info.appId}`,
|
|
335
|
+
`--app-name=${info.productName}`,
|
|
336
|
+
`--version=${info.version}`,
|
|
337
|
+
...(opts.minSdk ? [`--min-sdk=${opts.minSdk}`] : []),
|
|
338
|
+
...(opts.nodeFlavor ? [`--node-flavor=${opts.nodeFlavor}`] : []),
|
|
339
|
+
...(opts.libs ? [`--libs=${opts.libs}`] : []),
|
|
340
|
+
...(opts.excludeLibs ? [`--exclude-libs=${opts.excludeLibs}`] : []),
|
|
341
|
+
...(opts.noStrip ? ["--no-strip"] : []),
|
|
342
|
+
...(opts.retainMcp ? ["--mcp"] : []),
|
|
343
|
+
entry,
|
|
344
|
+
outfile,
|
|
345
|
+
];
|
|
346
|
+
log.info("release:package", `bun ${argv.map((a) => basename(a)).join(" ")}`);
|
|
347
|
+
const result = spawnSync("bun", argv, { cwd, stdio: "inherit", env: process.env });
|
|
348
|
+
if (result.status !== 0) {
|
|
349
|
+
log.error("release:package", `Android packaging failed for ${game}`);
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Main entry point
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
export async function release(args: string[]): Promise<void> {
|
|
360
|
+
const opts = parseReleaseArgs(args);
|
|
361
|
+
// Monorepo root when running inside the engine repo; null for standalone
|
|
362
|
+
// game repos (the game directory itself is the project root).
|
|
363
|
+
const monorepoRoot = findMonorepoRoot();
|
|
364
|
+
|
|
365
|
+
// Auto-detect the game when neither --game nor --games is given.
|
|
366
|
+
let games = opts.games;
|
|
367
|
+
if (games.length === 0) {
|
|
368
|
+
const detected = detectGame(monorepoRoot);
|
|
369
|
+
if (detected) {
|
|
370
|
+
games = [detected];
|
|
371
|
+
opts.game = detected;
|
|
372
|
+
} else {
|
|
373
|
+
log.error("release", "No game specified and none could be auto-detected.");
|
|
374
|
+
log.error("release", "Run \"draft release\" from a game directory, or use \"--game <name>\" / \"--games <csv>\" from the engine root.");
|
|
375
|
+
log.error("release", "Available games:");
|
|
376
|
+
print(formatGamesList(monorepoRoot ?? process.cwd()));
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const targets = classifyTargets(opts.target);
|
|
382
|
+
|
|
383
|
+
log.info("release", `
|
|
384
|
+
╔══════════════════════════════════════════╗
|
|
385
|
+
║ DownDraft Engine — Release ║
|
|
386
|
+
╚══════════════════════════════════════════╝
|
|
387
|
+
`);
|
|
388
|
+
|
|
389
|
+
log.info("release", ` Game(s): ${games.join(", ")}`);
|
|
390
|
+
log.info("release", ` Target: ${opts.target}`);
|
|
391
|
+
log.info("release", ` Stage: ${opts.stage}`);
|
|
392
|
+
log.info("release", ` Mode: ${opts.mode}`);
|
|
393
|
+
log.info("release", ` Output: ${opts.out}`);
|
|
394
|
+
if (opts.verbose) log.info("release", ` Verbose: on`);
|
|
395
|
+
log.info("release", "");
|
|
396
|
+
|
|
397
|
+
let fail = 0;
|
|
398
|
+
|
|
399
|
+
for (let _i = 0, _it = games, _n = _it.length; _i < _n; _i++) { const game = _it[_i];
|
|
400
|
+
const gameDir = resolveGameDir(game) ?? resolve(monorepoRoot ?? process.cwd(), "games", game);
|
|
401
|
+
const info = getGameInfo(gameDir, game);
|
|
402
|
+
log.info("release", `Game: ${game} | Product: "${info.productName}" | AppId: ${info.appId} | v${info.version}`);
|
|
403
|
+
log.info("release", "");
|
|
404
|
+
|
|
405
|
+
// Native packaging is a single compile step — build/package/release
|
|
406
|
+
// all produce the same artifact.
|
|
407
|
+
const isAndroid = targets.every((t) => MOBILE_TARGETS.has(t));
|
|
408
|
+
log.info("release", `[package] Compiling ${game} (${targets.join(", ")})...`);
|
|
409
|
+
const ok = isAndroid
|
|
410
|
+
? packageAndroid(game, gameDir, opts, info)
|
|
411
|
+
: packageNative(game, gameDir, targets, opts, info);
|
|
412
|
+
if (!ok) {
|
|
413
|
+
log.error("release", `Packaging failed for ${game} — skipping.`);
|
|
414
|
+
fail = 1;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
log.info("release", `${game} done.`);
|
|
419
|
+
log.info("release", "");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Summary
|
|
423
|
+
log.info("release", "");
|
|
424
|
+
if (fail) {
|
|
425
|
+
log.error("release", "One or more games failed — see output above.");
|
|
426
|
+
} else {
|
|
427
|
+
log.info("release", "All selected games processed successfully.");
|
|
428
|
+
}
|
|
429
|
+
log.info("release", `Artifacts: ${opts.out}/`);
|
|
430
|
+
|
|
431
|
+
process.exit(fail);
|
|
432
|
+
}
|