create-eigen-game 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-QI5NQSGJ.js → chunk-JEKP6QE4.js} +7 -5
- package/dist/chunk-JEKP6QE4.js.map +1 -0
- package/dist/cli.js +37 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/templates/project/README.md +14 -2
- package/templates/scaffold/project.gitignore +8 -0
- package/templates/worker/pnpm-workspace.yaml +17 -0
- package/dist/chunk-QI5NQSGJ.js.map +0 -1
|
@@ -217,7 +217,7 @@ function addContinuousIntegration(options) {
|
|
|
217
217
|
function initialiseRepository(root, name, run) {
|
|
218
218
|
try {
|
|
219
219
|
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root, stdio: "ignore" });
|
|
220
|
-
return;
|
|
220
|
+
return "existing";
|
|
221
221
|
} catch {
|
|
222
222
|
}
|
|
223
223
|
try {
|
|
@@ -225,13 +225,15 @@ function initialiseRepository(root, name, run) {
|
|
|
225
225
|
run("git", ["add", "--all"], root);
|
|
226
226
|
} catch {
|
|
227
227
|
console.warn("create-eigen-game: could not initialise a git repository. The project is complete \u2014 run `git init` yourself when ready.");
|
|
228
|
-
return;
|
|
228
|
+
return "failed";
|
|
229
229
|
}
|
|
230
230
|
try {
|
|
231
231
|
run("git", ["commit", "--quiet", "--message", `Scaffold ${name}`], root);
|
|
232
232
|
} catch {
|
|
233
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}"\`.`);
|
|
234
|
+
return "failed";
|
|
234
235
|
}
|
|
236
|
+
return "committed";
|
|
235
237
|
}
|
|
236
238
|
function scaffoldGame(options) {
|
|
237
239
|
const root = resolve(options.directory);
|
|
@@ -275,8 +277,8 @@ function scaffoldGame(options) {
|
|
|
275
277
|
rmSync(stagingRoot, { recursive: true, force: true });
|
|
276
278
|
throw error;
|
|
277
279
|
}
|
|
278
|
-
|
|
279
|
-
return { root, name };
|
|
280
|
+
const git = options.git ?? bootstrap ? initialiseRepository(root, name, run) : "skipped";
|
|
281
|
+
return { root, name, git };
|
|
280
282
|
}
|
|
281
283
|
|
|
282
284
|
export {
|
|
@@ -285,4 +287,4 @@ export {
|
|
|
285
287
|
addContinuousIntegration,
|
|
286
288
|
scaffoldGame
|
|
287
289
|
};
|
|
288
|
-
//# sourceMappingURL=chunk-
|
|
290
|
+
//# sourceMappingURL=chunk-JEKP6QE4.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\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 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":[]}
|
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,44 @@ import {
|
|
|
3
3
|
addContinuousIntegration,
|
|
4
4
|
detectPackageManager,
|
|
5
5
|
scaffoldGame
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-JEKP6QE4.js";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import { createInterface } from "readline/promises";
|
|
10
10
|
import { parseArgs } from "util";
|
|
11
|
+
|
|
12
|
+
// src/summary.ts
|
|
13
|
+
function columns(rows) {
|
|
14
|
+
const width = Math.max(...rows.map(([left]) => left.length));
|
|
15
|
+
return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);
|
|
16
|
+
}
|
|
17
|
+
function summarise(result, manager, ci) {
|
|
18
|
+
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
|
+
"",
|
|
24
|
+
"Write the game",
|
|
25
|
+
...columns([
|
|
26
|
+
["server/src/module/v1.ts", "the authoritative rules"],
|
|
27
|
+
["app/lib/game/v1/rules.dart", "the presentation twin"]
|
|
28
|
+
]),
|
|
29
|
+
"",
|
|
30
|
+
`Then, from ${result.name}/`,
|
|
31
|
+
...columns([
|
|
32
|
+
[`cd server && ${run} test:watch`, "rules and twin fixtures"],
|
|
33
|
+
[`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");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/cli.ts
|
|
11
44
|
var help = `Usage: create-eigen-game <game-slug> [options]
|
|
12
45
|
create-eigen-game add ci [directory] [options]
|
|
13
46
|
|
|
@@ -106,17 +139,15 @@ ${help}`);
|
|
|
106
139
|
${help}`);
|
|
107
140
|
}
|
|
108
141
|
const org = values.org === void 0 ? await askForOrg() : resolveOrg(values.org);
|
|
142
|
+
const manager = requestedManager ?? detectPackageManager() ?? "pnpm";
|
|
109
143
|
const result = scaffoldGame({
|
|
110
144
|
directory: positionals[0],
|
|
111
145
|
org,
|
|
112
|
-
packageManager:
|
|
146
|
+
packageManager: manager,
|
|
113
147
|
ci: values.ci,
|
|
114
148
|
git: !values["no-git"]
|
|
115
149
|
});
|
|
116
|
-
console.log(
|
|
117
|
-
if (!values.ci) {
|
|
118
|
-
console.log("No CI workflows were generated. Run `create-eigen-game add ci` inside the project when you want them.");
|
|
119
|
-
}
|
|
150
|
+
console.log(summarise(result, manager, values.ci === true));
|
|
120
151
|
}
|
|
121
152
|
try {
|
|
122
153
|
await main(process.argv.slice(2));
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { createInterface } from \"node:readline/promises\";\nimport { parseArgs } from \"node:util\";\nimport { addContinuousIntegration, detectPackageManager, type PackageManager, scaffoldGame } from \"./index.js\";\n\nconst help = `Usage: create-eigen-game <game-slug> [options]\n create-eigen-game add ci [directory] [options]\n\nCreates one repository containing an 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\n const result = scaffoldGame({\n directory: positionals[0],\n org,\n packageManager: requestedManager ?? detectPackageManager() ?? \"pnpm\",\n ci: values.ci,\n git: !values[\"no-git\"],\n });\n console.log(`Created ${result.name} in ${result.root}`);\n if (!values.ci) {\n console.log(\"No CI workflows were generated. Run `create-eigen-game add ci` inside the project when you want them.\");\n }\n}\n\ntry {\n 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"],"mappings":"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","names":["result"]}
|
|
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"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,9 +28,16 @@ interface ScaffoldOptions {
|
|
|
28
28
|
*/
|
|
29
29
|
run?: Runner;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* What became of the repository, so the CLI's closing summary can say rather
|
|
33
|
+
* than guess. `failed` has already warned by the time it is returned, and
|
|
34
|
+
* carries no advice of its own.
|
|
35
|
+
*/
|
|
36
|
+
type GitOutcome = "committed" | "existing" | "skipped" | "failed";
|
|
31
37
|
interface ScaffoldResult {
|
|
32
38
|
root: string;
|
|
33
39
|
name: string;
|
|
40
|
+
git: GitOutcome;
|
|
34
41
|
}
|
|
35
42
|
declare function detectPackageManager(userAgent?: string | undefined): PackageManager | undefined;
|
|
36
43
|
declare function decodeUtf8(bytes: Buffer): string | undefined;
|
|
@@ -52,4 +59,4 @@ declare function addContinuousIntegration(options: AddContinuousIntegrationOptio
|
|
|
52
59
|
};
|
|
53
60
|
declare function scaffoldGame(options: ScaffoldOptions): ScaffoldResult;
|
|
54
61
|
|
|
55
|
-
export { type AddContinuousIntegrationOptions, type PackageManager, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, decodeUtf8, detectPackageManager, scaffoldGame };
|
|
62
|
+
export { type AddContinuousIntegrationOptions, type GitOutcome, type PackageManager, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, decodeUtf8, detectPackageManager, scaffoldGame };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-eigen-game",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"wrangler": "^4.110.0",
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
28
|
"@eigeninteractive/rules": "0.2.1",
|
|
29
|
-
"@eigeninteractive/
|
|
30
|
-
"@eigeninteractive/
|
|
29
|
+
"@eigeninteractive/testkit": "0.2.1",
|
|
30
|
+
"@eigeninteractive/server": "0.2.1"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=22"
|
|
@@ -64,13 +64,25 @@ That builds Flutter into `server/public/` and deploys one Worker containing the
|
|
|
64
64
|
SPA and API. The app is `/`; invite and game URLs open the installed native app
|
|
65
65
|
or the browser SPA; the native install landing page is `/download`.
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
Android, Flutter Web, and the messaging service worker together:
|
|
67
|
+
Configure Android, Flutter Web, and the messaging service worker together:
|
|
69
68
|
|
|
70
69
|
```sh
|
|
71
70
|
{{PACKAGE_MANAGER}} run firebase:configure
|
|
72
71
|
```
|
|
73
72
|
|
|
73
|
+
That drives two CLIs, which are separate global installs and neither of which
|
|
74
|
+
comes with Flutter or Node:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npm install -g firebase-tools # the `firebase` CLI
|
|
78
|
+
dart pub global activate flutterfire_cli # the `flutterfire` CLI
|
|
79
|
+
export PATH="$PATH":"$HOME/.pub-cache/bin" # `activate` does not do this
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Then `firebase login`. Nothing before this point needs either tool — rules,
|
|
83
|
+
fixtures and `wrangler dev` all run without them — but the app itself throws
|
|
84
|
+
`Firebase is not configured` on launch until this has run once.
|
|
85
|
+
|
|
74
86
|
The command uses FlutterFire's selected Web app to generate both
|
|
75
87
|
`lib/firebase_options.dart` and `web/firebase-config.js`; do not copy Firebase
|
|
76
88
|
identifiers by hand. Put the public VAPID key in `app-config.json` separately —
|
|
@@ -9,3 +9,11 @@ server/public/*
|
|
|
9
9
|
*.log
|
|
10
10
|
|
|
11
11
|
app/pubspec_overrides.yaml
|
|
12
|
+
|
|
13
|
+
# The root package.json declares no dependencies — it only forwards scripts into
|
|
14
|
+
# server/ and app/. Running one of them still leaves a node_modules/ holding
|
|
15
|
+
# nothing but the package manager's own bookkeeping, and a lockfile describing
|
|
16
|
+
# no packages. Anchored, so server/pnpm-lock.yaml stays committed.
|
|
17
|
+
/node_modules/
|
|
18
|
+
/pnpm-lock.yaml
|
|
19
|
+
/package-lock.json
|
|
@@ -10,3 +10,20 @@
|
|
|
10
10
|
allowBuilds:
|
|
11
11
|
esbuild: true
|
|
12
12
|
workerd: true
|
|
13
|
+
|
|
14
|
+
# pnpm 11 refuses to install any version published less than a day ago
|
|
15
|
+
# (`minimumReleaseAge`, default 1440 minutes) — a quarantine against a
|
|
16
|
+
# compromised publish. The engine packages are subject to it like anything else.
|
|
17
|
+
#
|
|
18
|
+
# It is left commented out because nothing here needs it. A range that only a
|
|
19
|
+
# fresh release satisfies still resolves: `minimumReleaseAgeStrict` defaults to
|
|
20
|
+
# false unless `minimumReleaseAge` is set explicitly, so pnpm falls back to the
|
|
21
|
+
# new version rather than failing. What it costs is real — the exclusion drops
|
|
22
|
+
# the quarantine on packages that execute during your build.
|
|
23
|
+
#
|
|
24
|
+
# Uncomment it if you, or your organization, set `minimumReleaseAge` explicitly.
|
|
25
|
+
# Strict resolution is then on, and an install on the day an engine release
|
|
26
|
+
# ships fails outright instead of falling back.
|
|
27
|
+
#
|
|
28
|
+
# minimumReleaseAgeExclude:
|
|
29
|
+
# - "@eigeninteractive/*"
|
|
@@ -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\nexport interface ScaffoldResult {\n root: string;\n name: string;\n}\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst templatesRoot = resolve(packageRoot, \"templates\");\n\n/**\n * The engine range emitted into a scaffolded project's package.json.\n *\n * Read from this package's `@eigeninteractive/server` devDependency rather than\n * from its own version — see `engine-range.ts` for why that is the version the\n * templates are known to compile against, and what it replaced.\n */\nconst engineVersion = engineRange((JSON.parse(readFileSync(resolve(packageRoot, \"package.json\"), \"utf8\")) as { devDependencies?: Record<string, string> }).devDependencies?.[ENGINE_PACKAGE], () => {\n // Only reached in this workspace, where the manifest still says\n // `workspace:*`. A published tarball has no sibling here and never asks.\n const sibling = resolve(packageRoot, \"../server/package.json\");\n return existsSync(sibling) ? (JSON.parse(readFileSync(sibling, \"utf8\")) as { version: string }).version : undefined;\n});\n\n/**\n * The Flutter client range installed into the app half.\n *\n * A stated version, not a derived or resolved one. The Dart templates import\n * `package:eigen_flutter/eigen_flutter.dart` and\n * `package:eigen_flutter/testing/twin_fixtures.dart` and are written against a\n * specific Dart API — and `eigen_flutter` lives in another repository, versioned\n * independently, so nothing in this repository can compute which release that\n * is. Only compiling a scaffolded app establishes it, which is what the\n * `scaffold` job in checks.yml does on every change.\n *\n * This briefly resolved from pub.dev instead: \"the newest `eigen_flutter` whose\n * own `eigen_api` constraint targets the engine line being scaffolded\". That\n * predicate is wrong. The `eigen_api` constraint describes the WIRE the shell\n * speaks, not the Dart API these templates call, and the two move\n * independently — a future `eigen_flutter` may legitimately keep `eigen_api:\n * ^0.2.0` while renaming everything the templates touch. It would have been\n * selected, and the generated app would not compile.\n *\n * A caret RANGE rather than an exact version, so it still improves without a\n * republish: `flutter pub add eigen_flutter@^0.3.0` already picks the newest\n * 0.3.x at scaffold time, which is the part the pub.dev lookup was duplicating.\n * What it deliberately cannot do is cross to 0.4.x — the one move that needs a\n * human to confirm the templates still compile.\n *\n * Staleness is therefore a failing check rather than a broken scaffold: this is\n * only ever a release behind, never wrong.\n *\n * Raised to 0.3.0 because the notification icon moved into `eigen_flutter`'s\n * Android plugin: the scaffold stopped shipping `ic_notification.xml`, so a\n * game resolving 0.2.x would reference a drawable nothing provides.\n */\nconst flutterClientVersion = \"^0.3.0\";\n\nconst gameSlug = (value: string): string => {\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {\n throw new Error(\"destination directory name must be a lowercase kebab-case slug, for example my-game\");\n }\n return value;\n};\n\nconst dartName = (value: string): string => value.replaceAll(\"-\", \"_\");\n\nconst title = (value: string): string =>\n value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\" \");\n\nconst identifier = (value: string): string => {\n const pascal = value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\"\");\n return /^[0-9]/.test(pascal) ? `Game${pascal}` : pascal;\n};\n\nexport function detectPackageManager(userAgent = process.env.npm_config_user_agent): PackageManager | undefined {\n if (userAgent?.startsWith(\"pnpm/\")) return \"pnpm\";\n if (userAgent?.startsWith(\"npm/\")) return \"npm\";\n return undefined;\n}\n\n/**\n * Substitutes the scaffold's tokens, of which there are two deliberate kinds.\n *\n * `{{BRACED}}` for values that have no valid example — a package manager,\n * an engine version, an Android package id. They appear only in manifests,\n * READMEs, workflows and the `Appfile`: files nothing compiles.\n *\n * Bare words (`ExampleGame`, `example-game`, …) for names a real example game\n * genuinely has. This is what keeps `templates/worker` and the Dart templates\n * VALID, COMPILING SOURCE rather than text with holes in it, which the rest of\n * this package leans on hard: `typecheck` runs `tsc` over `templates/worker`,\n * the tests `import` its game module directly and build a real contract from\n * it, and the `scaffold` job compiles the Dart templates. Bracing these would\n * trade all of that for a marker.\n *\n * The cost, and the one rule for template authors: a template file cannot\n * contain the literal string `example-game`, `Example Game`, `ExampleGame` or\n * `example_game` and keep it — every occurrence is rewritten, in prose and in\n * code alike. Write around it, or brace a new token instead.\n *\n * Note this means the set of files carrying a token changes whenever someone\n * writes one into an existing file. Nothing needs to track that: `renderTree`\n * decides what to rewrite from the bytes, not from a list of paths.\n */\nfunction render(contents: string, name: string, manager: PackageManager, packageId: string): string {\n const replacements: ReadonlyArray<readonly [string, string]> = [\n [\"@game/example-game-server\", `@game/${name}-server`],\n [\"{{PACKAGE_MANAGER}}\", manager],\n [\"{{PACKAGE_ID}}\", packageId],\n [\"{{ENGINE_VERSION}}\", engineVersion],\n [\"ExampleGame\", identifier(name)],\n [\"Example Game\", title(name)],\n [\"example_game\", dartName(name)],\n [\"example-game\", name],\n ];\n return replacements.reduce((result, [from, to]) => result.replaceAll(from, to), contents);\n}\n\n/**\n * True when the bytes are valid UTF-8.\n *\n * This is not a guess at whether a file is \"binary\" — it is the precondition\n * of the operation. Token substitution is `String.replaceAll`, so a file that\n * cannot be decoded cannot take part, and one that can is safe to rewrite.\n *\n * Deliberately not an extension list. A DENY list (`.png`, `.jar`, …) fails\n * silently in the dangerous direction: the day someone adds a `.webp`, it is\n * corrupted with no error. An ALLOW list fails the safe way but fits this tree\n * badly — `Gemfile`, `Fastfile`, `.nvmrc`, `.fvmrc` and `.ruby-version` all\n * need rendering and none carry a usable extension, so it would have to be a\n * list of extensions *and* a list of bare filenames, both drifting.\n *\n * `.template` is not the signal either, despite the name: it marks files a\n * language server must not claim before they have an enclosing package. Five\n * of them contain no token at all, and nine token-bearing files are not\n * marked, so the two concepts are orthogonal here.\n *\n * `renderTree`'s test pins the resulting classification for the whole tree, so\n * this stays a reviewed decision rather than an invisible one.\n */\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\nexport function decodeUtf8(bytes: Buffer): string | undefined {\n try {\n return utf8.decode(bytes);\n } catch {\n return undefined;\n }\n}\n\nfunction renderTree(source: string, destination: string, name: string, manager: PackageManager, packageId: string): void {\n cpSync(source, destination, { recursive: true });\n // Walks the TEMPLATE, not the destination. The app overlay renders on top of\n // a `flutter create` result, so walking the destination meant rewriting\n // every file Flutter had just generated — including its binaries — for files\n // this scaffolder does not own and has no business touching.\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const templatePath = resolve(directory, entry.name);\n if (entry.isDirectory()) {\n visit(templatePath);\n continue;\n }\n if (!entry.isFile()) continue;\n\n const rendered = resolve(destination, relative(source, templatePath));\n const text = decodeUtf8(readFileSync(templatePath));\n // Undecodable files are already in place from the copy above, byte for\n // byte. Only decodable ones are rewritten.\n if (text !== undefined) {\n writeFileSync(rendered, render(text, name, manager, packageId));\n }\n // Template-only source must not be claimed by language servers before\n // it has an enclosing generated package. Restore the real extension\n // only in the rendered project.\n if (entry.name.endsWith(\".template\")) {\n renameSync(rendered, rendered.slice(0, -\".template\".length));\n }\n }\n };\n visit(source);\n\n // npm deliberately excludes files named `.gitignore` from package\n // tarballs. Their packaged twins live outside the C3 template so a direct\n // Git-based C3 render does not inherit a scaffolder-only helper file.\n const packagedGitignore = resolve(templatesRoot, \"scaffold\", `${basename(source)}.gitignore`);\n if (existsSync(packagedGitignore)) {\n writeFileSync(resolve(destination, \".gitignore\"), render(readFileSync(packagedGitignore, \"utf8\"), name, manager, packageId));\n }\n}\n\nfunction packageCommand(manager: PackageManager, operation: \"install\" | \"contract\"): [string, string[]] {\n if (operation === \"install\") return [manager, [\"install\"]];\n return [manager, [\"run\", \"contract\"]];\n}\n\nconst androidDesugaring = `// flutter_local_notifications requires desugaring in the application module.\n// A library plugin cannot enable this compiler setting transitively.\nandroid {\n compileOptions {\n isCoreLibraryDesugaringEnabled = true\n }\n}\n\ndependencies {\n coreLibraryDesugaring(\"com.android.tools:desugar_jdk_libs:2.1.4\")\n}`;\n\nfunction enableAndroidCoreLibraryDesugaring(appRoot: string): void {\n const gradlePath = resolve(appRoot, \"android/app/build.gradle.kts\");\n const gradle = readFileSync(gradlePath, \"utf8\");\n if (!gradle.includes('id(\"com.android.application\")')) {\n throw new Error(`Flutter created an unexpected Android application build file: ${gradlePath}`);\n }\n const settingExists = gradle.includes(\"isCoreLibraryDesugaringEnabled\");\n const dependencyExists = gradle.includes(\"coreLibraryDesugaring(\");\n if (settingExists && dependencyExists) return;\n if (settingExists || dependencyExists) {\n throw new Error(`Flutter created an incomplete Android core library desugaring configuration: ${gradlePath}`);\n }\n appendFileSync(gradlePath, `${gradle.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\"}${androidDesugaring}\\n`);\n}\n\n// A second top-level `android { }` block is valid Gradle Kotlin DSL — Gradle\n// merges repeated extension-configuration blocks in one file, which is what\n// lets this stay a pure append, same as the desugaring block above. A second\n// top-level `plugins { }` block is NOT valid (Gradle allows exactly one per\n// script), which is why the Crashlytics Gradle plugin registration —\n// `flutterfire configure`'s own territory — is deliberately left alone here.\n//\n// `Properties` needs a real `import`, prepended separately below — a\n// fully-qualified `java.util.Properties()` reference here was tried first,\n// to avoid needing to touch the top of the file at all, but fails to\n// resolve under AGP 9's Kotlin DSL script compilation\n// (`flutter create`'s current default): \"Unresolved reference 'util'\".\n// Confirmed by actually building a scaffolded project, not just reading it.\nconst androidReleaseSigning = `val releaseKeyProperties: Map<String, String> =\n rootProject.file(\"key.properties\")\n .takeIf { it.exists() }\n ?.readLines()\n ?.map { it.trim() }\n ?.filter { it.isNotEmpty() && !it.startsWith(\"#\") && it.contains(\"=\") }\n ?.associate { it.substringBefore(\"=\").trim() to it.substringAfter(\"=\").trim() }\n .orEmpty()\n\nandroid {\n signingConfigs {\n create(\"release\") {\n storeFile = releaseKeyProperties[\"storeFile\"]?.let { file(it) }\n storePassword = releaseKeyProperties[\"storePassword\"]\n keyAlias = releaseKeyProperties[\"keyAlias\"]\n keyPassword = releaseKeyProperties[\"keyPassword\"]\n }\n }\n buildTypes {\n release {\n signingConfig = if (releaseKeyProperties[\"storeFile\"] != null)\n signingConfigs.getByName(\"release\")\n else\n signingConfigs.getByName(\"debug\")\n isMinifyEnabled = true\n isShrinkResources = true\n proguardFiles(\n getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n \"proguard-rules.pro\",\n )\n }\n }\n}`;\n\nfunction enableAndroidReleaseSigning(appRoot: string): void {\n const gradlePath = resolve(appRoot, \"android/app/build.gradle.kts\");\n const gradle = readFileSync(gradlePath, \"utf8\");\n // Not `gradle.includes(\"signingConfigs\")` — Flutter's own template already\n // contains that substring (`signingConfig = signingConfigs.getByName(\"debug\")`),\n // so that check would always short-circuit and never append anything.\n if (gradle.includes(\"releaseKeyProperties\")) return;\n // A pure append. This previously also PREPENDED `import java.util.Properties`,\n // because Kotlin requires imports before every other top-level declaration —\n // which meant reaching into the very start of a file this scaffolder does\n // not own, the most position-dependent edit here and the one already broken\n // once by an AGP upgrade. Parsing `key.properties` with the Kotlin stdlib\n // needs no import, so the edit is now append-only and cannot collide with\n // whatever Flutter or `flutterfire configure` writes above it.\n appendFileSync(gradlePath, `${gradle.endsWith(\"\\n\") ? \"\" : \"\\n\"}${androidReleaseSigning}\\n`);\n}\n\n// `flutter_launcher_icons`/`flutter_native_splash` read these as plain\n// top-level pubspec keys, so appending is safe regardless of what\n// `flutter create` already wrote under `flutter:`. Deliberately omits\n// `ios`/`macos` — `scaffoldGame` only creates `--platforms android,web`, and\n// `flutter_launcher_icons` errors if told to target a platform whose\n// directory doesn't exist.\n// The colours are the 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): void {\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;\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;\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 }\n}\n\nexport function scaffoldGame(options: ScaffoldOptions): ScaffoldResult {\n const root = resolve(options.directory);\n const name = gameSlug(basename(root));\n const manager = options.packageManager ?? detectPackageManager() ?? \"pnpm\";\n const org = options.org?.trim() || \"com.example\";\n // Mirrors how `flutter create --org` derives the real Android\n // `applicationId`/iOS bundle id, so Fastlane's `Appfile` names the same\n // package the build actually produces.\n const packageId = `${org}.${dartName(name)}`;\n const bootstrap = options.bootstrap ?? true;\n const run = options.run ?? ((command, args, cwd) => execFileSync(command, args, { cwd, stdio: \"inherit\" }));\n\n if (existsSync(root)) throw new Error(`target already exists: ${root}`);\n\n mkdirSync(dirname(root), { recursive: true });\n\n // Build beside the destination and publish it with one rename. Failed\n // subprocesses never leave a half-scaffolded project at the requested path.\n const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));\n try {\n const serverRoot = resolve(stagingRoot, \"server\");\n const appRoot = resolve(stagingRoot, \"app\");\n\n renderTree(resolve(templatesRoot, \"worker\"), serverRoot, name, manager, packageId);\n renderTree(resolve(templatesRoot, \"project\"), stagingRoot, name, manager, packageId);\n // Opt-in. release.yml wants an upload keystore and a Play service account\n // that a brand-new project does not have, so generating it by default\n // means a red X on main from the first push until someone sets both up.\n // `create-eigen-game add ci` writes the same files whenever it is wanted.\n if (options.ci) renderTree(resolve(templatesRoot, \"ci\"), stagingRoot, name, manager, packageId);\n\n if (bootstrap) {\n run(\"flutter\", [\"create\", \"--empty\", \"--platforms\", \"android,web\", \"--project-name\", dartName(name), \"--org\", org, appRoot], stagingRoot);\n enableAndroidCoreLibraryDesugaring(appRoot);\n enableAndroidReleaseSigning(appRoot);\n } else {\n mkdirSync(appRoot, { recursive: true });\n }\n renderTree(resolve(templatesRoot, \"app-overlay\"), appRoot, name, manager, packageId);\n\n if (bootstrap) {\n configureLauncherIconsAndSplash(appRoot);\n run(\"flutter\", [\"pub\", \"add\", `eigen_flutter@${flutterClientVersion}`, \"firebase_core@^4.9.0\", \"firebase_messaging@^16.2.2\"], appRoot);\n run(\"flutter\", [\"pub\", \"add\", \"dev:flutter_launcher_icons\", \"dev:flutter_native_splash\"], appRoot);\n // Actually apply the icons rather than only configuring them. Both tools\n // write generated files (mipmaps, `web/icons/`, splash drawables and\n // styles) that are committed, not built — leaving them unrun would ship\n // Flutter's own blue logo until a game author happened to notice.\n run(\"dart\", [\"run\", \"flutter_launcher_icons\"], appRoot);\n run(\"dart\", [\"run\", \"flutter_native_splash:create\"], appRoot);\n const [install, installArgs] = packageCommand(manager, \"install\");\n run(install, installArgs, serverRoot);\n const [contract, contractArgs] = packageCommand(manager, \"contract\");\n run(contract, contractArgs, serverRoot);\n run(\"dart\", [\"run\", \"eigen_flutter:generate_payloads\", \"--contract\", \"../server/game-contract.json\", \"--output\", \"lib/game/generated/payloads.dart\", \"--fixtures-output\", \"test/fixtures\"], appRoot);\n }\n\n renameSync(stagingRoot, root);\n } 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 if (options.git ?? bootstrap) initialiseRepository(root, name, run);\n\n return { root, name };\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":[]}
|