@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 +4 -3
- package/scripts/native-lib-fetch.mjs +63 -0
- package/scripts/package-desktop.mjs +12 -6
- package/scripts/package-native.mjs +14 -7
- 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 +9 -5
- package/src/scaffold-plugin.ts +2 -2
- package/src/usage.ts +6 -2
- package/templates/_shared/partials/config-json-fields.eta +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@downdraft/cli",
|
|
3
|
-
"version": "0.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.
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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("
|
|
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
|
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 →
|
|
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
|
|
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}`,
|
package/src/scaffold-plugin.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// Plugin scaffold — generates a new plugin directory from a template.
|
|
3
3
|
//
|
|
4
|
-
// Usage: `
|
|
5
|
-
// `
|
|
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)" },
|