@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
package/src/args.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// args.ts — shared CLI argument parser
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// A dependency-free parser shared by every `draft` subcommand. Replaces the
|
|
6
|
+
// per-command hand-rolled argv loops with a single, consistent, validated
|
|
7
|
+
// surface:
|
|
8
|
+
//
|
|
9
|
+
// - Supports `--flag`, `--flag=value`, `--flag value`, `-x`, `-x value`.
|
|
10
|
+
// - Boolean flags, string flags, number flags, repeatable flags.
|
|
11
|
+
// - Enum validation, required-flag validation.
|
|
12
|
+
// - Unknown flags warn (hard-error when DOWNDRAFT_STRICT=1).
|
|
13
|
+
// - Built-in `--help` / `-h` (caller prints usage + exits 0).
|
|
14
|
+
//
|
|
15
|
+
// Each subcommand declares a `CommandSchema` and calls `parseArgs()`. The
|
|
16
|
+
// parser is pure (no I/O) so it is trivially unit-testable.
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
/** A single flag declaration. */
|
|
20
|
+
export interface FlagSpec {
|
|
21
|
+
/** Long name without `--` (e.g. `"game"`). */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Optional single-char alias without `-` (e.g. `"g"`). */
|
|
24
|
+
alias?: string;
|
|
25
|
+
type: "string" | "boolean" | "number";
|
|
26
|
+
/** Default value when the flag is absent. Repeatable flags default to `[]`. */
|
|
27
|
+
default?: string | boolean | number | string[];
|
|
28
|
+
/** Allowed values for `string` / `number` flags. */
|
|
29
|
+
enum?: string[];
|
|
30
|
+
/** Hard-error if absent. */
|
|
31
|
+
required?: boolean;
|
|
32
|
+
/** Collect repeated occurrences into `string[]`. Implies `type: "string"`. */
|
|
33
|
+
repeatable?: boolean;
|
|
34
|
+
/** One-line description for `--help` output. */
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A single positional declaration. */
|
|
39
|
+
export interface PositionalSpec {
|
|
40
|
+
name: string;
|
|
41
|
+
required?: boolean;
|
|
42
|
+
/** Collect all remaining positionals into `string[]`. Must be last. */
|
|
43
|
+
variadic?: boolean;
|
|
44
|
+
description?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Schema describing a command's accepted flags + positionals. */
|
|
48
|
+
export interface CommandSchema {
|
|
49
|
+
flags?: FlagSpec[];
|
|
50
|
+
positionals?: PositionalSpec[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Result of a successful parse. */
|
|
54
|
+
export interface ParsedArgs {
|
|
55
|
+
positionals: string[];
|
|
56
|
+
flags: Record<string, string | boolean | number | string[]>;
|
|
57
|
+
/** True if `--help` / `-h` was present. Caller should print usage + exit 0. */
|
|
58
|
+
help: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Thrown on a hard parse error (missing required, bad enum, bad number). */
|
|
62
|
+
export class ArgError extends Error {
|
|
63
|
+
constructor(message: string) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "ArgError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Write help/version text to stdout (bypasses the logger, which may
|
|
70
|
+
* redirect to stderr in MCP mode). */
|
|
71
|
+
export function print(text: string): void {
|
|
72
|
+
process.stdout.write(text + "\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether STRICT diagnostics are active (unknown flags → hard error). */
|
|
76
|
+
function isStrict(): boolean {
|
|
77
|
+
const v = process.env.DOWNDRAFT_STRICT;
|
|
78
|
+
if (v !== undefined) return v === "1";
|
|
79
|
+
return false; // parser default; dev-mode strictness is opt-in here
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Format a flag spec for usage output, e.g. `--game <name>, -g <name>`. */
|
|
83
|
+
export function formatFlag(spec: FlagSpec): string {
|
|
84
|
+
const placeholder = spec.type === "boolean" ? "" : ` <${spec.name}>`;
|
|
85
|
+
const long = `--${spec.name}${placeholder}`;
|
|
86
|
+
if (spec.alias) {
|
|
87
|
+
const short = `-${spec.alias}${placeholder}`;
|
|
88
|
+
return `${long}, ${short}`;
|
|
89
|
+
}
|
|
90
|
+
return long;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse `argv` (the slice AFTER the subcommand name) against `schema`.
|
|
95
|
+
*
|
|
96
|
+
* Throws `ArgError` on validation failures. Returns `{ help: true }` when
|
|
97
|
+
* `--help` / `-h` is present (no validation runs in that case).
|
|
98
|
+
*/
|
|
99
|
+
export function parseArgs(argv: string[], schema: CommandSchema): ParsedArgs {
|
|
100
|
+
const flags = schema.flags ?? [];
|
|
101
|
+
const positionals = schema.positionals ?? [];
|
|
102
|
+
|
|
103
|
+
// Build lookup maps.
|
|
104
|
+
const byLong = new Map<string, FlagSpec>();
|
|
105
|
+
const byShort = new Map<string, FlagSpec>();
|
|
106
|
+
flags.forEach((f) => {
|
|
107
|
+
byLong.set(f.name, f);
|
|
108
|
+
if (f.alias) byShort.set(f.alias, f);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Initialize defaults.
|
|
112
|
+
const out: Record<string, string | boolean | number | string[]> = {};
|
|
113
|
+
flags.forEach((f) => {
|
|
114
|
+
if (f.repeatable) {
|
|
115
|
+
out[f.name] = f.default ? [...(f.default as string[])] : [];
|
|
116
|
+
} else if (f.default !== undefined) {
|
|
117
|
+
out[f.name] = f.default;
|
|
118
|
+
} else if (f.type === "boolean") {
|
|
119
|
+
out[f.name] = false;
|
|
120
|
+
} else {
|
|
121
|
+
out[f.name] = "";
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const posValues: string[] = [];
|
|
126
|
+
let help = false;
|
|
127
|
+
const strict = isStrict();
|
|
128
|
+
|
|
129
|
+
let i = 0;
|
|
130
|
+
while (i < argv.length) {
|
|
131
|
+
const arg = argv[i++];
|
|
132
|
+
|
|
133
|
+
// `--` terminates flag parsing; remaining args are positionals.
|
|
134
|
+
if (arg === "--") {
|
|
135
|
+
while (i < argv.length) posValues.push(argv[i++]);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (arg === "--help" || arg === "-h") {
|
|
140
|
+
help = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Long flag: --name or --name=value
|
|
145
|
+
if (arg.startsWith("--")) {
|
|
146
|
+
const body = arg.slice(2);
|
|
147
|
+
const eq = body.indexOf("=");
|
|
148
|
+
const name = eq === -1 ? body : body.slice(0, eq);
|
|
149
|
+
const inline = eq === -1 ? undefined : body.slice(eq + 1);
|
|
150
|
+
|
|
151
|
+
const spec = byLong.get(name);
|
|
152
|
+
if (!spec) {
|
|
153
|
+
const msg = `Unknown flag: --${name}`;
|
|
154
|
+
if (strict) throw new ArgError(msg);
|
|
155
|
+
process.stderr.write(`warn: ${msg}\n`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (spec.type === "boolean") {
|
|
160
|
+
if (inline !== undefined) {
|
|
161
|
+
throw new ArgError(`--${name} does not take a value (got "${inline}")`);
|
|
162
|
+
}
|
|
163
|
+
out[spec.name] = true;
|
|
164
|
+
} else {
|
|
165
|
+
const value = inline !== undefined ? inline : consumeValue(argv, i, spec.name);
|
|
166
|
+
if (inline === undefined) i++; // consumed the next arg
|
|
167
|
+
assignFlag(out, spec, value);
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Short flag: -x or -xvalue or -x value
|
|
173
|
+
if (arg.startsWith("-") && arg.length > 1 && arg !== "-") {
|
|
174
|
+
const body = arg.slice(1);
|
|
175
|
+
// Support `-x=value` and `-xvalue` (attached) and `-x value` (separate).
|
|
176
|
+
let alias: string;
|
|
177
|
+
let inline: string | undefined;
|
|
178
|
+
if (body.length > 1) {
|
|
179
|
+
alias = body[0];
|
|
180
|
+
const rest = body.slice(1);
|
|
181
|
+
inline = rest.startsWith("=") ? rest.slice(1) : rest;
|
|
182
|
+
} else {
|
|
183
|
+
alias = body;
|
|
184
|
+
inline = undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const spec = byShort.get(alias);
|
|
188
|
+
if (!spec) {
|
|
189
|
+
const msg = `Unknown flag: -${alias}`;
|
|
190
|
+
if (strict) throw new ArgError(msg);
|
|
191
|
+
process.stderr.write(`warn: ${msg}\n`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (spec.type === "boolean") {
|
|
196
|
+
if (inline !== undefined) {
|
|
197
|
+
throw new ArgError(`-${alias} does not take a value (got "${inline}")`);
|
|
198
|
+
}
|
|
199
|
+
out[spec.name] = true;
|
|
200
|
+
} else {
|
|
201
|
+
const value = inline !== undefined ? inline : consumeValue(argv, i, spec.name);
|
|
202
|
+
if (inline === undefined) i++;
|
|
203
|
+
assignFlag(out, spec, value);
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Positional.
|
|
209
|
+
posValues.push(arg);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (help) return { positionals: posValues, flags: out, help: true };
|
|
213
|
+
|
|
214
|
+
// Validate required flags.
|
|
215
|
+
for (let _i = 0, _it = flags, _n = _it.length; _i < _n; _i++) { const f = _it[_i];
|
|
216
|
+
if (!f.required) continue;
|
|
217
|
+
const v = out[f.name];
|
|
218
|
+
const missing =
|
|
219
|
+
v === "" ||
|
|
220
|
+
v === false ||
|
|
221
|
+
(Array.isArray(v) && v.length === 0);
|
|
222
|
+
if (missing) {
|
|
223
|
+
throw new ArgError(`Missing required flag: --${f.name}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Validate positionals.
|
|
228
|
+
let pi = 0;
|
|
229
|
+
for (let _i = 0, _it = positionals, _n = _it.length; _i < _n; _i++) { const p = _it[_i];
|
|
230
|
+
if (p.variadic) {
|
|
231
|
+
// Variadic absorbs the rest; required means at least one.
|
|
232
|
+
if (p.required && posValues.slice(pi).length === 0) {
|
|
233
|
+
throw new ArgError(`Missing required positional: <${p.name}>`);
|
|
234
|
+
}
|
|
235
|
+
pi = posValues.length;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
if (pi >= posValues.length) {
|
|
239
|
+
if (p.required) {
|
|
240
|
+
throw new ArgError(`Missing required positional: <${p.name}>`);
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
pi++;
|
|
245
|
+
}
|
|
246
|
+
// Extra positionals not declared → warn (or error in strict).
|
|
247
|
+
const declared = positionals.reduce(
|
|
248
|
+
(n, p) => n + (p.variadic ? Infinity : 1),
|
|
249
|
+
0,
|
|
250
|
+
);
|
|
251
|
+
if (posValues.length > declared && declared !== Infinity) {
|
|
252
|
+
const msg = `Unexpected positional argument: ${posValues[declared]}`;
|
|
253
|
+
if (strict) throw new ArgError(msg);
|
|
254
|
+
process.stderr.write(`warn: ${msg}\n`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { positionals: posValues, flags: out, help: false };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Read the next argv token as a flag value, throwing if absent. */
|
|
261
|
+
function consumeValue(argv: string[], i: number, name: string): string {
|
|
262
|
+
if (i >= argv.length) {
|
|
263
|
+
throw new ArgError(`Flag --${name} requires a value`);
|
|
264
|
+
}
|
|
265
|
+
return argv[i];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Assign a parsed string value to `out` with type/enum/repeatable handling. */
|
|
269
|
+
function assignFlag(
|
|
270
|
+
out: Record<string, string | boolean | number | string[]>,
|
|
271
|
+
spec: FlagSpec,
|
|
272
|
+
raw: string,
|
|
273
|
+
): void {
|
|
274
|
+
if (spec.repeatable) {
|
|
275
|
+
(out[spec.name] as string[]).push(raw);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (spec.type === "number") {
|
|
279
|
+
const n = Number(raw);
|
|
280
|
+
if (raw === "" || Number.isNaN(n)) {
|
|
281
|
+
throw new ArgError(`--${spec.name} expects a number (got "${raw}")`);
|
|
282
|
+
}
|
|
283
|
+
out[spec.name] = n;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (spec.enum && !spec.enum.includes(raw)) {
|
|
287
|
+
throw new ArgError(
|
|
288
|
+
`--${spec.name} must be one of: ${spec.enum.join(", ")} (got "${raw}")`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
out[spec.name] = raw;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Render a `CommandSchema` + `usage` header into a help string.
|
|
296
|
+
*
|
|
297
|
+
* `usage` is the one-line synopsis (e.g. `"draft new [path] [options]"`).
|
|
298
|
+
*/
|
|
299
|
+
export function renderHelp(usage: string, schema: CommandSchema): string {
|
|
300
|
+
const lines: string[] = [];
|
|
301
|
+
lines.push(`Usage: ${usage}`);
|
|
302
|
+
lines.push("");
|
|
303
|
+
|
|
304
|
+
const pos = schema.positionals ?? [];
|
|
305
|
+
if (pos.length > 0) {
|
|
306
|
+
lines.push("Arguments:");
|
|
307
|
+
pos.forEach((p) => {
|
|
308
|
+
const label = p.variadic ? `<${p.name}...>` : `<${p.name}>`;
|
|
309
|
+
lines.push(` ${label.padEnd(22)} ${p.description ?? ""}`.trimEnd());
|
|
310
|
+
});
|
|
311
|
+
lines.push("");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const flags = schema.flags ?? [];
|
|
315
|
+
if (flags.length > 0) {
|
|
316
|
+
lines.push("Options:");
|
|
317
|
+
lines.push(` ${"-h, --help".padEnd(22)} Show this help and exit`);
|
|
318
|
+
flags.forEach((f) => {
|
|
319
|
+
const left = formatFlag(f);
|
|
320
|
+
const desc = f.description ?? "";
|
|
321
|
+
const req = f.required ? " (required)" : "";
|
|
322
|
+
const def =
|
|
323
|
+
f.default !== undefined && f.default !== "" && f.default !== false && !Array.isArray(f.default)
|
|
324
|
+
? ` (default: ${JSON.stringify(f.default)})`
|
|
325
|
+
: "";
|
|
326
|
+
const en = f.enum ? ` (one of: ${f.enum.join("|")})` : "";
|
|
327
|
+
lines.push(` ${left.padEnd(22)} ${desc}${req}${def}${en}`.trimEnd());
|
|
328
|
+
});
|
|
329
|
+
lines.push("");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return lines.join("\n");
|
|
333
|
+
}
|