create-eigen-game 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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/dist/chunk-BSJVRXI4.js +127 -0
  4. package/dist/chunk-BSJVRXI4.js.map +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +58 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/index.d.ts +21 -0
  9. package/dist/index.js +9 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +48 -0
  12. package/templates/app-overlay/app-config.json.template +6 -0
  13. package/templates/app-overlay/lib/firebase_options.dart.template +14 -0
  14. package/templates/app-overlay/lib/game/README.md +18 -0
  15. package/templates/app-overlay/lib/game/module.dart.template +29 -0
  16. package/templates/app-overlay/lib/game/v1/rules.dart.template +79 -0
  17. package/templates/app-overlay/lib/main.dart.template +39 -0
  18. package/templates/app-overlay/test/game/twin_fixtures_test.dart.template +28 -0
  19. package/templates/app-overlay/web/firebase-config.js.template +4 -0
  20. package/templates/app-overlay/web/firebase-messaging-sw.js.template +15 -0
  21. package/templates/app-overlay/web/flutter_bootstrap.js.template +19 -0
  22. package/templates/app-overlay/web/index.html.template +17 -0
  23. package/templates/project/README.md +104 -0
  24. package/templates/project/package.json +12 -0
  25. package/templates/scaffold/project.gitignore +9 -0
  26. package/templates/scaffold/worker.gitignore +10 -0
  27. package/templates/worker/.dev.vars.example +4 -0
  28. package/templates/worker/README.md +50 -0
  29. package/templates/worker/game-contract.json +109 -0
  30. package/templates/worker/package.json +51 -0
  31. package/templates/worker/public/.gitkeep +1 -0
  32. package/templates/worker/src/index.ts +17 -0
  33. package/templates/worker/src/module/fixtures/v1/counter.json +31 -0
  34. package/templates/worker/src/module/index.ts +4 -0
  35. package/templates/worker/src/module/v1.ts +57 -0
  36. package/templates/worker/test/twin.spec.ts +4 -0
  37. package/templates/worker/tsconfig.json +15 -0
  38. package/templates/worker/worker-configuration.d.ts +14718 -0
  39. package/templates/worker/wrangler.jsonc +40 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eigen Interactive
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # create-eigen-game
2
+
3
+ Creates one repository containing an Eigen Cloudflare Worker and Flutter app:
4
+
5
+ ```sh
6
+ pnpm create eigen-game my-game
7
+ # or
8
+ npm create eigen-game@latest my-game
9
+ ```
10
+
11
+ The command detects npm or pnpm from the invoking package manager. Pass
12
+ `--package-manager npm|pnpm` to override it and `--org games.example` to set
13
+ the Flutter organization. The destination basename is the one required
14
+ lowercase kebab-case slug. The CLI derives the display name (`My Game`), Dart
15
+ package name (`my_game`), and type prefix (`MyGame`) from `my-game`; there is no
16
+ separate name or prefix input.
17
+
18
+ The generated project has two application-owned halves:
19
+
20
+ ```text
21
+ my-game/
22
+ ├── server/ # authoritative TypeScript rules and Cloudflare Worker
23
+ └── app/ # generated Dart payloads and handwritten Flutter presentation
24
+ ```
25
+
26
+ The scaffolder creates the Flutter shell explicitly for Android and web,
27
+ installs both halves, emits `server/game-contract.json`, and
28
+ generates the initial Dart payload types and typed rules base. Engine
29
+ repositories remain ordinary npm/pub.dev dependencies and are not cloned.
30
+ It also supplies a starter v1 twin fixture, a Vitest runner, a Flutter fixture
31
+ runner, watch-mode rules tests, and generated-contract drift checks. At the
32
+ generated repository root, `pnpm run contract` (or `npm run contract`) emits
33
+ the TypeScript contract and regenerates the Dart payloads in one command;
34
+ `contract:check` checks both artifacts without rewriting them.
35
+
36
+ `pnpm run deploy` (or npm) is similarly whole-project: it builds Flutter web
37
+ into the Worker's Static Assets directory, applies D1 migrations, and deploys
38
+ one same-origin Worker. The Flutter app lives at `/`; the generated native
39
+ install page lives at `/download`. Both Android and web builds read the same
40
+ public values from `app/app-config.json` through Flutter's native
41
+ `--dart-define-from-file` support.
42
+
43
+ ## Template architecture
44
+
45
+ `templates/worker` is a valid standalone Cloudflare C3-style Worker template:
46
+ it contains its own `package.json`, `wrangler.jsonc`, TypeScript entry point,
47
+ generated Wrangler types, and game module. The combined Eigen CLI renders that
48
+ same template under `server/`; it does not maintain a second Worker skeleton.
49
+
50
+ `templates/app-overlay` is applied after
51
+ `flutter create --empty --platforms android,web`. It supplies the Firebase
52
+ Messaging service worker, the narrowly scoped Flutter bootstrap that registers
53
+ it before app startup, and one setup command that runs FlutterFire and derives
54
+ the worker's public Firebase configuration from the Web app FlutterFire
55
+ selected. Implementors never copy those identifiers into JavaScript by hand.
56
+ `eigen_flutter` bundles and loads the Cropper.js assets required by
57
+ `image_cropper` on web, so generated apps do not carry their own copy or
58
+ configure `web/index.html`.
59
+ On Android, `eigen_flutter` supplies FID messaging configuration through its
60
+ plugin manifest and native dependency graph; the scaffolder does not edit the
61
+ application manifest or `gradle.properties`. It adds the application-level core
62
+ library desugaring block required by `flutter_local_notifications`, because an
63
+ Android library cannot supply that compiler setting transitively. The CLI then
64
+ uses `flutter pub add` and the public
65
+ `eigen_flutter:generate_payloads` executable instead of editing Flutter's
66
+ generated YAML by hand.
67
+
68
+ The public CLI intentionally has no server-only or app-only modes.
69
+
70
+ The Worker directly depends on both `@eigeninteractive/rules` and
71
+ `@eigeninteractive/server`. Rules is the pure contract the game implements and
72
+ the shared peer identity used by server/kernel/testkit; server is the
73
+ Cloudflare runtime. Server deliberately does not re-export the rules surface.
74
+
75
+ ## Documentation
76
+
77
+ - [Quickstart](https://eigeninteractive.com/docs/getting-started/quickstart)
78
+ - [Manual setup and split repositories](https://eigeninteractive.com/docs/getting-started/manual-setup)
79
+ - [Deploy the web app](https://eigeninteractive.com/docs/ship-it/deploy-the-web-app)
80
+ - [The game contract](https://eigeninteractive.com/docs/build-a-game/the-contract)
@@ -0,0 +1,127 @@
1
+ // src/index.ts
2
+ import { execFileSync } from "child_process";
3
+ import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
4
+ import { basename, dirname, resolve } from "path";
5
+ import { fileURLToPath } from "url";
6
+ var templatesRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../templates");
7
+ var gameSlug = (value) => {
8
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
9
+ throw new Error("destination directory name must be a lowercase kebab-case slug, for example my-game");
10
+ }
11
+ return value;
12
+ };
13
+ var dartName = (value) => value.replaceAll("-", "_");
14
+ var title = (value) => value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
15
+ var identifier = (value) => {
16
+ const pascal = value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join("");
17
+ return /^[0-9]/.test(pascal) ? `Game${pascal}` : pascal;
18
+ };
19
+ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
20
+ if (userAgent?.startsWith("pnpm/")) return "pnpm";
21
+ if (userAgent?.startsWith("npm/")) return "npm";
22
+ return void 0;
23
+ }
24
+ function render(contents, name, manager) {
25
+ const replacements = [
26
+ ["@game/example-game-server", `@game/${name}-server`],
27
+ ["{{PACKAGE_MANAGER}}", manager],
28
+ ["ExampleGame", identifier(name)],
29
+ ["Example Game", title(name)],
30
+ ["example_game", dartName(name)],
31
+ ["example-game", name]
32
+ ];
33
+ return replacements.reduce((result, [from, to]) => result.replaceAll(from, to), contents);
34
+ }
35
+ function renderTree(source, destination, name, manager) {
36
+ cpSync(source, destination, { recursive: true });
37
+ const visit = (directory) => {
38
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
39
+ const path = resolve(directory, entry.name);
40
+ if (entry.isDirectory()) visit(path);
41
+ else if (entry.isFile()) {
42
+ writeFileSync(path, render(readFileSync(path, "utf8"), name, manager));
43
+ if (entry.name.endsWith(".template")) {
44
+ renameSync(path, path.slice(0, -".template".length));
45
+ }
46
+ }
47
+ }
48
+ };
49
+ visit(destination);
50
+ const packagedGitignore = resolve(templatesRoot, "scaffold", `${basename(source)}.gitignore`);
51
+ if (existsSync(packagedGitignore)) {
52
+ writeFileSync(resolve(destination, ".gitignore"), render(readFileSync(packagedGitignore, "utf8"), name, manager));
53
+ }
54
+ }
55
+ function packageCommand(manager, operation) {
56
+ if (operation === "install") return [manager, ["install"]];
57
+ return [manager, ["run", "contract"]];
58
+ }
59
+ var androidDesugaring = `// flutter_local_notifications requires desugaring in the application module.
60
+ // A library plugin cannot enable this compiler setting transitively.
61
+ android {
62
+ compileOptions {
63
+ isCoreLibraryDesugaringEnabled = true
64
+ }
65
+ }
66
+
67
+ dependencies {
68
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
69
+ }`;
70
+ function enableAndroidCoreLibraryDesugaring(appRoot) {
71
+ const gradlePath = resolve(appRoot, "android/app/build.gradle.kts");
72
+ const gradle = readFileSync(gradlePath, "utf8");
73
+ if (!gradle.includes('id("com.android.application")')) {
74
+ throw new Error(`Flutter created an unexpected Android application build file: ${gradlePath}`);
75
+ }
76
+ const settingExists = gradle.includes("isCoreLibraryDesugaringEnabled");
77
+ const dependencyExists = gradle.includes("coreLibraryDesugaring(");
78
+ if (settingExists && dependencyExists) return;
79
+ if (settingExists || dependencyExists) {
80
+ throw new Error(`Flutter created an incomplete Android core library desugaring configuration: ${gradlePath}`);
81
+ }
82
+ appendFileSync(gradlePath, `${gradle.endsWith("\n") ? "\n" : "\n\n"}${androidDesugaring}
83
+ `);
84
+ }
85
+ function scaffoldGame(options) {
86
+ const root = resolve(options.directory);
87
+ const name = gameSlug(basename(root));
88
+ const manager = options.packageManager ?? detectPackageManager() ?? "pnpm";
89
+ const org = options.org?.trim() || "com.example";
90
+ const bootstrap = options.bootstrap ?? true;
91
+ const run = options.run ?? ((command, args, cwd) => execFileSync(command, args, { cwd, stdio: "inherit" }));
92
+ if (existsSync(root)) throw new Error(`target already exists: ${root}`);
93
+ mkdirSync(dirname(root), { recursive: true });
94
+ const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));
95
+ try {
96
+ const serverRoot = resolve(stagingRoot, "server");
97
+ const appRoot = resolve(stagingRoot, "app");
98
+ renderTree(resolve(templatesRoot, "worker"), serverRoot, name, manager);
99
+ renderTree(resolve(templatesRoot, "project"), stagingRoot, name, manager);
100
+ if (bootstrap) {
101
+ run("flutter", ["create", "--empty", "--platforms", "android,web", "--project-name", dartName(name), "--org", org, appRoot], stagingRoot);
102
+ enableAndroidCoreLibraryDesugaring(appRoot);
103
+ } else {
104
+ mkdirSync(appRoot, { recursive: true });
105
+ }
106
+ renderTree(resolve(templatesRoot, "app-overlay"), appRoot, name, manager);
107
+ if (bootstrap) {
108
+ run("flutter", ["pub", "add", "eigen_flutter@^0.1.0", "firebase_core@^4.9.0", "firebase_messaging@^16.2.2"], appRoot);
109
+ const [install, installArgs] = packageCommand(manager, "install");
110
+ run(install, installArgs, serverRoot);
111
+ const [contract, contractArgs] = packageCommand(manager, "contract");
112
+ run(contract, contractArgs, serverRoot);
113
+ run("dart", ["run", "eigen_flutter:generate_payloads", "--contract", "../server/game-contract.json", "--output", "lib/game/generated/payloads.dart", "--fixtures-output", "test/fixtures"], appRoot);
114
+ }
115
+ renameSync(stagingRoot, root);
116
+ return { root, name };
117
+ } catch (error) {
118
+ rmSync(stagingRoot, { recursive: true, force: true });
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ export {
124
+ detectPackageManager,
125
+ scaffoldGame
126
+ };
127
+ //# sourceMappingURL=chunk-BSJVRXI4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport type PackageManager = \"npm\" | \"pnpm\";\n\nexport interface ScaffoldOptions {\n directory: string;\n org?: string;\n packageManager?: PackageManager;\n /**\n * Run Flutter, package installation, and contract/payload generation.\n * Kept as a programmatic test seam; the public CLI always bootstraps both\n * halves of the combined project.\n */\n bootstrap?: boolean;\n run?: (command: string, args: string[], cwd: string) => void;\n}\n\nexport interface ScaffoldResult {\n root: string;\n name: string;\n}\n\nconst templatesRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"../templates\");\n\nconst gameSlug = (value: string): string => {\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {\n throw new Error(\"destination directory name must be a lowercase kebab-case slug, for example my-game\");\n }\n return value;\n};\n\nconst dartName = (value: string): string => value.replaceAll(\"-\", \"_\");\n\nconst title = (value: string): string =>\n value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\" \");\n\nconst identifier = (value: string): string => {\n const pascal = value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\"\");\n return /^[0-9]/.test(pascal) ? `Game${pascal}` : pascal;\n};\n\nexport function detectPackageManager(userAgent = process.env.npm_config_user_agent): PackageManager | undefined {\n if (userAgent?.startsWith(\"pnpm/\")) return \"pnpm\";\n if (userAgent?.startsWith(\"npm/\")) return \"npm\";\n return undefined;\n}\n\nfunction render(contents: string, name: string, manager: PackageManager): string {\n const replacements: ReadonlyArray<readonly [string, string]> = [\n [\"@game/example-game-server\", `@game/${name}-server`],\n [\"{{PACKAGE_MANAGER}}\", manager],\n [\"ExampleGame\", identifier(name)],\n [\"Example Game\", title(name)],\n [\"example_game\", dartName(name)],\n [\"example-game\", name],\n ];\n return replacements.reduce((result, [from, to]) => result.replaceAll(from, to), contents);\n}\n\nfunction renderTree(source: string, destination: string, name: string, manager: PackageManager): void {\n cpSync(source, destination, { recursive: true });\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const path = resolve(directory, entry.name);\n if (entry.isDirectory()) visit(path);\n else if (entry.isFile()) {\n writeFileSync(path, render(readFileSync(path, \"utf8\"), name, manager));\n // Template-only source must not be claimed by language servers before\n // it has an enclosing generated package. Restore the real extension\n // only in the rendered project.\n if (entry.name.endsWith(\".template\")) {\n renameSync(path, path.slice(0, -\".template\".length));\n }\n }\n }\n };\n visit(destination);\n\n // npm deliberately excludes files named `.gitignore` from package\n // tarballs. Their packaged twins live outside the C3 template so a direct\n // Git-based C3 render does not inherit a scaffolder-only helper file.\n const packagedGitignore = resolve(templatesRoot, \"scaffold\", `${basename(source)}.gitignore`);\n if (existsSync(packagedGitignore)) {\n writeFileSync(resolve(destination, \".gitignore\"), render(readFileSync(packagedGitignore, \"utf8\"), name, manager));\n }\n}\n\nfunction packageCommand(manager: PackageManager, operation: \"install\" | \"contract\"): [string, string[]] {\n if (operation === \"install\") return [manager, [\"install\"]];\n return [manager, [\"run\", \"contract\"]];\n}\n\nconst androidDesugaring = `// flutter_local_notifications requires desugaring in the application module.\n// A library plugin cannot enable this compiler setting transitively.\nandroid {\n compileOptions {\n isCoreLibraryDesugaringEnabled = true\n }\n}\n\ndependencies {\n coreLibraryDesugaring(\"com.android.tools:desugar_jdk_libs:2.1.4\")\n}`;\n\nfunction enableAndroidCoreLibraryDesugaring(appRoot: string): void {\n const gradlePath = resolve(appRoot, \"android/app/build.gradle.kts\");\n const gradle = readFileSync(gradlePath, \"utf8\");\n if (!gradle.includes('id(\"com.android.application\")')) {\n throw new Error(`Flutter created an unexpected Android application build file: ${gradlePath}`);\n }\n const settingExists = gradle.includes(\"isCoreLibraryDesugaringEnabled\");\n const dependencyExists = gradle.includes(\"coreLibraryDesugaring(\");\n if (settingExists && dependencyExists) return;\n if (settingExists || dependencyExists) {\n throw new Error(`Flutter created an incomplete Android core library desugaring configuration: ${gradlePath}`);\n }\n appendFileSync(gradlePath, `${gradle.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\"}${androidDesugaring}\\n`);\n}\n\nexport function scaffoldGame(options: ScaffoldOptions): ScaffoldResult {\n const root = resolve(options.directory);\n const name = gameSlug(basename(root));\n const manager = options.packageManager ?? detectPackageManager() ?? \"pnpm\";\n const org = options.org?.trim() || \"com.example\";\n const bootstrap = options.bootstrap ?? true;\n const run = options.run ?? ((command, args, cwd) => execFileSync(command, args, { cwd, stdio: \"inherit\" }));\n\n if (existsSync(root)) throw new Error(`target already exists: ${root}`);\n mkdirSync(dirname(root), { recursive: true });\n\n // Build beside the destination and publish it with one rename. Failed\n // subprocesses never leave a half-scaffolded project at the requested path.\n const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));\n try {\n const serverRoot = resolve(stagingRoot, \"server\");\n const appRoot = resolve(stagingRoot, \"app\");\n\n renderTree(resolve(templatesRoot, \"worker\"), serverRoot, name, manager);\n renderTree(resolve(templatesRoot, \"project\"), stagingRoot, name, manager);\n\n if (bootstrap) {\n run(\"flutter\", [\"create\", \"--empty\", \"--platforms\", \"android,web\", \"--project-name\", dartName(name), \"--org\", org, appRoot], stagingRoot);\n enableAndroidCoreLibraryDesugaring(appRoot);\n } else {\n mkdirSync(appRoot, { recursive: true });\n }\n renderTree(resolve(templatesRoot, \"app-overlay\"), appRoot, name, manager);\n\n if (bootstrap) {\n run(\"flutter\", [\"pub\", \"add\", \"eigen_flutter@^0.1.0\", \"firebase_core@^4.9.0\", \"firebase_messaging@^16.2.2\"], appRoot);\n const [install, installArgs] = packageCommand(manager, \"install\");\n run(install, installArgs, serverRoot);\n const [contract, contractArgs] = packageCommand(manager, \"contract\");\n run(contract, contractArgs, serverRoot);\n run(\"dart\", [\"run\", \"eigen_flutter:generate_payloads\", \"--contract\", \"../server/game-contract.json\", \"--output\", \"lib/game/generated/payloads.dart\", \"--fixtures-output\", \"test/fixtures\"], appRoot);\n }\n\n renameSync(stagingRoot, root);\n return { root, name };\n } catch (error) {\n rmSync(stagingRoot, { recursive: true, force: true });\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ detectPackageManager,
4
+ scaffoldGame
5
+ } from "./chunk-BSJVRXI4.js";
6
+
7
+ // src/cli.ts
8
+ import { parseArgs } from "util";
9
+ var help = `Usage: create-eigen-game <game-slug> [options]
10
+
11
+ Creates one repository containing an Eigen Cloudflare Worker and Flutter app.
12
+ The destination basename is the lowercase kebab-case game slug; the CLI derives
13
+ the display name, Dart package name, and Dart/TypeScript type prefix from it.
14
+
15
+ Options:
16
+ --org <reverse-domain> Android/iOS organization (default: com.example)
17
+ --package-manager <pm> npm or pnpm (defaults to the invoking package manager)
18
+ -h, --help Show this help`;
19
+ function main(args) {
20
+ const { values, positionals } = parseArgs({
21
+ args,
22
+ allowPositionals: true,
23
+ strict: true,
24
+ options: {
25
+ help: { type: "boolean", short: "h" },
26
+ org: { type: "string" },
27
+ "package-manager": { type: "string" }
28
+ }
29
+ });
30
+ if (values.help) {
31
+ console.log(help);
32
+ return;
33
+ }
34
+ if (positionals.length !== 1) {
35
+ throw new Error(`expected exactly one destination directory
36
+
37
+ ${help}`);
38
+ }
39
+ const requestedManager = values["package-manager"];
40
+ if (requestedManager !== void 0 && requestedManager !== "npm" && requestedManager !== "pnpm") {
41
+ throw new Error("--package-manager must be npm or pnpm");
42
+ }
43
+ const packageManager = requestedManager ?? detectPackageManager() ?? "pnpm";
44
+ const result = scaffoldGame({
45
+ directory: positionals[0],
46
+ org: values.org,
47
+ packageManager
48
+ });
49
+ console.log(`Created ${result.name} in ${result.root}`);
50
+ }
51
+ try {
52
+ main(process.argv.slice(2));
53
+ } catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ console.error(`create-eigen-game: ${message}`);
56
+ process.exitCode = 1;
57
+ }
58
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { detectPackageManager, type PackageManager, scaffoldGame } from \"./index.js\";\n\nconst help = `Usage: create-eigen-game <game-slug> [options]\n\nCreates one repository containing an Eigen Cloudflare Worker and Flutter app.\nThe destination basename is the lowercase kebab-case game slug; the CLI derives\nthe display name, Dart package name, and Dart/TypeScript type prefix from it.\n\nOptions:\n --org <reverse-domain> Android/iOS organization (default: com.example)\n --package-manager <pm> npm or pnpm (defaults to the invoking package manager)\n -h, --help Show this help`;\n\nfunction main(args: string[]): void {\n const { values, positionals } = parseArgs({\n args,\n allowPositionals: true,\n strict: true,\n options: {\n help: { type: \"boolean\", short: \"h\" },\n org: { type: \"string\" },\n \"package-manager\": { type: \"string\" },\n },\n });\n\n if (values.help) {\n console.log(help);\n return;\n }\n if (positionals.length !== 1) {\n throw new Error(`expected exactly one destination directory\\n\\n${help}`);\n }\n\n const requestedManager = values[\"package-manager\"];\n if (requestedManager !== undefined && requestedManager !== \"npm\" && requestedManager !== \"pnpm\") {\n throw new Error(\"--package-manager must be npm or pnpm\");\n }\n const packageManager = (requestedManager as PackageManager | undefined) ?? detectPackageManager() ?? \"pnpm\";\n const result = scaffoldGame({\n directory: positionals[0],\n org: values.org,\n packageManager,\n });\n console.log(`Created ${result.name} in ${result.root}`);\n}\n\ntry {\n main(process.argv.slice(2));\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`create-eigen-game: ${message}`);\n process.exitCode = 1;\n}\n"],"mappings":";;;;;;;AACA,SAAS,iBAAiB;AAG1B,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWb,SAAS,KAAK,MAAsB;AAClC,QAAM,EAAE,QAAQ,YAAY,IAAI,UAAU;AAAA,IACxC;AAAA,IACA,kBAAkB;AAAA,IAClB,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,MAAM,EAAE,MAAM,WAAW,OAAO,IAAI;AAAA,MACpC,KAAK,EAAE,MAAM,SAAS;AAAA,MACtB,mBAAmB,EAAE,MAAM,SAAS;AAAA,IACtC;AAAA,EACF,CAAC;AAED,MAAI,OAAO,MAAM;AACf,YAAQ,IAAI,IAAI;AAChB;AAAA,EACF;AACA,MAAI,YAAY,WAAW,GAAG;AAC5B,UAAM,IAAI,MAAM;AAAA;AAAA,EAAiD,IAAI,EAAE;AAAA,EACzE;AAEA,QAAM,mBAAmB,OAAO,iBAAiB;AACjD,MAAI,qBAAqB,UAAa,qBAAqB,SAAS,qBAAqB,QAAQ;AAC/F,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AACA,QAAM,iBAAkB,oBAAmD,qBAAqB,KAAK;AACrG,QAAM,SAAS,aAAa;AAAA,IAC1B,WAAW,YAAY,CAAC;AAAA,IACxB,KAAK,OAAO;AAAA,IACZ;AAAA,EACF,CAAC;AACD,UAAQ,IAAI,WAAW,OAAO,IAAI,OAAO,OAAO,IAAI,EAAE;AACxD;AAEA,IAAI;AACF,OAAK,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC5B,SAAS,OAAO;AACd,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAQ,MAAM,sBAAsB,OAAO,EAAE;AAC7C,UAAQ,WAAW;AACrB;","names":[]}
@@ -0,0 +1,21 @@
1
+ type PackageManager = "npm" | "pnpm";
2
+ interface ScaffoldOptions {
3
+ directory: string;
4
+ org?: string;
5
+ packageManager?: PackageManager;
6
+ /**
7
+ * Run Flutter, package installation, and contract/payload generation.
8
+ * Kept as a programmatic test seam; the public CLI always bootstraps both
9
+ * halves of the combined project.
10
+ */
11
+ bootstrap?: boolean;
12
+ run?: (command: string, args: string[], cwd: string) => void;
13
+ }
14
+ interface ScaffoldResult {
15
+ root: string;
16
+ name: string;
17
+ }
18
+ declare function detectPackageManager(userAgent?: string | undefined): PackageManager | undefined;
19
+ declare function scaffoldGame(options: ScaffoldOptions): ScaffoldResult;
20
+
21
+ export { type PackageManager, type ScaffoldOptions, type ScaffoldResult, detectPackageManager, scaffoldGame };
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import {
2
+ detectPackageManager,
3
+ scaffoldGame
4
+ } from "./chunk-BSJVRXI4.js";
5
+ export {
6
+ detectPackageManager,
7
+ scaffoldGame
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "create-eigen-game",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "Scaffold an Eigen game Worker and Flutter app without cloning the engine repositories.",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-eigen-game": "./dist/cli.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "templates"
19
+ ],
20
+ "dependencies": {},
21
+ "devDependencies": {
22
+ "@types/node": "^26.1.1",
23
+ "tsup": "^8.5.1",
24
+ "typescript": "^5.9.3",
25
+ "vitest": "^4.1.10",
26
+ "wrangler": "^4.110.0",
27
+ "zod": "^4.4.3",
28
+ "@eigeninteractive/rules": "0.1.0",
29
+ "@eigeninteractive/testkit": "0.1.0",
30
+ "@eigeninteractive/server": "0.1.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/eigeninteractive/eigen-server.git",
41
+ "directory": "packages/create-eigen-game"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup src/index.ts src/cli.ts --format esm --dts --sourcemap --clean",
45
+ "test": "vitest run",
46
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p templates/worker/tsconfig.json"
47
+ }
48
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "API_BASE_URL": "",
3
+ "APP_HOST": "",
4
+ "GOOGLE_WEB_CLIENT_ID": "",
5
+ "FIREBASE_VAPID_KEY": ""
6
+ }
@@ -0,0 +1,14 @@
1
+ import 'package:firebase_core/firebase_core.dart';
2
+
3
+ /// Firebase configuration generated by the FlutterFire CLI.
4
+ ///
5
+ /// The scaffold keeps this throwing implementation so the project compiles
6
+ /// before Firebase is connected. `pnpm firebase:configure` or
7
+ /// `npm run firebase:configure` replaces this file with FlutterFire's generated
8
+ /// platform configuration.
9
+ abstract final class DefaultFirebaseOptions {
10
+ static FirebaseOptions get currentPlatform => throw StateError(
11
+ 'Firebase is not configured. Run `pnpm firebase:configure` or '
12
+ '`npm run firebase:configure` from the project root.',
13
+ );
14
+ }
@@ -0,0 +1,18 @@
1
+ # Example Game client module
2
+
3
+ The immutable payload classes and typed rules base under `generated/` come from
4
+ the authoritative TypeScript schemas. Regenerate them after changing a schema
5
+ or shared fixture:
6
+
7
+ ```sh
8
+ dart run eigen_flutter:generate_payloads \
9
+ --contract ../server/game-contract.json \
10
+ --output lib/game/generated/payloads.dart \
11
+ --fixtures-output test/fixtures
12
+ flutter test
13
+ ```
14
+
15
+ Keep `module.dart` and `v1/rules.dart` handwritten: they contain client-side
16
+ legality and preview behavior plus game presentation. The server remains
17
+ authoritative. The generated `test/game/twin_fixtures_test.dart` runs the
18
+ contract's same fixtures against this Dart rules registry.
@@ -0,0 +1,29 @@
1
+ import 'package:eigen_flutter/eigen_flutter.dart';
2
+ import 'package:flutter/material.dart';
3
+
4
+ import 'v1/rules.dart';
5
+
6
+ class ExampleGameModule extends GameModule {
7
+ const ExampleGameModule();
8
+
9
+ @override
10
+ Map<int, GameRules> get versions => const {1: ExampleGameRulesV1()};
11
+
12
+ @override
13
+ GameCreationSpec get creationSpec => const GameCreationSpec(
14
+ minPlayers: 2,
15
+ maxPlayers: 2,
16
+ timingConfigs: {'Untimed': UntimedConfig()},
17
+ defaultConfig: {'target': 10},
18
+ );
19
+
20
+ @override
21
+ Widget? buildCreationConfig({
22
+ required ValueChanged<Map<String, dynamic>> onChanged,
23
+ }) => null;
24
+
25
+ @override
26
+ Widget buildRules(BuildContext context) => const Text(
27
+ 'Take turns increasing the counter. The first player to reach the target wins.',
28
+ );
29
+ }
@@ -0,0 +1,79 @@
1
+ import 'package:eigen_flutter/eigen_flutter.dart';
2
+ import 'package:flutter/material.dart';
3
+
4
+ import '../generated/payloads.dart';
5
+
6
+ class ExampleGameRulesV1 extends ExampleGameV1RulesBase {
7
+ const ExampleGameRulesV1();
8
+
9
+ @override
10
+ bool isValidAction({
11
+ required ExampleGameV1Observation obs,
12
+ required List<int> pending,
13
+ required ExampleGameV1Action data,
14
+ required int playerIndex,
15
+ required ExampleGameV1Config config,
16
+ }) => pending.contains(playerIndex) && data.amount > 0;
17
+
18
+ @override
19
+ ExampleGameV1Observation? previewAction({
20
+ required ExampleGameV1Observation obs,
21
+ required List<int> pending,
22
+ required ExampleGameV1Action data,
23
+ required int playerIndex,
24
+ required ExampleGameV1Config config,
25
+ }) => ExampleGameV1Observation(count: obs.count + data.amount);
26
+
27
+ @override
28
+ Widget buildContent(GameContentContext context) =>
29
+ _CounterBoard(context: context, rules: this);
30
+
31
+ @override
32
+ String? ratingPool(RatingPoolArgs args) => null;
33
+
34
+ @override
35
+ bool botSeatable(BotSeatableArgs args) => true;
36
+ }
37
+
38
+ class _CounterBoard extends StatelessWidget {
39
+ const _CounterBoard({required this.context, required this.rules});
40
+
41
+ final GameContentContext context;
42
+ final ExampleGameRulesV1 rules;
43
+
44
+ @override
45
+ Widget build(BuildContext buildContext) {
46
+ final observation = context.frame.observation! as ExampleGameV1Observation;
47
+ final config = context.config as ExampleGameV1Config;
48
+ final seat = context.mySeat.indexOrNull;
49
+ final action = ExampleGameV1Action(amount: 1);
50
+ final canAct =
51
+ seat != null &&
52
+ !context.isReplay &&
53
+ !context.actionPending &&
54
+ rules.isValidAction(
55
+ obs: observation,
56
+ pending: context.frame.pendingPlayers,
57
+ data: action,
58
+ playerIndex: seat,
59
+ config: config,
60
+ );
61
+
62
+ return Center(
63
+ child: Column(
64
+ mainAxisSize: MainAxisSize.min,
65
+ children: [
66
+ Text(
67
+ '${observation.count} / ${config.target}',
68
+ style: Theme.of(buildContext).textTheme.displayMedium,
69
+ ),
70
+ const SizedBox(height: 24),
71
+ FilledButton(
72
+ onPressed: canAct ? () => context.onAction(action.toJson()) : null,
73
+ child: const Text('Add one'),
74
+ ),
75
+ ],
76
+ ),
77
+ );
78
+ }
79
+ }
@@ -0,0 +1,39 @@
1
+ import 'package:eigen_flutter/eigen_flutter.dart';
2
+ import 'package:firebase_core/firebase_core.dart';
3
+ import 'package:firebase_messaging/firebase_messaging.dart';
4
+ import 'package:flutter/material.dart';
5
+
6
+ import 'firebase_options.dart';
7
+ import 'game/module.dart';
8
+
9
+ const _apiBaseUrl = String.fromEnvironment('API_BASE_URL');
10
+ const _googleWebClientId = String.fromEnvironment('GOOGLE_WEB_CLIENT_ID');
11
+ const _firebaseVapidKey = String.fromEnvironment('FIREBASE_VAPID_KEY');
12
+ const _appHost = String.fromEnvironment('APP_HOST');
13
+
14
+ Future<void> main() async {
15
+ await runEngineApp(
16
+ module: const ExampleGameModule(),
17
+ config: AppConfig(
18
+ branding: const Branding(
19
+ appName: 'Example Game',
20
+ seedColor: Colors.indigo,
21
+ ),
22
+ engine: EngineConfig(
23
+ apiBaseUrl: _apiBaseUrl,
24
+ googleWebClientId: _googleWebClientId,
25
+ appHost: _appHost.isEmpty ? null : _appHost,
26
+ firebaseVapidKey: _firebaseVapidKey,
27
+ ),
28
+ ),
29
+ firebaseOptions: DefaultFirebaseOptions.currentPlatform,
30
+ onBackgroundMessage: _onBackgroundMessage,
31
+ );
32
+ }
33
+
34
+ @pragma('vm:entry-point')
35
+ Future<void> _onBackgroundMessage(RemoteMessage message) async {
36
+ await Firebase.initializeApp(
37
+ options: DefaultFirebaseOptions.currentPlatform,
38
+ );
39
+ }
@@ -0,0 +1,28 @@
1
+ import 'package:eigen_flutter/testing/twin_fixtures.dart';
2
+ import 'package:example_game/game/module.dart';
3
+ import 'package:flutter_test/flutter_test.dart';
4
+
5
+ void main() {
6
+ const module = ExampleGameModule();
7
+ final suites = loadTwinFixtureSuites('test/fixtures');
8
+
9
+ test('the generated twin fixture suite is present', () {
10
+ expect(suites, isNotEmpty);
11
+ });
12
+
13
+ for (final suite in suites) {
14
+ final rules = module.versions[suite.schemaVersion];
15
+ group('twin fixtures v${suite.schemaVersion} (${suite.path})', () {
16
+ for (final fixtureCase in suite.cases) {
17
+ test(fixtureCase.name, () {
18
+ expect(
19
+ rules,
20
+ isNotNull,
21
+ reason: 'no Dart rules unit for v${suite.schemaVersion}',
22
+ );
23
+ expect(runTwinFixtureCase(rules!, fixtureCase), isEmpty);
24
+ });
25
+ }
26
+ });
27
+ }
28
+ }
@@ -0,0 +1,4 @@
1
+ throw new Error(
2
+ "Firebase Web is not configured. Run `pnpm firebase:configure` or " +
3
+ "`npm run firebase:configure` from the project root.",
4
+ );
@@ -0,0 +1,15 @@
1
+ // Keep this version aligned with the Firebase JS SDK supported by the
2
+ // firebase_core_web version resolved by the Flutter app.
3
+ importScripts("https://www.gstatic.com/firebasejs/12.15.0/firebase-app-compat.js");
4
+ importScripts("https://www.gstatic.com/firebasejs/12.15.0/firebase-messaging-compat.js");
5
+ importScripts("firebase-config.js");
6
+
7
+ // `firebase:configure` generates this public configuration from the Web app
8
+ // selected by FlutterFire. Firebase Security Rules and the Eigen Worker's
9
+ // verified ID tokens enforce authorization; these identifiers are not server
10
+ // credentials.
11
+ firebase.initializeApp(self.firebaseConfig);
12
+
13
+ // Initializing Messaging is enough for notification payloads: the browser
14
+ // displays them while the app is backgrounded or closed.
15
+ firebase.messaging();