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