create-eigen-game 0.9.0 → 0.9.2
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 +2 -2
- package/dist/{chunk-XTTEOV5U.js → chunk-X6NSW7BX.js} +80 -53
- package/dist/chunk-X6NSW7BX.js.map +1 -0
- package/dist/cli.js +20 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +27 -13
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/templates/app-overlay/android/app/proguard-rules.pro +1 -1
- package/templates/app-overlay/app-config.json.template +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 +34 -18
- package/templates/project/package.json +1 -1
- package/templates/worker/README.md +2 -2
- package/templates/worker/src/index.ts +1 -1
- package/dist/chunk-XTTEOV5U.js.map +0 -1
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/prompt.ts","../src/summary.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { cancel, intro, isCI, isTTY, log, note, outro, taskLog } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport { addContinuousIntegration, applicationId, destinationProblem, detectPackageManager, type FirebaseProblem, firebaseReadiness, insideWorkTree, normaliseTerminalWidth, type PackageManager, plainReporter, type Reporter, scaffoldGame } from \"./index.js\";\nimport { askForGit, askForOrg, askForPackageManager, askForWorkflows, askToScaffoldWithoutFirebase, DEFAULT_ORG, ORG } from \"./prompt.js\";\nimport { summarise } from \"./summary.js\";\n\nconst version = (JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), \"../package.json\"), \"utf8\")) as { version: string }).version;\n\nconst PREREQUISITES = \"https://eigeninteractive.com/docs/getting-started/prerequisites\";\n\nconst help = `Usage: create-eigen-game <game-slug> [options]\n create-eigen-game add workflows [directory] [options]\n\nCreates one repository containing an EigenInteractive Cloudflare Worker and\nFlutter app, and commits it. The destination basename is the lowercase\nkebab-case game slug; the CLI derives the display name, Dart package name, and\nDart/TypeScript type prefix from it.\n\nEvery option below answers a question the CLI would otherwise ask. Pass it to\nskip the question; leave it out to be asked. With no terminal to ask on — CI, a\npipe, an agent session — an unanswered question is an error rather than a\ndefault quietly chosen for you, and the message prints the command to re-run.\n\nOptions:\n --org <reverse-domain> Android/iOS organization. The applicationId is this\n plus the game name, as in com.example.my_game, and\n Google Play makes it permanent at first upload\n --firebase-project <id>\n The Firebase project to configure against. Also the\n form that works with no terminal, since otherwise\n FlutterFire asks\n --no-firebase Do not configure Firebase, leaving firebase:configure\n as a step for later\n --git, --no-git Initialise a repository and commit the scaffold\n --workflows, --no-workflows\n Emit the GitHub Actions workflows. release.yml needs\n an upload keystore and a Play service account, so it\n fails on every push until both exist\n --package-manager <pm> npm or pnpm. Taken from the invoking package manager\n when there is one, so this is for global installs\n -h, --help Show this help\n -v, --version Show the version\n\nCommands:\n add workflows [directory]\n Add the workflows to an existing project, for when\n you are ready to ship rather than at scaffold time.\n Defaults to the current directory.`;\n\nnormaliseTerminalWidth(process.stdout);\n\n/**\n * Whether to draw, using clack's own predicates rather than a hand-rolled\n * `isTTY` — the same pair `taskLog` consults internally. A pipe has nowhere to\n * put a redraw, and a CI log is read after the fact, in full, by someone who\n * wants every line a tool printed.\n */\nconst decorated = isTTY(process.stdout) && !isCI();\n\n/**\n * The CLI's voice, in whichever of the two registers this terminal supports.\n *\n * Every message goes through here so the undecorated path cannot drift into\n * being the untested one.\n */\nconst ui = decorated\n ? {\n open: () => intro(color.inverse(\" EigenInteractive \")),\n info: (message: string) => log.info(message),\n warn: (message: string) => log.warn(message),\n success: (message: string) => log.success(message),\n block: (body: string) => note(body, \"Next\"),\n close: (message: string) => outro(color.bold(message)),\n stop: (message: string) => cancel(message),\n }\n : {\n open: () => console.log(\"\\nEigenInteractive\\n\"),\n info: (message: string) => console.log(message),\n warn: (message: string) => console.warn(message),\n success: (message: string) => console.log(message),\n block: (body: string) => console.log(`\\n${body}\\n`),\n close: (message: string) => console.log(message),\n stop: (message: string) => console.error(message),\n };\n\nfunction resolveManager(requested: string | undefined): PackageManager | undefined {\n if (requested === undefined) return undefined;\n if (requested !== \"npm\" && requested !== \"pnpm\") {\n throw new Error(\"--package-manager must be npm or pnpm\");\n }\n return requested;\n}\n\n/**\n * Why a scaffold asks about a second service at all, said once, before the\n * first question rather than in the error that follows it.\n *\n * Firebase is not an integration this engine offers; it is where a player\n * comes from. Somebody meeting that for the first time deserves the reason in\n * the same breath as the requirement, and deserves to know the size of it —\n * one free project, shared by every game they build.\n */\nfunction greet(): void {\n ui.open();\n ui.info(\"A game is one repository with two halves: a Cloudflare Worker that owns the rules,\\nand a Flutter app for Android and the web. This writes both, wired together and\\nalready playable.\");\n ui.info(\n `Firebase signs your players in. The Worker verifies Firebase ID tokens to decide who\\nholds a seat, and sends turn notifications through the same project — so a scaffolded\\napp throws ${color.dim(\"Firebase is not configured\")} at launch until one is connected.\\n\\nIt is free to start, one project serves every game you build, and this connects yours\\nas it goes. ${color.dim(PREREQUISITES)}`,\n );\n}\n\n/** What is missing, and the one command that fixes each — the same list the warning and the parting message are both built from. */\nfunction firebaseReport(problems: FirebaseProblem[]): string {\n const lines = problems.flatMap((problem) => [` ${problem.reason}`, ` ${color.cyan(problem.fix)}`]);\n // `dart pub global activate` does not put the binary on PATH, which is the\n // next thing to go wrong for anyone who has just run the command above.\n if (problems.some((problem) => problem.fix.startsWith(\"dart pub\"))) {\n lines.push(` ${color.dim('…then, if your shell cannot find it: export PATH=\"$PATH\":\"$HOME/.pub-cache/bin\"')}`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * The parting message when someone answers \"no\" to scaffolding without\n * Firebase — every command they need, in the order they run in.\n *\n * Deliberately not a failure of the scaffold: nothing has been written, they\n * asked for that, and the exit code is non-zero only so a script that wrapped\n * this can tell there is no project.\n */\nfunction stopForFirebase(problems: FirebaseProblem[]): void {\n // The commands only. What each one is for is in the warning directly above,\n // and a `note` draws a box sized to its longest line — so repeating the\n // reasons here is what makes it wrap into something unreadable at 80\n // columns, which is exactly the width this is most likely to be read at.\n note(`${problems.map((problem) => ` ${color.cyan(problem.fix)}`).join(\"\\n\")}\\n\\nThen run this again.\\n${color.dim(PREREQUISITES)}`, \"Set up Firebase\");\n ui.stop(\"Nothing was written.\");\n process.exitCode = 1;\n}\n\n/**\n * The error for a run with no terminal and an unanswered question.\n *\n * The strictness is the point — a default applied where nobody can see it is\n * how `com.example.my_game` reaches Google Play, which freezes it at the first\n * upload. What keeps that from being merely annoying is this message: the\n * whole command, with every default already filled in, so the fix is one\n * paste and the one value worth changing is visible in it.\n */\nfunction unanswered(directory: string, missing: { flags: string; suggestion: string }[]): string {\n const command = [\"npx create-eigen-game\", directory, ...missing.map((question) => question.suggestion)].join(\" \");\n return [`No terminal to ask on, so every answer has to come from a flag. These were not given:`, \"\", ...missing.map((question) => ` ${question.flags}`), \"\", \"That command, with each default filled in — read them before you run it:\", \"\", ` ${command}`].join(\"\\n\");\n}\n\n/** `--foo` and `--no-foo`, which `parseArgs` has no notion of: `undefined` means nobody said, which is what makes it a question. */\nfunction flagPair(yes: boolean | undefined, no: boolean | undefined, name: string): boolean | undefined {\n if (yes === true && no === true) throw new Error(`--${name} and --no-${name} contradict each other`);\n if (yes === true) return true;\n if (no === true) return false;\n return undefined;\n}\n\n/** `--org \"\"` falls through to the default rather than failing, matching what an empty answer to the prompt does. */\nfunction resolveOrg(value: string): string | undefined {\n if (value.trim() === \"\") return undefined;\n if (!ORG.test(value)) {\n throw new Error(`invalid organization: ${value}\\n\\nUse reverse domain notation — two or more dot-separated segments of letters, digits and underscores, each starting with a letter, as in ${DEFAULT_ORG} or dev.yourname.games.`);\n }\n return value;\n}\n\n/**\n * Shows each step running, and keeps its output only when it fails.\n *\n * Every tool a scaffold drives has plenty to say — pub resolving 179\n * dependencies, two icon generators, a package manager — and none of it is\n * read while it scrolls past. `taskLog` is exactly that bargain: the stream is\n * visible while the step runs, cleared when it succeeds, and left on screen\n * when it does not, so a failure is still debuggable from what is in the\n * terminal.\n *\n * `handOver` is the exception, and the reason `interactive` exists at all.\n * FlutterFire asks which Firebase project to use, so that step needs the\n * terminal rather than a captured pipe.\n */\nfunction clackReporter(): Reporter {\n // `taskLog` redraws by clearing lines, which is an escape-sequence storm in\n // anything that is not a terminal — a CI log, a pipe, `scaffold-e2e.mjs`.\n // There, the honest thing is the plain reporter: every tool's output, in\n // full, which is what a log gets read for anyway.\n if (!decorated) return plainReporter;\n\n let task: ReturnType<typeof taskLog> | undefined;\n let interactive = false;\n\n return {\n step(label, body) {\n task = taskLog({ title: label });\n try {\n const value = body();\n task.success(label);\n return value;\n } catch (error) {\n task.error(label);\n throw error;\n } finally {\n task = undefined;\n }\n },\n handOver(label, body) {\n log.step(`${label} ${color.dim(\"— FlutterFire takes over here\")}`);\n interactive = true;\n try {\n return body();\n } finally {\n interactive = false;\n }\n },\n emit(output) {\n const text = output.trimEnd();\n if (text !== \"\") task?.message(text);\n },\n warn(message) {\n ui.warn(message);\n },\n get interactive() {\n return interactive;\n },\n };\n}\n\nasync function main(args: string[]): Promise<void> {\n const { values, positionals } = parseArgs({\n args,\n allowPositionals: true,\n strict: true,\n options: {\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n org: { type: \"string\" },\n \"firebase-project\": { type: \"string\" },\n // Each negation is declared under the name it is typed with, because\n // `parseArgs` has no boolean negation of its own. `flagPair` puts the\n // two halves back together, and the third state — neither given — is the\n // one that matters: it is what turns the flag into a question.\n \"no-firebase\": { type: \"boolean\" },\n git: { type: \"boolean\" },\n \"no-git\": { type: \"boolean\" },\n workflows: { type: \"boolean\" },\n \"no-workflows\": { type: \"boolean\" },\n \"package-manager\": { type: \"string\" },\n },\n });\n\n if (values.help) {\n console.log(help);\n return;\n }\n if (values.version) {\n console.log(version);\n return;\n }\n\n const requestedManager = resolveManager(values[\"package-manager\"]);\n\n if (positionals[0] === \"add\") {\n // `ci` is the name this shipped under, and it is written into the README\n // of every project already scaffolded. Those READMEs are on disk and are\n // not going to be edited, so the old spelling keeps working; only the new\n // one is documented.\n if (positionals[1] !== \"workflows\" && positionals[1] !== \"ci\") {\n throw new Error(`unknown subcommand: add ${positionals[1] ?? \"\"}\\n\\n${help}`.trim());\n }\n if (positionals.length > 3) {\n throw new Error(`expected at most one directory\\n\\n${help}`);\n }\n const result = addContinuousIntegration({\n directory: positionals[2] ?? process.cwd(),\n packageManager: requestedManager,\n });\n console.log(`Added ${result.files.join(\" and \")} in ${result.root}`);\n return;\n }\n\n if (positionals.length !== 1) {\n throw new Error(`expected exactly one destination directory\\n\\n${help}`);\n }\n\n const directory = positionals[0];\n\n // Everything that makes the command impossible, before the greeting rather\n // than after the last question. `applicationId` validates the slug;\n // `destinationProblem` is the one `scaffoldGame` would raise at the far end,\n // asked here instead — being told the directory was occupied *after* giving\n // an organization is the same insult as being told about `flutterfire` after\n // two minutes of Flutter and pub.\n const game = basename(resolve(directory));\n applicationId(directory);\n const occupied = destinationProblem(directory);\n if (occupied !== undefined) throw new Error(occupied);\n\n greet();\n\n // Every question this run will ask, settled before any of it starts. A flag\n // is an answer already given; anything left over is asked, or — with nothing\n // to ask on — collected here and reported together, so one re-run can carry\n // all of them.\n const interactive = process.stdin.isTTY === true;\n const missing: { flags: string; suggestion: string }[] = [];\n const cancelled = () => {\n ui.stop(\"Nothing was written.\");\n process.exitCode = 130;\n };\n\n /**\n * Records a question that could not be asked, and hands back the default it\n * suggested. The value is never used — `missing` throws before anything is\n * written — but returning it keeps each answer definitely typed instead of\n * carrying a `| undefined` all the way to the scaffold call.\n */\n const needsFlag = <T>(flags: string, suggestion: string, fallback: T): T => {\n missing.push({ flags, suggestion });\n return fallback;\n };\n\n // Firebase goes first because it is the only answer that can end the run,\n // and ending it after the organization prompt would mean asking for a\n // permanent decision about a project that never gets created.\n const project = values[\"firebase-project\"]?.trim();\n if (project === \"\") throw new Error(\"--firebase-project needs a project id\");\n if (project !== undefined && values[\"no-firebase\"] === true) {\n throw new Error(\"--firebase-project and --no-firebase contradict each other\");\n }\n\n let firebase: boolean | string = false;\n if (project !== undefined) {\n firebase = project;\n } else if (values[\"no-firebase\"] !== true) {\n const readiness = firebaseReadiness();\n if (readiness.ready) {\n // Nothing for this CLI to ask: FlutterFire runs its own project picker\n // during the scaffold, and needs a terminal to do it.\n if (interactive) firebase = true;\n else firebase = needsFlag(\"--firebase-project <id>, or --no-firebase\", \"--no-firebase\", false);\n } else {\n ui.warn(`Firebase is not set up on this machine:\\n\\n${firebaseReport(readiness.problems)}`);\n if (!interactive) {\n firebase = needsFlag(\"--no-firebase, to scaffold without it\", \"--no-firebase\", false);\n } else {\n const carryOn = await askToScaffoldWithoutFirebase();\n if (carryOn === null) return cancelled();\n if (!carryOn) return stopForFirebase(readiness.problems);\n }\n }\n }\n\n let org: string | undefined;\n if (values.org !== undefined) {\n org = resolveOrg(values.org);\n } else if (interactive) {\n const answer = await askForOrg(game.replaceAll(\"-\", \"_\"), firebase !== false);\n if (answer === null) return cancelled();\n org = answer;\n } else {\n org = needsFlag(\"--org <reverse-domain>\", `--org ${DEFAULT_ORG}`, undefined);\n }\n\n // A destination inside a repository someone already has is not a question:\n // `scaffoldGame` declines to nest one repository in another whatever it is\n // told, and says so in the summary.\n let git = flagPair(values.git, values[\"no-git\"], \"git\");\n if (git === undefined && insideWorkTree(directory)) git = true;\n if (git === undefined) {\n if (!interactive) git = needsFlag(\"--git or --no-git\", \"--git\", true);\n else {\n const answer = await askForGit();\n if (answer === null) return cancelled();\n git = answer;\n }\n }\n\n let workflows = flagPair(values.workflows, values[\"no-workflows\"], \"workflows\");\n if (workflows === undefined) {\n if (!interactive) workflows = needsFlag(\"--workflows or --no-workflows\", \"--no-workflows\", false);\n else {\n const answer = await askForWorkflows();\n if (answer === null) return cancelled();\n workflows = answer;\n }\n }\n\n // Reached only when nothing else could say — `npm create` and `pnpm create`\n // both announce themselves in `npm_config_user_agent`. What is left is a\n // global install, where the old silent fallback to pnpm was a guess written\n // into every script in the generated project.\n let manager = requestedManager ?? detectPackageManager();\n if (manager === undefined) {\n if (!interactive) manager = needsFlag(\"--package-manager <npm|pnpm>\", \"--package-manager pnpm\", \"pnpm\" as PackageManager);\n else {\n const answer = await askForPackageManager();\n if (answer === null) return cancelled();\n manager = answer;\n }\n }\n\n if (missing.length > 0) throw new Error(unanswered(directory, missing));\n\n ui.success(`applicationId ${color.bold(applicationId(directory, org))}`);\n\n const result = scaffoldGame({\n directory,\n org,\n packageManager: manager,\n ci: workflows,\n git,\n firebase,\n reporter: clackReporter(),\n });\n\n const { status, next, footnotes, headline } = summarise(result, manager, workflows);\n for (const line of status) ui.success(line);\n ui.block(next);\n for (const line of footnotes) ui.info(line);\n ui.close(headline);\n}\n\ntry {\n await main(process.argv.slice(2));\n} catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n ui.stop(`create-eigen-game: ${message}`);\n process.exitCode = 1;\n}\n","/**\n * Every question this CLI asks, in one module so the wording lives together\n * and can be asserted — `cli.ts` is the bin entry and runs `main` on import,\n * which makes anything defined there untestable.\n *\n * The organization prompt is the reason this drops to `@clack/core` rather\n * than using `@clack/prompts`' `text`. The suffix: `flutter create --org X\n * --project-name Y` derives `X.Y`, so the organization is a *prefix*, and the\n * reliable way to say so is to render the rest of the identifier, dimmed,\n * while it is being typed. `text` has no hook for that; a custom `TextPrompt`\n * render returns a string, so it does.\n *\n * Everything else comes from `@clack/prompts` rather than being drawn here.\n * Its symbols are exported for exactly this, and they are not constants: each\n * falls back to ASCII on a terminal that cannot show the box-drawing\n * characters, which a hand-typed `│` cannot do.\n */\nimport type { Readable, Writable } from \"node:stream\";\nimport { TextPrompt } from \"@clack/core\";\nimport { confirm, isCancel, S_BAR, S_BAR_END, select, symbol } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport type { PackageManager } from \"./index.js\";\n\n/**\n * Where a question reads its answer from. Parameters for the same reason every\n * clack prompt takes them: a prompt is otherwise the one part of a CLI that\n * cannot be driven by a test.\n */\nexport interface Io {\n input?: Readable;\n output?: Writable;\n}\n\n/**\n * Two or more dot-separated Java identifiers. `flutter create --org` accepts\n * anything and defers the complaint to Gradle, which reports it as a manifest\n * error in a generated file — so `com.example-games` costs a full scaffold and\n * a first build before anyone learns that a hyphen is not legal in a package\n * segment.\n */\nexport const ORG = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$/;\n\n/** The default, which an empty answer accepts. */\nexport const DEFAULT_ORG = \"com.example\";\n\nconst bar = color.gray(S_BAR);\n\n/** A `com.acme.chess` answer to a game called `chess`, which yields `com.acme.chess.chess`. */\nexport function repeatsGameName(org: string, game: string): boolean {\n return org.split(\".\").at(-1) === game;\n}\n\n/** The organization implied by an answer that already ends in the game name. */\nexport function withoutGameName(org: string): string | undefined {\n const shorter = org.split(\".\").slice(0, -1).join(\".\");\n return ORG.test(shorter) ? shorter : undefined;\n}\n\n/**\n * Asks, with the game name rendered dimmed after the cursor so the shape of\n * the answer is visible while it is being given.\n *\n * Returns the organization, `undefined` for the default, or `null` when\n * cancelled.\n *\n * `input` and `output` default to this process's, and are parameters for the\n * same reason every clack prompt takes them: a prompt is otherwise the one\n * part of a CLI that cannot be driven by a test.\n */\nexport async function askForOrg(game: string, registering: boolean, io: Io = {}): Promise<string | undefined | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n const suffix = color.dim(`.${game}`);\n const prompt = new TextPrompt({\n input,\n output,\n // An empty answer is the default, not a mistake.\n validate: (value) => (value === undefined || value === \"\" || ORG.test(value) ? undefined : \"Two or more dot-separated segments, as in com.example or dev.yourname.games.\"),\n render() {\n const typed = String(this.value ?? \"\");\n const shown = typed === \"\" ? `${color.dim(DEFAULT_ORG)}${suffix}` : `${this.userInputWithCursor}${suffix}`;\n const problem = this.state === \"error\" ? `\\n${bar} ${color.yellow(this.error ?? \"\")}` : \"\";\n return [`${bar}`, `${symbol(this.state)} Organization in reverse domain notation`, `${bar} ${shown}${problem}`, color.gray(S_BAR_END)].join(\"\\n\");\n },\n });\n\n // The two things that make this answer permanent, said before it is given\n // rather than after: Google Play freezes the identifier at the first upload,\n // and FlutterFire matches an existing Android app on exactly this string, so\n // it also decides whether the run adopts an app the chosen Firebase project\n // already has or registers a new one.\n output.write(`${bar} ${color.dim(\"Prefixes the Android applicationId, which Google Play makes permanent at first upload.\")}\\n`);\n if (registering) output.write(`${bar} ${color.dim(\"Also the Android app registered in the Firebase project you pick next.\")}\\n`);\n\n const answer = await prompt.prompt();\n if (isCancel(answer)) return null;\n\n const org = String(answer).trim();\n if (org === \"\" || org === DEFAULT_ORG) return undefined;\n\n // The mistake the dimmed suffix is there to prevent, caught for the person\n // who pasted an identifier rather than typing one and never watched it grow.\n const shorter = repeatsGameName(org, game) ? withoutGameName(org) : undefined;\n if (shorter !== undefined) {\n const shorten = await confirm({\n message: `That gives ${color.yellow(`${org}.${game}`)} — the game name twice. Use ${color.green(shorter)} instead?`,\n initialValue: true,\n input,\n output,\n });\n if (isCancel(shorten)) return null;\n if (shorten) return shorter;\n }\n\n return org;\n}\n\n/**\n * A yes/no question, with its reason on the line above.\n *\n * The three below share this so cancellation is handled in one place: ^C\n * during any question has to mean the run stops having written nothing, and\n * that is only true if every call site checks. `null` is that answer.\n *\n * `hint` is written rather than passed to `confirm`, which has nowhere to put\n * it. It is the same dimmed line the organization prompt uses, and carries the\n * thing that makes the default the default — a question whose answer is\n * pre-chosen owes the reader why.\n */\nasync function ask(message: string, initialValue: boolean, hint: string, io: Io): Promise<boolean | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n output.write(`${bar}\\n${bar} ${color.dim(hint)}\\n`);\n const answer = await confirm({ message, initialValue, input, output });\n return isCancel(answer) ? null : answer;\n}\n\n/**\n * Asked only when {@link firebaseReadiness} found something missing, and\n * defaulted to \"no\" because the honest answer usually is: the tools are two\n * commands away, and the alternative is an app that throws at launch until\n * they are installed anyway.\n */\nexport function askToScaffoldWithoutFirebase(io: Io = {}): Promise<boolean | null> {\n return ask(\"Scaffold anyway, and connect Firebase yourself later?\", false, \"Everything except the app's sign-in still works, and `firebase:configure` finishes the job.\", io);\n}\n\n/** Defaulted to yes: the scaffold is a few hundred generated files, and a commit is what makes the first `git diff` the first game change rather than all of them. */\nexport function askForGit(io: Io = {}): Promise<boolean | null> {\n return ask(\"Initialise a git repository and commit the scaffold?\", true, \"Commits once, so your first diff is your first game change.\", io);\n}\n\n/**\n * Defaulted to no, and the hint says why rather than leaving it as an\n * arbitrary-looking choice: `release.yml` needs an upload keystore and a Play\n * service account, so a project that takes the workflows before it has either\n * gets a red `main` from its first push.\n */\nexport function askForWorkflows(io: Io = {}): Promise<boolean | null> {\n return ask(\"Add the GitHub Actions workflows?\", false, \"Release needs a signing keystore and a Play service account, so this fails until you have both. `add workflows` writes them later.\", io);\n}\n\n/**\n * The package manager, asked only when nothing else could say.\n *\n * `npm create` and `pnpm create` both set `npm_config_user_agent`, so this is\n * reached by the paths that do not: a global install, or running the binary\n * directly. Rare, but it decides what every generated script says, and the old\n * silent fallback to pnpm was a guess printed into a project's package.json.\n */\nexport async function askForPackageManager(io: Io = {}): Promise<PackageManager | null> {\n const answer = await select({\n message: \"Which package manager should the generated scripts use?\",\n options: [\n { value: \"pnpm\", label: \"pnpm\" },\n { value: \"npm\", label: \"npm\" },\n ],\n input: io.input ?? process.stdin,\n output: io.output ?? process.stdout,\n });\n return isCancel(answer) ? null : (answer as PackageManager);\n}\n","/**\n * The closing summary, and the only output of a scaffold anyone reads.\n *\n * A run drives several tools with plenty to say, so where the project went,\n * what to edit, and what to run are worth restating at the bottom. Returned in\n * pieces rather than as one block, because the CLI renders each in a different\n * shape: the outcomes as status lines, the commands as a boxed note, the\n * destination as the closing line.\n *\n * Its own module so it can be asserted: `cli.ts` is the bin entry and runs\n * `main` on import, which makes anything defined there untestable. The package\n * `exports` map does not expose this, so it is internal despite being\n * importable by the tests.\n */\nimport type { PackageManager, ScaffoldResult } from \"./index.js\";\n\nexport interface Summary {\n /** What became of the repository and of Firebase, when there is something to say. */\n status: string[];\n /** The commands worth having, as one block. */\n next: string;\n /** One-line asides, after the block. */\n footnotes: string[];\n /** The closing line. */\n headline: string;\n}\n\n/** Two-column rows, aligned to the widest left cell. */\nfunction columns(rows: [string, string][]): string[] {\n const width = Math.max(...rows.map(([left]) => left.length));\n return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);\n}\n\nexport function summarise(result: ScaffoldResult, manager: PackageManager, workflows: boolean): Summary {\n const run = manager === \"npm\" ? \"npm run\" : \"pnpm\";\n const status: string[] = [];\n\n // Only the outcomes that leave the reader something to know. `failed` has\n // already warned, in more detail than a summary line could carry.\n if (result.git === \"committed\") status.push(`Committed as \"Scaffold ${result.name}\"`);\n if (result.git === \"existing\") status.push(\"Created inside a repository you already had, so nothing was committed\");\n // `failed` has warned already, and the step reappears below as the next\n // thing to run, which is the whole of the advice.\n if (result.firebase === \"configured\") status.push(result.git === \"committed\" ? \"Firebase is configured, and everything it generated is in that commit\" : \"Firebase is configured\");\n\n const next = [\n \"Write the game\",\n ...columns([\n [\"server/src/module/v1.ts\", \"the authoritative rules\"],\n [\"app/lib/game/v1/rules.dart\", \"the presentation twin\"],\n ]),\n \"\",\n `Then, from ${result.name}/`,\n ...columns([\n [`cd server && ${run} test:watch`, \"rules and twin fixtures\"],\n [`cd server && ${run} dev`, \"the Worker, on http://localhost:8787\"],\n [`${run} contract`, \"regenerate after a schema change\"],\n // Named here because it is the one step the game itself cannot do\n // without: rules, fixtures and the Worker all run unconfigured, and the\n // app throws `Firebase is not configured` at launch until this has run\n // once.\n ...(result.firebase === \"configured\" ? [] : [[`${run} firebase:configure`, \"connect Firebase before the app runs\"] as [string, string]]),\n ]),\n ].join(\"\\n\");\n\n const footnotes = [\"Docs: https://eigeninteractive.com\"];\n if (!workflows) footnotes.unshift(\"No CI workflows yet: `create-eigen-game add workflows` writes them when shipping is the next step.\");\n\n return { status, next, footnotes, headline: `${result.name} is ready, at ${result.root}` };\n}\n"],"mappings":"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","names":["color","color","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/prompt.ts","../src/summary.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { cancel, intro, isCI, isTTY, log, note, outro, taskLog } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport { addContinuousIntegration, applicationId, destinationProblem, detectPackageManager, type FirebaseProblem, firebaseReadiness, insideWorkTree, normaliseTerminalWidth, type PackageManager, plainReporter, type Reporter, scaffoldGame } from \"./index.js\";\nimport { askForGit, askForOrg, askForPackageManager, askForWorkflows, askToScaffoldWithoutFirebase, DEFAULT_ORG, ORG } from \"./prompt.js\";\nimport { summarise } from \"./summary.js\";\n\nconst version = (JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), \"../package.json\"), \"utf8\")) as { version: string }).version;\n\nconst PREREQUISITES = \"https://eigeninteractive.com/docs/getting-started/prerequisites\";\n\nconst help = `Usage: create-eigen-game <game-slug> [options]\n create-eigen-game add workflows [directory] [options]\n\nCreates one repository containing an EigenInteractive Cloudflare Worker and\nFlutter app, and commits it. The destination basename is the lowercase\nkebab-case game slug; the CLI derives the display name, Dart package name, and\nDart/TypeScript type prefix from it.\n\nEvery option below answers a question the CLI would otherwise ask. Pass it to\nskip the question; leave it out to be asked. With no terminal to ask on (CI, a\npipe, an agent session) an unanswered question is an error rather than a\ndefault quietly chosen for you, and the message prints the command to re-run.\n\nOptions:\n --org <reverse-domain> Android/iOS organization. The applicationId is this\n plus the game name, as in com.example.my_game, and\n Google Play makes it permanent at first upload\n --firebase-project <id>\n The Firebase project to configure against. Also the\n form that works with no terminal, since otherwise\n FlutterFire asks\n --no-firebase Do not configure Firebase, leaving firebase:configure\n as a step for later\n --git, --no-git Initialise a repository and commit the scaffold\n --workflows, --no-workflows\n Emit the GitHub Actions workflows. release.yml needs\n an upload keystore and a Play service account, so it\n fails on every push until both exist\n --package-manager <pm> npm or pnpm. Taken from the invoking package manager\n when there is one, so this is for global installs\n -h, --help Show this help\n -v, --version Show the version\n\nCommands:\n add workflows [directory]\n Add the workflows to an existing project, for when\n you are ready to ship rather than at scaffold time.\n Defaults to the current directory.`;\n\nnormaliseTerminalWidth(process.stdout);\n\n/**\n * Whether to draw, using clack's own predicates rather than a hand-rolled\n * `isTTY`, the same pair `taskLog` consults internally. A pipe has nowhere to\n * put a redraw, and a CI log is read after the fact, in full, by someone who\n * wants every line a tool printed.\n */\nconst decorated = isTTY(process.stdout) && !isCI();\n\n/**\n * The CLI's voice, in whichever of the two registers this terminal supports.\n *\n * Every message goes through here so the undecorated path cannot drift into\n * being the untested one.\n */\nconst ui = decorated\n ? {\n open: () => intro(color.inverse(\" EigenInteractive \")),\n info: (message: string) => log.info(message),\n warn: (message: string) => log.warn(message),\n success: (message: string) => log.success(message),\n block: (body: string) => note(body, \"Next\"),\n close: (message: string) => outro(color.bold(message)),\n stop: (message: string) => cancel(message),\n }\n : {\n open: () => console.log(\"\\nEigenInteractive\\n\"),\n info: (message: string) => console.log(message),\n warn: (message: string) => console.warn(message),\n success: (message: string) => console.log(message),\n block: (body: string) => console.log(`\\n${body}\\n`),\n close: (message: string) => console.log(message),\n stop: (message: string) => console.error(message),\n };\n\nfunction resolveManager(requested: string | undefined): PackageManager | undefined {\n if (requested === undefined) return undefined;\n if (requested !== \"npm\" && requested !== \"pnpm\") {\n throw new Error(\"--package-manager must be npm or pnpm\");\n }\n return requested;\n}\n\n/**\n * Why a scaffold asks about a second service at all, said once, before the\n * first question rather than in the error that follows it.\n *\n * Firebase is not an integration this engine offers; it is where a player\n * comes from. Somebody meeting that for the first time deserves the reason in\n * the same breath as the requirement, and deserves to know the size of it:\n * one free project, shared by every game they build.\n */\nfunction greet(): void {\n ui.open();\n ui.info(\"A game is one repository with two halves: a Cloudflare Worker that owns the rules,\\nand a Flutter app for Android and the web. This writes both, wired together and\\nalready playable.\");\n ui.info(\n `Firebase signs your players in. The Worker verifies Firebase ID tokens to decide who\\nholds a seat, and sends turn notifications through the same project, so a scaffolded\\napp throws ${color.dim(\"Firebase is not configured\")} at launch until one is connected.\\n\\nIt is free to start, one project serves every game you build, and this connects yours\\nas it goes. ${color.dim(PREREQUISITES)}`,\n );\n}\n\n/** What is missing, and the one command that fixes each: the same list the warning and the parting message are both built from. */\nfunction firebaseReport(problems: FirebaseProblem[]): string {\n const lines = problems.flatMap((problem) => [` ${problem.reason}`, ` ${color.cyan(problem.fix)}`]);\n // `dart pub global activate` does not put the binary on PATH, which is the\n // next thing to go wrong for anyone who has just run the command above.\n if (problems.some((problem) => problem.fix.startsWith(\"dart pub\"))) {\n lines.push(` ${color.dim('…then, if your shell cannot find it: export PATH=\"$PATH\":\"$HOME/.pub-cache/bin\"')}`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * The parting message when someone answers \"no\" to scaffolding without\n * Firebase: every command they need, in the order they run in.\n *\n * Deliberately not a failure of the scaffold: nothing has been written, they\n * asked for that, and the exit code is non-zero only so a script that wrapped\n * this can tell there is no project.\n */\nfunction stopForFirebase(problems: FirebaseProblem[]): void {\n // The commands only. What each one is for is in the warning directly above,\n // and a `note` draws a box sized to its longest line, so repeating the\n // reasons here is what makes it wrap into something unreadable at 80\n // columns, which is exactly the width this is most likely to be read at.\n note(`${problems.map((problem) => ` ${color.cyan(problem.fix)}`).join(\"\\n\")}\\n\\nThen run this again.\\n${color.dim(PREREQUISITES)}`, \"Set up Firebase\");\n ui.stop(\"Nothing was written.\");\n process.exitCode = 1;\n}\n\n/**\n * The error for a run with no terminal and an unanswered question.\n *\n * The strictness is the point: a default applied where nobody can see it is\n * how `com.example.my_game` reaches Google Play, which freezes it at the first\n * upload. What keeps that from being merely annoying is this message: the\n * whole command, with every default already filled in, so the fix is one\n * paste and the one value worth changing is visible in it.\n */\nfunction unanswered(directory: string, missing: { flags: string; suggestion: string }[]): string {\n const command = [\"npx create-eigen-game\", directory, ...missing.map((question) => question.suggestion)].join(\" \");\n return [`No terminal to ask on, so every answer has to come from a flag. These were not given:`, \"\", ...missing.map((question) => ` ${question.flags}`), \"\", \"That command, with each default filled in. Read them before you run it:\", \"\", ` ${command}`].join(\"\\n\");\n}\n\n/** `--foo` and `--no-foo`, which `parseArgs` has no notion of: `undefined` means nobody said, which is what makes it a question. */\nfunction flagPair(yes: boolean | undefined, no: boolean | undefined, name: string): boolean | undefined {\n if (yes === true && no === true) throw new Error(`--${name} and --no-${name} contradict each other`);\n if (yes === true) return true;\n if (no === true) return false;\n return undefined;\n}\n\n/** `--org \"\"` falls through to the default rather than failing, matching what an empty answer to the prompt does. */\nfunction resolveOrg(value: string): string | undefined {\n if (value.trim() === \"\") return undefined;\n if (!ORG.test(value)) {\n throw new Error(`invalid organization: ${value}\\n\\nUse reverse domain notation: two or more dot-separated segments of letters, digits and underscores, each starting with a letter, as in ${DEFAULT_ORG} or dev.yourname.games.`);\n }\n return value;\n}\n\n/**\n * Shows each step running, and keeps its output only when it fails.\n *\n * Every tool a scaffold drives has plenty to say (pub resolving 179\n * dependencies, two icon generators, a package manager) and none of it is\n * read while it scrolls past. `taskLog` is exactly that bargain: the stream is\n * visible while the step runs, cleared when it succeeds, and left on screen\n * when it does not, so a failure is still debuggable from what is in the\n * terminal.\n *\n * `handOver` is the exception, and the reason `interactive` exists at all.\n * FlutterFire asks which Firebase project to use, so that step needs the\n * terminal rather than a captured pipe.\n */\nfunction clackReporter(): Reporter {\n // `taskLog` redraws by clearing lines, which is an escape-sequence storm in\n // anything that is not a terminal: a CI log, a pipe, `scaffold-e2e.mjs`.\n // There, the honest thing is the plain reporter: every tool's output, in\n // full, which is what a log gets read for anyway.\n if (!decorated) return plainReporter;\n\n let task: ReturnType<typeof taskLog> | undefined;\n let interactive = false;\n\n return {\n step(label, body) {\n task = taskLog({ title: label });\n try {\n const value = body();\n task.success(label);\n return value;\n } catch (error) {\n task.error(label);\n throw error;\n } finally {\n task = undefined;\n }\n },\n handOver(label, body) {\n log.step(`${label} ${color.dim(\"(FlutterFire takes over here)\")}`);\n interactive = true;\n try {\n return body();\n } finally {\n interactive = false;\n }\n },\n emit(output) {\n const text = output.trimEnd();\n if (text !== \"\") task?.message(text);\n },\n warn(message) {\n ui.warn(message);\n },\n get interactive() {\n return interactive;\n },\n };\n}\n\nasync function main(args: string[]): Promise<void> {\n const { values, positionals } = parseArgs({\n args,\n allowPositionals: true,\n strict: true,\n options: {\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n org: { type: \"string\" },\n \"firebase-project\": { type: \"string\" },\n // Each negation is declared under the name it is typed with, because\n // `parseArgs` has no boolean negation of its own. `flagPair` puts the\n // two halves back together, and the third state, neither given, is the\n // one that matters: it is what turns the flag into a question.\n \"no-firebase\": { type: \"boolean\" },\n git: { type: \"boolean\" },\n \"no-git\": { type: \"boolean\" },\n workflows: { type: \"boolean\" },\n \"no-workflows\": { type: \"boolean\" },\n \"package-manager\": { type: \"string\" },\n },\n });\n\n if (values.help) {\n console.log(help);\n return;\n }\n if (values.version) {\n console.log(version);\n return;\n }\n\n const requestedManager = resolveManager(values[\"package-manager\"]);\n\n if (positionals[0] === \"add\") {\n // `ci` is the name this shipped under, and it is written into the README\n // of every project already scaffolded. Those READMEs are on disk and are\n // not going to be edited, so the old spelling keeps working; only the new\n // one is documented.\n if (positionals[1] !== \"workflows\" && positionals[1] !== \"ci\") {\n throw new Error(`unknown subcommand: add ${positionals[1] ?? \"\"}\\n\\n${help}`.trim());\n }\n if (positionals.length > 3) {\n throw new Error(`expected at most one directory\\n\\n${help}`);\n }\n const result = addContinuousIntegration({\n directory: positionals[2] ?? process.cwd(),\n packageManager: requestedManager,\n });\n console.log(`Added ${result.files.join(\" and \")} in ${result.root}`);\n return;\n }\n\n if (positionals.length !== 1) {\n throw new Error(`expected exactly one destination directory\\n\\n${help}`);\n }\n\n const directory = positionals[0];\n\n // Everything that makes the command impossible, before the greeting rather\n // than after the last question. `applicationId` validates the slug;\n // `destinationProblem` is the one `scaffoldGame` would raise at the far end,\n // asked here instead. Being told the directory was occupied *after* giving\n // an organization is the same insult as being told about `flutterfire` after\n // two minutes of Flutter and pub.\n const game = basename(resolve(directory));\n applicationId(directory);\n const occupied = destinationProblem(directory);\n if (occupied !== undefined) throw new Error(occupied);\n\n greet();\n\n // Every question this run will ask, settled before any of it starts. A flag\n // is an answer already given; anything left over is asked, or, with nothing\n // to ask on, collected here and reported together, so one re-run can carry\n // all of them.\n const interactive = process.stdin.isTTY === true;\n const missing: { flags: string; suggestion: string }[] = [];\n const cancelled = () => {\n ui.stop(\"Nothing was written.\");\n process.exitCode = 130;\n };\n\n /**\n * Records a question that could not be asked, and hands back the default it\n * suggested. The value is never used, since `missing` throws before anything\n * is written, but returning it keeps each answer definitely typed instead of\n * carrying a `| undefined` all the way to the scaffold call.\n */\n const needsFlag = <T>(flags: string, suggestion: string, fallback: T): T => {\n missing.push({ flags, suggestion });\n return fallback;\n };\n\n // Firebase goes first because it is the only answer that can end the run,\n // and ending it after the organization prompt would mean asking for a\n // permanent decision about a project that never gets created.\n const project = values[\"firebase-project\"]?.trim();\n if (project === \"\") throw new Error(\"--firebase-project needs a project id\");\n if (project !== undefined && values[\"no-firebase\"] === true) {\n throw new Error(\"--firebase-project and --no-firebase contradict each other\");\n }\n\n let firebase: boolean | string = false;\n if (project !== undefined) {\n firebase = project;\n } else if (values[\"no-firebase\"] !== true) {\n const readiness = firebaseReadiness();\n if (readiness.ready) {\n // Nothing for this CLI to ask: FlutterFire runs its own project picker\n // during the scaffold, and needs a terminal to do it.\n if (interactive) firebase = true;\n else firebase = needsFlag(\"--firebase-project <id>, or --no-firebase\", \"--no-firebase\", false);\n } else {\n ui.warn(`Firebase is not set up on this machine:\\n\\n${firebaseReport(readiness.problems)}`);\n if (!interactive) {\n firebase = needsFlag(\"--no-firebase, to scaffold without it\", \"--no-firebase\", false);\n } else {\n const carryOn = await askToScaffoldWithoutFirebase();\n if (carryOn === null) return cancelled();\n if (!carryOn) return stopForFirebase(readiness.problems);\n }\n }\n }\n\n let org: string | undefined;\n if (values.org !== undefined) {\n org = resolveOrg(values.org);\n } else if (interactive) {\n const answer = await askForOrg(game.replaceAll(\"-\", \"_\"), firebase !== false);\n if (answer === null) return cancelled();\n org = answer;\n } else {\n org = needsFlag(\"--org <reverse-domain>\", `--org ${DEFAULT_ORG}`, undefined);\n }\n\n // A destination inside a repository someone already has is not a question:\n // `scaffoldGame` declines to nest one repository in another whatever it is\n // told, and says so in the summary.\n let git = flagPair(values.git, values[\"no-git\"], \"git\");\n if (git === undefined && insideWorkTree(directory)) git = true;\n if (git === undefined) {\n if (!interactive) git = needsFlag(\"--git or --no-git\", \"--git\", true);\n else {\n const answer = await askForGit();\n if (answer === null) return cancelled();\n git = answer;\n }\n }\n\n let workflows = flagPair(values.workflows, values[\"no-workflows\"], \"workflows\");\n if (workflows === undefined) {\n if (!interactive) workflows = needsFlag(\"--workflows or --no-workflows\", \"--no-workflows\", false);\n else {\n const answer = await askForWorkflows();\n if (answer === null) return cancelled();\n workflows = answer;\n }\n }\n\n // Reached only when nothing else could say. `npm create` and `pnpm create`\n // both announce themselves in `npm_config_user_agent`. What is left is a\n // global install, where the old silent fallback to pnpm was a guess written\n // into every script in the generated project.\n let manager = requestedManager ?? detectPackageManager();\n if (manager === undefined) {\n if (!interactive) manager = needsFlag(\"--package-manager <npm|pnpm>\", \"--package-manager pnpm\", \"pnpm\" as PackageManager);\n else {\n const answer = await askForPackageManager();\n if (answer === null) return cancelled();\n manager = answer;\n }\n }\n\n if (missing.length > 0) throw new Error(unanswered(directory, missing));\n\n ui.success(`applicationId ${color.bold(applicationId(directory, org))}`);\n\n const result = scaffoldGame({\n directory,\n org,\n packageManager: manager,\n ci: workflows,\n git,\n firebase,\n reporter: clackReporter(),\n });\n\n const { status, next, footnotes, headline } = summarise(result, manager, workflows);\n for (const line of status) ui.success(line);\n ui.block(next);\n for (const line of footnotes) ui.info(line);\n ui.close(headline);\n}\n\ntry {\n await main(process.argv.slice(2));\n} catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n ui.stop(`create-eigen-game: ${message}`);\n process.exitCode = 1;\n}\n","/**\n * Every question this CLI asks, in one module so the wording lives together\n * and can be asserted. `cli.ts` is the bin entry and runs `main` on import,\n * which makes anything defined there untestable.\n *\n * The organization prompt is the reason this drops to `@clack/core` rather\n * than using `@clack/prompts`' `text`. The suffix: `flutter create --org X\n * --project-name Y` derives `X.Y`, so the organization is a *prefix*, and the\n * reliable way to say so is to render the rest of the identifier, dimmed,\n * while it is being typed. `text` has no hook for that; a custom `TextPrompt`\n * render returns a string, so it does.\n *\n * Everything else comes from `@clack/prompts` rather than being drawn here.\n * Its symbols are exported for exactly this, and they are not constants: each\n * falls back to ASCII on a terminal that cannot show the box-drawing\n * characters, which a hand-typed `│` cannot do.\n */\nimport type { Readable, Writable } from \"node:stream\";\nimport { TextPrompt } from \"@clack/core\";\nimport { confirm, isCancel, S_BAR, S_BAR_END, select, symbol } from \"@clack/prompts\";\nimport color from \"picocolors\";\nimport type { PackageManager } from \"./index.js\";\n\n/**\n * Where a question reads its answer from. Parameters for the same reason every\n * clack prompt takes them: a prompt is otherwise the one part of a CLI that\n * cannot be driven by a test.\n */\nexport interface Io {\n input?: Readable;\n output?: Writable;\n}\n\n/**\n * Two or more dot-separated Java identifiers. `flutter create --org` accepts\n * anything and defers the complaint to Gradle, which reports it as a manifest\n * error in a generated file, so `com.example-games` costs a full scaffold and\n * a first build before anyone learns that a hyphen is not legal in a package\n * segment.\n */\nexport const ORG = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$/;\n\n/** The default, which an empty answer accepts. */\nexport const DEFAULT_ORG = \"com.example\";\n\nconst bar = color.gray(S_BAR);\n\n/** A `com.acme.chess` answer to a game called `chess`, which yields `com.acme.chess.chess`. */\nexport function repeatsGameName(org: string, game: string): boolean {\n return org.split(\".\").at(-1) === game;\n}\n\n/** The organization implied by an answer that already ends in the game name. */\nexport function withoutGameName(org: string): string | undefined {\n const shorter = org.split(\".\").slice(0, -1).join(\".\");\n return ORG.test(shorter) ? shorter : undefined;\n}\n\n/**\n * Asks, with the game name rendered dimmed after the cursor so the shape of\n * the answer is visible while it is being given.\n *\n * Returns the organization, `undefined` for the default, or `null` when\n * cancelled.\n *\n * `input` and `output` default to this process's, and are parameters for the\n * same reason every clack prompt takes them: a prompt is otherwise the one\n * part of a CLI that cannot be driven by a test.\n */\nexport async function askForOrg(game: string, registering: boolean, io: Io = {}): Promise<string | undefined | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n const suffix = color.dim(`.${game}`);\n const prompt = new TextPrompt({\n input,\n output,\n // An empty answer is the default, not a mistake.\n validate: (value) => (value === undefined || value === \"\" || ORG.test(value) ? undefined : \"Two or more dot-separated segments, as in com.example or dev.yourname.games.\"),\n render() {\n const typed = String(this.value ?? \"\");\n const shown = typed === \"\" ? `${color.dim(DEFAULT_ORG)}${suffix}` : `${this.userInputWithCursor}${suffix}`;\n const problem = this.state === \"error\" ? `\\n${bar} ${color.yellow(this.error ?? \"\")}` : \"\";\n return [`${bar}`, `${symbol(this.state)} Organization in reverse domain notation`, `${bar} ${shown}${problem}`, color.gray(S_BAR_END)].join(\"\\n\");\n },\n });\n\n // The two things that make this answer permanent, said before it is given\n // rather than after: Google Play freezes the identifier at the first upload,\n // and FlutterFire matches an existing Android app on exactly this string, so\n // it also decides whether the run adopts an app the chosen Firebase project\n // already has or registers a new one.\n output.write(`${bar} ${color.dim(\"Prefixes the Android applicationId, which Google Play makes permanent at first upload.\")}\\n`);\n if (registering) output.write(`${bar} ${color.dim(\"Also the Android app registered in the Firebase project you pick next.\")}\\n`);\n\n const answer = await prompt.prompt();\n if (isCancel(answer)) return null;\n\n const org = String(answer).trim();\n if (org === \"\" || org === DEFAULT_ORG) return undefined;\n\n // The mistake the dimmed suffix is there to prevent, caught for the person\n // who pasted an identifier rather than typing one and never watched it grow.\n const shorter = repeatsGameName(org, game) ? withoutGameName(org) : undefined;\n if (shorter !== undefined) {\n const shorten = await confirm({\n message: `That gives ${color.yellow(`${org}.${game}`)}, the game name twice. Use ${color.green(shorter)} instead?`,\n initialValue: true,\n input,\n output,\n });\n if (isCancel(shorten)) return null;\n if (shorten) return shorter;\n }\n\n return org;\n}\n\n/**\n * A yes/no question, with its reason on the line above.\n *\n * The three below share this so cancellation is handled in one place: ^C\n * during any question has to mean the run stops having written nothing, and\n * that is only true if every call site checks. `null` is that answer.\n *\n * `hint` is written rather than passed to `confirm`, which has nowhere to put\n * it. It is the same dimmed line the organization prompt uses, and carries the\n * thing that makes the default the default: a question whose answer is\n * pre-chosen owes the reader why.\n */\nasync function ask(message: string, initialValue: boolean, hint: string, io: Io): Promise<boolean | null> {\n const input = io.input ?? process.stdin;\n const output = io.output ?? process.stdout;\n output.write(`${bar}\\n${bar} ${color.dim(hint)}\\n`);\n const answer = await confirm({ message, initialValue, input, output });\n return isCancel(answer) ? null : answer;\n}\n\n/**\n * Asked only when {@link firebaseReadiness} found something missing, and\n * defaulted to \"no\" because the honest answer usually is: the tools are two\n * commands away, and the alternative is an app that throws at launch until\n * they are installed anyway.\n */\nexport function askToScaffoldWithoutFirebase(io: Io = {}): Promise<boolean | null> {\n return ask(\"Scaffold anyway, and connect Firebase yourself later?\", false, \"Everything except the app's sign-in still works, and `firebase:configure` finishes the job.\", io);\n}\n\n/** Defaulted to yes: the scaffold is a few hundred generated files, and a commit is what makes the first `git diff` the first game change rather than all of them. */\nexport function askForGit(io: Io = {}): Promise<boolean | null> {\n return ask(\"Initialise a git repository and commit the scaffold?\", true, \"Commits once, so your first diff is your first game change.\", io);\n}\n\n/**\n * Defaulted to no, and the hint says why rather than leaving it as an\n * arbitrary-looking choice: `release.yml` needs an upload keystore and a Play\n * service account, so a project that takes the workflows before it has either\n * gets a red `main` from its first push.\n */\nexport function askForWorkflows(io: Io = {}): Promise<boolean | null> {\n return ask(\"Add the GitHub Actions workflows?\", false, \"Release needs a signing keystore and a Play service account, so this fails until you have both. `add workflows` writes them later.\", io);\n}\n\n/**\n * The package manager, asked only when nothing else could say.\n *\n * `npm create` and `pnpm create` both set `npm_config_user_agent`, so this is\n * reached by the paths that do not: a global install, or running the binary\n * directly. Rare, but it decides what every generated script says, and the old\n * silent fallback to pnpm was a guess printed into a project's package.json.\n */\nexport async function askForPackageManager(io: Io = {}): Promise<PackageManager | null> {\n const answer = await select({\n message: \"Which package manager should the generated scripts use?\",\n options: [\n { value: \"pnpm\", label: \"pnpm\" },\n { value: \"npm\", label: \"npm\" },\n ],\n input: io.input ?? process.stdin,\n output: io.output ?? process.stdout,\n });\n return isCancel(answer) ? null : (answer as PackageManager);\n}\n","/**\n * The closing summary, and the only output of a scaffold anyone reads.\n *\n * A run drives several tools with plenty to say, so where the project went,\n * what to edit, and what to run are worth restating at the bottom. Returned in\n * pieces rather than as one block, because the CLI renders each in a different\n * shape: the outcomes as status lines, the commands as a boxed note, the\n * destination as the closing line.\n *\n * Its own module so it can be asserted: `cli.ts` is the bin entry and runs\n * `main` on import, which makes anything defined there untestable. The package\n * `exports` map does not expose this, so it is internal despite being\n * importable by the tests.\n */\nimport type { PackageManager, ScaffoldResult } from \"./index.js\";\n\nexport interface Summary {\n /** What became of the repository and of Firebase, when there is something to say. */\n status: string[];\n /** The commands worth having, as one block. */\n next: string;\n /** One-line asides, after the block. */\n footnotes: string[];\n /** The closing line. */\n headline: string;\n}\n\n/** Two-column rows, aligned to the widest left cell. */\nfunction columns(rows: [string, string][]): string[] {\n const width = Math.max(...rows.map(([left]) => left.length));\n return rows.map(([left, right]) => ` ${left.padEnd(width)} ${right}`);\n}\n\nexport function summarise(result: ScaffoldResult, manager: PackageManager, workflows: boolean): Summary {\n const run = manager === \"npm\" ? \"npm run\" : \"pnpm\";\n const status: string[] = [];\n\n // Only the outcomes that leave the reader something to know. `failed` has\n // already warned, in more detail than a summary line could carry.\n if (result.git === \"committed\") status.push(`Committed as \"Scaffold ${result.name}\"`);\n if (result.git === \"existing\") status.push(\"Created inside a repository you already had, so nothing was committed\");\n // `failed` has warned already, and the step reappears below as the next\n // thing to run, which is the whole of the advice.\n if (result.firebase === \"configured\") status.push(result.git === \"committed\" ? \"Firebase is configured, and everything it generated is in that commit\" : \"Firebase is configured\");\n if (result.link?.projectId != null) status.push(`FIREBASE_PROJECT_ID is set to ${result.link.projectId}, and app-config.json points at the local Worker`);\n\n // What is left, and only what is left. Everything here comes from the\n // Firebase console rather than from any CLI, which is why a scaffold that\n // filled in everything it could still ends with a list: the web target\n // refuses to start without a VAPID key, and there is no OAuth client to copy\n // until somebody enables the provider that creates it.\n const remaining: [string, string][] = [];\n if (result.link !== undefined) {\n if (result.link.googleWebClientId === null) {\n remaining.push([\"Enable Google sign-in\", \"Console > Authentication > Sign-in method\"]);\n remaining.push([\"GOOGLE_WEB_CLIENT_ID\", \"that same page, under Web SDK configuration\"]);\n }\n remaining.push([\"FIREBASE_VAPID_KEY\", \"Console > Project settings > Cloud Messaging > Web configuration\"]);\n }\n\n const next = [\n \"Write the game\",\n ...columns([\n [\"server/src/module/v1.ts\", \"the authoritative rules\"],\n [\"app/lib/game/v1/rules.dart\", \"the presentation twin\"],\n ]),\n \"\",\n `Then, from ${result.name}/`,\n ...columns([\n [`cd server && ${run} test:watch`, \"rules and twin fixtures\"],\n [`cd server && ${run} dev`, \"the Worker, on http://localhost:8787\"],\n [`${run} contract`, \"regenerate after a schema change\"],\n // Named here because it is the one step the game itself cannot do\n // without: rules, fixtures and the Worker all run unconfigured, and the\n // app throws `Firebase is not configured` at launch until this has run\n // once.\n ...(result.firebase === \"configured\" ? [] : [[`${run} firebase:configure`, \"connect Firebase before the app runs\"] as [string, string]]),\n ]),\n ...(remaining.length === 0 ? [] : [\"\", \"Before the app runs, from the Firebase console\", ...columns(remaining), \"\", \" The values go in app/app-config.json. Both are public, not secrets.\"]),\n ].join(\"\\n\");\n\n const footnotes = [\"Docs: https://eigeninteractive.com\"];\n if (!workflows) footnotes.unshift(\"No CI workflows yet: `create-eigen-game add workflows` writes them when shipping is the next step.\");\n\n return { status, next, footnotes, headline: `${result.name} is ready, at ${result.root}` };\n}\n"],"mappings":"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","names":["color","color","result"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** What the Firebase step managed to settle, and what it left for the console. */
|
|
2
|
+
interface FirebaseLink {
|
|
3
|
+
/** The project now in `wrangler.jsonc`, or null when FlutterFire recorded none. */
|
|
4
|
+
projectId: string | null;
|
|
5
|
+
/** The OAuth web client now in `app-config.json`, or null when Google sign-in is not enabled yet. */
|
|
6
|
+
googleWebClientId: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
type PackageManager = "npm" | "pnpm";
|
|
2
10
|
type Runner = (command: string, args: string[], cwd: string) => void;
|
|
3
11
|
/**
|
|
@@ -27,7 +35,7 @@ interface Reporter {
|
|
|
27
35
|
/**
|
|
28
36
|
* Gives a terminal that reports no width the width clack would have assumed.
|
|
29
37
|
*
|
|
30
|
-
* A pty can report itself as a TTY and set `columns` to 0
|
|
38
|
+
* A pty can report itself as a TTY and set `columns` to 0. `script` does, and
|
|
31
39
|
* so do some container terminals. clack falls back to 80 for a stream with no
|
|
32
40
|
* `columns` at all, but `0` is a number and satisfies that check, so the width
|
|
33
41
|
* it draws its rules from goes negative and a scaffold dies partway through
|
|
@@ -46,7 +54,7 @@ declare const plainReporter: Reporter;
|
|
|
46
54
|
*
|
|
47
55
|
* `interactive` inherits, which is the only way FlutterFire can put a question
|
|
48
56
|
* on screen and read the answer. Otherwise both streams are captured and
|
|
49
|
-
* handed to the reporter
|
|
57
|
+
* handed to the reporter: on success so it can discard them, and on failure
|
|
50
58
|
* so the reason survives.
|
|
51
59
|
*/
|
|
52
60
|
declare function capturingRunner(reporter: Reporter): Runner;
|
|
@@ -61,7 +69,7 @@ interface ScaffoldOptions {
|
|
|
61
69
|
*/
|
|
62
70
|
bootstrap?: boolean;
|
|
63
71
|
/**
|
|
64
|
-
* Emit the GitHub Actions workflows. Off by default
|
|
72
|
+
* Emit the GitHub Actions workflows. Off by default; see the call site for
|
|
65
73
|
* why, and `addContinuousIntegration` for adding them to an existing
|
|
66
74
|
* project later.
|
|
67
75
|
*/
|
|
@@ -109,9 +117,15 @@ interface ScaffoldResult {
|
|
|
109
117
|
* step or spell it out. `failed` has already warned.
|
|
110
118
|
*/
|
|
111
119
|
firebase: "configured" | "failed" | "skipped";
|
|
120
|
+
/**
|
|
121
|
+
* What the Firebase step made knowable and this scaffold therefore filled
|
|
122
|
+
* in. Absent unless `firebase` is `configured`, since there is nothing to
|
|
123
|
+
* read otherwise.
|
|
124
|
+
*/
|
|
125
|
+
link?: FirebaseLink;
|
|
112
126
|
}
|
|
113
127
|
/**
|
|
114
|
-
* The Android `applicationId
|
|
128
|
+
* The Android `applicationId`, and iOS bundle id, a scaffold at `directory`
|
|
115
129
|
* will produce, so the CLI can show it rather than describe it.
|
|
116
130
|
*
|
|
117
131
|
* `flutter create --org X --project-name Y` derives `X.Y`. The organization is
|
|
@@ -164,8 +178,8 @@ type FirebaseReadiness = {
|
|
|
164
178
|
* `flutterfire` is missing costs nothing here and costs two minutes of Flutter
|
|
165
179
|
* and pub at the far end, which is where the step actually runs.
|
|
166
180
|
*
|
|
167
|
-
* Every problem, not the first one. The checks are independent
|
|
168
|
-
* two ecosystems, plus a sign-in
|
|
181
|
+
* Every problem, not the first one. The checks are independent (two CLIs from
|
|
182
|
+
* two ecosystems, plus a sign-in) so short-circuiting turns one setup into a
|
|
169
183
|
* sequence of runs that each reveal the next missing piece. Reported in the
|
|
170
184
|
* order they have to be fixed in: the CLI, then the sign-in it stores, then
|
|
171
185
|
* the bridge that consumes both.
|
|
@@ -180,21 +194,21 @@ declare function firebaseReadiness(probe?: Probe): FirebaseReadiness;
|
|
|
180
194
|
* Whether the scaffold would land inside a repository that already exists.
|
|
181
195
|
*
|
|
182
196
|
* Asked of the nearest ancestor that exists, because the destination does not
|
|
183
|
-
* yet. The point is not to change what {@link scaffoldGame} does
|
|
184
|
-
* declines to nest a repository
|
|
197
|
+
* yet. The point is not to change what {@link scaffoldGame} does, which already
|
|
198
|
+
* declines to nest a repository, but to stop the CLI asking a question whose
|
|
185
199
|
* answer cannot matter.
|
|
186
200
|
*/
|
|
187
201
|
/**
|
|
188
202
|
* Why this destination cannot be scaffolded into, said as a whole message, or
|
|
189
203
|
* `undefined` when there is nothing in the way.
|
|
190
204
|
*
|
|
191
|
-
* A directory that exists and is *empty* is not in the way
|
|
192
|
-
* out of habit
|
|
205
|
+
* A directory that exists and is *empty* is not in the way, since people make
|
|
206
|
+
* one out of habit, so it is removed and rewritten rather than refused over
|
|
193
207
|
* nothing. Anything with contents in it is refused, and a cloned repository is
|
|
194
208
|
* not an exception: `.git` is in the way like any other entry, and the message
|
|
195
209
|
* names it. Publishing into one would mean parking `.git`, renaming staging
|
|
196
210
|
* over the top and putting it back, with a failure in the middle able to
|
|
197
|
-
* orphan a remote
|
|
211
|
+
* orphan a remote, which is a lot of care to spend on a flow that has a
|
|
198
212
|
* one-line alternative.
|
|
199
213
|
*
|
|
200
214
|
* Refused, and deliberately not offered as a "remove it and continue?"
|
|
@@ -205,11 +219,11 @@ declare function firebaseReadiness(probe?: Probe): FirebaseReadiness;
|
|
|
205
219
|
* question this CLI asks is reversible. This one would not be.
|
|
206
220
|
*
|
|
207
221
|
* Exported so the CLI can ask it before the first question rather than after
|
|
208
|
-
* the last one
|
|
222
|
+
* the last one. {@link scaffoldGame} asks it again, for callers that are not
|
|
209
223
|
* the CLI.
|
|
210
224
|
*/
|
|
211
225
|
declare function destinationProblem(directory: string): string | undefined;
|
|
212
226
|
declare function insideWorkTree(directory: string, probe?: Probe): boolean;
|
|
213
227
|
declare function scaffoldGame(options: ScaffoldOptions): ScaffoldResult;
|
|
214
228
|
|
|
215
|
-
export { type AddContinuousIntegrationOptions, type FirebaseProblem, type FirebaseReadiness, type GitOutcome, type PackageManager, type Probe, type Reporter, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, applicationId, capturingRunner, decodeUtf8, destinationProblem, detectPackageManager, firebaseReadiness, insideWorkTree, normaliseTerminalWidth, plainReporter, scaffoldGame };
|
|
229
|
+
export { type AddContinuousIntegrationOptions, type FirebaseLink, type FirebaseProblem, type FirebaseReadiness, type GitOutcome, type PackageManager, type Probe, type Reporter, type Runner, type ScaffoldOptions, type ScaffoldResult, addContinuousIntegration, applicationId, capturingRunner, decodeUtf8, destinationProblem, detectPackageManager, firebaseReadiness, insideWorkTree, normaliseTerminalWidth, plainReporter, scaffoldGame };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-eigen-game",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Scaffold an EigenInteractive game Worker and Flutter app without cloning the engine repositories.",
|
|
6
6
|
"type": "module",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"vitest": "^4.1.10",
|
|
30
30
|
"wrangler": "^4.110.0",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@eigeninteractive/rules": "0.2.
|
|
33
|
-
"@eigeninteractive/server": "0.2.
|
|
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,16 +47,28 @@ 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
|
|
54
54
|
|
|
55
55
|
The initial scaffold already generates Dart payloads from the Worker contract.
|
|
56
56
|
Implement the client-side legality, preview and presentation rules under
|
|
57
|
-
`app/lib/game
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
`app/lib/game/`.
|
|
58
|
+
|
|
59
|
+
`app/app-config.json` holds the public build-time values. They are Dart
|
|
60
|
+
compilation environment declarations, not secrets; private credentials belong
|
|
61
|
+
only in the Worker. Scaffolding fills in `API_BASE_URL` for local development
|
|
62
|
+
and, once Firebase is configured, `GOOGLE_WEB_CLIENT_ID`. Two are left:
|
|
63
|
+
|
|
64
|
+
- `FIREBASE_VAPID_KEY`, from Firebase Console > Project settings > Cloud
|
|
65
|
+
Messaging > Web configuration. The web app will not start without it.
|
|
66
|
+
- `APP_HOST`, your hostname at deploy time, which turns on invite and replay
|
|
67
|
+
sharing.
|
|
68
|
+
|
|
69
|
+
An empty `GOOGLE_WEB_CLIENT_ID` means the Google sign-in provider was not
|
|
70
|
+
enabled when the project was configured. Turn it on under Authentication >
|
|
71
|
+
Sign-in method, then copy the Web SDK configuration client id.
|
|
60
72
|
|
|
61
73
|
See `app/lib/game/README.md` for the regeneration command.
|
|
62
74
|
|
|
@@ -87,7 +99,7 @@ Configure Android, Flutter Web, and the messaging service worker together:
|
|
|
87
99
|
```
|
|
88
100
|
|
|
89
101
|
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
|
|
102
|
+
is already configured and this is the command for changing it afterwards: a
|
|
91
103
|
different project, or picking up a configuration change. If it was skipped, the
|
|
92
104
|
app throws `Firebase is not configured` the moment it launches; rules, fixtures
|
|
93
105
|
and `wrangler dev` all run regardless.
|
|
@@ -96,12 +108,16 @@ It drives two CLIs, which are separate global installs and neither of which
|
|
|
96
108
|
comes with Flutter or Node:
|
|
97
109
|
|
|
98
110
|
```sh
|
|
99
|
-
|
|
111
|
+
curl -sL https://firebase.tools | bash # the `firebase` CLI
|
|
100
112
|
dart pub global activate flutterfire_cli # the `flutterfire` CLI
|
|
101
113
|
export PATH="$PATH":"$HOME/.pub-cache/bin" # `activate` does not do this
|
|
102
114
|
firebase login # both CLIs share this
|
|
103
115
|
```
|
|
104
116
|
|
|
117
|
+
The first line is Google's own installer; [the Firebase CLI
|
|
118
|
+
reference](https://firebase.google.com/docs/cli) has the other ways, including
|
|
119
|
+
Windows.
|
|
120
|
+
|
|
105
121
|
A run with no project chosen yet asks which to use, and can create one. Name it
|
|
106
122
|
up front to skip the prompts, which is also how CI runs this:
|
|
107
123
|
|
|
@@ -113,8 +129,8 @@ Later runs reuse the project recorded in `app/firebase.json`, so there is
|
|
|
113
129
|
nothing to pass again. Add `--help` for the rest.
|
|
114
130
|
|
|
115
131
|
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
|
|
132
|
+
`app/lib/firebase_options.dart` and `app/web/firebase-config.js` (the last two
|
|
133
|
+
replacing scaffolded placeholders that throw) and lets FlutterFire add the
|
|
118
134
|
Google Services plugin to the two Android Gradle files. **Commit all of it.**
|
|
119
135
|
Every one is a public app identifier rather than a credential, nothing here is
|
|
120
136
|
git-ignored, and an Android or web build fails without them. The Firebase
|
|
@@ -130,7 +146,7 @@ new".
|
|
|
130
146
|
|
|
131
147
|
The command uses FlutterFire's selected Web app to generate both
|
|
132
148
|
`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
|
|
149
|
+
identifiers by hand. Put the public VAPID key in `app-config.json` separately;
|
|
134
150
|
Firebase's app SDK configuration does not include that Web Push certificate.
|
|
135
151
|
`eigen_flutter` bundles the pinned Cropper.js assets and loads them only when
|
|
136
152
|
avatar editing starts, so the app needs no Cropper.js setup or runtime CDN. The
|
|
@@ -163,8 +179,8 @@ with `wrangler secret put`; never commit the downloaded JSON or `.dev.vars`.
|
|
|
163
179
|
## Store release
|
|
164
180
|
|
|
165
181
|
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
|
-
|
|
182
|
+
upload keystore and a Play service account that a new project does not have,
|
|
183
|
+
so it would fail on every push until both exist. Add it when you are ready to
|
|
168
184
|
ship, from the project root:
|
|
169
185
|
|
|
170
186
|
```sh
|
|
@@ -178,7 +194,7 @@ thing up front.)
|
|
|
178
194
|
`release.yml` builds a signed, obfuscated Android App
|
|
179
195
|
Bundle on every push to `main` and uploads it to the Play Store internal
|
|
180
196
|
track via `app/fastlane`. `checks.yml` is the PR gate it
|
|
181
|
-
calls first
|
|
197
|
+
calls first, and it needs no secrets, since `app-config.json`,
|
|
182
198
|
`firebase_options.dart` and `google-services.json` are public app
|
|
183
199
|
identifiers, not credentials. Commit them for real once
|
|
184
200
|
`{{PACKAGE_MANAGER}} run firebase:configure` has been run, rather than
|
|
@@ -197,15 +213,15 @@ required:
|
|
|
197
213
|
|
|
198
214
|
Per-app one-time setup: create an upload keystore, add the four signing
|
|
199
215
|
secrets above, create the Google Play service account and add its JSON, and
|
|
200
|
-
do the **first** Play Console upload by hand to create the listing
|
|
201
|
-
|
|
216
|
+
do the **first** Play Console upload by hand to create the listing.
|
|
217
|
+
Everything after that flows through `fastlane android internal` /
|
|
202
218
|
`fastlane android production`. The Fastlane lanes upload the binary only
|
|
203
219
|
(`skip_upload_metadata`/`images`/`screenshots`), so the store listing stays
|
|
204
220
|
hand-maintained in the Console.
|
|
205
221
|
|
|
206
222
|
### Replace the placeholder branding
|
|
207
223
|
|
|
208
|
-
A fresh scaffold is already branded
|
|
224
|
+
A fresh scaffold is already branded, with the **EigenInteractive mark**, not
|
|
209
225
|
this game's. The launcher icon, splash and web icons were generated at scaffold
|
|
210
226
|
time so the
|
|
211
227
|
app looks finished from the first run, but shipping them to a store would put
|
|
@@ -220,11 +236,11 @@ the engine's mark on your listing. Replace them before a first release:
|
|
|
220
236
|
The notification icon is the exception: `eigen_flutter` ships a default
|
|
221
237
|
`ic_notification` drawable, so nothing is missing and nothing breaks if you
|
|
222
238
|
leave it. To use your own, add
|
|
223
|
-
`android/app/src/main/res/drawable/ic_notification.xml
|
|
239
|
+
`android/app/src/main/res/drawable/ic_notification.xml`, a monochrome
|
|
224
240
|
`<vector>`, not an SVG. Android resource merging gives your app precedence
|
|
225
241
|
over the package, so there is nothing to delete or override explicitly.
|
|
226
242
|
|
|
227
|
-
Also update the palette in `pubspec.yaml
|
|
243
|
+
Also update the palette in `pubspec.yaml`. `adaptive_icon_background` and the
|
|
228
244
|
`flutter_native_splash` colours are the EigenInteractive ink/paper pair, which
|
|
229
245
|
will not suit different artwork. Then regenerate:
|
|
230
246
|
|
|
@@ -245,5 +261,5 @@ cider bump patch # 1.0.0 -> 1.0.1
|
|
|
245
261
|
```
|
|
246
262
|
|
|
247
263
|
The build number is set from `github.run_number` in CI and is not managed by
|
|
248
|
-
hand. Worker deployment is unaffected by any of this
|
|
264
|
+
hand. Worker deployment is unaffected by any of this; it stays the manual
|
|
249
265
|
`{{PACKAGE_MANAGER}} run deploy` described above.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"contract": "cd server && {{PACKAGE_MANAGER}} run contract && cd ../app && dart run eigen_flutter:generate_payloads --contract ../server/game-contract.json --output lib/game/generated/payloads.dart --fixtures-output test/fixtures",
|
|
8
8
|
"contract:check": "cd server && {{PACKAGE_MANAGER}} run contract:check && cd ../app && dart run eigen_flutter:generate_payloads --contract ../server/game-contract.json --output lib/game/generated/payloads.dart --fixtures-output test/fixtures --check",
|
|
9
9
|
"deploy": "{{PACKAGE_MANAGER}} run build:web && cd server && {{PACKAGE_MANAGER}} run deploy",
|
|
10
|
-
"firebase:configure": "cd app && dart run eigen_flutter:configure_firebase",
|
|
10
|
+
"firebase:configure": "cd app && dart run eigen_flutter:configure_firebase --worker ../server",
|
|
11
11
|
"format": "biome check --write .",
|
|
12
12
|
"lint": "biome check ."
|
|
13
13
|
},
|
|
@@ -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: {
|