@downdraft/cli 0.1.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downdraft/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-alpha.5",
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.1.2-alpha.5",
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,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,7 @@ 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 { resolvePlatLibDir } from "./native-lib-fetch.mjs";
72
73
 
73
74
  // ── Args ──
74
75
 
@@ -509,16 +510,21 @@ async function stageEngineLibs(nativeDir) {
509
510
  const platLibCandidates = [...new Set([
510
511
  join(repoRoot, "packages/platform-native/native", platKey),
511
512
  platPkg && join(platPkg, "native", platKey),
513
+ platPkg && join(platPkg, platKey),
512
514
  platNpm && join(platNpm, "lib"),
513
515
  ].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 {
516
+ // Foreign-platform optional deps aren't installed on this host — fetch
517
+ // the @downdraft/native-<plat>-<arch> tarball from npm into a cache.
518
+ const platPkgVersion = platPkg &&
519
+ JSON.parse(readFileSync(join(platPkg, "package.json"), "utf8")).version;
520
+ const platStageDir = await resolvePlatLibDir(
521
+ platKey, platLibCandidates, join(gameDir, "node_modules", ".cache"), platPkgVersion);
522
+ if (!platStageDir) {
520
523
  throw new Error(`no libdowndraft_platform build found for ${platKey} — searched: ${platLibCandidates.join(", ")}`);
521
524
  }
525
+ for (const f of readdirSync(platStageDir)) {
526
+ if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
527
+ }
522
528
  // Engine cdylibs via the shared crate registry (same candidates as
523
529
  // package-native.mjs).
524
530
  let CRATES = [];
@@ -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
@@ -560,20 +561,26 @@ function pkgRoot(name) {
560
561
  // wgpu — no external deps) stages under native/<platform>-<arch>/. Probe
561
562
  // order: monorepo dev staging, the installed platform-native package, the
562
563
  // per-target @downdraft/native-<plat>-<arch> package.
564
+ const platPkg = pkgRoot("@downdraft/platform-native");
563
565
  const platLibCandidates = [...new Set([
564
566
  join(monorepoRoot, "packages/platform-native/native", platKey),
565
- pkgRoot("@downdraft/platform-native") && join(pkgRoot("@downdraft/platform-native"), "native", platKey),
567
+ platPkg && join(platPkg, "native", platKey),
568
+ platPkg && join(platPkg, platKey),
566
569
  pkgRoot(`@downdraft/native-${platKey}`) && join(pkgRoot(`@downdraft/native-${platKey}`), "lib"),
567
570
  ].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 {
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) {
574
578
  console.error(`no libdowndraft_platform build found for ${platKey} — searched: ${platLibCandidates.join(", ")}`);
575
579
  process.exit(1);
576
580
  }
581
+ for (const f of readdirSync(platStageDir)) {
582
+ if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
583
+ }
577
584
 
578
585
  // Engine cdylibs — staged if a build exists: new dist/<plat>-<arch>/ and
579
586
  // 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,10 +228,12 @@ 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
238
  for (const r of runtimes) {
237
239
  if (!RUNTIMES.includes(r)) {
@@ -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 %>"