create-eigen-game 0.9.1 → 0.9.3
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-45XDHKSP.js → chunk-X6NSW7BX.js} +76 -49
- package/dist/chunk-X6NSW7BX.js.map +1 -0
- package/dist/cli.js +12 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/templates/app-overlay/app-config.json.template +1 -1
- package/templates/project/README.md +15 -3
- package/templates/project/package.json +1 -1
- package/templates/worker/package.json +1 -0
- package/templates/worker/tsconfig.json +1 -1
- package/templates/worker/vitest.config.mts +24 -0
- package/dist/chunk-45XDHKSP.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { execFileSync } from "child_process";
|
|
3
|
-
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { basename, dirname, relative, resolve } from "path";
|
|
3
|
+
import { appendFileSync, cpSync, existsSync as existsSync2, mkdirSync, mkdtempSync, readdirSync, readFileSync as readFileSync2, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
+
import { basename, dirname, relative, resolve as resolve2 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
7
7
|
// src/engine-range.ts
|
|
@@ -20,6 +20,32 @@ function engineRange(declared, readSiblingVersion) {
|
|
|
20
20
|
return `^${version}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/link-firebase.ts
|
|
24
|
+
import { existsSync, readFileSync } from "fs";
|
|
25
|
+
import { resolve } from "path";
|
|
26
|
+
function readJson(path) {
|
|
27
|
+
if (!existsSync(path)) return void 0;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function configuredProject(appRoot) {
|
|
35
|
+
const record = readJson(resolve(appRoot, "firebase.json"));
|
|
36
|
+
const projectId = record?.flutter?.platforms?.dart?.["lib/firebase_options.dart"]?.projectId;
|
|
37
|
+
return typeof projectId === "string" && projectId !== "" ? projectId : null;
|
|
38
|
+
}
|
|
39
|
+
function configuredWebClientId(appRoot) {
|
|
40
|
+
const config = readJson(resolve(appRoot, "app-config.json"));
|
|
41
|
+
const value = config?.GOOGLE_WEB_CLIENT_ID;
|
|
42
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
43
|
+
}
|
|
44
|
+
function readFirebaseLink(root) {
|
|
45
|
+
const appRoot = resolve(root, "app");
|
|
46
|
+
return { projectId: configuredProject(appRoot), googleWebClientId: configuredWebClientId(appRoot) };
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
// src/index.ts
|
|
24
50
|
function normaliseTerminalWidth(stream) {
|
|
25
51
|
if (stream.isTTY === true && (stream.columns ?? 0) < 1) stream.columns = 80;
|
|
@@ -46,11 +72,11 @@ function capturingRunner(reporter) {
|
|
|
46
72
|
}
|
|
47
73
|
};
|
|
48
74
|
}
|
|
49
|
-
var packageRoot =
|
|
50
|
-
var templatesRoot =
|
|
51
|
-
var engineVersion = engineRange(JSON.parse(
|
|
52
|
-
const sibling =
|
|
53
|
-
return
|
|
75
|
+
var packageRoot = resolve2(dirname(fileURLToPath(import.meta.url)), "..");
|
|
76
|
+
var templatesRoot = resolve2(packageRoot, "templates");
|
|
77
|
+
var engineVersion = engineRange(JSON.parse(readFileSync2(resolve2(packageRoot, "package.json"), "utf8")).devDependencies?.[ENGINE_PACKAGE], () => {
|
|
78
|
+
const sibling = resolve2(packageRoot, "../server/package.json");
|
|
79
|
+
return existsSync2(sibling) ? JSON.parse(readFileSync2(sibling, "utf8")).version : void 0;
|
|
54
80
|
});
|
|
55
81
|
var flutterClientVersion = "^0.3.0";
|
|
56
82
|
var gameSlug = (value) => {
|
|
@@ -61,7 +87,7 @@ var gameSlug = (value) => {
|
|
|
61
87
|
};
|
|
62
88
|
var dartName = (value) => value.replaceAll("-", "_");
|
|
63
89
|
function applicationId(directory, org) {
|
|
64
|
-
return `${org?.trim() || "com.example"}.${dartName(gameSlug(basename(
|
|
90
|
+
return `${org?.trim() || "com.example"}.${dartName(gameSlug(basename(resolve2(directory))))}`;
|
|
65
91
|
}
|
|
66
92
|
var title = (value) => value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
67
93
|
var identifier = (value) => {
|
|
@@ -98,14 +124,14 @@ function renderTree(source, destination, name, manager, packageId) {
|
|
|
98
124
|
cpSync(source, destination, { recursive: true });
|
|
99
125
|
const visit = (directory) => {
|
|
100
126
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
101
|
-
const templatePath =
|
|
127
|
+
const templatePath = resolve2(directory, entry.name);
|
|
102
128
|
if (entry.isDirectory()) {
|
|
103
129
|
visit(templatePath);
|
|
104
130
|
continue;
|
|
105
131
|
}
|
|
106
132
|
if (!entry.isFile()) continue;
|
|
107
|
-
const rendered =
|
|
108
|
-
const text = decodeUtf8(
|
|
133
|
+
const rendered = resolve2(destination, relative(source, templatePath));
|
|
134
|
+
const text = decodeUtf8(readFileSync2(templatePath));
|
|
109
135
|
if (text !== void 0) {
|
|
110
136
|
writeFileSync(rendered, render(text, name, manager, packageId));
|
|
111
137
|
}
|
|
@@ -115,9 +141,9 @@ function renderTree(source, destination, name, manager, packageId) {
|
|
|
115
141
|
}
|
|
116
142
|
};
|
|
117
143
|
visit(source);
|
|
118
|
-
const packagedGitignore =
|
|
119
|
-
if (
|
|
120
|
-
writeFileSync(
|
|
144
|
+
const packagedGitignore = resolve2(templatesRoot, "scaffold", `${basename(source)}.gitignore`);
|
|
145
|
+
if (existsSync2(packagedGitignore)) {
|
|
146
|
+
writeFileSync(resolve2(destination, ".gitignore"), render(readFileSync2(packagedGitignore, "utf8"), name, manager, packageId));
|
|
121
147
|
}
|
|
122
148
|
}
|
|
123
149
|
function packageCommand(manager, operation) {
|
|
@@ -136,8 +162,8 @@ dependencies {
|
|
|
136
162
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
|
137
163
|
}`;
|
|
138
164
|
function enableAndroidCoreLibraryDesugaring(appRoot) {
|
|
139
|
-
const gradlePath =
|
|
140
|
-
const gradle =
|
|
165
|
+
const gradlePath = resolve2(appRoot, "android/app/build.gradle.kts");
|
|
166
|
+
const gradle = readFileSync2(gradlePath, "utf8");
|
|
141
167
|
if (!gradle.includes('id("com.android.application")')) {
|
|
142
168
|
throw new Error(`Flutter created an unexpected Android application build file: ${gradlePath}`);
|
|
143
169
|
}
|
|
@@ -184,8 +210,8 @@ android {
|
|
|
184
210
|
}
|
|
185
211
|
}`;
|
|
186
212
|
function enableAndroidReleaseSigning(appRoot) {
|
|
187
|
-
const gradlePath =
|
|
188
|
-
const gradle =
|
|
213
|
+
const gradlePath = resolve2(appRoot, "android/app/build.gradle.kts");
|
|
214
|
+
const gradle = readFileSync2(gradlePath, "utf8");
|
|
189
215
|
if (gradle.includes("releaseKeyProperties")) return;
|
|
190
216
|
appendFileSync(gradlePath, `${gradle.endsWith("\n") ? "" : "\n"}${androidReleaseSigning}
|
|
191
217
|
`);
|
|
@@ -217,29 +243,29 @@ flutter_native_splash:
|
|
|
217
243
|
web: true
|
|
218
244
|
`;
|
|
219
245
|
function configureLauncherIconsAndSplash(appRoot) {
|
|
220
|
-
const pubspecPath =
|
|
221
|
-
const pubspec =
|
|
246
|
+
const pubspecPath = resolve2(appRoot, "pubspec.yaml");
|
|
247
|
+
const pubspec = readFileSync2(pubspecPath, "utf8");
|
|
222
248
|
if (pubspec.includes("flutter_launcher_icons:")) return;
|
|
223
249
|
appendFileSync(pubspecPath, `${pubspec.endsWith("\n") ? "" : "\n"}${launcherIconsAndSplashConfig}`);
|
|
224
250
|
}
|
|
225
251
|
function addContinuousIntegration(options) {
|
|
226
|
-
const root =
|
|
227
|
-
const manifestPath =
|
|
228
|
-
if (!
|
|
252
|
+
const root = resolve2(options.directory);
|
|
253
|
+
const manifestPath = resolve2(root, "package.json");
|
|
254
|
+
if (!existsSync2(manifestPath) || !existsSync2(resolve2(root, "server")) || !existsSync2(resolve2(root, "app"))) {
|
|
229
255
|
throw new Error(`not an EigenInteractive game project (expected package.json, server/ and app/): ${root}`);
|
|
230
256
|
}
|
|
231
|
-
const manifest = JSON.parse(
|
|
257
|
+
const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
232
258
|
const name = gameSlug(manifest.name ?? basename(root));
|
|
233
259
|
const recorded = manifest.scripts?.contract?.includes("pnpm ") ? "pnpm" : manifest.scripts?.contract?.includes("npm ") ? "npm" : void 0;
|
|
234
260
|
const manager = options.packageManager ?? recorded ?? detectPackageManager() ?? "pnpm";
|
|
235
|
-
const appfile =
|
|
236
|
-
const packageId =
|
|
237
|
-
const workflows =
|
|
238
|
-
const existing =
|
|
261
|
+
const appfile = resolve2(root, "app/fastlane/Appfile");
|
|
262
|
+
const packageId = existsSync2(appfile) ? /package_name\("([^"]+)"\)/.exec(readFileSync2(appfile, "utf8"))?.[1] ?? `com.example.${dartName(name)}` : `com.example.${dartName(name)}`;
|
|
263
|
+
const workflows = resolve2(root, ".github/workflows");
|
|
264
|
+
const existing = existsSync2(workflows) ? readdirSync(workflows).filter((f) => f === "checks.yml" || f === "release.yml") : [];
|
|
239
265
|
if (existing.length > 0) {
|
|
240
266
|
throw new Error(`refusing to overwrite existing workflows: ${existing.join(", ")}`);
|
|
241
267
|
}
|
|
242
|
-
renderTree(
|
|
268
|
+
renderTree(resolve2(templatesRoot, "ci"), root, name, manager, packageId);
|
|
243
269
|
return { root, files: [".github/workflows/checks.yml", ".github/workflows/release.yml"] };
|
|
244
270
|
}
|
|
245
271
|
var probeCommand = (command, args) => {
|
|
@@ -272,8 +298,8 @@ function signedOut(probe) {
|
|
|
272
298
|
}
|
|
273
299
|
}
|
|
274
300
|
function destinationProblem(directory) {
|
|
275
|
-
const root =
|
|
276
|
-
if (!
|
|
301
|
+
const root = resolve2(directory);
|
|
302
|
+
if (!existsSync2(root)) return void 0;
|
|
277
303
|
if (!statSync(root).isDirectory()) return `${root} already exists, and is a file.`;
|
|
278
304
|
const entries = readdirSync(root);
|
|
279
305
|
if (entries.length === 0) return void 0;
|
|
@@ -284,8 +310,8 @@ function destinationProblem(directory) {
|
|
|
284
310
|
Scaffold somewhere else, or clear it out yourself. This will not delete a directory you already had.`;
|
|
285
311
|
}
|
|
286
312
|
function insideWorkTree(directory, probe = probeCommand) {
|
|
287
|
-
let candidate =
|
|
288
|
-
while (!
|
|
313
|
+
let candidate = resolve2(directory);
|
|
314
|
+
while (!existsSync2(candidate)) {
|
|
289
315
|
const parent = dirname(candidate);
|
|
290
316
|
if (parent === candidate) return false;
|
|
291
317
|
candidate = parent;
|
|
@@ -293,18 +319,18 @@ function insideWorkTree(directory, probe = probeCommand) {
|
|
|
293
319
|
return probe("git", ["-C", candidate, "rev-parse", "--is-inside-work-tree"]).ok;
|
|
294
320
|
}
|
|
295
321
|
function configureFirebase(appRoot, project, run, reporter) {
|
|
296
|
-
const placeholder =
|
|
322
|
+
const placeholder = resolve2(appRoot, "lib/firebase_options.dart");
|
|
297
323
|
const parked = `${placeholder}.placeholder`;
|
|
298
|
-
const parking =
|
|
324
|
+
const parking = existsSync2(placeholder);
|
|
299
325
|
if (parking) renameSync(placeholder, parked);
|
|
300
326
|
try {
|
|
301
327
|
reporter.step("Preparing the Firebase configurator", () => run("dart", ["run", "eigen_flutter:configure_firebase", "--help"], appRoot));
|
|
302
|
-
reporter.handOver("Configuring Firebase", () => run("dart", ["run", "eigen_flutter:configure_firebase", ...typeof project === "string" ? ["--project", project] : []], appRoot));
|
|
328
|
+
reporter.handOver("Configuring Firebase", () => run("dart", ["run", "eigen_flutter:configure_firebase", "--worker", "../server", ...typeof project === "string" ? ["--project", project] : []], appRoot));
|
|
303
329
|
if (parking) rmSync(parked, { force: true });
|
|
304
330
|
return "configured";
|
|
305
331
|
} catch {
|
|
306
332
|
if (parking) {
|
|
307
|
-
if (
|
|
333
|
+
if (existsSync2(placeholder)) rmSync(parked, { force: true });
|
|
308
334
|
else renameSync(parked, placeholder);
|
|
309
335
|
}
|
|
310
336
|
reporter.warn("Could not configure Firebase. The project is complete. Run `firebase:configure` yourself, then commit what it writes.");
|
|
@@ -333,7 +359,7 @@ function initialiseRepository(root, name, run, reporter) {
|
|
|
333
359
|
return "committed";
|
|
334
360
|
}
|
|
335
361
|
function scaffoldGame(options) {
|
|
336
|
-
const root =
|
|
362
|
+
const root = resolve2(options.directory);
|
|
337
363
|
const name = gameSlug(basename(root));
|
|
338
364
|
const manager = options.packageManager ?? detectPackageManager() ?? "pnpm";
|
|
339
365
|
const org = options.org?.trim() || "com.example";
|
|
@@ -343,15 +369,15 @@ function scaffoldGame(options) {
|
|
|
343
369
|
const run = options.run ?? capturingRunner(reporter);
|
|
344
370
|
const occupied = destinationProblem(root);
|
|
345
371
|
if (occupied !== void 0) throw new Error(occupied);
|
|
346
|
-
if (
|
|
372
|
+
if (existsSync2(root)) rmdirSync(root);
|
|
347
373
|
mkdirSync(dirname(root), { recursive: true });
|
|
348
|
-
const stagingRoot = mkdtempSync(
|
|
374
|
+
const stagingRoot = mkdtempSync(resolve2(dirname(root), `.${basename(root)}-`));
|
|
349
375
|
try {
|
|
350
|
-
const serverRoot =
|
|
351
|
-
const appRoot =
|
|
352
|
-
renderTree(
|
|
353
|
-
renderTree(
|
|
354
|
-
if (options.ci) renderTree(
|
|
376
|
+
const serverRoot = resolve2(stagingRoot, "server");
|
|
377
|
+
const appRoot = resolve2(stagingRoot, "app");
|
|
378
|
+
renderTree(resolve2(templatesRoot, "worker"), serverRoot, name, manager, packageId);
|
|
379
|
+
renderTree(resolve2(templatesRoot, "project"), stagingRoot, name, manager, packageId);
|
|
380
|
+
if (options.ci) renderTree(resolve2(templatesRoot, "ci"), stagingRoot, name, manager, packageId);
|
|
355
381
|
if (bootstrap) {
|
|
356
382
|
reporter.step("Creating the Flutter app", () => {
|
|
357
383
|
run("flutter", ["create", "--empty", "--platforms", "android,web", "--project-name", dartName(name), "--org", org, appRoot], stagingRoot);
|
|
@@ -361,7 +387,7 @@ function scaffoldGame(options) {
|
|
|
361
387
|
} else {
|
|
362
388
|
mkdirSync(appRoot, { recursive: true });
|
|
363
389
|
}
|
|
364
|
-
renderTree(
|
|
390
|
+
renderTree(resolve2(templatesRoot, "app-overlay"), appRoot, name, manager, packageId);
|
|
365
391
|
if (bootstrap) {
|
|
366
392
|
reporter.step("Adding the Flutter packages", () => {
|
|
367
393
|
configureLauncherIconsAndSplash(appRoot);
|
|
@@ -390,9 +416,10 @@ function scaffoldGame(options) {
|
|
|
390
416
|
rmSync(stagingRoot, { recursive: true, force: true });
|
|
391
417
|
throw error;
|
|
392
418
|
}
|
|
393
|
-
const firebase = options.firebase ? configureFirebase(
|
|
419
|
+
const firebase = options.firebase ? configureFirebase(resolve2(root, "app"), options.firebase, run, reporter) : "skipped";
|
|
420
|
+
const link = firebase === "configured" ? readFirebaseLink(root) : void 0;
|
|
394
421
|
const git = options.git ?? bootstrap ? initialiseRepository(root, name, run, reporter) : "skipped";
|
|
395
|
-
return { root, name, git, firebase };
|
|
422
|
+
return { root, name, git, firebase, link };
|
|
396
423
|
}
|
|
397
424
|
|
|
398
425
|
export {
|
|
@@ -408,4 +435,4 @@ export {
|
|
|
408
435
|
insideWorkTree,
|
|
409
436
|
scaffoldGame
|
|
410
437
|
};
|
|
411
|
-
//# sourceMappingURL=chunk-
|
|
438
|
+
//# sourceMappingURL=chunk-X6NSW7BX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/engine-range.ts","../src/link-firebase.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmdirSync, rmSync, statSync, 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\";\nimport { type FirebaseLink, readFirebaseLink } from \"./link-firebase.js\";\n\nexport type { FirebaseLink } from \"./link-firebase.js\";\n\nexport type PackageManager = \"npm\" | \"pnpm\";\n\nexport type Runner = (command: string, args: string[], cwd: string) => void;\n\n/**\n * Where a scaffold says what it is doing, so that presentation belongs to the\n * caller and this module only has to name things.\n *\n * The default is the behaviour a library caller expects with no opinion:\n * subprocesses inherit the terminal and warnings go to `console.warn`. The CLI\n * substitutes one that captures each step's output and shows it only if the\n * step fails.\n */\nexport interface Reporter {\n /** Runs `body` as one named step. Its subprocesses can be captured. */\n step<T>(label: string, body: () => T): T;\n /**\n * Runs `body` as a step that owns the terminal, because something inside it\n * asks questions. FlutterFire is the only one.\n */\n handOver<T>(label: string, body: () => T): T;\n /** Output from a subprocess inside the current step. */\n emit(output: string): void;\n /** A failure the scaffold survives. */\n warn(message: string): void;\n /** Whether subprocesses must inherit the terminal rather than be captured. */\n readonly interactive: boolean;\n}\n\n/**\n * Gives a terminal that reports no width the width clack would have assumed.\n *\n * A pty can report itself as a TTY and set `columns` to 0. `script` does, and\n * so do some container terminals. clack falls back to 80 for a stream with no\n * `columns` at all, but `0` is a number and satisfies that check, so the width\n * it draws its rules from goes negative and a scaffold dies partway through\n * with `RangeError: Invalid string length`. Supplying the default its check\n * misses is better than giving up the drawing.\n */\nexport function normaliseTerminalWidth(stream: { isTTY?: boolean; columns?: number }): void {\n if (stream.isTTY === true && (stream.columns ?? 0) < 1) stream.columns = 80;\n}\n\n/** Runs every step plainly, which is what a library caller gets by default. */\nexport const plainReporter: Reporter = {\n step: (_label, body) => body(),\n handOver: (_label, body) => body(),\n emit: (output) => process.stdout.write(output),\n warn: (message) => console.warn(`create-eigen-game: ${message}`),\n interactive: true,\n};\n\n/**\n * The runner a scaffold uses when the caller has not supplied one: it asks the\n * reporter whether the terminal is spoken for.\n *\n * `interactive` inherits, which is the only way FlutterFire can put a question\n * on screen and read the answer. Otherwise both streams are captured and\n * handed to the reporter: on success so it can discard them, and on failure\n * so the reason survives.\n */\nexport function capturingRunner(reporter: Reporter): Runner {\n return (command, args, cwd) => {\n if (reporter.interactive) {\n execFileSync(command, args, { cwd, stdio: \"inherit\" });\n return;\n }\n try {\n reporter.emit(execFileSync(command, args, { cwd, encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"pipe\"] }));\n } catch (error: unknown) {\n // Both streams, because which one a tool failed on is its own business:\n // pub explains itself on stdout, Gradle on stderr.\n const { stdout, stderr } = error as { stdout?: string; stderr?: string };\n reporter.emit(`${stdout ?? \"\"}${stderr ?? \"\"}`);\n throw error;\n }\n };\n}\n\nexport interface ScaffoldOptions {\n directory: string;\n org?: string;\n packageManager?: PackageManager;\n /**\n * Run Flutter, package installation, and contract/payload generation.\n * Kept as a programmatic test seam; the public CLI always bootstraps both\n * halves of the combined project.\n */\n bootstrap?: boolean;\n /**\n * Emit the GitHub Actions workflows. Off by default; see the call site for\n * why, and `addContinuousIntegration` for adding them to an existing\n * project later.\n */\n ci?: boolean;\n /**\n * Initialise a repository and commit the scaffold. Defaults to `bootstrap`,\n * because the commit is only worth having once the generated files exist.\n */\n git?: boolean;\n /**\n * Configure Firebase before the first commit, by running the Flutter\n * client's `configure_firebase`. A string names the project to use; `true`\n * lets FlutterFire ask, which is also where a project can be created.\n *\n * Explicit here, though the CLI turns it on by default: this is the one step\n * that reaches outside the destination directory, and a library caller\n * should say so rather than discover it. {@link firebaseReadiness} is how\n * the CLI decides.\n */\n firebase?: boolean | string;\n /**\n * Runs the bootstrap subprocesses. A seam: the tests substitute a recorder,\n * and `scripts/scaffold-e2e.mjs` wraps it to point the generated server at\n * this workspace's engine rather than npm's copy.\n */\n run?: Runner;\n /**\n * Where the scaffold says what it is doing. See {@link Reporter}; the\n * default runs every step plainly.\n */\n reporter?: Reporter;\n}\n\n/**\n * What became of the repository, so the CLI's closing summary can say rather\n * than guess. `failed` has already warned by the time it is returned, and\n * carries no advice of its own.\n */\nexport type GitOutcome = \"committed\" | \"existing\" | \"skipped\" | \"failed\";\n\nexport interface ScaffoldResult {\n root: string;\n name: string;\n git: GitOutcome;\n /**\n * Whether Firebase was configured, so the summary can either stop naming the\n * step or spell it out. `failed` has already warned.\n */\n firebase: \"configured\" | \"failed\" | \"skipped\";\n /**\n * What the Firebase step made knowable and this scaffold therefore filled\n * in. Absent unless `firebase` is `configured`, since there is nothing to\n * read otherwise.\n */\n link?: FirebaseLink;\n}\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst templatesRoot = resolve(packageRoot, \"templates\");\n\n/**\n * The engine range emitted into a scaffolded project's package.json.\n *\n * Read from this package's `@eigeninteractive/server` devDependency rather than\n * from its own version. See `engine-range.ts` for why that is the version the\n * templates are known to compile against, and what it replaced.\n */\nconst engineVersion = engineRange((JSON.parse(readFileSync(resolve(packageRoot, \"package.json\"), \"utf8\")) as { devDependencies?: Record<string, string> }).devDependencies?.[ENGINE_PACKAGE], () => {\n // Only reached in this workspace, where the manifest still says\n // `workspace:*`. A published tarball has no sibling here and never asks.\n const sibling = resolve(packageRoot, \"../server/package.json\");\n return existsSync(sibling) ? (JSON.parse(readFileSync(sibling, \"utf8\")) as { version: string }).version : undefined;\n});\n\n/**\n * The Flutter client range installed into the app half.\n *\n * A stated version, not a derived or resolved one. The Dart templates import\n * `package:eigen_flutter/eigen_flutter.dart` and\n * `package:eigen_flutter/testing/twin_fixtures.dart` and are written against a\n * specific Dart API, and `eigen_flutter` lives in another repository, versioned\n * independently, so nothing in this repository can compute which release that\n * is. Only compiling a scaffolded app establishes it, which is what the\n * `scaffold` job in checks.yml does on every change.\n *\n * This briefly resolved from pub.dev instead: \"the newest `eigen_flutter` whose\n * own `eigen_api` constraint targets the engine line being scaffolded\". That\n * predicate is wrong. The `eigen_api` constraint describes the WIRE the shell\n * speaks, not the Dart API these templates call, and the two move\n * independently: a future `eigen_flutter` may legitimately keep `eigen_api:\n * ^0.2.0` while renaming everything the templates touch. It would have been\n * selected, and the generated app would not compile.\n *\n * A caret RANGE rather than an exact version, so it still improves without a\n * republish: `flutter pub add eigen_flutter@^0.3.0` already picks the newest\n * 0.3.x at scaffold time, which is the part the pub.dev lookup was duplicating.\n * What it deliberately cannot do is cross to 0.4.x, the one move that needs a\n * human to confirm the templates still compile.\n *\n * Staleness is therefore a failing check rather than a broken scaffold: this is\n * only ever a release behind, never wrong.\n *\n * Raised to 0.3.0 because the notification icon moved into `eigen_flutter`'s\n * Android plugin: the scaffold stopped shipping `ic_notification.xml`, so a\n * game resolving 0.2.x would reference a drawable nothing provides.\n */\nconst flutterClientVersion = \"^0.3.0\";\n\nconst gameSlug = (value: string): string => {\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {\n throw new Error(\"destination directory name must be a lowercase kebab-case slug, for example my-game\");\n }\n return value;\n};\n\nconst dartName = (value: string): string => value.replaceAll(\"-\", \"_\");\n\n/**\n * The Android `applicationId`, and iOS bundle id, a scaffold at `directory`\n * will produce, so the CLI can show it rather than describe it.\n *\n * `flutter create --org X --project-name Y` derives `X.Y`. The organization is\n * the *prefix*, not the whole identifier, which is the part worth seeing\n * before answering: Google Play makes it permanent at first upload.\n */\nexport function applicationId(directory: string, org?: string): string {\n return `${org?.trim() || \"com.example\"}.${dartName(gameSlug(basename(resolve(directory))))}`;\n}\n\nconst title = (value: string): string =>\n value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\" \");\n\nconst identifier = (value: string): string => {\n const pascal = value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\"\");\n return /^[0-9]/.test(pascal) ? `Game${pascal}` : pascal;\n};\n\nexport function detectPackageManager(userAgent = process.env.npm_config_user_agent): PackageManager | undefined {\n if (userAgent?.startsWith(\"pnpm/\")) return \"pnpm\";\n if (userAgent?.startsWith(\"npm/\")) return \"npm\";\n return undefined;\n}\n\n/**\n * Substitutes the scaffold's tokens, of which there are two deliberate kinds.\n *\n * `{{BRACED}}` for values that have no valid example: a package manager,\n * an engine version, an Android package id. They appear only in manifests,\n * READMEs, workflows and the `Appfile`: files nothing compiles.\n *\n * Bare words (`ExampleGame`, `example-game`, …) for names a real example game\n * genuinely has. This is what keeps `templates/worker` and the Dart templates\n * VALID, COMPILING SOURCE rather than text with holes in it, which the rest of\n * this package leans on hard: `typecheck` runs `tsc` over `templates/worker`,\n * the tests `import` its game module directly and build a real contract from\n * it, and the `scaffold` job compiles the Dart templates. Bracing these would\n * trade all of that for a marker.\n *\n * The cost, and the one rule for template authors: a template file cannot\n * contain the literal string `example-game`, `Example Game`, `ExampleGame` or\n * `example_game` and keep it; every occurrence is rewritten, in prose and in\n * code alike. Write around it, or brace a new token instead.\n *\n * Note this means the set of files carrying a token changes whenever someone\n * writes one into an existing file. Nothing needs to track that: `renderTree`\n * decides what to rewrite from the bytes, not from a list of paths.\n */\nfunction render(contents: string, name: string, manager: PackageManager, packageId: string): string {\n const replacements: ReadonlyArray<readonly [string, string]> = [\n [\"@game/example-game-server\", `@game/${name}-server`],\n [\"{{PACKAGE_MANAGER}}\", manager],\n [\"{{PACKAGE_ID}}\", packageId],\n [\"{{ENGINE_VERSION}}\", engineVersion],\n [\"ExampleGame\", identifier(name)],\n [\"Example Game\", title(name)],\n [\"example_game\", dartName(name)],\n [\"example-game\", name],\n ];\n return replacements.reduce((result, [from, to]) => result.replaceAll(from, to), contents);\n}\n\n/**\n * True when the bytes are valid UTF-8.\n *\n * This is not a guess at whether a file is \"binary\"; it is the precondition\n * of the operation. Token substitution is `String.replaceAll`, so a file that\n * cannot be decoded cannot take part, and one that can is safe to rewrite.\n *\n * Deliberately not an extension list. A DENY list (`.png`, `.jar`, …) fails\n * silently in the dangerous direction: the day someone adds a `.webp`, it is\n * corrupted with no error. An ALLOW list fails the safe way but fits this tree\n * badly: `Gemfile`, `Fastfile`, `.nvmrc`, `.fvmrc` and `.ruby-version` all\n * need rendering and none carry a usable extension, so it would have to be a\n * list of extensions *and* a list of bare filenames, both drifting.\n *\n * `.template` is not the signal either, despite the name: it marks files a\n * language server must not claim before they have an enclosing package. Five\n * of them contain no token at all, and nine token-bearing files are not\n * marked, so the two concepts are orthogonal here.\n *\n * `renderTree`'s test pins the resulting classification for the whole tree, so\n * this stays a reviewed decision rather than an invisible one.\n */\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\nexport function decodeUtf8(bytes: Buffer): string | undefined {\n try {\n return utf8.decode(bytes);\n } catch {\n return undefined;\n }\n}\n\nfunction renderTree(source: string, destination: string, name: string, manager: PackageManager, packageId: string): void {\n cpSync(source, destination, { recursive: true });\n // Walks the TEMPLATE, not the destination. The app overlay renders on top of\n // a `flutter create` result, so walking the destination meant rewriting\n // every file Flutter had just generated, including its binaries, for files\n // this scaffolder does not own and has no business touching.\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const templatePath = resolve(directory, entry.name);\n if (entry.isDirectory()) {\n visit(templatePath);\n continue;\n }\n if (!entry.isFile()) continue;\n\n const rendered = resolve(destination, relative(source, templatePath));\n const text = decodeUtf8(readFileSync(templatePath));\n // Undecodable files are already in place from the copy above, byte for\n // byte. Only decodable ones are rewritten.\n if (text !== undefined) {\n writeFileSync(rendered, render(text, name, manager, packageId));\n }\n // Template-only source must not be claimed by language servers before\n // it has an enclosing generated package. Restore the real extension\n // only in the rendered project.\n if (entry.name.endsWith(\".template\")) {\n renameSync(rendered, rendered.slice(0, -\".template\".length));\n }\n }\n };\n visit(source);\n\n // npm deliberately excludes files named `.gitignore` from package\n // tarballs. Their packaged twins live outside the C3 template so a direct\n // Git-based C3 render does not inherit a scaffolder-only helper file.\n const packagedGitignore = resolve(templatesRoot, \"scaffold\", `${basename(source)}.gitignore`);\n if (existsSync(packagedGitignore)) {\n writeFileSync(resolve(destination, \".gitignore\"), render(readFileSync(packagedGitignore, \"utf8\"), name, manager, packageId));\n }\n}\n\nfunction packageCommand(manager: PackageManager, operation: \"install\" | \"contract\" | \"cf-typegen\"): [string, string[]] {\n if (operation === \"install\") return [manager, [\"install\"]];\n return [manager, [\"run\", operation]];\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, since 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\")`, because 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`, since `scaffoldGame` only creates `--platforms android,web`, and\n// `flutter_launcher_icons` errors if told to target a platform whose\n// directory doesn't exist.\n// The colours are the EigenInteractive palette (ink #1B1E24, paper #F4F1EA) because the\n// shipped placeholder art is the EigenInteractive mark: the adaptive foreground is the\n// reversed, light-on-dark variant, so an ink background is what makes it\n// legible. Rebranding a game means replacing the four PNGs *and* these\n// colours together. See https://eigeninteractive.com/docs/ship-it/branding.\nconst launcherIconsAndSplashConfig = `\nflutter_launcher_icons:\n android: true\n web:\n generate: true\n image_path: \"assets/icon/icon.png\"\n image_path: \"assets/icon/icon.png\"\n adaptive_icon_background: \"#1B1E24\"\n adaptive_icon_foreground: \"assets/icon/icon_foreground.png\"\n min_sdk_android: 21\n\nflutter_native_splash:\n color: \"#F4F1EA\"\n color_dark: \"#1B1E24\"\n image: assets/icon/splash.png\n image_dark: assets/icon/splash_dark.png\n fullscreen: true\n android_12:\n color: \"#F4F1EA\"\n color_dark: \"#1B1E24\"\n image: assets/icon/splash.png\n image_dark: assets/icon/splash_dark.png\n icon_background_color: \"#F4F1EA\"\n icon_background_color_dark: \"#1B1E24\"\n web: true\n`;\n\nfunction configureLauncherIconsAndSplash(appRoot: string): void {\n const pubspecPath = resolve(appRoot, \"pubspec.yaml\");\n const pubspec = readFileSync(pubspecPath, \"utf8\");\n if (pubspec.includes(\"flutter_launcher_icons:\")) return;\n appendFileSync(pubspecPath, `${pubspec.endsWith(\"\\n\") ? \"\" : \"\\n\"}${launcherIconsAndSplashConfig}`);\n}\n\nexport interface AddContinuousIntegrationOptions {\n directory: string;\n /** Overrides the manager recorded in the project's own package.json. */\n packageManager?: PackageManager;\n}\n\n/**\n * Adds the GitHub Actions workflows to an existing generated project.\n *\n * The companion to `ci: false`: a game can be built and played locally for as\n * long as its author likes, then gain the same PR gate and signed release\n * pipeline a `--ci` scaffold would have produced.\n */\nexport function addContinuousIntegration(options: AddContinuousIntegrationOptions): { root: string; files: string[] } {\n const root = resolve(options.directory);\n const manifestPath = resolve(root, \"package.json\");\n if (!existsSync(manifestPath) || !existsSync(resolve(root, \"server\")) || !existsSync(resolve(root, \"app\"))) {\n throw new Error(`not an EigenInteractive game project (expected package.json, server/ and app/): ${root}`);\n }\n\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as {\n name?: string;\n scripts?: Record<string, string>;\n };\n const name = gameSlug(manifest.name ?? basename(root));\n\n // The generated `contract` script embeds the manager the project was\n // scaffolded with (`cd server && pnpm run contract && …`), which is a more\n // reliable signal than a lockfile: it is present before anything is\n // installed, and it is what the project actually uses.\n const recorded = manifest.scripts?.contract?.includes(\"pnpm \") ? \"pnpm\" : manifest.scripts?.contract?.includes(\"npm \") ? \"npm\" : undefined;\n const manager = options.packageManager ?? recorded ?? detectPackageManager() ?? \"pnpm\";\n\n // Only `{{PACKAGE_MANAGER}}` appears in these templates, but the real\n // package id is read back rather than reconstructed, so a project scaffolded\n // under a different `--org` cannot be silently rewritten to `com.example`.\n const appfile = resolve(root, \"app/fastlane/Appfile\");\n const packageId = existsSync(appfile) ? (/package_name\\(\"([^\"]+)\"\\)/.exec(readFileSync(appfile, \"utf8\"))?.[1] ?? `com.example.${dartName(name)}`) : `com.example.${dartName(name)}`;\n\n const workflows = resolve(root, \".github/workflows\");\n const existing = existsSync(workflows) ? readdirSync(workflows).filter((f) => f === \"checks.yml\" || f === \"release.yml\") : [];\n if (existing.length > 0) {\n throw new Error(`refusing to overwrite existing workflows: ${existing.join(\", \")}`);\n }\n\n renderTree(resolve(templatesRoot, \"ci\"), root, name, manager, packageId);\n return { root, files: [\".github/workflows/checks.yml\", \".github/workflows/release.yml\"] };\n}\n\n/** Asks a command a question. `ok` is \"ran and succeeded\"; a tool that is not installed and one that fails are both `false`, because neither can be used. */\nexport type Probe = (command: string, args: string[]) => { ok: boolean; stdout: string };\n\nconst probeCommand: Probe = (command, args) => {\n try {\n return { ok: true, stdout: execFileSync(command, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] }) };\n } catch {\n return { ok: false, stdout: \"\" };\n }\n};\n\n/** One thing standing between this machine and a configured Firebase project, and the single command that clears it. */\nexport interface FirebaseProblem {\n /** Said as a sentence fragment, so it reads after \"Firebase is not set up here:\". */\n reason: string;\n /** The one command that fixes this, and nothing else. */\n fix: string;\n}\n\nexport type FirebaseReadiness = { ready: true } | { ready: false; problems: FirebaseProblem[] };\n\n/**\n * Whether this machine can configure Firebase, asked *before* the scaffold\n * rather than after it.\n *\n * `configure_firebase` runs the same three checks and is the authority on\n * them; this one exists only to move the answer earlier. Learning that\n * `flutterfire` is missing costs nothing here and costs two minutes of Flutter\n * and pub at the far end, which is where the step actually runs.\n *\n * Every problem, not the first one. The checks are independent (two CLIs from\n * two ecosystems, plus a sign-in) so short-circuiting turns one setup into a\n * sequence of runs that each reveal the next missing piece. Reported in the\n * order they have to be fixed in: the CLI, then the sign-in it stores, then\n * the bridge that consumes both.\n *\n * The sign-in check fails open, for the same reason it does there: only an\n * answer that positively reports no accounts counts as \"no\", so a `firebase`\n * that errors or grows a different output shape is left to the command that\n * actually needs the credentials.\n */\nexport function firebaseReadiness(probe: Probe = probeCommand): FirebaseReadiness {\n const problems: FirebaseProblem[] = [];\n\n if (probe(\"firebase\", [\"--version\"]).ok) {\n if (signedOut(probe)) problems.push({ reason: \"no Google account is signed in to the Firebase CLI\", fix: \"firebase login\" });\n } else {\n // Nothing to say about the sign-in: the CLI that would answer is the one\n // that is missing, and `firebase login` is what installing leads to anyway.\n // Google's own installer, which picks a standalone binary or npm to suit\n // the machine. `npm install -g firebase-tools` also works and is what this\n // used to print, but it needs a global npm prefix the reader may not have\n // write access to, and it is not the install the Firebase docs lead with.\n problems.push({ reason: \"the `firebase` CLI is not installed\", fix: \"curl -sL https://firebase.tools | bash\" });\n }\n\n if (!probe(\"flutterfire\", [\"--version\"]).ok) {\n problems.push({ reason: \"the `flutterfire` CLI is not installed\", fix: \"dart pub global activate flutterfire_cli\" });\n }\n\n return problems.length === 0 ? { ready: true } : { ready: false, problems };\n}\n\n/**\n * Whether the Firebase CLI holds no credentials.\n *\n * `login:list --json` prints the stored refresh and access tokens along with\n * the account list. Read the shape, count the entries, and never surface the\n * output: not in a warning, not in a captured stream, not on the failure\n * path. That is why this returns a boolean rather than the response.\n */\nfunction signedOut(probe: Probe): boolean {\n const accounts = probe(\"firebase\", [\"login:list\", \"--json\"]);\n if (!accounts.ok) return false;\n try {\n const { result } = JSON.parse(accounts.stdout) as { result?: unknown };\n return Array.isArray(result) && result.length === 0;\n } catch {\n // Unparseable is not evidence of anything.\n return false;\n }\n}\n\n/**\n * Whether the scaffold would land inside a repository that already exists.\n *\n * Asked of the nearest ancestor that exists, because the destination does not\n * yet. The point is not to change what {@link scaffoldGame} does, which already\n * declines to nest a repository, but to stop the CLI asking a question whose\n * answer cannot matter.\n */\n/**\n * Why this destination cannot be scaffolded into, said as a whole message, or\n * `undefined` when there is nothing in the way.\n *\n * A directory that exists and is *empty* is not in the way, since people make\n * one out of habit, so it is removed and rewritten rather than refused over\n * nothing. Anything with contents in it is refused, and a cloned repository is\n * not an exception: `.git` is in the way like any other entry, and the message\n * names it. Publishing into one would mean parking `.git`, renaming staging\n * over the top and putting it back, with a failure in the middle able to\n * orphan a remote, which is a lot of care to spend on a flow that has a\n * one-line alternative.\n *\n * Refused, and deliberately not offered as a \"remove it and continue?\"\n * question, which is where several scaffolders in this family go. This one\n * writes ninety-odd files and runs `flutter create`; the cost of getting that\n * answer wrong against a mistyped path has no upper bound, and `rm -rf` is one\n * command that belongs to the person who can see what is in there. Every other\n * question this CLI asks is reversible. This one would not be.\n *\n * Exported so the CLI can ask it before the first question rather than after\n * the last one. {@link scaffoldGame} asks it again, for callers that are not\n * the CLI.\n */\nexport function destinationProblem(directory: string): string | undefined {\n const root = resolve(directory);\n if (!existsSync(root)) return undefined;\n\n if (!statSync(root).isDirectory()) return `${root} already exists, and is a file.`;\n\n const entries = readdirSync(root);\n if (entries.length === 0) return undefined;\n\n // Named, not counted. \"not empty\" invites an `ls`; the three that are\n // actually there usually settle whether this was the wrong path or a\n // directory that has already been scaffolded once.\n const shown = entries.slice(0, 3).sort().join(\", \");\n const rest = entries.length > 3 ? `, and ${entries.length - 3} more` : \"\";\n return `${root} already exists, and is not empty: it holds ${shown}${rest}.\\n\\nScaffold somewhere else, or clear it out yourself. This will not delete a directory you already had.`;\n}\n\nexport function insideWorkTree(directory: string, probe: Probe = probeCommand): boolean {\n let candidate = resolve(directory);\n while (!existsSync(candidate)) {\n const parent = dirname(candidate);\n if (parent === candidate) return false;\n candidate = parent;\n }\n return probe(\"git\", [\"-C\", candidate, \"rev-parse\", \"--is-inside-work-tree\"]).ok;\n}\n\n/**\n * Runs the Flutter client's `configure_firebase` against the generated app.\n *\n * The command owns everything about how Firebase is configured: which\n * platforms, which CLIs must exist, whether anyone is signed in, and what to\n * do when no project has been chosen yet. This only decides *when*: before the\n * scaffold commit, so a configured project is committed configured.\n *\n * Not fatal. Every failure here (a missing `flutterfire`, no Google login, a\n * cancelled picker) leaves a complete and usable project that is exactly what\n * a scaffold without `--firebase` produces, so it warns with the command to\n * re-run and lets the commit happen.\n */\nfunction configureFirebase(appRoot: string, project: boolean | string, run: Runner, reporter: Reporter): \"configured\" | \"failed\" {\n // FlutterFire asks before overwriting `firebase_options.dart`, and the\n // scaffold has just written one, a throwing placeholder whose entire\n // purpose is to be replaced by this step. Answering that is a question with\n // one right answer, so the placeholder is moved out of the way and the\n // question never gets asked.\n const placeholder = resolve(appRoot, \"lib/firebase_options.dart\");\n const parked = `${placeholder}.placeholder`;\n const parking = existsSync(placeholder);\n if (parking) renameSync(placeholder, parked);\n\n try {\n // `dart run` compiles the package executable on first use and says so.\n // Doing it against `--help`, which returns immediately, moves that noise\n // into a step whose output can be captured; the real run cannot be, since\n // FlutterFire prompts through it.\n reporter.step(\"Preparing the Firebase configurator\", () => run(\"dart\", [\"run\", \"eigen_flutter:configure_firebase\", \"--help\"], appRoot));\n // `--worker` is what makes this fill in `FIREBASE_PROJECT_ID` as well as\n // the app's own values. An app-only repository omits it and gets the app\n // half; a combined scaffold has the Worker one directory over.\n reporter.handOver(\"Configuring Firebase\", () => run(\"dart\", [\"run\", \"eigen_flutter:configure_firebase\", \"--worker\", \"../server\", ...(typeof project === \"string\" ? [\"--project\", project] : [])], appRoot));\n if (parking) rmSync(parked, { force: true });\n return \"configured\";\n } catch {\n // Back only if nothing took its place. FlutterFire writes\n // `firebase_options.dart` before the service worker configuration is\n // derived from it, so a late failure leaves a real one that is worth more\n // than the placeholder.\n if (parking) {\n if (existsSync(placeholder)) rmSync(parked, { force: true });\n else renameSync(parked, placeholder);\n }\n reporter.warn(\"Could not configure Firebase. The project is complete. Run `firebase:configure` yourself, then commit what it writes.\");\n return \"failed\";\n }\n}\n\n/**\n * Commits the scaffold, so the first `git diff` is the first game change.\n *\n * This matters more here than in a scaffolder that only writes source. The\n * bootstrap runs `flutter_launcher_icons` and `flutter_native_splash`, which\n * write generated-but-committed files across `android/`, `web/` and\n * `assets/`; without a baseline the first branding change is indistinguishable\n * from the ninety files the scaffolder happened to produce.\n *\n * Nothing here is fatal. The project is complete and valid before this runs,\n * so a missing `git`, or the unconfigured `user.email` that a fresh CI image\n * has, warns and leaves the tree alone rather than discarding a scaffold that\n * took two minutes of Flutter and pub to produce.\n */\nfunction initialiseRepository(root: string, name: string, run: Runner, reporter: Reporter): GitOutcome {\n // Scaffolding inside an existing checkout (a monorepo, or a repository\n // created ahead of time) is a legitimate thing to do, and a nested\n // repository there is silent breakage: the outer `git add` records a gitlink\n // and the app's files never leave the machine. Asked directly rather than\n // through `run`, because it is a question, not a step.\n try {\n execFileSync(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], { cwd: root, stdio: \"ignore\" });\n return \"existing\";\n } catch {\n // Not inside a work tree, which is the case this function is for.\n }\n\n try {\n run(\"git\", [\"init\", \"--quiet\"], root);\n run(\"git\", [\"add\", \"--all\"], root);\n } catch {\n reporter.warn(\"Could not initialise a git repository. The project is complete. Run `git init` yourself when ready.\");\n return \"failed\";\n }\n\n try {\n run(\"git\", [\"commit\", \"--quiet\", \"--message\", `Scaffold ${name}`], root);\n } catch {\n reporter.warn(`Initialised a repository and staged the scaffold, but could not commit it. Set \\`user.name\\` and \\`user.email\\`, then \\`git commit -m \"Scaffold ${name}\"\\`.`);\n return \"failed\";\n }\n\n return \"committed\";\n}\n\nexport function scaffoldGame(options: ScaffoldOptions): ScaffoldResult {\n const root = resolve(options.directory);\n const name = gameSlug(basename(root));\n const manager = options.packageManager ?? detectPackageManager() ?? \"pnpm\";\n const org = options.org?.trim() || \"com.example\";\n // Fastlane's `Appfile` names the same package the build actually produces,\n // and the CLI shows the same one before asking.\n const packageId = applicationId(root, org);\n const bootstrap = options.bootstrap ?? true;\n const reporter = options.reporter ?? plainReporter;\n const run = options.run ?? capturingRunner(reporter);\n\n const occupied = destinationProblem(root);\n if (occupied !== undefined) throw new Error(occupied);\n // Empty, or `destinationProblem` would have said so. Removed rather than\n // scaffolded into, because the rename below publishes the staging directory\n // *as* this path and cannot do that while something is standing there.\n if (existsSync(root)) rmdirSync(root);\n\n mkdirSync(dirname(root), { recursive: true });\n\n // Build beside the destination and publish it with one rename. Failed\n // subprocesses never leave a half-scaffolded project at the requested path.\n const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));\n try {\n const serverRoot = resolve(stagingRoot, \"server\");\n const appRoot = resolve(stagingRoot, \"app\");\n\n renderTree(resolve(templatesRoot, \"worker\"), serverRoot, name, manager, packageId);\n renderTree(resolve(templatesRoot, \"project\"), stagingRoot, name, manager, packageId);\n // Opt-in. release.yml wants an upload keystore and a Play service account\n // that a brand-new project does not have, so generating it by default\n // means a red X on main from the first push until someone sets both up.\n // `create-eigen-game add ci` writes the same files whenever it is wanted.\n if (options.ci) renderTree(resolve(templatesRoot, \"ci\"), stagingRoot, name, manager, packageId);\n\n if (bootstrap) {\n reporter.step(\"Creating the Flutter app\", () => {\n run(\"flutter\", [\"create\", \"--empty\", \"--platforms\", \"android,web\", \"--project-name\", dartName(name), \"--org\", org, appRoot], stagingRoot);\n enableAndroidCoreLibraryDesugaring(appRoot);\n enableAndroidReleaseSigning(appRoot);\n });\n } else {\n mkdirSync(appRoot, { recursive: true });\n }\n renderTree(resolve(templatesRoot, \"app-overlay\"), appRoot, name, manager, packageId);\n\n if (bootstrap) {\n reporter.step(\"Adding the Flutter packages\", () => {\n configureLauncherIconsAndSplash(appRoot);\n run(\"flutter\", [\"pub\", \"add\", `eigen_flutter@${flutterClientVersion}`, \"firebase_core@^4.9.0\", \"firebase_messaging@^16.2.2\"], appRoot);\n run(\"flutter\", [\"pub\", \"add\", \"dev:flutter_launcher_icons\", \"dev:flutter_native_splash\"], appRoot);\n });\n // Actually apply the icons rather than only configuring them. Both tools\n // write generated files (mipmaps, `web/icons/`, splash drawables and\n // styles) that are committed, not built; leaving them unrun would ship\n // Flutter's own blue logo until a game author happened to notice.\n reporter.step(\"Generating the icons and splash\", () => {\n run(\"dart\", [\"run\", \"flutter_launcher_icons\"], appRoot);\n run(\"dart\", [\"run\", \"flutter_native_splash:create\"], appRoot);\n });\n const [install, installArgs] = packageCommand(manager, \"install\");\n const [typegen, typegenArgs] = packageCommand(manager, \"cf-typegen\");\n reporter.step(\"Installing the Worker packages\", () => {\n run(install, installArgs, serverRoot);\n // `worker-configuration.d.ts` is committed, and its header stamps the\n // workerd version that produced the runtime types. `wrangler` floats\n // on a caret and Cloudflare ships workerd about weekly, so the copy in\n // `templates/worker` is stale for every scaffold made more than a few\n // days after it was last regenerated, and `wrangler dev` opens by\n // saying the types might be out of date. Regenerating against the\n // wrangler that was just installed, rather than the one this package\n // was built with, is what makes that true. It has to follow the server\n // install for the same reason it is worth doing: it reads the wrangler\n // that install resolved.\n run(typegen, typegenArgs, serverRoot);\n // The root holds Biome, which lints and formats both the Worker and\n // the repository's own JSON. Installed after the server so a failure\n // here costs the cheaper of the two.\n run(install, installArgs, stagingRoot);\n });\n const [contract, contractArgs] = packageCommand(manager, \"contract\");\n reporter.step(\"Generating the contract and payloads\", () => {\n run(contract, contractArgs, serverRoot);\n run(\"dart\", [\"run\", \"eigen_flutter:generate_payloads\", \"--contract\", \"../server/game-contract.json\", \"--output\", \"lib/game/generated/payloads.dart\", \"--fixtures-output\", \"test/fixtures\"], appRoot);\n });\n }\n\n renameSync(stagingRoot, root);\n } catch (error) {\n rmSync(stagingRoot, { recursive: true, force: true });\n throw error;\n }\n\n // Both steps below are deliberately outside the staging guard: the project\n // is published by this point, and neither a Firebase project that could not\n // be configured nor a repository that failed to initialise is a reason to\n // delete two minutes of Flutter and pub.\n //\n // Firebase first, so what it writes (`firebase.json`,\n // `android/app/google-services.json`, the real `firebase_options.dart` and\n // `web/firebase-config.js` in place of the throwing placeholders, and\n // FlutterFire's two Gradle edits) lands in the scaffold commit rather than\n // arriving as the project's first diff.\n const firebase = options.firebase ? configureFirebase(resolve(root, \"app\"), options.firebase, run, reporter) : \"skipped\";\n // What that step settled, so the summary can name what it did not.\n // `configure_firebase` does the writing, including into the Worker's\n // wrangler.jsonc, which is what `--worker` above is for.\n const link = firebase === \"configured\" ? readFirebaseLink(root) : undefined;\n const git = (options.git ?? bootstrap) ? initialiseRepository(root, name, run, reporter) : \"skipped\";\n\n return { root, name, git, firebase, link };\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 and 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","/**\n * Reads the two deployment values that exist only once a Firebase project\n * does, so the closing summary can name what is still missing.\n *\n * Reading only. `configure_firebase` writes them, into `wrangler.jsonc` and\n * `app-config.json`, because it is also what a project runs later when\n * reconfiguring, and one implementation is what keeps the scaffold-time path\n * and the re-run path identical. This module exists so the summary can tell\n * the difference between \"filled in\" and \"still owed\", not to do it again.\n *\n * Its own module for the same reason `summary.ts` is: `index.ts` runs a\n * scaffold, and this is worth asserting without one. The package `exports` map\n * does not expose it.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\n/** What the Firebase step managed to settle, and what it left for the console. */\nexport interface FirebaseLink {\n /** The project now in `wrangler.jsonc`, or null when FlutterFire recorded none. */\n projectId: string | null;\n /** The OAuth web client now in `app-config.json`, or null when Google sign-in is not enabled yet. */\n googleWebClientId: string | null;\n}\n\nfunction readJson(path: string): unknown {\n if (!existsSync(path)) return undefined;\n try {\n return JSON.parse(readFileSync(path, \"utf8\"));\n } catch {\n // A file this step only reads is never worth failing a finished scaffold\n // for. The value counts as unset, and the summary says so.\n return undefined;\n }\n}\n\n/**\n * The project FlutterFire recorded for the Dart output this app uses.\n *\n * `firebase.json` is FlutterFire's own record rather than something this\n * scaffold writes, so it is read defensively: a shape that does not match is\n * treated as \"not configured\", which is the same answer as a run that never\n * happened.\n */\nexport function configuredProject(appRoot: string): string | null {\n const record = readJson(resolve(appRoot, \"firebase.json\")) as { flutter?: { platforms?: { dart?: Record<string, { projectId?: unknown }> } } } | undefined;\n const projectId = record?.flutter?.platforms?.dart?.[\"lib/firebase_options.dart\"]?.projectId;\n return typeof projectId === \"string\" && projectId !== \"\" ? projectId : null;\n}\n\n/**\n * The OAuth web client id that `configure_firebase` copied into\n * `app-config.json`, when there was one to copy.\n *\n * Empty is an ordinary outcome rather than a failure. Firebase creates that\n * client when the Google sign-in provider is enabled, which is a console\n * action no CLI can perform, so a project that has never had it enabled has\n * nothing to offer here.\n */\nexport function configuredWebClientId(appRoot: string): string | null {\n const config = readJson(resolve(appRoot, \"app-config.json\")) as Record<string, unknown> | undefined;\n const value = config?.GOOGLE_WEB_CLIENT_ID;\n return typeof value === \"string\" && value !== \"\" ? value : null;\n}\n\n/** What the Firebase step settled, for the summary to report. */\nexport function readFirebaseLink(root: string): FirebaseLink {\n const appRoot = resolve(root, \"app\");\n return { projectId: configuredProject(appRoot), googleWebClientId: configuredWebClientId(appRoot) };\n}\n"],"mappings":"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","names":["existsSync","readFileSync","resolve","resolve","readFileSync","existsSync"]}
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
normaliseTerminalWidth,
|
|
10
10
|
plainReporter,
|
|
11
11
|
scaffoldGame
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-X6NSW7BX.js";
|
|
13
13
|
|
|
14
14
|
// src/cli.ts
|
|
15
15
|
import { readFileSync } from "fs";
|
|
@@ -113,6 +113,15 @@ function summarise(result, manager, workflows) {
|
|
|
113
113
|
if (result.git === "committed") status.push(`Committed as "Scaffold ${result.name}"`);
|
|
114
114
|
if (result.git === "existing") status.push("Created inside a repository you already had, so nothing was committed");
|
|
115
115
|
if (result.firebase === "configured") status.push(result.git === "committed" ? "Firebase is configured, and everything it generated is in that commit" : "Firebase is configured");
|
|
116
|
+
if (result.link?.projectId != null) status.push(`FIREBASE_PROJECT_ID is set to ${result.link.projectId}, and app-config.json points at the local Worker`);
|
|
117
|
+
const remaining = [];
|
|
118
|
+
if (result.link !== void 0) {
|
|
119
|
+
if (result.link.googleWebClientId === null) {
|
|
120
|
+
remaining.push(["Enable Google sign-in", "Console > Authentication > Sign-in method"]);
|
|
121
|
+
remaining.push(["GOOGLE_WEB_CLIENT_ID", "that same page, under Web SDK configuration"]);
|
|
122
|
+
}
|
|
123
|
+
remaining.push(["FIREBASE_VAPID_KEY", "Console > Project settings > Cloud Messaging > Web configuration"]);
|
|
124
|
+
}
|
|
116
125
|
const next = [
|
|
117
126
|
"Write the game",
|
|
118
127
|
...columns([
|
|
@@ -130,7 +139,8 @@ function summarise(result, manager, workflows) {
|
|
|
130
139
|
// app throws `Firebase is not configured` at launch until this has run
|
|
131
140
|
// once.
|
|
132
141
|
...result.firebase === "configured" ? [] : [[`${run} firebase:configure`, "connect Firebase before the app runs"]]
|
|
133
|
-
])
|
|
142
|
+
]),
|
|
143
|
+
...remaining.length === 0 ? [] : ["", "Before the app runs, from the Firebase console", ...columns(remaining), "", " The values go in app/app-config.json. Both are public, not secrets."]
|
|
134
144
|
].join("\n");
|
|
135
145
|
const footnotes = ["Docs: https://eigeninteractive.com"];
|
|
136
146
|
if (!workflows) footnotes.unshift("No CI workflows yet: `create-eigen-game add workflows` writes them when shipping is the next step.");
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/prompt.ts","../src/summary.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { cancel, intro, isCI, isTTY, log, note, outro, taskLog } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport { addContinuousIntegration, applicationId, destinationProblem, detectPackageManager, type FirebaseProblem, firebaseReadiness, insideWorkTree, normaliseTerminalWidth, type PackageManager, plainReporter, type Reporter, scaffoldGame } from \"./index.js\";\nimport { askForGit, askForOrg, askForPackageManager, askForWorkflows, askToScaffoldWithoutFirebase, DEFAULT_ORG, ORG } from \"./prompt.js\";\nimport { summarise } from \"./summary.js\";\n\nconst version = (JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), \"../package.json\"), \"utf8\")) as { version: string }).version;\n\nconst PREREQUISITES = \"https://eigeninteractive.com/docs/getting-started/prerequisites\";\n\nconst help = `Usage: create-eigen-game <game-slug> [options]\n create-eigen-game add workflows [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\nEvery option below answers a question the CLI would otherwise ask. Pass it to\nskip the question; leave it out to be asked. With no terminal to ask on (CI, a\npipe, an agent session) an unanswered question is an error rather than a\ndefault quietly chosen for you, and the message prints the command to re-run.\n\nOptions:\n --org <reverse-domain> Android/iOS organization. The applicationId is this\n plus the game name, as in com.example.my_game, and\n Google Play makes it permanent at first upload\n --firebase-project <id>\n The Firebase project to configure against. Also the\n form that works with no terminal, since otherwise\n FlutterFire asks\n --no-firebase Do not configure Firebase, leaving firebase:configure\n as a step for later\n --git, --no-git Initialise a repository and commit the scaffold\n --workflows, --no-workflows\n Emit the GitHub Actions workflows. release.yml needs\n an upload keystore and a Play service account, so it\n fails on every push until both exist\n --package-manager <pm> npm or pnpm. Taken from the invoking package manager\n when there is one, so this is for global installs\n -h, --help Show this help\n -v, --version Show the version\n\nCommands:\n add workflows [directory]\n Add the workflows to an existing project, for when\n you are ready to ship rather than at scaffold time.\n Defaults to the current directory.`;\n\nnormaliseTerminalWidth(process.stdout);\n\n/**\n * Whether to draw, using clack's own predicates rather than a hand-rolled\n * `isTTY`, the same pair `taskLog` consults internally. A pipe has nowhere to\n * put a redraw, and a CI log is read after the fact, in full, by someone who\n * wants every line a tool printed.\n */\nconst decorated = isTTY(process.stdout) && !isCI();\n\n/**\n * The CLI's voice, in whichever of the two registers this terminal supports.\n *\n * Every message goes through here so the undecorated path cannot drift into\n * being the untested one.\n */\nconst ui = decorated\n ? {\n open: () => intro(color.inverse(\" EigenInteractive \")),\n info: (message: string) => log.info(message),\n warn: (message: string) => log.warn(message),\n success: (message: string) => log.success(message),\n block: (body: string) => note(body, \"Next\"),\n close: (message: string) => outro(color.bold(message)),\n stop: (message: string) => cancel(message),\n }\n : {\n open: () => console.log(\"\\nEigenInteractive\\n\"),\n info: (message: string) => console.log(message),\n warn: (message: string) => console.warn(message),\n success: (message: string) => console.log(message),\n block: (body: string) => console.log(`\\n${body}\\n`),\n close: (message: string) => console.log(message),\n stop: (message: string) => console.error(message),\n };\n\nfunction resolveManager(requested: string | undefined): PackageManager | undefined {\n if (requested === undefined) return undefined;\n if (requested !== \"npm\" && requested !== \"pnpm\") {\n throw new Error(\"--package-manager must be npm or pnpm\");\n }\n return requested;\n}\n\n/**\n * Why a scaffold asks about a second service at all, said once, before the\n * first question rather than in the error that follows it.\n *\n * Firebase is not an integration this engine offers; it is where a player\n * comes from. Somebody meeting that for the first time deserves the reason in\n * the same breath as the requirement, and deserves to know the size of it:\n * one free project, shared by every game they build.\n */\nfunction greet(): void {\n ui.open();\n ui.info(\"A game is one repository with two halves: a Cloudflare Worker that owns the rules,\\nand a Flutter app for Android and the web. This writes both, wired together and\\nalready playable.\");\n ui.info(\n `Firebase signs your players in. The Worker verifies Firebase ID tokens to decide who\\nholds a seat, and sends turn notifications through the same project, so a scaffolded\\napp throws ${color.dim(\"Firebase is not configured\")} at launch until one is connected.\\n\\nIt is free to start, one project serves every game you build, and this connects yours\\nas it goes. ${color.dim(PREREQUISITES)}`,\n );\n}\n\n/** What is missing, and the one command that fixes each: the same list the warning and the parting message are both built from. */\nfunction firebaseReport(problems: FirebaseProblem[]): string {\n const lines = problems.flatMap((problem) => [` ${problem.reason}`, ` ${color.cyan(problem.fix)}`]);\n // `dart pub global activate` does not put the binary on PATH, which is the\n // next thing to go wrong for anyone who has just run the command above.\n if (problems.some((problem) => problem.fix.startsWith(\"dart pub\"))) {\n lines.push(` ${color.dim('…then, if your shell cannot find it: export PATH=\"$PATH\":\"$HOME/.pub-cache/bin\"')}`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * The parting message when someone answers \"no\" to scaffolding without\n * Firebase: every command they need, in the order they run in.\n *\n * Deliberately not a failure of the scaffold: nothing has been written, they\n * asked for that, and the exit code is non-zero only so a script that wrapped\n * this can tell there is no project.\n */\nfunction stopForFirebase(problems: FirebaseProblem[]): void {\n // The commands only. What each one is for is in the warning directly above,\n // and a `note` draws a box sized to its longest line, so repeating the\n // reasons here is what makes it wrap into something unreadable at 80\n // columns, which is exactly the width this is most likely to be read at.\n note(`${problems.map((problem) => ` ${color.cyan(problem.fix)}`).join(\"\\n\")}\\n\\nThen run this again.\\n${color.dim(PREREQUISITES)}`, \"Set up Firebase\");\n ui.stop(\"Nothing was written.\");\n process.exitCode = 1;\n}\n\n/**\n * The error for a run with no terminal and an unanswered question.\n *\n * The strictness is the point: a default applied where nobody can see it is\n * how `com.example.my_game` reaches Google Play, which freezes it at the first\n * upload. What keeps that from being merely annoying is this message: the\n * whole command, with every default already filled in, so the fix is one\n * paste and the one value worth changing is visible in it.\n */\nfunction unanswered(directory: string, missing: { flags: string; suggestion: string }[]): string {\n const command = [\"npx create-eigen-game\", directory, ...missing.map((question) => question.suggestion)].join(\" \");\n return [`No terminal to ask on, so every answer has to come from a flag. These were not given:`, \"\", ...missing.map((question) => ` ${question.flags}`), \"\", \"That command, with each default filled in. Read them before you run it:\", \"\", ` ${command}`].join(\"\\n\");\n}\n\n/** `--foo` and `--no-foo`, which `parseArgs` has no notion of: `undefined` means nobody said, which is what makes it a question. */\nfunction flagPair(yes: boolean | undefined, no: boolean | undefined, name: string): boolean | undefined {\n if (yes === true && no === true) throw new Error(`--${name} and --no-${name} contradict each other`);\n if (yes === true) return true;\n if (no === true) return false;\n return undefined;\n}\n\n/** `--org \"\"` falls through to the default rather than failing, matching what an empty answer to the prompt does. */\nfunction resolveOrg(value: string): string | undefined {\n if (value.trim() === \"\") return undefined;\n if (!ORG.test(value)) {\n throw new Error(`invalid organization: ${value}\\n\\nUse reverse domain notation: two or more dot-separated segments of letters, digits and underscores, each starting with a letter, as in ${DEFAULT_ORG} or dev.yourname.games.`);\n }\n return value;\n}\n\n/**\n * Shows each step running, and keeps its output only when it fails.\n *\n * Every tool a scaffold drives has plenty to say (pub resolving 179\n * dependencies, two icon generators, a package manager) and none of it is\n * read while it scrolls past. `taskLog` is exactly that bargain: the stream is\n * visible while the step runs, cleared when it succeeds, and left on screen\n * when it does not, so a failure is still debuggable from what is in the\n * terminal.\n *\n * `handOver` is the exception, and the reason `interactive` exists at all.\n * FlutterFire asks which Firebase project to use, so that step needs the\n * terminal rather than a captured pipe.\n */\nfunction clackReporter(): Reporter {\n // `taskLog` redraws by clearing lines, which is an escape-sequence storm in\n // anything that is not a terminal: a CI log, a pipe, `scaffold-e2e.mjs`.\n // There, the honest thing is the plain reporter: every tool's output, in\n // full, which is what a log gets read for anyway.\n if (!decorated) return plainReporter;\n\n let task: ReturnType<typeof taskLog> | undefined;\n let interactive = false;\n\n return {\n step(label, body) {\n task = taskLog({ title: label });\n try {\n const value = body();\n task.success(label);\n return value;\n } catch (error) {\n task.error(label);\n throw error;\n } finally {\n task = undefined;\n }\n },\n handOver(label, body) {\n log.step(`${label} ${color.dim(\"(FlutterFire takes over here)\")}`);\n interactive = true;\n try {\n return body();\n } finally {\n interactive = false;\n }\n },\n emit(output) {\n const text = output.trimEnd();\n if (text !== \"\") task?.message(text);\n },\n warn(message) {\n ui.warn(message);\n },\n get interactive() {\n return interactive;\n },\n };\n}\n\nasync function main(args: string[]): Promise<void> {\n const { values, positionals } = parseArgs({\n args,\n allowPositionals: true,\n strict: true,\n options: {\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n org: { type: \"string\" },\n \"firebase-project\": { type: \"string\" },\n // Each negation is declared under the name it is typed with, because\n // `parseArgs` has no boolean negation of its own. `flagPair` puts the\n // two halves back together, and the third state, neither given, is the\n // one that matters: it is what turns the flag into a question.\n \"no-firebase\": { type: \"boolean\" },\n git: { type: \"boolean\" },\n \"no-git\": { type: \"boolean\" },\n workflows: { type: \"boolean\" },\n \"no-workflows\": { type: \"boolean\" },\n \"package-manager\": { type: \"string\" },\n },\n });\n\n if (values.help) {\n console.log(help);\n return;\n }\n if (values.version) {\n console.log(version);\n return;\n }\n\n const requestedManager = resolveManager(values[\"package-manager\"]);\n\n if (positionals[0] === \"add\") {\n // `ci` is the name this shipped under, and it is written into the README\n // of every project already scaffolded. Those READMEs are on disk and are\n // not going to be edited, so the old spelling keeps working; only the new\n // one is documented.\n if (positionals[1] !== \"workflows\" && positionals[1] !== \"ci\") {\n throw new Error(`unknown subcommand: add ${positionals[1] ?? \"\"}\\n\\n${help}`.trim());\n }\n if (positionals.length > 3) {\n throw new Error(`expected at most one directory\\n\\n${help}`);\n }\n const result = addContinuousIntegration({\n directory: positionals[2] ?? process.cwd(),\n packageManager: requestedManager,\n });\n console.log(`Added ${result.files.join(\" and \")} in ${result.root}`);\n return;\n }\n\n if (positionals.length !== 1) {\n throw new Error(`expected exactly one destination directory\\n\\n${help}`);\n }\n\n const directory = positionals[0];\n\n // Everything that makes the command impossible, before the greeting rather\n // than after the last question. `applicationId` validates the slug;\n // `destinationProblem` is the one `scaffoldGame` would raise at the far end,\n // asked here instead. Being told the directory was occupied *after* giving\n // an organization is the same insult as being told about `flutterfire` after\n // two minutes of Flutter and pub.\n const game = basename(resolve(directory));\n applicationId(directory);\n const occupied = destinationProblem(directory);\n if (occupied !== undefined) throw new Error(occupied);\n\n greet();\n\n // Every question this run will ask, settled before any of it starts. A flag\n // is an answer already given; anything left over is asked, or, with nothing\n // to ask on, collected here and reported together, so one re-run can carry\n // all of them.\n const interactive = process.stdin.isTTY === true;\n const missing: { flags: string; suggestion: string }[] = [];\n const cancelled = () => {\n ui.stop(\"Nothing was written.\");\n process.exitCode = 130;\n };\n\n /**\n * Records a question that could not be asked, and hands back the default it\n * suggested. The value is never used, since `missing` throws before anything\n * is written, but returning it keeps each answer definitely typed instead of\n * carrying a `| undefined` all the way to the scaffold call.\n */\n const needsFlag = <T>(flags: string, suggestion: string, fallback: T): T => {\n missing.push({ flags, suggestion });\n return fallback;\n };\n\n // Firebase goes first because it is the only answer that can end the run,\n // and ending it after the organization prompt would mean asking for a\n // permanent decision about a project that never gets created.\n const project = values[\"firebase-project\"]?.trim();\n if (project === \"\") throw new Error(\"--firebase-project needs a project id\");\n if (project !== undefined && values[\"no-firebase\"] === true) {\n throw new Error(\"--firebase-project and --no-firebase contradict each other\");\n }\n\n let firebase: boolean | string = false;\n if (project !== undefined) {\n firebase = project;\n } else if (values[\"no-firebase\"] !== true) {\n const readiness = firebaseReadiness();\n if (readiness.ready) {\n // Nothing for this CLI to ask: FlutterFire runs its own project picker\n // during the scaffold, and needs a terminal to do it.\n if (interactive) firebase = true;\n else firebase = needsFlag(\"--firebase-project <id>, or --no-firebase\", \"--no-firebase\", false);\n } else {\n ui.warn(`Firebase is not set up on this machine:\\n\\n${firebaseReport(readiness.problems)}`);\n if (!interactive) {\n firebase = needsFlag(\"--no-firebase, to scaffold without it\", \"--no-firebase\", false);\n } else {\n const carryOn = await askToScaffoldWithoutFirebase();\n if (carryOn === null) return cancelled();\n if (!carryOn) return stopForFirebase(readiness.problems);\n }\n }\n }\n\n let org: string | undefined;\n if (values.org !== undefined) {\n org = resolveOrg(values.org);\n } else if (interactive) {\n const answer = await askForOrg(game.replaceAll(\"-\", \"_\"), firebase !== false);\n if (answer === null) return cancelled();\n org = answer;\n } else {\n org = needsFlag(\"--org <reverse-domain>\", `--org ${DEFAULT_ORG}`, undefined);\n }\n\n // A destination inside a repository someone already has is not a question:\n // `scaffoldGame` declines to nest one repository in another whatever it is\n // told, and says so in the summary.\n let git = flagPair(values.git, values[\"no-git\"], \"git\");\n if (git === undefined && insideWorkTree(directory)) git = true;\n if (git === undefined) {\n if (!interactive) git = needsFlag(\"--git or --no-git\", \"--git\", true);\n else {\n const answer = await askForGit();\n if (answer === null) return cancelled();\n git = answer;\n }\n }\n\n let workflows = flagPair(values.workflows, values[\"no-workflows\"], \"workflows\");\n if (workflows === undefined) {\n if (!interactive) workflows = needsFlag(\"--workflows or --no-workflows\", \"--no-workflows\", false);\n else {\n const answer = await askForWorkflows();\n if (answer === null) return cancelled();\n workflows = answer;\n }\n }\n\n // Reached only when nothing else could say. `npm create` and `pnpm create`\n // both announce themselves in `npm_config_user_agent`. What is left is a\n // global install, where the old silent fallback to pnpm was a guess written\n // into every script in the generated project.\n let manager = requestedManager ?? detectPackageManager();\n if (manager === undefined) {\n if (!interactive) manager = needsFlag(\"--package-manager <npm|pnpm>\", \"--package-manager pnpm\", \"pnpm\" as PackageManager);\n else {\n const answer = await askForPackageManager();\n if (answer === null) return cancelled();\n manager = answer;\n }\n }\n\n if (missing.length > 0) throw new Error(unanswered(directory, missing));\n\n ui.success(`applicationId ${color.bold(applicationId(directory, org))}`);\n\n const result = scaffoldGame({\n directory,\n org,\n packageManager: manager,\n ci: workflows,\n git,\n firebase,\n reporter: clackReporter(),\n });\n\n const { status, next, footnotes, headline } = summarise(result, manager, workflows);\n for (const line of status) ui.success(line);\n ui.block(next);\n for (const line of footnotes) ui.info(line);\n ui.close(headline);\n}\n\ntry {\n await main(process.argv.slice(2));\n} catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n ui.stop(`create-eigen-game: ${message}`);\n process.exitCode = 1;\n}\n","/**\n * Every question this CLI asks, in one module so the wording lives together\n * and can be asserted. `cli.ts` is the bin entry and runs `main` on import,\n * which makes anything defined there untestable.\n *\n * The organization prompt is the reason this drops to `@clack/core` rather\n * than using `@clack/prompts`' `text`. The suffix: `flutter create --org X\n * --project-name Y` derives `X.Y`, so the organization is a *prefix*, and the\n * reliable way to say so is to render the rest of the identifier, dimmed,\n * while it is being typed. `text` has no hook for that; a custom `TextPrompt`\n * render returns a string, so it does.\n *\n * Everything else comes from `@clack/prompts` rather than being drawn here.\n * Its symbols are exported for exactly this, and they are not constants: each\n * falls back to ASCII on a terminal that cannot show the box-drawing\n * characters, which a hand-typed `│` cannot do.\n */\nimport type { Readable, Writable } from \"node:stream\";\nimport { TextPrompt } from \"@clack/core\";\nimport { confirm, isCancel, S_BAR, S_BAR_END, select, symbol } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport type { PackageManager } from \"./index.js\";\n\n/**\n * Where a question reads its answer from. Parameters for the same reason every\n * clack prompt takes them: a prompt is otherwise the one part of a CLI that\n * cannot be driven by a test.\n */\nexport interface Io {\n input?: Readable;\n output?: Writable;\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 */\nexport const ORG = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$/;\n\n/** The default, which an empty answer accepts. */\nexport const DEFAULT_ORG = \"com.example\";\n\nconst bar = color.gray(S_BAR);\n\n/** A `com.acme.chess` answer to a game called `chess`, which yields `com.acme.chess.chess`. */\nexport function repeatsGameName(org: string, game: string): boolean {\n return org.split(\".\").at(-1) === game;\n}\n\n/** The organization implied by an answer that already ends in the game name. */\nexport function withoutGameName(org: string): string | undefined {\n const shorter = org.split(\".\").slice(0, -1).join(\".\");\n return ORG.test(shorter) ? shorter : undefined;\n}\n\n/**\n * Asks, with the game name rendered dimmed after the cursor so the shape of\n * the answer is visible while it is being given.\n *\n * Returns the organization, `undefined` for the default, or `null` when\n * cancelled.\n *\n * `input` and `output` default to this process's, and are parameters for the\n * same reason every clack prompt takes them: a prompt is otherwise the one\n * part of a CLI that cannot be driven by a test.\n */\nexport async function askForOrg(game: string, registering: boolean, io: Io = {}): Promise<string | undefined | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n const suffix = color.dim(`.${game}`);\n const prompt = new TextPrompt({\n input,\n output,\n // An empty answer is the default, not a mistake.\n validate: (value) => (value === undefined || value === \"\" || ORG.test(value) ? undefined : \"Two or more dot-separated segments, as in com.example or dev.yourname.games.\"),\n render() {\n const typed = String(this.value ?? \"\");\n const shown = typed === \"\" ? `${color.dim(DEFAULT_ORG)}${suffix}` : `${this.userInputWithCursor}${suffix}`;\n const problem = this.state === \"error\" ? `\\n${bar} ${color.yellow(this.error ?? \"\")}` : \"\";\n return [`${bar}`, `${symbol(this.state)} Organization in reverse domain notation`, `${bar} ${shown}${problem}`, color.gray(S_BAR_END)].join(\"\\n\");\n },\n });\n\n // The two things that make this answer permanent, said before it is given\n // rather than after: Google Play freezes the identifier at the first upload,\n // and FlutterFire matches an existing Android app on exactly this string, so\n // it also decides whether the run adopts an app the chosen Firebase project\n // already has or registers a new one.\n output.write(`${bar} ${color.dim(\"Prefixes the Android applicationId, which Google Play makes permanent at first upload.\")}\\n`);\n if (registering) output.write(`${bar} ${color.dim(\"Also the Android app registered in the Firebase project you pick next.\")}\\n`);\n\n const answer = await prompt.prompt();\n if (isCancel(answer)) return null;\n\n const org = String(answer).trim();\n if (org === \"\" || org === DEFAULT_ORG) return undefined;\n\n // The mistake the dimmed suffix is there to prevent, caught for the person\n // who pasted an identifier rather than typing one and never watched it grow.\n const shorter = repeatsGameName(org, game) ? withoutGameName(org) : undefined;\n if (shorter !== undefined) {\n const shorten = await confirm({\n message: `That gives ${color.yellow(`${org}.${game}`)}, the game name twice. Use ${color.green(shorter)} instead?`,\n initialValue: true,\n input,\n output,\n });\n if (isCancel(shorten)) return null;\n if (shorten) return shorter;\n }\n\n return org;\n}\n\n/**\n * A yes/no question, with its reason on the line above.\n *\n * The three below share this so cancellation is handled in one place: ^C\n * during any question has to mean the run stops having written nothing, and\n * that is only true if every call site checks. `null` is that answer.\n *\n * `hint` is written rather than passed to `confirm`, which has nowhere to put\n * it. It is the same dimmed line the organization prompt uses, and carries the\n * thing that makes the default the default: a question whose answer is\n * pre-chosen owes the reader why.\n */\nasync function ask(message: string, initialValue: boolean, hint: string, io: Io): Promise<boolean | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n output.write(`${bar}\\n${bar} ${color.dim(hint)}\\n`);\n const answer = await confirm({ message, initialValue, input, output });\n return isCancel(answer) ? null : answer;\n}\n\n/**\n * Asked only when {@link firebaseReadiness} found something missing, and\n * defaulted to \"no\" because the honest answer usually is: the tools are two\n * commands away, and the alternative is an app that throws at launch until\n * they are installed anyway.\n */\nexport function askToScaffoldWithoutFirebase(io: Io = {}): Promise<boolean | null> {\n return ask(\"Scaffold anyway, and connect Firebase yourself later?\", false, \"Everything except the app's sign-in still works, and `firebase:configure` finishes the job.\", io);\n}\n\n/** Defaulted to yes: the scaffold is a few hundred generated files, and a commit is what makes the first `git diff` the first game change rather than all of them. */\nexport function askForGit(io: Io = {}): Promise<boolean | null> {\n return ask(\"Initialise a git repository and commit the scaffold?\", true, \"Commits once, so your first diff is your first game change.\", io);\n}\n\n/**\n * Defaulted to no, and the hint says why rather than leaving it as an\n * arbitrary-looking choice: `release.yml` needs an upload keystore and a Play\n * service account, so a project that takes the workflows before it has either\n * gets a red `main` from its first push.\n */\nexport function askForWorkflows(io: Io = {}): Promise<boolean | null> {\n return ask(\"Add the GitHub Actions workflows?\", false, \"Release needs a signing keystore and a Play service account, so this fails until you have both. `add workflows` writes them later.\", io);\n}\n\n/**\n * The package manager, asked only when nothing else could say.\n *\n * `npm create` and `pnpm create` both set `npm_config_user_agent`, so this is\n * reached by the paths that do not: a global install, or running the binary\n * directly. Rare, but it decides what every generated script says, and the old\n * silent fallback to pnpm was a guess printed into a project's package.json.\n */\nexport async function askForPackageManager(io: Io = {}): Promise<PackageManager | null> {\n const answer = await select({\n message: \"Which package manager should the generated scripts use?\",\n options: [\n { value: \"pnpm\", label: \"pnpm\" },\n { value: \"npm\", label: \"npm\" },\n ],\n input: io.input ?? process.stdin,\n output: io.output ?? process.stdout,\n });\n return isCancel(answer) ? null : (answer as PackageManager);\n}\n","/**\n * The closing summary, and the only output of a scaffold anyone reads.\n *\n * A run drives several tools with plenty to say, so where the project went,\n * what to edit, and what to run are worth restating at the bottom. Returned in\n * pieces rather than as one block, because the CLI renders each in a different\n * shape: the outcomes as status lines, the commands as a boxed note, the\n * destination as the closing line.\n *\n * Its own module so it can be asserted: `cli.ts` is the bin entry and runs\n * `main` on import, which makes anything defined there untestable. The package\n * `exports` map does not expose this, so it is internal despite being\n * importable by the tests.\n */\nimport type { PackageManager, ScaffoldResult } from \"./index.js\";\n\nexport interface Summary {\n /** What became of the repository and of Firebase, when there is something to say. */\n status: string[];\n /** The commands worth having, as one block. */\n next: string;\n /** One-line asides, after the block. */\n footnotes: string[];\n /** The closing line. */\n headline: string;\n}\n\n/** Two-column rows, aligned to the widest left cell. */\nfunction columns(rows: [string, string][]): string[] {\n const width = Math.max(...rows.map(([left]) => left.length));\n return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);\n}\n\nexport function summarise(result: ScaffoldResult, manager: PackageManager, workflows: boolean): Summary {\n const run = manager === \"npm\" ? \"npm run\" : \"pnpm\";\n const status: string[] = [];\n\n // Only the outcomes that leave the reader something to know. `failed` has\n // already warned, in more detail than a summary line could carry.\n if (result.git === \"committed\") status.push(`Committed as \"Scaffold ${result.name}\"`);\n if (result.git === \"existing\") status.push(\"Created inside a repository you already had, so nothing was committed\");\n // `failed` has warned already, and the step reappears below as the next\n // thing to run, which is the whole of the advice.\n if (result.firebase === \"configured\") status.push(result.git === \"committed\" ? \"Firebase is configured, and everything it generated is in that commit\" : \"Firebase is configured\");\n\n const next = [\n \"Write the game\",\n ...columns([\n [\"server/src/module/v1.ts\", \"the authoritative rules\"],\n [\"app/lib/game/v1/rules.dart\", \"the presentation twin\"],\n ]),\n \"\",\n `Then, from ${result.name}/`,\n ...columns([\n [`cd server && ${run} test:watch`, \"rules and twin fixtures\"],\n [`cd server && ${run} dev`, \"the Worker, on http://localhost:8787\"],\n [`${run} contract`, \"regenerate after a schema change\"],\n // Named here because it is the one step the game itself cannot do\n // without: rules, fixtures and the Worker all run unconfigured, and the\n // app throws `Firebase is not configured` at launch until this has run\n // once.\n ...(result.firebase === \"configured\" ? [] : [[`${run} firebase:configure`, \"connect Firebase before the app runs\"] as [string, string]]),\n ]),\n ].join(\"\\n\");\n\n const footnotes = [\"Docs: https://eigeninteractive.com\"];\n if (!workflows) footnotes.unshift(\"No CI workflows yet: `create-eigen-game add workflows` writes them when shipping is the next step.\");\n\n return { status, next, footnotes, headline: `${result.name} is ready, at ${result.root}` };\n}\n"],"mappings":"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","names":["color","color","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/prompt.ts","../src/summary.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { cancel, intro, isCI, isTTY, log, note, outro, taskLog } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport { addContinuousIntegration, applicationId, destinationProblem, detectPackageManager, type FirebaseProblem, firebaseReadiness, insideWorkTree, normaliseTerminalWidth, type PackageManager, plainReporter, type Reporter, scaffoldGame } from \"./index.js\";\nimport { askForGit, askForOrg, askForPackageManager, askForWorkflows, askToScaffoldWithoutFirebase, DEFAULT_ORG, ORG } from \"./prompt.js\";\nimport { summarise } from \"./summary.js\";\n\nconst version = (JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), \"../package.json\"), \"utf8\")) as { version: string }).version;\n\nconst PREREQUISITES = \"https://eigeninteractive.com/docs/getting-started/prerequisites\";\n\nconst help = `Usage: create-eigen-game <game-slug> [options]\n create-eigen-game add workflows [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\nEvery option below answers a question the CLI would otherwise ask. Pass it to\nskip the question; leave it out to be asked. With no terminal to ask on (CI, a\npipe, an agent session) an unanswered question is an error rather than a\ndefault quietly chosen for you, and the message prints the command to re-run.\n\nOptions:\n --org <reverse-domain> Android/iOS organization. The applicationId is this\n plus the game name, as in com.example.my_game, and\n Google Play makes it permanent at first upload\n --firebase-project <id>\n The Firebase project to configure against. Also the\n form that works with no terminal, since otherwise\n FlutterFire asks\n --no-firebase Do not configure Firebase, leaving firebase:configure\n as a step for later\n --git, --no-git Initialise a repository and commit the scaffold\n --workflows, --no-workflows\n Emit the GitHub Actions workflows. release.yml needs\n an upload keystore and a Play service account, so it\n fails on every push until both exist\n --package-manager <pm> npm or pnpm. Taken from the invoking package manager\n when there is one, so this is for global installs\n -h, --help Show this help\n -v, --version Show the version\n\nCommands:\n add workflows [directory]\n Add the workflows to an existing project, for when\n you are ready to ship rather than at scaffold time.\n Defaults to the current directory.`;\n\nnormaliseTerminalWidth(process.stdout);\n\n/**\n * Whether to draw, using clack's own predicates rather than a hand-rolled\n * `isTTY`, the same pair `taskLog` consults internally. A pipe has nowhere to\n * put a redraw, and a CI log is read after the fact, in full, by someone who\n * wants every line a tool printed.\n */\nconst decorated = isTTY(process.stdout) && !isCI();\n\n/**\n * The CLI's voice, in whichever of the two registers this terminal supports.\n *\n * Every message goes through here so the undecorated path cannot drift into\n * being the untested one.\n */\nconst ui = decorated\n ? {\n open: () => intro(color.inverse(\" EigenInteractive \")),\n info: (message: string) => log.info(message),\n warn: (message: string) => log.warn(message),\n success: (message: string) => log.success(message),\n block: (body: string) => note(body, \"Next\"),\n close: (message: string) => outro(color.bold(message)),\n stop: (message: string) => cancel(message),\n }\n : {\n open: () => console.log(\"\\nEigenInteractive\\n\"),\n info: (message: string) => console.log(message),\n warn: (message: string) => console.warn(message),\n success: (message: string) => console.log(message),\n block: (body: string) => console.log(`\\n${body}\\n`),\n close: (message: string) => console.log(message),\n stop: (message: string) => console.error(message),\n };\n\nfunction resolveManager(requested: string | undefined): PackageManager | undefined {\n if (requested === undefined) return undefined;\n if (requested !== \"npm\" && requested !== \"pnpm\") {\n throw new Error(\"--package-manager must be npm or pnpm\");\n }\n return requested;\n}\n\n/**\n * Why a scaffold asks about a second service at all, said once, before the\n * first question rather than in the error that follows it.\n *\n * Firebase is not an integration this engine offers; it is where a player\n * comes from. Somebody meeting that for the first time deserves the reason in\n * the same breath as the requirement, and deserves to know the size of it:\n * one free project, shared by every game they build.\n */\nfunction greet(): void {\n ui.open();\n ui.info(\"A game is one repository with two halves: a Cloudflare Worker that owns the rules,\\nand a Flutter app for Android and the web. This writes both, wired together and\\nalready playable.\");\n ui.info(\n `Firebase signs your players in. The Worker verifies Firebase ID tokens to decide who\\nholds a seat, and sends turn notifications through the same project, so a scaffolded\\napp throws ${color.dim(\"Firebase is not configured\")} at launch until one is connected.\\n\\nIt is free to start, one project serves every game you build, and this connects yours\\nas it goes. ${color.dim(PREREQUISITES)}`,\n );\n}\n\n/** What is missing, and the one command that fixes each: the same list the warning and the parting message are both built from. */\nfunction firebaseReport(problems: FirebaseProblem[]): string {\n const lines = problems.flatMap((problem) => [` ${problem.reason}`, ` ${color.cyan(problem.fix)}`]);\n // `dart pub global activate` does not put the binary on PATH, which is the\n // next thing to go wrong for anyone who has just run the command above.\n if (problems.some((problem) => problem.fix.startsWith(\"dart pub\"))) {\n lines.push(` ${color.dim('…then, if your shell cannot find it: export PATH=\"$PATH\":\"$HOME/.pub-cache/bin\"')}`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * The parting message when someone answers \"no\" to scaffolding without\n * Firebase: every command they need, in the order they run in.\n *\n * Deliberately not a failure of the scaffold: nothing has been written, they\n * asked for that, and the exit code is non-zero only so a script that wrapped\n * this can tell there is no project.\n */\nfunction stopForFirebase(problems: FirebaseProblem[]): void {\n // The commands only. What each one is for is in the warning directly above,\n // and a `note` draws a box sized to its longest line, so repeating the\n // reasons here is what makes it wrap into something unreadable at 80\n // columns, which is exactly the width this is most likely to be read at.\n note(`${problems.map((problem) => ` ${color.cyan(problem.fix)}`).join(\"\\n\")}\\n\\nThen run this again.\\n${color.dim(PREREQUISITES)}`, \"Set up Firebase\");\n ui.stop(\"Nothing was written.\");\n process.exitCode = 1;\n}\n\n/**\n * The error for a run with no terminal and an unanswered question.\n *\n * The strictness is the point: a default applied where nobody can see it is\n * how `com.example.my_game` reaches Google Play, which freezes it at the first\n * upload. What keeps that from being merely annoying is this message: the\n * whole command, with every default already filled in, so the fix is one\n * paste and the one value worth changing is visible in it.\n */\nfunction unanswered(directory: string, missing: { flags: string; suggestion: string }[]): string {\n const command = [\"npx create-eigen-game\", directory, ...missing.map((question) => question.suggestion)].join(\" \");\n return [`No terminal to ask on, so every answer has to come from a flag. These were not given:`, \"\", ...missing.map((question) => ` ${question.flags}`), \"\", \"That command, with each default filled in. Read them before you run it:\", \"\", ` ${command}`].join(\"\\n\");\n}\n\n/** `--foo` and `--no-foo`, which `parseArgs` has no notion of: `undefined` means nobody said, which is what makes it a question. */\nfunction flagPair(yes: boolean | undefined, no: boolean | undefined, name: string): boolean | undefined {\n if (yes === true && no === true) throw new Error(`--${name} and --no-${name} contradict each other`);\n if (yes === true) return true;\n if (no === true) return false;\n return undefined;\n}\n\n/** `--org \"\"` falls through to the default rather than failing, matching what an empty answer to the prompt does. */\nfunction resolveOrg(value: string): string | undefined {\n if (value.trim() === \"\") return undefined;\n if (!ORG.test(value)) {\n throw new Error(`invalid organization: ${value}\\n\\nUse reverse domain notation: two or more dot-separated segments of letters, digits and underscores, each starting with a letter, as in ${DEFAULT_ORG} or dev.yourname.games.`);\n }\n return value;\n}\n\n/**\n * Shows each step running, and keeps its output only when it fails.\n *\n * Every tool a scaffold drives has plenty to say (pub resolving 179\n * dependencies, two icon generators, a package manager) and none of it is\n * read while it scrolls past. `taskLog` is exactly that bargain: the stream is\n * visible while the step runs, cleared when it succeeds, and left on screen\n * when it does not, so a failure is still debuggable from what is in the\n * terminal.\n *\n * `handOver` is the exception, and the reason `interactive` exists at all.\n * FlutterFire asks which Firebase project to use, so that step needs the\n * terminal rather than a captured pipe.\n */\nfunction clackReporter(): Reporter {\n // `taskLog` redraws by clearing lines, which is an escape-sequence storm in\n // anything that is not a terminal: a CI log, a pipe, `scaffold-e2e.mjs`.\n // There, the honest thing is the plain reporter: every tool's output, in\n // full, which is what a log gets read for anyway.\n if (!decorated) return plainReporter;\n\n let task: ReturnType<typeof taskLog> | undefined;\n let interactive = false;\n\n return {\n step(label, body) {\n task = taskLog({ title: label });\n try {\n const value = body();\n task.success(label);\n return value;\n } catch (error) {\n task.error(label);\n throw error;\n } finally {\n task = undefined;\n }\n },\n handOver(label, body) {\n log.step(`${label} ${color.dim(\"(FlutterFire takes over here)\")}`);\n interactive = true;\n try {\n return body();\n } finally {\n interactive = false;\n }\n },\n emit(output) {\n const text = output.trimEnd();\n if (text !== \"\") task?.message(text);\n },\n warn(message) {\n ui.warn(message);\n },\n get interactive() {\n return interactive;\n },\n };\n}\n\nasync function main(args: string[]): Promise<void> {\n const { values, positionals } = parseArgs({\n args,\n allowPositionals: true,\n strict: true,\n options: {\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n org: { type: \"string\" },\n \"firebase-project\": { type: \"string\" },\n // Each negation is declared under the name it is typed with, because\n // `parseArgs` has no boolean negation of its own. `flagPair` puts the\n // two halves back together, and the third state, neither given, is the\n // one that matters: it is what turns the flag into a question.\n \"no-firebase\": { type: \"boolean\" },\n git: { type: \"boolean\" },\n \"no-git\": { type: \"boolean\" },\n workflows: { type: \"boolean\" },\n \"no-workflows\": { type: \"boolean\" },\n \"package-manager\": { type: \"string\" },\n },\n });\n\n if (values.help) {\n console.log(help);\n return;\n }\n if (values.version) {\n console.log(version);\n return;\n }\n\n const requestedManager = resolveManager(values[\"package-manager\"]);\n\n if (positionals[0] === \"add\") {\n // `ci` is the name this shipped under, and it is written into the README\n // of every project already scaffolded. Those READMEs are on disk and are\n // not going to be edited, so the old spelling keeps working; only the new\n // one is documented.\n if (positionals[1] !== \"workflows\" && positionals[1] !== \"ci\") {\n throw new Error(`unknown subcommand: add ${positionals[1] ?? \"\"}\\n\\n${help}`.trim());\n }\n if (positionals.length > 3) {\n throw new Error(`expected at most one directory\\n\\n${help}`);\n }\n const result = addContinuousIntegration({\n directory: positionals[2] ?? process.cwd(),\n packageManager: requestedManager,\n });\n console.log(`Added ${result.files.join(\" and \")} in ${result.root}`);\n return;\n }\n\n if (positionals.length !== 1) {\n throw new Error(`expected exactly one destination directory\\n\\n${help}`);\n }\n\n const directory = positionals[0];\n\n // Everything that makes the command impossible, before the greeting rather\n // than after the last question. `applicationId` validates the slug;\n // `destinationProblem` is the one `scaffoldGame` would raise at the far end,\n // asked here instead. Being told the directory was occupied *after* giving\n // an organization is the same insult as being told about `flutterfire` after\n // two minutes of Flutter and pub.\n const game = basename(resolve(directory));\n applicationId(directory);\n const occupied = destinationProblem(directory);\n if (occupied !== undefined) throw new Error(occupied);\n\n greet();\n\n // Every question this run will ask, settled before any of it starts. A flag\n // is an answer already given; anything left over is asked, or, with nothing\n // to ask on, collected here and reported together, so one re-run can carry\n // all of them.\n const interactive = process.stdin.isTTY === true;\n const missing: { flags: string; suggestion: string }[] = [];\n const cancelled = () => {\n ui.stop(\"Nothing was written.\");\n process.exitCode = 130;\n };\n\n /**\n * Records a question that could not be asked, and hands back the default it\n * suggested. The value is never used, since `missing` throws before anything\n * is written, but returning it keeps each answer definitely typed instead of\n * carrying a `| undefined` all the way to the scaffold call.\n */\n const needsFlag = <T>(flags: string, suggestion: string, fallback: T): T => {\n missing.push({ flags, suggestion });\n return fallback;\n };\n\n // Firebase goes first because it is the only answer that can end the run,\n // and ending it after the organization prompt would mean asking for a\n // permanent decision about a project that never gets created.\n const project = values[\"firebase-project\"]?.trim();\n if (project === \"\") throw new Error(\"--firebase-project needs a project id\");\n if (project !== undefined && values[\"no-firebase\"] === true) {\n throw new Error(\"--firebase-project and --no-firebase contradict each other\");\n }\n\n let firebase: boolean | string = false;\n if (project !== undefined) {\n firebase = project;\n } else if (values[\"no-firebase\"] !== true) {\n const readiness = firebaseReadiness();\n if (readiness.ready) {\n // Nothing for this CLI to ask: FlutterFire runs its own project picker\n // during the scaffold, and needs a terminal to do it.\n if (interactive) firebase = true;\n else firebase = needsFlag(\"--firebase-project <id>, or --no-firebase\", \"--no-firebase\", false);\n } else {\n ui.warn(`Firebase is not set up on this machine:\\n\\n${firebaseReport(readiness.problems)}`);\n if (!interactive) {\n firebase = needsFlag(\"--no-firebase, to scaffold without it\", \"--no-firebase\", false);\n } else {\n const carryOn = await askToScaffoldWithoutFirebase();\n if (carryOn === null) return cancelled();\n if (!carryOn) return stopForFirebase(readiness.problems);\n }\n }\n }\n\n let org: string | undefined;\n if (values.org !== undefined) {\n org = resolveOrg(values.org);\n } else if (interactive) {\n const answer = await askForOrg(game.replaceAll(\"-\", \"_\"), firebase !== false);\n if (answer === null) return cancelled();\n org = answer;\n } else {\n org = needsFlag(\"--org <reverse-domain>\", `--org ${DEFAULT_ORG}`, undefined);\n }\n\n // A destination inside a repository someone already has is not a question:\n // `scaffoldGame` declines to nest one repository in another whatever it is\n // told, and says so in the summary.\n let git = flagPair(values.git, values[\"no-git\"], \"git\");\n if (git === undefined && insideWorkTree(directory)) git = true;\n if (git === undefined) {\n if (!interactive) git = needsFlag(\"--git or --no-git\", \"--git\", true);\n else {\n const answer = await askForGit();\n if (answer === null) return cancelled();\n git = answer;\n }\n }\n\n let workflows = flagPair(values.workflows, values[\"no-workflows\"], \"workflows\");\n if (workflows === undefined) {\n if (!interactive) workflows = needsFlag(\"--workflows or --no-workflows\", \"--no-workflows\", false);\n else {\n const answer = await askForWorkflows();\n if (answer === null) return cancelled();\n workflows = answer;\n }\n }\n\n // Reached only when nothing else could say. `npm create` and `pnpm create`\n // both announce themselves in `npm_config_user_agent`. What is left is a\n // global install, where the old silent fallback to pnpm was a guess written\n // into every script in the generated project.\n let manager = requestedManager ?? detectPackageManager();\n if (manager === undefined) {\n if (!interactive) manager = needsFlag(\"--package-manager <npm|pnpm>\", \"--package-manager pnpm\", \"pnpm\" as PackageManager);\n else {\n const answer = await askForPackageManager();\n if (answer === null) return cancelled();\n manager = answer;\n }\n }\n\n if (missing.length > 0) throw new Error(unanswered(directory, missing));\n\n ui.success(`applicationId ${color.bold(applicationId(directory, org))}`);\n\n const result = scaffoldGame({\n directory,\n org,\n packageManager: manager,\n ci: workflows,\n git,\n firebase,\n reporter: clackReporter(),\n });\n\n const { status, next, footnotes, headline } = summarise(result, manager, workflows);\n for (const line of status) ui.success(line);\n ui.block(next);\n for (const line of footnotes) ui.info(line);\n ui.close(headline);\n}\n\ntry {\n await main(process.argv.slice(2));\n} catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n ui.stop(`create-eigen-game: ${message}`);\n process.exitCode = 1;\n}\n","/**\n * Every question this CLI asks, in one module so the wording lives together\n * and can be asserted. `cli.ts` is the bin entry and runs `main` on import,\n * which makes anything defined there untestable.\n *\n * The organization prompt is the reason this drops to `@clack/core` rather\n * than using `@clack/prompts`' `text`. The suffix: `flutter create --org X\n * --project-name Y` derives `X.Y`, so the organization is a *prefix*, and the\n * reliable way to say so is to render the rest of the identifier, dimmed,\n * while it is being typed. `text` has no hook for that; a custom `TextPrompt`\n * render returns a string, so it does.\n *\n * Everything else comes from `@clack/prompts` rather than being drawn here.\n * Its symbols are exported for exactly this, and they are not constants: each\n * falls back to ASCII on a terminal that cannot show the box-drawing\n * characters, which a hand-typed `│` cannot do.\n */\nimport type { Readable, Writable } from \"node:stream\";\nimport { TextPrompt } from \"@clack/core\";\nimport { confirm, isCancel, S_BAR, S_BAR_END, select, symbol } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport type { PackageManager } from \"./index.js\";\n\n/**\n * Where a question reads its answer from. Parameters for the same reason every\n * clack prompt takes them: a prompt is otherwise the one part of a CLI that\n * cannot be driven by a test.\n */\nexport interface Io {\n input?: Readable;\n output?: Writable;\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 */\nexport const ORG = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$/;\n\n/** The default, which an empty answer accepts. */\nexport const DEFAULT_ORG = \"com.example\";\n\nconst bar = color.gray(S_BAR);\n\n/** A `com.acme.chess` answer to a game called `chess`, which yields `com.acme.chess.chess`. */\nexport function repeatsGameName(org: string, game: string): boolean {\n return org.split(\".\").at(-1) === game;\n}\n\n/** The organization implied by an answer that already ends in the game name. */\nexport function withoutGameName(org: string): string | undefined {\n const shorter = org.split(\".\").slice(0, -1).join(\".\");\n return ORG.test(shorter) ? shorter : undefined;\n}\n\n/**\n * Asks, with the game name rendered dimmed after the cursor so the shape of\n * the answer is visible while it is being given.\n *\n * Returns the organization, `undefined` for the default, or `null` when\n * cancelled.\n *\n * `input` and `output` default to this process's, and are parameters for the\n * same reason every clack prompt takes them: a prompt is otherwise the one\n * part of a CLI that cannot be driven by a test.\n */\nexport async function askForOrg(game: string, registering: boolean, io: Io = {}): Promise<string | undefined | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n const suffix = color.dim(`.${game}`);\n const prompt = new TextPrompt({\n input,\n output,\n // An empty answer is the default, not a mistake.\n validate: (value) => (value === undefined || value === \"\" || ORG.test(value) ? undefined : \"Two or more dot-separated segments, as in com.example or dev.yourname.games.\"),\n render() {\n const typed = String(this.value ?? \"\");\n const shown = typed === \"\" ? `${color.dim(DEFAULT_ORG)}${suffix}` : `${this.userInputWithCursor}${suffix}`;\n const problem = this.state === \"error\" ? `\\n${bar} ${color.yellow(this.error ?? \"\")}` : \"\";\n return [`${bar}`, `${symbol(this.state)} Organization in reverse domain notation`, `${bar} ${shown}${problem}`, color.gray(S_BAR_END)].join(\"\\n\");\n },\n });\n\n // The two things that make this answer permanent, said before it is given\n // rather than after: Google Play freezes the identifier at the first upload,\n // and FlutterFire matches an existing Android app on exactly this string, so\n // it also decides whether the run adopts an app the chosen Firebase project\n // already has or registers a new one.\n output.write(`${bar} ${color.dim(\"Prefixes the Android applicationId, which Google Play makes permanent at first upload.\")}\\n`);\n if (registering) output.write(`${bar} ${color.dim(\"Also the Android app registered in the Firebase project you pick next.\")}\\n`);\n\n const answer = await prompt.prompt();\n if (isCancel(answer)) return null;\n\n const org = String(answer).trim();\n if (org === \"\" || org === DEFAULT_ORG) return undefined;\n\n // The mistake the dimmed suffix is there to prevent, caught for the person\n // who pasted an identifier rather than typing one and never watched it grow.\n const shorter = repeatsGameName(org, game) ? withoutGameName(org) : undefined;\n if (shorter !== undefined) {\n const shorten = await confirm({\n message: `That gives ${color.yellow(`${org}.${game}`)}, the game name twice. Use ${color.green(shorter)} instead?`,\n initialValue: true,\n input,\n output,\n });\n if (isCancel(shorten)) return null;\n if (shorten) return shorter;\n }\n\n return org;\n}\n\n/**\n * A yes/no question, with its reason on the line above.\n *\n * The three below share this so cancellation is handled in one place: ^C\n * during any question has to mean the run stops having written nothing, and\n * that is only true if every call site checks. `null` is that answer.\n *\n * `hint` is written rather than passed to `confirm`, which has nowhere to put\n * it. It is the same dimmed line the organization prompt uses, and carries the\n * thing that makes the default the default: a question whose answer is\n * pre-chosen owes the reader why.\n */\nasync function ask(message: string, initialValue: boolean, hint: string, io: Io): Promise<boolean | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n output.write(`${bar}\\n${bar} ${color.dim(hint)}\\n`);\n const answer = await confirm({ message, initialValue, input, output });\n return isCancel(answer) ? null : answer;\n}\n\n/**\n * Asked only when {@link firebaseReadiness} found something missing, and\n * defaulted to \"no\" because the honest answer usually is: the tools are two\n * commands away, and the alternative is an app that throws at launch until\n * they are installed anyway.\n */\nexport function askToScaffoldWithoutFirebase(io: Io = {}): Promise<boolean | null> {\n return ask(\"Scaffold anyway, and connect Firebase yourself later?\", false, \"Everything except the app's sign-in still works, and `firebase:configure` finishes the job.\", io);\n}\n\n/** Defaulted to yes: the scaffold is a few hundred generated files, and a commit is what makes the first `git diff` the first game change rather than all of them. */\nexport function askForGit(io: Io = {}): Promise<boolean | null> {\n return ask(\"Initialise a git repository and commit the scaffold?\", true, \"Commits once, so your first diff is your first game change.\", io);\n}\n\n/**\n * Defaulted to no, and the hint says why rather than leaving it as an\n * arbitrary-looking choice: `release.yml` needs an upload keystore and a Play\n * service account, so a project that takes the workflows before it has either\n * gets a red `main` from its first push.\n */\nexport function askForWorkflows(io: Io = {}): Promise<boolean | null> {\n return ask(\"Add the GitHub Actions workflows?\", false, \"Release needs a signing keystore and a Play service account, so this fails until you have both. `add workflows` writes them later.\", io);\n}\n\n/**\n * The package manager, asked only when nothing else could say.\n *\n * `npm create` and `pnpm create` both set `npm_config_user_agent`, so this is\n * reached by the paths that do not: a global install, or running the binary\n * directly. Rare, but it decides what every generated script says, and the old\n * silent fallback to pnpm was a guess printed into a project's package.json.\n */\nexport async function askForPackageManager(io: Io = {}): Promise<PackageManager | null> {\n const answer = await select({\n message: \"Which package manager should the generated scripts use?\",\n options: [\n { value: \"pnpm\", label: \"pnpm\" },\n { value: \"npm\", label: \"npm\" },\n ],\n input: io.input ?? process.stdin,\n output: io.output ?? process.stdout,\n });\n return isCancel(answer) ? null : (answer as PackageManager);\n}\n","/**\n * The closing summary, and the only output of a scaffold anyone reads.\n *\n * A run drives several tools with plenty to say, so where the project went,\n * what to edit, and what to run are worth restating at the bottom. Returned in\n * pieces rather than as one block, because the CLI renders each in a different\n * shape: the outcomes as status lines, the commands as a boxed note, the\n * destination as the closing line.\n *\n * Its own module so it can be asserted: `cli.ts` is the bin entry and runs\n * `main` on import, which makes anything defined there untestable. The package\n * `exports` map does not expose this, so it is internal despite being\n * importable by the tests.\n */\nimport type { PackageManager, ScaffoldResult } from \"./index.js\";\n\nexport interface Summary {\n /** What became of the repository and of Firebase, when there is something to say. */\n status: string[];\n /** The commands worth having, as one block. */\n next: string;\n /** One-line asides, after the block. */\n footnotes: string[];\n /** The closing line. */\n headline: string;\n}\n\n/** Two-column rows, aligned to the widest left cell. */\nfunction columns(rows: [string, string][]): string[] {\n const width = Math.max(...rows.map(([left]) => left.length));\n return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);\n}\n\nexport function summarise(result: ScaffoldResult, manager: PackageManager, workflows: boolean): Summary {\n const run = manager === \"npm\" ? \"npm run\" : \"pnpm\";\n const status: string[] = [];\n\n // Only the outcomes that leave the reader something to know. `failed` has\n // already warned, in more detail than a summary line could carry.\n if (result.git === \"committed\") status.push(`Committed as \"Scaffold ${result.name}\"`);\n if (result.git === \"existing\") status.push(\"Created inside a repository you already had, so nothing was committed\");\n // `failed` has warned already, and the step reappears below as the next\n // thing to run, which is the whole of the advice.\n if (result.firebase === \"configured\") status.push(result.git === \"committed\" ? \"Firebase is configured, and everything it generated is in that commit\" : \"Firebase is configured\");\n if (result.link?.projectId != null) status.push(`FIREBASE_PROJECT_ID is set to ${result.link.projectId}, and app-config.json points at the local Worker`);\n\n // What is left, and only what is left. Everything here comes from the\n // Firebase console rather than from any CLI, which is why a scaffold that\n // filled in everything it could still ends with a list: the web target\n // refuses to start without a VAPID key, and there is no OAuth client to copy\n // until somebody enables the provider that creates it.\n const remaining: [string, string][] = [];\n if (result.link !== undefined) {\n if (result.link.googleWebClientId === null) {\n remaining.push([\"Enable Google sign-in\", \"Console > Authentication > Sign-in method\"]);\n remaining.push([\"GOOGLE_WEB_CLIENT_ID\", \"that same page, under Web SDK configuration\"]);\n }\n remaining.push([\"FIREBASE_VAPID_KEY\", \"Console > Project settings > Cloud Messaging > Web configuration\"]);\n }\n\n const next = [\n \"Write the game\",\n ...columns([\n [\"server/src/module/v1.ts\", \"the authoritative rules\"],\n [\"app/lib/game/v1/rules.dart\", \"the presentation twin\"],\n ]),\n \"\",\n `Then, from ${result.name}/`,\n ...columns([\n [`cd server && ${run} test:watch`, \"rules and twin fixtures\"],\n [`cd server && ${run} dev`, \"the Worker, on http://localhost:8787\"],\n [`${run} contract`, \"regenerate after a schema change\"],\n // Named here because it is the one step the game itself cannot do\n // without: rules, fixtures and the Worker all run unconfigured, and the\n // app throws `Firebase is not configured` at launch until this has run\n // once.\n ...(result.firebase === \"configured\" ? [] : [[`${run} firebase:configure`, \"connect Firebase before the app runs\"] as [string, string]]),\n ]),\n ...(remaining.length === 0 ? [] : [\"\", \"Before the app runs, from the Firebase console\", ...columns(remaining), \"\", \" The values go in app/app-config.json. Both are public, not secrets.\"]),\n ].join(\"\\n\");\n\n const footnotes = [\"Docs: https://eigeninteractive.com\"];\n if (!workflows) footnotes.unshift(\"No CI workflows yet: `create-eigen-game add workflows` writes them when shipping is the next step.\");\n\n return { status, next, footnotes, headline: `${result.name} is ready, at ${result.root}` };\n}\n"],"mappings":"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","names":["color","color","result"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** What the Firebase step managed to settle, and what it left for the console. */
|
|
2
|
+
interface FirebaseLink {
|
|
3
|
+
/** The project now in `wrangler.jsonc`, or null when FlutterFire recorded none. */
|
|
4
|
+
projectId: string | null;
|
|
5
|
+
/** The OAuth web client now in `app-config.json`, or null when Google sign-in is not enabled yet. */
|
|
6
|
+
googleWebClientId: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
type PackageManager = "npm" | "pnpm";
|
|
2
10
|
type Runner = (command: string, args: string[], cwd: string) => void;
|
|
3
11
|
/**
|
|
@@ -109,6 +117,12 @@ interface ScaffoldResult {
|
|
|
109
117
|
* step or spell it out. `failed` has already warned.
|
|
110
118
|
*/
|
|
111
119
|
firebase: "configured" | "failed" | "skipped";
|
|
120
|
+
/**
|
|
121
|
+
* What the Firebase step made knowable and this scaffold therefore filled
|
|
122
|
+
* in. Absent unless `firebase` is `configured`, since there is nothing to
|
|
123
|
+
* read otherwise.
|
|
124
|
+
*/
|
|
125
|
+
link?: FirebaseLink;
|
|
112
126
|
}
|
|
113
127
|
/**
|
|
114
128
|
* The Android `applicationId`, and iOS bundle id, a scaffold at `directory`
|
|
@@ -212,4 +226,4 @@ declare function destinationProblem(directory: string): string | undefined;
|
|
|
212
226
|
declare function insideWorkTree(directory: string, probe?: Probe): boolean;
|
|
213
227
|
declare function scaffoldGame(options: ScaffoldOptions): ScaffoldResult;
|
|
214
228
|
|
|
215
|
-
export { type AddContinuousIntegrationOptions, type FirebaseProblem, type FirebaseReadiness, type GitOutcome, type PackageManager, type Probe, type Reporter, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, applicationId, capturingRunner, decodeUtf8, destinationProblem, detectPackageManager, firebaseReadiness, insideWorkTree, normaliseTerminalWidth, plainReporter, scaffoldGame };
|
|
229
|
+
export { type AddContinuousIntegrationOptions, type FirebaseLink, type FirebaseProblem, type FirebaseReadiness, type GitOutcome, type PackageManager, type Probe, type Reporter, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, applicationId, capturingRunner, decodeUtf8, destinationProblem, detectPackageManager, firebaseReadiness, insideWorkTree, normaliseTerminalWidth, plainReporter, 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.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Scaffold an EigenInteractive game Worker and Flutter app without cloning the engine repositories.",
|
|
6
6
|
"type": "module",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"vitest": "^4.1.10",
|
|
30
30
|
"wrangler": "^4.110.0",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@eigeninteractive/rules": "0.
|
|
33
|
-
"@eigeninteractive/server": "0.
|
|
34
|
-
"@eigeninteractive/testkit": "0.
|
|
32
|
+
"@eigeninteractive/rules": "0.3.0",
|
|
33
|
+
"@eigeninteractive/server": "0.3.0",
|
|
34
|
+
"@eigeninteractive/testkit": "0.3.0"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=22"
|
|
@@ -54,9 +54,21 @@ to satisfy it as shipped.
|
|
|
54
54
|
|
|
55
55
|
The initial scaffold already generates Dart payloads from the Worker contract.
|
|
56
56
|
Implement the client-side legality, preview and presentation rules under
|
|
57
|
-
`app/lib/game
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
`app/lib/game/`.
|
|
58
|
+
|
|
59
|
+
`app/app-config.json` holds the public build-time values. They are Dart
|
|
60
|
+
compilation environment declarations, not secrets; private credentials belong
|
|
61
|
+
only in the Worker. Scaffolding fills in `API_BASE_URL` for local development
|
|
62
|
+
and, once Firebase is configured, `GOOGLE_WEB_CLIENT_ID`. Two are left:
|
|
63
|
+
|
|
64
|
+
- `FIREBASE_VAPID_KEY`, from Firebase Console > Project settings > Cloud
|
|
65
|
+
Messaging > Web configuration. The web app will not start without it.
|
|
66
|
+
- `APP_HOST`, your hostname at deploy time, which turns on invite and replay
|
|
67
|
+
sharing.
|
|
68
|
+
|
|
69
|
+
An empty `GOOGLE_WEB_CLIENT_ID` means the Google sign-in provider was not
|
|
70
|
+
enabled when the project was configured. Turn it on under Authentication >
|
|
71
|
+
Sign-in method, then copy the Web SDK configuration client id.
|
|
60
72
|
|
|
61
73
|
See `app/lib/game/README.md` for the regeneration command.
|
|
62
74
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"contract": "cd server && {{PACKAGE_MANAGER}} run contract && cd ../app && dart run eigen_flutter:generate_payloads --contract ../server/game-contract.json --output lib/game/generated/payloads.dart --fixtures-output test/fixtures",
|
|
8
8
|
"contract:check": "cd server && {{PACKAGE_MANAGER}} run contract:check && cd ../app && dart run eigen_flutter:generate_payloads --contract ../server/game-contract.json --output lib/game/generated/payloads.dart --fixtures-output test/fixtures --check",
|
|
9
9
|
"deploy": "{{PACKAGE_MANAGER}} run build:web && cd server && {{PACKAGE_MANAGER}} run deploy",
|
|
10
|
-
"firebase:configure": "cd app && dart run eigen_flutter:configure_firebase",
|
|
10
|
+
"firebase:configure": "cd app && dart run eigen_flutter:configure_firebase --worker ../server",
|
|
11
11
|
"format": "biome check --write .",
|
|
12
12
|
"lint": "biome check ."
|
|
13
13
|
},
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"contract:check": "eigen-contract --check",
|
|
32
32
|
"deploy": "wrangler d1 migrations apply GAME_DB --remote && wrangler deploy",
|
|
33
33
|
"dev": "{{PACKAGE_MANAGER}} run db:migrate:local && wrangler dev",
|
|
34
|
+
"inspect": "eigen-inspect",
|
|
34
35
|
"start": "{{PACKAGE_MANAGER}} run db:migrate:local && wrangler dev",
|
|
35
36
|
"test": "vitest run",
|
|
36
37
|
"test:watch": "vitest",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { configDefaults, defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The twin fixtures are read with `readFileSync` at collect time, so they are
|
|
5
|
+
* not in Vite's module graph, and `test:watch` would ignore a fixture-only
|
|
6
|
+
* edit: Vitest looks a changed path up in the graph, finds nothing, and reruns
|
|
7
|
+
* nothing. `forceRerunTriggers` is the supported way to watch a file consumed
|
|
8
|
+
* outside the graph, and Vitest's own defaults use it for `package.json` and
|
|
9
|
+
* the config files for exactly that reason.
|
|
10
|
+
*
|
|
11
|
+
* Spread the defaults rather than replacing them. Config resolution is a
|
|
12
|
+
* shallow merge, so a bare array silently stops manifest and config edits from
|
|
13
|
+
* triggering a rerun.
|
|
14
|
+
*
|
|
15
|
+
* A match reruns the whole suite rather than one file. That is cheap here
|
|
16
|
+
* because these tests are pure Node. Keep the trigger off any
|
|
17
|
+
* `@cloudflare/vitest-pool-workers` project added later, where a fixture
|
|
18
|
+
* cannot change the result and the rebuild is not free.
|
|
19
|
+
*/
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
test: {
|
|
22
|
+
forceRerunTriggers: [...configDefaults.forceRerunTriggers, "**/src/module/fixtures/**/*.json"],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -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, rmdirSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { ENGINE_PACKAGE, engineRange } from \"./engine-range.js\";\n\nexport type PackageManager = \"npm\" | \"pnpm\";\n\nexport type Runner = (command: string, args: string[], cwd: string) => void;\n\n/**\n * Where a scaffold says what it is doing, so that presentation belongs to the\n * caller and this module only has to name things.\n *\n * The default is the behaviour a library caller expects with no opinion:\n * subprocesses inherit the terminal and warnings go to `console.warn`. The CLI\n * substitutes one that captures each step's output and shows it only if the\n * step fails.\n */\nexport interface Reporter {\n /** Runs `body` as one named step. Its subprocesses can be captured. */\n step<T>(label: string, body: () => T): T;\n /**\n * Runs `body` as a step that owns the terminal, because something inside it\n * asks questions. FlutterFire is the only one.\n */\n handOver<T>(label: string, body: () => T): T;\n /** Output from a subprocess inside the current step. */\n emit(output: string): void;\n /** A failure the scaffold survives. */\n warn(message: string): void;\n /** Whether subprocesses must inherit the terminal rather than be captured. */\n readonly interactive: boolean;\n}\n\n/**\n * Gives a terminal that reports no width the width clack would have assumed.\n *\n * A pty can report itself as a TTY and set `columns` to 0. `script` does, and\n * so do some container terminals. clack falls back to 80 for a stream with no\n * `columns` at all, but `0` is a number and satisfies that check, so the width\n * it draws its rules from goes negative and a scaffold dies partway through\n * with `RangeError: Invalid string length`. Supplying the default its check\n * misses is better than giving up the drawing.\n */\nexport function normaliseTerminalWidth(stream: { isTTY?: boolean; columns?: number }): void {\n if (stream.isTTY === true && (stream.columns ?? 0) < 1) stream.columns = 80;\n}\n\n/** Runs every step plainly, which is what a library caller gets by default. */\nexport const plainReporter: Reporter = {\n step: (_label, body) => body(),\n handOver: (_label, body) => body(),\n emit: (output) => process.stdout.write(output),\n warn: (message) => console.warn(`create-eigen-game: ${message}`),\n interactive: true,\n};\n\n/**\n * The runner a scaffold uses when the caller has not supplied one: it asks the\n * reporter whether the terminal is spoken for.\n *\n * `interactive` inherits, which is the only way FlutterFire can put a question\n * on screen and read the answer. Otherwise both streams are captured and\n * handed to the reporter: on success so it can discard them, and on failure\n * so the reason survives.\n */\nexport function capturingRunner(reporter: Reporter): Runner {\n return (command, args, cwd) => {\n if (reporter.interactive) {\n execFileSync(command, args, { cwd, stdio: \"inherit\" });\n return;\n }\n try {\n reporter.emit(execFileSync(command, args, { cwd, encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"pipe\"] }));\n } catch (error: unknown) {\n // Both streams, because which one a tool failed on is its own business:\n // pub explains itself on stdout, Gradle on stderr.\n const { stdout, stderr } = error as { stdout?: string; stderr?: string };\n reporter.emit(`${stdout ?? \"\"}${stderr ?? \"\"}`);\n throw error;\n }\n };\n}\n\nexport interface ScaffoldOptions {\n directory: string;\n org?: string;\n packageManager?: PackageManager;\n /**\n * Run Flutter, package installation, and contract/payload generation.\n * Kept as a programmatic test seam; the public CLI always bootstraps both\n * halves of the combined project.\n */\n bootstrap?: boolean;\n /**\n * Emit the GitHub Actions workflows. Off by default; see the call site for\n * why, and `addContinuousIntegration` for adding them to an existing\n * project later.\n */\n ci?: boolean;\n /**\n * Initialise a repository and commit the scaffold. Defaults to `bootstrap`,\n * because the commit is only worth having once the generated files exist.\n */\n git?: boolean;\n /**\n * Configure Firebase before the first commit, by running the Flutter\n * client's `configure_firebase`. A string names the project to use; `true`\n * lets FlutterFire ask, which is also where a project can be created.\n *\n * Explicit here, though the CLI turns it on by default: this is the one step\n * that reaches outside the destination directory, and a library caller\n * should say so rather than discover it. {@link firebaseReadiness} is how\n * the CLI decides.\n */\n firebase?: boolean | string;\n /**\n * Runs the bootstrap subprocesses. A seam: the tests substitute a recorder,\n * and `scripts/scaffold-e2e.mjs` wraps it to point the generated server at\n * this workspace's engine rather than npm's copy.\n */\n run?: Runner;\n /**\n * Where the scaffold says what it is doing. See {@link Reporter}; the\n * default runs every step plainly.\n */\n reporter?: Reporter;\n}\n\n/**\n * What became of the repository, so the CLI's closing summary can say rather\n * than guess. `failed` has already warned by the time it is returned, and\n * carries no advice of its own.\n */\nexport type GitOutcome = \"committed\" | \"existing\" | \"skipped\" | \"failed\";\n\nexport interface ScaffoldResult {\n root: string;\n name: string;\n git: GitOutcome;\n /**\n * Whether Firebase was configured, so the summary can either stop naming the\n * step or spell it out. `failed` has already warned.\n */\n firebase: \"configured\" | \"failed\" | \"skipped\";\n}\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst templatesRoot = resolve(packageRoot, \"templates\");\n\n/**\n * The engine range emitted into a scaffolded project's package.json.\n *\n * Read from this package's `@eigeninteractive/server` devDependency rather than\n * from its own version. See `engine-range.ts` for why that is the version the\n * templates are known to compile against, and what it replaced.\n */\nconst engineVersion = engineRange((JSON.parse(readFileSync(resolve(packageRoot, \"package.json\"), \"utf8\")) as { devDependencies?: Record<string, string> }).devDependencies?.[ENGINE_PACKAGE], () => {\n // Only reached in this workspace, where the manifest still says\n // `workspace:*`. A published tarball has no sibling here and never asks.\n const sibling = resolve(packageRoot, \"../server/package.json\");\n return existsSync(sibling) ? (JSON.parse(readFileSync(sibling, \"utf8\")) as { version: string }).version : undefined;\n});\n\n/**\n * The Flutter client range installed into the app half.\n *\n * A stated version, not a derived or resolved one. The Dart templates import\n * `package:eigen_flutter/eigen_flutter.dart` and\n * `package:eigen_flutter/testing/twin_fixtures.dart` and are written against a\n * specific Dart API, and `eigen_flutter` lives in another repository, versioned\n * independently, so nothing in this repository can compute which release that\n * is. Only compiling a scaffolded app establishes it, which is what the\n * `scaffold` job in checks.yml does on every change.\n *\n * This briefly resolved from pub.dev instead: \"the newest `eigen_flutter` whose\n * own `eigen_api` constraint targets the engine line being scaffolded\". That\n * predicate is wrong. The `eigen_api` constraint describes the WIRE the shell\n * speaks, not the Dart API these templates call, and the two move\n * independently: a future `eigen_flutter` may legitimately keep `eigen_api:\n * ^0.2.0` while renaming everything the templates touch. It would have been\n * selected, and the generated app would not compile.\n *\n * A caret RANGE rather than an exact version, so it still improves without a\n * republish: `flutter pub add eigen_flutter@^0.3.0` already picks the newest\n * 0.3.x at scaffold time, which is the part the pub.dev lookup was duplicating.\n * What it deliberately cannot do is cross to 0.4.x, the one move that needs a\n * human to confirm the templates still compile.\n *\n * Staleness is therefore a failing check rather than a broken scaffold: this is\n * only ever a release behind, never wrong.\n *\n * Raised to 0.3.0 because the notification icon moved into `eigen_flutter`'s\n * Android plugin: the scaffold stopped shipping `ic_notification.xml`, so a\n * game resolving 0.2.x would reference a drawable nothing provides.\n */\nconst flutterClientVersion = \"^0.3.0\";\n\nconst gameSlug = (value: string): string => {\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {\n throw new Error(\"destination directory name must be a lowercase kebab-case slug, for example my-game\");\n }\n return value;\n};\n\nconst dartName = (value: string): string => value.replaceAll(\"-\", \"_\");\n\n/**\n * The Android `applicationId`, and iOS bundle id, a scaffold at `directory`\n * will produce, so the CLI can show it rather than describe it.\n *\n * `flutter create --org X --project-name Y` derives `X.Y`. The organization is\n * the *prefix*, not the whole identifier, which is the part worth seeing\n * before answering: Google Play makes it permanent at first upload.\n */\nexport function applicationId(directory: string, org?: string): string {\n return `${org?.trim() || \"com.example\"}.${dartName(gameSlug(basename(resolve(directory))))}`;\n}\n\nconst title = (value: string): string =>\n value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\" \");\n\nconst identifier = (value: string): string => {\n const pascal = value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\"\");\n return /^[0-9]/.test(pascal) ? `Game${pascal}` : pascal;\n};\n\nexport function detectPackageManager(userAgent = process.env.npm_config_user_agent): PackageManager | undefined {\n if (userAgent?.startsWith(\"pnpm/\")) return \"pnpm\";\n if (userAgent?.startsWith(\"npm/\")) return \"npm\";\n return undefined;\n}\n\n/**\n * Substitutes the scaffold's tokens, of which there are two deliberate kinds.\n *\n * `{{BRACED}}` for values that have no valid example: a package manager,\n * an engine version, an Android package id. They appear only in manifests,\n * READMEs, workflows and the `Appfile`: files nothing compiles.\n *\n * Bare words (`ExampleGame`, `example-game`, …) for names a real example game\n * genuinely has. This is what keeps `templates/worker` and the Dart templates\n * VALID, COMPILING SOURCE rather than text with holes in it, which the rest of\n * this package leans on hard: `typecheck` runs `tsc` over `templates/worker`,\n * the tests `import` its game module directly and build a real contract from\n * it, and the `scaffold` job compiles the Dart templates. Bracing these would\n * trade all of that for a marker.\n *\n * The cost, and the one rule for template authors: a template file cannot\n * contain the literal string `example-game`, `Example Game`, `ExampleGame` or\n * `example_game` and keep it; every occurrence is rewritten, in prose and in\n * code alike. Write around it, or brace a new token instead.\n *\n * Note this means the set of files carrying a token changes whenever someone\n * writes one into an existing file. Nothing needs to track that: `renderTree`\n * decides what to rewrite from the bytes, not from a list of paths.\n */\nfunction render(contents: string, name: string, manager: PackageManager, packageId: string): string {\n const replacements: ReadonlyArray<readonly [string, string]> = [\n [\"@game/example-game-server\", `@game/${name}-server`],\n [\"{{PACKAGE_MANAGER}}\", manager],\n [\"{{PACKAGE_ID}}\", packageId],\n [\"{{ENGINE_VERSION}}\", engineVersion],\n [\"ExampleGame\", identifier(name)],\n [\"Example Game\", title(name)],\n [\"example_game\", dartName(name)],\n [\"example-game\", name],\n ];\n return replacements.reduce((result, [from, to]) => result.replaceAll(from, to), contents);\n}\n\n/**\n * True when the bytes are valid UTF-8.\n *\n * This is not a guess at whether a file is \"binary\"; it is the precondition\n * of the operation. Token substitution is `String.replaceAll`, so a file that\n * cannot be decoded cannot take part, and one that can is safe to rewrite.\n *\n * Deliberately not an extension list. A DENY list (`.png`, `.jar`, …) fails\n * silently in the dangerous direction: the day someone adds a `.webp`, it is\n * corrupted with no error. An ALLOW list fails the safe way but fits this tree\n * badly: `Gemfile`, `Fastfile`, `.nvmrc`, `.fvmrc` and `.ruby-version` all\n * need rendering and none carry a usable extension, so it would have to be a\n * list of extensions *and* a list of bare filenames, both drifting.\n *\n * `.template` is not the signal either, despite the name: it marks files a\n * language server must not claim before they have an enclosing package. Five\n * of them contain no token at all, and nine token-bearing files are not\n * marked, so the two concepts are orthogonal here.\n *\n * `renderTree`'s test pins the resulting classification for the whole tree, so\n * this stays a reviewed decision rather than an invisible one.\n */\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\nexport function decodeUtf8(bytes: Buffer): string | undefined {\n try {\n return utf8.decode(bytes);\n } catch {\n return undefined;\n }\n}\n\nfunction renderTree(source: string, destination: string, name: string, manager: PackageManager, packageId: string): void {\n cpSync(source, destination, { recursive: true });\n // Walks the TEMPLATE, not the destination. The app overlay renders on top of\n // a `flutter create` result, so walking the destination meant rewriting\n // every file Flutter had just generated, including its binaries, for files\n // this scaffolder does not own and has no business touching.\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const templatePath = resolve(directory, entry.name);\n if (entry.isDirectory()) {\n visit(templatePath);\n continue;\n }\n if (!entry.isFile()) continue;\n\n const rendered = resolve(destination, relative(source, templatePath));\n const text = decodeUtf8(readFileSync(templatePath));\n // Undecodable files are already in place from the copy above, byte for\n // byte. Only decodable ones are rewritten.\n if (text !== undefined) {\n writeFileSync(rendered, render(text, name, manager, packageId));\n }\n // Template-only source must not be claimed by language servers before\n // it has an enclosing generated package. Restore the real extension\n // only in the rendered project.\n if (entry.name.endsWith(\".template\")) {\n renameSync(rendered, rendered.slice(0, -\".template\".length));\n }\n }\n };\n visit(source);\n\n // npm deliberately excludes files named `.gitignore` from package\n // tarballs. Their packaged twins live outside the C3 template so a direct\n // Git-based C3 render does not inherit a scaffolder-only helper file.\n const packagedGitignore = resolve(templatesRoot, \"scaffold\", `${basename(source)}.gitignore`);\n if (existsSync(packagedGitignore)) {\n writeFileSync(resolve(destination, \".gitignore\"), render(readFileSync(packagedGitignore, \"utf8\"), name, manager, packageId));\n }\n}\n\nfunction packageCommand(manager: PackageManager, operation: \"install\" | \"contract\" | \"cf-typegen\"): [string, string[]] {\n if (operation === \"install\") return [manager, [\"install\"]];\n return [manager, [\"run\", operation]];\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, since 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\")`, because 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`, since `scaffoldGame` only creates `--platforms android,web`, and\n// `flutter_launcher_icons` errors if told to target a platform whose\n// directory doesn't exist.\n// The colours are the EigenInteractive palette (ink #1B1E24, paper #F4F1EA) because the\n// shipped placeholder art is the EigenInteractive mark: the adaptive foreground is the\n// reversed, light-on-dark variant, so an ink background is what makes it\n// legible. Rebranding a game means replacing the four PNGs *and* these\n// colours together. See https://eigeninteractive.com/docs/ship-it/branding.\nconst launcherIconsAndSplashConfig = `\nflutter_launcher_icons:\n android: true\n web:\n generate: true\n image_path: \"assets/icon/icon.png\"\n image_path: \"assets/icon/icon.png\"\n adaptive_icon_background: \"#1B1E24\"\n adaptive_icon_foreground: \"assets/icon/icon_foreground.png\"\n min_sdk_android: 21\n\nflutter_native_splash:\n color: \"#F4F1EA\"\n color_dark: \"#1B1E24\"\n image: assets/icon/splash.png\n image_dark: assets/icon/splash_dark.png\n fullscreen: true\n android_12:\n color: \"#F4F1EA\"\n color_dark: \"#1B1E24\"\n image: assets/icon/splash.png\n image_dark: assets/icon/splash_dark.png\n icon_background_color: \"#F4F1EA\"\n icon_background_color_dark: \"#1B1E24\"\n web: true\n`;\n\nfunction configureLauncherIconsAndSplash(appRoot: string): void {\n const pubspecPath = resolve(appRoot, \"pubspec.yaml\");\n const pubspec = readFileSync(pubspecPath, \"utf8\");\n if (pubspec.includes(\"flutter_launcher_icons:\")) return;\n appendFileSync(pubspecPath, `${pubspec.endsWith(\"\\n\") ? \"\" : \"\\n\"}${launcherIconsAndSplashConfig}`);\n}\n\nexport interface AddContinuousIntegrationOptions {\n directory: string;\n /** Overrides the manager recorded in the project's own package.json. */\n packageManager?: PackageManager;\n}\n\n/**\n * Adds the GitHub Actions workflows to an existing generated project.\n *\n * The companion to `ci: false`: a game can be built and played locally for as\n * long as its author likes, then gain the same PR gate and signed release\n * pipeline a `--ci` scaffold would have produced.\n */\nexport function addContinuousIntegration(options: AddContinuousIntegrationOptions): { root: string; files: string[] } {\n const root = resolve(options.directory);\n const manifestPath = resolve(root, \"package.json\");\n if (!existsSync(manifestPath) || !existsSync(resolve(root, \"server\")) || !existsSync(resolve(root, \"app\"))) {\n throw new Error(`not an EigenInteractive game project (expected package.json, server/ and app/): ${root}`);\n }\n\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as {\n name?: string;\n scripts?: Record<string, string>;\n };\n const name = gameSlug(manifest.name ?? basename(root));\n\n // The generated `contract` script embeds the manager the project was\n // scaffolded with (`cd server && pnpm run contract && …`), which is a more\n // reliable signal than a lockfile: it is present before anything is\n // installed, and it is what the project actually uses.\n const recorded = manifest.scripts?.contract?.includes(\"pnpm \") ? \"pnpm\" : manifest.scripts?.contract?.includes(\"npm \") ? \"npm\" : undefined;\n const manager = options.packageManager ?? recorded ?? detectPackageManager() ?? \"pnpm\";\n\n // Only `{{PACKAGE_MANAGER}}` appears in these templates, but the real\n // package id is read back rather than reconstructed, so a project scaffolded\n // under a different `--org` cannot be silently rewritten to `com.example`.\n const appfile = resolve(root, \"app/fastlane/Appfile\");\n const packageId = existsSync(appfile) ? (/package_name\\(\"([^\"]+)\"\\)/.exec(readFileSync(appfile, \"utf8\"))?.[1] ?? `com.example.${dartName(name)}`) : `com.example.${dartName(name)}`;\n\n const workflows = resolve(root, \".github/workflows\");\n const existing = existsSync(workflows) ? readdirSync(workflows).filter((f) => f === \"checks.yml\" || f === \"release.yml\") : [];\n if (existing.length > 0) {\n throw new Error(`refusing to overwrite existing workflows: ${existing.join(\", \")}`);\n }\n\n renderTree(resolve(templatesRoot, \"ci\"), root, name, manager, packageId);\n return { root, files: [\".github/workflows/checks.yml\", \".github/workflows/release.yml\"] };\n}\n\n/** Asks a command a question. `ok` is \"ran and succeeded\"; a tool that is not installed and one that fails are both `false`, because neither can be used. */\nexport type Probe = (command: string, args: string[]) => { ok: boolean; stdout: string };\n\nconst probeCommand: Probe = (command, args) => {\n try {\n return { ok: true, stdout: execFileSync(command, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] }) };\n } catch {\n return { ok: false, stdout: \"\" };\n }\n};\n\n/** One thing standing between this machine and a configured Firebase project, and the single command that clears it. */\nexport interface FirebaseProblem {\n /** Said as a sentence fragment, so it reads after \"Firebase is not set up here:\". */\n reason: string;\n /** The one command that fixes this, and nothing else. */\n fix: string;\n}\n\nexport type FirebaseReadiness = { ready: true } | { ready: false; problems: FirebaseProblem[] };\n\n/**\n * Whether this machine can configure Firebase, asked *before* the scaffold\n * rather than after it.\n *\n * `configure_firebase` runs the same three checks and is the authority on\n * them; this one exists only to move the answer earlier. Learning that\n * `flutterfire` is missing costs nothing here and costs two minutes of Flutter\n * and pub at the far end, which is where the step actually runs.\n *\n * Every problem, not the first one. The checks are independent (two CLIs from\n * two ecosystems, plus a sign-in) so short-circuiting turns one setup into a\n * sequence of runs that each reveal the next missing piece. Reported in the\n * order they have to be fixed in: the CLI, then the sign-in it stores, then\n * the bridge that consumes both.\n *\n * The sign-in check fails open, for the same reason it does there: only an\n * answer that positively reports no accounts counts as \"no\", so a `firebase`\n * that errors or grows a different output shape is left to the command that\n * actually needs the credentials.\n */\nexport function firebaseReadiness(probe: Probe = probeCommand): FirebaseReadiness {\n const problems: FirebaseProblem[] = [];\n\n if (probe(\"firebase\", [\"--version\"]).ok) {\n if (signedOut(probe)) problems.push({ reason: \"no Google account is signed in to the Firebase CLI\", fix: \"firebase login\" });\n } else {\n // Nothing to say about the sign-in: the CLI that would answer is the one\n // that is missing, and `firebase login` is what installing leads to anyway.\n // Google's own installer, which picks a standalone binary or npm to suit\n // the machine. `npm install -g firebase-tools` also works and is what this\n // used to print, but it needs a global npm prefix the reader may not have\n // write access to, and it is not the install the Firebase docs lead with.\n problems.push({ reason: \"the `firebase` CLI is not installed\", fix: \"curl -sL https://firebase.tools | bash\" });\n }\n\n if (!probe(\"flutterfire\", [\"--version\"]).ok) {\n problems.push({ reason: \"the `flutterfire` CLI is not installed\", fix: \"dart pub global activate flutterfire_cli\" });\n }\n\n return problems.length === 0 ? { ready: true } : { ready: false, problems };\n}\n\n/**\n * Whether the Firebase CLI holds no credentials.\n *\n * `login:list --json` prints the stored refresh and access tokens along with\n * the account list. Read the shape, count the entries, and never surface the\n * output: not in a warning, not in a captured stream, not on the failure\n * path. That is why this returns a boolean rather than the response.\n */\nfunction signedOut(probe: Probe): boolean {\n const accounts = probe(\"firebase\", [\"login:list\", \"--json\"]);\n if (!accounts.ok) return false;\n try {\n const { result } = JSON.parse(accounts.stdout) as { result?: unknown };\n return Array.isArray(result) && result.length === 0;\n } catch {\n // Unparseable is not evidence of anything.\n return false;\n }\n}\n\n/**\n * Whether the scaffold would land inside a repository that already exists.\n *\n * Asked of the nearest ancestor that exists, because the destination does not\n * yet. The point is not to change what {@link scaffoldGame} does, which already\n * declines to nest a repository, but to stop the CLI asking a question whose\n * answer cannot matter.\n */\n/**\n * Why this destination cannot be scaffolded into, said as a whole message, or\n * `undefined` when there is nothing in the way.\n *\n * A directory that exists and is *empty* is not in the way, since people make\n * one out of habit, so it is removed and rewritten rather than refused over\n * nothing. Anything with contents in it is refused, and a cloned repository is\n * not an exception: `.git` is in the way like any other entry, and the message\n * names it. Publishing into one would mean parking `.git`, renaming staging\n * over the top and putting it back, with a failure in the middle able to\n * orphan a remote, which is a lot of care to spend on a flow that has a\n * one-line alternative.\n *\n * Refused, and deliberately not offered as a \"remove it and continue?\"\n * question, which is where several scaffolders in this family go. This one\n * writes ninety-odd files and runs `flutter create`; the cost of getting that\n * answer wrong against a mistyped path has no upper bound, and `rm -rf` is one\n * command that belongs to the person who can see what is in there. Every other\n * question this CLI asks is reversible. This one would not be.\n *\n * Exported so the CLI can ask it before the first question rather than after\n * the last one. {@link scaffoldGame} asks it again, for callers that are not\n * the CLI.\n */\nexport function destinationProblem(directory: string): string | undefined {\n const root = resolve(directory);\n if (!existsSync(root)) return undefined;\n\n if (!statSync(root).isDirectory()) return `${root} already exists, and is a file.`;\n\n const entries = readdirSync(root);\n if (entries.length === 0) return undefined;\n\n // Named, not counted. \"not empty\" invites an `ls`; the three that are\n // actually there usually settle whether this was the wrong path or a\n // directory that has already been scaffolded once.\n const shown = entries.slice(0, 3).sort().join(\", \");\n const rest = entries.length > 3 ? `, and ${entries.length - 3} more` : \"\";\n return `${root} already exists, and is not empty: it holds ${shown}${rest}.\\n\\nScaffold somewhere else, or clear it out yourself. This will not delete a directory you already had.`;\n}\n\nexport function insideWorkTree(directory: string, probe: Probe = probeCommand): boolean {\n let candidate = resolve(directory);\n while (!existsSync(candidate)) {\n const parent = dirname(candidate);\n if (parent === candidate) return false;\n candidate = parent;\n }\n return probe(\"git\", [\"-C\", candidate, \"rev-parse\", \"--is-inside-work-tree\"]).ok;\n}\n\n/**\n * Runs the Flutter client's `configure_firebase` against the generated app.\n *\n * The command owns everything about how Firebase is configured: which\n * platforms, which CLIs must exist, whether anyone is signed in, and what to\n * do when no project has been chosen yet. This only decides *when*: before the\n * scaffold commit, so a configured project is committed configured.\n *\n * Not fatal. Every failure here (a missing `flutterfire`, no Google login, a\n * cancelled picker) leaves a complete and usable project that is exactly what\n * a scaffold without `--firebase` produces, so it warns with the command to\n * re-run and lets the commit happen.\n */\nfunction configureFirebase(appRoot: string, project: boolean | string, run: Runner, reporter: Reporter): \"configured\" | \"failed\" {\n // FlutterFire asks before overwriting `firebase_options.dart`, and the\n // scaffold has just written one, a throwing placeholder whose entire\n // purpose is to be replaced by this step. Answering that is a question with\n // one right answer, so the placeholder is moved out of the way and the\n // question never gets asked.\n const placeholder = resolve(appRoot, \"lib/firebase_options.dart\");\n const parked = `${placeholder}.placeholder`;\n const parking = existsSync(placeholder);\n if (parking) renameSync(placeholder, parked);\n\n try {\n // `dart run` compiles the package executable on first use and says so.\n // Doing it against `--help`, which returns immediately, moves that noise\n // into a step whose output can be captured; the real run cannot be, since\n // FlutterFire prompts through it.\n reporter.step(\"Preparing the Firebase configurator\", () => run(\"dart\", [\"run\", \"eigen_flutter:configure_firebase\", \"--help\"], appRoot));\n reporter.handOver(\"Configuring Firebase\", () => run(\"dart\", [\"run\", \"eigen_flutter:configure_firebase\", ...(typeof project === \"string\" ? [\"--project\", project] : [])], appRoot));\n if (parking) rmSync(parked, { force: true });\n return \"configured\";\n } catch {\n // Back only if nothing took its place. FlutterFire writes\n // `firebase_options.dart` before the service worker configuration is\n // derived from it, so a late failure leaves a real one that is worth more\n // than the placeholder.\n if (parking) {\n if (existsSync(placeholder)) rmSync(parked, { force: true });\n else renameSync(parked, placeholder);\n }\n reporter.warn(\"Could not configure Firebase. The project is complete. Run `firebase:configure` yourself, then commit what it writes.\");\n return \"failed\";\n }\n}\n\n/**\n * Commits the scaffold, so the first `git diff` is the first game change.\n *\n * This matters more here than in a scaffolder that only writes source. The\n * bootstrap runs `flutter_launcher_icons` and `flutter_native_splash`, which\n * write generated-but-committed files across `android/`, `web/` and\n * `assets/`; without a baseline the first branding change is indistinguishable\n * from the ninety files the scaffolder happened to produce.\n *\n * Nothing here is fatal. The project is complete and valid before this runs,\n * so a missing `git`, or the unconfigured `user.email` that a fresh CI image\n * has, warns and leaves the tree alone rather than discarding a scaffold that\n * took two minutes of Flutter and pub to produce.\n */\nfunction initialiseRepository(root: string, name: string, run: Runner, reporter: Reporter): GitOutcome {\n // Scaffolding inside an existing checkout (a monorepo, or a repository\n // created ahead of time) is a legitimate thing to do, and a nested\n // repository there is silent breakage: the outer `git add` records a gitlink\n // and the app's files never leave the machine. Asked directly rather than\n // through `run`, because it is a question, not a step.\n try {\n execFileSync(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], { cwd: root, stdio: \"ignore\" });\n return \"existing\";\n } catch {\n // Not inside a work tree, which is the case this function is for.\n }\n\n try {\n run(\"git\", [\"init\", \"--quiet\"], root);\n run(\"git\", [\"add\", \"--all\"], root);\n } catch {\n reporter.warn(\"Could not initialise a git repository. The project is complete. Run `git init` yourself when ready.\");\n return \"failed\";\n }\n\n try {\n run(\"git\", [\"commit\", \"--quiet\", \"--message\", `Scaffold ${name}`], root);\n } catch {\n reporter.warn(`Initialised a repository and staged the scaffold, but could not commit it. Set \\`user.name\\` and \\`user.email\\`, then \\`git commit -m \"Scaffold ${name}\"\\`.`);\n return \"failed\";\n }\n\n return \"committed\";\n}\n\nexport function scaffoldGame(options: ScaffoldOptions): ScaffoldResult {\n const root = resolve(options.directory);\n const name = gameSlug(basename(root));\n const manager = options.packageManager ?? detectPackageManager() ?? \"pnpm\";\n const org = options.org?.trim() || \"com.example\";\n // Fastlane's `Appfile` names the same package the build actually produces,\n // and the CLI shows the same one before asking.\n const packageId = applicationId(root, org);\n const bootstrap = options.bootstrap ?? true;\n const reporter = options.reporter ?? plainReporter;\n const run = options.run ?? capturingRunner(reporter);\n\n const occupied = destinationProblem(root);\n if (occupied !== undefined) throw new Error(occupied);\n // Empty, or `destinationProblem` would have said so. Removed rather than\n // scaffolded into, because the rename below publishes the staging directory\n // *as* this path and cannot do that while something is standing there.\n if (existsSync(root)) rmdirSync(root);\n\n mkdirSync(dirname(root), { recursive: true });\n\n // Build beside the destination and publish it with one rename. Failed\n // subprocesses never leave a half-scaffolded project at the requested path.\n const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));\n try {\n const serverRoot = resolve(stagingRoot, \"server\");\n const appRoot = resolve(stagingRoot, \"app\");\n\n renderTree(resolve(templatesRoot, \"worker\"), serverRoot, name, manager, packageId);\n renderTree(resolve(templatesRoot, \"project\"), stagingRoot, name, manager, packageId);\n // Opt-in. release.yml wants an upload keystore and a Play service account\n // that a brand-new project does not have, so generating it by default\n // means a red X on main from the first push until someone sets both up.\n // `create-eigen-game add ci` writes the same files whenever it is wanted.\n if (options.ci) renderTree(resolve(templatesRoot, \"ci\"), stagingRoot, name, manager, packageId);\n\n if (bootstrap) {\n reporter.step(\"Creating the Flutter app\", () => {\n run(\"flutter\", [\"create\", \"--empty\", \"--platforms\", \"android,web\", \"--project-name\", dartName(name), \"--org\", org, appRoot], stagingRoot);\n enableAndroidCoreLibraryDesugaring(appRoot);\n enableAndroidReleaseSigning(appRoot);\n });\n } else {\n mkdirSync(appRoot, { recursive: true });\n }\n renderTree(resolve(templatesRoot, \"app-overlay\"), appRoot, name, manager, packageId);\n\n if (bootstrap) {\n reporter.step(\"Adding the Flutter packages\", () => {\n configureLauncherIconsAndSplash(appRoot);\n run(\"flutter\", [\"pub\", \"add\", `eigen_flutter@${flutterClientVersion}`, \"firebase_core@^4.9.0\", \"firebase_messaging@^16.2.2\"], appRoot);\n run(\"flutter\", [\"pub\", \"add\", \"dev:flutter_launcher_icons\", \"dev:flutter_native_splash\"], appRoot);\n });\n // Actually apply the icons rather than only configuring them. Both tools\n // write generated files (mipmaps, `web/icons/`, splash drawables and\n // styles) that are committed, not built; leaving them unrun would ship\n // Flutter's own blue logo until a game author happened to notice.\n reporter.step(\"Generating the icons and splash\", () => {\n run(\"dart\", [\"run\", \"flutter_launcher_icons\"], appRoot);\n run(\"dart\", [\"run\", \"flutter_native_splash:create\"], appRoot);\n });\n const [install, installArgs] = packageCommand(manager, \"install\");\n const [typegen, typegenArgs] = packageCommand(manager, \"cf-typegen\");\n reporter.step(\"Installing the Worker packages\", () => {\n run(install, installArgs, serverRoot);\n // `worker-configuration.d.ts` is committed, and its header stamps the\n // workerd version that produced the runtime types. `wrangler` floats\n // on a caret and Cloudflare ships workerd about weekly, so the copy in\n // `templates/worker` is stale for every scaffold made more than a few\n // days after it was last regenerated, and `wrangler dev` opens by\n // saying the types might be out of date. Regenerating against the\n // wrangler that was just installed, rather than the one this package\n // was built with, is what makes that true. It has to follow the server\n // install for the same reason it is worth doing: it reads the wrangler\n // that install resolved.\n run(typegen, typegenArgs, serverRoot);\n // The root holds Biome, which lints and formats both the Worker and\n // the repository's own JSON. Installed after the server so a failure\n // here costs the cheaper of the two.\n run(install, installArgs, stagingRoot);\n });\n const [contract, contractArgs] = packageCommand(manager, \"contract\");\n reporter.step(\"Generating the contract and payloads\", () => {\n run(contract, contractArgs, serverRoot);\n run(\"dart\", [\"run\", \"eigen_flutter:generate_payloads\", \"--contract\", \"../server/game-contract.json\", \"--output\", \"lib/game/generated/payloads.dart\", \"--fixtures-output\", \"test/fixtures\"], appRoot);\n });\n }\n\n renameSync(stagingRoot, root);\n } catch (error) {\n rmSync(stagingRoot, { recursive: true, force: true });\n throw error;\n }\n\n // Both steps below are deliberately outside the staging guard: the project\n // is published by this point, and neither a Firebase project that could not\n // be configured nor a repository that failed to initialise is a reason to\n // delete two minutes of Flutter and pub.\n //\n // Firebase first, so what it writes (`firebase.json`,\n // `android/app/google-services.json`, the real `firebase_options.dart` and\n // `web/firebase-config.js` in place of the throwing placeholders, and\n // FlutterFire's two Gradle edits) lands in the scaffold commit rather than\n // arriving as the project's first diff.\n const firebase = options.firebase ? configureFirebase(resolve(root, \"app\"), options.firebase, run, reporter) : \"skipped\";\n const git = (options.git ?? bootstrap) ? initialiseRepository(root, name, run, reporter) : \"skipped\";\n\n return { root, name, git, firebase };\n}\n","/**\n * Where the engine range emitted into a scaffolded project comes from.\n *\n * Its own module because the two branches below execute in different worlds,\n * one only inside this repository and 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":[]}
|