create-eigen-game 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -6
- package/dist/{chunk-DL4LFOFR.js → chunk-45XDHKSP.js} +47 -20
- package/dist/chunk-45XDHKSP.js.map +1 -0
- package/dist/cli.js +195 -62
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +53 -7
- package/dist/index.js +5 -1
- package/package.json +4 -4
- package/templates/app-overlay/android/app/proguard-rules.pro +1 -1
- package/templates/app-overlay/lib/main.dart.template +1 -1
- package/templates/ci/.github/workflows/checks.yml.template +4 -4
- package/templates/ci/.github/workflows/release.yml.template +5 -5
- package/templates/project/README.md +23 -18
- package/templates/worker/README.md +2 -2
- package/templates/worker/src/index.ts +1 -1
- package/dist/chunk-DL4LFOFR.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ interface Reporter {
|
|
|
27
27
|
/**
|
|
28
28
|
* Gives a terminal that reports no width the width clack would have assumed.
|
|
29
29
|
*
|
|
30
|
-
* A pty can report itself as a TTY and set `columns` to 0
|
|
30
|
+
* A pty can report itself as a TTY and set `columns` to 0. `script` does, and
|
|
31
31
|
* so do some container terminals. clack falls back to 80 for a stream with no
|
|
32
32
|
* `columns` at all, but `0` is a number and satisfies that check, so the width
|
|
33
33
|
* it draws its rules from goes negative and a scaffold dies partway through
|
|
@@ -46,7 +46,7 @@ declare const plainReporter: Reporter;
|
|
|
46
46
|
*
|
|
47
47
|
* `interactive` inherits, which is the only way FlutterFire can put a question
|
|
48
48
|
* on screen and read the answer. Otherwise both streams are captured and
|
|
49
|
-
* handed to the reporter
|
|
49
|
+
* handed to the reporter: on success so it can discard them, and on failure
|
|
50
50
|
* so the reason survives.
|
|
51
51
|
*/
|
|
52
52
|
declare function capturingRunner(reporter: Reporter): Runner;
|
|
@@ -61,7 +61,7 @@ interface ScaffoldOptions {
|
|
|
61
61
|
*/
|
|
62
62
|
bootstrap?: boolean;
|
|
63
63
|
/**
|
|
64
|
-
* Emit the GitHub Actions workflows. Off by default
|
|
64
|
+
* Emit the GitHub Actions workflows. Off by default; see the call site for
|
|
65
65
|
* why, and `addContinuousIntegration` for adding them to an existing
|
|
66
66
|
* project later.
|
|
67
67
|
*/
|
|
@@ -111,7 +111,7 @@ interface ScaffoldResult {
|
|
|
111
111
|
firebase: "configured" | "failed" | "skipped";
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
|
-
* The Android `applicationId
|
|
114
|
+
* The Android `applicationId`, and iOS bundle id, a scaffold at `directory`
|
|
115
115
|
* will produce, so the CLI can show it rather than describe it.
|
|
116
116
|
*
|
|
117
117
|
* `flutter create --org X --project-name Y` derives `X.Y`. The organization is
|
|
@@ -142,12 +142,18 @@ type Probe = (command: string, args: string[]) => {
|
|
|
142
142
|
ok: boolean;
|
|
143
143
|
stdout: string;
|
|
144
144
|
};
|
|
145
|
+
/** One thing standing between this machine and a configured Firebase project, and the single command that clears it. */
|
|
146
|
+
interface FirebaseProblem {
|
|
147
|
+
/** Said as a sentence fragment, so it reads after "Firebase is not set up here:". */
|
|
148
|
+
reason: string;
|
|
149
|
+
/** The one command that fixes this, and nothing else. */
|
|
150
|
+
fix: string;
|
|
151
|
+
}
|
|
145
152
|
type FirebaseReadiness = {
|
|
146
153
|
ready: true;
|
|
147
154
|
} | {
|
|
148
155
|
ready: false;
|
|
149
|
-
|
|
150
|
-
fix: string;
|
|
156
|
+
problems: FirebaseProblem[];
|
|
151
157
|
};
|
|
152
158
|
/**
|
|
153
159
|
* Whether this machine can configure Firebase, asked *before* the scaffold
|
|
@@ -158,12 +164,52 @@ type FirebaseReadiness = {
|
|
|
158
164
|
* `flutterfire` is missing costs nothing here and costs two minutes of Flutter
|
|
159
165
|
* and pub at the far end, which is where the step actually runs.
|
|
160
166
|
*
|
|
167
|
+
* Every problem, not the first one. The checks are independent (two CLIs from
|
|
168
|
+
* two ecosystems, plus a sign-in) so short-circuiting turns one setup into a
|
|
169
|
+
* sequence of runs that each reveal the next missing piece. Reported in the
|
|
170
|
+
* order they have to be fixed in: the CLI, then the sign-in it stores, then
|
|
171
|
+
* the bridge that consumes both.
|
|
172
|
+
*
|
|
161
173
|
* The sign-in check fails open, for the same reason it does there: only an
|
|
162
174
|
* answer that positively reports no accounts counts as "no", so a `firebase`
|
|
163
175
|
* that errors or grows a different output shape is left to the command that
|
|
164
176
|
* actually needs the credentials.
|
|
165
177
|
*/
|
|
166
178
|
declare function firebaseReadiness(probe?: Probe): FirebaseReadiness;
|
|
179
|
+
/**
|
|
180
|
+
* Whether the scaffold would land inside a repository that already exists.
|
|
181
|
+
*
|
|
182
|
+
* Asked of the nearest ancestor that exists, because the destination does not
|
|
183
|
+
* yet. The point is not to change what {@link scaffoldGame} does, which already
|
|
184
|
+
* declines to nest a repository, but to stop the CLI asking a question whose
|
|
185
|
+
* answer cannot matter.
|
|
186
|
+
*/
|
|
187
|
+
/**
|
|
188
|
+
* Why this destination cannot be scaffolded into, said as a whole message, or
|
|
189
|
+
* `undefined` when there is nothing in the way.
|
|
190
|
+
*
|
|
191
|
+
* A directory that exists and is *empty* is not in the way, since people make
|
|
192
|
+
* one out of habit, so it is removed and rewritten rather than refused over
|
|
193
|
+
* nothing. Anything with contents in it is refused, and a cloned repository is
|
|
194
|
+
* not an exception: `.git` is in the way like any other entry, and the message
|
|
195
|
+
* names it. Publishing into one would mean parking `.git`, renaming staging
|
|
196
|
+
* over the top and putting it back, with a failure in the middle able to
|
|
197
|
+
* orphan a remote, which is a lot of care to spend on a flow that has a
|
|
198
|
+
* one-line alternative.
|
|
199
|
+
*
|
|
200
|
+
* Refused, and deliberately not offered as a "remove it and continue?"
|
|
201
|
+
* question, which is where several scaffolders in this family go. This one
|
|
202
|
+
* writes ninety-odd files and runs `flutter create`; the cost of getting that
|
|
203
|
+
* answer wrong against a mistyped path has no upper bound, and `rm -rf` is one
|
|
204
|
+
* command that belongs to the person who can see what is in there. Every other
|
|
205
|
+
* question this CLI asks is reversible. This one would not be.
|
|
206
|
+
*
|
|
207
|
+
* Exported so the CLI can ask it before the first question rather than after
|
|
208
|
+
* the last one. {@link scaffoldGame} asks it again, for callers that are not
|
|
209
|
+
* the CLI.
|
|
210
|
+
*/
|
|
211
|
+
declare function destinationProblem(directory: string): string | undefined;
|
|
212
|
+
declare function insideWorkTree(directory: string, probe?: Probe): boolean;
|
|
167
213
|
declare function scaffoldGame(options: ScaffoldOptions): ScaffoldResult;
|
|
168
214
|
|
|
169
|
-
export { type AddContinuousIntegrationOptions, type FirebaseReadiness, type GitOutcome, type PackageManager, type Probe, type Reporter, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, applicationId, capturingRunner, decodeUtf8, detectPackageManager, firebaseReadiness, normaliseTerminalWidth, plainReporter, scaffoldGame };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -3,19 +3,23 @@ import {
|
|
|
3
3
|
applicationId,
|
|
4
4
|
capturingRunner,
|
|
5
5
|
decodeUtf8,
|
|
6
|
+
destinationProblem,
|
|
6
7
|
detectPackageManager,
|
|
7
8
|
firebaseReadiness,
|
|
9
|
+
insideWorkTree,
|
|
8
10
|
normaliseTerminalWidth,
|
|
9
11
|
plainReporter,
|
|
10
12
|
scaffoldGame
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-45XDHKSP.js";
|
|
12
14
|
export {
|
|
13
15
|
addContinuousIntegration,
|
|
14
16
|
applicationId,
|
|
15
17
|
capturingRunner,
|
|
16
18
|
decodeUtf8,
|
|
19
|
+
destinationProblem,
|
|
17
20
|
detectPackageManager,
|
|
18
21
|
firebaseReadiness,
|
|
22
|
+
insideWorkTree,
|
|
19
23
|
normaliseTerminalWidth,
|
|
20
24
|
plainReporter,
|
|
21
25
|
scaffoldGame
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-eigen-game",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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/
|
|
33
|
-
"@eigeninteractive/
|
|
34
|
-
"@eigeninteractive/testkit": "0.2.
|
|
32
|
+
"@eigeninteractive/rules": "0.2.5",
|
|
33
|
+
"@eigeninteractive/server": "0.2.5",
|
|
34
|
+
"@eigeninteractive/testkit": "0.2.5"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=22"
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
# google_sign_in (Credential Manager), in_app_update, and in_app_review use
|
|
5
5
|
# Google Play libraries that include their own consumer rules via Maven.
|
|
6
6
|
#
|
|
7
|
-
# Only libraries confirmed to lack consumer rules are listed below
|
|
7
|
+
# Only libraries confirmed to lack consumer rules are listed below: currently
|
|
8
8
|
# none. Add a keep rule here only after confirming a dependency ships no
|
|
9
9
|
# consumer rules of its own and is stripped by R8.
|
|
@@ -17,7 +17,7 @@ Future<void> main() async {
|
|
|
17
17
|
// `seedColor` is left at its default, the EigenInteractive teal that the
|
|
18
18
|
// placeholder icon and the worker's own pages are drawn in. To make the
|
|
19
19
|
// app yours, import 'package:flutter/material.dart' and add
|
|
20
|
-
// `seedColor: Colors.deepPurple` (or any Color)
|
|
20
|
+
// `seedColor: Colors.deepPurple` (or any Color), and Material 3 rebuilds the
|
|
21
21
|
// whole light and dark scheme from it. Do it alongside the launcher-icon
|
|
22
22
|
// and splash colours in pubspec.yaml, which cannot read Dart:
|
|
23
23
|
// https://eigeninteractive.com/docs/ship-it/branding
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
steps:
|
|
38
38
|
- uses: actions/checkout@v7
|
|
39
39
|
|
|
40
|
-
# `corepack enable`, not `pnpm/action-setup
|
|
40
|
+
# `corepack enable`, not `pnpm/action-setup`, because this project can be
|
|
41
41
|
# scaffolded with either npm or pnpm (see the {{PACKAGE_MANAGER}}
|
|
42
42
|
# commands below), and corepack puts both on PATH regardless of which
|
|
43
43
|
# one a game author actually chose.
|
|
@@ -72,9 +72,9 @@ jobs:
|
|
|
72
72
|
node-version-file: .nvmrc
|
|
73
73
|
cache: {{PACKAGE_MANAGER}}
|
|
74
74
|
|
|
75
|
-
# `contract:check` (below) round-trips through both halves
|
|
75
|
+
# `contract:check` (below) round-trips through both halves: it
|
|
76
76
|
# invokes the server's own contract generator, then verifies the app's
|
|
77
|
-
# generated payloads still match
|
|
77
|
+
# generated payloads still match, so the server's Node dependencies
|
|
78
78
|
# have to exist here too, not just in the `server` job's own runner.
|
|
79
79
|
- name: Install server dependencies
|
|
80
80
|
working-directory: server
|
|
@@ -110,7 +110,7 @@ jobs:
|
|
|
110
110
|
|
|
111
111
|
# A VM widget test can pass while a dependency still imports dart:io or
|
|
112
112
|
# calls an unsupported plugin. Compiling the real web entrypoint is
|
|
113
|
-
# what actually proves it ships
|
|
113
|
+
# what actually proves it ships, mirroring eigen-flutter's own `example`
|
|
114
114
|
# job, which does the same for the RPS reference app.
|
|
115
115
|
#
|
|
116
116
|
# No Android SDK step in this job, for the same reason
|
|
@@ -2,13 +2,13 @@ name: Release
|
|
|
2
2
|
|
|
3
3
|
# Ships a signed Android build on every push to main:
|
|
4
4
|
#
|
|
5
|
-
# 1. checks
|
|
6
|
-
# 2. build
|
|
7
|
-
# 3. deploy
|
|
5
|
+
# 1. checks : the exact PR gate, re-run against the merge commit.
|
|
6
|
+
# 2. build : a signed, obfuscated AAB.
|
|
7
|
+
# 3. deploy : fastlane uploads it to the Play Store internal track.
|
|
8
8
|
#
|
|
9
9
|
# Worker deployment is NOT here, on purpose: it applies D1 migrations against
|
|
10
10
|
# a real database, so it stays the deliberate, manual `{{PACKAGE_MANAGER}} run
|
|
11
|
-
# deploy` from a machine with `wrangler login
|
|
11
|
+
# deploy` from a machine with `wrangler login`, the same reasoning the
|
|
12
12
|
# engine's own CI uses for never deploying a Worker automatically.
|
|
13
13
|
on:
|
|
14
14
|
push:
|
|
@@ -60,7 +60,7 @@ jobs:
|
|
|
60
60
|
|
|
61
61
|
# The keystore and Play service account are the only things here that
|
|
62
62
|
# are genuinely secret. `app-config.json`, `firebase_options.dart` and
|
|
63
|
-
# `google-services.json` are public app identifiers, not credentials
|
|
63
|
+
# `google-services.json` are public app identifiers, not credentials, so
|
|
64
64
|
# commit them for real once `firebase:configure` has been run, rather
|
|
65
65
|
# than reconstructing them from secrets on every build. See
|
|
66
66
|
# https://eigeninteractive.com/docs/ship-it/configure and
|
|
@@ -47,7 +47,7 @@ whitespace everywhere, including the YAML that Biome does not format, and
|
|
|
47
47
|
`.vscode/` recommends the Biome extension and turns formatting on save on for
|
|
48
48
|
the languages it owns. Nothing generated depends on a comment surviving in a
|
|
49
49
|
file a formatter or a tool may rewrite, so nothing is exempted from formatting.
|
|
50
|
-
Change any of it
|
|
50
|
+
Change any of it, since it is your repository, but the generated files are written
|
|
51
51
|
to satisfy it as shipped.
|
|
52
52
|
|
|
53
53
|
## Flutter app
|
|
@@ -87,7 +87,7 @@ Configure Android, Flutter Web, and the messaging service worker together:
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
Scaffolding does this for you, so a project created with the tooling in place
|
|
90
|
-
is already configured and this is the command for changing it afterwards
|
|
90
|
+
is already configured and this is the command for changing it afterwards: a
|
|
91
91
|
different project, or picking up a configuration change. If it was skipped, the
|
|
92
92
|
app throws `Firebase is not configured` the moment it launches; rules, fixtures
|
|
93
93
|
and `wrangler dev` all run regardless.
|
|
@@ -96,12 +96,16 @@ It drives two CLIs, which are separate global installs and neither of which
|
|
|
96
96
|
comes with Flutter or Node:
|
|
97
97
|
|
|
98
98
|
```sh
|
|
99
|
-
|
|
99
|
+
curl -sL https://firebase.tools | bash # the `firebase` CLI
|
|
100
100
|
dart pub global activate flutterfire_cli # the `flutterfire` CLI
|
|
101
101
|
export PATH="$PATH":"$HOME/.pub-cache/bin" # `activate` does not do this
|
|
102
102
|
firebase login # both CLIs share this
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
The first line is Google's own installer; [the Firebase CLI
|
|
106
|
+
reference](https://firebase.google.com/docs/cli) has the other ways, including
|
|
107
|
+
Windows.
|
|
108
|
+
|
|
105
109
|
A run with no project chosen yet asks which to use, and can create one. Name it
|
|
106
110
|
up front to skip the prompts, which is also how CI runs this:
|
|
107
111
|
|
|
@@ -113,8 +117,8 @@ Later runs reuse the project recorded in `app/firebase.json`, so there is
|
|
|
113
117
|
nothing to pass again. Add `--help` for the rest.
|
|
114
118
|
|
|
115
119
|
It writes `app/firebase.json`, `app/android/app/google-services.json`,
|
|
116
|
-
`app/lib/firebase_options.dart` and `app/web/firebase-config.js`
|
|
117
|
-
replacing scaffolded placeholders that throw
|
|
120
|
+
`app/lib/firebase_options.dart` and `app/web/firebase-config.js` (the last two
|
|
121
|
+
replacing scaffolded placeholders that throw) and lets FlutterFire add the
|
|
118
122
|
Google Services plugin to the two Android Gradle files. **Commit all of it.**
|
|
119
123
|
Every one is a public app identifier rather than a credential, nothing here is
|
|
120
124
|
git-ignored, and an Android or web build fails without them. The Firebase
|
|
@@ -130,7 +134,7 @@ new".
|
|
|
130
134
|
|
|
131
135
|
The command uses FlutterFire's selected Web app to generate both
|
|
132
136
|
`lib/firebase_options.dart` and `web/firebase-config.js`; do not copy Firebase
|
|
133
|
-
identifiers by hand. Put the public VAPID key in `app-config.json` separately
|
|
137
|
+
identifiers by hand. Put the public VAPID key in `app-config.json` separately;
|
|
134
138
|
Firebase's app SDK configuration does not include that Web Push certificate.
|
|
135
139
|
`eigen_flutter` bundles the pinned Cropper.js assets and loads them only when
|
|
136
140
|
avatar editing starts, so the app needs no Cropper.js setup or runtime CDN. The
|
|
@@ -163,21 +167,22 @@ with `wrangler secret put`; never commit the downloaded JSON or `.dev.vars`.
|
|
|
163
167
|
## Store release
|
|
164
168
|
|
|
165
169
|
CI is **not** generated by default, because the release pipeline needs an
|
|
166
|
-
upload keystore and a Play service account that a new project does not have
|
|
167
|
-
|
|
170
|
+
upload keystore and a Play service account that a new project does not have,
|
|
171
|
+
so it would fail on every push until both exist. Add it when you are ready to
|
|
168
172
|
ship, from the project root:
|
|
169
173
|
|
|
170
174
|
```sh
|
|
171
|
-
npx create-eigen-game add
|
|
175
|
+
npx create-eigen-game add workflows
|
|
172
176
|
```
|
|
173
177
|
|
|
174
|
-
That writes `.github/workflows/checks.yml` and `release.yml`. (
|
|
175
|
-
|
|
178
|
+
That writes `.github/workflows/checks.yml` and `release.yml`. (Answering yes to
|
|
179
|
+
the workflows question at scaffold time, or passing `--workflows`, does the same
|
|
180
|
+
thing up front.)
|
|
176
181
|
|
|
177
182
|
`release.yml` builds a signed, obfuscated Android App
|
|
178
183
|
Bundle on every push to `main` and uploads it to the Play Store internal
|
|
179
184
|
track via `app/fastlane`. `checks.yml` is the PR gate it
|
|
180
|
-
calls first
|
|
185
|
+
calls first, and it needs no secrets, since `app-config.json`,
|
|
181
186
|
`firebase_options.dart` and `google-services.json` are public app
|
|
182
187
|
identifiers, not credentials. Commit them for real once
|
|
183
188
|
`{{PACKAGE_MANAGER}} run firebase:configure` has been run, rather than
|
|
@@ -196,15 +201,15 @@ required:
|
|
|
196
201
|
|
|
197
202
|
Per-app one-time setup: create an upload keystore, add the four signing
|
|
198
203
|
secrets above, create the Google Play service account and add its JSON, and
|
|
199
|
-
do the **first** Play Console upload by hand to create the listing
|
|
200
|
-
|
|
204
|
+
do the **first** Play Console upload by hand to create the listing.
|
|
205
|
+
Everything after that flows through `fastlane android internal` /
|
|
201
206
|
`fastlane android production`. The Fastlane lanes upload the binary only
|
|
202
207
|
(`skip_upload_metadata`/`images`/`screenshots`), so the store listing stays
|
|
203
208
|
hand-maintained in the Console.
|
|
204
209
|
|
|
205
210
|
### Replace the placeholder branding
|
|
206
211
|
|
|
207
|
-
A fresh scaffold is already branded
|
|
212
|
+
A fresh scaffold is already branded, with the **EigenInteractive mark**, not
|
|
208
213
|
this game's. The launcher icon, splash and web icons were generated at scaffold
|
|
209
214
|
time so the
|
|
210
215
|
app looks finished from the first run, but shipping them to a store would put
|
|
@@ -219,11 +224,11 @@ the engine's mark on your listing. Replace them before a first release:
|
|
|
219
224
|
The notification icon is the exception: `eigen_flutter` ships a default
|
|
220
225
|
`ic_notification` drawable, so nothing is missing and nothing breaks if you
|
|
221
226
|
leave it. To use your own, add
|
|
222
|
-
`android/app/src/main/res/drawable/ic_notification.xml
|
|
227
|
+
`android/app/src/main/res/drawable/ic_notification.xml`, a monochrome
|
|
223
228
|
`<vector>`, not an SVG. Android resource merging gives your app precedence
|
|
224
229
|
over the package, so there is nothing to delete or override explicitly.
|
|
225
230
|
|
|
226
|
-
Also update the palette in `pubspec.yaml
|
|
231
|
+
Also update the palette in `pubspec.yaml`. `adaptive_icon_background` and the
|
|
227
232
|
`flutter_native_splash` colours are the EigenInteractive ink/paper pair, which
|
|
228
233
|
will not suit different artwork. Then regenerate:
|
|
229
234
|
|
|
@@ -244,5 +249,5 @@ cider bump patch # 1.0.0 -> 1.0.1
|
|
|
244
249
|
```
|
|
245
250
|
|
|
246
251
|
The build number is set from `github.run_number` in CI and is not managed by
|
|
247
|
-
hand. Worker deployment is unaffected by any of this
|
|
252
|
+
hand. Worker deployment is unaffected by any of this; it stays the manual
|
|
248
253
|
`{{PACKAGE_MANAGER}} run deploy` described above.
|
|
@@ -63,8 +63,8 @@ fetches its platform binary, and `workerd` is the runtime `vitest` and
|
|
|
63
63
|
the file entirely, so the scaffold is correct under both.
|
|
64
64
|
|
|
65
65
|
`minimumReleaseAgeExclude` appears on its own. pnpm 11 will not install a
|
|
66
|
-
version published less than a day ago
|
|
67
|
-
publish
|
|
66
|
+
version published less than a day ago, a quarantine against a compromised
|
|
67
|
+
publish, and the engine packages are subject to it like any other dependency.
|
|
68
68
|
When a range can only be satisfied by a release younger than that, as `^0.3.0`
|
|
69
69
|
is on the day 0.3.0 ships, pnpm installs it anyway and records the exact
|
|
70
70
|
version it allowed. Each entry names one version, so the exemption expires with
|
|
@@ -19,7 +19,7 @@ export default createEngine({
|
|
|
19
19
|
// tagline, screenshots and structured data, and /terms, /privacy and
|
|
20
20
|
// /delete-account start serving the engine's default documents with your
|
|
21
21
|
// details in them. The app stores require all three, so this is not optional
|
|
22
|
-
// for long
|
|
22
|
+
// for long, but read what they say before you publish, because you are the
|
|
23
23
|
// one on the hook for it. https://eigeninteractive.com/docs/ship-it/branding
|
|
24
24
|
//
|
|
25
25
|
// site: {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/engine-range.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { ENGINE_PACKAGE, engineRange } from \"./engine-range.js\";\n\nexport type PackageManager = \"npm\" | \"pnpm\";\n\nexport type Runner = (command: string, args: string[], cwd: string) => void;\n\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 — Gradle\n// merges repeated extension-configuration blocks in one file, which is what\n// lets this stay a pure append, same as the desugaring block above. A second\n// top-level `plugins { }` block is NOT valid (Gradle allows exactly one per\n// script), which is why the Crashlytics Gradle plugin registration —\n// `flutterfire configure`'s own territory — is deliberately left alone here.\n//\n// `Properties` needs a real `import`, prepended separately below — a\n// fully-qualified `java.util.Properties()` reference here was tried first,\n// to avoid needing to touch the top of the file at all, but fails to\n// resolve under AGP 9's Kotlin DSL script compilation\n// (`flutter create`'s current default): \"Unresolved reference 'util'\".\n// Confirmed by actually building a scaffolded project, not just reading it.\nconst androidReleaseSigning = `val releaseKeyProperties: Map<String, String> =\n rootProject.file(\"key.properties\")\n .takeIf { it.exists() }\n ?.readLines()\n ?.map { it.trim() }\n ?.filter { it.isNotEmpty() && !it.startsWith(\"#\") && it.contains(\"=\") }\n ?.associate { it.substringBefore(\"=\").trim() to it.substringAfter(\"=\").trim() }\n .orEmpty()\n\nandroid {\n signingConfigs {\n create(\"release\") {\n storeFile = releaseKeyProperties[\"storeFile\"]?.let { file(it) }\n storePassword = releaseKeyProperties[\"storePassword\"]\n keyAlias = releaseKeyProperties[\"keyAlias\"]\n keyPassword = releaseKeyProperties[\"keyPassword\"]\n }\n }\n buildTypes {\n release {\n signingConfig = if (releaseKeyProperties[\"storeFile\"] != null)\n signingConfigs.getByName(\"release\")\n else\n signingConfigs.getByName(\"debug\")\n isMinifyEnabled = true\n isShrinkResources = true\n proguardFiles(\n getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n \"proguard-rules.pro\",\n )\n }\n }\n}`;\n\nfunction enableAndroidReleaseSigning(appRoot: string): void {\n const gradlePath = resolve(appRoot, \"android/app/build.gradle.kts\");\n const gradle = readFileSync(gradlePath, \"utf8\");\n // Not `gradle.includes(\"signingConfigs\")` — Flutter's own template already\n // contains that substring (`signingConfig = signingConfigs.getByName(\"debug\")`),\n // so that check would always short-circuit and never append anything.\n if (gradle.includes(\"releaseKeyProperties\")) return;\n // A pure append. This previously also PREPENDED `import java.util.Properties`,\n // because Kotlin requires imports before every other top-level declaration —\n // which meant reaching into the very start of a file this scaffolder does\n // not own, the most position-dependent edit here and the one already broken\n // once by an AGP upgrade. Parsing `key.properties` with the Kotlin stdlib\n // needs no import, so the edit is now append-only and cannot collide with\n // whatever Flutter or `flutterfire configure` writes above it.\n appendFileSync(gradlePath, `${gradle.endsWith(\"\\n\") ? \"\" : \"\\n\"}${androidReleaseSigning}\\n`);\n}\n\n// `flutter_launcher_icons`/`flutter_native_splash` read these as plain\n// top-level pubspec keys, so appending is safe regardless of what\n// `flutter create` already wrote under `flutter:`. Deliberately omits\n// `ios`/`macos` — `scaffoldGame` only creates `--platforms android,web`, and\n// `flutter_launcher_icons` errors if told to target a platform whose\n// directory doesn't exist.\n// The colours are the EigenInteractive palette (ink #1B1E24, paper #F4F1EA) because the\n// shipped placeholder art is the EigenInteractive mark: the adaptive foreground is the\n// reversed, light-on-dark variant, so an ink background is what makes it\n// legible. Rebranding a game means replacing the four PNGs *and* these\n// colours together — see https://eigeninteractive.com/docs/ship-it/branding.\nconst launcherIconsAndSplashConfig = `\nflutter_launcher_icons:\n android: true\n web:\n generate: true\n image_path: \"assets/icon/icon.png\"\n image_path: \"assets/icon/icon.png\"\n adaptive_icon_background: \"#1B1E24\"\n adaptive_icon_foreground: \"assets/icon/icon_foreground.png\"\n min_sdk_android: 21\n\nflutter_native_splash:\n color: \"#F4F1EA\"\n color_dark: \"#1B1E24\"\n image: assets/icon/splash.png\n image_dark: assets/icon/splash_dark.png\n fullscreen: true\n android_12:\n color: \"#F4F1EA\"\n color_dark: \"#1B1E24\"\n image: assets/icon/splash.png\n image_dark: assets/icon/splash_dark.png\n icon_background_color: \"#F4F1EA\"\n icon_background_color_dark: \"#1B1E24\"\n web: true\n`;\n\nfunction configureLauncherIconsAndSplash(appRoot: string): void {\n const pubspecPath = resolve(appRoot, \"pubspec.yaml\");\n const pubspec = readFileSync(pubspecPath, \"utf8\");\n if (pubspec.includes(\"flutter_launcher_icons:\")) return;\n appendFileSync(pubspecPath, `${pubspec.endsWith(\"\\n\") ? \"\" : \"\\n\"}${launcherIconsAndSplashConfig}`);\n}\n\nexport interface AddContinuousIntegrationOptions {\n directory: string;\n /** Overrides the manager recorded in the project's own package.json. */\n packageManager?: PackageManager;\n}\n\n/**\n * Adds the GitHub Actions workflows to an existing generated project.\n *\n * The companion to `ci: false`: a game can be built and played locally for as\n * long as its author likes, then gain the same PR gate and signed release\n * pipeline a `--ci` scaffold would have produced.\n */\nexport function addContinuousIntegration(options: AddContinuousIntegrationOptions): { root: string; files: string[] } {\n const root = resolve(options.directory);\n const manifestPath = resolve(root, \"package.json\");\n if (!existsSync(manifestPath) || !existsSync(resolve(root, \"server\")) || !existsSync(resolve(root, \"app\"))) {\n throw new Error(`not an EigenInteractive game project (expected package.json, server/ and app/): ${root}`);\n }\n\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as {\n name?: string;\n scripts?: Record<string, string>;\n };\n const name = gameSlug(manifest.name ?? basename(root));\n\n // The generated `contract` script embeds the manager the project was\n // scaffolded with (`cd server && pnpm run contract && …`), which is a more\n // reliable signal than a lockfile — it is present before anything is\n // installed, and it is what the project actually uses.\n const recorded = manifest.scripts?.contract?.includes(\"pnpm \") ? \"pnpm\" : manifest.scripts?.contract?.includes(\"npm \") ? \"npm\" : undefined;\n const manager = options.packageManager ?? recorded ?? detectPackageManager() ?? \"pnpm\";\n\n // Only `{{PACKAGE_MANAGER}}` appears in these templates, but the real\n // package id is read back rather than reconstructed, so a project scaffolded\n // under a different `--org` cannot be silently rewritten to `com.example`.\n const appfile = resolve(root, \"app/fastlane/Appfile\");\n const packageId = existsSync(appfile) ? (/package_name\\(\"([^\"]+)\"\\)/.exec(readFileSync(appfile, \"utf8\"))?.[1] ?? `com.example.${dartName(name)}`) : `com.example.${dartName(name)}`;\n\n const workflows = resolve(root, \".github/workflows\");\n const existing = existsSync(workflows) ? readdirSync(workflows).filter((f) => f === \"checks.yml\" || f === \"release.yml\") : [];\n if (existing.length > 0) {\n throw new Error(`refusing to overwrite existing workflows: ${existing.join(\", \")}`);\n }\n\n renderTree(resolve(templatesRoot, \"ci\"), root, name, manager, packageId);\n return { root, files: [\".github/workflows/checks.yml\", \".github/workflows/release.yml\"] };\n}\n\n/** Asks a command a question. `ok` is \"ran and succeeded\"; a tool that is not installed and one that fails are both `false`, because neither can be used. */\nexport type Probe = (command: string, args: string[]) => { ok: boolean; stdout: string };\n\nconst probeCommand: Probe = (command, args) => {\n try {\n return { ok: true, stdout: execFileSync(command, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] }) };\n } catch {\n return { ok: false, stdout: \"\" };\n }\n};\n\n/** What each Firebase tool needs, for a machine that has neither. */\nconst firebaseTools: Record<string, string> = {\n flutterfire: \"dart pub global activate flutterfire_cli\",\n firebase: \"npm install -g firebase-tools\",\n};\n\nexport type FirebaseReadiness = { ready: true } | { ready: false; reason: string; fix: string };\n\n/**\n * Whether this machine can configure Firebase, asked *before* the scaffold\n * rather than after it.\n *\n * `configure_firebase` runs the same three checks and is the authority on\n * them; this one exists only to move the answer earlier. Learning that\n * `flutterfire` is missing costs nothing here and costs two minutes of Flutter\n * and pub at the far end, which is where the step actually runs.\n *\n * The sign-in check fails open, for the same reason it does there: only an\n * answer that positively reports no accounts counts as \"no\", so a `firebase`\n * that errors or grows a different output shape is left to the command that\n * actually needs the credentials.\n */\nexport function firebaseReadiness(probe: Probe = probeCommand): FirebaseReadiness {\n for (const [tool, fix] of Object.entries(firebaseTools)) {\n if (!probe(tool, [\"--version\"]).ok) return { ready: false, reason: `\\`${tool}\\` is not installed`, fix };\n }\n\n const accounts = probe(\"firebase\", [\"login:list\", \"--json\"]);\n if (accounts.ok) {\n try {\n const { result } = JSON.parse(accounts.stdout) as { result?: unknown };\n if (Array.isArray(result) && result.length === 0) {\n return { ready: false, reason: \"no Google account is signed in to the Firebase CLI\", fix: \"firebase login\" };\n }\n } catch {\n // Unparseable is not evidence of anything.\n }\n }\n\n return { ready: true };\n}\n\n/**\n * Runs the Flutter client's `configure_firebase` against the generated app.\n *\n * The command owns everything about how Firebase is configured — which\n * platforms, which CLIs must exist, whether anyone is signed in, and what to\n * do when no project has been chosen yet. This only decides *when*: before the\n * scaffold commit, so a configured project is committed configured.\n *\n * Not fatal. Every failure here — a missing `flutterfire`, no Google login, a\n * cancelled picker — leaves a complete and usable project that is exactly what\n * a scaffold without `--firebase` produces, so it warns with the command to\n * re-run and lets the commit happen.\n */\nfunction configureFirebase(appRoot: string, project: boolean | string, run: Runner, reporter: Reporter): \"configured\" | \"failed\" {\n // FlutterFire asks before overwriting `firebase_options.dart`, and the\n // scaffold has just written one — a throwing placeholder whose entire\n // purpose is to be replaced by this step. Answering that is a question with\n // one right answer, so the placeholder is moved out of the way and the\n // question never gets asked.\n const placeholder = resolve(appRoot, \"lib/firebase_options.dart\");\n const parked = `${placeholder}.placeholder`;\n const parking = existsSync(placeholder);\n if (parking) renameSync(placeholder, parked);\n\n try {\n // `dart run` compiles the package executable on first use and says so.\n // Doing it against `--help`, which returns immediately, moves that noise\n // into a step whose output can be captured — the real run cannot be, since\n // FlutterFire prompts through it.\n reporter.step(\"Preparing the Firebase configurator\", () => run(\"dart\", [\"run\", \"eigen_flutter:configure_firebase\", \"--help\"], appRoot));\n reporter.handOver(\"Configuring Firebase\", () => run(\"dart\", [\"run\", \"eigen_flutter:configure_firebase\", ...(typeof project === \"string\" ? [\"--project\", project] : [])], appRoot));\n if (parking) rmSync(parked, { force: true });\n return \"configured\";\n } catch {\n // Back only if nothing took its place. FlutterFire writes\n // `firebase_options.dart` before the service worker configuration is\n // derived from it, so a late failure leaves a real one that is worth more\n // than the placeholder.\n if (parking) {\n if (existsSync(placeholder)) rmSync(parked, { force: true });\n else renameSync(parked, placeholder);\n }\n reporter.warn(\"Could not configure Firebase. The project is complete — run `firebase:configure` yourself, then commit what it writes.\");\n return \"failed\";\n }\n}\n\n/**\n * Commits the scaffold, so the first `git diff` is the first game change.\n *\n * This matters more here than in a scaffolder that only writes source. The\n * bootstrap runs `flutter_launcher_icons` and `flutter_native_splash`, which\n * write generated-but-committed files across `android/`, `web/` and\n * `assets/`; without a baseline the first branding change is indistinguishable\n * from the ninety files the scaffolder happened to produce.\n *\n * Nothing here is fatal. The project is complete and valid before this runs,\n * so a missing `git`, or the unconfigured `user.email` that a fresh CI image\n * has, warns and leaves the tree alone rather than discarding a scaffold that\n * took two minutes of Flutter and pub to produce.\n */\nfunction initialiseRepository(root: string, name: string, run: Runner, reporter: Reporter): GitOutcome {\n // Scaffolding inside an existing checkout — a monorepo, or a repository\n // created ahead of time — is a legitimate thing to do, and a nested\n // repository there is silent breakage: the outer `git add` records a gitlink\n // and the app's files never leave the machine. Asked directly rather than\n // through `run`, because it is a question, not a step.\n try {\n execFileSync(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], { cwd: root, stdio: \"ignore\" });\n return \"existing\";\n } catch {\n // Not inside a work tree, which is the case this function is for.\n }\n\n try {\n run(\"git\", [\"init\", \"--quiet\"], root);\n run(\"git\", [\"add\", \"--all\"], root);\n } catch {\n reporter.warn(\"Could not initialise a git repository. The project is complete — run `git init` yourself when ready.\");\n return \"failed\";\n }\n\n try {\n run(\"git\", [\"commit\", \"--quiet\", \"--message\", `Scaffold ${name}`], root);\n } catch {\n reporter.warn(`Initialised a repository and staged the scaffold, but could not commit it. Set \\`user.name\\` and \\`user.email\\`, then \\`git commit -m \"Scaffold ${name}\"\\`.`);\n return \"failed\";\n }\n\n return \"committed\";\n}\n\nexport function scaffoldGame(options: ScaffoldOptions): ScaffoldResult {\n const root = resolve(options.directory);\n const name = gameSlug(basename(root));\n const manager = options.packageManager ?? detectPackageManager() ?? \"pnpm\";\n const org = options.org?.trim() || \"com.example\";\n // Fastlane's `Appfile` names the same package the build actually produces,\n // and the CLI shows the same one before asking.\n const packageId = applicationId(root, org);\n const bootstrap = options.bootstrap ?? true;\n const reporter = options.reporter ?? plainReporter;\n const run = options.run ?? capturingRunner(reporter);\n\n if (existsSync(root)) throw new Error(`target already exists: ${root}`);\n\n mkdirSync(dirname(root), { recursive: true });\n\n // Build beside the destination and publish it with one rename. Failed\n // subprocesses never leave a half-scaffolded project at the requested path.\n const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));\n try {\n const serverRoot = resolve(stagingRoot, \"server\");\n const appRoot = resolve(stagingRoot, \"app\");\n\n renderTree(resolve(templatesRoot, \"worker\"), serverRoot, name, manager, packageId);\n renderTree(resolve(templatesRoot, \"project\"), stagingRoot, name, manager, packageId);\n // Opt-in. release.yml wants an upload keystore and a Play service account\n // that a brand-new project does not have, so generating it by default\n // means a red X on main from the first push until someone sets both up.\n // `create-eigen-game add ci` writes the same files whenever it is wanted.\n if (options.ci) renderTree(resolve(templatesRoot, \"ci\"), stagingRoot, name, manager, packageId);\n\n if (bootstrap) {\n reporter.step(\"Creating the Flutter app\", () => {\n run(\"flutter\", [\"create\", \"--empty\", \"--platforms\", \"android,web\", \"--project-name\", dartName(name), \"--org\", org, appRoot], stagingRoot);\n enableAndroidCoreLibraryDesugaring(appRoot);\n enableAndroidReleaseSigning(appRoot);\n });\n } else {\n mkdirSync(appRoot, { recursive: true });\n }\n renderTree(resolve(templatesRoot, \"app-overlay\"), appRoot, name, manager, packageId);\n\n if (bootstrap) {\n reporter.step(\"Adding the Flutter packages\", () => {\n configureLauncherIconsAndSplash(appRoot);\n run(\"flutter\", [\"pub\", \"add\", `eigen_flutter@${flutterClientVersion}`, \"firebase_core@^4.9.0\", \"firebase_messaging@^16.2.2\"], appRoot);\n run(\"flutter\", [\"pub\", \"add\", \"dev:flutter_launcher_icons\", \"dev:flutter_native_splash\"], appRoot);\n });\n // Actually apply the icons rather than only configuring them. Both tools\n // write generated files (mipmaps, `web/icons/`, splash drawables and\n // styles) that are committed, not built — leaving them unrun would ship\n // Flutter's own blue logo until a game author happened to notice.\n reporter.step(\"Generating the icons and splash\", () => {\n run(\"dart\", [\"run\", \"flutter_launcher_icons\"], appRoot);\n run(\"dart\", [\"run\", \"flutter_native_splash:create\"], appRoot);\n });\n const [install, installArgs] = packageCommand(manager, \"install\");\n 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, 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":[]}
|