@downdraft/cli 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/package.json +36 -3
- package/scripts/package-mobile.mjs +779 -0
- package/scripts/package-native.mjs +616 -0
- package/src/args.ts +333 -0
- package/src/assets.ts +523 -0
- package/src/blob-store-s3.ts +145 -0
- package/src/build-games.ts +45 -0
- package/src/build.ts +40 -0
- package/src/bun-env.d.ts +6 -0
- package/src/debug.ts +56 -0
- package/src/dev.ts +231 -0
- package/src/dist.ts +36 -0
- package/src/export.ts +38 -0
- package/src/index.ts +131 -0
- package/src/list-games.ts +37 -0
- package/src/mcp.ts +320 -0
- package/src/new.ts +63 -0
- package/src/paths.ts +101 -0
- package/src/plugin-command.ts +214 -0
- package/src/process-utils.ts +179 -0
- package/src/release.ts +374 -0
- package/src/scaffold-plugin.ts +280 -0
- package/src/scaffold.ts +207 -0
- package/src/test.ts +187 -0
- package/src/usage.ts +255 -0
- package/templates/_shared/ai-companion/devin-config.eta +11 -0
- package/templates/_shared/ai-companion/engine-prompt.eta +155 -0
- package/templates/_shared/ai-companion/logging-rule.eta +3 -0
- package/templates/_shared/ai-companion/loops-rule.eta +3 -0
- package/templates/_shared/partials/config-json-fields.eta +3 -0
- package/templates/_shared/partials/dev-deps.eta +8 -0
- package/templates/_shared/partials/package-json-fields.eta +12 -0
- package/templates/_shared/partials/tsconfig.eta +16 -0
- package/templates/_shared/partials/vscode-extensions.eta +11 -0
- package/templates/_shared/partials/vscode-settings.eta +29 -0
- package/templates/_shared/partials/vscode-tasks.eta +93 -0
- package/templates/full/.gitignore.eta +5 -0
- package/templates/full/.vscode/extensions.json.eta +1 -0
- package/templates/full/.vscode/settings.json.eta +1 -0
- package/templates/full/.vscode/tasks.json.eta +1 -0
- package/templates/full/README.md.eta +45 -0
- package/templates/full/assets/shaders/.eta +0 -0
- package/templates/full/downdraft.config.json.eta +10 -0
- package/templates/full/package.json.eta +9 -0
- package/templates/full/src/entities/.eta +0 -0
- package/templates/full/src/game-module.ts.eta +30 -0
- package/templates/full/src/native-entry.ts.eta +14 -0
- package/templates/full/src/sim-host.ts.eta +19 -0
- package/templates/full/src/sim-worker.ts.eta +99 -0
- package/templates/full/src/systems/.eta +0 -0
- package/templates/full/tsconfig.json.eta +1 -0
- package/templates/gamemodule/README.md.eta +32 -0
- package/templates/gamemodule/downdraft.config.json.eta +3 -0
- package/templates/gamemodule/package.json.eta +9 -0
- package/templates/gamemodule/src/game-module.ts.eta +63 -0
- package/templates/gamemodule/src/native-entry.ts.eta +14 -0
- package/templates/gamemodule/src/sim-host.ts.eta +25 -0
- package/templates/gamemodule/src/sim-worker.ts.eta +24 -0
- package/templates/gamemodule/tsconfig.json.eta +1 -0
- package/templates/minimal/.gitignore.eta +5 -0
- package/templates/minimal/.vscode/extensions.json.eta +1 -0
- package/templates/minimal/.vscode/settings.json.eta +1 -0
- package/templates/minimal/.vscode/tasks.json.eta +1 -0
- package/templates/minimal/README.md.eta +32 -0
- package/templates/minimal/downdraft.config.json.eta +3 -0
- package/templates/minimal/package.json.eta +9 -0
- package/templates/minimal/src/game-module.ts.eta +44 -0
- package/templates/minimal/src/native-entry.ts.eta +14 -0
- package/templates/minimal/tsconfig.json.eta +1 -0
- package/templates/physics/.gitignore.eta +5 -0
- package/templates/physics/.vscode/extensions.json.eta +1 -0
- package/templates/physics/.vscode/settings.json.eta +1 -0
- package/templates/physics/.vscode/tasks.json.eta +1 -0
- package/templates/physics/README.md.eta +36 -0
- package/templates/physics/downdraft.config.json.eta +6 -0
- package/templates/physics/package.json.eta +9 -0
- package/templates/physics/src/game-module.ts.eta +29 -0
- package/templates/physics/src/native-entry.ts.eta +14 -0
- package/templates/physics/src/sim-host.ts.eta +19 -0
- package/templates/physics/src/sim-worker.ts.eta +155 -0
- package/templates/physics/tsconfig.json.eta +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// draft build-games — [DEPRECATED] alias for `draft release --games=<csv>`
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Native packaging produces a single compiled binary per desktop target —
|
|
6
|
+
// the legacy `platform:format` specs collapse to plain targets
|
|
7
|
+
// (win | linux | mac); android/ios hard-error in `release`.
|
|
8
|
+
|
|
9
|
+
import { createLogger } from "@downdraft/engine";
|
|
10
|
+
import { ArgError, parseArgs as parseArgv, print, renderHelp } from "./args";
|
|
11
|
+
import { getCommand } from "./usage";
|
|
12
|
+
|
|
13
|
+
const log = createLogger();
|
|
14
|
+
|
|
15
|
+
export async function buildGames(argv: string[]): Promise<void> {
|
|
16
|
+
const entry = getCommand("build-games")!;
|
|
17
|
+
// Check for --help before parsing (parseArgv validates required flags).
|
|
18
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
19
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
log.warn("build-games", "`draft build-games` is deprecated — use `draft release --games=<csv> --target=<csv>` instead.");
|
|
24
|
+
log.warn("build-games", "Delegating to `release`...");
|
|
25
|
+
|
|
26
|
+
const parsed = parseArgv(argv, entry.schema);
|
|
27
|
+
const games = ((parsed.flags.games as string) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
28
|
+
const platforms = ((parsed.flags.platforms as string) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
29
|
+
if (games.length === 0 || platforms.length === 0) {
|
|
30
|
+
throw new ArgError("Usage: draft build-games --games=<g1,g2> --platforms=<p1,p2>");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Legacy specs carry a format suffix (win:portable, linux:AppImage) — native
|
|
34
|
+
// packaging has a single artifact per platform, so only the target matters.
|
|
35
|
+
const targets = [...new Set(platforms.map((s) => s.split(":")[0]))];
|
|
36
|
+
|
|
37
|
+
const releaseArgs: string[] = [
|
|
38
|
+
`--games=${games.join(",")}`,
|
|
39
|
+
`--target=${targets.join(",")}`,
|
|
40
|
+
];
|
|
41
|
+
if (argv.includes("--verbose") || argv.includes("-v")) releaseArgs.push("--verbose");
|
|
42
|
+
|
|
43
|
+
const { release } = await import("./release");
|
|
44
|
+
await release(releaseArgs);
|
|
45
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// draft build — [DEPRECATED] alias for `draft release --stage=build`
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// The old `draft build` was a naive file-copy; the native pipeline compiles
|
|
6
|
+
// src/native-entry.ts via scripts/package-native.mjs. This module remains as
|
|
7
|
+
// a backward-compat alias that delegates to `release` with a warning.
|
|
8
|
+
|
|
9
|
+
import { createLogger } from "@downdraft/engine";
|
|
10
|
+
import { parseArgs, print, renderHelp } from "./args";
|
|
11
|
+
import { release } from "./release";
|
|
12
|
+
import { getCommand } from "./usage";
|
|
13
|
+
|
|
14
|
+
const log = createLogger();
|
|
15
|
+
|
|
16
|
+
export async function build(args: string[]): Promise<void> {
|
|
17
|
+
const entry = getCommand("build")!;
|
|
18
|
+
const parsed = parseArgs(args, entry.schema);
|
|
19
|
+
if (parsed.help) {
|
|
20
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
log.warn("build", "`draft build` is deprecated — use `draft release --stage=build` instead.");
|
|
25
|
+
log.warn("build", "Delegating to `release`...");
|
|
26
|
+
|
|
27
|
+
// Map old build args → release args.
|
|
28
|
+
const gameArg = parsed.flags.game as string | undefined;
|
|
29
|
+
const target = parsed.flags.target as string;
|
|
30
|
+
const mode = parsed.flags.mode as string;
|
|
31
|
+
|
|
32
|
+
// Build the release argv from the old build args.
|
|
33
|
+
const releaseArgs: string[] = ["--stage=build", `--mode=${mode}`];
|
|
34
|
+
if (gameArg) releaseArgs.push(`--game=${gameArg}`);
|
|
35
|
+
// Map build target → release target (skip "current" — release defaults to all desktop).
|
|
36
|
+
if (target && target !== "current") releaseArgs.push(`--target=${target}`);
|
|
37
|
+
if (parsed.flags.verbose as boolean) releaseArgs.push("--verbose");
|
|
38
|
+
|
|
39
|
+
await release(releaseArgs);
|
|
40
|
+
}
|
package/src/bun-env.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The CLI runs under Bun, which extends ImportMeta with `dir`/`path`/`file`.
|
|
2
|
+
// Declared locally rather than pulling in all of bun-types so the CLI stays
|
|
3
|
+
// typecheckable without exposing Bun globals to the rest of the repo.
|
|
4
|
+
interface ImportMeta {
|
|
5
|
+
readonly dir: string;
|
|
6
|
+
}
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createLogger } from "@downdraft/engine";
|
|
2
|
+
import { parseArgs, print, renderHelp } from "./args";
|
|
3
|
+
import { getCommand } from "./usage";
|
|
4
|
+
|
|
5
|
+
const log = createLogger();
|
|
6
|
+
|
|
7
|
+
export async function debug(args: string[]): Promise<void> {
|
|
8
|
+
const entry = getCommand("debug")!;
|
|
9
|
+
const parsed = parseArgs(args, entry.schema);
|
|
10
|
+
if (parsed.help) {
|
|
11
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const projectPath = parsed.positionals[0] ?? ".";
|
|
16
|
+
const verbose = parsed.flags.verbose as boolean;
|
|
17
|
+
const noDevtools = parsed.flags["no-devtools"] as boolean;
|
|
18
|
+
const inspector = parsed.flags.inspector as boolean;
|
|
19
|
+
|
|
20
|
+
// Wire --no-devtools to the runtime env var so the engine picks it up.
|
|
21
|
+
if (noDevtools) {
|
|
22
|
+
process.env.DOWNDRAFT_DISABLE_DEVTOOLS = "1";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
log.info("debug", `
|
|
26
|
+
╔══════════════════════════════════════════╗
|
|
27
|
+
║ DownDraft Engine — Debug Mode ║
|
|
28
|
+
╚══════════════════════════════════════════╝
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
log.info("debug", ` Project: ${projectPath}`);
|
|
32
|
+
log.info("debug", ` Mode: debug`);
|
|
33
|
+
log.info("debug", ` DevTools: ${noDevtools ? "disabled" : "enabled"}`);
|
|
34
|
+
log.info("debug", ` Verbose: ${verbose ? "on" : "off"}`);
|
|
35
|
+
log.info("debug", ` Inspector: ${inspector ? "on" : "off"}`);
|
|
36
|
+
|
|
37
|
+
log.info("debug", "Telemetry enabled:");
|
|
38
|
+
log.info("debug", " - Frame time tracking");
|
|
39
|
+
log.info("debug", " - CPU/GPU timing");
|
|
40
|
+
log.info("debug", " - Memory usage");
|
|
41
|
+
log.info("debug", " - System-level profiling");
|
|
42
|
+
|
|
43
|
+
log.info("debug", "Debug engine ready. Press Ctrl+C to stop.");
|
|
44
|
+
|
|
45
|
+
if (inspector) {
|
|
46
|
+
log.info("debug", "Inspector mode: attach to the engine process's inspector endpoint.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Keep process alive
|
|
50
|
+
await new Promise<void>((resolve) => {
|
|
51
|
+
process.on("SIGINT", () => {
|
|
52
|
+
log.info("debug", "Shutting down...");
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
package/src/dev.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createLogger } from "@downdraft/engine";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { parseArgs, print, renderHelp } from "./args";
|
|
8
|
+
import { formatGamesList } from "./list-games";
|
|
9
|
+
import { buildCwd, findMonorepoRoot } from "./paths";
|
|
10
|
+
import { killProcessTree } from "./process-utils";
|
|
11
|
+
import { getCommand } from "./usage";
|
|
12
|
+
|
|
13
|
+
const log = createLogger();
|
|
14
|
+
const GAME_CONFIG = "downdraft.config.json";
|
|
15
|
+
const NATIVE_ENTRY = "src/native-entry.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Exit code the dev shell uses to request a clean process restart
|
|
19
|
+
* (Tier-5: dev-shell/loader/config changes, unrecoverable teardown failure).
|
|
20
|
+
* Duplicated from platform-native's dev-constants.mjs — the CLI cannot import
|
|
21
|
+
* engine package internals at type-check time in every layout, so keep the
|
|
22
|
+
* literal in sync.
|
|
23
|
+
*/
|
|
24
|
+
const DD_RESTART_EXIT = 75;
|
|
25
|
+
|
|
26
|
+
/** Resolve packages/platform-native/src/dev/ — via package resolution first
|
|
27
|
+
* (standalone installs), then the monorepo layout. */
|
|
28
|
+
function resolveDevShellDir(): string {
|
|
29
|
+
try {
|
|
30
|
+
const req = createRequire(import.meta.url);
|
|
31
|
+
const pkgJson = req.resolve("@downdraft/platform-native/package.json");
|
|
32
|
+
const dir = join(dirname(pkgJson), "src", "dev");
|
|
33
|
+
if (existsSync(join(dir, "dev-shell.mjs"))) return dir;
|
|
34
|
+
} catch { /* fall through to monorepo guess */ }
|
|
35
|
+
const root = findMonorepoRoot();
|
|
36
|
+
if (root) return join(root, "packages/platform-native/src/dev");
|
|
37
|
+
return join(dirname(fileURLToPath(import.meta.url)), "../../platform-native/src/dev");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the game directory by walking up from `process.cwd()` looking for
|
|
42
|
+
* `downdraft.config.json`, falling back to `src/native-entry.ts` presence.
|
|
43
|
+
* This lets games run `draft dev` from their own directory (the scaffolded
|
|
44
|
+
* `package.json` already sets `"dev": "draft dev"`).
|
|
45
|
+
*
|
|
46
|
+
* Returns `{ game, gameDir }` or `null` if no game could be found.
|
|
47
|
+
*/
|
|
48
|
+
function resolveGameDir(): { game: string; gameDir: string } | null {
|
|
49
|
+
let dir = process.cwd();
|
|
50
|
+
for (;;) {
|
|
51
|
+
if (existsSync(resolve(dir, GAME_CONFIG)) || existsSync(resolve(dir, NATIVE_ENTRY))) {
|
|
52
|
+
return { game: basename(dir), gameDir: dir };
|
|
53
|
+
}
|
|
54
|
+
const parent = dirname(dir);
|
|
55
|
+
if (parent === dir) break; // reached filesystem root
|
|
56
|
+
dir = parent;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function dev(args: string[]): Promise<void> {
|
|
62
|
+
const entry = getCommand("dev")!;
|
|
63
|
+
const parsed = parseArgs(args, entry.schema);
|
|
64
|
+
if (parsed.help) {
|
|
65
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return devNative(parsed);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Native dev mode — runs the game with winit + wgpu under Bun, Node, or Deno.
|
|
74
|
+
* Looks for `src/native-entry.ts` in the game directory.
|
|
75
|
+
*/
|
|
76
|
+
async function devNative(parsed: any): Promise<void> {
|
|
77
|
+
const resolved = resolveGameDir();
|
|
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.`);
|
|
81
|
+
print(formatGamesList(findMonorepoRoot() ?? process.cwd()));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { game, gameDir } = resolved;
|
|
86
|
+
const nativeEntry = resolve(gameDir, NATIVE_ENTRY);
|
|
87
|
+
|
|
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.`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const verbose = parsed.flags.verbose as boolean;
|
|
95
|
+
const noHmr = parsed.flags["no-hmr"] as boolean;
|
|
96
|
+
|
|
97
|
+
const env = { ...process.env };
|
|
98
|
+
const port = parsed.flags.port as number;
|
|
99
|
+
if (port) env.MCP_PORT = String(port);
|
|
100
|
+
|
|
101
|
+
// Signal handling lives outside the respawn loop — each spawn registers
|
|
102
|
+
// itself as the current child so SIGINT/SIGTERM always hit the live one.
|
|
103
|
+
let currentChild: import("child_process").ChildProcess | null = null;
|
|
104
|
+
let shuttingDown = false;
|
|
105
|
+
const onSignal = () => {
|
|
106
|
+
if (shuttingDown) return;
|
|
107
|
+
shuttingDown = true;
|
|
108
|
+
log.info("DownDraft", "Shutting down...");
|
|
109
|
+
const child = currentChild;
|
|
110
|
+
try { child?.kill("SIGINT"); } catch {}
|
|
111
|
+
if (child?.pid) {
|
|
112
|
+
try { process.kill(-child.pid, "SIGTERM"); } catch {}
|
|
113
|
+
}
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
if (child?.pid) {
|
|
116
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch {}
|
|
117
|
+
killProcessTree(child.pid);
|
|
118
|
+
}
|
|
119
|
+
process.exit(130);
|
|
120
|
+
}, 2000);
|
|
121
|
+
};
|
|
122
|
+
process.on("SIGINT", onSignal);
|
|
123
|
+
process.on("SIGTERM", onSignal);
|
|
124
|
+
process.on("SIGHUP", onSignal);
|
|
125
|
+
|
|
126
|
+
// ── Legacy path: --no-hmr spawns the entry directly under Bun (no watcher,
|
|
127
|
+
// no ModuleRunner — the pre-HMR behavior, kept as an escape hatch). ──
|
|
128
|
+
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
|
+
const code = await spawnAndForward("bun", ["run", nativeEntry], buildCwd(gameDir), env, {
|
|
133
|
+
onChild: (c) => { currentChild = c; },
|
|
134
|
+
});
|
|
135
|
+
if (!shuttingDown) process.exit(code);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── HMR path: spawn the dev shell under the selected JS runtime. ──
|
|
140
|
+
const devDir = resolveDevShellDir();
|
|
141
|
+
const devShell = join(devDir, "dev-shell.mjs");
|
|
142
|
+
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.");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Runtime detection is delegated to the dev package (it owns the matrix).
|
|
149
|
+
const { detectRuntime, spawnArgsFor, findDenoConfig } =
|
|
150
|
+
await import(resolve(devDir, "runtime-detect.mjs"));
|
|
151
|
+
|
|
152
|
+
const requested = (parsed.flags.runtime as string | undefined) ?? process.env.DD_RUNTIME;
|
|
153
|
+
const runtime = detectRuntime(requested);
|
|
154
|
+
if (!runtime) {
|
|
155
|
+
if (requested) {
|
|
156
|
+
log.error("DownDraft", `Requested runtime "${requested}" is not available on PATH.`);
|
|
157
|
+
} else {
|
|
158
|
+
log.error("DownDraft", "No JS runtime found — need one of: bun, node, deno.");
|
|
159
|
+
}
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
env.DD_GAME_DIR = gameDir;
|
|
164
|
+
env.DD_ENTRY = nativeEntry;
|
|
165
|
+
env.DD_RUNTIME = runtime;
|
|
166
|
+
env.DD_HMR = "1";
|
|
167
|
+
env.DOWNDRAFT_DEV = "1";
|
|
168
|
+
if (verbose) env.DD_VERBOSE = "1";
|
|
169
|
+
const repoRoot = findMonorepoRoot();
|
|
170
|
+
if (repoRoot) env.DD_REPO_ROOT = repoRoot;
|
|
171
|
+
|
|
172
|
+
const { cmd, args } = spawnArgsFor(runtime, devShell, {
|
|
173
|
+
configPath: runtime === "deno" ? findDenoConfig(gameDir, repoRoot) : undefined,
|
|
174
|
+
});
|
|
175
|
+
|
|
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");
|
|
181
|
+
|
|
182
|
+
// Respawn loop: the dev shell exits with DD_RESTART_EXIT when a changed
|
|
183
|
+
// file affects the loader/config layer (Tier-5) — respawn in place.
|
|
184
|
+
for (;;) {
|
|
185
|
+
const code = await spawnAndForward(cmd, args, buildCwd(gameDir), env, {
|
|
186
|
+
onChild: (c) => { currentChild = c; },
|
|
187
|
+
});
|
|
188
|
+
if (shuttingDown) return; // signal handler already owns exit
|
|
189
|
+
if (code === DD_RESTART_EXIT) {
|
|
190
|
+
log.info("DownDraft", "Restarting dev shell (infrastructure change)...");
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
process.exit(code);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Spawn `cmd args`, forward stdio, resolve the exit code (signal exits are
|
|
198
|
+
* mapped to 128+n so the caller's respawn loop can distinguish them). */
|
|
199
|
+
function spawnAndForward(
|
|
200
|
+
cmd: string,
|
|
201
|
+
args: string[],
|
|
202
|
+
cwd: string,
|
|
203
|
+
env: NodeJS.ProcessEnv,
|
|
204
|
+
opts: { onChild?: (child: import("child_process").ChildProcess) => void } = {},
|
|
205
|
+
): Promise<number> {
|
|
206
|
+
const child = spawn(cmd, args, {
|
|
207
|
+
cwd,
|
|
208
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
209
|
+
env,
|
|
210
|
+
detached: true,
|
|
211
|
+
});
|
|
212
|
+
opts.onChild?.(child);
|
|
213
|
+
|
|
214
|
+
child.stdout?.on("data", (data: Buffer) => process.stdout.write(data));
|
|
215
|
+
child.stderr?.on("data", (data: Buffer) => process.stderr.write(data));
|
|
216
|
+
|
|
217
|
+
return new Promise<number>((resolvePromise, reject) => {
|
|
218
|
+
child.on("error", (err) => {
|
|
219
|
+
log.error("DownDraft", `Failed to spawn ${cmd}: ${err.message}`);
|
|
220
|
+
reject(err);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
child.on("exit", (code, signal) => {
|
|
224
|
+
if (signal) {
|
|
225
|
+
resolvePromise(128 + (signal === "SIGINT" ? 2 : signal === "SIGTERM" ? 15 : 1));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
resolvePromise(code ?? 0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
package/src/dist.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// draft dist — [DEPRECATED] alias for `draft release --stage=package`
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Desktop packaging is native: `draft release` compiles the game's
|
|
6
|
+
// src/native-entry.ts into a standalone Bun binary via the
|
|
7
|
+
// package-native.mjs script shipped in this package. This command delegates
|
|
8
|
+
// to `release` with a deprecation warning.
|
|
9
|
+
|
|
10
|
+
import { createLogger } from "@downdraft/engine";
|
|
11
|
+
import { parseArgs, print, renderHelp } from "./args";
|
|
12
|
+
import { getCommand } from "./usage";
|
|
13
|
+
|
|
14
|
+
const log = createLogger();
|
|
15
|
+
|
|
16
|
+
export async function dist(args: string[]): Promise<void> {
|
|
17
|
+
const entry = getCommand("dist")!;
|
|
18
|
+
const parsed = parseArgs(args, entry.schema);
|
|
19
|
+
if (parsed.help) {
|
|
20
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
log.warn("dist", "`draft dist` is deprecated — use `draft release --stage=package` instead.");
|
|
25
|
+
log.warn("dist", "Delegating to `release`...");
|
|
26
|
+
|
|
27
|
+
const releaseArgs: string[] = ["--stage=package"];
|
|
28
|
+
const game = parsed.flags.game as string | undefined;
|
|
29
|
+
const target = parsed.flags.target as string | undefined;
|
|
30
|
+
if (game) releaseArgs.push(`--game=${game}`);
|
|
31
|
+
if (target) releaseArgs.push(`--target=${target}`);
|
|
32
|
+
if (parsed.flags.verbose as boolean) releaseArgs.push("--verbose");
|
|
33
|
+
|
|
34
|
+
const { release } = await import("./release");
|
|
35
|
+
await release(releaseArgs);
|
|
36
|
+
}
|
package/src/export.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// draft export — [DEPRECATED] alias for `draft release --stage=package`
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// The old `draft export` copied a Vite `dist/` tree into per-platform
|
|
6
|
+
// launcher folders. Native packaging produces a single compiled binary +
|
|
7
|
+
// staged runtime tree instead — this command delegates to `release`.
|
|
8
|
+
|
|
9
|
+
import { createLogger } from "@downdraft/engine";
|
|
10
|
+
import { parseArgs, print, renderHelp } from "./args";
|
|
11
|
+
import { getCommand } from "./usage";
|
|
12
|
+
|
|
13
|
+
const log = createLogger();
|
|
14
|
+
|
|
15
|
+
export async function exportGame(args: string[]): Promise<void> {
|
|
16
|
+
const entry = getCommand("export")!;
|
|
17
|
+
const parsed = parseArgs(args, entry.schema);
|
|
18
|
+
if (parsed.help) {
|
|
19
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
log.warn("export", "`draft export` is deprecated — use `draft release --stage=package` instead.");
|
|
24
|
+
log.warn("export", "Delegating to `release`...");
|
|
25
|
+
|
|
26
|
+
const releaseArgs: string[] = ["--stage=package"];
|
|
27
|
+
const target = parsed.flags.target as string | undefined;
|
|
28
|
+
if (target) releaseArgs.push(`--target=${target}`);
|
|
29
|
+
if (parsed.flags.verbose as boolean) releaseArgs.push("--verbose");
|
|
30
|
+
|
|
31
|
+
const { basename } = await import("node:path");
|
|
32
|
+
const projectPath = parsed.positionals[0] ?? ".";
|
|
33
|
+
const gameName = basename(projectPath);
|
|
34
|
+
if (gameName && gameName !== ".") releaseArgs.push(`--game=${gameName}`);
|
|
35
|
+
|
|
36
|
+
const { release } = await import("./release");
|
|
37
|
+
await release(releaseArgs);
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createLogger } from "@downdraft/engine";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { ArgError, print, renderHelp } from "./args";
|
|
6
|
+
import { assets } from "./assets";
|
|
7
|
+
import { build } from "./build";
|
|
8
|
+
import { buildGames } from "./build-games";
|
|
9
|
+
import { debug } from "./debug";
|
|
10
|
+
import { dev } from "./dev";
|
|
11
|
+
import { dist } from "./dist";
|
|
12
|
+
import { exportGame } from "./export";
|
|
13
|
+
import { mcp } from "./mcp";
|
|
14
|
+
import { newProject } from "./new";
|
|
15
|
+
import { pluginCommand } from "./plugin-command";
|
|
16
|
+
import { release } from "./release";
|
|
17
|
+
import { runTest } from "./test";
|
|
18
|
+
import { getCommand, renderTopLevelHelp } from "./usage";
|
|
19
|
+
|
|
20
|
+
const log = createLogger();
|
|
21
|
+
|
|
22
|
+
// Read the CLI package version for --version output.
|
|
23
|
+
function getCliVersion(): string {
|
|
24
|
+
try {
|
|
25
|
+
const pkgPath = resolve(import.meta.dir, "../package.json");
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
27
|
+
return pkg.version ?? "0.0.0";
|
|
28
|
+
} catch {
|
|
29
|
+
return "0.0.0";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const command = process.argv[2];
|
|
34
|
+
|
|
35
|
+
async function main() {
|
|
36
|
+
// Global flags handled before command dispatch.
|
|
37
|
+
if (command === "--version" || command === "-V") {
|
|
38
|
+
print(`draft ${getCliVersion()}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (command === "--help" || command === "-h" || command === undefined) {
|
|
42
|
+
print(renderTopLevelHelp(getCliVersion()));
|
|
43
|
+
if (command === undefined) process.exit(1);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `help [command]` action.
|
|
48
|
+
if (command === "help") {
|
|
49
|
+
const target = process.argv[3];
|
|
50
|
+
if (!target) {
|
|
51
|
+
print(renderTopLevelHelp(getCliVersion()));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const entry = getCommand(target);
|
|
55
|
+
if (!entry) {
|
|
56
|
+
log.error("CLI", `Unknown command: ${target}`);
|
|
57
|
+
log.info("CLI", `Run 'draft --help' for the list of commands.`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
switch (command) {
|
|
66
|
+
case "new":
|
|
67
|
+
await newProject(process.argv.slice(3));
|
|
68
|
+
break;
|
|
69
|
+
case "release":
|
|
70
|
+
await release(process.argv.slice(3));
|
|
71
|
+
break;
|
|
72
|
+
case "dev":
|
|
73
|
+
await dev(process.argv.slice(3));
|
|
74
|
+
break;
|
|
75
|
+
case "debug":
|
|
76
|
+
await debug(process.argv.slice(3));
|
|
77
|
+
break;
|
|
78
|
+
case "build":
|
|
79
|
+
await build(process.argv.slice(3));
|
|
80
|
+
break;
|
|
81
|
+
case "build-games":
|
|
82
|
+
await buildGames(process.argv.slice(3));
|
|
83
|
+
break;
|
|
84
|
+
case "dist":
|
|
85
|
+
await dist(process.argv.slice(3));
|
|
86
|
+
break;
|
|
87
|
+
case "export":
|
|
88
|
+
await exportGame(process.argv.slice(3));
|
|
89
|
+
break;
|
|
90
|
+
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.");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
case "assets":
|
|
94
|
+
await assets(process.argv.slice(3));
|
|
95
|
+
break;
|
|
96
|
+
case "test":
|
|
97
|
+
await runTest(process.argv.slice(3));
|
|
98
|
+
break;
|
|
99
|
+
case "mcp":
|
|
100
|
+
await mcp(process.argv.slice(3));
|
|
101
|
+
break;
|
|
102
|
+
case "plugin":
|
|
103
|
+
await pluginCommand(process.argv.slice(3));
|
|
104
|
+
break;
|
|
105
|
+
case "mod":
|
|
106
|
+
// `dd mod` is an alias for `dd plugin` but defaults to mod.json format.
|
|
107
|
+
await pluginCommand(["--mod", ...process.argv.slice(3)]);
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
print(renderTopLevelHelp(getCliVersion()));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err instanceof ArgError) {
|
|
115
|
+
log.error("CLI", err.message);
|
|
116
|
+
const entry = getCommand(command);
|
|
117
|
+
if (entry) {
|
|
118
|
+
print("");
|
|
119
|
+
print(renderHelp(entry.usage, entry.schema));
|
|
120
|
+
}
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
main().then(() => {
|
|
128
|
+
// Explicit exit — the logger may hold open stream handles that keep
|
|
129
|
+
// the event loop alive after main() returns.
|
|
130
|
+
process.exit(0);
|
|
131
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { GAME_CONFIG_FILE } from "./paths";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* List available game names from the monorepo `games/` directory.
|
|
8
|
+
* A directory counts as a game if it contains `downdraft.config.json` or
|
|
9
|
+
* `src/native-entry.ts` (same predicate as `isGameDir` in paths.ts).
|
|
10
|
+
* Returns an empty array if the `games/` directory doesn't exist.
|
|
11
|
+
*/
|
|
12
|
+
export function listGames(repoRoot: string): string[] {
|
|
13
|
+
const gamesDir = resolve(repoRoot, "games");
|
|
14
|
+
if (!existsSync(gamesDir)) return [];
|
|
15
|
+
const out: string[] = [];
|
|
16
|
+
for (const entry of readdirSync(gamesDir)) {
|
|
17
|
+
const dir = resolve(gamesDir, entry);
|
|
18
|
+
try {
|
|
19
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
20
|
+
} catch {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (existsSync(resolve(dir, GAME_CONFIG_FILE)) || existsSync(resolve(dir, "src/native-entry.ts"))) {
|
|
24
|
+
out.push(entry);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out.sort();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format the available-games list for an error message.
|
|
32
|
+
*/
|
|
33
|
+
export function formatGamesList(repoRoot: string): string {
|
|
34
|
+
const games = listGames(repoRoot);
|
|
35
|
+
if (games.length === 0) return " (no games found in games/)";
|
|
36
|
+
return games.map((g) => ` - ${g}`).join("\n");
|
|
37
|
+
}
|