@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/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
- function getGameInfo(gameDir: string, game: string): { productName: string; appId: string; version: string } {
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
- if (!existsSync(pkgPath)) {
179
- return { productName: game, appId: `com.downdraft.${game}`, version: "0.0.0" };
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 { productName: game, appId: `com.downdraft.${game}`, version: "0.0.0" };
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: { productName: string; appId: string; version: string },
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 script ships inside @downdraft/cli (files: ["scripts"]) so
216
- // standalone game repos resolve it through the installed package, not the
217
- // engine monorepo.
218
- const script = fileURLToPath(new URL("../scripts/package-native.mjs", import.meta.url));
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 outfile = resolve(opts.out, `${game}-${target}`);
225
- const argv = [
226
- script,
227
- `--target=${target}`,
228
- `--mode=${opts.mode}`,
229
- `--product-name=${info.productName}`,
230
- `--product-version=${info.version}`,
231
- ...(opts.retainMcp ? ["--mcp"] : []),
232
- entry,
233
- outfile,
234
- ];
235
- log.info("release:package", `bun ${argv.map((a) => basename(a)).join(" ")}`);
236
- const result = spawnSync("bun", argv, {
237
- cwd,
238
- stdio: "inherit",
239
- env: process.env,
240
- });
241
- if (result.status !== 0) {
242
- log.error("release:package", `Native packaging failed for ${game} (${target})`);
243
- ok = false;
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: { productName: string; appId: string; version: string },
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 { productName, appId, version } = getGameInfo(gameDir, game);
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, { productName, appId, version })
353
- : packageNative(game, gameDir, targets, opts, { productName, appId, version });
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;
@@ -1,8 +1,8 @@
1
1
  // ============================================================================
2
2
  // Plugin scaffold — generates a new plugin directory from a template.
3
3
  //
4
- // Usage: `dd plugin new <name> --format <format> --game <game> [options]`
5
- // `dd mod new <name> --game <game> [options]`
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: "Deprecated no-op — native packaging produces a single binary + staged tree" },
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)" },
@@ -1,3 +1,2 @@
1
1
  "name": "<%= it.name %>",
2
- "version": "<%= it.version %>",
3
- "engine": "downdraft"
2
+ "version": "<%= it.version %>"