@downdraft/cli 0.1.0 → 0.1.2-alpha.5
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/package.json +4 -3
- package/scripts/default-icon.png +0 -0
- package/scripts/native-lib-fetch.mjs +63 -0
- package/scripts/package-desktop.mjs +937 -0
- package/scripts/package-mobile.mjs +47 -4
- package/scripts/package-native.mjs +39 -10
- package/src/dev.ts +63 -22
- package/src/index.ts +12 -6
- package/src/mcp.ts +6 -2
- package/src/paths.ts +17 -0
- package/src/plugin-command.ts +10 -10
- package/src/release.ts +103 -41
- package/src/scaffold-plugin.ts +2 -2
- package/src/usage.ts +7 -2
- package/templates/_shared/partials/config-json-fields.eta +1 -2
package/src/release.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { basename, resolve } from "node:path";
|
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { parseArgs, print, renderHelp } from "./args";
|
|
32
32
|
import { formatGamesList, listGames } from "./list-games";
|
|
33
|
-
import { findGameDirUpward, findMonorepoRoot, resolveGameDir } from "./paths";
|
|
33
|
+
import { findGameDirUpward, findMonorepoRoot, readGameConfig, resolveGameDir } from "./paths";
|
|
34
34
|
import { getCommand } from "./usage";
|
|
35
35
|
|
|
36
36
|
const log = createLogger();
|
|
@@ -56,6 +56,8 @@ interface ReleaseArgs {
|
|
|
56
56
|
out: string;
|
|
57
57
|
retainMcp: boolean;
|
|
58
58
|
verbose: boolean;
|
|
59
|
+
runtime: string | null;
|
|
60
|
+
format: string | null;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
// ---------------------------------------------------------------------------
|
|
@@ -84,10 +86,6 @@ function parseReleaseArgs(args: string[]): ReleaseArgs {
|
|
|
84
86
|
if (buildOnly) stage = "build";
|
|
85
87
|
else if (skipBuild) stage = "package";
|
|
86
88
|
|
|
87
|
-
if (parsed.flags.format) {
|
|
88
|
-
log.warn("release", "--format is gone — native packaging produces a single binary + staged tree.");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
89
|
return {
|
|
92
90
|
game,
|
|
93
91
|
games,
|
|
@@ -103,6 +101,8 @@ function parseReleaseArgs(args: string[]): ReleaseArgs {
|
|
|
103
101
|
out: (parsed.flags.out as string) || "release",
|
|
104
102
|
retainMcp: parsed.flags.mcp as boolean,
|
|
105
103
|
verbose: parsed.flags.verbose as boolean,
|
|
104
|
+
runtime: (parsed.flags.runtime as string) || null,
|
|
105
|
+
format: (parsed.flags.format as string) || null,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -173,20 +173,27 @@ function classifyTargets(target: string): string[] {
|
|
|
173
173
|
// Game info resolution
|
|
174
174
|
// ---------------------------------------------------------------------------
|
|
175
175
|
|
|
176
|
-
|
|
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 {
|
|
177
185
|
const pkgPath = resolve(gameDir, "package.json");
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
186
|
+
const fallback: GameInfo = { productName: game, appId: `com.downdraft.${game}`, version: "0.0.0", build: {} };
|
|
187
|
+
if (!existsSync(pkgPath)) return fallback;
|
|
181
188
|
try {
|
|
182
189
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
183
190
|
const productName = pkg.productName ?? pkg.build?.productName ??
|
|
184
191
|
String(pkg.name ?? game).replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
185
192
|
const appId = pkg.build?.appId ?? `com.downdraft.${game}`;
|
|
186
193
|
const version = pkg.version ?? "0.0.0";
|
|
187
|
-
return { productName, appId, version };
|
|
194
|
+
return { productName, appId, version, build: pkg.build ?? {} };
|
|
188
195
|
} catch {
|
|
189
|
-
return
|
|
196
|
+
return fallback;
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
|
|
@@ -205,42 +212,97 @@ function packageNative(
|
|
|
205
212
|
gameDir: string,
|
|
206
213
|
targets: string[],
|
|
207
214
|
opts: ReleaseArgs,
|
|
208
|
-
info:
|
|
215
|
+
info: GameInfo,
|
|
209
216
|
): boolean {
|
|
210
217
|
const entry = resolve(gameDir, "src/native-entry.ts");
|
|
211
218
|
if (!existsSync(entry)) {
|
|
212
219
|
log.error("release:package", `No src/native-entry.ts in ${gameDir} — the game has no native entry.`);
|
|
213
220
|
return false;
|
|
214
221
|
}
|
|
215
|
-
// The packaging
|
|
216
|
-
// standalone game repos resolve
|
|
217
|
-
// engine monorepo.
|
|
218
|
-
const
|
|
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));
|
|
219
227
|
// cwd is the relativization root for staged assets — the monorepo root
|
|
220
228
|
// when present (engine sources relativize cleanly), else the game dir.
|
|
221
229
|
const cwd = findMonorepoRoot(gameDir) ?? gameDir;
|
|
230
|
+
|
|
231
|
+
// Runtime matrix: --runtime flag → build.runtime in package.json →
|
|
232
|
+
// downdraft.config.json "runtime" → bun.
|
|
233
|
+
// "all" expands to the full matrix (one appdir per runtime).
|
|
234
|
+
const RUNTIMES = ["bun", "node", "deno"];
|
|
235
|
+
const requested = opts.runtime ?? info.build.runtime
|
|
236
|
+
?? (readGameConfig(gameDir).runtime as string | undefined) ?? "bun";
|
|
237
|
+
const runtimes = requested === "all" ? RUNTIMES : [requested];
|
|
238
|
+
for (const r of runtimes) {
|
|
239
|
+
if (!RUNTIMES.includes(r)) {
|
|
240
|
+
log.error("release:package", `Unknown runtime "${r}" — expected bun, node, deno, or all.`);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Non-bun runtimes are linux-only today — the emit-tree packaging hasn't
|
|
245
|
+
// been wired for win/mac.
|
|
246
|
+
const nonBun = runtimes.filter((r) => r !== "bun");
|
|
247
|
+
if (nonBun.length && targets.some((t) => t !== "linux")) {
|
|
248
|
+
log.warn("release:package", `--runtime=${nonBun.join(",")} is linux-only today — win/mac targets still embed bun.`);
|
|
249
|
+
}
|
|
250
|
+
|
|
222
251
|
let ok = true;
|
|
223
252
|
targets.forEach((target) => {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
253
|
+
for (const runtime of runtimes) {
|
|
254
|
+
if (target === "linux") {
|
|
255
|
+
// Linux: package-desktop.mjs owns the format matrix (dir, deb,
|
|
256
|
+
// appimage, flatpak). Formats come from --format → build.linux.target
|
|
257
|
+
// → "dir".
|
|
258
|
+
const format = opts.format
|
|
259
|
+
?? (Array.isArray(info.build.linux?.target) ? info.build.linux.target.join(",") : info.build.linux?.target)
|
|
260
|
+
?? "dir";
|
|
261
|
+
const argv = [
|
|
262
|
+
scriptDesktop,
|
|
263
|
+
`--runtime=${runtime}`,
|
|
264
|
+
`--format=${format}`,
|
|
265
|
+
`--mode=${opts.mode}`,
|
|
266
|
+
`--product-name=${info.productName}`,
|
|
267
|
+
`--product-version=${info.version}`,
|
|
268
|
+
`--app-id=${info.appId}`,
|
|
269
|
+
...(opts.retainMcp ? ["--mcp"] : []),
|
|
270
|
+
entry,
|
|
271
|
+
resolve(opts.out),
|
|
272
|
+
];
|
|
273
|
+
log.info("release:package", `bun package-desktop.mjs --runtime=${runtime} --format=${format} ${basename(entry)}`);
|
|
274
|
+
const result = spawnSync("bun", argv, { cwd, stdio: "inherit", env: process.env });
|
|
275
|
+
if (result.status !== 0) {
|
|
276
|
+
log.error("release:package", `Packaging failed for ${game} (${target}/${runtime})`);
|
|
277
|
+
ok = false;
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
// win/mac: single-binary bun compile via package-native.mjs. Each
|
|
282
|
+
// target gets its own dir so the staged native/ + dd-assets/ trees
|
|
283
|
+
// don't interleave across platforms.
|
|
284
|
+
if (runtime !== "bun") continue;
|
|
285
|
+
const outfile = resolve(opts.out, `${game}-${target}`, `${game}-${target}`);
|
|
286
|
+
const argv = [
|
|
287
|
+
scriptNative,
|
|
288
|
+
`--target=${target}`,
|
|
289
|
+
`--mode=${opts.mode}`,
|
|
290
|
+
`--product-name=${info.productName}`,
|
|
291
|
+
`--product-version=${info.version}`,
|
|
292
|
+
...(opts.retainMcp ? ["--mcp"] : []),
|
|
293
|
+
entry,
|
|
294
|
+
outfile,
|
|
295
|
+
];
|
|
296
|
+
log.info("release:package", `bun ${argv.map((a) => basename(a)).join(" ")}`);
|
|
297
|
+
const result = spawnSync("bun", argv, {
|
|
298
|
+
cwd,
|
|
299
|
+
stdio: "inherit",
|
|
300
|
+
env: process.env,
|
|
301
|
+
});
|
|
302
|
+
if (result.status !== 0) {
|
|
303
|
+
log.error("release:package", `Native packaging failed for ${game} (${target})`);
|
|
304
|
+
ok = false;
|
|
305
|
+
}
|
|
244
306
|
}
|
|
245
307
|
});
|
|
246
308
|
return ok;
|
|
@@ -259,7 +321,7 @@ function packageAndroid(
|
|
|
259
321
|
game: string,
|
|
260
322
|
gameDir: string,
|
|
261
323
|
opts: ReleaseArgs,
|
|
262
|
-
info:
|
|
324
|
+
info: GameInfo,
|
|
263
325
|
): boolean {
|
|
264
326
|
const entry = resolve(gameDir, "src/native-entry.ts");
|
|
265
327
|
if (!existsSync(entry)) {
|
|
@@ -340,8 +402,8 @@ export async function release(args: string[]): Promise<void> {
|
|
|
340
402
|
|
|
341
403
|
for (let _i = 0, _it = games, _n = _it.length; _i < _n; _i++) { const game = _it[_i];
|
|
342
404
|
const gameDir = resolveGameDir(game) ?? resolve(monorepoRoot ?? process.cwd(), "games", game);
|
|
343
|
-
const
|
|
344
|
-
log.info("release", `Game: ${game} | Product: "${productName}" | AppId: ${appId} | v${version}`);
|
|
405
|
+
const info = getGameInfo(gameDir, game);
|
|
406
|
+
log.info("release", `Game: ${game} | Product: "${info.productName}" | AppId: ${info.appId} | v${info.version}`);
|
|
345
407
|
log.info("release", "");
|
|
346
408
|
|
|
347
409
|
// Native packaging is a single compile step — build/package/release
|
|
@@ -349,8 +411,8 @@ export async function release(args: string[]): Promise<void> {
|
|
|
349
411
|
const isAndroid = targets.every((t) => MOBILE_TARGETS.has(t));
|
|
350
412
|
log.info("release", `[package] Compiling ${game} (${targets.join(", ")})...`);
|
|
351
413
|
const ok = isAndroid
|
|
352
|
-
? packageAndroid(game, gameDir, opts,
|
|
353
|
-
: packageNative(game, gameDir, targets, opts,
|
|
414
|
+
? packageAndroid(game, gameDir, opts, info)
|
|
415
|
+
: packageNative(game, gameDir, targets, opts, info);
|
|
354
416
|
if (!ok) {
|
|
355
417
|
log.error("release", `Packaging failed for ${game} — skipping.`);
|
|
356
418
|
fail = 1;
|
package/src/scaffold-plugin.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// Plugin scaffold — generates a new plugin directory from a template.
|
|
3
3
|
//
|
|
4
|
-
// Usage: `
|
|
5
|
-
// `
|
|
4
|
+
// Usage: `draft plugin new <name> --format <format> --game <game> [options]`
|
|
5
|
+
// `draft mod new <name> --game <game> [options]`
|
|
6
6
|
//
|
|
7
7
|
// Generates:
|
|
8
8
|
// - mod.json (manifest, when --mod is used) or plugin.json (legacy)
|
package/src/usage.ts
CHANGED
|
@@ -46,7 +46,8 @@ export const COMMANDS: CommandEntry[] = [
|
|
|
46
46
|
{ name: "game", alias: "g", type: "string", description: "Game to release (games/<game>). For multiple games, use --games. If omitted, infers from the current directory, or defaults to the only game in games/." },
|
|
47
47
|
{ name: "games", type: "string", description: "Comma-separated game names (e.g. sandjongg,to-the-ocean)" },
|
|
48
48
|
{ name: "target", alias: "t", type: "string", default: "all", enum: [...RELEASE_TARGETS], description: "Target platform(s): win, linux, mac, android, or all (android can't mix with desktop targets)" },
|
|
49
|
-
{ name: "format", type: "string", description: "
|
|
49
|
+
{ name: "format", type: "string", description: "Package format(s), comma-separated: dir, deb, appimage, flatpak (linux only; default: build.linux.target in package.json, else dir)" },
|
|
50
|
+
{ name: "runtime", type: "string", enum: ["bun", "node", "deno", "all"], description: "JS runtime embedded in the package: bun (compiled binary), node, deno, or all (default: build.runtime in package.json, \"runtime\" in downdraft.config.json, else bun). node/deno are linux-only today" },
|
|
50
51
|
{ name: "stage", type: "string", default: "release", enum: [...RELEASE_STAGES], description: "Stage: build, package, or release (all compile the native binary)" },
|
|
51
52
|
{ name: "mode", type: "string", default: "prod", enum: [...BUILD_MODES], description: "Build mode" },
|
|
52
53
|
{ name: "abi", type: "string", default: "arm64-v8a", description: "Android ABI(s): arm64-v8a, x86_64, or all (android target only)" },
|
|
@@ -89,10 +90,13 @@ export const COMMANDS: CommandEntry[] = [
|
|
|
89
90
|
flags: [
|
|
90
91
|
{ name: "entry", type: "string", description: "Game entrypoint file (defaults to src/native-entry.ts)" },
|
|
91
92
|
{ name: "port", type: "number", description: "MCP HTTP port (default: auto-assign)" },
|
|
92
|
-
{ name: "runtime", type: "string", enum: ["bun", "node", "deno"], description: "JS runtime hosting the dev shell (default: bun → node → deno auto-detect)" },
|
|
93
|
+
{ name: "runtime", type: "string", enum: ["bun", "node", "deno"], description: "JS runtime hosting the dev shell (default: DD_RUNTIME env, \"runtime\" in downdraft.config.json, else bun → node → deno auto-detect)" },
|
|
93
94
|
{ name: "watch", type: "boolean", description: "Back-compat no-op — the HMR dev shell always watches" },
|
|
94
95
|
{ name: "no-hmr", type: "boolean", description: "Disable HMR — spawn the entry directly (bun run), no dev shell" },
|
|
96
|
+
{ name: "engine-source", type: "boolean", description: "Resolve @downdraft/engine|platform-native from linked TypeScript source (default in the monorepo; enables file-level engine HMR)" },
|
|
97
|
+
{ name: "engine-dist", type: "boolean", description: "Resolve engine packages from their pre-built dist/ tree (default outside the monorepo; externalized = near-zero transform cost, no engine-file HMR)" },
|
|
95
98
|
{ name: "native", type: "boolean", description: "Back-compat alias — native is the default runtime" },
|
|
99
|
+
{ name: "native-debug", type: "boolean", description: "Load debug native libraries (built via build-native.mjs --debug) — sets DD_NATIVE_PROFILE=debug for the spawned runtime" },
|
|
96
100
|
{ name: "verbose", alias: "v", type: "boolean", description: "Verbose logging" },
|
|
97
101
|
],
|
|
98
102
|
},
|
|
@@ -195,6 +199,7 @@ export const COMMANDS: CommandEntry[] = [
|
|
|
195
199
|
{ name: "pid", type: "number", description: "Select instance by PID" },
|
|
196
200
|
{ name: "url", type: "string", description: "Explicit MCP HTTP URL (skips PID-file discovery)" },
|
|
197
201
|
{ name: "token", type: "string", description: "Bearer token (auto-read from <pid>.token otherwise)" },
|
|
202
|
+
{ name: "editor", type: "boolean", description: "Target the instance's editor endpoint (<pid>.editor — createMcpModule transport:\"http\") instead of the automation endpoint" },
|
|
198
203
|
{ name: "json", type: "boolean", description: "Print the raw result envelope (image data elided to {bytes} unless --out)" },
|
|
199
204
|
{ name: "out", type: "string", description: "Write image/binary result blocks to file(s)" },
|
|
200
205
|
{ name: "max-bytes", type: "number", description: "Cap printed output in bytes (default 262144)" },
|