create-eigen-game 0.13.0 → 0.13.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/dist/cli.js CHANGED
@@ -1,156 +1,206 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- addContinuousIntegration,
4
- applicationId,
5
- destinationProblem,
6
- detectPackageManager,
7
- firebaseReadiness,
8
- insideWorkTree,
9
- normaliseTerminalWidth,
10
- plainReporter,
11
- scaffoldGame
12
- } from "./chunk-L7EVXO3Y.js";
13
-
14
- // src/cli.ts
15
- import { readFileSync } from "fs";
16
- import { basename, dirname, resolve } from "path";
17
- import { fileURLToPath } from "url";
18
- import { parseArgs } from "util";
19
- import { cancel, intro, isCI, isTTY, log, note, outro, taskLog } from "@clack/prompts";
20
- import color2 from "picocolors";
21
-
22
- // src/prompt.ts
23
- import { TextPrompt } from "@clack/core";
24
- import { confirm, isCancel, S_BAR, S_BAR_END, select, symbol } from "@clack/prompts";
2
+ import { a as destinationProblem, c as insideWorkTree, d as scaffoldGame, l as normaliseTerminalWidth, n as applicationId, o as detectPackageManager, s as firebaseReadiness, t as addContinuousIntegration, u as plainReporter } from "./src-q9BxESYC.js";
3
+ import { readFileSync } from "node:fs";
4
+ import { basename, dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseArgs } from "node:util";
7
+ import { S_BAR, S_BAR_END, cancel, confirm, intro, isCI, isCancel, isTTY, log, note, outro, select, symbol, taskLog } from "@clack/prompts";
25
8
  import color from "picocolors";
26
- var ORG = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$/;
27
- var DEFAULT_ORG = "com.example";
28
- var bar = color.gray(S_BAR);
9
+ import { TextPrompt } from "@clack/core";
10
+ //#region src/prompt.ts
11
+ /**
12
+ * Two or more dot-separated Java identifiers. `flutter create --org` accepts
13
+ * anything and defers the complaint to Gradle, which reports it as a manifest
14
+ * error in a generated file, so `com.example-games` costs a full scaffold and
15
+ * a first build before anyone learns that a hyphen is not legal in a package
16
+ * segment.
17
+ */
18
+ const ORG = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$/;
19
+ /** The default, which an empty answer accepts. */
20
+ const DEFAULT_ORG = "com.example";
21
+ const bar = color.gray(S_BAR);
22
+ /** A `com.acme.chess` answer to a game called `chess`, which yields `com.acme.chess.chess`. */
29
23
  function repeatsGameName(org, game) {
30
- return org.split(".").at(-1) === game;
24
+ return org.split(".").at(-1) === game;
31
25
  }
26
+ /** The organization implied by an answer that already ends in the game name. */
32
27
  function withoutGameName(org) {
33
- const shorter = org.split(".").slice(0, -1).join(".");
34
- return ORG.test(shorter) ? shorter : void 0;
28
+ const shorter = org.split(".").slice(0, -1).join(".");
29
+ return ORG.test(shorter) ? shorter : void 0;
35
30
  }
31
+ /**
32
+ * Asks, with the game name rendered dimmed after the cursor so the shape of
33
+ * the answer is visible while it is being given.
34
+ *
35
+ * Returns the organization, `undefined` for the default, or `null` when
36
+ * cancelled.
37
+ *
38
+ * `input` and `output` default to this process's, and are parameters for the
39
+ * same reason every clack prompt takes them: a prompt is otherwise the one
40
+ * part of a CLI that cannot be driven by a test.
41
+ */
36
42
  async function askForOrg(game, registering, io = {}) {
37
- const input = io.input ?? process.stdin;
38
- const output = io.output ?? process.stdout;
39
- const suffix = color.dim(`.${game}`);
40
- const prompt = new TextPrompt({
41
- input,
42
- output,
43
- // An empty answer is the default, not a mistake.
44
- validate: (value) => value === void 0 || value === "" || ORG.test(value) ? void 0 : "Two or more dot-separated segments, as in com.example or dev.yourname.games.",
45
- render() {
46
- const typed = String(this.value ?? "");
47
- const shown = typed === "" ? `${color.dim(DEFAULT_ORG)}${suffix}` : `${this.userInputWithCursor}${suffix}`;
48
- const problem = this.state === "error" ? `
49
- ${bar} ${color.yellow(this.error ?? "")}` : "";
50
- return [`${bar}`, `${symbol(this.state)} Organization in reverse domain notation`, `${bar} ${shown}${problem}`, color.gray(S_BAR_END)].join("\n");
51
- }
52
- });
53
- output.write(`${bar} ${color.dim("Prefixes the Android applicationId, which Google Play makes permanent at first upload.")}
54
- `);
55
- if (registering) output.write(`${bar} ${color.dim("Also the Android app registered in the Firebase project you pick next.")}
56
- `);
57
- const answer = await prompt.prompt();
58
- if (isCancel(answer)) return null;
59
- const org = String(answer).trim();
60
- if (org === "" || org === DEFAULT_ORG) return void 0;
61
- const shorter = repeatsGameName(org, game) ? withoutGameName(org) : void 0;
62
- if (shorter !== void 0) {
63
- const shorten = await confirm({
64
- message: `That gives ${color.yellow(`${org}.${game}`)}, the game name twice. Use ${color.green(shorter)} instead?`,
65
- initialValue: true,
66
- input,
67
- output
68
- });
69
- if (isCancel(shorten)) return null;
70
- if (shorten) return shorter;
71
- }
72
- return org;
43
+ const input = io.input ?? process.stdin;
44
+ const output = io.output ?? process.stdout;
45
+ const suffix = color.dim(`.${game}`);
46
+ const prompt = new TextPrompt({
47
+ input,
48
+ output,
49
+ validate: (value) => value === void 0 || value === "" || ORG.test(value) ? void 0 : "Two or more dot-separated segments, as in com.example or dev.yourname.games.",
50
+ render() {
51
+ const shown = String(this.value ?? "") === "" ? `${color.dim(DEFAULT_ORG)}${suffix}` : `${this.userInputWithCursor}${suffix}`;
52
+ const problem = this.state === "error" ? `\n${bar} ${color.yellow(this.error ?? "")}` : "";
53
+ return [
54
+ `${bar}`,
55
+ `${symbol(this.state)} Organization in reverse domain notation`,
56
+ `${bar} ${shown}${problem}`,
57
+ color.gray(S_BAR_END)
58
+ ].join("\n");
59
+ }
60
+ });
61
+ output.write(`${bar} ${color.dim("Prefixes the Android applicationId, which Google Play makes permanent at first upload.")}\n`);
62
+ if (registering) output.write(`${bar} ${color.dim("Also the Android app registered in the Firebase project you pick next.")}\n`);
63
+ const answer = await prompt.prompt();
64
+ if (isCancel(answer)) return null;
65
+ const org = String(answer).trim();
66
+ if (org === "" || org === "com.example") return void 0;
67
+ const shorter = repeatsGameName(org, game) ? withoutGameName(org) : void 0;
68
+ if (shorter !== void 0) {
69
+ const shorten = await confirm({
70
+ message: `That gives ${color.yellow(`${org}.${game}`)}, the game name twice. Use ${color.green(shorter)} instead?`,
71
+ initialValue: true,
72
+ input,
73
+ output
74
+ });
75
+ if (isCancel(shorten)) return null;
76
+ if (shorten) return shorter;
77
+ }
78
+ return org;
73
79
  }
80
+ /**
81
+ * A yes/no question, with its reason on the line above.
82
+ *
83
+ * The three below share this so cancellation is handled in one place: ^C
84
+ * during any question has to mean the run stops having written nothing, and
85
+ * that is only true if every call site checks. `null` is that answer.
86
+ *
87
+ * `hint` is written rather than passed to `confirm`, which has nowhere to put
88
+ * it. It is the same dimmed line the organization prompt uses, and carries the
89
+ * thing that makes the default the default: a question whose answer is
90
+ * pre-chosen owes the reader why.
91
+ */
74
92
  async function ask(message, initialValue, hint, io) {
75
- const input = io.input ?? process.stdin;
76
- const output = io.output ?? process.stdout;
77
- output.write(`${bar}
78
- ${bar} ${color.dim(hint)}
79
- `);
80
- const answer = await confirm({ message, initialValue, input, output });
81
- return isCancel(answer) ? null : answer;
93
+ const input = io.input ?? process.stdin;
94
+ const output = io.output ?? process.stdout;
95
+ output.write(`${bar}\n${bar} ${color.dim(hint)}\n`);
96
+ const answer = await confirm({
97
+ message,
98
+ initialValue,
99
+ input,
100
+ output
101
+ });
102
+ return isCancel(answer) ? null : answer;
82
103
  }
104
+ /**
105
+ * Asked only when {@link firebaseReadiness} found something missing, and
106
+ * defaulted to "no" because the honest answer usually is: the tools are two
107
+ * commands away, and the alternative is an app that throws at launch until
108
+ * they are installed anyway.
109
+ */
83
110
  function askToScaffoldWithoutFirebase(io = {}) {
84
- 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);
111
+ 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);
85
112
  }
113
+ /** 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. */
86
114
  function askForGit(io = {}) {
87
- return ask("Initialise a git repository and commit the scaffold?", true, "Commits once, so your first diff is your first game change.", io);
115
+ return ask("Initialise a git repository and commit the scaffold?", true, "Commits once, so your first diff is your first game change.", io);
88
116
  }
117
+ /**
118
+ * Defaulted to no, and the hint says why rather than leaving it as an
119
+ * arbitrary-looking choice: `release.yml` needs an upload keystore and a Play
120
+ * service account, so a project that takes the workflows before it has either
121
+ * gets a red `main` from its first push.
122
+ */
89
123
  function askForWorkflows(io = {}) {
90
- 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);
124
+ 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);
91
125
  }
126
+ /**
127
+ * The package manager, asked only when nothing else could say.
128
+ *
129
+ * `npm create` and `pnpm create` both set `npm_config_user_agent`, so this is
130
+ * reached by the paths that do not: a global install, or running the binary
131
+ * directly. Rare, but it decides what every generated script says, and the old
132
+ * silent fallback to pnpm was a guess printed into a project's package.json.
133
+ */
92
134
  async function askForPackageManager(io = {}) {
93
- const answer = await select({
94
- message: "Which package manager should the generated scripts use?",
95
- options: [
96
- { value: "pnpm", label: "pnpm" },
97
- { value: "npm", label: "npm" }
98
- ],
99
- input: io.input ?? process.stdin,
100
- output: io.output ?? process.stdout
101
- });
102
- return isCancel(answer) ? null : answer;
135
+ const answer = await select({
136
+ message: "Which package manager should the generated scripts use?",
137
+ options: [{
138
+ value: "pnpm",
139
+ label: "pnpm"
140
+ }, {
141
+ value: "npm",
142
+ label: "npm"
143
+ }],
144
+ input: io.input ?? process.stdin,
145
+ output: io.output ?? process.stdout
146
+ });
147
+ return isCancel(answer) ? null : answer;
103
148
  }
104
-
105
- // src/summary.ts
149
+ //#endregion
150
+ //#region src/summary.ts
151
+ /** Two-column rows, aligned to the widest left cell. */
106
152
  function columns(rows) {
107
- const width = Math.max(...rows.map(([left]) => left.length));
108
- return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);
153
+ const width = Math.max(...rows.map(([left]) => left.length));
154
+ return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);
109
155
  }
110
156
  function summarise(result, manager, workflows) {
111
- const run = manager === "npm" ? "npm run" : "pnpm";
112
- const status = [];
113
- if (result.git === "committed") status.push(`Committed as "Scaffold ${result.name}"`);
114
- if (result.git === "existing") status.push("Created inside a repository you already had, so nothing was committed");
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
- }
125
- const next = [
126
- "Write the game",
127
- ...columns([
128
- ["server/src/module/v1.ts", "the authoritative rules"],
129
- ["app/lib/game/v1/rules.dart", "the presentation twin"]
130
- ]),
131
- "",
132
- `Then, from ${result.name}/`,
133
- ...columns([
134
- [`cd server && ${run} test:watch`, "rules and twin fixtures"],
135
- [`cd server && ${run} dev`, "the Worker, on http://localhost:8787"],
136
- [`${run} contract`, "regenerate after a schema change"],
137
- // Named here because it is the one step the game itself cannot do
138
- // without: rules, fixtures and the Worker all run unconfigured, and the
139
- // app throws `Firebase is not configured` at launch until this has run
140
- // once.
141
- ...result.firebase === "configured" ? [] : [[`${run} firebase:configure`, "connect Firebase before the app runs"]]
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."]
144
- ].join("\n");
145
- const footnotes = ["Docs: https://eigeninteractive.com"];
146
- if (!workflows) footnotes.unshift("No CI workflows yet: `create-eigen-game add workflows` writes them when shipping is the next step.");
147
- return { status, next, footnotes, headline: `${result.name} is ready, at ${result.root}` };
157
+ const run = manager === "npm" ? "npm run" : "pnpm";
158
+ const status = [];
159
+ if (result.git === "committed") status.push(`Committed as "Scaffold ${result.name}"`);
160
+ if (result.git === "existing") status.push("Created inside a repository you already had, so nothing was committed");
161
+ if (result.firebase === "configured") status.push(result.git === "committed" ? "Firebase is configured, and everything it generated is in that commit" : "Firebase is configured");
162
+ 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`);
163
+ const remaining = [];
164
+ if (result.link !== void 0) {
165
+ if (result.link.googleWebClientId === null) {
166
+ remaining.push(["Enable Google sign-in", "Console > Authentication > Sign-in method"]);
167
+ remaining.push(["GOOGLE_WEB_CLIENT_ID", "that same page, under Web SDK configuration"]);
168
+ }
169
+ remaining.push(["FIREBASE_VAPID_KEY", "Console > Project settings > Cloud Messaging > Web configuration"]);
170
+ }
171
+ const next = [
172
+ "Write the game",
173
+ ...columns([["server/src/module/v1.ts", "the authoritative rules"], ["app/lib/game/v1/rules.dart", "the presentation twin"]]),
174
+ "",
175
+ `Then, from ${result.name}/`,
176
+ ...columns([
177
+ [`cd server && ${run} test:watch`, "rules and twin fixtures"],
178
+ [`cd server && ${run} dev`, "the Worker, on http://localhost:8787"],
179
+ [`${run} contract`, "regenerate after a schema change"],
180
+ ...result.firebase === "configured" ? [] : [[`${run} firebase:configure`, "connect Firebase before the app runs"]]
181
+ ]),
182
+ ...remaining.length === 0 ? [] : [
183
+ "",
184
+ "Before the app runs, from the Firebase console",
185
+ ...columns(remaining),
186
+ "",
187
+ " The values go in app/app-config.json. Both are public, not secrets."
188
+ ]
189
+ ].join("\n");
190
+ const footnotes = ["Docs: https://eigeninteractive.com"];
191
+ if (!workflows) footnotes.unshift("No CI workflows yet: `create-eigen-game add workflows` writes them when shipping is the next step.");
192
+ return {
193
+ status,
194
+ next,
195
+ footnotes,
196
+ headline: `${result.name} is ready, at ${result.root}`
197
+ };
148
198
  }
149
-
150
- // src/cli.ts
151
- var version = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8")).version;
152
- var PREREQUISITES = "https://eigeninteractive.com/docs/getting-started/prerequisites";
153
- var help = `Usage: create-eigen-game <game-slug> [options]
199
+ //#endregion
200
+ //#region src/cli.ts
201
+ const version = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8")).version;
202
+ const PREREQUISITES = "https://eigeninteractive.com/docs/getting-started/prerequisites";
203
+ const help = `Usage: create-eigen-game <game-slug> [options]
154
204
  create-eigen-game add workflows [directory] [options]
155
205
 
156
206
  Creates one repository containing an EigenInteractive Cloudflare Worker and
@@ -189,274 +239,312 @@ Commands:
189
239
  you are ready to ship rather than at scaffold time.
190
240
  Defaults to the current directory.`;
191
241
  normaliseTerminalWidth(process.stdout);
192
- var decorated = isTTY(process.stdout) && !isCI();
193
- var ui = decorated ? {
194
- open: () => intro(color2.inverse(" EigenInteractive ")),
195
- info: (message) => log.info(message),
196
- warn: (message) => log.warn(message),
197
- success: (message) => log.success(message),
198
- block: (body) => note(body, "Next"),
199
- close: (message) => outro(color2.bold(message)),
200
- stop: (message) => cancel(message)
242
+ /**
243
+ * Whether to draw, using clack's own predicates rather than a hand-rolled
244
+ * `isTTY`, the same pair `taskLog` consults internally. A pipe has nowhere to
245
+ * put a redraw, and a CI log is read after the fact, in full, by someone who
246
+ * wants every line a tool printed.
247
+ */
248
+ const decorated = isTTY(process.stdout) && !isCI();
249
+ /**
250
+ * The CLI's voice, in whichever of the two registers this terminal supports.
251
+ *
252
+ * Every message goes through here so the undecorated path cannot drift into
253
+ * being the untested one.
254
+ */
255
+ const ui = decorated ? {
256
+ open: () => intro(color.inverse(" EigenInteractive ")),
257
+ info: (message) => log.info(message),
258
+ warn: (message) => log.warn(message),
259
+ success: (message) => log.success(message),
260
+ block: (body) => note(body, "Next"),
261
+ close: (message) => outro(color.bold(message)),
262
+ stop: (message) => cancel(message)
201
263
  } : {
202
- open: () => console.log("\nEigenInteractive\n"),
203
- info: (message) => console.log(message),
204
- warn: (message) => console.warn(message),
205
- success: (message) => console.log(message),
206
- block: (body) => console.log(`
207
- ${body}
208
- `),
209
- close: (message) => console.log(message),
210
- stop: (message) => console.error(message)
264
+ open: () => console.log("\nEigenInteractive\n"),
265
+ info: (message) => console.log(message),
266
+ warn: (message) => console.warn(message),
267
+ success: (message) => console.log(message),
268
+ block: (body) => console.log(`\n${body}\n`),
269
+ close: (message) => console.log(message),
270
+ stop: (message) => console.error(message)
211
271
  };
212
272
  function resolveManager(requested) {
213
- if (requested === void 0) return void 0;
214
- if (requested !== "npm" && requested !== "pnpm") {
215
- throw new Error("--package-manager must be npm or pnpm");
216
- }
217
- return requested;
273
+ if (requested === void 0) return void 0;
274
+ if (requested !== "npm" && requested !== "pnpm") throw new Error("--package-manager must be npm or pnpm");
275
+ return requested;
218
276
  }
277
+ /**
278
+ * Why a scaffold asks about a second service at all, said once, before the
279
+ * first question rather than in the error that follows it.
280
+ *
281
+ * Firebase is not an integration this engine offers; it is where a player
282
+ * comes from. Somebody meeting that for the first time deserves the reason in
283
+ * the same breath as the requirement, and deserves to know the size of it:
284
+ * one free project, shared by every game they build.
285
+ */
219
286
  function greet() {
220
- ui.open();
221
- 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.");
222
- ui.info(
223
- `Firebase signs your players in. The Worker verifies Firebase ID tokens to decide who
224
- holds a seat, and sends turn notifications through the same project, so a scaffolded
225
- app throws ${color2.dim("Firebase is not configured")} at launch until one is connected.
226
-
227
- It is free to start, one project serves every game you build, and this connects yours
228
- as it goes. ${color2.dim(PREREQUISITES)}`
229
- );
287
+ ui.open();
288
+ 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.");
289
+ ui.info(`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)}`);
230
290
  }
291
+ /** What is missing, and the one command that fixes each: the same list the warning and the parting message are both built from. */
231
292
  function firebaseReport(problems) {
232
- const lines = problems.flatMap((problem) => [` ${problem.reason}`, ` ${color2.cyan(problem.fix)}`]);
233
- if (problems.some((problem) => problem.fix.startsWith("dart pub"))) {
234
- lines.push(` ${color2.dim('\u2026then, if your shell cannot find it: export PATH="$PATH":"$HOME/.pub-cache/bin"')}`);
235
- }
236
- return lines.join("\n");
293
+ const lines = problems.flatMap((problem) => [` ${problem.reason}`, ` ${color.cyan(problem.fix)}`]);
294
+ if (problems.some((problem) => problem.fix.startsWith("dart pub"))) lines.push(` ${color.dim("…then, if your shell cannot find it: export PATH=\"$PATH\":\"$HOME/.pub-cache/bin\"")}`);
295
+ return lines.join("\n");
237
296
  }
297
+ /**
298
+ * The parting message when someone answers "no" to scaffolding without
299
+ * Firebase: every command they need, in the order they run in.
300
+ *
301
+ * Deliberately not a failure of the scaffold: nothing has been written, they
302
+ * asked for that, and the exit code is non-zero only so a script that wrapped
303
+ * this can tell there is no project.
304
+ */
238
305
  function stopForFirebase(problems) {
239
- note(`${problems.map((problem) => ` ${color2.cyan(problem.fix)}`).join("\n")}
240
-
241
- Then run this again.
242
- ${color2.dim(PREREQUISITES)}`, "Set up Firebase");
243
- ui.stop("Nothing was written.");
244
- process.exitCode = 1;
306
+ note(`${problems.map((problem) => ` ${color.cyan(problem.fix)}`).join("\n")}\n\nThen run this again.\n${color.dim(PREREQUISITES)}`, "Set up Firebase");
307
+ ui.stop("Nothing was written.");
308
+ process.exitCode = 1;
245
309
  }
310
+ /**
311
+ * The error for a run with no terminal and an unanswered question.
312
+ *
313
+ * The strictness is the point: a default applied where nobody can see it is
314
+ * how `com.example.my_game` reaches Google Play, which freezes it at the first
315
+ * upload. What keeps that from being merely annoying is this message: the
316
+ * whole command, with every default already filled in, so the fix is one
317
+ * paste and the one value worth changing is visible in it.
318
+ */
246
319
  function unanswered(directory, missing) {
247
- const command = ["npx create-eigen-game", directory, ...missing.map((question) => question.suggestion)].join(" ");
248
- 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");
320
+ const command = [
321
+ "npx create-eigen-game",
322
+ directory,
323
+ ...missing.map((question) => question.suggestion)
324
+ ].join(" ");
325
+ return [
326
+ `No terminal to ask on, so every answer has to come from a flag. These were not given:`,
327
+ "",
328
+ ...missing.map((question) => ` ${question.flags}`),
329
+ "",
330
+ "That command, with each default filled in. Read them before you run it:",
331
+ "",
332
+ ` ${command}`
333
+ ].join("\n");
249
334
  }
335
+ /** `--foo` and `--no-foo`, which `parseArgs` has no notion of: `undefined` means nobody said, which is what makes it a question. */
250
336
  function flagPair(yes, no, name) {
251
- if (yes === true && no === true) throw new Error(`--${name} and --no-${name} contradict each other`);
252
- if (yes === true) return true;
253
- if (no === true) return false;
254
- return void 0;
337
+ if (yes === true && no === true) throw new Error(`--${name} and --no-${name} contradict each other`);
338
+ if (yes === true) return true;
339
+ if (no === true) return false;
255
340
  }
341
+ /** `--org ""` falls through to the default rather than failing, matching what an empty answer to the prompt does. */
256
342
  function resolveOrg(value) {
257
- if (value.trim() === "") return void 0;
258
- if (!ORG.test(value)) {
259
- throw new Error(`invalid organization: ${value}
260
-
261
- Use 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.`);
262
- }
263
- return value;
343
+ if (value.trim() === "") return void 0;
344
+ if (!ORG.test(value)) 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.`);
345
+ return value;
264
346
  }
347
+ /**
348
+ * Shows each step running, and keeps its output only when it fails.
349
+ *
350
+ * Every tool a scaffold drives has plenty to say (pub resolving 179
351
+ * dependencies, two icon generators, a package manager) and none of it is
352
+ * read while it scrolls past. `taskLog` is exactly that bargain: the stream is
353
+ * visible while the step runs, cleared when it succeeds, and left on screen
354
+ * when it does not, so a failure is still debuggable from what is in the
355
+ * terminal.
356
+ *
357
+ * `handOver` is the exception, and the reason `interactive` exists at all.
358
+ * FlutterFire asks which Firebase project to use, so that step needs the
359
+ * terminal rather than a captured pipe.
360
+ */
265
361
  function clackReporter() {
266
- if (!decorated) return plainReporter;
267
- let task;
268
- let interactive = false;
269
- return {
270
- step(label, body) {
271
- task = taskLog({ title: label });
272
- try {
273
- const value = body();
274
- task.success(label);
275
- return value;
276
- } catch (error) {
277
- task.error(label);
278
- throw error;
279
- } finally {
280
- task = void 0;
281
- }
282
- },
283
- handOver(label, body) {
284
- log.step(`${label} ${color2.dim("(FlutterFire takes over here)")}`);
285
- interactive = true;
286
- try {
287
- return body();
288
- } finally {
289
- interactive = false;
290
- }
291
- },
292
- emit(output) {
293
- const text = output.trimEnd();
294
- if (text !== "") task?.message(text);
295
- },
296
- warn(message) {
297
- ui.warn(message);
298
- },
299
- get interactive() {
300
- return interactive;
301
- }
302
- };
362
+ if (!decorated) return plainReporter;
363
+ let task;
364
+ let interactive = false;
365
+ return {
366
+ step(label, body) {
367
+ task = taskLog({ title: label });
368
+ try {
369
+ const value = body();
370
+ task.success(label);
371
+ return value;
372
+ } catch (error) {
373
+ task.error(label);
374
+ throw error;
375
+ } finally {
376
+ task = void 0;
377
+ }
378
+ },
379
+ handOver(label, body) {
380
+ log.step(`${label} ${color.dim("(FlutterFire takes over here)")}`);
381
+ interactive = true;
382
+ try {
383
+ return body();
384
+ } finally {
385
+ interactive = false;
386
+ }
387
+ },
388
+ emit(output) {
389
+ const text = output.trimEnd();
390
+ if (text !== "") task?.message(text);
391
+ },
392
+ warn(message) {
393
+ ui.warn(message);
394
+ },
395
+ get interactive() {
396
+ return interactive;
397
+ }
398
+ };
303
399
  }
304
400
  async function main(args) {
305
- const { values, positionals } = parseArgs({
306
- args,
307
- allowPositionals: true,
308
- strict: true,
309
- options: {
310
- help: { type: "boolean", short: "h" },
311
- version: { type: "boolean", short: "v" },
312
- org: { type: "string" },
313
- "firebase-project": { type: "string" },
314
- // Each negation is declared under the name it is typed with, because
315
- // `parseArgs` has no boolean negation of its own. `flagPair` puts the
316
- // two halves back together, and the third state, neither given, is the
317
- // one that matters: it is what turns the flag into a question.
318
- "no-firebase": { type: "boolean" },
319
- git: { type: "boolean" },
320
- "no-git": { type: "boolean" },
321
- workflows: { type: "boolean" },
322
- "no-workflows": { type: "boolean" },
323
- "package-manager": { type: "string" }
324
- }
325
- });
326
- if (values.help) {
327
- console.log(help);
328
- return;
329
- }
330
- if (values.version) {
331
- console.log(version);
332
- return;
333
- }
334
- const requestedManager = resolveManager(values["package-manager"]);
335
- if (positionals[0] === "add") {
336
- if (positionals[1] !== "workflows" && positionals[1] !== "ci") {
337
- throw new Error(`unknown subcommand: add ${positionals[1] ?? ""}
338
-
339
- ${help}`.trim());
340
- }
341
- if (positionals.length > 3) {
342
- throw new Error(`expected at most one directory
343
-
344
- ${help}`);
345
- }
346
- const result2 = addContinuousIntegration({
347
- directory: positionals[2] ?? process.cwd(),
348
- packageManager: requestedManager
349
- });
350
- console.log(`Added ${result2.files.join(" and ")} in ${result2.root}`);
351
- return;
352
- }
353
- if (positionals.length !== 1) {
354
- throw new Error(`expected exactly one destination directory
355
-
356
- ${help}`);
357
- }
358
- const directory = positionals[0];
359
- const game = basename(resolve(directory));
360
- applicationId(directory);
361
- const occupied = destinationProblem(directory);
362
- if (occupied !== void 0) throw new Error(occupied);
363
- greet();
364
- const interactive = process.stdin.isTTY === true;
365
- const missing = [];
366
- const cancelled = () => {
367
- ui.stop("Nothing was written.");
368
- process.exitCode = 130;
369
- };
370
- const needsFlag = (flags, suggestion, fallback) => {
371
- missing.push({ flags, suggestion });
372
- return fallback;
373
- };
374
- const project = values["firebase-project"]?.trim();
375
- if (project === "") throw new Error("--firebase-project needs a project id");
376
- if (project !== void 0 && values["no-firebase"] === true) {
377
- throw new Error("--firebase-project and --no-firebase contradict each other");
378
- }
379
- let firebase = false;
380
- if (project !== void 0) {
381
- firebase = project;
382
- } else if (values["no-firebase"] !== true) {
383
- const readiness = firebaseReadiness();
384
- if (readiness.ready) {
385
- if (interactive) firebase = true;
386
- else firebase = needsFlag("--firebase-project <id>, or --no-firebase", "--no-firebase", false);
387
- } else {
388
- ui.warn(`Firebase is not set up on this machine:
389
-
390
- ${firebaseReport(readiness.problems)}`);
391
- if (!interactive) {
392
- firebase = needsFlag("--no-firebase, to scaffold without it", "--no-firebase", false);
393
- } else {
394
- const carryOn = await askToScaffoldWithoutFirebase();
395
- if (carryOn === null) return cancelled();
396
- if (!carryOn) return stopForFirebase(readiness.problems);
397
- }
398
- }
399
- }
400
- let org;
401
- if (values.org !== void 0) {
402
- org = resolveOrg(values.org);
403
- } else if (interactive) {
404
- const answer = await askForOrg(game.replaceAll("-", "_"), firebase !== false);
405
- if (answer === null) return cancelled();
406
- org = answer;
407
- } else {
408
- org = needsFlag("--org <reverse-domain>", `--org ${DEFAULT_ORG}`, void 0);
409
- }
410
- let git = flagPair(values.git, values["no-git"], "git");
411
- if (git === void 0 && insideWorkTree(directory)) git = true;
412
- if (git === void 0) {
413
- if (!interactive) git = needsFlag("--git or --no-git", "--git", true);
414
- else {
415
- const answer = await askForGit();
416
- if (answer === null) return cancelled();
417
- git = answer;
418
- }
419
- }
420
- let workflows = flagPair(values.workflows, values["no-workflows"], "workflows");
421
- if (workflows === void 0) {
422
- if (!interactive) workflows = needsFlag("--workflows or --no-workflows", "--no-workflows", false);
423
- else {
424
- const answer = await askForWorkflows();
425
- if (answer === null) return cancelled();
426
- workflows = answer;
427
- }
428
- }
429
- let manager = requestedManager ?? detectPackageManager();
430
- if (manager === void 0) {
431
- if (!interactive) manager = needsFlag("--package-manager <npm|pnpm>", "--package-manager pnpm", "pnpm");
432
- else {
433
- const answer = await askForPackageManager();
434
- if (answer === null) return cancelled();
435
- manager = answer;
436
- }
437
- }
438
- if (missing.length > 0) throw new Error(unanswered(directory, missing));
439
- ui.success(`applicationId ${color2.bold(applicationId(directory, org))}`);
440
- const result = scaffoldGame({
441
- directory,
442
- org,
443
- packageManager: manager,
444
- ci: workflows,
445
- git,
446
- firebase,
447
- reporter: clackReporter()
448
- });
449
- const { status, next, footnotes, headline } = summarise(result, manager, workflows);
450
- for (const line of status) ui.success(line);
451
- ui.block(next);
452
- for (const line of footnotes) ui.info(line);
453
- ui.close(headline);
401
+ const { values, positionals } = parseArgs({
402
+ args,
403
+ allowPositionals: true,
404
+ strict: true,
405
+ options: {
406
+ help: {
407
+ type: "boolean",
408
+ short: "h"
409
+ },
410
+ version: {
411
+ type: "boolean",
412
+ short: "v"
413
+ },
414
+ org: { type: "string" },
415
+ "firebase-project": { type: "string" },
416
+ "no-firebase": { type: "boolean" },
417
+ git: { type: "boolean" },
418
+ "no-git": { type: "boolean" },
419
+ workflows: { type: "boolean" },
420
+ "no-workflows": { type: "boolean" },
421
+ "package-manager": { type: "string" }
422
+ }
423
+ });
424
+ if (values.help) {
425
+ console.log(help);
426
+ return;
427
+ }
428
+ if (values.version) {
429
+ console.log(version);
430
+ return;
431
+ }
432
+ const requestedManager = resolveManager(values["package-manager"]);
433
+ if (positionals[0] === "add") {
434
+ if (positionals[1] !== "workflows" && positionals[1] !== "ci") throw new Error(`unknown subcommand: add ${positionals[1] ?? ""}\n\n${help}`.trim());
435
+ if (positionals.length > 3) throw new Error(`expected at most one directory\n\n${help}`);
436
+ const result = addContinuousIntegration({
437
+ directory: positionals[2] ?? process.cwd(),
438
+ packageManager: requestedManager
439
+ });
440
+ console.log(`Added ${result.files.join(" and ")} in ${result.root}`);
441
+ return;
442
+ }
443
+ if (positionals.length !== 1) throw new Error(`expected exactly one destination directory\n\n${help}`);
444
+ const directory = positionals[0];
445
+ const game = basename(resolve(directory));
446
+ applicationId(directory);
447
+ const occupied = destinationProblem(directory);
448
+ if (occupied !== void 0) throw new Error(occupied);
449
+ greet();
450
+ const interactive = process.stdin.isTTY === true;
451
+ const missing = [];
452
+ const cancelled = () => {
453
+ ui.stop("Nothing was written.");
454
+ process.exitCode = 130;
455
+ };
456
+ /**
457
+ * Records a question that could not be asked, and hands back the default it
458
+ * suggested. The value is never used, since `missing` throws before anything
459
+ * is written, but returning it keeps each answer definitely typed instead of
460
+ * carrying a `| undefined` all the way to the scaffold call.
461
+ */
462
+ const needsFlag = (flags, suggestion, fallback) => {
463
+ missing.push({
464
+ flags,
465
+ suggestion
466
+ });
467
+ return fallback;
468
+ };
469
+ const project = values["firebase-project"]?.trim();
470
+ if (project === "") throw new Error("--firebase-project needs a project id");
471
+ if (project !== void 0 && values["no-firebase"] === true) throw new Error("--firebase-project and --no-firebase contradict each other");
472
+ let firebase = false;
473
+ if (project !== void 0) firebase = project;
474
+ else if (values["no-firebase"] !== true) {
475
+ const readiness = firebaseReadiness();
476
+ if (readiness.ready) {
477
+ if (interactive) firebase = true;
478
+ else firebase = needsFlag("--firebase-project <id>, or --no-firebase", "--no-firebase", false);
479
+ } else {
480
+ ui.warn(`Firebase is not set up on this machine:\n\n${firebaseReport(readiness.problems)}`);
481
+ if (!interactive) firebase = needsFlag("--no-firebase, to scaffold without it", "--no-firebase", false);
482
+ else {
483
+ const carryOn = await askToScaffoldWithoutFirebase();
484
+ if (carryOn === null) return cancelled();
485
+ if (!carryOn) return stopForFirebase(readiness.problems);
486
+ }
487
+ }
488
+ }
489
+ let org;
490
+ if (values.org !== void 0) org = resolveOrg(values.org);
491
+ else if (interactive) {
492
+ const answer = await askForOrg(game.replaceAll("-", "_"), firebase !== false);
493
+ if (answer === null) return cancelled();
494
+ org = answer;
495
+ } else org = needsFlag("--org <reverse-domain>", `--org ${DEFAULT_ORG}`, void 0);
496
+ let git = flagPair(values.git, values["no-git"], "git");
497
+ if (git === void 0 && insideWorkTree(directory)) git = true;
498
+ if (git === void 0) {
499
+ if (!interactive) git = needsFlag("--git or --no-git", "--git", true);
500
+ else {
501
+ const answer = await askForGit();
502
+ if (answer === null) return cancelled();
503
+ git = answer;
504
+ }
505
+ }
506
+ let workflows = flagPair(values.workflows, values["no-workflows"], "workflows");
507
+ if (workflows === void 0) {
508
+ if (!interactive) workflows = needsFlag("--workflows or --no-workflows", "--no-workflows", false);
509
+ else {
510
+ const answer = await askForWorkflows();
511
+ if (answer === null) return cancelled();
512
+ workflows = answer;
513
+ }
514
+ }
515
+ let manager = requestedManager ?? detectPackageManager();
516
+ if (manager === void 0) {
517
+ if (!interactive) manager = needsFlag("--package-manager <npm|pnpm>", "--package-manager pnpm", "pnpm");
518
+ else {
519
+ const answer = await askForPackageManager();
520
+ if (answer === null) return cancelled();
521
+ manager = answer;
522
+ }
523
+ }
524
+ if (missing.length > 0) throw new Error(unanswered(directory, missing));
525
+ ui.success(`applicationId ${color.bold(applicationId(directory, org))}`);
526
+ const { status, next, footnotes, headline } = summarise(scaffoldGame({
527
+ directory,
528
+ org,
529
+ packageManager: manager,
530
+ ci: workflows,
531
+ git,
532
+ firebase,
533
+ reporter: clackReporter()
534
+ }), manager, workflows);
535
+ for (const line of status) ui.success(line);
536
+ ui.block(next);
537
+ for (const line of footnotes) ui.info(line);
538
+ ui.close(headline);
454
539
  }
455
540
  try {
456
- await main(process.argv.slice(2));
541
+ await main(process.argv.slice(2));
457
542
  } catch (error) {
458
- const message = error instanceof Error ? error.message : String(error);
459
- ui.stop(`create-eigen-game: ${message}`);
460
- process.exitCode = 1;
543
+ const message = error instanceof Error ? error.message : String(error);
544
+ ui.stop(`create-eigen-game: ${message}`);
545
+ process.exitCode = 1;
461
546
  }
547
+ //#endregion
548
+ export {};
549
+
462
550
  //# sourceMappingURL=cli.js.map