@downdraft/cli 0.1.1 → 0.2.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downdraft/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/knackstedt/downdraft.git",
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@aws-sdk/client-s3": "^3.700.0",
15
- "@downdraft/engine": "^0.1.0",
15
+ "@downdraft/engine": "^0.2.0",
16
16
  "eta": "^3.5.0",
17
17
  "jimp": "^1.6.1",
18
18
  "resedit": "3.1.0"
@@ -31,7 +31,8 @@
31
31
  "scripts",
32
32
  "templates",
33
33
  "README.md",
34
- "!**/*.spec.ts"
34
+ "!**/*.spec.ts",
35
+ "!**/*.test.ts"
35
36
  ],
36
37
  "description": "The `draft` command-line tool — project scaffolding (draft new), dev server, builds, release packaging, mobile targets, and plugin/module scaffolding."
37
38
  }
@@ -0,0 +1,45 @@
1
+ // engine-source.mjs — decides whether bundling resolves @downdraft/* via
2
+ // the "downdraft-source" export condition (TypeScript src/) or the default
3
+ // condition (pre-built dist/).
4
+ //
5
+ // Published packages ship dist/ so standalone consumers bundle fast-built
6
+ // ESM. In the engine monorepo (or any install where dist/ hasn't been
7
+ // generated) the default target doesn't exist — without this the packager
8
+ // fails with "Could not resolve: @downdraft/...". Mirrors `draft dev`'s
9
+ // rule: monorepo defaults to source, everything else defaults to dist,
10
+ // --engine-source / --engine-dist override.
11
+ import { existsSync } from "node:fs";
12
+ import { createRequire } from "node:module";
13
+ import { dirname, join, resolve, sep } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ export const SOURCE_CONDITION = "downdraft-source";
17
+
18
+ const monorepoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
19
+
20
+ function enginePkgRoot(fromFile) {
21
+ try {
22
+ const req = createRequire(resolve(fromFile));
23
+ return dirname(req.resolve("@downdraft/engine/package.json"));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * @param {string[]} argv raw flag list (process.argv.slice(2))
31
+ * @param {string} fromFile file to anchor @downdraft/engine resolution at
32
+ * (typically the game entry — covers workspace links and standalone repos)
33
+ * @returns {string[] | undefined} Bun.build `conditions` override
34
+ */
35
+ export function engineConditions(argv, fromFile) {
36
+ if (argv.includes("--engine-dist")) return undefined;
37
+ if (argv.includes("--engine-source")) return [SOURCE_CONDITION];
38
+ const pkg = enginePkgRoot(fromFile);
39
+ // Workspace-linked engine → always source (a stale dist/ would silently
40
+ // package old code). Standalone consumer → dist when it exists.
41
+ if (!pkg || pkg.startsWith(monorepoRoot + sep) || !existsSync(join(pkg, "dist"))) {
42
+ return [SOURCE_CONDITION];
43
+ }
44
+ return undefined;
45
+ }
@@ -0,0 +1,63 @@
1
+ // ============================================================================
2
+ // native-lib-fetch.mjs — fetch @downdraft/native-<plat>-<arch> lib dirs
3
+ //
4
+ // The per-platform binary packages are published to npm as optional deps of
5
+ // @downdraft/platform-native, but npm/bun/deno skip installing packages whose
6
+ // os/cpu don't match the host — so cross-packaging (--target=win from linux,
7
+ // etc.) has no local copy of the target's cdylibs. The tarballs are still on
8
+ // the registry though; this fetches and caches them under
9
+ // <cacheRoot>/downdraft-native/<plat>-<arch>/lib.
10
+ // ============================================================================
11
+
12
+ import { execFileSync } from "node:child_process";
13
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { dirname, join } from "node:path";
16
+
17
+ /**
18
+ * Return a dir containing the target platform's cdylibs — the first existing
19
+ * candidate, else the @downdraft/native-<platKey> npm tarball fetched into
20
+ * the cache. Returns null when nothing is available.
21
+ *
22
+ * @param {string} platKey e.g. "win32-x64", "darwin-arm64"
23
+ * @param {string[]} candidates existing dirs to try before fetching
24
+ * @param {string} cacheRoot parent dir for the fetch cache
25
+ * @param {string|null} version preferred package version (platform-native's,
26
+ * lockstep) — falls back to the registry's latest tag
27
+ */
28
+ export async function resolvePlatLibDir(platKey, candidates, cacheRoot, version) {
29
+ const hit = candidates.find((d) => d && existsSync(d));
30
+ if (hit) return hit;
31
+
32
+ const pkgName = `@downdraft/native-${platKey}`;
33
+ const cacheDir = join(cacheRoot, "downdraft-native", platKey, "lib");
34
+ if (existsSync(cacheDir) && readdirSync(cacheDir).length > 0) return cacheDir;
35
+
36
+ try {
37
+ const meta = await (await fetch(
38
+ `https://registry.npmjs.org/${pkgName.replace("/", "%2F")}`)).json();
39
+ const ver = meta?.versions?.[version] ? version : meta?.["dist-tags"]?.latest;
40
+ const tarball = ver && meta.versions[ver]?.dist?.tarball;
41
+ if (!tarball) return null;
42
+
43
+ const tmp = mkdtempSync(join(tmpdir(), "dd-native-"));
44
+ try {
45
+ const tgz = join(tmp, "pkg.tgz");
46
+ writeFileSync(tgz, Buffer.from(await (await fetch(tarball)).arrayBuffer()));
47
+ execFileSync("tar", ["-xzf", tgz, "-C", tmp], { stdio: "pipe" });
48
+ const libDir = join(tmp, "package", "lib");
49
+ if (!existsSync(libDir) || readdirSync(libDir).length === 0) return null;
50
+ rmSync(dirname(cacheDir), { recursive: true, force: true });
51
+ mkdirSync(dirname(cacheDir), { recursive: true });
52
+ // cpSync not renameSync: TMPDIR may be a different filesystem.
53
+ cpSync(libDir, cacheDir, { recursive: true });
54
+ console.log(`[package] fetched ${pkgName}@${ver} from npm → ${cacheDir}`);
55
+ return cacheDir;
56
+ } finally {
57
+ rmSync(tmp, { recursive: true, force: true });
58
+ }
59
+ } catch (e) {
60
+ console.warn(`[package] ${pkgName} fetch failed: ${e?.message ?? e}`);
61
+ return null;
62
+ }
63
+ }
@@ -69,6 +69,8 @@ import {
69
69
  import { tmpdir } from "node:os";
70
70
  import { basename, dirname, join, relative, resolve } from "node:path";
71
71
  import { fileURLToPath } from "node:url";
72
+ import { engineConditions } from "./engine-source.mjs";
73
+ import { resolvePlatLibDir } from "./native-lib-fetch.mjs";
72
74
 
73
75
  // ── Args ──
74
76
 
@@ -100,7 +102,7 @@ if (!["bun", "node", "deno"].includes(runtime)) {
100
102
  const formats = (flagValue("format") ?? (Array.isArray(linuxCfg.target) ? linuxCfg.target.join(",") : linuxCfg.target) ?? "dir")
101
103
  .split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
102
104
  const KNOWN_FORMATS = new Set(["dir", "deb", "appimage", "flatpak"]);
103
- for (const f of formats) {
105
+ for (const f of formats.values()) {
104
106
  if (!KNOWN_FORMATS.has(f)) {
105
107
  console.error(`unknown --format "${f}" — expected dir, deb, appimage, or flatpak`);
106
108
  process.exit(1);
@@ -208,7 +210,7 @@ function assetRel(absPath) {
208
210
  function walkDir(dir, results = []) {
209
211
  let entries;
210
212
  try { entries = readdirSync(dir); } catch { return results; }
211
- for (const e of entries) {
213
+ for (const e of entries.values()) {
212
214
  const full = `${dir}/${e}`;
213
215
  let st;
214
216
  try { st = statSync(full); } catch { continue; }
@@ -461,6 +463,7 @@ const makeEmitPlugin = (collectWorkers) => ({
461
463
 
462
464
  // ── Scan pass: discover Worker entrypoints to a fixpoint ──
463
465
 
466
+ const engineConds = engineConditions(argv, resolve(entry));
464
467
  const scanOutdir = mkdtempSync(join(tmpdir(), "dd-scan-"));
465
468
  let prevCount = -1;
466
469
  while (workerEntries.size !== prevCount) {
@@ -468,6 +471,7 @@ while (workerEntries.size !== prevCount) {
468
471
  const scan = await Bun.build({
469
472
  entrypoints: [resolve(entry), ...workerEntries],
470
473
  target: "node",
474
+ conditions: engineConds,
471
475
  plugins: [makeEmitPlugin(true)],
472
476
  splitting: false,
473
477
  sourcemap: "none",
@@ -509,16 +513,21 @@ async function stageEngineLibs(nativeDir) {
509
513
  const platLibCandidates = [...new Set([
510
514
  join(repoRoot, "packages/platform-native/native", platKey),
511
515
  platPkg && join(platPkg, "native", platKey),
516
+ platPkg && join(platPkg, platKey),
512
517
  platNpm && join(platNpm, "lib"),
513
518
  ].filter(Boolean))];
514
- const platStageDir = platLibCandidates.find((d) => existsSync(d));
515
- if (platStageDir) {
516
- for (const f of readdirSync(platStageDir)) {
517
- if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
518
- }
519
- } else {
519
+ // Foreign-platform optional deps aren't installed on this host — fetch
520
+ // the @downdraft/native-<plat>-<arch> tarball from npm into a cache.
521
+ const platPkgVersion = platPkg &&
522
+ JSON.parse(readFileSync(join(platPkg, "package.json"), "utf8")).version;
523
+ const platStageDir = await resolvePlatLibDir(
524
+ platKey, platLibCandidates, join(gameDir, "node_modules", ".cache"), platPkgVersion);
525
+ if (!platStageDir) {
520
526
  throw new Error(`no libdowndraft_platform build found for ${platKey} — searched: ${platLibCandidates.join(", ")}`);
521
527
  }
528
+ for (const f of readdirSync(platStageDir)) {
529
+ if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
530
+ }
522
531
  // Engine cdylibs via the shared crate registry (same candidates as
523
532
  // package-native.mjs).
524
533
  let CRATES = [];
@@ -540,7 +549,7 @@ async function stageEngineLibs(nativeDir) {
540
549
  join(engineRoot, pkgRel, "dist", file),
541
550
  ] : []),
542
551
  ];
543
- for (const p of candidates) {
552
+ for (const p of candidates.values()) {
544
553
  if (existsSync(p)) { copyFileSync(p, join(nativeDir, file)); break; }
545
554
  }
546
555
  }
@@ -586,6 +595,7 @@ if (runtime === "bun") {
586
595
  entrypoints,
587
596
  target: "node",
588
597
  root: repoRoot,
598
+ conditions: engineConds,
589
599
  naming: { entry: "[dir]/[name].mjs" },
590
600
  plugins: [makeEmitPlugin(false)],
591
601
  splitting: false,
@@ -663,7 +673,7 @@ stageGameNative(nativeDir);
663
673
  let grew = true;
664
674
  while (grew) {
665
675
  grew = false;
666
- for (const f of [...assetFiles]) {
676
+ for (const f of [...assetFiles].values()) {
667
677
  if (!/\.(mjs|cjs|js)$/.test(f)) continue;
668
678
  let src;
669
679
  try { src = readFileSync(f, "utf-8"); } catch { continue; }
@@ -694,7 +704,7 @@ if (runtime !== "bun") {
694
704
  }
695
705
 
696
706
  // build.files — electron-builder-style extra files into the appdir root.
697
- for (const f of Array.isArray(build.files) ? build.files : []) {
707
+ for (const f of (Array.isArray(build.files) ? build.files : []).values()) {
698
708
  const src = resolve(gameDir, typeof f === "string" ? f : f.from);
699
709
  const dest = join(appdir, typeof f === "string" ? basename(f) : (f.to ?? basename(f.from)));
700
710
  if (!existsSync(src)) { console.warn(`[package-desktop] files entry missing: ${src}`); continue; }
@@ -736,7 +746,7 @@ async function iconSet(destDir) {
736
746
  const { Jimp } = await import("jimp");
737
747
  img = await Jimp.read(iconPath);
738
748
  } catch { /* unreadable — verbatim fallback below */ }
739
- for (const s of sizes) {
749
+ for (const s of sizes.values()) {
740
750
  const dir = join(destDir, `${s}x${s}`, "apps");
741
751
  mkdirSync(dir, { recursive: true });
742
752
  const dest = join(dir, `${exeName}.png`);
@@ -928,4 +938,4 @@ async function fetchKoffiPrebuild(pkgName, ver) {
928
938
  }
929
939
 
930
940
  console.log(`[package-desktop] done (${runtime}):`);
931
- for (const a of artifacts) console.log(` ${a}`);
941
+ for (const a of artifacts.values()) console.log(` ${a}`);
@@ -39,6 +39,7 @@ import {
39
39
  import { tmpdir } from "node:os";
40
40
  import { basename, dirname, join, relative, resolve } from "node:path";
41
41
  import { fileURLToPath } from "node:url";
42
+ import { engineConditions } from "./engine-source.mjs";
42
43
 
43
44
  // ── args ──
44
45
 
@@ -77,11 +78,12 @@ if (!entry || !outfileArg) {
77
78
  const ABIS = { "arm64-v8a": "aarch64-linux-android", "x86_64": "x86_64-linux-android" };
78
79
  const KOFFI_PKGS = { "arm64-v8a": "@koromix/koffi-android-arm64", "x86_64": "@koromix/koffi-android-x64" };
79
80
  const abis = abiArg === "all" ? Object.keys(ABIS) : abiArg.split(",").map((s) => s.trim());
80
- for (const a of abis) {
81
+ for (const a of abis.values()) {
81
82
  if (!ABIS[a]) throw new Error(`unknown --abi "${a}" — expected: ${Object.keys(ABIS).join(", ")}, all`);
82
83
  }
83
84
 
84
85
  const repoRoot = process.cwd();
86
+ const engineConds = engineConditions(argv, resolve(entry));
85
87
  const scriptDir = dirname(fileURLToPath(import.meta.url));
86
88
  const monorepoRoot = resolve(scriptDir, "../../..");
87
89
  const gameDir = dirname(dirname(resolve(entry)));
@@ -117,7 +119,7 @@ const btVer = readdirSync(btRoot).sort().pop();
117
119
  const bt = join(btRoot, btVer);
118
120
  const platformDir = readdirSync(join(sdkRoot, "platforms")).sort().pop();
119
121
  const androidJar = join(sdkRoot, "platforms", platformDir, "android.jar");
120
- for (const tool of ["aapt2", "zipalign", "apksigner"]) {
122
+ for (const tool of ["aapt2", "zipalign", "apksigner"].values()) {
121
123
  if (!existsSync(join(bt, tool))) throw new Error(`missing build-tool ${tool} in ${bt}`);
122
124
  }
123
125
  const adb = join(sdkRoot, "platform-tools", "adb");
@@ -185,7 +187,7 @@ function assetRel(absPath) {
185
187
  function walkDir(dir, results = []) {
186
188
  let entries;
187
189
  try { entries = readdirSync(dir); } catch { return results; }
188
- for (const e of entries) {
190
+ for (const e of entries.values()) {
189
191
  const full = `${dir}/${e}`;
190
192
  let st;
191
193
  try { st = statSync(full); } catch { continue; }
@@ -443,6 +445,7 @@ while (workerEntries.size !== prevCount) {
443
445
  const scan = await Bun.build({
444
446
  entrypoints: [resolve(entry), ...workerEntries],
445
447
  target: "node",
448
+ conditions: engineConds,
446
449
  plugins: [makePlugin(true)],
447
450
  splitting: false,
448
451
  sourcemap: "none",
@@ -466,6 +469,7 @@ const build = await Bun.build({
466
469
  // No `compile` — libnode executes the extracted ESM files.
467
470
  target: "node",
468
471
  root: repoRoot,
472
+ conditions: engineConds,
469
473
  naming: { entry: "[dir]/[name].mjs" },
470
474
  plugins: [makePlugin(false)],
471
475
  splitting: false,
@@ -500,7 +504,7 @@ if (!koffiRoot) {
500
504
  const nm = join(bundleDir, "node_modules");
501
505
  cpSync(koffiRoot, join(nm, "koffi"), { recursive: true });
502
506
  const koffiVer = JSON.parse(readFileSync(join(koffiRoot, "package.json"), "utf-8")).version;
503
- for (const abi of abis) {
507
+ for (const abi of abis.values()) {
504
508
  const pkgName = KOFFI_PKGS[abi];
505
509
  // The @koromix prebuild pkgs carry no importable entry — probe the dir
506
510
  // beside koffi (same layout koffi's loader expects), else fetch npm.
@@ -544,7 +548,7 @@ async function fetchKoffiPrebuild(pkgName, ver) {
544
548
  let grew = true;
545
549
  while (grew) {
546
550
  grew = false;
547
- for (const f of [...assetFiles]) {
551
+ for (const f of [...assetFiles].values()) {
548
552
  if (!/\.(mjs|cjs|js)$/.test(f)) continue;
549
553
  let src;
550
554
  try { src = readFileSync(f, "utf-8"); } catch { continue; }
@@ -573,7 +577,7 @@ for (const f of assetFiles.values()) {
573
577
  // dd-env.txt — optional process env for the JS thread (debug probes like
574
578
  // DD_BENCH_TEST; Android has no launch-env channel).
575
579
  if (envPairs.length) {
576
- for (const p of envPairs) {
580
+ for (const p of envPairs.values()) {
577
581
  if (!/^[A-Za-z_][A-Za-z0-9_]*=/.test(p)) {
578
582
  throw new Error(`--env "${p}" must be KEY=VALUE (KEY: [A-Za-z_][A-Za-z0-9_]*)`);
579
583
  }
@@ -591,7 +595,7 @@ if (envPairs.length) {
591
595
  // (.so siblings are handled per-ABI in the native-libs section below.)
592
596
  {
593
597
  let staged = 0;
594
- for (const abi of abis) {
598
+ for (const abi of abis.values()) {
595
599
  const platDir = join(gameDir, "native", "dist",
596
600
  `android-${abi.startsWith("arm64") ? "arm64" : "x64"}`);
597
601
  if (!existsSync(platDir)) continue;
@@ -661,7 +665,7 @@ const bundleText = walkDir(bundleDir)
661
665
  .map((f) => readFileSync(f, "utf-8"))
662
666
  .join("\n");
663
667
 
664
- for (const abi of abis) {
668
+ for (const abi of abis.values()) {
665
669
  const triple = ABIS[abi];
666
670
  const libDir = join(nativeStage, abi);
667
671
  mkdirSync(libDir, { recursive: true });
@@ -717,7 +721,7 @@ for (const abi of abis) {
717
721
  // --libs= forces). JS resolves them by soname via the app lib dir.
718
722
  const crates = await import(fileURLToPath(new URL("../../../scripts/native-crates.mjs", import.meta.url)));
719
723
  const nodePlat = `android-${abi.startsWith("arm64") ? "arm64" : "x64"}`;
720
- for (const crate of crates.CRATES) {
724
+ for (const crate of crates.CRATES.values()) {
721
725
  if (crate.androidOnly || crate.noAndroid) continue;
722
726
  const name = crate.lib.replace(/^downdraft_/, "");
723
727
  if (excludeLibs.includes(name) || excludeLibs.includes(crate.lib)) continue;
@@ -46,6 +46,8 @@ import { createRequire } from "node:module";
46
46
  import { tmpdir } from "node:os";
47
47
  import { basename, dirname, join, relative, resolve } from "node:path";
48
48
  import { fileURLToPath } from "node:url";
49
+ import { engineConditions } from "./engine-source.mjs";
50
+ import { resolvePlatLibDir } from "./native-lib-fetch.mjs";
49
51
 
50
52
  // CRATES is shared with the repo's scripts/native-crates.mjs when this file
51
53
  // runs inside the monorepo. In a published @downdraft/cli install the repo
@@ -100,6 +102,7 @@ let outfile = outfileArg;
100
102
  if (target.exe && !outfile.endsWith(".exe")) outfile += ".exe";
101
103
 
102
104
  const repoRoot = process.cwd();
105
+ const engineConds = engineConditions(argv, resolve(entry));
103
106
  const QUERY_RE = /\?(raw|url|json)$/;
104
107
  const WORKER_RE = /new\s+Worker\(\s*new\s+URL\(\s*["'`]([^"'`]+)["'`]\s*,\s*import\.meta\.url\s*\)/g;
105
108
  const ASSET_URL_RE = /new\s+URL\(\s*["'`]([^"'`]+)["'`]\s*,\s*import\.meta\.url\s*\)/g;
@@ -397,6 +400,7 @@ while (workerEntries.size !== prevCount) {
397
400
  const scan = await Bun.build({
398
401
  entrypoints: [resolve(entry), ...workerEntries],
399
402
  target: "bun",
403
+ conditions: engineConds,
400
404
  plugins: [makePlugin(true)],
401
405
  splitting: false,
402
406
  sourcemap: "none",
@@ -419,6 +423,7 @@ const result = await Bun.build({
419
423
  entrypoints,
420
424
  compile: { outfile: resolve(outfile), ...(target.bun ? { target: target.bun } : {}) },
421
425
  target: "bun",
426
+ conditions: engineConds,
422
427
  plugins: [makePlugin(false)],
423
428
  splitting: false,
424
429
  sourcemap: "none",
@@ -560,20 +565,26 @@ function pkgRoot(name) {
560
565
  // wgpu — no external deps) stages under native/<platform>-<arch>/. Probe
561
566
  // order: monorepo dev staging, the installed platform-native package, the
562
567
  // per-target @downdraft/native-<plat>-<arch> package.
568
+ const platPkg = pkgRoot("@downdraft/platform-native");
563
569
  const platLibCandidates = [...new Set([
564
570
  join(monorepoRoot, "packages/platform-native/native", platKey),
565
- pkgRoot("@downdraft/platform-native") && join(pkgRoot("@downdraft/platform-native"), "native", platKey),
571
+ platPkg && join(platPkg, "native", platKey),
572
+ platPkg && join(platPkg, platKey),
566
573
  pkgRoot(`@downdraft/native-${platKey}`) && join(pkgRoot(`@downdraft/native-${platKey}`), "lib"),
567
574
  ].filter(Boolean))];
568
- const platStageDir = platLibCandidates.find((d) => existsSync(d));
569
- if (platStageDir) {
570
- for (const f of readdirSync(platStageDir)) {
571
- if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
572
- }
573
- } else {
575
+ // Foreign-platform optional deps aren't installed on this host — fetch the
576
+ // @downdraft/native-<plat>-<arch> tarball from npm into a cache instead.
577
+ const platPkgVersion = platPkg &&
578
+ JSON.parse(readFileSync(join(platPkg, "package.json"), "utf8")).version;
579
+ const platStageDir = await resolvePlatLibDir(
580
+ platKey, platLibCandidates, join(gameDir, "node_modules", ".cache"), platPkgVersion);
581
+ if (!platStageDir) {
574
582
  console.error(`no libdowndraft_platform build found for ${platKey} — searched: ${platLibCandidates.join(", ")}`);
575
583
  process.exit(1);
576
584
  }
585
+ for (const f of readdirSync(platStageDir)) {
586
+ if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
587
+ }
577
588
 
578
589
  // Engine cdylibs — staged if a build exists: new dist/<plat>-<arch>/ and
579
590
  // dist/ layouts (build-native.mjs), the installed @downdraft/engine package
package/src/dev.ts CHANGED
@@ -6,7 +6,7 @@ import { basename, dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parseArgs, print, renderHelp } from "./args";
8
8
  import { formatGamesList } from "./list-games";
9
- import { buildCwd, findMonorepoRoot } from "./paths";
9
+ import { buildCwd, findMonorepoRoot, readGameConfig } from "./paths";
10
10
  import { killProcessTree } from "./process-utils";
11
11
  import { getCommand } from "./usage";
12
12
 
@@ -76,8 +76,8 @@ export async function dev(args: string[]): Promise<void> {
76
76
  async function devNative(parsed: any): Promise<void> {
77
77
  const resolved = resolveGameDir();
78
78
  if (!resolved) {
79
- log.error("DownDraft", `No game directory found (looking for downdraft.config.json or src/native-entry.ts from "${process.cwd()}").`);
80
- log.error("DownDraft", `Run "draft dev" from a game directory.`);
79
+ log.error("draft", `No game directory found (looking for downdraft.config.json or src/native-entry.ts from "${process.cwd()}").`);
80
+ log.error("draft", `Run "draft dev" from a game directory.`);
81
81
  print(formatGamesList(findMonorepoRoot() ?? process.cwd()));
82
82
  process.exit(1);
83
83
  }
@@ -86,8 +86,8 @@ async function devNative(parsed: any): Promise<void> {
86
86
  const nativeEntry = resolve(gameDir, NATIVE_ENTRY);
87
87
 
88
88
  if (!existsSync(nativeEntry)) {
89
- log.error("DownDraft", `No native entry point found at "${nativeEntry}".`);
90
- log.error("DownDraft", `The game "${game}" needs a src/native-entry.ts to run on the native runtime.`);
89
+ log.error("draft", `No native entry point found at "${nativeEntry}".`);
90
+ log.error("draft", `The game "${game}" needs a src/native-entry.ts to run on the native runtime.`);
91
91
  process.exit(1);
92
92
  }
93
93
 
@@ -97,6 +97,9 @@ async function devNative(parsed: any): Promise<void> {
97
97
  const env = { ...process.env };
98
98
  const port = parsed.flags.port as number;
99
99
  if (port) env.MCP_PORT = String(port);
100
+ // --native-debug → the runtime resolves <plat>-<arch>-debug/ lib dirs
101
+ // (staged by build-native.mjs --debug) instead of release artifacts.
102
+ if (parsed.flags["native-debug"]) env.DD_NATIVE_PROFILE = "debug";
100
103
 
101
104
  // Signal handling lives outside the respawn loop — each spawn registers
102
105
  // itself as the current child so SIGINT/SIGTERM always hit the live one.
@@ -105,7 +108,7 @@ async function devNative(parsed: any): Promise<void> {
105
108
  const onSignal = () => {
106
109
  if (shuttingDown) return;
107
110
  shuttingDown = true;
108
- log.info("DownDraft", "Shutting down...");
111
+ log.info("draft", "Shutting down...");
109
112
  const child = currentChild;
110
113
  try { child?.kill("SIGINT"); } catch {}
111
114
  if (child?.pid) {
@@ -126,9 +129,7 @@ async function devNative(parsed: any): Promise<void> {
126
129
  // ── Legacy path: --no-hmr spawns the entry directly under Bun (no watcher,
127
130
  // no ModuleRunner — the pre-HMR behavior, kept as an escape hatch). ──
128
131
  if (noHmr) {
129
- log.info("DownDraft", "Starting in native mode — direct spawn, no HMR (--no-hmr)...");
130
- log.info("DownDraft", ` Game: ${game}`);
131
- log.info("DownDraft", ` Entry: ${nativeEntry}`);
132
+ log.info("draft", `dev ${game} — ${nativeEntry} (bun, no hmr)`);
132
133
  const code = await spawnAndForward("bun", ["run", nativeEntry], buildCwd(gameDir), env, {
133
134
  onChild: (c) => { currentChild = c; },
134
135
  });
@@ -140,8 +141,8 @@ async function devNative(parsed: any): Promise<void> {
140
141
  const devDir = resolveDevShellDir();
141
142
  const devShell = join(devDir, "dev-shell.mjs");
142
143
  if (!existsSync(devShell)) {
143
- log.error("DownDraft", `Dev shell not found at "${devShell}".`);
144
- log.error("DownDraft", "Is @downdraft/platform-native installed? Try `draft dev --no-hmr` for the legacy path.");
144
+ log.error("draft", `Dev shell not found at "${devShell}".`);
145
+ log.error("draft", "Is @downdraft/platform-native installed? Try `draft dev --no-hmr` for the legacy path.");
145
146
  process.exit(1);
146
147
  }
147
148
 
@@ -149,13 +150,18 @@ async function devNative(parsed: any): Promise<void> {
149
150
  const { detectRuntime, spawnArgsFor, findDenoConfig } =
150
151
  await import(resolve(devDir, "runtime-detect.mjs"));
151
152
 
152
- const requested = (parsed.flags.runtime as string | undefined) ?? process.env.DD_RUNTIME;
153
+ // --runtime flag → DD_RUNTIME → downdraft.config.json "runtime" → detect.
154
+ // (absent flags parse as "" here, so || not ??)
155
+ const requested = (parsed.flags.runtime as string | undefined)
156
+ || process.env.DD_RUNTIME
157
+ || (readGameConfig(gameDir).runtime as string | undefined)
158
+ || undefined;
153
159
  const runtime = detectRuntime(requested);
154
160
  if (!runtime) {
155
161
  if (requested) {
156
- log.error("DownDraft", `Requested runtime "${requested}" is not available on PATH.`);
162
+ log.error("draft", `Requested runtime "${requested}" is not available on PATH.`);
157
163
  } else {
158
- log.error("DownDraft", "No JS runtime found — need one of: bun, node, deno.");
164
+ log.error("draft", "No JS runtime found — need one of: bun, node, deno.");
159
165
  }
160
166
  process.exit(1);
161
167
  }
@@ -165,19 +171,43 @@ async function devNative(parsed: any): Promise<void> {
165
171
  env.DD_RUNTIME = runtime;
166
172
  env.DD_HMR = "1";
167
173
  env.DOWNDRAFT_DEV = "1";
168
- if (verbose) env.DD_VERBOSE = "1";
174
+ if (verbose) {
175
+ env.DD_VERBOSE = "1";
176
+ // Diagnostics demoted below info (vite internals, pacing, gpu-registry)
177
+ // become visible under --verbose unless an explicit level is set.
178
+ env.EMBER_LOG_LEVEL = env.EMBER_LOG_LEVEL ?? "debug";
179
+ }
169
180
  const repoRoot = findMonorepoRoot();
170
181
  if (repoRoot) env.DD_REPO_ROOT = repoRoot;
171
182
 
183
+ // Engine resolution mode: "downdraft-source" resolves @downdraft/* to
184
+ // linked TypeScript source (per-module transforms + full engine HMR) —
185
+ // the default in the monorepo. Everything else resolves the pre-built
186
+ // dist/ tree (externalized native ESM — near-zero transform cost, no
187
+ // engine-file HMR). --engine-source/--engine-dist override.
188
+ let engineSource: boolean;
189
+ if (parsed.flags["engine-source"]) engineSource = true;
190
+ else if (parsed.flags["engine-dist"]) engineSource = false;
191
+ else engineSource = !!repoRoot;
192
+ if (!engineSource && !engineDistExists(gameDir)) {
193
+ log.warn("draft",
194
+ "engine dist/ not found — run `bun run build:dist` in the engine repo " +
195
+ "(or reinstall @downdraft/engine); resolving engine from source.");
196
+ engineSource = true;
197
+ }
198
+ env.DD_ENGINE_SOURCE = engineSource ? "1" : "";
199
+ env.DD_ENGINE_DIST = engineSource ? "" : "1";
200
+ // Deno reads export conditions from the env (flag equivalent passed to
201
+ // spawnArgsFor below); the var also propagates to Deno workers.
202
+ if (engineSource) env.DENO_CONDITIONS = "downdraft-source";
203
+
172
204
  const { cmd, args } = spawnArgsFor(runtime, devShell, {
173
205
  configPath: runtime === "deno" ? findDenoConfig(gameDir, repoRoot) : undefined,
206
+ conditions: engineSource ? ["downdraft-source"] : [],
174
207
  });
175
208
 
176
- log.info("DownDraft", `Starting in native mode (${runtime} + SDL + wgpu-native, HMR on)...`);
177
- log.info("DownDraft", ` Game: ${game}`);
178
- log.info("DownDraft", ` Entry: ${nativeEntry}`);
179
- log.info("DownDraft", ` Runtime: ${cmd} ${args.join(" ")}`);
180
- if (verbose) log.info("DownDraft", " Verbose: on");
209
+ log.info("draft", `dev ${game} — ${nativeEntry} (runtime=${runtime}, hmr, engine=${engineSource ? "source" : "dist"})`);
210
+ if (verbose) log.info("draft", `dev shell: ${cmd} ${args.join(" ")}`);
181
211
 
182
212
  // Respawn loop: the dev shell exits with DD_RESTART_EXIT when a changed
183
213
  // file affects the loader/config layer (Tier-5) — respawn in place.
@@ -187,13 +217,24 @@ async function devNative(parsed: any): Promise<void> {
187
217
  });
188
218
  if (shuttingDown) return; // signal handler already owns exit
189
219
  if (code === DD_RESTART_EXIT) {
190
- log.info("DownDraft", "Restarting dev shell (infrastructure change)...");
220
+ log.info("draft", "Restarting dev shell (infrastructure change)...");
191
221
  continue;
192
222
  }
193
223
  process.exit(code);
194
224
  }
195
225
  }
196
226
 
227
+ /** True when the resolved @downdraft/engine package has a pre-built dist/. */
228
+ function engineDistExists(gameDir: string): boolean {
229
+ try {
230
+ const req = createRequire(join(gameDir, "package.json"));
231
+ const pkgRoot = dirname(req.resolve("@downdraft/engine/package.json"));
232
+ return existsSync(join(pkgRoot, "dist"));
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+
197
238
  /** Spawn `cmd args`, forward stdio, resolve the exit code (signal exits are
198
239
  * mapped to 128+n so the caller's respawn loop can distinguish them). */
199
240
  function spawnAndForward(
@@ -216,7 +257,7 @@ function spawnAndForward(
216
257
 
217
258
  return new Promise<number>((resolvePromise, reject) => {
218
259
  child.on("error", (err) => {
219
- log.error("DownDraft", `Failed to spawn ${cmd}: ${err.message}`);
260
+ log.error("draft", `Failed to spawn ${cmd}: ${err.message}`);
220
261
  reject(err);
221
262
  });
222
263
 
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { createLogger } from "@downdraft/engine";
2
+ import { createLogger, getLogTheme } from "@downdraft/engine";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import { ArgError, print, renderHelp } from "./args";
@@ -19,6 +19,12 @@ import { getCommand, renderTopLevelHelp } from "./usage";
19
19
 
20
20
  const log = createLogger();
21
21
 
22
+ // Detect the log theme once against the real TTY and hand it to every
23
+ // spawned child (dev shell, games, test runner) — their piped stdout can't
24
+ // answer the OSC-11 probe, so without this they re-run blocking
25
+ // gsettings/dbus probes per process. An explicit DD_LOG_THEME wins.
26
+ process.env.DD_LOG_THEME ??= getLogTheme();
27
+
22
28
  // Read the CLI package version for --version output.
23
29
  function getCliVersion(): string {
24
30
  try {
@@ -53,8 +59,8 @@ async function main() {
53
59
  }
54
60
  const entry = getCommand(target);
55
61
  if (!entry) {
56
- log.error("CLI", `Unknown command: ${target}`);
57
- log.info("CLI", `Run 'draft --help' for the list of commands.`);
62
+ log.error("draft", `Unknown command: ${target}`);
63
+ log.info("draft", `Run 'draft --help' for the list of commands.`);
58
64
  process.exit(1);
59
65
  }
60
66
  print(renderHelp(entry.usage, entry.schema));
@@ -88,7 +94,7 @@ async function main() {
88
94
  await exportGame(process.argv.slice(3));
89
95
  break;
90
96
  case "mobile":
91
- log.error("CLI", "`draft mobile` is removed — the Capacitor WebView shell was dormant and has been deleted. Mobile support returns with a native (winit+wgpu) port.");
97
+ log.error("draft", "`draft mobile` is removed — the Capacitor WebView shell was dormant and has been deleted. Mobile support returns with a native (winit+wgpu) port.");
92
98
  process.exit(1);
93
99
  case "assets":
94
100
  await assets(process.argv.slice(3));
@@ -103,7 +109,7 @@ async function main() {
103
109
  await pluginCommand(process.argv.slice(3));
104
110
  break;
105
111
  case "mod":
106
- // `dd mod` is an alias for `dd plugin` but defaults to mod.json format.
112
+ // `draft mod` is an alias for `draft plugin` but defaults to mod.json format.
107
113
  await pluginCommand(["--mod", ...process.argv.slice(3)]);
108
114
  break;
109
115
  default:
@@ -112,7 +118,7 @@ async function main() {
112
118
  }
113
119
  } catch (err) {
114
120
  if (err instanceof ArgError) {
115
- log.error("CLI", err.message);
121
+ log.error("draft", err.message);
116
122
  const entry = getCommand(command);
117
123
  if (entry) {
118
124
  print("");
package/src/mcp.ts CHANGED
@@ -36,6 +36,7 @@ interface McpFlags {
36
36
  pid: number;
37
37
  url: string;
38
38
  token: string;
39
+ editor: boolean;
39
40
  json: boolean;
40
41
  out: string;
41
42
  maxBytes: number;
@@ -55,6 +56,7 @@ function selectorFrom(f: McpFlags): GameClientOptions {
55
56
  pid: f.pid || undefined,
56
57
  url: f.url || undefined,
57
58
  token: f.token || undefined,
59
+ endpoint: f.editor ? "editor" : undefined,
58
60
  timeoutMs: f.timeout || undefined,
59
61
  };
60
62
  }
@@ -218,6 +220,7 @@ export async function mcp(argv: string[]): Promise<void> {
218
220
  pid: parsed.flags.pid as number,
219
221
  url: parsed.flags.url as string,
220
222
  token: parsed.flags.token as string,
223
+ editor: parsed.flags.editor as boolean,
221
224
  json: parsed.flags.json as boolean,
222
225
  out: parsed.flags.out as string,
223
226
  maxBytes: (parsed.flags["max-bytes"] as number) || DEFAULT_MAX_BYTES,
@@ -236,13 +239,14 @@ export async function mcp(argv: string[]): Promise<void> {
236
239
 
237
240
  switch (action) {
238
241
  case "instances": {
239
- const insts = listGameInstances({ appId: f.app || undefined });
242
+ let insts = listGameInstances({ appId: f.app || undefined });
243
+ if (f.editor) insts = insts.filter((i) => i.editorPort !== undefined);
240
244
  if (insts.length === 0) {
241
245
  process.stderr.write("draft-mcp: no running instances\n");
242
246
  return;
243
247
  }
244
248
  insts.forEach((i) => {
245
- print(`${i.pid}\t${i.port}\t${i.appId ?? "-"}`);
249
+ print(`${i.pid}\t${i.port ?? "-"}\t${i.appId ?? "-"}\t${i.editorPort ?? "-"}`);
246
250
  });
247
251
  return;
248
252
  }
package/src/paths.ts CHANGED
@@ -28,6 +28,23 @@ function isGameDir(dir: string): boolean {
28
28
  || existsSync(join(dir, NATIVE_ENTRY_FILE));
29
29
  }
30
30
 
31
+ /**
32
+ * Read a game's `downdraft.config.json` — engine-level settings that apply
33
+ * regardless of the game's package.json build section. Missing/invalid files
34
+ * yield `{}` (the file doubles as a game-dir marker, so it may be absent).
35
+ *
36
+ * { "runtime": "bun" | "node" | "deno" } — default JS runtime for
37
+ * `draft dev`/`draft release` (overridden by --runtime / DD_RUNTIME).
38
+ */
39
+ export function readGameConfig(gameDir: string): Record<string, unknown> {
40
+ try {
41
+ const cfg = JSON.parse(readFileSync(join(gameDir, GAME_CONFIG_FILE), "utf8"));
42
+ return cfg && typeof cfg === "object" ? cfg : {};
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
47
+
31
48
  /**
32
49
  * Find the downdraft monorepo root — a directory whose package.json is named
33
50
  * "downdraft-engine" and contains packages/engine. Searches cwd ancestry first,
@@ -1,9 +1,9 @@
1
1
  // ============================================================================
2
- // `dd plugin` — CLI subcommand for plugin management.
2
+ // `draft plugin` — CLI subcommand for plugin management.
3
3
  //
4
4
  // Usage:
5
- // dd plugin new <name> --format <format> --game <game> [options]
6
- // dd plugin list [--game <game>]
5
+ // draft plugin new <name> --format <format> --game <game> [options]
6
+ // draft plugin list [--game <game>]
7
7
  // ============================================================================
8
8
 
9
9
  import { createLogger } from "@downdraft/engine";
@@ -16,8 +16,8 @@ import { scaffoldPlugin } from "./scaffold-plugin";
16
16
  const log = createLogger();
17
17
 
18
18
  export async function pluginCommand(args: string[]): Promise<void> {
19
- // `dd mod new ...` injects --mod flag so scaffoldPlugin generates mod.json.
20
- // `dd plugin new ...` defaults to plugin.json (legacy).
19
+ // `draft mod new ...` injects --mod flag so scaffoldPlugin generates mod.json.
20
+ // `draft plugin new ...` defaults to plugin.json (legacy).
21
21
  let modMode = false;
22
22
  if (args[0] === "--mod") {
23
23
  modMode = true;
@@ -46,7 +46,7 @@ export async function pluginCommand(args: string[]): Promise<void> {
46
46
  }
47
47
 
48
48
  async function pluginNew(args: string[], modMode: boolean): Promise<void> {
49
- // Simple arg parsing: dd plugin new <name> --format <format> --game <game> [--version <ver>] [--author <name>] [--description <desc>] [--target <dir>] [--force] [--mod] [--with <ext>]
49
+ // Simple arg parsing: draft plugin new <name> --format <format> --game <game> [--version <ver>] [--author <name>] [--description <desc>] [--target <dir>] [--force] [--mod] [--with <ext>]
50
50
  const positional: string[] = [];
51
51
  const flags: Record<string, string | boolean> = {};
52
52
  for (let i = 0; i < args.length; i++) {
@@ -67,7 +67,7 @@ async function pluginNew(args: string[], modMode: boolean): Promise<void> {
67
67
 
68
68
  const id = positional[0];
69
69
  if (!id) {
70
- log.error("plugin", `Usage: dd ${modMode ? "mod" : "plugin"} new <name> --format <format> --game <game>`);
70
+ log.error("plugin", `Usage: draft ${modMode ? "mod" : "plugin"} new <name> --format <format> --game <game>`);
71
71
  process.exit(1);
72
72
  }
73
73
 
@@ -190,11 +190,11 @@ function pluginList(args: string[]): void {
190
190
  function printPluginHelp(modMode: boolean): void {
191
191
  const cmd = modMode ? "mod" : "plugin";
192
192
  print(`
193
- dd ${cmd} — manage user-authored ${modMode ? "mods" : "plugins"}
193
+ draft ${cmd} — manage user-authored ${modMode ? "mods" : "plugins"}
194
194
 
195
195
  Usage:
196
- dd ${cmd} new <name> --format <format> --game <game> [options]
197
- dd ${cmd} list [--game <game>]
196
+ draft ${cmd} new <name> --format <format> --game <game> [options]
197
+ draft ${cmd} list [--game <game>]
198
198
 
199
199
  Commands:
200
200
  new Scaffold a new ${modMode ? "mod" : "plugin"} directory
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();
@@ -228,12 +228,14 @@ function packageNative(
228
228
  // when present (engine sources relativize cleanly), else the game dir.
229
229
  const cwd = findMonorepoRoot(gameDir) ?? gameDir;
230
230
 
231
- // Runtime matrix: --runtime flag → build.runtime in package.json → bun.
231
+ // Runtime matrix: --runtime flag → build.runtime in package.json →
232
+ // downdraft.config.json "runtime" → bun.
232
233
  // "all" expands to the full matrix (one appdir per runtime).
233
234
  const RUNTIMES = ["bun", "node", "deno"];
234
- const requested = opts.runtime ?? info.build.runtime ?? "bun";
235
+ const requested = opts.runtime ?? info.build.runtime
236
+ ?? (readGameConfig(gameDir).runtime as string | undefined) ?? "bun";
235
237
  const runtimes = requested === "all" ? RUNTIMES : [requested];
236
- for (const r of runtimes) {
238
+ for (const r of runtimes.values()) {
237
239
  if (!RUNTIMES.includes(r)) {
238
240
  log.error("release:package", `Unknown runtime "${r}" — expected bun, node, deno, or all.`);
239
241
  return false;
@@ -248,7 +250,7 @@ function packageNative(
248
250
 
249
251
  let ok = true;
250
252
  targets.forEach((target) => {
251
- for (const runtime of runtimes) {
253
+ for (const runtime of runtimes.values()) {
252
254
  if (target === "linux") {
253
255
  // Linux: package-desktop.mjs owns the format matrix (dir, deb,
254
256
  // appimage, flatpak). Formats come from --format → build.linux.target
@@ -276,9 +278,11 @@ function packageNative(
276
278
  }
277
279
  continue;
278
280
  }
279
- // win/mac: single-binary bun compile via package-native.mjs.
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.
280
284
  if (runtime !== "bun") continue;
281
- const outfile = resolve(opts.out, `${game}-${target}`);
285
+ const outfile = resolve(opts.out, `${game}-${target}`, `${game}-${target}`);
282
286
  const argv = [
283
287
  scriptNative,
284
288
  `--target=${target}`,
@@ -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
@@ -47,7 +47,7 @@ export const COMMANDS: CommandEntry[] = [
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
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, else bun). node/deno are linux-only today" },
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" },
51
51
  { name: "stage", type: "string", default: "release", enum: [...RELEASE_STAGES], description: "Stage: build, package, or release (all compile the native binary)" },
52
52
  { name: "mode", type: "string", default: "prod", enum: [...BUILD_MODES], description: "Build mode" },
53
53
  { name: "abi", type: "string", default: "arm64-v8a", description: "Android ABI(s): arm64-v8a, x86_64, or all (android target only)" },
@@ -90,10 +90,13 @@ export const COMMANDS: CommandEntry[] = [
90
90
  flags: [
91
91
  { name: "entry", type: "string", description: "Game entrypoint file (defaults to src/native-entry.ts)" },
92
92
  { name: "port", type: "number", description: "MCP HTTP port (default: auto-assign)" },
93
- { 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)" },
94
94
  { name: "watch", type: "boolean", description: "Back-compat no-op — the HMR dev shell always watches" },
95
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)" },
96
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" },
97
100
  { name: "verbose", alias: "v", type: "boolean", description: "Verbose logging" },
98
101
  ],
99
102
  },
@@ -196,6 +199,7 @@ export const COMMANDS: CommandEntry[] = [
196
199
  { name: "pid", type: "number", description: "Select instance by PID" },
197
200
  { name: "url", type: "string", description: "Explicit MCP HTTP URL (skips PID-file discovery)" },
198
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" },
199
203
  { name: "json", type: "boolean", description: "Print the raw result envelope (image data elided to {bytes} unless --out)" },
200
204
  { name: "out", type: "string", description: "Write image/binary result blocks to file(s)" },
201
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 %>"