@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.
@@ -146,7 +146,7 @@ const GLOB_RE = new RegExp(IMPORT_META.source + String.raw`\.glob\(\s*(["'\`])([
146
146
  const GLOB_EAGER_RE = new RegExp(
147
147
  IMPORT_META.source + String.raw`\.glob\(\s*(["'\`])([^"'\`]+)\1\s*,\s*(\{[^)]*\beager\s*:\s*true[^)]*\})\s*\)`, "g");
148
148
  const STAGE_GLOB_RE = new RegExp(
149
- `(?:${IMPORT_META.source}\\.glob|_glob)\\(\\s*(["'\`])([^"'\`]+)\\1|createGlob\\([^)]*\\)\\(\\s*(["'\`])([^"'\`]+)\\3`, "g");
149
+ `(?:${IMPORT_META.source}\\.glob|_glob|globAssets)\\(\\s*(["'\`])([^"'\`]+)\\1|createGlob\\([^)]*\\)\\(\\s*(["'\`])([^"'\`]+)\\3`, "g");
150
150
  const STAGE_READ_RE = new RegExp(
151
151
  String.raw`(?:resolve|join)\(\s*(?:__dirname|${IMPORT_META.source}\.(?:dir|dirname|url))\s*,\s*(["'\`])([^"'\`]+)\1`, "g");
152
152
  const META_DIR_RE = new RegExp(IMPORT_META.source + String.raw`\.(?:dir|dirname)\b`, "g");
@@ -414,7 +414,11 @@ const makePlugin = (collectWorkers) => ({
414
414
  `const __ddModUrl = __ddP2F(__ddModDir + "/" + ${JSON.stringify(modBase)}).href;\n` +
415
415
  (needsGlob
416
416
  ? `import { createGlob as __ddCreateGlob } from "@downdraft/engine/platform/glob-polyfill";\n` +
417
- `const __ddGlob = __ddCreateGlob(__ddModDir);\n`
417
+ // Lazy — eager construction runs at module-init time and can
418
+ // hit a TDZ on consts declared later in the same module
419
+ // (glob-polyfill.ts itself matches REQUIRE_GLOB_RE via a
420
+ // comment).
421
+ `const __ddGlob = (p, o) => __ddCreateGlob(__ddModDir)(p, o);\n`
418
422
  : "") +
419
423
  metaOut;
420
424
  }
@@ -577,6 +581,31 @@ if (envPairs.length) {
577
581
  writeFileSync(join(bundleDir, "dd-env.txt"), envPairs.join("\n") + "\n");
578
582
  }
579
583
 
584
+ // Game-native addons — convention: <game>/native/dist/android-<arch>/*.node
585
+ // are napi modules the game built itself. .node files aren't jniLibs (the OS
586
+ // only installs lib*.so), so they ride inside the extracted bundle instead:
587
+ // the shell unpacks them to a real path under DOWNDRAFT_BUNDLE_DIR/native/
588
+ // and require()/process.dlopen resolves napi_* against the RTLD_GLOBAL
589
+ // libnode (the -Wl,-z,global link flag exists for exactly this).
590
+ // Staged BEFORE manifest.txt so the extractor's file list covers them.
591
+ // (.so siblings are handled per-ABI in the native-libs section below.)
592
+ {
593
+ let staged = 0;
594
+ for (const abi of abis) {
595
+ const platDir = join(gameDir, "native", "dist",
596
+ `android-${abi.startsWith("arm64") ? "arm64" : "x64"}`);
597
+ if (!existsSync(platDir)) continue;
598
+ const dest = join(bundleDir, "native", `android-${abi.startsWith("arm64") ? "arm64" : "x64"}`);
599
+ for (const f of readdirSync(platDir)) {
600
+ if (!f.endsWith(".node")) continue;
601
+ mkdirSync(dest, { recursive: true });
602
+ copyFileSync(join(platDir, f), join(dest, f));
603
+ staged++;
604
+ }
605
+ }
606
+ if (staged) console.log(`[package-mobile] staged ${staged} game-native addon(s) into bundle/native/`);
607
+ }
608
+
580
609
  // manifest.txt — the shell's extractor follows this verbatim (AAssetDir
581
610
  // can't enumerate subdirectories). aapt2 silently drops asset paths with
582
611
  // dot-prefixed segments (.bun, .gitignore), so each line is
@@ -645,11 +674,11 @@ for (const abi of abis) {
645
674
  ? join(monorepoRoot, `packages/node-mobile/build/dist/android-${nodeFlavor}`, abi, "libnode.so")
646
675
  : null,
647
676
  join(monorepoRoot, "packages/node-mobile/build/dist/android", abi, "libnode.so"),
648
- join(monorepoRoot, `packages/node-mobile-android-${abi.startsWith("arm64") ? "arm64" : "x64"}/lib/libnode.so`),
677
+ join(monorepoRoot, `packages/native-mobile-android-${abi.startsWith("arm64") ? "arm64" : "x64"}/lib/libnode.so`),
649
678
  ].filter(Boolean);
650
679
  let libnode = libnodeCandidates.find(existsSync);
651
680
  if (!libnode) {
652
- try { libnode = join(dirname(gameRequire.resolve(`@downdraft/node-mobile-android-${abi.startsWith("arm64") ? "arm64" : "x64"}/package.json`)), "lib/libnode.so"); } catch { /* absent */ }
681
+ try { libnode = join(dirname(gameRequire.resolve(`@downdraft/native-mobile-android-${abi.startsWith("arm64") ? "arm64" : "x64"}/package.json`)), "lib/libnode.so"); } catch { /* absent */ }
653
682
  }
654
683
  if (!libnode || !existsSync(libnode)) {
655
684
  throw new Error(`libnode.so for ${abi} not found — build via packages/node-mobile/scripts/build-android.mjs`);
@@ -703,6 +732,20 @@ for (const abi of abis) {
703
732
  if (existsSync(src)) copyFileSync(src, join(libDir, file));
704
733
  }
705
734
 
735
+ // Game-native cdylibs — <game>/native/dist/android-<arch>/lib*.so ride in
736
+ // jniLibs like the engine crates: the OS installs them to the app's native
737
+ // lib dir, where koffi/Deno.dlopen resolves them by bare soname (same
738
+ // mechanism lib-paths.ts's android branch relies on).
739
+ const gamePlatDir = join(gameDir, "native", "dist", nodePlat);
740
+ if (existsSync(gamePlatDir)) {
741
+ for (const f of readdirSync(gamePlatDir)) {
742
+ if (f.startsWith("lib") && f.endsWith(".so")) {
743
+ copyFileSync(join(gamePlatDir, f), join(libDir, f));
744
+ console.log(`[package-mobile] staged game-native ${f} → lib/${abi}/`);
745
+ }
746
+ }
747
+ }
748
+
706
749
  // Strip staged .so's — debug info and the symtab never map at runtime, so
707
750
  // this is pure APK/install size (libnode alone sheds ~25MB). Crash
708
751
  // symbolication still works: .dynsym and unwind info are preserved.
@@ -46,6 +46,7 @@ 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 { resolvePlatLibDir } from "./native-lib-fetch.mjs";
49
50
 
50
51
  // CRATES is shared with the repo's scripts/native-crates.mjs when this file
51
52
  // runs inside the monorepo. In a published @downdraft/cli install the repo
@@ -110,7 +111,7 @@ const GLOB_RE = new RegExp(IMPORT_META.source + String.raw`\.glob\(\s*(["'\`])([
110
111
  // Staging scan covers all glob call forms: import.meta.glob("pat"), the
111
112
  // common _glob("pat") alias, and createGlob(modDir)("pat").
112
113
  const STAGE_GLOB_RE = new RegExp(
113
- `(?:${IMPORT_META.source}\\.glob|_glob)\\(\\s*(["'\`])([^"'\`]+)\\1|createGlob\\([^)]*\\)\\(\\s*(["'\`])([^"'\`]+)\\3`, "g");
114
+ `(?:${IMPORT_META.source}\\.glob|_glob|globAssets)\\(\\s*(["'\`])([^"'\`]+)\\1|createGlob\\([^)]*\\)\\(\\s*(["'\`])([^"'\`]+)\\3`, "g");
114
115
  // Module-relative file reads: resolve(__dirname, "./zstd.wasm"),
115
116
  // join(import.meta.dir, "data.bin") — stage the referenced file.
116
117
  const STAGE_READ_RE = new RegExp(
@@ -371,7 +372,11 @@ const makePlugin = (collectWorkers) => ({
371
372
  `const __ddModUrl = __ddP2F(__ddModDir + "/" + ${JSON.stringify(modBase)}).href;\n` +
372
373
  (needsGlob
373
374
  ? `import { createGlob as __ddCreateGlob } from "@downdraft/engine/platform/glob-polyfill";\n` +
374
- `const __ddGlob = __ddCreateGlob(__ddModDir);\n`
375
+ // Lazy — eager construction runs at module-init time and can
376
+ // hit a TDZ on consts declared later in the same module
377
+ // (glob-polyfill.ts itself matches REQUIRE_GLOB_RE via a
378
+ // comment).
379
+ `const __ddGlob = (p, o) => __ddCreateGlob(__ddModDir)(p, o);\n`
375
380
  : "") +
376
381
  metaOut;
377
382
  }
@@ -440,8 +445,8 @@ result.outputs.forEach((out) => {
440
445
  // Metadata precedence: --product-name/--product-version/--icon flags, then
441
446
  // the game's package.json (productName / build.productName, version,
442
447
  // build.icon or icon.ico|icon.png in the game dir), then the game name.
448
+ const gameDir = dirname(dirname(resolve(entry))); // src/native-entry.ts → game root
443
449
  if (target.exe) {
444
- const gameDir = dirname(dirname(resolve(entry))); // src/native-entry.ts → game root
445
450
  let pkg = {};
446
451
  try { pkg = JSON.parse(readFileSync(join(gameDir, "package.json"), "utf-8")); } catch { /* standalone entry */ }
447
452
  const titleize = (s) => String(s).replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
@@ -556,20 +561,26 @@ function pkgRoot(name) {
556
561
  // wgpu — no external deps) stages under native/<platform>-<arch>/. Probe
557
562
  // order: monorepo dev staging, the installed platform-native package, the
558
563
  // per-target @downdraft/native-<plat>-<arch> package.
564
+ const platPkg = pkgRoot("@downdraft/platform-native");
559
565
  const platLibCandidates = [...new Set([
560
566
  join(monorepoRoot, "packages/platform-native/native", platKey),
561
- pkgRoot("@downdraft/platform-native") && join(pkgRoot("@downdraft/platform-native"), "native", platKey),
567
+ platPkg && join(platPkg, "native", platKey),
568
+ platPkg && join(platPkg, platKey),
562
569
  pkgRoot(`@downdraft/native-${platKey}`) && join(pkgRoot(`@downdraft/native-${platKey}`), "lib"),
563
570
  ].filter(Boolean))];
564
- const platStageDir = platLibCandidates.find((d) => existsSync(d));
565
- if (platStageDir) {
566
- for (const f of readdirSync(platStageDir)) {
567
- if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
568
- }
569
- } else {
571
+ // Foreign-platform optional deps aren't installed on this host — fetch the
572
+ // @downdraft/native-<plat>-<arch> tarball from npm into a cache instead.
573
+ const platPkgVersion = platPkg &&
574
+ JSON.parse(readFileSync(join(platPkg, "package.json"), "utf8")).version;
575
+ const platStageDir = await resolvePlatLibDir(
576
+ platKey, platLibCandidates, join(gameDir, "node_modules", ".cache"), platPkgVersion);
577
+ if (!platStageDir) {
570
578
  console.error(`no libdowndraft_platform build found for ${platKey} — searched: ${platLibCandidates.join(", ")}`);
571
579
  process.exit(1);
572
580
  }
581
+ for (const f of readdirSync(platStageDir)) {
582
+ if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
583
+ }
573
584
 
574
585
  // Engine cdylibs — staged if a build exists: new dist/<plat>-<arch>/ and
575
586
  // dist/ layouts (build-native.mjs), the installed @downdraft/engine package
@@ -603,6 +614,24 @@ engineCrates.forEach((crate) => {
603
614
  }
604
615
  });
605
616
 
617
+ // Game-native libs — convention: <game>/native/dist/<platform>-<arch>/ holds
618
+ // artifacts the game built itself (cdylib .so/.dll/.dylib loaded via the FFI
619
+ // adapter, or .node napi addons loaded via createRequire/process.dlopen).
620
+ // Staged flat into native/ so resolveNativeLibrary's <execDir>/native lookup
621
+ // and execDir-relative .node resolution both hit.
622
+ const gameNativeDir = join(gameDir, "native", "dist", platKey);
623
+ if (existsSync(gameNativeDir)) {
624
+ let gameLibs = 0;
625
+ for (const f of readdirSync(gameNativeDir)) {
626
+ if (f.endsWith(libExt) || f.endsWith(".node")) {
627
+ copyFileSync(join(gameNativeDir, f), join(nativeDir, f));
628
+ staged.add(f);
629
+ gameLibs++;
630
+ }
631
+ }
632
+ if (gameLibs) console.log(`staged ${gameLibs} game-native lib(s) from ${gameNativeDir}`);
633
+ }
634
+
606
635
  // Assets referenced via ?url / new URL / import.meta.glob.
607
636
  const assetRoot = join(outdir, "dd-assets");
608
637
  let stagedAssets = 0;
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