@vampgg/cli 1.0.0-beta.1

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.
@@ -0,0 +1,153 @@
1
+ //#region src/config/types.d.ts
2
+ interface FrameworkConfig {
3
+ /** Path to the bebop.json config file (default: "./bebop.json") */
4
+ bebopConfig?: string;
5
+ schemas: {
6
+ entity: string;
7
+ actions: string;
8
+ state: string;
9
+ tags: string;
10
+ /**
11
+ * Path for the generated bebop mutation schema (EntityDelta, MutationScope).
12
+ * Defaults to a `mutation.bop` sibling of the entity schema.
13
+ */
14
+ mutation?: string;
15
+ };
16
+ outFile: string;
17
+ }
18
+ interface BebopConfig {
19
+ include?: string[];
20
+ exclude?: string[];
21
+ generators?: {
22
+ ts?: {
23
+ outFile?: string;
24
+ };
25
+ };
26
+ watchOptions?: {
27
+ excludeDirectories?: string[];
28
+ };
29
+ }
30
+ //#endregion
31
+ //#region src/config/loader.d.ts
32
+ declare function loadBebopConfig(cwd: string, configPath?: string): BebopConfig;
33
+ declare function loadVampConfig(cwd: string): FrameworkConfig;
34
+ //#endregion
35
+ //#region src/generators/parse-bop.d.ts
36
+ interface SchemaField {
37
+ name: string;
38
+ typeId: number;
39
+ isArray: boolean;
40
+ isMap: boolean;
41
+ /** For scalars, the scalar type name. For definitions, the definition name. */
42
+ typeName: string;
43
+ /** For arrays, the member type name */
44
+ memberTypeName?: string;
45
+ /** For maps, the key type name */
46
+ keyTypeName?: string;
47
+ /** For maps, the value type name */
48
+ valueTypeName?: string;
49
+ /** Bebop field tag (for messages) */
50
+ constantValue?: number | null;
51
+ }
52
+ interface SchemaDefinition {
53
+ name: string;
54
+ kind: "struct" | "message" | "union" | "enum";
55
+ fields: SchemaField[];
56
+ /** For unions: branch info */
57
+ branches?: {
58
+ discriminator: number;
59
+ typeName: string;
60
+ }[];
61
+ }
62
+ interface ParsedSchema {
63
+ definitions: Map<string, SchemaDefinition>;
64
+ }
65
+ declare function parseSchema(bebopSchemaBytes: Uint8Array): ParsedSchema;
66
+ declare function loadSchemaFromFile(bebopTsPath: string): Uint8Array;
67
+ declare function loadAndParseSchema(bebopTsPath: string): ParsedSchema;
68
+ //#endregion
69
+ //#region src/generators/codegen.d.ts
70
+ declare function generate(cwd: string, config: BebopConfig, vampConfig: FrameworkConfig): string;
71
+ //#endregion
72
+ //#region src/generators/parse-bop-source.d.ts
73
+ /** Recursive classification of a type token (base/array/map). */
74
+ interface TypeClassification {
75
+ /** Resolved element/base type name (e.g. "guid", "Pool"). For maps this is "map". */
76
+ typeName: string;
77
+ isArray: boolean;
78
+ isMap: boolean;
79
+ /** For arrays, the member type name */
80
+ memberType?: string;
81
+ /** For maps, the key type name */
82
+ keyType?: string;
83
+ /** For maps, the value type name (raw substring) */
84
+ valueType?: string;
85
+ /** For maps, the recursive classification of the value type */
86
+ valueClassified?: TypeClassification;
87
+ /** For arrays, the recursive classification of the member type */
88
+ memberClassified?: TypeClassification;
89
+ /** True when the resolved base/member type is a bebop scalar */
90
+ isScalar: boolean;
91
+ }
92
+ interface SourceField extends TypeClassification {
93
+ /** Bebop field tag/index */
94
+ index: number;
95
+ name: string;
96
+ /** Raw type token as written (e.g. "guid", "Pool", "Tags", "guid[]", "map[guid, Foo]") */
97
+ rawType: string;
98
+ }
99
+ interface SourceMessage {
100
+ name: string;
101
+ fields: SourceField[];
102
+ }
103
+ /**
104
+ * Extract the body of `message <name> { ... }` from bebop source text.
105
+ * Returns null if the message is not found.
106
+ */
107
+ declare function extractMessageBody(source: string, messageName: string): string | null;
108
+ /**
109
+ * Parse `message <name>` field declarations from bebop source text.
110
+ *
111
+ * This is intentionally a lightweight, syntax-focused parser (no import
112
+ * resolution). It only understands the constrained field syntax used in
113
+ * generated/authored entity schemas: `<index> -> <type> <name>;`.
114
+ *
115
+ * Nested/inline message blocks inside the message body are stripped before
116
+ * field extraction; their fields are NOT emitted as fields of this message. A
117
+ * warning is logged so the user knows an inline component was ignored.
118
+ */
119
+ declare function parseMessage(source: string, messageName: string): SourceMessage | null;
120
+ /** Convenience wrapper for the `Entity` message. */
121
+ declare function parseEntityMessage(source: string): SourceMessage;
122
+ //#endregion
123
+ //#region src/generators/emit-mutation-bop.d.ts
124
+ /**
125
+ * Emit the bebop schema providing serializable EntityDelta + MutationScope
126
+ * types, derived from the Entity message.
127
+ *
128
+ * @param entity Parsed Entity message.
129
+ * @param entityImportPath Import path to the entity schema, relative to the
130
+ * emitted file (e.g. "./entity.bop").
131
+ * @param userDeltas Set of `<Type>Delta` message names already reachable from
132
+ * the user's schema (so they are reused verbatim, not re-synthesized).
133
+ * @param components Map of component name -> parsed component message, used to
134
+ * synthesize a `<Type>Delta` when one is not user-supplied.
135
+ * @param entitySchemaPath Absolute path to the entity schema (for error text).
136
+ */
137
+ declare function emitMutationSchema(entity: SourceMessage, entityImportPath: string, userDeltas?: ReadonlySet<string>, components?: ReadonlyMap<string, SourceMessage>, entitySchemaPath?: string): string;
138
+ //#endregion
139
+ //#region src/generators/generate-mutation-schema.d.ts
140
+ /** Default mutation schema path: a `mutation.bop` sibling of the entity schema. */
141
+ declare function resolveMutationPath(cwd: string, vampConfig: FrameworkConfig): string;
142
+ /**
143
+ * Parse the user's entity schema and (re)generate the bebop mutation schema
144
+ * (EntityDelta, MutationType, MutationRecord, MutationScope) next to it.
145
+ *
146
+ * IMPORTANT: this must run BEFORE `bebopc build` so the generated schema is
147
+ * picked up and compiled into serializable types.
148
+ *
149
+ * @returns the absolute path of the written mutation schema.
150
+ */
151
+ declare function generateMutationSchema(cwd: string, vampConfig: FrameworkConfig): string;
152
+ //#endregion
153
+ export { type BebopConfig, type FrameworkConfig, type ParsedSchema, type SchemaDefinition, type SchemaField, type SourceField, type SourceMessage, emitMutationSchema, extractMessageBody, generate, generateMutationSchema, loadAndParseSchema, loadBebopConfig, loadSchemaFromFile, loadVampConfig, parseEntityMessage, parseMessage, parseSchema, resolveMutationPath };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as extractMessageBody, c as generate, d as parseSchema, f as loadBebopConfig, i as emitMutationSchema, l as loadAndParseSchema, n as resolveMutationPath, o as parseEntityMessage, p as loadVampConfig, s as parseMessage, t as generateMutationSchema, u as loadSchemaFromFile } from "./generate-mutation-schema-DPVvPX4h.mjs";
2
+ export { emitMutationSchema, extractMessageBody, generate, generateMutationSchema, loadAndParseSchema, loadBebopConfig, loadSchemaFromFile, loadVampConfig, parseEntityMessage, parseMessage, parseSchema, resolveMutationPath };
@@ -0,0 +1 @@
1
+ export { };
package/dist/vamp.mjs ADDED
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ import { c as generate, f as loadBebopConfig, p as loadVampConfig, r as resolveBebopImport, t as generateMutationSchema } from "./generate-mutation-schema-DPVvPX4h.mjs";
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { relative, resolve } from "node:path";
5
+ import { defineCommand, runMain } from "citty";
6
+ import { execSync } from "node:child_process";
7
+ //#region src/commands/generate.ts
8
+ /**
9
+ * Build a watch-mode scheduler that runs `run` on each change, never dropping a
10
+ * change saved while a run is in flight: a mid-run change sets a pending flag
11
+ * that triggers exactly one follow-up after the current run completes.
12
+ *
13
+ * Extracted (and exported) so the pending-rerun semantics can be unit-tested
14
+ * without invoking the real generator (plan 21 §4d / Case E).
15
+ */
16
+ function createWatchScheduler(run, debounce, onComplete) {
17
+ let isRunning = false;
18
+ let pending = false;
19
+ const runOnce = () => {
20
+ isRunning = true;
21
+ const ok = run();
22
+ isRunning = false;
23
+ onComplete?.(ok);
24
+ if (pending) {
25
+ pending = false;
26
+ debounce(runOnce);
27
+ }
28
+ };
29
+ const onChange = () => {
30
+ if (isRunning) {
31
+ pending = true;
32
+ return;
33
+ }
34
+ debounce(runOnce);
35
+ };
36
+ return { onChange };
37
+ }
38
+ const generateCommand = defineCommand({
39
+ meta: {
40
+ name: "generate",
41
+ description: "Run bebopc build, then emit game.generated.ts with component map, delta types, helpers, and factory"
42
+ },
43
+ args: {
44
+ cwd: {
45
+ type: "string",
46
+ description: "Working directory",
47
+ default: process.cwd()
48
+ },
49
+ "skip-bebopc": {
50
+ type: "boolean",
51
+ description: "Skip running bebopc build",
52
+ default: false
53
+ },
54
+ watch: {
55
+ type: "boolean",
56
+ description: "Watch schema files for changes and regenerate on change",
57
+ default: false
58
+ }
59
+ },
60
+ async run({ args }) {
61
+ const cwd = args.cwd;
62
+ const vampConfig = loadVampConfig(cwd);
63
+ const bebopConfig = loadBebopConfig(cwd, vampConfig.bebopConfig);
64
+ const runGenerate = () => {
65
+ console.log("Generating mutation schema...");
66
+ const mutationPath = generateMutationSchema(cwd, vampConfig);
67
+ console.log(`Generated ${mutationPath}`);
68
+ const bebopTsPath = resolve(cwd, bebopConfig.generators?.ts?.outFile ?? "./src/bebop.ts");
69
+ if (!args["skip-bebopc"]) {
70
+ try {
71
+ execSync("npx --no-install bebopc --version", {
72
+ cwd,
73
+ stdio: "pipe"
74
+ });
75
+ } catch {
76
+ console.error("bebopc not found. Install it (e.g. `pnpm add -D bebop-tools`) before running `vamp generate`.");
77
+ return false;
78
+ }
79
+ console.log("Running bebopc build...");
80
+ try {
81
+ execSync("npx --no-install bebopc build", {
82
+ cwd,
83
+ stdio: "inherit"
84
+ });
85
+ } catch (err) {
86
+ console.error("bebopc build failed:");
87
+ const e = err;
88
+ if (e.stderr?.length) console.error(e.stderr.toString());
89
+ return false;
90
+ }
91
+ if (!existsSync(bebopTsPath)) {
92
+ console.error(`bebopc build did not produce ${bebopTsPath}`);
93
+ return false;
94
+ }
95
+ }
96
+ console.log("Generating ECS types...");
97
+ const outPath = generate(cwd, bebopConfig, vampConfig);
98
+ console.log(`Generated ${outPath}`);
99
+ return true;
100
+ };
101
+ if (!args.watch) {
102
+ if (!runGenerate()) process.exit(1);
103
+ return;
104
+ }
105
+ const chokidar = await import("chokidar");
106
+ const schemaPaths = [
107
+ resolve(cwd, vampConfig.schemas.entity),
108
+ resolve(cwd, vampConfig.schemas.actions),
109
+ resolve(cwd, vampConfig.schemas.state),
110
+ resolve(cwd, vampConfig.schemas.tags)
111
+ ];
112
+ let debounceTimer = null;
113
+ const debounce = (fn) => {
114
+ if (debounceTimer) clearTimeout(debounceTimer);
115
+ debounceTimer = setTimeout(fn, 100);
116
+ };
117
+ const scheduler = createWatchScheduler(runGenerate, debounce, (ok) => {
118
+ if (ok) console.log("Watching for changes...");
119
+ });
120
+ const watcher = chokidar.watch(schemaPaths, { persistent: true });
121
+ watcher.on("change", (path) => {
122
+ console.log(`\nSchema changed: ${path}`);
123
+ scheduler.onChange();
124
+ });
125
+ console.log("Watching for changes...");
126
+ if (!runGenerate()) {
127
+ await watcher.close();
128
+ process.exit(1);
129
+ }
130
+ }
131
+ });
132
+ //#endregion
133
+ //#region src/templates/entity.bop.ts
134
+ /**
135
+ * Entity schema template. `__POOL_IMPORT__` is replaced at scaffold time with a
136
+ * path to `@vampgg/utils/schema/pool.bop` resolved via Node module resolution, so
137
+ * it is correct for the actual (hoisted or pnpm) `node_modules` layout.
138
+ */
139
+ const entityTemplate = `import "__POOL_IMPORT__"
140
+ import "./tags.bop"
141
+
142
+ message Entity {
143
+ \t1 -> string id;
144
+ \t2 -> string sk;
145
+ \t3 -> Tags[] tags;
146
+ \t4 -> string parent;
147
+ \t5 -> string[] children;
148
+ \t6 -> Pool health;
149
+ }
150
+ `;
151
+ /** Fallback import path used when `@vampgg/utils` cannot be resolved at init time. */
152
+ const POOL_IMPORT_PLACEHOLDER = "__POOL_IMPORT__";
153
+ const POOL_IMPORT_FALLBACK = "../node_modules/@vampgg/utils/schema/pool.bop";
154
+ //#endregion
155
+ //#region src/templates/actions.bop.ts
156
+ const actionsTemplate = `union Actions {
157
+ \t1 -> message Attack {
158
+ \t\t1 -> guid source;
159
+ \t\t2 -> guid target;
160
+ \t\t3 -> uint32 damage;
161
+ \t}
162
+ }
163
+ `;
164
+ //#endregion
165
+ //#region src/templates/state.bop.ts
166
+ const stateTemplate = `message State {
167
+ \t1 -> string ns;
168
+ }
169
+ `;
170
+ //#endregion
171
+ //#region src/templates/tags.bop.ts
172
+ const tagsTemplate = `enum Tags {
173
+ Default = 1;
174
+ }
175
+ `;
176
+ //#endregion
177
+ //#region src/commands/init.ts
178
+ /**
179
+ * Resolve a bebop import path (relative to `schemaDir`) for
180
+ * `@vampgg/utils/schema/pool.bop` using Node module resolution, so the scaffolded
181
+ * import is correct under hoisted or pnpm `node_modules` layouts. Falls back to
182
+ * a literal path (with a warning) when resolution fails.
183
+ *
184
+ * `resolve` is injectable so the fallback branch can be tested deterministically
185
+ * (Node resolution from a temp dir is environment-dependent — it may still find
186
+ * a `@vampgg/utils` in an ancestor `node_modules`).
187
+ */
188
+ function resolvePoolImport(cwd, schemaDir, resolveImport = resolveBebopImport) {
189
+ const resolved = resolveImport("@vampgg/utils/schema/pool.bop", cwd);
190
+ if (!resolved) {
191
+ console.warn("Warning: could not resolve '@vampgg/utils/schema/pool.bop'. Scaffolding a literal import path that may not resolve under your node_modules layout — fix the import in schema/entity.bop if `bebopc build` fails.");
192
+ return POOL_IMPORT_FALLBACK;
193
+ }
194
+ const rel = relative(schemaDir, resolved).split("\\").join("/");
195
+ return rel.startsWith(".") ? rel : `./${rel}`;
196
+ }
197
+ //#endregion
198
+ //#region src/vamp.ts
199
+ runMain(defineCommand({
200
+ meta: {
201
+ name: "vamp",
202
+ description: "ECS code generator for @vampgg"
203
+ },
204
+ subCommands: {
205
+ generate: generateCommand,
206
+ init: defineCommand({
207
+ meta: {
208
+ name: "init",
209
+ description: "Scaffold schema/ dir, template .bop files, and create bebop.json + vamp.json"
210
+ },
211
+ args: { cwd: {
212
+ type: "string",
213
+ description: "Working directory",
214
+ default: process.cwd()
215
+ } },
216
+ run({ args }) {
217
+ const cwd = args.cwd;
218
+ const schemaDir = resolve(cwd, "schema");
219
+ mkdirSync(schemaDir, { recursive: true });
220
+ console.log("Created schema/");
221
+ const poolImport = resolvePoolImport(cwd, schemaDir);
222
+ const files = [
223
+ {
224
+ path: "schema/entity.bop",
225
+ content: entityTemplate.replace(POOL_IMPORT_PLACEHOLDER, poolImport)
226
+ },
227
+ {
228
+ path: "schema/actions.bop",
229
+ content: actionsTemplate
230
+ },
231
+ {
232
+ path: "schema/state.bop",
233
+ content: stateTemplate
234
+ },
235
+ {
236
+ path: "schema/tags.bop",
237
+ content: tagsTemplate
238
+ }
239
+ ];
240
+ for (const file of files) {
241
+ const fullPath = resolve(cwd, file.path);
242
+ if (existsSync(fullPath)) console.log(`Skipping ${file.path} (already exists)`);
243
+ else {
244
+ writeFileSync(fullPath, file.content, "utf-8");
245
+ console.log(`Created ${file.path}`);
246
+ }
247
+ }
248
+ const bebopConfigPath = resolve(cwd, "bebop.json");
249
+ if (existsSync(bebopConfigPath)) console.log("Skipping bebop.json (already exists)");
250
+ else {
251
+ writeFileSync(bebopConfigPath, JSON.stringify({
252
+ include: ["schema/**/*.bop"],
253
+ generators: { ts: { outFile: "./src/bebop.ts" } }
254
+ }, null, 2) + "\n", "utf-8");
255
+ console.log("Created bebop.json");
256
+ }
257
+ const vampConfigPath = resolve(cwd, "vamp.json");
258
+ if (existsSync(vampConfigPath)) console.log("Skipping vamp.json (already exists)");
259
+ else {
260
+ writeFileSync(vampConfigPath, JSON.stringify({
261
+ schemas: {
262
+ entity: "schema/entity.bop",
263
+ actions: "schema/actions.bop",
264
+ state: "schema/state.bop",
265
+ tags: "schema/tags.bop"
266
+ },
267
+ outFile: "./src/game.generated.ts"
268
+ }, null, 2) + "\n", "utf-8");
269
+ console.log("Created vamp.json");
270
+ }
271
+ }
272
+ })
273
+ }
274
+ })).catch((err) => {
275
+ console.error(err);
276
+ process.exit(1);
277
+ });
278
+ //#endregion
279
+ export {};
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@vampgg/cli",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "ECS code generator CLI for @vampgg",
5
+ "homepage": "https://github.com/sammccord/vamp/tree/main/tools/cli#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/sammccord/vamp/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Sam McCord <sam.mccord@protonmail.com>",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/sammccord/vamp.git",
14
+ "directory": "tools/cli"
15
+ },
16
+ "bin": {
17
+ "vamp": "./dist/vamp.mjs"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "type": "module",
23
+ "exports": {
24
+ ".": "./dist/index.mjs",
25
+ "./vamp": "./dist/vamp.mjs",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "bebop": "3.2.3",
33
+ "chokidar": "^3.6.0",
34
+ "citty": "^0.1.6",
35
+ "jsonc-parser": "^3.3.1"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^24",
39
+ "bebop-tools": "^3.2.3",
40
+ "typescript": "^5",
41
+ "vite-plus": "^0.1.24",
42
+ "vitest": "npm:@voidzero-dev/vite-plus-test@latest",
43
+ "@vampgg/ecs": "1.0.0-beta.1",
44
+ "@vampgg/utils": "1.0.0-beta.1",
45
+ "@vampgg/worker": "1.0.0-beta.1"
46
+ },
47
+ "scripts": {
48
+ "build": "vp pack",
49
+ "dev": "vp pack --watch",
50
+ "test": "vp test",
51
+ "typecheck": "tsc --noEmit",
52
+ "codegen:typecheck-fixtures": "vp test --run roundtrip"
53
+ }
54
+ }