create-eigen-game 0.7.2 → 0.8.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.
@@ -21,6 +21,31 @@ function engineRange(declared, readSiblingVersion) {
21
21
  }
22
22
 
23
23
  // src/index.ts
24
+ function normaliseTerminalWidth(stream) {
25
+ if (stream.isTTY === true && (stream.columns ?? 0) < 1) stream.columns = 80;
26
+ }
27
+ var plainReporter = {
28
+ step: (_label, body) => body(),
29
+ handOver: (_label, body) => body(),
30
+ emit: (output) => process.stdout.write(output),
31
+ warn: (message) => console.warn(`create-eigen-game: ${message}`),
32
+ interactive: true
33
+ };
34
+ function capturingRunner(reporter) {
35
+ return (command, args, cwd) => {
36
+ if (reporter.interactive) {
37
+ execFileSync(command, args, { cwd, stdio: "inherit" });
38
+ return;
39
+ }
40
+ try {
41
+ reporter.emit(execFileSync(command, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }));
42
+ } catch (error) {
43
+ const { stdout, stderr } = error;
44
+ reporter.emit(`${stdout ?? ""}${stderr ?? ""}`);
45
+ throw error;
46
+ }
47
+ };
48
+ }
24
49
  var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
25
50
  var templatesRoot = resolve(packageRoot, "templates");
26
51
  var engineVersion = engineRange(JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")).devDependencies?.[ENGINE_PACKAGE], () => {
@@ -35,6 +60,9 @@ var gameSlug = (value) => {
35
60
  return value;
36
61
  };
37
62
  var dartName = (value) => value.replaceAll("-", "_");
63
+ function applicationId(directory, org) {
64
+ return `${org?.trim() || "com.example"}.${dartName(gameSlug(basename(resolve(directory))))}`;
65
+ }
38
66
  var title = (value) => value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
39
67
  var identifier = (value) => {
40
68
  const pascal = value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join("");
@@ -214,7 +242,53 @@ function addContinuousIntegration(options) {
214
242
  renderTree(resolve(templatesRoot, "ci"), root, name, manager, packageId);
215
243
  return { root, files: [".github/workflows/checks.yml", ".github/workflows/release.yml"] };
216
244
  }
217
- function initialiseRepository(root, name, run) {
245
+ var probeCommand = (command, args) => {
246
+ try {
247
+ return { ok: true, stdout: execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }) };
248
+ } catch {
249
+ return { ok: false, stdout: "" };
250
+ }
251
+ };
252
+ var firebaseTools = {
253
+ flutterfire: "dart pub global activate flutterfire_cli",
254
+ firebase: "npm install -g firebase-tools"
255
+ };
256
+ function firebaseReadiness(probe = probeCommand) {
257
+ for (const [tool, fix] of Object.entries(firebaseTools)) {
258
+ if (!probe(tool, ["--version"]).ok) return { ready: false, reason: `\`${tool}\` is not installed`, fix };
259
+ }
260
+ const accounts = probe("firebase", ["login:list", "--json"]);
261
+ if (accounts.ok) {
262
+ try {
263
+ const { result } = JSON.parse(accounts.stdout);
264
+ if (Array.isArray(result) && result.length === 0) {
265
+ return { ready: false, reason: "no Google account is signed in to the Firebase CLI", fix: "firebase login" };
266
+ }
267
+ } catch {
268
+ }
269
+ }
270
+ return { ready: true };
271
+ }
272
+ function configureFirebase(appRoot, project, run, reporter) {
273
+ const placeholder = resolve(appRoot, "lib/firebase_options.dart");
274
+ const parked = `${placeholder}.placeholder`;
275
+ const parking = existsSync(placeholder);
276
+ if (parking) renameSync(placeholder, parked);
277
+ try {
278
+ reporter.step("Preparing the Firebase configurator", () => run("dart", ["run", "eigen_flutter:configure_firebase", "--help"], appRoot));
279
+ reporter.handOver("Configuring Firebase", () => run("dart", ["run", "eigen_flutter:configure_firebase", ...typeof project === "string" ? ["--project", project] : []], appRoot));
280
+ if (parking) rmSync(parked, { force: true });
281
+ return "configured";
282
+ } catch {
283
+ if (parking) {
284
+ if (existsSync(placeholder)) rmSync(parked, { force: true });
285
+ else renameSync(parked, placeholder);
286
+ }
287
+ reporter.warn("Could not configure Firebase. The project is complete \u2014 run `firebase:configure` yourself, then commit what it writes.");
288
+ return "failed";
289
+ }
290
+ }
291
+ function initialiseRepository(root, name, run, reporter) {
218
292
  try {
219
293
  execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root, stdio: "ignore" });
220
294
  return "existing";
@@ -224,13 +298,13 @@ function initialiseRepository(root, name, run) {
224
298
  run("git", ["init", "--quiet"], root);
225
299
  run("git", ["add", "--all"], root);
226
300
  } catch {
227
- console.warn("create-eigen-game: could not initialise a git repository. The project is complete \u2014 run `git init` yourself when ready.");
301
+ reporter.warn("Could not initialise a git repository. The project is complete \u2014 run `git init` yourself when ready.");
228
302
  return "failed";
229
303
  }
230
304
  try {
231
305
  run("git", ["commit", "--quiet", "--message", `Scaffold ${name}`], root);
232
306
  } catch {
233
- console.warn(`create-eigen-game: initialised a repository and staged the scaffold, but could not commit it. Set \`user.name\` and \`user.email\`, then \`git commit -m "Scaffold ${name}"\`.`);
307
+ reporter.warn(`Initialised a repository and staged the scaffold, but could not commit it. Set \`user.name\` and \`user.email\`, then \`git commit -m "Scaffold ${name}"\`.`);
234
308
  return "failed";
235
309
  }
236
310
  return "committed";
@@ -240,9 +314,10 @@ function scaffoldGame(options) {
240
314
  const name = gameSlug(basename(root));
241
315
  const manager = options.packageManager ?? detectPackageManager() ?? "pnpm";
242
316
  const org = options.org?.trim() || "com.example";
243
- const packageId = `${org}.${dartName(name)}`;
317
+ const packageId = applicationId(root, org);
244
318
  const bootstrap = options.bootstrap ?? true;
245
- const run = options.run ?? ((command, args, cwd) => execFileSync(command, args, { cwd, stdio: "inherit" }));
319
+ const reporter = options.reporter ?? plainReporter;
320
+ const run = options.run ?? capturingRunner(reporter);
246
321
  if (existsSync(root)) throw new Error(`target already exists: ${root}`);
247
322
  mkdirSync(dirname(root), { recursive: true });
248
323
  const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));
@@ -253,39 +328,55 @@ function scaffoldGame(options) {
253
328
  renderTree(resolve(templatesRoot, "project"), stagingRoot, name, manager, packageId);
254
329
  if (options.ci) renderTree(resolve(templatesRoot, "ci"), stagingRoot, name, manager, packageId);
255
330
  if (bootstrap) {
256
- run("flutter", ["create", "--empty", "--platforms", "android,web", "--project-name", dartName(name), "--org", org, appRoot], stagingRoot);
257
- enableAndroidCoreLibraryDesugaring(appRoot);
258
- enableAndroidReleaseSigning(appRoot);
331
+ reporter.step("Creating the Flutter app", () => {
332
+ run("flutter", ["create", "--empty", "--platforms", "android,web", "--project-name", dartName(name), "--org", org, appRoot], stagingRoot);
333
+ enableAndroidCoreLibraryDesugaring(appRoot);
334
+ enableAndroidReleaseSigning(appRoot);
335
+ });
259
336
  } else {
260
337
  mkdirSync(appRoot, { recursive: true });
261
338
  }
262
339
  renderTree(resolve(templatesRoot, "app-overlay"), appRoot, name, manager, packageId);
263
340
  if (bootstrap) {
264
- configureLauncherIconsAndSplash(appRoot);
265
- run("flutter", ["pub", "add", `eigen_flutter@${flutterClientVersion}`, "firebase_core@^4.9.0", "firebase_messaging@^16.2.2"], appRoot);
266
- run("flutter", ["pub", "add", "dev:flutter_launcher_icons", "dev:flutter_native_splash"], appRoot);
267
- run("dart", ["run", "flutter_launcher_icons"], appRoot);
268
- run("dart", ["run", "flutter_native_splash:create"], appRoot);
341
+ reporter.step("Adding the Flutter packages", () => {
342
+ configureLauncherIconsAndSplash(appRoot);
343
+ run("flutter", ["pub", "add", `eigen_flutter@${flutterClientVersion}`, "firebase_core@^4.9.0", "firebase_messaging@^16.2.2"], appRoot);
344
+ run("flutter", ["pub", "add", "dev:flutter_launcher_icons", "dev:flutter_native_splash"], appRoot);
345
+ });
346
+ reporter.step("Generating the icons and splash", () => {
347
+ run("dart", ["run", "flutter_launcher_icons"], appRoot);
348
+ run("dart", ["run", "flutter_native_splash:create"], appRoot);
349
+ });
269
350
  const [install, installArgs] = packageCommand(manager, "install");
270
- run(install, installArgs, serverRoot);
271
- run(install, installArgs, stagingRoot);
351
+ reporter.step("Installing the Worker packages", () => {
352
+ run(install, installArgs, serverRoot);
353
+ run(install, installArgs, stagingRoot);
354
+ });
272
355
  const [contract, contractArgs] = packageCommand(manager, "contract");
273
- run(contract, contractArgs, serverRoot);
274
- run("dart", ["run", "eigen_flutter:generate_payloads", "--contract", "../server/game-contract.json", "--output", "lib/game/generated/payloads.dart", "--fixtures-output", "test/fixtures"], appRoot);
356
+ reporter.step("Generating the contract and payloads", () => {
357
+ run(contract, contractArgs, serverRoot);
358
+ run("dart", ["run", "eigen_flutter:generate_payloads", "--contract", "../server/game-contract.json", "--output", "lib/game/generated/payloads.dart", "--fixtures-output", "test/fixtures"], appRoot);
359
+ });
275
360
  }
276
361
  renameSync(stagingRoot, root);
277
362
  } catch (error) {
278
363
  rmSync(stagingRoot, { recursive: true, force: true });
279
364
  throw error;
280
365
  }
281
- const git = options.git ?? bootstrap ? initialiseRepository(root, name, run) : "skipped";
282
- return { root, name, git };
366
+ const firebase = options.firebase ? configureFirebase(resolve(root, "app"), options.firebase, run, reporter) : "skipped";
367
+ const git = options.git ?? bootstrap ? initialiseRepository(root, name, run, reporter) : "skipped";
368
+ return { root, name, git, firebase };
283
369
  }
284
370
 
285
371
  export {
372
+ normaliseTerminalWidth,
373
+ plainReporter,
374
+ capturingRunner,
375
+ applicationId,
286
376
  detectPackageManager,
287
377
  decodeUtf8,
288
378
  addContinuousIntegration,
379
+ firebaseReadiness,
289
380
  scaffoldGame
290
381
  };
291
- //# sourceMappingURL=chunk-LGV7OJQY.js.map
382
+ //# sourceMappingURL=chunk-6WJG743G.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 type Runner = (command: string, args: string[], cwd: string) => void;\n\n/**\n * Where a scaffold says what it is doing, so that presentation belongs to the\n * caller and this module only has to name things.\n *\n * The default is the behaviour a library caller expects with no opinion:\n * subprocesses inherit the terminal and warnings go to `console.warn`. The CLI\n * substitutes one that captures each step's output and shows it only if the\n * step fails.\n */\nexport interface Reporter {\n /** Runs `body` as one named step. Its subprocesses can be captured. */\n step<T>(label: string, body: () => T): T;\n /**\n * Runs `body` as a step that owns the terminal, because something inside it\n * asks questions. FlutterFire is the only one.\n */\n handOver<T>(label: string, body: () => T): T;\n /** Output from a subprocess inside the current step. */\n emit(output: string): void;\n /** A failure the scaffold survives. */\n warn(message: string): void;\n /** Whether subprocesses must inherit the terminal rather than be captured. */\n readonly interactive: boolean;\n}\n\n/**\n * Gives a terminal that reports no width the width clack would have assumed.\n *\n * A pty can report itself as a TTY and set `columns` to 0 — `script` does, and\n * so do some container terminals. clack falls back to 80 for a stream with no\n * `columns` at all, but `0` is a number and satisfies that check, so the width\n * it draws its rules from goes negative and a scaffold dies partway through\n * with `RangeError: Invalid string length`. Supplying the default its check\n * misses is better than giving up the drawing.\n */\nexport function normaliseTerminalWidth(stream: { isTTY?: boolean; columns?: number }): void {\n if (stream.isTTY === true && (stream.columns ?? 0) < 1) stream.columns = 80;\n}\n\n/** Runs every step plainly, which is what a library caller gets by default. */\nexport const plainReporter: Reporter = {\n step: (_label, body) => body(),\n handOver: (_label, body) => body(),\n emit: (output) => process.stdout.write(output),\n warn: (message) => console.warn(`create-eigen-game: ${message}`),\n interactive: true,\n};\n\n/**\n * The runner a scaffold uses when the caller has not supplied one: it asks the\n * reporter whether the terminal is spoken for.\n *\n * `interactive` inherits, which is the only way FlutterFire can put a question\n * on screen and read the answer. Otherwise both streams are captured and\n * handed to the reporter — on success so it can discard them, and on failure\n * so the reason survives.\n */\nexport function capturingRunner(reporter: Reporter): Runner {\n return (command, args, cwd) => {\n if (reporter.interactive) {\n execFileSync(command, args, { cwd, stdio: \"inherit\" });\n return;\n }\n try {\n reporter.emit(execFileSync(command, args, { cwd, encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"pipe\"] }));\n } catch (error: unknown) {\n // Both streams, because which one a tool failed on is its own business:\n // pub explains itself on stdout, Gradle on stderr.\n const { stdout, stderr } = error as { stdout?: string; stderr?: string };\n reporter.emit(`${stdout ?? \"\"}${stderr ?? \"\"}`);\n throw error;\n }\n };\n}\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 * Initialise a repository and commit the scaffold. Defaults to `bootstrap`,\n * because the commit is only worth having once the generated files exist.\n */\n git?: boolean;\n /**\n * Configure Firebase before the first commit, by running the Flutter\n * client's `configure_firebase`. A string names the project to use; `true`\n * lets FlutterFire ask, which is also where a project can be created.\n *\n * Explicit here, though the CLI turns it on by default: this is the one step\n * that reaches outside the destination directory, and a library caller\n * should say so rather than discover it. {@link firebaseReadiness} is how\n * the CLI decides.\n */\n firebase?: boolean | string;\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?: Runner;\n /**\n * Where the scaffold says what it is doing. See {@link Reporter}; the\n * default runs every step plainly.\n */\n reporter?: Reporter;\n}\n\n/**\n * What became of the repository, so the CLI's closing summary can say rather\n * than guess. `failed` has already warned by the time it is returned, and\n * carries no advice of its own.\n */\nexport type GitOutcome = \"committed\" | \"existing\" | \"skipped\" | \"failed\";\n\nexport interface ScaffoldResult {\n root: string;\n name: string;\n git: GitOutcome;\n /**\n * Whether Firebase was configured, so the summary can either stop naming the\n * step or spell it out. `failed` has already warned.\n */\n firebase: \"configured\" | \"failed\" | \"skipped\";\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\n/**\n * The Android `applicationId` — and iOS bundle id — a scaffold at `directory`\n * will produce, so the CLI can show it rather than describe it.\n *\n * `flutter create --org X --project-name Y` derives `X.Y`. The organization is\n * the *prefix*, not the whole identifier, which is the part worth seeing\n * before answering: Google Play makes it permanent at first upload.\n */\nexport function applicationId(directory: string, org?: string): string {\n return `${org?.trim() || \"com.example\"}.${dartName(gameSlug(basename(resolve(directory))))}`;\n}\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 EigenInteractive palette (ink #1B1E24, paper #F4F1EA) because the\n// shipped placeholder art is the EigenInteractive 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 EigenInteractive 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\n/** Asks a command a question. `ok` is \"ran and succeeded\"; a tool that is not installed and one that fails are both `false`, because neither can be used. */\nexport type Probe = (command: string, args: string[]) => { ok: boolean; stdout: string };\n\nconst probeCommand: Probe = (command, args) => {\n try {\n return { ok: true, stdout: execFileSync(command, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] }) };\n } catch {\n return { ok: false, stdout: \"\" };\n }\n};\n\n/** What each Firebase tool needs, for a machine that has neither. */\nconst firebaseTools: Record<string, string> = {\n flutterfire: \"dart pub global activate flutterfire_cli\",\n firebase: \"npm install -g firebase-tools\",\n};\n\nexport type FirebaseReadiness = { ready: true } | { ready: false; reason: string; fix: string };\n\n/**\n * Whether this machine can configure Firebase, asked *before* the scaffold\n * rather than after it.\n *\n * `configure_firebase` runs the same three checks and is the authority on\n * them; this one exists only to move the answer earlier. Learning that\n * `flutterfire` is missing costs nothing here and costs two minutes of Flutter\n * and pub at the far end, which is where the step actually runs.\n *\n * The sign-in check fails open, for the same reason it does there: only an\n * answer that positively reports no accounts counts as \"no\", so a `firebase`\n * that errors or grows a different output shape is left to the command that\n * actually needs the credentials.\n */\nexport function firebaseReadiness(probe: Probe = probeCommand): FirebaseReadiness {\n for (const [tool, fix] of Object.entries(firebaseTools)) {\n if (!probe(tool, [\"--version\"]).ok) return { ready: false, reason: `\\`${tool}\\` is not installed`, fix };\n }\n\n const accounts = probe(\"firebase\", [\"login:list\", \"--json\"]);\n if (accounts.ok) {\n try {\n const { result } = JSON.parse(accounts.stdout) as { result?: unknown };\n if (Array.isArray(result) && result.length === 0) {\n return { ready: false, reason: \"no Google account is signed in to the Firebase CLI\", fix: \"firebase login\" };\n }\n } catch {\n // Unparseable is not evidence of anything.\n }\n }\n\n return { ready: true };\n}\n\n/**\n * Runs the Flutter client's `configure_firebase` against the generated app.\n *\n * The command owns everything about how Firebase is configured — which\n * platforms, which CLIs must exist, whether anyone is signed in, and what to\n * do when no project has been chosen yet. This only decides *when*: before the\n * scaffold commit, so a configured project is committed configured.\n *\n * Not fatal. Every failure here — a missing `flutterfire`, no Google login, a\n * cancelled picker — leaves a complete and usable project that is exactly what\n * a scaffold without `--firebase` produces, so it warns with the command to\n * re-run and lets the commit happen.\n */\nfunction configureFirebase(appRoot: string, project: boolean | string, run: Runner, reporter: Reporter): \"configured\" | \"failed\" {\n // FlutterFire asks before overwriting `firebase_options.dart`, and the\n // scaffold has just written one — a throwing placeholder whose entire\n // purpose is to be replaced by this step. Answering that is a question with\n // one right answer, so the placeholder is moved out of the way and the\n // question never gets asked.\n const placeholder = resolve(appRoot, \"lib/firebase_options.dart\");\n const parked = `${placeholder}.placeholder`;\n const parking = existsSync(placeholder);\n if (parking) renameSync(placeholder, parked);\n\n try {\n // `dart run` compiles the package executable on first use and says so.\n // Doing it against `--help`, which returns immediately, moves that noise\n // into a step whose output can be captured — the real run cannot be, since\n // FlutterFire prompts through it.\n reporter.step(\"Preparing the Firebase configurator\", () => run(\"dart\", [\"run\", \"eigen_flutter:configure_firebase\", \"--help\"], appRoot));\n reporter.handOver(\"Configuring Firebase\", () => run(\"dart\", [\"run\", \"eigen_flutter:configure_firebase\", ...(typeof project === \"string\" ? [\"--project\", project] : [])], appRoot));\n if (parking) rmSync(parked, { force: true });\n return \"configured\";\n } catch {\n // Back only if nothing took its place. FlutterFire writes\n // `firebase_options.dart` before the service worker configuration is\n // derived from it, so a late failure leaves a real one that is worth more\n // than the placeholder.\n if (parking) {\n if (existsSync(placeholder)) rmSync(parked, { force: true });\n else renameSync(parked, placeholder);\n }\n reporter.warn(\"Could not configure Firebase. The project is complete — run `firebase:configure` yourself, then commit what it writes.\");\n return \"failed\";\n }\n}\n\n/**\n * Commits the scaffold, so the first `git diff` is the first game change.\n *\n * This matters more here than in a scaffolder that only writes source. The\n * bootstrap runs `flutter_launcher_icons` and `flutter_native_splash`, which\n * write generated-but-committed files across `android/`, `web/` and\n * `assets/`; without a baseline the first branding change is indistinguishable\n * from the ninety files the scaffolder happened to produce.\n *\n * Nothing here is fatal. The project is complete and valid before this runs,\n * so a missing `git`, or the unconfigured `user.email` that a fresh CI image\n * has, warns and leaves the tree alone rather than discarding a scaffold that\n * took two minutes of Flutter and pub to produce.\n */\nfunction initialiseRepository(root: string, name: string, run: Runner, reporter: Reporter): GitOutcome {\n // Scaffolding inside an existing checkout — a monorepo, or a repository\n // created ahead of time — is a legitimate thing to do, and a nested\n // repository there is silent breakage: the outer `git add` records a gitlink\n // and the app's files never leave the machine. Asked directly rather than\n // through `run`, because it is a question, not a step.\n try {\n execFileSync(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], { cwd: root, stdio: \"ignore\" });\n return \"existing\";\n } catch {\n // Not inside a work tree, which is the case this function is for.\n }\n\n try {\n run(\"git\", [\"init\", \"--quiet\"], root);\n run(\"git\", [\"add\", \"--all\"], root);\n } catch {\n reporter.warn(\"Could not initialise a git repository. The project is complete — run `git init` yourself when ready.\");\n return \"failed\";\n }\n\n try {\n run(\"git\", [\"commit\", \"--quiet\", \"--message\", `Scaffold ${name}`], root);\n } catch {\n reporter.warn(`Initialised a repository and staged the scaffold, but could not commit it. Set \\`user.name\\` and \\`user.email\\`, then \\`git commit -m \"Scaffold ${name}\"\\`.`);\n return \"failed\";\n }\n\n return \"committed\";\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 // Fastlane's `Appfile` names the same package the build actually produces,\n // and the CLI shows the same one before asking.\n const packageId = applicationId(root, org);\n const bootstrap = options.bootstrap ?? true;\n const reporter = options.reporter ?? plainReporter;\n const run = options.run ?? capturingRunner(reporter);\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 reporter.step(\"Creating the Flutter app\", () => {\n run(\"flutter\", [\"create\", \"--empty\", \"--platforms\", \"android,web\", \"--project-name\", dartName(name), \"--org\", org, appRoot], stagingRoot);\n enableAndroidCoreLibraryDesugaring(appRoot);\n enableAndroidReleaseSigning(appRoot);\n });\n } else {\n mkdirSync(appRoot, { recursive: true });\n }\n renderTree(resolve(templatesRoot, \"app-overlay\"), appRoot, name, manager, packageId);\n\n if (bootstrap) {\n reporter.step(\"Adding the Flutter packages\", () => {\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 });\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 reporter.step(\"Generating the icons and splash\", () => {\n run(\"dart\", [\"run\", \"flutter_launcher_icons\"], appRoot);\n run(\"dart\", [\"run\", \"flutter_native_splash:create\"], appRoot);\n });\n const [install, installArgs] = packageCommand(manager, \"install\");\n reporter.step(\"Installing the Worker packages\", () => {\n run(install, installArgs, serverRoot);\n // The root holds Biome, which lints and formats both the Worker and\n // the repository's own JSON. Installed after the server so a failure\n // here costs the cheaper of the two.\n run(install, installArgs, stagingRoot);\n });\n const [contract, contractArgs] = packageCommand(manager, \"contract\");\n reporter.step(\"Generating the contract and payloads\", () => {\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\n renameSync(stagingRoot, root);\n } catch (error) {\n rmSync(stagingRoot, { recursive: true, force: true });\n throw error;\n }\n\n // Both steps below are deliberately outside the staging guard: the project\n // is published by this point, and neither a Firebase project that could not\n // be configured nor a repository that failed to initialise is a reason to\n // delete two minutes of Flutter and pub.\n //\n // Firebase first, so what it writes — `firebase.json`,\n // `android/app/google-services.json`, the real `firebase_options.dart` and\n // `web/firebase-config.js` in place of the throwing placeholders, and\n // FlutterFire's two Gradle edits — lands in the scaffold commit rather than\n // arriving as the project's first diff.\n const firebase = options.firebase ? configureFirebase(resolve(root, \"app\"), options.firebase, run, reporter) : \"skipped\";\n const git = (options.git ?? bootstrap) ? initialiseRepository(root, name, run, reporter) : \"skipped\";\n\n return { root, name, git, firebase };\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,13 +1,72 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  addContinuousIntegration,
4
+ applicationId,
4
5
  detectPackageManager,
6
+ firebaseReadiness,
7
+ normaliseTerminalWidth,
8
+ plainReporter,
5
9
  scaffoldGame
6
- } from "./chunk-LGV7OJQY.js";
10
+ } from "./chunk-6WJG743G.js";
7
11
 
8
12
  // src/cli.ts
9
- import { createInterface } from "readline/promises";
13
+ import { basename, resolve } from "path";
10
14
  import { parseArgs } from "util";
15
+ import { cancel, intro, isCI, isTTY, log, note, outro, taskLog } from "@clack/prompts";
16
+ import color2 from "picocolors";
17
+
18
+ // src/prompt.ts
19
+ import { TextPrompt } from "@clack/core";
20
+ import { confirm, isCancel, S_BAR, S_BAR_END, symbol } from "@clack/prompts";
21
+ import color from "picocolors";
22
+ var ORG = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$/;
23
+ var DEFAULT_ORG = "com.example";
24
+ var bar = color.gray(S_BAR);
25
+ function repeatsGameName(org, game) {
26
+ return org.split(".").at(-1) === game;
27
+ }
28
+ function withoutGameName(org) {
29
+ const shorter = org.split(".").slice(0, -1).join(".");
30
+ return ORG.test(shorter) ? shorter : void 0;
31
+ }
32
+ async function askForOrg(game, registering, io = {}) {
33
+ const input = io.input ?? process.stdin;
34
+ const output = io.output ?? process.stdout;
35
+ const suffix = color.dim(`.${game}`);
36
+ const prompt = new TextPrompt({
37
+ input,
38
+ output,
39
+ // An empty answer is the default, not a mistake.
40
+ validate: (value) => value === void 0 || value === "" || ORG.test(value) ? void 0 : "Two or more dot-separated segments, as in com.example or dev.yourname.games.",
41
+ render() {
42
+ const typed = String(this.value ?? "");
43
+ const shown = typed === "" ? `${color.dim(DEFAULT_ORG)}${suffix}` : `${this.userInputWithCursor}${suffix}`;
44
+ const problem = this.state === "error" ? `
45
+ ${bar} ${color.yellow(this.error ?? "")}` : "";
46
+ return [`${bar}`, `${symbol(this.state)} Organization in reverse domain notation`, `${bar} ${shown}${problem}`, color.gray(S_BAR_END)].join("\n");
47
+ }
48
+ });
49
+ output.write(`${bar} ${color.dim("Prefixes the Android applicationId, which Google Play makes permanent at first upload.")}
50
+ `);
51
+ if (registering) output.write(`${bar} ${color.dim("Also the Android app registered in the Firebase project you pick next.")}
52
+ `);
53
+ const answer = await prompt.prompt();
54
+ if (isCancel(answer)) return null;
55
+ const org = String(answer).trim();
56
+ if (org === "" || org === DEFAULT_ORG) return void 0;
57
+ const shorter = repeatsGameName(org, game) ? withoutGameName(org) : void 0;
58
+ if (shorter !== void 0) {
59
+ const shorten = await confirm({
60
+ message: `That gives ${color.yellow(`${org}.${game}`)} \u2014 the game name twice. Use ${color.green(shorter)} instead?`,
61
+ initialValue: true,
62
+ input,
63
+ output
64
+ });
65
+ if (isCancel(shorten)) return null;
66
+ if (shorten) return shorter;
67
+ }
68
+ return org;
69
+ }
11
70
 
12
71
  // src/summary.ts
13
72
  function columns(rows) {
@@ -16,11 +75,11 @@ function columns(rows) {
16
75
  }
17
76
  function summarise(result, manager, ci) {
18
77
  const run = manager === "npm" ? "npm run" : "pnpm";
19
- const lines = ["", `${result.name} is ready, at ${result.root}`];
20
- if (result.git === "committed") lines.push(`Committed as "Scaffold ${result.name}".`);
21
- if (result.git === "existing") lines.push("Created inside a repository you already had, so nothing was committed.");
22
- lines.push(
23
- "",
78
+ const status = [];
79
+ if (result.git === "committed") status.push(`Committed as "Scaffold ${result.name}"`);
80
+ if (result.git === "existing") status.push("Created inside a repository you already had, so nothing was committed");
81
+ if (result.firebase === "configured") status.push(result.git === "committed" ? "Firebase is configured, and everything it generated is in that commit" : "Firebase is configured");
82
+ const next = [
24
83
  "Write the game",
25
84
  ...columns([
26
85
  ["server/src/module/v1.ts", "the authoritative rules"],
@@ -31,13 +90,17 @@ function summarise(result, manager, ci) {
31
90
  ...columns([
32
91
  [`cd server && ${run} test:watch`, "rules and twin fixtures"],
33
92
  [`cd server && ${run} dev`, "the Worker, on http://localhost:8787"],
34
- [`${run} contract`, "regenerate after a schema change"]
35
- ]),
36
- ""
37
- );
38
- if (!ci) lines.push("No CI workflows yet: `create-eigen-game add ci` writes them when shipping is the next step.");
39
- lines.push("Docs: https://eigeninteractive.com");
40
- return lines.join("\n");
93
+ [`${run} contract`, "regenerate after a schema change"],
94
+ // Named here because it is the one step the game itself cannot do
95
+ // without: rules, fixtures and the Worker all run unconfigured, and the
96
+ // app throws `Firebase is not configured` at launch until this has run
97
+ // once.
98
+ ...result.firebase === "configured" ? [] : [[`${run} firebase:configure`, "connect Firebase before the app runs"]]
99
+ ])
100
+ ].join("\n");
101
+ const footnotes = ["Docs: https://eigeninteractive.com"];
102
+ if (!ci) footnotes.unshift("No CI workflows yet: `create-eigen-game add ci` writes them when shipping is the next step.");
103
+ return { status, next, footnotes, headline: `${result.name} is ready, at ${result.root}` };
41
104
  }
42
105
 
43
106
  // src/cli.ts
@@ -53,8 +116,17 @@ Options:
53
116
  --ci Also emit the GitHub Actions workflows (off by
54
117
  default: release.yml needs an upload keystore and a
55
118
  Play service account, so it fails until both exist)
56
- --org <reverse-domain> Android/iOS organization (default: com.example).
57
- Asked for interactively when omitted.
119
+ --org <reverse-domain> Android/iOS organization (default: com.example). The
120
+ applicationId is this plus the game name, as in
121
+ com.example.my_game. Asked for when omitted.
122
+ --no-firebase Do not configure Firebase. Firebase is configured
123
+ before the first commit by default, whenever the two
124
+ CLIs are installed and signed in; this skips it and
125
+ prints the command to run later
126
+ --firebase-project <id>
127
+ The Firebase project to configure against, instead of
128
+ being asked. Works without a terminal, which is how
129
+ CI drives the step
58
130
  --no-git Do not initialise a repository or commit the scaffold
59
131
  --package-manager <pm> npm or pnpm (defaults to the invoking package manager)
60
132
  -h, --help Show this help
@@ -63,6 +135,27 @@ Commands:
63
135
  add ci [directory] Add the workflows to an existing project, for when
64
136
  you are ready to ship rather than at scaffold time.
65
137
  Defaults to the current directory.`;
138
+ normaliseTerminalWidth(process.stdout);
139
+ var decorated = isTTY(process.stdout) && !isCI();
140
+ var ui = decorated ? {
141
+ open: () => intro(color2.inverse(" create-eigen-game ")),
142
+ info: (message) => log.info(message),
143
+ warn: (message) => log.warn(message),
144
+ success: (message) => log.success(message),
145
+ block: (body) => note(body, "Next"),
146
+ close: (message) => outro(color2.bold(message)),
147
+ stop: (message) => cancel(message)
148
+ } : {
149
+ open: () => console.log("\ncreate-eigen-game\n"),
150
+ info: (message) => console.log(message),
151
+ warn: (message) => console.warn(message),
152
+ success: (message) => console.log(message),
153
+ block: (body) => console.log(`
154
+ ${body}
155
+ `),
156
+ close: (message) => console.log(message),
157
+ stop: (message) => console.error(message)
158
+ };
66
159
  function resolveManager(requested) {
67
160
  if (requested === void 0) return void 0;
68
161
  if (requested !== "npm" && requested !== "pnpm") {
@@ -70,30 +163,72 @@ function resolveManager(requested) {
70
163
  }
71
164
  return requested;
72
165
  }
73
- var ORG = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$/;
166
+ function chooseFirebase(disabled, project) {
167
+ if (project !== void 0) {
168
+ if (project.trim() === "") throw new Error("--firebase-project needs a project id");
169
+ return project.trim();
170
+ }
171
+ if (disabled === true) return false;
172
+ if (!process.stdin.isTTY) {
173
+ ui.info("No terminal to choose a Firebase project on, so Firebase is left unconfigured.\nName one with --firebase-project <id>, or configure it later.");
174
+ return false;
175
+ }
176
+ const readiness = firebaseReadiness();
177
+ if (readiness.ready) return true;
178
+ ui.warn(`Skipping Firebase: ${readiness.reason}.
179
+
180
+ ${color2.cyan(readiness.fix)}
181
+
182
+ The scaffold does not need it, and ends by naming the command that finishes the job.`);
183
+ return false;
184
+ }
74
185
  function resolveOrg(value) {
75
186
  if (value.trim() === "") return void 0;
76
187
  if (!ORG.test(value)) {
77
188
  throw new Error(`invalid organization: ${value}
78
189
 
79
- Use reverse domain notation \u2014 two or more dot-separated segments of letters, digits and underscores, each starting with a letter, as in com.example or dev.yourname.games.`);
190
+ Use reverse domain notation \u2014 two or more dot-separated segments of letters, digits and underscores, each starting with a letter, as in ${DEFAULT_ORG} or dev.yourname.games.`);
80
191
  }
81
192
  return value;
82
193
  }
83
- async function askForOrg() {
84
- if (!process.stdin.isTTY) return void 0;
85
- const rl = createInterface({ input: process.stdin, output: process.stdout });
86
- try {
87
- console.log("\nThe organization becomes the Android applicationId, which Google Play makes permanent at first upload.");
88
- for (; ; ) {
89
- const answer = (await rl.question("Organization in reverse domain notation [com.example]: ")).trim();
90
- if (answer === "") return void 0;
91
- if (ORG.test(answer)) return answer;
92
- console.log("Two or more dot-separated segments, as in com.example or dev.yourname.games.\n");
194
+ function clackReporter() {
195
+ if (!decorated) return plainReporter;
196
+ let task;
197
+ let interactive = false;
198
+ return {
199
+ step(label, body) {
200
+ task = taskLog({ title: label });
201
+ try {
202
+ const value = body();
203
+ task.success(label);
204
+ return value;
205
+ } catch (error) {
206
+ task.error(label);
207
+ throw error;
208
+ } finally {
209
+ task = void 0;
210
+ }
211
+ },
212
+ handOver(label, body) {
213
+ log.step(`${label} ${color2.dim("\u2014 FlutterFire takes over here")}`);
214
+ interactive = true;
215
+ try {
216
+ return body();
217
+ } finally {
218
+ interactive = false;
219
+ }
220
+ },
221
+ emit(output) {
222
+ const text = output.trimEnd();
223
+ if (text !== "") task?.message(text);
224
+ },
225
+ warn(message) {
226
+ ui.warn(message);
227
+ },
228
+ get interactive() {
229
+ return interactive;
93
230
  }
94
- } finally {
95
- rl.close();
96
- }
231
+ };
97
232
  }
98
233
  async function main(args) {
99
234
  const { values, positionals } = parseArgs({
@@ -104,8 +239,10 @@ async function main(args) {
104
239
  help: { type: "boolean", short: "h" },
105
240
  ci: { type: "boolean" },
106
241
  org: { type: "string" },
107
- // Node's `parseArgs` has no boolean negation, so the flag is declared
108
- // under the name it is typed with rather than as `git: false`.
242
+ // Declared under the name it is typed with, as `no-git` is: `parseArgs`
243
+ // has no boolean negation.
244
+ "no-firebase": { type: "boolean" },
245
+ "firebase-project": { type: "string" },
109
246
  "no-git": { type: "boolean" },
110
247
  "package-manager": { type: "string" }
111
248
  }
@@ -138,22 +275,45 @@ ${help}`);
138
275
 
139
276
  ${help}`);
140
277
  }
141
- const org = values.org === void 0 ? await askForOrg() : resolveOrg(values.org);
278
+ const directory = positionals[0];
142
279
  const manager = requestedManager ?? detectPackageManager() ?? "pnpm";
280
+ ui.open();
281
+ const game = basename(resolve(directory));
282
+ applicationId(directory);
283
+ const firebase = chooseFirebase(values["no-firebase"], values["firebase-project"]);
284
+ let org;
285
+ if (values.org === void 0 && process.stdin.isTTY) {
286
+ const answer = await askForOrg(game.replaceAll("-", "_"), firebase !== false);
287
+ if (answer === null) {
288
+ ui.stop("Nothing was written.");
289
+ process.exitCode = 130;
290
+ return;
291
+ }
292
+ org = answer;
293
+ } else if (values.org !== void 0) {
294
+ org = resolveOrg(values.org);
295
+ }
296
+ ui.success(`applicationId ${color2.bold(applicationId(directory, org))}`);
143
297
  const result = scaffoldGame({
144
- directory: positionals[0],
298
+ directory,
145
299
  org,
146
300
  packageManager: manager,
147
301
  ci: values.ci,
148
- git: !values["no-git"]
302
+ git: !values["no-git"],
303
+ firebase,
304
+ reporter: clackReporter()
149
305
  });
150
- console.log(summarise(result, manager, values.ci === true));
306
+ const { status, next, footnotes, headline } = summarise(result, manager, values.ci === true);
307
+ for (const line of status) ui.success(line);
308
+ ui.block(next);
309
+ for (const line of footnotes) ui.info(line);
310
+ ui.close(headline);
151
311
  }
152
312
  try {
153
313
  await main(process.argv.slice(2));
154
314
  } catch (error) {
155
315
  const message = error instanceof Error ? error.message : String(error);
156
- console.error(`create-eigen-game: ${message}`);
316
+ ui.stop(`create-eigen-game: ${message}`);
157
317
  process.exitCode = 1;
158
318
  }
159
319
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/summary.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { createInterface } from \"node:readline/promises\";\nimport { parseArgs } from \"node:util\";\nimport { addContinuousIntegration, detectPackageManager, type PackageManager, scaffoldGame } from \"./index.js\";\nimport { summarise } from \"./summary.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 EigenInteractive Cloudflare Worker and\nFlutter app, and commits it. The destination basename is the lowercase\nkebab-case game slug; the CLI derives the display name, Dart package name, and\nDart/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 Asked for interactively when omitted.\n --no-git Do not initialise a repository or commit the scaffold\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\n/**\n * Two or more dot-separated Java identifiers. `flutter create --org` accepts\n * anything and defers the complaint to Gradle, which reports it as a manifest\n * error in a generated file — so `com.example-games` costs a full scaffold and\n * a first build before anyone learns that a hyphen is not legal in a package\n * segment.\n */\nconst ORG = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$/;\n\n/** `--org \"\"` falls through to the default rather than failing, matching what an empty answer to the prompt does. */\nfunction resolveOrg(value: string): string | undefined {\n if (value.trim() === \"\") return undefined;\n if (!ORG.test(value)) {\n throw new Error(`invalid organization: ${value}\\n\\nUse reverse domain notation — two or more dot-separated segments of letters, digits and underscores, each starting with a letter, as in com.example or dev.yourname.games.`);\n }\n return value;\n}\n\n/**\n * The one value worth interrupting for. It becomes the Android\n * `applicationId`, which Google Play treats as the permanent identity of the\n * app: it cannot be changed after the first upload, and a game published as\n * `com.example.my-game` has to be relisted under a new identity, losing its\n * install base and reviews. Everything else the scaffolder decides is a\n * find-and-replace away.\n *\n * Skipped when there is no terminal, so `--org` remains the whole interface\n * for CI, `scripts/scaffold-e2e.mjs` and anything piping input.\n */\nasync function askForOrg(): Promise<string | undefined> {\n if (!process.stdin.isTTY) return undefined;\n\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n console.log(\"\\nThe organization becomes the Android applicationId, which Google Play makes permanent at first upload.\");\n for (;;) {\n const answer = (await rl.question(\"Organization in reverse domain notation [com.example]: \")).trim();\n if (answer === \"\") return undefined;\n if (ORG.test(answer)) return answer;\n console.log(\"Two or more dot-separated segments, as in com.example or dev.yourname.games.\\n\");\n }\n } finally {\n rl.close();\n }\n}\n\nasync function main(args: string[]): Promise<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 // Node's `parseArgs` has no boolean negation, so the flag is declared\n // under the name it is typed with rather than as `git: false`.\n \"no-git\": { type: \"boolean\" },\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 org = values.org === undefined ? await askForOrg() : resolveOrg(values.org);\n const manager = requestedManager ?? detectPackageManager() ?? \"pnpm\";\n\n const result = scaffoldGame({\n directory: positionals[0],\n org,\n packageManager: manager,\n ci: values.ci,\n git: !values[\"no-git\"],\n });\n console.log(summarise(result, manager, values.ci === true));\n}\n\ntry {\n await 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","/**\n * The closing summary, and the only output of a scaffold anyone reads.\n *\n * A run prints several screens that belong to other tools — pnpm's `dlx`\n * install, `flutter create`, pub, two icon generators, the server install — so\n * the single line this used to end on was indistinguishable from the noise\n * above it. Where the project went, what to edit, and what to run are worth\n * restating at the bottom.\n *\n * Its own module so it can be asserted: `cli.ts` is the bin entry and runs\n * `main` on import, which makes anything defined there untestable. The package\n * `exports` map does not expose this, so it is internal despite being\n * importable by the tests.\n */\nimport type { PackageManager, ScaffoldResult } from \"./index.js\";\n\n/** Two-column rows, aligned to the widest left cell. */\nfunction columns(rows: [string, string][]): string[] {\n const width = Math.max(...rows.map(([left]) => left.length));\n return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);\n}\n\nexport function summarise(result: ScaffoldResult, manager: PackageManager, ci: boolean): string {\n const run = manager === \"npm\" ? \"npm run\" : \"pnpm\";\n const lines = [\"\", `${result.name} is ready, at ${result.root}`];\n\n // Only the outcomes that leave the reader something to know. `failed` has\n // already warned, in more detail than a summary line could carry.\n if (result.git === \"committed\") lines.push(`Committed as \"Scaffold ${result.name}\".`);\n if (result.git === \"existing\") lines.push(\"Created inside a repository you already had, so nothing was committed.\");\n\n lines.push(\n \"\",\n \"Write the game\",\n ...columns([\n [\"server/src/module/v1.ts\", \"the authoritative rules\"],\n [\"app/lib/game/v1/rules.dart\", \"the presentation twin\"],\n ]),\n \"\",\n `Then, from ${result.name}/`,\n ...columns([\n [`cd server && ${run} test:watch`, \"rules and twin fixtures\"],\n [`cd server && ${run} dev`, \"the Worker, on http://localhost:8787\"],\n [`${run} contract`, \"regenerate after a schema change\"],\n ]),\n \"\",\n );\n\n if (!ci) lines.push(\"No CI workflows yet: `create-eigen-game add ci` writes them when shipping is the next step.\");\n lines.push(\"Docs: https://eigeninteractive.com\");\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["result"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/prompt.ts","../src/summary.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { basename, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { cancel, intro, isCI, isTTY, log, note, outro, taskLog } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport { addContinuousIntegration, applicationId, detectPackageManager, firebaseReadiness, normaliseTerminalWidth, type PackageManager, plainReporter, type Reporter, scaffoldGame } from \"./index.js\";\nimport { askForOrg, DEFAULT_ORG, ORG } from \"./prompt.js\";\nimport { summarise } from \"./summary.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 EigenInteractive Cloudflare Worker and\nFlutter app, and commits it. The destination basename is the lowercase\nkebab-case game slug; the CLI derives the display name, Dart package name, and\nDart/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). The\n applicationId is this plus the game name, as in\n com.example.my_game. Asked for when omitted.\n --no-firebase Do not configure Firebase. Firebase is configured\n before the first commit by default, whenever the two\n CLIs are installed and signed in; this skips it and\n prints the command to run later\n --firebase-project <id>\n The Firebase project to configure against, instead of\n being asked. Works without a terminal, which is how\n CI drives the step\n --no-git Do not initialise a repository or commit the scaffold\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\nnormaliseTerminalWidth(process.stdout);\n\n/**\n * Whether to draw, using clack's own predicates rather than a hand-rolled\n * `isTTY` — the same pair `taskLog` consults internally. A pipe has nowhere to\n * put a redraw, and a CI log is read after the fact, in full, by someone who\n * wants every line a tool printed.\n */\nconst decorated = isTTY(process.stdout) && !isCI();\n\n/**\n * The CLI's voice, in whichever of the two registers this terminal supports.\n *\n * Every message goes through here so the undecorated path cannot drift into\n * being the untested one.\n */\nconst ui = decorated\n ? {\n open: () => intro(color.inverse(\" create-eigen-game \")),\n info: (message: string) => log.info(message),\n warn: (message: string) => log.warn(message),\n success: (message: string) => log.success(message),\n block: (body: string) => note(body, \"Next\"),\n close: (message: string) => outro(color.bold(message)),\n stop: (message: string) => cancel(message),\n }\n : {\n open: () => console.log(\"\\ncreate-eigen-game\\n\"),\n info: (message: string) => console.log(message),\n warn: (message: string) => console.warn(message),\n success: (message: string) => console.log(message),\n block: (body: string) => console.log(`\\n${body}\\n`),\n close: (message: string) => console.log(message),\n stop: (message: string) => console.error(message),\n };\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\n/**\n * Whether this run configures Firebase, and against which project.\n *\n * On by default, because a scaffold that stops short of it is not a runnable\n * app: `firebase_options.dart` is a throwing placeholder until this has run,\n * so the first `flutter run` ends at `Firebase is not configured`. Doing it\n * before the scaffold commit also keeps the six files it writes out of the\n * project's first diff.\n *\n * Never at the cost of the scaffold, though. Everything that would make the\n * step fail — no CLIs, no login, no terminal to answer the project prompt on —\n * turns it off and says so, here, before the two minutes of Flutter and pub\n * rather than after them. What is left is exactly a `--no-firebase` scaffold,\n * and the summary ends by naming the command that finishes the job.\n */\nfunction chooseFirebase(disabled: boolean | undefined, project: string | undefined): boolean | string {\n // An explicitly named project settles it: FlutterFire has nothing to ask, so\n // this is also the form that works on a machine with no terminal.\n if (project !== undefined) {\n if (project.trim() === \"\") throw new Error(\"--firebase-project needs a project id\");\n return project.trim();\n }\n if (disabled === true) return false;\n\n if (!process.stdin.isTTY) {\n ui.info(\"No terminal to choose a Firebase project on, so Firebase is left unconfigured.\\nName one with --firebase-project <id>, or configure it later.\");\n return false;\n }\n\n const readiness = firebaseReadiness();\n if (readiness.ready) return true;\n\n ui.warn(`Skipping Firebase: ${readiness.reason}.\\n\\n ${color.cyan(readiness.fix)}\\n\\nThe scaffold does not need it, and ends by naming the command that finishes the job.`);\n return false;\n}\n\n/** `--org \"\"` falls through to the default rather than failing, matching what an empty answer to the prompt does. */\nfunction resolveOrg(value: string): string | undefined {\n if (value.trim() === \"\") return undefined;\n if (!ORG.test(value)) {\n throw new Error(`invalid organization: ${value}\\n\\nUse reverse domain notation — two or more dot-separated segments of letters, digits and underscores, each starting with a letter, as in ${DEFAULT_ORG} or dev.yourname.games.`);\n }\n return value;\n}\n\n/**\n * Shows each step running, and keeps its output only when it fails.\n *\n * Every tool a scaffold drives has plenty to say — pub resolving 179\n * dependencies, two icon generators, a package manager — and none of it is\n * read while it scrolls past. `taskLog` is exactly that bargain: the stream is\n * visible while the step runs, cleared when it succeeds, and left on screen\n * when it does not, so a failure is still debuggable from what is in the\n * terminal.\n *\n * `handOver` is the exception, and the reason `interactive` exists at all.\n * FlutterFire asks which Firebase project to use, so that step needs the\n * terminal rather than a captured pipe.\n */\nfunction clackReporter(): Reporter {\n // `taskLog` redraws by clearing lines, which is an escape-sequence storm in\n // anything that is not a terminal — a CI log, a pipe, `scaffold-e2e.mjs`.\n // There, the honest thing is the plain reporter: every tool's output, in\n // full, which is what a log gets read for anyway.\n if (!decorated) return plainReporter;\n\n let task: ReturnType<typeof taskLog> | undefined;\n let interactive = false;\n\n return {\n step(label, body) {\n task = taskLog({ title: label });\n try {\n const value = body();\n task.success(label);\n return value;\n } catch (error) {\n task.error(label);\n throw error;\n } finally {\n task = undefined;\n }\n },\n handOver(label, body) {\n log.step(`${label} ${color.dim(\"— FlutterFire takes over here\")}`);\n interactive = true;\n try {\n return body();\n } finally {\n interactive = false;\n }\n },\n emit(output) {\n const text = output.trimEnd();\n if (text !== \"\") task?.message(text);\n },\n warn(message) {\n ui.warn(message);\n },\n get interactive() {\n return interactive;\n },\n };\n}\n\nasync function main(args: string[]): Promise<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 // Declared under the name it is typed with, as `no-git` is: `parseArgs`\n // has no boolean negation.\n \"no-firebase\": { type: \"boolean\" },\n \"firebase-project\": { type: \"string\" },\n \"no-git\": { type: \"boolean\" },\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 directory = positionals[0];\n const manager = requestedManager ?? detectPackageManager() ?? \"pnpm\";\n\n ui.open();\n\n // Both decisions about what this run will do, settled before any of it\n // starts. `applicationId` validates the slug on the way, so a destination\n // that cannot be a game name fails here rather than after the first prompt.\n const game = basename(resolve(directory));\n applicationId(directory);\n const firebase = chooseFirebase(values[\"no-firebase\"], values[\"firebase-project\"]);\n\n let org: string | undefined;\n if (values.org === undefined && process.stdin.isTTY) {\n const answer = await askForOrg(game.replaceAll(\"-\", \"_\"), firebase !== false);\n if (answer === null) {\n ui.stop(\"Nothing was written.\");\n process.exitCode = 130;\n return;\n }\n org = answer;\n } else if (values.org !== undefined) {\n org = resolveOrg(values.org);\n }\n\n ui.success(`applicationId ${color.bold(applicationId(directory, org))}`);\n\n const result = scaffoldGame({\n directory,\n org,\n packageManager: manager,\n ci: values.ci,\n git: !values[\"no-git\"],\n firebase,\n reporter: clackReporter(),\n });\n\n const { status, next, footnotes, headline } = summarise(result, manager, values.ci === true);\n for (const line of status) ui.success(line);\n ui.block(next);\n for (const line of footnotes) ui.info(line);\n ui.close(headline);\n}\n\ntry {\n await main(process.argv.slice(2));\n} catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n ui.stop(`create-eigen-game: ${message}`);\n process.exitCode = 1;\n}\n","/**\n * The organization prompt, which is the one question this CLI asks and the one\n * answer that is expensive to get wrong.\n *\n * Its own module because it drops to `@clack/core` rather than using\n * `@clack/prompts`' `text`. The reason is the suffix: `flutter create --org X\n * --project-name Y` derives `X.Y`, so the organization is a *prefix*, and the\n * reliable way to say so is to render the rest of the identifier, dimmed,\n * while it is being typed. `text` has no hook for that; a custom `TextPrompt`\n * render returns a string, so it does.\n *\n * Everything else comes from `@clack/prompts` rather than being drawn here.\n * Its symbols are exported for exactly this, and they are not constants: each\n * falls back to ASCII on a terminal that cannot show the box-drawing\n * characters, which a hand-typed `│` cannot do.\n */\nimport type { Readable, Writable } from \"node:stream\";\nimport { TextPrompt } from \"@clack/core\";\nimport { confirm, isCancel, S_BAR, S_BAR_END, symbol } from \"@clack/prompts\";\nimport color from \"picocolors\";\n\n/**\n * Two or more dot-separated Java identifiers. `flutter create --org` accepts\n * anything and defers the complaint to Gradle, which reports it as a manifest\n * error in a generated file — so `com.example-games` costs a full scaffold and\n * a first build before anyone learns that a hyphen is not legal in a package\n * segment.\n */\nexport const ORG = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$/;\n\n/** The default, which an empty answer accepts. */\nexport const DEFAULT_ORG = \"com.example\";\n\nconst bar = color.gray(S_BAR);\n\n/** A `com.acme.chess` answer to a game called `chess`, which yields `com.acme.chess.chess`. */\nexport function repeatsGameName(org: string, game: string): boolean {\n return org.split(\".\").at(-1) === game;\n}\n\n/** The organization implied by an answer that already ends in the game name. */\nexport function withoutGameName(org: string): string | undefined {\n const shorter = org.split(\".\").slice(0, -1).join(\".\");\n return ORG.test(shorter) ? shorter : undefined;\n}\n\n/**\n * Asks, with the game name rendered dimmed after the cursor so the shape of\n * the answer is visible while it is being given.\n *\n * Returns the organization, `undefined` for the default, or `null` when\n * cancelled.\n *\n * `input` and `output` default to this process's, and are parameters for the\n * same reason every clack prompt takes them: a prompt is otherwise the one\n * part of a CLI that cannot be driven by a test.\n */\nexport async function askForOrg(game: string, registering: boolean, io: { input?: Readable; output?: Writable } = {}): Promise<string | undefined | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n const suffix = color.dim(`.${game}`);\n const prompt = new TextPrompt({\n input,\n output,\n // An empty answer is the default, not a mistake.\n validate: (value) => (value === undefined || value === \"\" || ORG.test(value) ? undefined : \"Two or more dot-separated segments, as in com.example or dev.yourname.games.\"),\n render() {\n const typed = String(this.value ?? \"\");\n const shown = typed === \"\" ? `${color.dim(DEFAULT_ORG)}${suffix}` : `${this.userInputWithCursor}${suffix}`;\n const problem = this.state === \"error\" ? `\\n${bar} ${color.yellow(this.error ?? \"\")}` : \"\";\n return [`${bar}`, `${symbol(this.state)} Organization in reverse domain notation`, `${bar} ${shown}${problem}`, color.gray(S_BAR_END)].join(\"\\n\");\n },\n });\n\n // The two things that make this answer permanent, said before it is given\n // rather than after: Google Play freezes the identifier at the first upload,\n // and FlutterFire matches an existing Android app on exactly this string, so\n // it also decides whether the run adopts an app the chosen Firebase project\n // already has or registers a new one.\n output.write(`${bar} ${color.dim(\"Prefixes the Android applicationId, which Google Play makes permanent at first upload.\")}\\n`);\n if (registering) output.write(`${bar} ${color.dim(\"Also the Android app registered in the Firebase project you pick next.\")}\\n`);\n\n const answer = await prompt.prompt();\n if (isCancel(answer)) return null;\n\n const org = String(answer).trim();\n if (org === \"\" || org === DEFAULT_ORG) return undefined;\n\n // The mistake the dimmed suffix is there to prevent, caught for the person\n // who pasted an identifier rather than typing one and never watched it grow.\n const shorter = repeatsGameName(org, game) ? withoutGameName(org) : undefined;\n if (shorter !== undefined) {\n const shorten = await confirm({\n message: `That gives ${color.yellow(`${org}.${game}`)} — the game name twice. Use ${color.green(shorter)} instead?`,\n initialValue: true,\n input,\n output,\n });\n if (isCancel(shorten)) return null;\n if (shorten) return shorter;\n }\n\n return org;\n}\n","/**\n * The closing summary, and the only output of a scaffold anyone reads.\n *\n * A run drives several tools with plenty to say, so where the project went,\n * what to edit, and what to run are worth restating at the bottom. Returned in\n * pieces rather than as one block, because the CLI renders each in a different\n * shape: the outcomes as status lines, the commands as a boxed note, the\n * destination as the closing line.\n *\n * Its own module so it can be asserted: `cli.ts` is the bin entry and runs\n * `main` on import, which makes anything defined there untestable. The package\n * `exports` map does not expose this, so it is internal despite being\n * importable by the tests.\n */\nimport type { PackageManager, ScaffoldResult } from \"./index.js\";\n\nexport interface Summary {\n /** What became of the repository and of Firebase, when there is something to say. */\n status: string[];\n /** The commands worth having, as one block. */\n next: string;\n /** One-line asides, after the block. */\n footnotes: string[];\n /** The closing line. */\n headline: string;\n}\n\n/** Two-column rows, aligned to the widest left cell. */\nfunction columns(rows: [string, string][]): string[] {\n const width = Math.max(...rows.map(([left]) => left.length));\n return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);\n}\n\nexport function summarise(result: ScaffoldResult, manager: PackageManager, ci: boolean): Summary {\n const run = manager === \"npm\" ? \"npm run\" : \"pnpm\";\n const status: string[] = [];\n\n // Only the outcomes that leave the reader something to know. `failed` has\n // already warned, in more detail than a summary line could carry.\n if (result.git === \"committed\") status.push(`Committed as \"Scaffold ${result.name}\"`);\n if (result.git === \"existing\") status.push(\"Created inside a repository you already had, so nothing was committed\");\n // `failed` has warned already, and the step reappears below as the next\n // thing to run, which is the whole of the advice.\n if (result.firebase === \"configured\") status.push(result.git === \"committed\" ? \"Firebase is configured, and everything it generated is in that commit\" : \"Firebase is configured\");\n\n const next = [\n \"Write the game\",\n ...columns([\n [\"server/src/module/v1.ts\", \"the authoritative rules\"],\n [\"app/lib/game/v1/rules.dart\", \"the presentation twin\"],\n ]),\n \"\",\n `Then, from ${result.name}/`,\n ...columns([\n [`cd server && ${run} test:watch`, \"rules and twin fixtures\"],\n [`cd server && ${run} dev`, \"the Worker, on http://localhost:8787\"],\n [`${run} contract`, \"regenerate after a schema change\"],\n // Named here because it is the one step the game itself cannot do\n // without: rules, fixtures and the Worker all run unconfigured, and the\n // app throws `Firebase is not configured` at launch until this has run\n // once.\n ...(result.firebase === \"configured\" ? [] : [[`${run} firebase:configure`, \"connect Firebase before the app runs\"] as [string, string]]),\n ]),\n ].join(\"\\n\");\n\n const footnotes = [\"Docs: https://eigeninteractive.com\"];\n if (!ci) footnotes.unshift(\"No CI workflows yet: `create-eigen-game add ci` writes them when shipping is the next step.\");\n\n return { status, next, footnotes, headline: `${result.name} is ready, at ${result.root}` };\n}\n"],"mappings":"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","names":["color","color","result"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,55 @@
1
1
  type PackageManager = "npm" | "pnpm";
2
2
  type Runner = (command: string, args: string[], cwd: string) => void;
3
+ /**
4
+ * Where a scaffold says what it is doing, so that presentation belongs to the
5
+ * caller and this module only has to name things.
6
+ *
7
+ * The default is the behaviour a library caller expects with no opinion:
8
+ * subprocesses inherit the terminal and warnings go to `console.warn`. The CLI
9
+ * substitutes one that captures each step's output and shows it only if the
10
+ * step fails.
11
+ */
12
+ interface Reporter {
13
+ /** Runs `body` as one named step. Its subprocesses can be captured. */
14
+ step<T>(label: string, body: () => T): T;
15
+ /**
16
+ * Runs `body` as a step that owns the terminal, because something inside it
17
+ * asks questions. FlutterFire is the only one.
18
+ */
19
+ handOver<T>(label: string, body: () => T): T;
20
+ /** Output from a subprocess inside the current step. */
21
+ emit(output: string): void;
22
+ /** A failure the scaffold survives. */
23
+ warn(message: string): void;
24
+ /** Whether subprocesses must inherit the terminal rather than be captured. */
25
+ readonly interactive: boolean;
26
+ }
27
+ /**
28
+ * Gives a terminal that reports no width the width clack would have assumed.
29
+ *
30
+ * A pty can report itself as a TTY and set `columns` to 0 — `script` does, and
31
+ * so do some container terminals. clack falls back to 80 for a stream with no
32
+ * `columns` at all, but `0` is a number and satisfies that check, so the width
33
+ * it draws its rules from goes negative and a scaffold dies partway through
34
+ * with `RangeError: Invalid string length`. Supplying the default its check
35
+ * misses is better than giving up the drawing.
36
+ */
37
+ declare function normaliseTerminalWidth(stream: {
38
+ isTTY?: boolean;
39
+ columns?: number;
40
+ }): void;
41
+ /** Runs every step plainly, which is what a library caller gets by default. */
42
+ declare const plainReporter: Reporter;
43
+ /**
44
+ * The runner a scaffold uses when the caller has not supplied one: it asks the
45
+ * reporter whether the terminal is spoken for.
46
+ *
47
+ * `interactive` inherits, which is the only way FlutterFire can put a question
48
+ * on screen and read the answer. Otherwise both streams are captured and
49
+ * handed to the reporter — on success so it can discard them, and on failure
50
+ * so the reason survives.
51
+ */
52
+ declare function capturingRunner(reporter: Reporter): Runner;
3
53
  interface ScaffoldOptions {
4
54
  directory: string;
5
55
  org?: string;
@@ -21,12 +71,28 @@ interface ScaffoldOptions {
21
71
  * because the commit is only worth having once the generated files exist.
22
72
  */
23
73
  git?: boolean;
74
+ /**
75
+ * Configure Firebase before the first commit, by running the Flutter
76
+ * client's `configure_firebase`. A string names the project to use; `true`
77
+ * lets FlutterFire ask, which is also where a project can be created.
78
+ *
79
+ * Explicit here, though the CLI turns it on by default: this is the one step
80
+ * that reaches outside the destination directory, and a library caller
81
+ * should say so rather than discover it. {@link firebaseReadiness} is how
82
+ * the CLI decides.
83
+ */
84
+ firebase?: boolean | string;
24
85
  /**
25
86
  * Runs the bootstrap subprocesses. A seam: the tests substitute a recorder,
26
87
  * and `scripts/scaffold-e2e.mjs` wraps it to point the generated server at
27
88
  * this workspace's engine rather than npm's copy.
28
89
  */
29
90
  run?: Runner;
91
+ /**
92
+ * Where the scaffold says what it is doing. See {@link Reporter}; the
93
+ * default runs every step plainly.
94
+ */
95
+ reporter?: Reporter;
30
96
  }
31
97
  /**
32
98
  * What became of the repository, so the CLI's closing summary can say rather
@@ -38,7 +104,21 @@ interface ScaffoldResult {
38
104
  root: string;
39
105
  name: string;
40
106
  git: GitOutcome;
107
+ /**
108
+ * Whether Firebase was configured, so the summary can either stop naming the
109
+ * step or spell it out. `failed` has already warned.
110
+ */
111
+ firebase: "configured" | "failed" | "skipped";
41
112
  }
113
+ /**
114
+ * The Android `applicationId` — and iOS bundle id — a scaffold at `directory`
115
+ * will produce, so the CLI can show it rather than describe it.
116
+ *
117
+ * `flutter create --org X --project-name Y` derives `X.Y`. The organization is
118
+ * the *prefix*, not the whole identifier, which is the part worth seeing
119
+ * before answering: Google Play makes it permanent at first upload.
120
+ */
121
+ declare function applicationId(directory: string, org?: string): string;
42
122
  declare function detectPackageManager(userAgent?: string | undefined): PackageManager | undefined;
43
123
  declare function decodeUtf8(bytes: Buffer): string | undefined;
44
124
  interface AddContinuousIntegrationOptions {
@@ -57,6 +137,33 @@ declare function addContinuousIntegration(options: AddContinuousIntegrationOptio
57
137
  root: string;
58
138
  files: string[];
59
139
  };
140
+ /** Asks a command a question. `ok` is "ran and succeeded"; a tool that is not installed and one that fails are both `false`, because neither can be used. */
141
+ type Probe = (command: string, args: string[]) => {
142
+ ok: boolean;
143
+ stdout: string;
144
+ };
145
+ type FirebaseReadiness = {
146
+ ready: true;
147
+ } | {
148
+ ready: false;
149
+ reason: string;
150
+ fix: string;
151
+ };
152
+ /**
153
+ * Whether this machine can configure Firebase, asked *before* the scaffold
154
+ * rather than after it.
155
+ *
156
+ * `configure_firebase` runs the same three checks and is the authority on
157
+ * them; this one exists only to move the answer earlier. Learning that
158
+ * `flutterfire` is missing costs nothing here and costs two minutes of Flutter
159
+ * and pub at the far end, which is where the step actually runs.
160
+ *
161
+ * The sign-in check fails open, for the same reason it does there: only an
162
+ * answer that positively reports no accounts counts as "no", so a `firebase`
163
+ * that errors or grows a different output shape is left to the command that
164
+ * actually needs the credentials.
165
+ */
166
+ declare function firebaseReadiness(probe?: Probe): FirebaseReadiness;
60
167
  declare function scaffoldGame(options: ScaffoldOptions): ScaffoldResult;
61
168
 
62
- export { type AddContinuousIntegrationOptions, type GitOutcome, type PackageManager, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, decodeUtf8, detectPackageManager, scaffoldGame };
169
+ export { type AddContinuousIntegrationOptions, type FirebaseReadiness, type GitOutcome, type PackageManager, type Probe, type Reporter, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, applicationId, capturingRunner, decodeUtf8, detectPackageManager, firebaseReadiness, normaliseTerminalWidth, plainReporter, scaffoldGame };
package/dist/index.js CHANGED
@@ -1,13 +1,23 @@
1
1
  import {
2
2
  addContinuousIntegration,
3
+ applicationId,
4
+ capturingRunner,
3
5
  decodeUtf8,
4
6
  detectPackageManager,
7
+ firebaseReadiness,
8
+ normaliseTerminalWidth,
9
+ plainReporter,
5
10
  scaffoldGame
6
- } from "./chunk-LGV7OJQY.js";
11
+ } from "./chunk-6WJG743G.js";
7
12
  export {
8
13
  addContinuousIntegration,
14
+ applicationId,
15
+ capturingRunner,
9
16
  decodeUtf8,
10
17
  detectPackageManager,
18
+ firebaseReadiness,
19
+ normaliseTerminalWidth,
20
+ plainReporter,
11
21
  scaffoldGame
12
22
  };
13
23
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-eigen-game",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "description": "Scaffold an EigenInteractive game Worker and Flutter app without cloning the engine repositories.",
6
6
  "type": "module",
@@ -17,7 +17,11 @@
17
17
  "dist",
18
18
  "templates"
19
19
  ],
20
- "dependencies": {},
20
+ "dependencies": {
21
+ "@clack/core": "^1.4.3",
22
+ "@clack/prompts": "^1.7.0",
23
+ "picocolors": "^1.1.1"
24
+ },
21
25
  "devDependencies": {
22
26
  "@types/node": "^26.1.1",
23
27
  "tsup": "^8.5.1",
@@ -25,9 +29,9 @@
25
29
  "vitest": "^4.1.10",
26
30
  "wrangler": "^4.110.0",
27
31
  "zod": "^4.4.3",
28
- "@eigeninteractive/rules": "0.2.3",
29
- "@eigeninteractive/server": "0.2.3",
30
- "@eigeninteractive/testkit": "0.2.3"
32
+ "@eigeninteractive/rules": "0.2.4",
33
+ "@eigeninteractive/testkit": "0.2.4",
34
+ "@eigeninteractive/server": "0.2.4"
31
35
  },
32
36
  "engines": {
33
37
  "node": ">=22"
@@ -86,18 +86,47 @@ Configure Android, Flutter Web, and the messaging service worker together:
86
86
  {{PACKAGE_MANAGER}} run firebase:configure
87
87
  ```
88
88
 
89
- That drives two CLIs, which are separate global installs and neither of which
89
+ Scaffolding does this for you, so a project created with the tooling in place
90
+ is already configured and this is the command for changing it afterwards — a
91
+ different project, or picking up a configuration change. If it was skipped, the
92
+ app throws `Firebase is not configured` the moment it launches; rules, fixtures
93
+ and `wrangler dev` all run regardless.
94
+
95
+ It drives two CLIs, which are separate global installs and neither of which
90
96
  comes with Flutter or Node:
91
97
 
92
98
  ```sh
93
99
  npm install -g firebase-tools # the `firebase` CLI
94
100
  dart pub global activate flutterfire_cli # the `flutterfire` CLI
95
101
  export PATH="$PATH":"$HOME/.pub-cache/bin" # `activate` does not do this
102
+ firebase login # both CLIs share this
103
+ ```
104
+
105
+ A run with no project chosen yet asks which to use, and can create one. Name it
106
+ up front to skip the prompts, which is also how CI runs this:
107
+
108
+ ```sh
109
+ {{PACKAGE_MANAGER}} run firebase:configure -- --project my-project-id
96
110
  ```
97
111
 
98
- Then `firebase login`. Nothing before this point needs either tool — rules,
99
- fixtures and `wrangler dev` all run without them — but the app itself throws
100
- `Firebase is not configured` on launch until this has run once.
112
+ Later runs reuse the project recorded in `app/firebase.json`, so there is
113
+ nothing to pass again. Add `--help` for the rest.
114
+
115
+ It writes `app/firebase.json`, `app/android/app/google-services.json`,
116
+ `app/lib/firebase_options.dart` and `app/web/firebase-config.js` — the last two
117
+ replacing scaffolded placeholders that throw — and lets FlutterFire add the
118
+ Google Services plugin to the two Android Gradle files. **Commit all of it.**
119
+ Every one is a public app identifier rather than a credential, nothing here is
120
+ git-ignored, and an Android or web build fails without them. The Firebase
121
+ secrets are the Worker's service-account email and private key, which live in
122
+ `server/.dev.vars` and `wrangler secret put`, and never in `app/`.
123
+
124
+ Each run ends by naming the project and the two app IDs it configured against.
125
+ Worth reading when the project is one that already had apps in it: FlutterFire
126
+ matches an existing Android app on the `applicationId` and an existing Web app
127
+ on its display name, and adopts either without comment, so those IDs are the
128
+ only thing distinguishing "reused what was there" from "registered something
129
+ new".
101
130
 
102
131
  The command uses FlutterFire's selected Web app to generate both
103
132
  `lib/firebase_options.dart` and `web/firebase-config.js`; do not copy Firebase
@@ -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, 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 type Runner = (command: string, args: string[], cwd: string) => void;\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 * Initialise a repository and commit the scaffold. Defaults to `bootstrap`,\n * because the commit is only worth having once the generated files exist.\n */\n git?: 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?: Runner;\n}\n\n/**\n * What became of the repository, so the CLI's closing summary can say rather\n * than guess. `failed` has already warned by the time it is returned, and\n * carries no advice of its own.\n */\nexport type GitOutcome = \"committed\" | \"existing\" | \"skipped\" | \"failed\";\n\nexport interface ScaffoldResult {\n root: string;\n name: string;\n git: GitOutcome;\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 EigenInteractive palette (ink #1B1E24, paper #F4F1EA) because the\n// shipped placeholder art is the EigenInteractive 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 EigenInteractive 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\n/**\n * Commits the scaffold, so the first `git diff` is the first game change.\n *\n * This matters more here than in a scaffolder that only writes source. The\n * bootstrap runs `flutter_launcher_icons` and `flutter_native_splash`, which\n * write generated-but-committed files across `android/`, `web/` and\n * `assets/`; without a baseline the first branding change is indistinguishable\n * from the ninety files the scaffolder happened to produce.\n *\n * Nothing here is fatal. The project is complete and valid before this runs,\n * so a missing `git`, or the unconfigured `user.email` that a fresh CI image\n * has, warns and leaves the tree alone rather than discarding a scaffold that\n * took two minutes of Flutter and pub to produce.\n */\nfunction initialiseRepository(root: string, name: string, run: Runner): GitOutcome {\n // Scaffolding inside an existing checkout — a monorepo, or a repository\n // created ahead of time — is a legitimate thing to do, and a nested\n // repository there is silent breakage: the outer `git add` records a gitlink\n // and the app's files never leave the machine. Asked directly rather than\n // through `run`, because it is a question, not a step.\n try {\n execFileSync(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], { cwd: root, stdio: \"ignore\" });\n return \"existing\";\n } catch {\n // Not inside a work tree, which is the case this function is for.\n }\n\n try {\n run(\"git\", [\"init\", \"--quiet\"], root);\n run(\"git\", [\"add\", \"--all\"], root);\n } catch {\n console.warn(\"create-eigen-game: could not initialise a git repository. The project is complete — run `git init` yourself when ready.\");\n return \"failed\";\n }\n\n try {\n run(\"git\", [\"commit\", \"--quiet\", \"--message\", `Scaffold ${name}`], root);\n } catch {\n console.warn(`create-eigen-game: initialised a repository and staged the scaffold, but could not commit it. Set \\`user.name\\` and \\`user.email\\`, then \\`git commit -m \"Scaffold ${name}\"\\`.`);\n return \"failed\";\n }\n\n return \"committed\";\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 // The root holds Biome, which lints and formats both the Worker and the\n // repository's own JSON. Installed after the server so a failure here\n // costs the cheaper of the two.\n run(install, installArgs, stagingRoot);\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 } catch (error) {\n rmSync(stagingRoot, { recursive: true, force: true });\n throw error;\n }\n\n // Deliberately outside the staging guard: the project is published by this\n // point, and a repository that failed to initialise is not a reason to\n // delete it.\n const git = (options.git ?? bootstrap) ? initialiseRepository(root, name, run) : \"skipped\";\n\n return { root, name, git };\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":[]}