@downdraft/cli 0.1.0 → 0.1.2-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/scripts/default-icon.png +0 -0
- package/scripts/native-lib-fetch.mjs +63 -0
- package/scripts/package-desktop.mjs +937 -0
- package/scripts/package-mobile.mjs +47 -4
- package/scripts/package-native.mjs +39 -10
- package/src/dev.ts +63 -22
- package/src/index.ts +12 -6
- package/src/mcp.ts +6 -2
- package/src/paths.ts +17 -0
- package/src/plugin-command.ts +10 -10
- package/src/release.ts +103 -41
- package/src/scaffold-plugin.ts +2 -2
- package/src/usage.ts +7 -2
- package/templates/_shared/partials/config-json-fields.eta +1 -2
|
@@ -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
|
-
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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("
|
|
80
|
-
log.error("
|
|
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("
|
|
90
|
-
log.error("
|
|
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("
|
|
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("
|
|
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("
|
|
144
|
-
log.error("
|
|
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
|
-
|
|
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("
|
|
162
|
+
log.error("draft", `Requested runtime "${requested}" is not available on PATH.`);
|
|
157
163
|
} else {
|
|
158
|
-
log.error("
|
|
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)
|
|
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("
|
|
177
|
-
log.info("
|
|
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("
|
|
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("
|
|
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("
|
|
57
|
-
log.info("
|
|
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("
|
|
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
|
-
// `
|
|
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("
|
|
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
|
-
|
|
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,
|
package/src/plugin-command.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// `
|
|
2
|
+
// `draft plugin` — CLI subcommand for plugin management.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
// `
|
|
20
|
-
// `
|
|
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:
|
|
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:
|
|
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
|
-
|
|
193
|
+
draft ${cmd} — manage user-authored ${modMode ? "mods" : "plugins"}
|
|
194
194
|
|
|
195
195
|
Usage:
|
|
196
|
-
|
|
197
|
-
|
|
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
|