create-eigen-game 0.3.0 → 0.4.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 CHANGED
@@ -1,4 +1,4 @@
1
- # create-eigen-game
1
+ # <img src="https://eigeninteractive.com/brand/favicon-32.png" width="16" align="top"> create-eigen-game
2
2
 
3
3
  Creates one repository containing an Eigen Cloudflare Worker and Flutter app:
4
4
 
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import { execFileSync } from "child_process";
3
3
  import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
4
- import { basename, dirname, resolve } from "path";
4
+ import { basename, dirname, relative, resolve } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
7
7
  // src/engine-range.ts
@@ -27,7 +27,7 @@ var engineVersion = engineRange(JSON.parse(readFileSync(resolve(packageRoot, "pa
27
27
  const sibling = resolve(packageRoot, "../server/package.json");
28
28
  return existsSync(sibling) ? JSON.parse(readFileSync(sibling, "utf8")).version : void 0;
29
29
  });
30
- var flutterClientVersion = "^0.2.0";
30
+ var flutterClientVersion = "^0.3.0";
31
31
  var gameSlug = (value) => {
32
32
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
33
33
  throw new Error("destination directory name must be a lowercase kebab-case slug, for example my-game");
@@ -45,10 +45,11 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
45
45
  if (userAgent?.startsWith("npm/")) return "npm";
46
46
  return void 0;
47
47
  }
48
- function render(contents, name, manager) {
48
+ function render(contents, name, manager, packageId) {
49
49
  const replacements = [
50
50
  ["@game/example-game-server", `@game/${name}-server`],
51
51
  ["{{PACKAGE_MANAGER}}", manager],
52
+ ["{{PACKAGE_ID}}", packageId],
52
53
  ["{{ENGINE_VERSION}}", engineVersion],
53
54
  ["ExampleGame", identifier(name)],
54
55
  ["Example Game", title(name)],
@@ -57,24 +58,38 @@ function render(contents, name, manager) {
57
58
  ];
58
59
  return replacements.reduce((result, [from, to]) => result.replaceAll(from, to), contents);
59
60
  }
60
- function renderTree(source, destination, name, manager) {
61
+ var utf8 = new TextDecoder("utf-8", { fatal: true });
62
+ function decodeUtf8(bytes) {
63
+ try {
64
+ return utf8.decode(bytes);
65
+ } catch {
66
+ return void 0;
67
+ }
68
+ }
69
+ function renderTree(source, destination, name, manager, packageId) {
61
70
  cpSync(source, destination, { recursive: true });
62
71
  const visit = (directory) => {
63
72
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
64
- const path = resolve(directory, entry.name);
65
- if (entry.isDirectory()) visit(path);
66
- else if (entry.isFile()) {
67
- writeFileSync(path, render(readFileSync(path, "utf8"), name, manager));
68
- if (entry.name.endsWith(".template")) {
69
- renameSync(path, path.slice(0, -".template".length));
70
- }
73
+ const templatePath = resolve(directory, entry.name);
74
+ if (entry.isDirectory()) {
75
+ visit(templatePath);
76
+ continue;
77
+ }
78
+ if (!entry.isFile()) continue;
79
+ const rendered = resolve(destination, relative(source, templatePath));
80
+ const text = decodeUtf8(readFileSync(templatePath));
81
+ if (text !== void 0) {
82
+ writeFileSync(rendered, render(text, name, manager, packageId));
83
+ }
84
+ if (entry.name.endsWith(".template")) {
85
+ renameSync(rendered, rendered.slice(0, -".template".length));
71
86
  }
72
87
  }
73
88
  };
74
- visit(destination);
89
+ visit(source);
75
90
  const packagedGitignore = resolve(templatesRoot, "scaffold", `${basename(source)}.gitignore`);
76
91
  if (existsSync(packagedGitignore)) {
77
- writeFileSync(resolve(destination, ".gitignore"), render(readFileSync(packagedGitignore, "utf8"), name, manager));
92
+ writeFileSync(resolve(destination, ".gitignore"), render(readFileSync(packagedGitignore, "utf8"), name, manager, packageId));
78
93
  }
79
94
  }
80
95
  function packageCommand(manager, operation) {
@@ -107,11 +122,104 @@ function enableAndroidCoreLibraryDesugaring(appRoot) {
107
122
  appendFileSync(gradlePath, `${gradle.endsWith("\n") ? "\n" : "\n\n"}${androidDesugaring}
108
123
  `);
109
124
  }
125
+ var androidReleaseSigning = `val releaseKeyProperties: Map<String, String> =
126
+ rootProject.file("key.properties")
127
+ .takeIf { it.exists() }
128
+ ?.readLines()
129
+ ?.map { it.trim() }
130
+ ?.filter { it.isNotEmpty() && !it.startsWith("#") && it.contains("=") }
131
+ ?.associate { it.substringBefore("=").trim() to it.substringAfter("=").trim() }
132
+ .orEmpty()
133
+
134
+ android {
135
+ signingConfigs {
136
+ create("release") {
137
+ storeFile = releaseKeyProperties["storeFile"]?.let { file(it) }
138
+ storePassword = releaseKeyProperties["storePassword"]
139
+ keyAlias = releaseKeyProperties["keyAlias"]
140
+ keyPassword = releaseKeyProperties["keyPassword"]
141
+ }
142
+ }
143
+ buildTypes {
144
+ release {
145
+ signingConfig = if (releaseKeyProperties["storeFile"] != null)
146
+ signingConfigs.getByName("release")
147
+ else
148
+ signingConfigs.getByName("debug")
149
+ isMinifyEnabled = true
150
+ isShrinkResources = true
151
+ proguardFiles(
152
+ getDefaultProguardFile("proguard-android-optimize.txt"),
153
+ "proguard-rules.pro",
154
+ )
155
+ }
156
+ }
157
+ }`;
158
+ function enableAndroidReleaseSigning(appRoot) {
159
+ const gradlePath = resolve(appRoot, "android/app/build.gradle.kts");
160
+ const gradle = readFileSync(gradlePath, "utf8");
161
+ if (gradle.includes("releaseKeyProperties")) return;
162
+ appendFileSync(gradlePath, `${gradle.endsWith("\n") ? "" : "\n"}${androidReleaseSigning}
163
+ `);
164
+ }
165
+ var launcherIconsAndSplashConfig = `
166
+ flutter_launcher_icons:
167
+ android: true
168
+ web:
169
+ generate: true
170
+ image_path: "assets/icon/icon.png"
171
+ image_path: "assets/icon/icon.png"
172
+ adaptive_icon_background: "#1B1E24"
173
+ adaptive_icon_foreground: "assets/icon/icon_foreground.png"
174
+ min_sdk_android: 21
175
+
176
+ flutter_native_splash:
177
+ color: "#F4F1EA"
178
+ color_dark: "#1B1E24"
179
+ image: assets/icon/splash.png
180
+ image_dark: assets/icon/splash_dark.png
181
+ fullscreen: true
182
+ android_12:
183
+ color: "#F4F1EA"
184
+ color_dark: "#1B1E24"
185
+ image: assets/icon/splash.png
186
+ image_dark: assets/icon/splash_dark.png
187
+ icon_background_color: "#F4F1EA"
188
+ icon_background_color_dark: "#1B1E24"
189
+ web: true
190
+ `;
191
+ function configureLauncherIconsAndSplash(appRoot) {
192
+ const pubspecPath = resolve(appRoot, "pubspec.yaml");
193
+ const pubspec = readFileSync(pubspecPath, "utf8");
194
+ if (pubspec.includes("flutter_launcher_icons:")) return;
195
+ appendFileSync(pubspecPath, `${pubspec.endsWith("\n") ? "" : "\n"}${launcherIconsAndSplashConfig}`);
196
+ }
197
+ function addContinuousIntegration(options) {
198
+ const root = resolve(options.directory);
199
+ const manifestPath = resolve(root, "package.json");
200
+ if (!existsSync(manifestPath) || !existsSync(resolve(root, "server")) || !existsSync(resolve(root, "app"))) {
201
+ throw new Error(`not an Eigen game project (expected package.json, server/ and app/): ${root}`);
202
+ }
203
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
204
+ const name = gameSlug(manifest.name ?? basename(root));
205
+ const recorded = manifest.scripts?.contract?.includes("pnpm ") ? "pnpm" : manifest.scripts?.contract?.includes("npm ") ? "npm" : void 0;
206
+ const manager = options.packageManager ?? recorded ?? detectPackageManager() ?? "pnpm";
207
+ const appfile = resolve(root, "app/fastlane/Appfile");
208
+ const packageId = existsSync(appfile) ? /package_name\("([^"]+)"\)/.exec(readFileSync(appfile, "utf8"))?.[1] ?? `com.example.${dartName(name)}` : `com.example.${dartName(name)}`;
209
+ const workflows = resolve(root, ".github/workflows");
210
+ const existing = existsSync(workflows) ? readdirSync(workflows).filter((f) => f === "checks.yml" || f === "release.yml") : [];
211
+ if (existing.length > 0) {
212
+ throw new Error(`refusing to overwrite existing workflows: ${existing.join(", ")}`);
213
+ }
214
+ renderTree(resolve(templatesRoot, "ci"), root, name, manager, packageId);
215
+ return { root, files: [".github/workflows/checks.yml", ".github/workflows/release.yml"] };
216
+ }
110
217
  function scaffoldGame(options) {
111
218
  const root = resolve(options.directory);
112
219
  const name = gameSlug(basename(root));
113
220
  const manager = options.packageManager ?? detectPackageManager() ?? "pnpm";
114
221
  const org = options.org?.trim() || "com.example";
222
+ const packageId = `${org}.${dartName(name)}`;
115
223
  const bootstrap = options.bootstrap ?? true;
116
224
  const run = options.run ?? ((command, args, cwd) => execFileSync(command, args, { cwd, stdio: "inherit" }));
117
225
  if (existsSync(root)) throw new Error(`target already exists: ${root}`);
@@ -120,17 +228,23 @@ function scaffoldGame(options) {
120
228
  try {
121
229
  const serverRoot = resolve(stagingRoot, "server");
122
230
  const appRoot = resolve(stagingRoot, "app");
123
- renderTree(resolve(templatesRoot, "worker"), serverRoot, name, manager);
124
- renderTree(resolve(templatesRoot, "project"), stagingRoot, name, manager);
231
+ renderTree(resolve(templatesRoot, "worker"), serverRoot, name, manager, packageId);
232
+ renderTree(resolve(templatesRoot, "project"), stagingRoot, name, manager, packageId);
233
+ if (options.ci) renderTree(resolve(templatesRoot, "ci"), stagingRoot, name, manager, packageId);
125
234
  if (bootstrap) {
126
235
  run("flutter", ["create", "--empty", "--platforms", "android,web", "--project-name", dartName(name), "--org", org, appRoot], stagingRoot);
127
236
  enableAndroidCoreLibraryDesugaring(appRoot);
237
+ enableAndroidReleaseSigning(appRoot);
128
238
  } else {
129
239
  mkdirSync(appRoot, { recursive: true });
130
240
  }
131
- renderTree(resolve(templatesRoot, "app-overlay"), appRoot, name, manager);
241
+ renderTree(resolve(templatesRoot, "app-overlay"), appRoot, name, manager, packageId);
132
242
  if (bootstrap) {
243
+ configureLauncherIconsAndSplash(appRoot);
133
244
  run("flutter", ["pub", "add", `eigen_flutter@${flutterClientVersion}`, "firebase_core@^4.9.0", "firebase_messaging@^16.2.2"], appRoot);
245
+ run("flutter", ["pub", "add", "dev:flutter_launcher_icons", "dev:flutter_native_splash"], appRoot);
246
+ run("dart", ["run", "flutter_launcher_icons"], appRoot);
247
+ run("dart", ["run", "flutter_native_splash:create"], appRoot);
134
248
  const [install, installArgs] = packageCommand(manager, "install");
135
249
  run(install, installArgs, serverRoot);
136
250
  const [contract, contractArgs] = packageCommand(manager, "contract");
@@ -147,6 +261,8 @@ function scaffoldGame(options) {
147
261
 
148
262
  export {
149
263
  detectPackageManager,
264
+ decodeUtf8,
265
+ addContinuousIntegration,
150
266
  scaffoldGame
151
267
  };
152
- //# sourceMappingURL=chunk-DEHAHQTO.js.map
268
+ //# sourceMappingURL=chunk-6KBM5WCK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/engine-range.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, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { ENGINE_PACKAGE, engineRange } from \"./engine-range.js\";\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 /**\n * Emit the GitHub Actions workflows. Off by default — see the call site for\n * why, and `addContinuousIntegration` for adding them to an existing\n * project later.\n */\n ci?: boolean;\n /**\n * Runs the bootstrap subprocesses. A seam: the tests substitute a recorder,\n * and `scripts/scaffold-e2e.mjs` wraps it to point the generated server at\n * this workspace's engine rather than npm's copy.\n */\n run?: (command: string, args: string[], cwd: string) => void;\n}\n\nexport interface ScaffoldResult {\n root: string;\n name: string;\n}\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst templatesRoot = resolve(packageRoot, \"templates\");\n\n/**\n * The engine range emitted into a scaffolded project's package.json.\n *\n * Read from this package's `@eigeninteractive/server` devDependency rather than\n * from its own version — see `engine-range.ts` for why that is the version the\n * templates are known to compile against, and what it replaced.\n */\nconst engineVersion = engineRange((JSON.parse(readFileSync(resolve(packageRoot, \"package.json\"), \"utf8\")) as { devDependencies?: Record<string, string> }).devDependencies?.[ENGINE_PACKAGE], () => {\n // Only reached in this workspace, where the manifest still says\n // `workspace:*`. A published tarball has no sibling here and never asks.\n const sibling = resolve(packageRoot, \"../server/package.json\");\n return existsSync(sibling) ? (JSON.parse(readFileSync(sibling, \"utf8\")) as { version: string }).version : undefined;\n});\n\n/**\n * The Flutter client range installed into the app half.\n *\n * A stated version, not a derived or resolved one. The Dart templates import\n * `package:eigen_flutter/eigen_flutter.dart` and\n * `package:eigen_flutter/testing/twin_fixtures.dart` and are written against a\n * specific Dart API — and `eigen_flutter` lives in another repository, versioned\n * independently, so nothing in this repository can compute which release that\n * is. Only compiling a scaffolded app establishes it, which is what the\n * `scaffold` job in checks.yml does on every change.\n *\n * This briefly resolved from pub.dev instead: \"the newest `eigen_flutter` whose\n * own `eigen_api` constraint targets the engine line being scaffolded\". That\n * predicate is wrong. The `eigen_api` constraint describes the WIRE the shell\n * speaks, not the Dart API these templates call, and the two move\n * independently — a future `eigen_flutter` may legitimately keep `eigen_api:\n * ^0.2.0` while renaming everything the templates touch. It would have been\n * selected, and the generated app would not compile.\n *\n * A caret RANGE rather than an exact version, so it still improves without a\n * republish: `flutter pub add eigen_flutter@^0.3.0` already picks the newest\n * 0.3.x at scaffold time, which is the part the pub.dev lookup was duplicating.\n * What it deliberately cannot do is cross to 0.4.x — the one move that needs a\n * human to confirm the templates still compile.\n *\n * Staleness is therefore a failing check rather than a broken scaffold: this is\n * only ever a release behind, never wrong.\n *\n * Raised to 0.3.0 because the notification icon moved into `eigen_flutter`'s\n * Android plugin: the scaffold stopped shipping `ic_notification.xml`, so a\n * game resolving 0.2.x would reference a drawable nothing provides.\n */\nconst flutterClientVersion = \"^0.3.0\";\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\n/**\n * Substitutes the scaffold's tokens, of which there are two deliberate kinds.\n *\n * `{{BRACED}}` for values that have no valid example — a package manager,\n * an engine version, an Android package id. They appear only in manifests,\n * READMEs, workflows and the `Appfile`: files nothing compiles.\n *\n * Bare words (`ExampleGame`, `example-game`, …) for names a real example game\n * genuinely has. This is what keeps `templates/worker` and the Dart templates\n * VALID, COMPILING SOURCE rather than text with holes in it, which the rest of\n * this package leans on hard: `typecheck` runs `tsc` over `templates/worker`,\n * the tests `import` its game module directly and build a real contract from\n * it, and the `scaffold` job compiles the Dart templates. Bracing these would\n * trade all of that for a marker.\n *\n * The cost, and the one rule for template authors: a template file cannot\n * contain the literal string `example-game`, `Example Game`, `ExampleGame` or\n * `example_game` and keep it — every occurrence is rewritten, in prose and in\n * code alike. Write around it, or brace a new token instead.\n *\n * Note this means the set of files carrying a token changes whenever someone\n * writes one into an existing file. Nothing needs to track that: `renderTree`\n * decides what to rewrite from the bytes, not from a list of paths.\n */\nfunction render(contents: string, name: string, manager: PackageManager, packageId: string): string {\n const replacements: ReadonlyArray<readonly [string, string]> = [\n [\"@game/example-game-server\", `@game/${name}-server`],\n [\"{{PACKAGE_MANAGER}}\", manager],\n [\"{{PACKAGE_ID}}\", packageId],\n [\"{{ENGINE_VERSION}}\", engineVersion],\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\n/**\n * True when the bytes are valid UTF-8.\n *\n * This is not a guess at whether a file is \"binary\" — it is the precondition\n * of the operation. Token substitution is `String.replaceAll`, so a file that\n * cannot be decoded cannot take part, and one that can is safe to rewrite.\n *\n * Deliberately not an extension list. A DENY list (`.png`, `.jar`, …) fails\n * silently in the dangerous direction: the day someone adds a `.webp`, it is\n * corrupted with no error. An ALLOW list fails the safe way but fits this tree\n * badly — `Gemfile`, `Fastfile`, `.nvmrc`, `.fvmrc` and `.ruby-version` all\n * need rendering and none carry a usable extension, so it would have to be a\n * list of extensions *and* a list of bare filenames, both drifting.\n *\n * `.template` is not the signal either, despite the name: it marks files a\n * language server must not claim before they have an enclosing package. Five\n * of them contain no token at all, and nine token-bearing files are not\n * marked, so the two concepts are orthogonal here.\n *\n * `renderTree`'s test pins the resulting classification for the whole tree, so\n * this stays a reviewed decision rather than an invisible one.\n */\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\nexport function decodeUtf8(bytes: Buffer): string | undefined {\n try {\n return utf8.decode(bytes);\n } catch {\n return undefined;\n }\n}\n\nfunction renderTree(source: string, destination: string, name: string, manager: PackageManager, packageId: string): void {\n cpSync(source, destination, { recursive: true });\n // Walks the TEMPLATE, not the destination. The app overlay renders on top of\n // a `flutter create` result, so walking the destination meant rewriting\n // every file Flutter had just generated — including its binaries — for files\n // this scaffolder does not own and has no business touching.\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const templatePath = resolve(directory, entry.name);\n if (entry.isDirectory()) {\n visit(templatePath);\n continue;\n }\n if (!entry.isFile()) continue;\n\n const rendered = resolve(destination, relative(source, templatePath));\n const text = decodeUtf8(readFileSync(templatePath));\n // Undecodable files are already in place from the copy above, byte for\n // byte. Only decodable ones are rewritten.\n if (text !== undefined) {\n writeFileSync(rendered, render(text, name, manager, packageId));\n }\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(rendered, rendered.slice(0, -\".template\".length));\n }\n }\n };\n visit(source);\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, packageId));\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\n// A second top-level `android { }` block is valid Gradle Kotlin DSL — Gradle\n// merges repeated extension-configuration blocks in one file, which is what\n// lets this stay a pure append, same as the desugaring block above. A second\n// top-level `plugins { }` block is NOT valid (Gradle allows exactly one per\n// script), which is why the Crashlytics Gradle plugin registration —\n// `flutterfire configure`'s own territory — is deliberately left alone here.\n//\n// `Properties` needs a real `import`, prepended separately below — a\n// fully-qualified `java.util.Properties()` reference here was tried first,\n// to avoid needing to touch the top of the file at all, but fails to\n// resolve under AGP 9's Kotlin DSL script compilation\n// (`flutter create`'s current default): \"Unresolved reference 'util'\".\n// Confirmed by actually building a scaffolded project, not just reading it.\nconst androidReleaseSigning = `val releaseKeyProperties: Map<String, String> =\n rootProject.file(\"key.properties\")\n .takeIf { it.exists() }\n ?.readLines()\n ?.map { it.trim() }\n ?.filter { it.isNotEmpty() && !it.startsWith(\"#\") && it.contains(\"=\") }\n ?.associate { it.substringBefore(\"=\").trim() to it.substringAfter(\"=\").trim() }\n .orEmpty()\n\nandroid {\n signingConfigs {\n create(\"release\") {\n storeFile = releaseKeyProperties[\"storeFile\"]?.let { file(it) }\n storePassword = releaseKeyProperties[\"storePassword\"]\n keyAlias = releaseKeyProperties[\"keyAlias\"]\n keyPassword = releaseKeyProperties[\"keyPassword\"]\n }\n }\n buildTypes {\n release {\n signingConfig = if (releaseKeyProperties[\"storeFile\"] != null)\n signingConfigs.getByName(\"release\")\n else\n signingConfigs.getByName(\"debug\")\n isMinifyEnabled = true\n isShrinkResources = true\n proguardFiles(\n getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n \"proguard-rules.pro\",\n )\n }\n }\n}`;\n\nfunction enableAndroidReleaseSigning(appRoot: string): void {\n const gradlePath = resolve(appRoot, \"android/app/build.gradle.kts\");\n const gradle = readFileSync(gradlePath, \"utf8\");\n // Not `gradle.includes(\"signingConfigs\")` — Flutter's own template already\n // contains that substring (`signingConfig = signingConfigs.getByName(\"debug\")`),\n // so that check would always short-circuit and never append anything.\n if (gradle.includes(\"releaseKeyProperties\")) return;\n // A pure append. This previously also PREPENDED `import java.util.Properties`,\n // because Kotlin requires imports before every other top-level declaration —\n // which meant reaching into the very start of a file this scaffolder does\n // not own, the most position-dependent edit here and the one already broken\n // once by an AGP upgrade. Parsing `key.properties` with the Kotlin stdlib\n // needs no import, so the edit is now append-only and cannot collide with\n // whatever Flutter or `flutterfire configure` writes above it.\n appendFileSync(gradlePath, `${gradle.endsWith(\"\\n\") ? \"\" : \"\\n\"}${androidReleaseSigning}\\n`);\n}\n\n// `flutter_launcher_icons`/`flutter_native_splash` read these as plain\n// top-level pubspec keys, so appending is safe regardless of what\n// `flutter create` already wrote under `flutter:`. Deliberately omits\n// `ios`/`macos` — `scaffoldGame` only creates `--platforms android,web`, and\n// `flutter_launcher_icons` errors if told to target a platform whose\n// directory doesn't exist.\n// The colours are the Eigen palette (ink #1B1E24, paper #F4F1EA) because the\n// shipped placeholder art is the Eigen mark: the adaptive foreground is the\n// reversed, light-on-dark variant, so an ink background is what makes it\n// legible. Rebranding a game means replacing the four PNGs *and* these\n// colours together — see https://eigeninteractive.com/docs/ship-it/branding.\nconst launcherIconsAndSplashConfig = `\nflutter_launcher_icons:\n android: true\n web:\n generate: true\n image_path: \"assets/icon/icon.png\"\n image_path: \"assets/icon/icon.png\"\n adaptive_icon_background: \"#1B1E24\"\n adaptive_icon_foreground: \"assets/icon/icon_foreground.png\"\n min_sdk_android: 21\n\nflutter_native_splash:\n color: \"#F4F1EA\"\n color_dark: \"#1B1E24\"\n image: assets/icon/splash.png\n image_dark: assets/icon/splash_dark.png\n fullscreen: true\n android_12:\n color: \"#F4F1EA\"\n color_dark: \"#1B1E24\"\n image: assets/icon/splash.png\n image_dark: assets/icon/splash_dark.png\n icon_background_color: \"#F4F1EA\"\n icon_background_color_dark: \"#1B1E24\"\n web: true\n`;\n\nfunction configureLauncherIconsAndSplash(appRoot: string): void {\n const pubspecPath = resolve(appRoot, \"pubspec.yaml\");\n const pubspec = readFileSync(pubspecPath, \"utf8\");\n if (pubspec.includes(\"flutter_launcher_icons:\")) return;\n appendFileSync(pubspecPath, `${pubspec.endsWith(\"\\n\") ? \"\" : \"\\n\"}${launcherIconsAndSplashConfig}`);\n}\n\nexport interface AddContinuousIntegrationOptions {\n directory: string;\n /** Overrides the manager recorded in the project's own package.json. */\n packageManager?: PackageManager;\n}\n\n/**\n * Adds the GitHub Actions workflows to an existing generated project.\n *\n * The companion to `ci: false`: a game can be built and played locally for as\n * long as its author likes, then gain the same PR gate and signed release\n * pipeline a `--ci` scaffold would have produced.\n */\nexport function addContinuousIntegration(options: AddContinuousIntegrationOptions): { root: string; files: string[] } {\n const root = resolve(options.directory);\n const manifestPath = resolve(root, \"package.json\");\n if (!existsSync(manifestPath) || !existsSync(resolve(root, \"server\")) || !existsSync(resolve(root, \"app\"))) {\n throw new Error(`not an Eigen game project (expected package.json, server/ and app/): ${root}`);\n }\n\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as {\n name?: string;\n scripts?: Record<string, string>;\n };\n const name = gameSlug(manifest.name ?? basename(root));\n\n // The generated `contract` script embeds the manager the project was\n // scaffolded with (`cd server && pnpm run contract && …`), which is a more\n // reliable signal than a lockfile — it is present before anything is\n // installed, and it is what the project actually uses.\n const recorded = manifest.scripts?.contract?.includes(\"pnpm \") ? \"pnpm\" : manifest.scripts?.contract?.includes(\"npm \") ? \"npm\" : undefined;\n const manager = options.packageManager ?? recorded ?? detectPackageManager() ?? \"pnpm\";\n\n // Only `{{PACKAGE_MANAGER}}` appears in these templates, but the real\n // package id is read back rather than reconstructed, so a project scaffolded\n // under a different `--org` cannot be silently rewritten to `com.example`.\n const appfile = resolve(root, \"app/fastlane/Appfile\");\n const packageId = existsSync(appfile) ? (/package_name\\(\"([^\"]+)\"\\)/.exec(readFileSync(appfile, \"utf8\"))?.[1] ?? `com.example.${dartName(name)}`) : `com.example.${dartName(name)}`;\n\n const workflows = resolve(root, \".github/workflows\");\n const existing = existsSync(workflows) ? readdirSync(workflows).filter((f) => f === \"checks.yml\" || f === \"release.yml\") : [];\n if (existing.length > 0) {\n throw new Error(`refusing to overwrite existing workflows: ${existing.join(\", \")}`);\n }\n\n renderTree(resolve(templatesRoot, \"ci\"), root, name, manager, packageId);\n return { root, files: [\".github/workflows/checks.yml\", \".github/workflows/release.yml\"] };\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 // Mirrors how `flutter create --org` derives the real Android\n // `applicationId`/iOS bundle id, so Fastlane's `Appfile` names the same\n // package the build actually produces.\n const packageId = `${org}.${dartName(name)}`;\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\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, packageId);\n renderTree(resolve(templatesRoot, \"project\"), stagingRoot, name, manager, packageId);\n // Opt-in. release.yml wants an upload keystore and a Play service account\n // that a brand-new project does not have, so generating it by default\n // means a red X on main from the first push until someone sets both up.\n // `create-eigen-game add ci` writes the same files whenever it is wanted.\n if (options.ci) renderTree(resolve(templatesRoot, \"ci\"), stagingRoot, name, manager, packageId);\n\n if (bootstrap) {\n run(\"flutter\", [\"create\", \"--empty\", \"--platforms\", \"android,web\", \"--project-name\", dartName(name), \"--org\", org, appRoot], stagingRoot);\n enableAndroidCoreLibraryDesugaring(appRoot);\n enableAndroidReleaseSigning(appRoot);\n } else {\n mkdirSync(appRoot, { recursive: true });\n }\n renderTree(resolve(templatesRoot, \"app-overlay\"), appRoot, name, manager, packageId);\n\n if (bootstrap) {\n configureLauncherIconsAndSplash(appRoot);\n run(\"flutter\", [\"pub\", \"add\", `eigen_flutter@${flutterClientVersion}`, \"firebase_core@^4.9.0\", \"firebase_messaging@^16.2.2\"], appRoot);\n run(\"flutter\", [\"pub\", \"add\", \"dev:flutter_launcher_icons\", \"dev:flutter_native_splash\"], appRoot);\n // Actually apply the icons rather than only configuring them. Both tools\n // write generated files (mipmaps, `web/icons/`, splash drawables and\n // styles) that are committed, not built — leaving them unrun would ship\n // Flutter's own blue logo until a game author happened to notice.\n run(\"dart\", [\"run\", \"flutter_launcher_icons\"], appRoot);\n run(\"dart\", [\"run\", \"flutter_native_splash:create\"], 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","/**\n * Where the engine range emitted into a scaffolded project comes from.\n *\n * Its own module because the two branches below execute in different worlds —\n * one only inside this repository, the other only inside a published tarball —\n * and a function whose published-only path never runs in CI is a function that\n * is not tested. `src/index.ts` uses it; the package `exports` map does not\n * expose it, so it is internal despite being importable by the tests.\n */\n\n/** The dependency whose version the scaffolder emits into the generated server. */\nexport const ENGINE_PACKAGE = \"@eigeninteractive/server\";\n\n/**\n * The engine range for a scaffolded project, from this package's own\n * `@eigeninteractive/server` devDependency.\n *\n * That number is not maintained by anyone: it is the version CI compiled the\n * templates against, because `pnpm -r typecheck` runs\n * `tsc -p templates/worker/tsconfig.json` inside this workspace. Emitting\n * anything else would mean shipping templates paired with an engine no build\n * ever saw.\n *\n * It replaced a derivation from this package's OWN version, which was correct\n * only while `create-eigen-game` sat in the `fixed` changesets group — and that\n * membership is what forced an engine-wide release for every scaffolder-only\n * change. Reading the dependency keeps the guarantee and drops the coupling.\n *\n * Two branches, because the same manifest reads differently in the two places\n * this runs:\n *\n * - **Published tarball** — pnpm rewrites `workspace:*` to the exact version\n * while packing, so the manifest already states it (`\"0.2.0\"`), and no\n * sibling package exists on disk to consult.\n * - **This workspace** — the manifest still says `workspace:*`, which names no\n * version, so the sibling package is read directly.\n *\n * @param declared the raw `@eigeninteractive/server` devDependency value\n * @param readSiblingVersion consulted only for the `workspace:` protocol\n */\nexport function engineRange(declared: string | undefined, readSiblingVersion: () => string | undefined): string {\n if (!declared) {\n throw new Error(`create-eigen-game has no ${ENGINE_PACKAGE} devDependency, which is where the engine range it emits comes from`);\n }\n\n const version = declared.startsWith(\"workspace:\") ? readSiblingVersion() : declared;\n\n if (!version) {\n throw new Error(`create-eigen-game declares ${ENGINE_PACKAGE} as \"${declared}\" but the workspace package could not be read, so there is no engine version to emit`);\n }\n\n // pnpm packs `workspace:*` as a bare version, which is what makes the caret\n // below correct. Asserting it means a change in that behaviour fails here\n // rather than emitting something like `^^0.3.0` into a real project.\n if (!/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {\n throw new Error(`expected an exact ${ENGINE_PACKAGE} version to build a range from, got \"${version}\"`);\n }\n\n return `^${version}`;\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -1,21 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ addContinuousIntegration,
3
4
  detectPackageManager,
4
5
  scaffoldGame
5
- } from "./chunk-DEHAHQTO.js";
6
+ } from "./chunk-6KBM5WCK.js";
6
7
 
7
8
  // src/cli.ts
8
9
  import { parseArgs } from "util";
9
10
  var help = `Usage: create-eigen-game <game-slug> [options]
11
+ create-eigen-game add ci [directory] [options]
10
12
 
11
13
  Creates one repository containing an Eigen Cloudflare Worker and Flutter app.
12
14
  The destination basename is the lowercase kebab-case game slug; the CLI derives
13
15
  the display name, Dart package name, and Dart/TypeScript type prefix from it.
14
16
 
15
17
  Options:
18
+ --ci Also emit the GitHub Actions workflows (off by
19
+ default: release.yml needs an upload keystore and a
20
+ Play service account, so it fails until both exist)
16
21
  --org <reverse-domain> Android/iOS organization (default: com.example)
17
22
  --package-manager <pm> npm or pnpm (defaults to the invoking package manager)
18
- -h, --help Show this help`;
23
+ -h, --help Show this help
24
+
25
+ Commands:
26
+ add ci [directory] Add the workflows to an existing project, for when
27
+ you are ready to ship rather than at scaffold time.
28
+ Defaults to the current directory.`;
29
+ function resolveManager(requested) {
30
+ if (requested === void 0) return void 0;
31
+ if (requested !== "npm" && requested !== "pnpm") {
32
+ throw new Error("--package-manager must be npm or pnpm");
33
+ }
34
+ return requested;
35
+ }
19
36
  function main(args) {
20
37
  const { values, positionals } = parseArgs({
21
38
  args,
@@ -23,6 +40,7 @@ function main(args) {
23
40
  strict: true,
24
41
  options: {
25
42
  help: { type: "boolean", short: "h" },
43
+ ci: { type: "boolean" },
26
44
  org: { type: "string" },
27
45
  "package-manager": { type: "string" }
28
46
  }
@@ -31,22 +49,40 @@ function main(args) {
31
49
  console.log(help);
32
50
  return;
33
51
  }
52
+ const requestedManager = resolveManager(values["package-manager"]);
53
+ if (positionals[0] === "add") {
54
+ if (positionals[1] !== "ci") {
55
+ throw new Error(`unknown subcommand: add ${positionals[1] ?? ""}
56
+
57
+ ${help}`.trim());
58
+ }
59
+ if (positionals.length > 3) {
60
+ throw new Error(`expected at most one directory
61
+
62
+ ${help}`);
63
+ }
64
+ const result2 = addContinuousIntegration({
65
+ directory: positionals[2] ?? process.cwd(),
66
+ packageManager: requestedManager
67
+ });
68
+ console.log(`Added ${result2.files.join(" and ")} in ${result2.root}`);
69
+ return;
70
+ }
34
71
  if (positionals.length !== 1) {
35
72
  throw new Error(`expected exactly one destination directory
36
73
 
37
74
  ${help}`);
38
75
  }
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
76
  const result = scaffoldGame({
45
77
  directory: positionals[0],
46
78
  org: values.org,
47
- packageManager
79
+ packageManager: requestedManager ?? detectPackageManager() ?? "pnpm",
80
+ ci: values.ci
48
81
  });
49
82
  console.log(`Created ${result.name} in ${result.root}`);
83
+ if (!values.ci) {
84
+ console.log("No CI workflows were generated. Run `create-eigen-game add ci` inside the project when you want them.");
85
+ }
50
86
  }
51
87
  try {
52
88
  main(process.argv.slice(2));
package/dist/cli.js.map CHANGED
@@ -1 +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: unknown) {\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,OAAgB;AACvB,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAQ,MAAM,sBAAsB,OAAO,EAAE;AAC7C,UAAQ,WAAW;AACrB;","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { addContinuousIntegration, detectPackageManager, type PackageManager, scaffoldGame } from \"./index.js\";\n\nconst help = `Usage: create-eigen-game <game-slug> [options]\n create-eigen-game add ci [directory] [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 --ci Also emit the GitHub Actions workflows (off by\n default: release.yml needs an upload keystore and a\n Play service account, so it fails until both exist)\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\nCommands:\n add ci [directory] Add the workflows to an existing project, for when\n you are ready to ship rather than at scaffold time.\n Defaults to the current directory.`;\n\nfunction resolveManager(requested: string | undefined): PackageManager | undefined {\n if (requested === undefined) return undefined;\n if (requested !== \"npm\" && requested !== \"pnpm\") {\n throw new Error(\"--package-manager must be npm or pnpm\");\n }\n return requested;\n}\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 ci: { type: \"boolean\" },\n org: { type: \"string\" },\n \"package-manager\": { type: \"string\" },\n },\n });\n\n if (values.help) {\n console.log(help);\n return;\n }\n\n const requestedManager = resolveManager(values[\"package-manager\"]);\n\n if (positionals[0] === \"add\") {\n if (positionals[1] !== \"ci\") {\n throw new Error(`unknown subcommand: add ${positionals[1] ?? \"\"}\\n\\n${help}`.trim());\n }\n if (positionals.length > 3) {\n throw new Error(`expected at most one directory\\n\\n${help}`);\n }\n const result = addContinuousIntegration({\n directory: positionals[2] ?? process.cwd(),\n packageManager: requestedManager,\n });\n console.log(`Added ${result.files.join(\" and \")} in ${result.root}`);\n return;\n }\n\n if (positionals.length !== 1) {\n throw new Error(`expected exactly one destination directory\\n\\n${help}`);\n }\n\n const result = scaffoldGame({\n directory: positionals[0],\n org: values.org,\n packageManager: requestedManager ?? detectPackageManager() ?? \"pnpm\",\n ci: values.ci,\n });\n console.log(`Created ${result.name} in ${result.root}`);\n if (!values.ci) {\n console.log(\"No CI workflows were generated. Run `create-eigen-game add ci` inside the project when you want them.\");\n }\n}\n\ntry {\n main(process.argv.slice(2));\n} catch (error: unknown) {\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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBb,SAAS,eAAe,WAA2D;AACjF,MAAI,cAAc,OAAW,QAAO;AACpC,MAAI,cAAc,SAAS,cAAc,QAAQ;AAC/C,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AACA,SAAO;AACT;AAEA,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,IAAI,EAAE,MAAM,UAAU;AAAA,MACtB,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;AAEA,QAAM,mBAAmB,eAAe,OAAO,iBAAiB,CAAC;AAEjE,MAAI,YAAY,CAAC,MAAM,OAAO;AAC5B,QAAI,YAAY,CAAC,MAAM,MAAM;AAC3B,YAAM,IAAI,MAAM,2BAA2B,YAAY,CAAC,KAAK,EAAE;AAAA;AAAA,EAAO,IAAI,GAAG,KAAK,CAAC;AAAA,IACrF;AACA,QAAI,YAAY,SAAS,GAAG;AAC1B,YAAM,IAAI,MAAM;AAAA;AAAA,EAAqC,IAAI,EAAE;AAAA,IAC7D;AACA,UAAMA,UAAS,yBAAyB;AAAA,MACtC,WAAW,YAAY,CAAC,KAAK,QAAQ,IAAI;AAAA,MACzC,gBAAgB;AAAA,IAClB,CAAC;AACD,YAAQ,IAAI,SAASA,QAAO,MAAM,KAAK,OAAO,CAAC,OAAOA,QAAO,IAAI,EAAE;AACnE;AAAA,EACF;AAEA,MAAI,YAAY,WAAW,GAAG;AAC5B,UAAM,IAAI,MAAM;AAAA;AAAA,EAAiD,IAAI,EAAE;AAAA,EACzE;AAEA,QAAM,SAAS,aAAa;AAAA,IAC1B,WAAW,YAAY,CAAC;AAAA,IACxB,KAAK,OAAO;AAAA,IACZ,gBAAgB,oBAAoB,qBAAqB,KAAK;AAAA,IAC9D,IAAI,OAAO;AAAA,EACb,CAAC;AACD,UAAQ,IAAI,WAAW,OAAO,IAAI,OAAO,OAAO,IAAI,EAAE;AACtD,MAAI,CAAC,OAAO,IAAI;AACd,YAAQ,IAAI,uGAAuG;AAAA,EACrH;AACF;AAEA,IAAI;AACF,OAAK,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC5B,SAAS,OAAgB;AACvB,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAQ,MAAM,sBAAsB,OAAO,EAAE;AAC7C,UAAQ,WAAW;AACrB;","names":["result"]}
package/dist/index.d.ts CHANGED
@@ -9,6 +9,12 @@ interface ScaffoldOptions {
9
9
  * halves of the combined project.
10
10
  */
11
11
  bootstrap?: boolean;
12
+ /**
13
+ * Emit the GitHub Actions workflows. Off by default — see the call site for
14
+ * why, and `addContinuousIntegration` for adding them to an existing
15
+ * project later.
16
+ */
17
+ ci?: boolean;
12
18
  /**
13
19
  * Runs the bootstrap subprocesses. A seam: the tests substitute a recorder,
14
20
  * and `scripts/scaffold-e2e.mjs` wraps it to point the generated server at
@@ -21,6 +27,23 @@ interface ScaffoldResult {
21
27
  name: string;
22
28
  }
23
29
  declare function detectPackageManager(userAgent?: string | undefined): PackageManager | undefined;
30
+ declare function decodeUtf8(bytes: Buffer): string | undefined;
31
+ interface AddContinuousIntegrationOptions {
32
+ directory: string;
33
+ /** Overrides the manager recorded in the project's own package.json. */
34
+ packageManager?: PackageManager;
35
+ }
36
+ /**
37
+ * Adds the GitHub Actions workflows to an existing generated project.
38
+ *
39
+ * The companion to `ci: false`: a game can be built and played locally for as
40
+ * long as its author likes, then gain the same PR gate and signed release
41
+ * pipeline a `--ci` scaffold would have produced.
42
+ */
43
+ declare function addContinuousIntegration(options: AddContinuousIntegrationOptions): {
44
+ root: string;
45
+ files: string[];
46
+ };
24
47
  declare function scaffoldGame(options: ScaffoldOptions): ScaffoldResult;
25
48
 
26
- export { type PackageManager, type ScaffoldOptions, type ScaffoldResult, detectPackageManager, scaffoldGame };
49
+ export { type AddContinuousIntegrationOptions, type PackageManager, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, decodeUtf8, detectPackageManager, scaffoldGame };
package/dist/index.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import {
2
+ addContinuousIntegration,
3
+ decodeUtf8,
2
4
  detectPackageManager,
3
5
  scaffoldGame
4
- } from "./chunk-DEHAHQTO.js";
6
+ } from "./chunk-6KBM5WCK.js";
5
7
  export {
8
+ addContinuousIntegration,
9
+ decodeUtf8,
6
10
  detectPackageManager,
7
11
  scaffoldGame
8
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-eigen-game",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "description": "Scaffold an Eigen game Worker and Flutter app without cloning the engine repositories.",
6
6
  "type": "module",
@@ -0,0 +1,3 @@
1
+ {
2
+ "flutter": "3.44.8"
3
+ }
@@ -0,0 +1 @@
1
+ 4.0
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to
7
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+
9
+ ## [Unreleased]
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "fastlane", "~> 2.233"
@@ -0,0 +1,9 @@
1
+ # Flutter engine rules are added automatically by the Flutter Gradle plugin.
2
+ #
3
+ # image_cropper ships its own consumer-proguard-rules.pro (OkHttp + uCrop).
4
+ # google_sign_in (Credential Manager), in_app_update, and in_app_review use
5
+ # Google Play libraries that include their own consumer rules via Maven.
6
+ #
7
+ # Only libraries confirmed to lack consumer rules are listed below — currently
8
+ # none. Add a keep rule here only after confirming a dependency ships no
9
+ # consumer rules of its own and is stripped by R8.
@@ -0,0 +1 @@
1
+ package_name("{{PACKAGE_ID}}")
@@ -0,0 +1,29 @@
1
+ default_platform(:android)
2
+
3
+ platform :android do
4
+ desc "Upload AAB to Google Play internal testing track"
5
+ lane :internal do
6
+ upload_to_play_store(
7
+ track: "internal",
8
+ aab: "build/app/outputs/bundle/release/app-release.aab",
9
+ skip_upload_apk: true,
10
+ skip_upload_metadata: true,
11
+ skip_upload_changelogs: true,
12
+ skip_upload_images: true,
13
+ skip_upload_screenshots: true,
14
+ )
15
+ end
16
+
17
+ desc "Upload AAB directly to Google Play production track"
18
+ lane :production do
19
+ upload_to_play_store(
20
+ track: "production",
21
+ aab: "build/app/outputs/bundle/release/app-release.aab",
22
+ skip_upload_apk: true,
23
+ skip_upload_metadata: true,
24
+ skip_upload_changelogs: true,
25
+ skip_upload_images: true,
26
+ skip_upload_screenshots: true,
27
+ )
28
+ end
29
+ end
@@ -9,6 +9,9 @@
9
9
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
10
10
  <meta name="apple-mobile-web-app-title" content="Example Game">
11
11
  <title>Example Game</title>
12
+ <!-- `flutter_launcher_icons` generates favicon.png and web/icons/ from
13
+ assets/icon/icon.png, and rewrites this href in place. -->
14
+ <link rel="icon" type="image/png" href="favicon.png">
12
15
  <link rel="manifest" href="manifest.json">
13
16
  </head>
14
17
  <body>
@@ -0,0 +1,125 @@
1
+ name: Checks
2
+
3
+ # The single definition of "is this commit good?", invoked two ways:
4
+ #
5
+ # - directly, on every pull request;
6
+ # - by `release.yml` via `workflow_call`, on every push to main, so the
7
+ # commit that is about to ship a Play Store build clears exactly the
8
+ # same bar.
9
+ #
10
+ # There is deliberately no `push: [main]` trigger here. Release owns main and
11
+ # calls this, so adding one would run the whole gate twice per merge.
12
+ on:
13
+ pull_request:
14
+ branches: [main]
15
+ workflow_call:
16
+
17
+ # A pull request's run is superseded freely; a run gating a release is not.
18
+ # Keyed on the SHA rather than the ref so a called run never shares a group
19
+ # with release.yml's own.
20
+ concurrency:
21
+ group: checks-${{ github.event.pull_request.number || github.sha }}
22
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
23
+
24
+ # Needs no secrets or Cloudflare/Play credentials, which is why `release.yml`
25
+ # calls it without `secrets: inherit`. Worker deployment and the signed
26
+ # Android build both happen in `release.yml` instead.
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ server:
32
+ name: Server
33
+ runs-on: ubuntu-latest
34
+ defaults:
35
+ run:
36
+ working-directory: server
37
+ steps:
38
+ - uses: actions/checkout@v7
39
+
40
+ # `corepack enable`, not `pnpm/action-setup` — this project can be
41
+ # scaffolded with either npm or pnpm (see the {{PACKAGE_MANAGER}}
42
+ # commands below), and corepack puts both on PATH regardless of which
43
+ # one a game author actually chose.
44
+ - name: Enable corepack
45
+ run: corepack enable
46
+
47
+ - uses: actions/setup-node@v7
48
+ with:
49
+ node-version-file: .nvmrc
50
+ cache: {{PACKAGE_MANAGER}}
51
+
52
+ - name: Install dependencies
53
+ run: {{PACKAGE_MANAGER}} install
54
+
55
+ - name: Test
56
+ run: {{PACKAGE_MANAGER}} run test
57
+
58
+ - name: Typecheck
59
+ run: {{PACKAGE_MANAGER}} run typecheck
60
+
61
+ app:
62
+ name: App
63
+ runs-on: ubuntu-latest
64
+ steps:
65
+ - uses: actions/checkout@v7
66
+
67
+ - name: Enable corepack
68
+ run: corepack enable
69
+
70
+ - uses: actions/setup-node@v7
71
+ with:
72
+ node-version-file: .nvmrc
73
+ cache: {{PACKAGE_MANAGER}}
74
+
75
+ # `contract:check` (below) round-trips through both halves — it
76
+ # invokes the server's own contract generator, then verifies the app's
77
+ # generated payloads still match — so the server's Node dependencies
78
+ # have to exist here too, not just in the `server` job's own runner.
79
+ - name: Install server dependencies
80
+ working-directory: server
81
+ run: {{PACKAGE_MANAGER}} install
82
+
83
+ - uses: subosito/flutter-action@v2
84
+ with:
85
+ flutter-version-file: app/.fvmrc
86
+ channel: stable
87
+ cache: true
88
+
89
+ - name: Install app dependencies
90
+ working-directory: app
91
+ run: flutter pub get
92
+
93
+ # The generated payload types are committed, so a game rule or a
94
+ # server schema that changed without regenerating them would otherwise
95
+ # go unnoticed until a decode failure at runtime.
96
+ - name: Contract and generated payloads are in sync
97
+ run: {{PACKAGE_MANAGER}} run contract:check
98
+
99
+ - name: Check formatting
100
+ working-directory: app
101
+ run: dart format --output=none --set-exit-if-changed .
102
+
103
+ - name: Analyze
104
+ working-directory: app
105
+ run: flutter analyze
106
+
107
+ - name: Test
108
+ working-directory: app
109
+ run: flutter test
110
+
111
+ # A VM widget test can pass while a dependency still imports dart:io or
112
+ # calls an unsupported plugin. Compiling the real web entrypoint is
113
+ # what actually proves it ships — mirrors eigen-flutter's own `example`
114
+ # job, which does the same for the RPS reference app.
115
+ #
116
+ # No Android SDK step in this job, for the same reason
117
+ # `create-eigen-game`'s own `scaffold-e2e.mjs` check skips one:
118
+ # `flutter analyze`/`test`/`build web` don't need it, and it would
119
+ # roughly double this job for no extra coverage. The Android build is
120
+ # verified for real, with a real signing config, in `release.yml`.
121
+ - name: Build web
122
+ working-directory: app
123
+ run: >
124
+ flutter build web --release
125
+ --dart-define-from-file=app-config.json
@@ -0,0 +1,130 @@
1
+ name: Release
2
+
3
+ # Ships a signed Android build on every push to main:
4
+ #
5
+ # 1. checks — the exact PR gate, re-run against the merge commit.
6
+ # 2. build — a signed, obfuscated AAB.
7
+ # 3. deploy — fastlane uploads it to the Play Store internal track.
8
+ #
9
+ # Worker deployment is NOT here, on purpose: it applies D1 migrations against
10
+ # a real database, so it stays the deliberate, manual `{{PACKAGE_MANAGER}} run
11
+ # deploy` from a machine with `wrangler login` — the same reasoning the
12
+ # engine's own CI uses for never deploying a Worker automatically.
13
+ on:
14
+ push:
15
+ branches: [main]
16
+
17
+ concurrency:
18
+ group: release
19
+ cancel-in-progress: false
20
+
21
+ # Default deny. Every job below grants itself only what it needs.
22
+ permissions: {}
23
+
24
+ jobs:
25
+ # The same gate every pull request clears, re-run against the commit that
26
+ # is about to ship. Needs no secrets, so it is called without
27
+ # `secrets: inherit`.
28
+ checks:
29
+ name: Checks
30
+ uses: ./.github/workflows/checks.yml
31
+ permissions:
32
+ contents: read
33
+
34
+ build:
35
+ name: Build signed AAB
36
+ needs: checks
37
+ runs-on: ubuntu-latest
38
+ permissions:
39
+ contents: read
40
+ defaults:
41
+ run:
42
+ working-directory: app
43
+ steps:
44
+ - uses: actions/checkout@v7
45
+
46
+ - uses: actions/setup-java@v5.6.0
47
+ with:
48
+ distribution: temurin
49
+ java-version: "17"
50
+ cache: gradle
51
+
52
+ - uses: subosito/flutter-action@v2
53
+ with:
54
+ flutter-version-file: app/.fvmrc
55
+ channel: stable
56
+ cache: true
57
+
58
+ - name: Install dependencies
59
+ run: flutter pub get
60
+
61
+ # The keystore and Play service account are the only things here that
62
+ # are genuinely secret. `app-config.json`, `firebase_options.dart` and
63
+ # `google-services.json` are public app identifiers, not credentials —
64
+ # commit them for real once `firebase:configure` has been run, rather
65
+ # than reconstructing them from secrets on every build. See
66
+ # https://eigeninteractive.com/docs/ship-it/configure and
67
+ # https://eigeninteractive.com/docs/ship-it/store-release.
68
+ - name: Decode keystore
69
+ run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > "${{ github.workspace }}/upload-keystore.jks"
70
+
71
+ - name: Write key.properties
72
+ env:
73
+ STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
74
+ KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
75
+ run: |
76
+ echo "storeFile=${{ github.workspace }}/upload-keystore.jks" > android/key.properties
77
+ echo "storePassword=$STORE_PASSWORD" >> android/key.properties
78
+ echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
79
+ echo "keyPassword=$KEY_PASSWORD" >> android/key.properties
80
+
81
+ - name: Build AAB
82
+ run: |
83
+ flutter build appbundle --release \
84
+ --build-number=${{ github.run_number }} \
85
+ --dart-define-from-file=app-config.json \
86
+ --obfuscate --split-debug-info=build/debug-info/android/
87
+
88
+ - uses: actions/upload-artifact@v7
89
+ with:
90
+ name: app-release-${{ github.run_number }}
91
+ path: app/build/app/outputs/bundle/release/app-release.aab
92
+ retention-days: 30
93
+
94
+ - uses: actions/upload-artifact@v7
95
+ with:
96
+ name: debug-info-android-${{ github.run_number }}
97
+ path: app/build/debug-info/android/
98
+ retention-days: 30
99
+
100
+ # `environment: play-store` is not decoration: it is where a required
101
+ # reviewer would be attached if this ever wants one, matching how
102
+ # eigen-flutter's own release.yml gates its real pub.dev publish job with
103
+ # `environment: npm`.
104
+ deploy:
105
+ name: Deploy to Google Play
106
+ needs: build
107
+ runs-on: ubuntu-latest
108
+ environment: play-store
109
+ permissions:
110
+ contents: read
111
+ defaults:
112
+ run:
113
+ working-directory: app
114
+ steps:
115
+ - uses: actions/checkout@v7
116
+
117
+ - uses: ruby/setup-ruby@v1
118
+ with:
119
+ bundler-cache: true
120
+ working-directory: app
121
+
122
+ - uses: actions/download-artifact@v8
123
+ with:
124
+ name: app-release-${{ github.run_number }}
125
+ path: app/build/app/outputs/bundle/release/
126
+
127
+ - name: Deploy to internal track
128
+ env:
129
+ SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
130
+ run: bundle exec fastlane android internal
@@ -0,0 +1 @@
1
+ 24
@@ -102,3 +102,89 @@ deletion. Copy `server/.dev.vars.example` to `server/.dev.vars`, then fill its
102
102
  service-account email and private key. The Firebase project ID and web origin
103
103
  remain in `server/wrangler.jsonc`. For deployment, store the two credentials
104
104
  with `wrangler secret put`; never commit the downloaded JSON or `.dev.vars`.
105
+
106
+ ## Store release
107
+
108
+ CI is **not** generated by default, because the release pipeline needs an
109
+ upload keystore and a Play service account that a new project does not have
110
+ — it would fail on every push until both exist. Add it when you are ready to
111
+ ship, from the project root:
112
+
113
+ ```sh
114
+ npx create-eigen-game add ci
115
+ ```
116
+
117
+ That writes `.github/workflows/checks.yml` and `release.yml`. (Passing
118
+ `--ci` at scaffold time does the same thing up front.)
119
+
120
+ `release.yml` builds a signed, obfuscated Android App
121
+ Bundle on every push to `main` and uploads it to the Play Store internal
122
+ track via `app/fastlane`. `checks.yml` is the PR gate it
123
+ calls first — it needs no secrets, since `app-config.json`,
124
+ `firebase_options.dart` and `google-services.json` are public app
125
+ identifiers, not credentials. Commit them for real once
126
+ `{{PACKAGE_MANAGER}} run firebase:configure` has been run, rather than
127
+ reconstructing them from secrets on every build. The full reasoning is at
128
+ https://eigeninteractive.com/docs/ship-it/configure and
129
+ https://eigeninteractive.com/docs/ship-it/store-release.
130
+
131
+ Only five things are genuinely secret, so only five repository secrets are
132
+ required:
133
+
134
+ | Secret | Used for |
135
+ |---|---|
136
+ | `KEYSTORE_BASE64` | The upload keystore, base64-encoded (`base64 -i upload-keystore.jks`) |
137
+ | `KEYSTORE_PASSWORD`, `KEY_ALIAS`, `KEY_PASSWORD` | Signing `android/key.properties` |
138
+ | `GOOGLE_PLAY_JSON_KEY` | A Play service account JSON with the *Release* permission, for `fastlane`'s `upload_to_play_store` |
139
+
140
+ Per-app one-time setup: create an upload keystore, add the four signing
141
+ secrets above, create the Google Play service account and add its JSON, and
142
+ do the **first** Play Console upload by hand to create the listing —
143
+ everything after that flows through `fastlane android internal` /
144
+ `fastlane android production`. The Fastlane lanes upload the binary only
145
+ (`skip_upload_metadata`/`images`/`screenshots`), so the store listing stays
146
+ hand-maintained in the Console.
147
+
148
+ ### Replace the placeholder branding
149
+
150
+ A fresh scaffold is already branded — with the **Eigen mark**, not this game's.
151
+ The launcher icon, splash and web icons were generated at scaffold time so the
152
+ app looks finished from the first run, but shipping them to a store would put
153
+ the engine's mark on your listing. Replace them before a first release:
154
+
155
+ | Asset | What it is |
156
+ |---|---|
157
+ | `assets/icon/icon.png` | 1024×1024 launcher icon, full bleed |
158
+ | `assets/icon/icon_foreground.png` | 1024×1024 Android adaptive foreground, transparent, artwork inside the centre 66% |
159
+ | `assets/icon/splash.png` / `splash_dark.png` | Splash artwork for light and dark |
160
+
161
+ The notification icon is the exception: `eigen_flutter` ships a default
162
+ `ic_notification` drawable, so nothing is missing and nothing breaks if you
163
+ leave it. To use your own, add
164
+ `android/app/src/main/res/drawable/ic_notification.xml` — a monochrome
165
+ `<vector>`, not an SVG. Android resource merging gives your app precedence
166
+ over the package, so there is nothing to delete or override explicitly.
167
+
168
+ Also update the palette in `pubspec.yaml` — `adaptive_icon_background` and the
169
+ `flutter_native_splash` colours are the Eigen ink/paper pair, which will not
170
+ suit different artwork. Then regenerate:
171
+
172
+ ```bash
173
+ dart run flutter_launcher_icons
174
+ dart run flutter_native_splash:create
175
+ ```
176
+
177
+ The full checklist is at https://eigeninteractive.com/docs/ship-it/branding.
178
+
179
+ Versioning uses [`cider`](https://pub.dev/packages/cider) against
180
+ `app/pubspec.yaml` and `app/CHANGELOG.md`:
181
+
182
+ ```bash
183
+ dart pub global activate cider # once, from app/
184
+ cider log added "New feature description"
185
+ cider bump patch # 1.0.0 -> 1.0.1
186
+ ```
187
+
188
+ The build number is set from `github.run_number` in CI and is not managed by
189
+ hand. Worker deployment is unaffected by any of this — it stays the manual
190
+ `{{PACKAGE_MANAGER}} run deploy` described above.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts","../src/engine-range.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\";\nimport { ENGINE_PACKAGE, engineRange } from \"./engine-range.js\";\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 /**\n * Runs the bootstrap subprocesses. A seam: the tests substitute a recorder,\n * and `scripts/scaffold-e2e.mjs` wraps it to point the generated server at\n * this workspace's engine rather than npm's copy.\n */\n run?: (command: string, args: string[], cwd: string) => void;\n}\n\nexport interface ScaffoldResult {\n root: string;\n name: string;\n}\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst templatesRoot = resolve(packageRoot, \"templates\");\n\n/**\n * The engine range emitted into a scaffolded project's package.json.\n *\n * Read from this package's `@eigeninteractive/server` devDependency rather than\n * from its own version — see `engine-range.ts` for why that is the version the\n * templates are known to compile against, and what it replaced.\n */\nconst engineVersion = engineRange((JSON.parse(readFileSync(resolve(packageRoot, \"package.json\"), \"utf8\")) as { devDependencies?: Record<string, string> }).devDependencies?.[ENGINE_PACKAGE], () => {\n // Only reached in this workspace, where the manifest still says\n // `workspace:*`. A published tarball has no sibling here and never asks.\n const sibling = resolve(packageRoot, \"../server/package.json\");\n return existsSync(sibling) ? (JSON.parse(readFileSync(sibling, \"utf8\")) as { version: string }).version : undefined;\n});\n\n/**\n * The Flutter client range installed into the app half.\n *\n * A stated version, not a derived or resolved one. The Dart templates import\n * `package:eigen_flutter/eigen_flutter.dart` and\n * `package:eigen_flutter/testing/twin_fixtures.dart` and are written against a\n * specific Dart API — and `eigen_flutter` lives in another repository, versioned\n * independently, so nothing in this repository can compute which release that\n * is. Only compiling a scaffolded app establishes it, which is what the\n * `scaffold` job in checks.yml does on every change.\n *\n * This briefly resolved from pub.dev instead: \"the newest `eigen_flutter` whose\n * own `eigen_api` constraint targets the engine line being scaffolded\". That\n * predicate is wrong. The `eigen_api` constraint describes the WIRE the shell\n * speaks, not the Dart API these templates call, and the two move\n * independently — a future `eigen_flutter` may legitimately keep `eigen_api:\n * ^0.2.0` while renaming everything the templates touch. It would have been\n * selected, and the generated app would not compile.\n *\n * A caret RANGE rather than an exact version, so it still improves without a\n * republish: `flutter pub add eigen_flutter@^0.2.0` already picks the newest\n * 0.2.x at scaffold time, which is the part the pub.dev lookup was duplicating.\n * What it deliberately cannot do is cross to 0.3.x — the one move that needs a\n * human to confirm the templates still compile.\n *\n * Staleness is therefore a failing check rather than a broken scaffold: this is\n * only ever a release behind, never wrong.\n */\nconst flutterClientVersion = \"^0.2.0\";\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 [\"{{ENGINE_VERSION}}\", engineVersion],\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\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@${flutterClientVersion}`, \"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","/**\n * Where the engine range emitted into a scaffolded project comes from.\n *\n * Its own module because the two branches below execute in different worlds —\n * one only inside this repository, the other only inside a published tarball —\n * and a function whose published-only path never runs in CI is a function that\n * is not tested. `src/index.ts` uses it; the package `exports` map does not\n * expose it, so it is internal despite being importable by the tests.\n */\n\n/** The dependency whose version the scaffolder emits into the generated server. */\nexport const ENGINE_PACKAGE = \"@eigeninteractive/server\";\n\n/**\n * The engine range for a scaffolded project, from this package's own\n * `@eigeninteractive/server` devDependency.\n *\n * That number is not maintained by anyone: it is the version CI compiled the\n * templates against, because `pnpm -r typecheck` runs\n * `tsc -p templates/worker/tsconfig.json` inside this workspace. Emitting\n * anything else would mean shipping templates paired with an engine no build\n * ever saw.\n *\n * It replaced a derivation from this package's OWN version, which was correct\n * only while `create-eigen-game` sat in the `fixed` changesets group — and that\n * membership is what forced an engine-wide release for every scaffolder-only\n * change. Reading the dependency keeps the guarantee and drops the coupling.\n *\n * Two branches, because the same manifest reads differently in the two places\n * this runs:\n *\n * - **Published tarball** — pnpm rewrites `workspace:*` to the exact version\n * while packing, so the manifest already states it (`\"0.2.0\"`), and no\n * sibling package exists on disk to consult.\n * - **This workspace** — the manifest still says `workspace:*`, which names no\n * version, so the sibling package is read directly.\n *\n * @param declared the raw `@eigeninteractive/server` devDependency value\n * @param readSiblingVersion consulted only for the `workspace:` protocol\n */\nexport function engineRange(declared: string | undefined, readSiblingVersion: () => string | undefined): string {\n if (!declared) {\n throw new Error(`create-eigen-game has no ${ENGINE_PACKAGE} devDependency, which is where the engine range it emits comes from`);\n }\n\n const version = declared.startsWith(\"workspace:\") ? readSiblingVersion() : declared;\n\n if (!version) {\n throw new Error(`create-eigen-game declares ${ENGINE_PACKAGE} as \"${declared}\" but the workspace package could not be read, so there is no engine version to emit`);\n }\n\n // pnpm packs `workspace:*` as a bare version, which is what makes the caret\n // below correct. Asserting it means a change in that behaviour fails here\n // rather than emitting something like `^^0.3.0` into a real project.\n if (!/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {\n throw new Error(`expected an exact ${ENGINE_PACKAGE} version to build a range from, got \"${version}\"`);\n }\n\n return `^${version}`;\n}\n"],"mappings":"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","names":[]}