Repository: JoshuaKGoldberg/create-typescript-app (ts)

Issue:
🐛 Bug: Migrating an existing package.json shouldn't remove "bin/" entry from "files"
### Bug Report Checklist

- [X] I have tried restarting my IDE and the issue persists.
- [X] I have pulled the latest `main` branch of the repository.
- [X] I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue.

### Expected

Running `npx create-typescript-app --mode migrate` (with any base) on an existing repository shouldn't wipe out `files` entries if they correspond to files that should exist in the package.


### Actual

Running `npx create-typescript-app --mode migrate` (with any base) on an existing repository right now overrides the repository's `files` entry (among others). But if the repository has file(s) under `bin/`, that means the new `files` entry won't include them anymore. Not good.

```diff
	"files": [
-		"bin/",
		"lib/",
		"package.json",
		"LICENSE.md",
		"README.md"
	],
```

### Additional Info

_No response_
🛠 Tooling: Use --auto in end-to-end migration test
### Bug Report Checklist

- [X] I have tried restarting my IDE and the issue persists.
- [X] I have pulled the latest `main` branch of the repository.
- [X] I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue.

### Overview

I'd added an `--auto` mode in #1046. It'd be nice to have it be used in the end-to-end migration task. That way the test won't need to manually declare values in the CLI

This wouldn't IMO change how thorough the test is: instead of testing each of those options' ability to be read from CLI values, it'd instead test that they can be inferred by `--auto`.

### Additional Info

I suppose a second end-to-end test could be added - so that one tests `--auto` while the other tests manual values? I don't think that'd be worth the overhead though. At least unless and until the performance work in #860 makes the end-to-end tests _very_ fast.
🐛 Bug: --auto doesn't copy all defaults
### Bug Report Checklist

- [X] I have tried restarting my IDE and the issue persists.
- [X] I have pulled the latest `main` branch of the repository.
- [X] I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue.

### Expected

Running with `--auto --mode migrate` should infer all inferable options from the existing repository.

### Actual

Per https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1157#issuecomment-2282338266, I'm seeing quite a few missing:

* `.github/CODE_OF_CONDUCT.md`: Under `## Enforcement`, the email was switched from `github@joshuakgoldberg.com` to `git@joshuakgoldberg.com`
* `.github/DEVELOPMENT.md`: The initial _`> If you'd like a more guided walkthrough, ...`_ is missing
* `.github/SECURITY.md`: The email was switched from `github@joshuakgoldberg.com` to `git@joshuakgoldberg.com`
* `.vscode/launch.json`: The _Debug Program_ task was removed
* `package.json`:
  * The name was switched from `"JoshuaKGoldberg"` to `"Josh Goldberg"`
  * The `"bin"` entry was missing, as well as `"bin/index.js"` under `"files"`

### Additional Info

I'm fixing these in #1612.

💖 

Pull request:
fix: correct remaining --auto mismatches
## PR Checklist

- [x] Addresses an existing open issue: fixes #934; fixes #1157; fixes #1613
- [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md) were taken

## Overview

Fills in a lot of logic around defaults that was previously not hooked up. `--auto` now completely works as expected in this repository.

💖 

Patch:
diff --git a/package.json b/package.json
index bfc92a9..20f1be4 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
 	},
 	"license": "MIT",
 	"author": {
-		"name": "Josh Goldberg",
+		"name": "Josh Goldberg ✨",
 		"email": "npm@joshuakgoldberg.com"
 	},
 	"type": "module",
diff --git a/src/shared/options/createOptionDefaults/index.ts b/src/shared/options/createOptionDefaults/index.ts
index 4d72a74..3e51dae 100644
--- a/src/shared/options/createOptionDefaults/index.ts
+++ b/src/shared/options/createOptionDefaults/index.ts
@@ -12,6 +12,7 @@ import { PromptedOptions } from "../../types.js";
 import { parsePackageAuthor } from "./parsePackageAuthor.js";
 import { readDefaultsFromDevelopment } from "./readDefaultsFromDevelopment.js";
 import { readDefaultsFromReadme } from "./readDefaultsFromReadme.js";
+import { readGitHubEmail } from "./readGitHubEmail.js";
 
 export function createOptionDefaults(promptedOptions?: PromptedOptions) {
 	const gitDefaults = tryCatchLazyValueAsync(async () =>
@@ -29,19 +30,25 @@ export function createOptionDefaults(promptedOptions?: PromptedOptions) {
 	);
 
 	return {
-		author: async () => (await packageAuthor()).author ?? npmDefaults.name,
+		author: async () =>
+			(await packageAuthor()).author ?? (await npmDefaults())?.name,
 		bin: async () => (await packageData()).bin,
 		description: async () => (await packageData()).description,
 		email: async () => {
-			const gitEmail = await tryCatchAsync(
-				async () => (await $`git config --get user.email`).stdout,
-			);
+			const githubEmail =
+				(await readGitHubEmail()) ??
+				(await tryCatchAsync(
+					async () => (await $`git config --get user.email`).stdout,
+				));
 			const npmEmail =
 				(await npmDefaults())?.email ?? (await packageAuthor()).email;
 
 			/* eslint-disable @typescript-eslint/no-non-null-assertion */
-			return gitEmail || npmEmail
-				? { github: (gitEmail || npmEmail)!, npm: (npmEmail || gitEmail)! }
+			return githubEmail || npmEmail
+				? {
+						github: (githubEmail || npmEmail)!,
+						npm: (npmEmail || githubEmail)!,
+					}
 				: undefined;
 			/* eslint-enable @typescript-eslint/no-non-null-assertion */
 		},
diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.ts b/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.ts
index f3c2403..b0ebe26 100644
--- a/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.ts
+++ b/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.ts
@@ -3,18 +3,20 @@ import lazyValue from "lazy-value";
 import { readFileSafe } from "../../readFileSafe.js";
 
 export function readDefaultsFromDevelopment() {
-	const development = lazyValue(
-		async () => await readFileSafe(".github/DEVELOPMENT.md", ""),
-	);
+	return {
+		guide: lazyValue(async () => {
+			const tag = /> .*guided walkthrough, see \[((?!\[).+)\]\((.+)\)/i.exec(
+				await readFileSafe(".github/DEVELOPMENT.md", ""),
+			);
 
-	const guideTag = lazyValue(async () =>
-		/> .*guided walkthrough, see \[((?!\[).+)\]\((.+)\)/i.exec(
-			await development(),
-		),
-	);
+			if (!tag) {
+				return undefined;
+			}
 
-	return {
-		guide: async () => (await guideTag())?.[2],
-		guideTitle: async () => (await guideTag())?.[1],
+			return {
+				href: tag[2],
+				title: tag[1],
+			};
+		}),
 	};
 }
diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
index 1a89c5d..68afdf5 100644
--- a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
+++ b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
@@ -6,18 +6,32 @@ export function readDefaultsFromReadme() {
 	const readme = lazyValue(async () => await readFileSafe("README.md", ""));
 
 	const imageTag = lazyValue(
-		async () => /<img.+src.+\/>/.exec(await readme())?.[0],
+		async () => /\n<img.+src=.+>/.exec(await readme())?.[0],
 	);
 
 	return {
-		logo: async () =>
-			(await imageTag())
-				?.match(/src\s*=(.+)?\/>/)?.[1]
-				?.replaceAll(/^['"]|['"]$/g, ""),
+		logo: async () => {
+			const tag = await imageTag();
+
+			if (!tag) {
+				return undefined;
+			}
+
+			const src = /src\s*=(.+)['"/]>/
+				.exec(tag)?.[1]
+				?.replaceAll(/^['"]|['"]$/g, "");
+
+			if (!src) {
+				return undefined;
+			}
+
+			return {
+				alt: /alt=['"](.+)['"]\s*src=/.exec(tag)?.[1] ?? "Project logo",
+				src,
+			};
+		},
 		title: async () =>
-			/^(?:# |<h1\s+align="center">)(.*?)(?:<\/h1>)?$/i
-				.exec(await readme())?.[1]
-				?.trim()
-				.replace(/<[^>]+(?:>|$)/g, ""),
+			(/^<h1\s+align="center">(.+)<\/h1>/.exec(await readme()) ??
+				/^# (.+)/.exec(await readme()))?.[1],
 	};
 }
diff --git a/src/shared/options/createOptionDefaults/readGitHubEmail.ts b/src/shared/options/createOptionDefaults/readGitHubEmail.ts
new file mode 100644
index 0000000..ea52bea
--- /dev/null
+++ b/src/shared/options/createOptionDefaults/readGitHubEmail.ts
@@ -0,0 +1,12 @@
+import { readFileSafe } from "../../readFileSafe.js";
+
+export async function readGitHubEmail() {
+	// The create-typescript-app template puts the GitHub email in the CoC.
+	// If they seem to be using the template, we can go with that.
+	const codeOfConduct = await readFileSafe(".github/CODE_OF_CONDUCT.md", "");
+	if (!codeOfConduct.includes("Contributor Covenant Code of Conduct")) {
+		return undefined;
+	}
+
+	return /for enforcement at[\r\n]+(.+)\.[\r\n]+All/.exec(codeOfConduct)?.[1];
+}
diff --git a/src/shared/options/readOptions.ts b/src/shared/options/readOptions.ts
index 75aab67..4a49172 100644
--- a/src/shared/options/readOptions.ts
+++ b/src/shared/options/readOptions.ts
@@ -263,14 +263,16 @@ export async function readOptions(
 	const augmentedOptions = await augmentOptionsWithExcludes({
 		...options,
 		access: options.access ?? "public",
-		author: options.author ?? (await defaults.owner()),
+		author:
+			options.author ?? (await defaults.author()) ?? (await defaults.owner()),
+		bin: options.bin ?? (await defaults.bin()),
 		description: options.description,
 		directory:
 			options.directory ?? promptedOptions.directory ?? options.repository,
 		email,
 		funding: options.funding ?? (await defaults.funding()),
-		guide,
-		logo,
+		guide: guide ?? (await defaults.guide()),
+		logo: logo ?? (await defaults.logo()),
 		mode,
 		owner: options.owner,
 		repository,


Test patch:
diff --git a/script/migrate-test-e2e.ts b/script/migrate-test-e2e.ts
index 5468e14..ace0371 100644
--- a/script/migrate-test-e2e.ts
+++ b/script/migrate-test-e2e.ts
@@ -4,8 +4,6 @@ import * as fs from "node:fs/promises";
 import { rimraf } from "rimraf";
 import { assert, describe, expect, test } from "vitest";
 
-import packageData from "../package.json" assert { type: "json" };
-
 const filesExpectedToBeChanged = [
 	"README.md",
 	"knip.json",
@@ -21,21 +19,6 @@ const filesThatMightBeChanged = new Set([
 	"script/__snapshots__/migrate-test-e2e.ts.snap",
 ]);
 
-const {
-	author: { email: emailNpm, name: authorName },
-	description,
-	name: repository,
-} = packageData;
-const emailGithub = "github@joshuakgoldberg.com";
-const bin = "./bin/index.js";
-const guide =
-	"https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository";
-const guideTitle = "Contributing to a create-typescript-app Repository";
-const logo = "./docs/create-typescript-app.png";
-const logoAlt = `Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'`;
-const owner = "JoshuaKGoldberg";
-const title = "Create TypeScript App";
-
 await rimraf("coverage*");
 
 const originalReadme = (await fs.readFile("README.md")).toString();
@@ -46,7 +29,7 @@ const originalSnapshots = (
 
 await $({
 	stdio: "inherit",
-})`c8 -o ./coverage -r html -r lcov --src src node ${bin} --base everything --author ${authorName} --mode migrate --bin ${bin} --description ${description} --email-github ${emailGithub} --email-npm ${emailNpm} --guide ${guide} --guide-title ${guideTitle} --logo ${logo} --logo-alt ${logoAlt} --owner ${owner} --title ${title} --repository ${repository} --skip-all-contributors-api --skip-github-api --skip-install`;
+})`c8 -o ./coverage -r html -r lcov --src src node ./bin/index.js --auto --mode migrate --skip-all-contributors-api --skip-github-api --skip-install`;
 
 // All Contributors seems to not be using Prettier to format files...
 await fs.writeFile(
diff --git a/src/shared/options/createOptionDefaults/index.test.ts b/src/shared/options/createOptionDefaults/index.test.ts
index 6876c25..519a477 100644
--- a/src/shared/options/createOptionDefaults/index.test.ts
+++ b/src/shared/options/createOptionDefaults/index.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it, vi } from "vitest";
+import { beforeEach, describe, expect, it, vi } from "vitest";
 
 import { createOptionDefaults } from "./index.js";
 
@@ -34,6 +34,14 @@ vi.mock("../../packages.js", () => ({
 	},
 }));
 
+const mockReadGitHubEmail = vi.fn();
+
+vi.mock("./readGitHubEmail.js", () => ({
+	get readGitHubEmail() {
+		return mockReadGitHubEmail;
+	},
+}));
+
 describe("createOptionDefaults", () => {
 	describe("bin", () => {
 		it("returns undefined when package data does not have a bin", async () => {
@@ -56,24 +64,29 @@ describe("createOptionDefaults", () => {
 	});
 
 	describe("email", () => {
+		beforeEach(() => {
+			mockNpmUser.mockImplementation((username: string) => ({
+				email: `npm-${username}@test.com`,
+			}));
+		});
+
 		it("returns the npm whoami email from npm when only an npm exists", async () => {
 			mock$.mockImplementation(([command]: string[]) =>
-				command === "npm whoami" ? { stdout: "npm-username" } : undefined,
+				command === "npm whoami" ? { stdout: "username" } : undefined,
 			);
-			mockNpmUser.mockImplementation((username: string) => ({
-				email: `test@${username}.com`,
-			}));
+			mockReadGitHubEmail.mockResolvedValueOnce(undefined);
 
 			const actual = await createOptionDefaults().email();
 
 			expect(actual).toEqual({
-				github: "test@npm-username.com",
-				npm: "test@npm-username.com",
+				github: "npm-username@test.com",
+				npm: "npm-username@test.com",
 			});
 		});
 
 		it("returns the npm whoami email from npm when only a package author email exists", async () => {
 			mock$.mockResolvedValue({ stdout: "" });
+			mockReadGitHubEmail.mockResolvedValueOnce(undefined);
 			mockReadPackageData.mockResolvedValue({
 				author: {
 					email: "test@package.com",
@@ -88,41 +101,75 @@ describe("createOptionDefaults", () => {
 			});
 		});
 
+		it("returns the github email when only a github email exists", async () => {
+			mock$.mockResolvedValue({ stdout: "" });
+			mockReadPackageData.mockResolvedValueOnce({});
+			mockReadGitHubEmail.mockResolvedValueOnce("github@test.com");
+
+			const actual = await createOptionDefaults().email();
+
+			expect(actual).toEqual({
+				github: "github@test.com",
+				npm: "github@test.com",
+			});
+		});
+
 		it("returns the git user email when only a git user email exists", async () => {
 			mock$.mockImplementation(([command]: string[]) =>
 				command === "git config --get user.email"
-					? { stdout: "test@git.com" }
+					? { stdout: "git@test.com" }
 					: undefined,
 			);
+			mockReadGitHubEmail.mockResolvedValueOnce(undefined);
+			mockReadPackageData.mockResolvedValue({});
+
+			const actual = await createOptionDefaults().email();
+
+			expect(actual).toEqual({
+				github: "git@test.com",
+				npm: "git@test.com",
+			});
+		});
+
+		it("returns both the git user email and the npm user email when only those two exist", async () => {
+			mock$.mockImplementation(([command]: string[]) => ({
+				stdout:
+					command === "git config --get user.email"
+						? "git@test.com"
+						: "username",
+			}));
+			mockReadGitHubEmail.mockResolvedValueOnce(undefined);
 			mockReadPackageData.mockResolvedValue({});
 
 			const actual = await createOptionDefaults().email();
 
 			expect(actual).toEqual({
-				github: "test@git.com",
-				npm: "test@git.com",
+				github: "git@test.com",
+				npm: "npm-username@test.com",
 			});
 		});
 
-		it("returns both the git user email and the npm user email when both exist", async () => {
+		it("returns all three emails when they all exist", async () => {
 			mock$.mockImplementation(([command]: string[]) => ({
 				stdout:
 					command === "git config --get user.email"
-						? "test@git.com"
-						: "npm-username",
+						? "git@test.com"
+						: "username",
 			}));
+			mockReadGitHubEmail.mockResolvedValueOnce("github@test.com");
 			mockReadPackageData.mockResolvedValue({});
 
 			const actual = await createOptionDefaults().email();
 
 			expect(actual).toEqual({
-				github: "test@git.com",
-				npm: "test@npm-username.com",
+				github: "github@test.com",
+				npm: "npm-username@test.com",
 			});
 		});
 
-		it("returns undefined when neither git nor npm emails exist", async () => {
+		it("returns undefined when none of the emails exist", async () => {
 			mock$.mockResolvedValue({ stdout: "" });
+			mockReadGitHubEmail.mockResolvedValueOnce(undefined);
 			mockReadPackageData.mockResolvedValue({});
 
 			const actual = await createOptionDefaults().email();
diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.test.ts b/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.test.ts
index e585f31..1bb51c5 100644
--- a/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.test.ts
+++ b/src/shared/options/createOptionDefaults/readDefaultsFromDevelopment.test.ts
@@ -20,7 +20,7 @@ describe("readDefaultsFromDevelopment", () => {
 			expect(guide).toBeUndefined();
 		});
 
-		it("reads guide when it exists", async () => {
+		it("reads the href and title when the tag exists", async () => {
 			mockReadFileSafe.mockResolvedValue(`# Development
 
 > If you'd like a more guided walkthrough, see [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository).
@@ -29,33 +29,10 @@ describe("readDefaultsFromDevelopment", () => {
 
 			const guide = await readDefaultsFromDevelopment().guide();
 
-			expect(guide).toBe(
-				"https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository",
-			);
-		});
-	});
-
-	describe("guideTitle", () => {
-		it("defaults to undefined when .github/DEVELOPMENT.md cannot be found", async () => {
-			mockReadFileSafe.mockResolvedValue("");
-
-			const guideTitle = await readDefaultsFromDevelopment().guideTitle();
-
-			expect(guideTitle).toBeUndefined();
-		});
-
-		it("reads guideTitle when it exists", async () => {
-			mockReadFileSafe.mockResolvedValue(`# Development
-
-> If you'd like a more guided walkthrough, see [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository).
-> It'll walk you through the common activities you'll need to contribute.
-`);
-
-			const guideTitle = await readDefaultsFromDevelopment().guideTitle();
-
-			expect(guideTitle).toBe(
-				"Contributing to a create-typescript-app Repository",
-			);
+			expect(guide).toEqual({
+				href: "https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository",
+				title: "Contributing to a create-typescript-app Repository",
+			});
 		});
 	});
 });
diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
index ae708bc..f733e17 100644
--- a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
+++ b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
@@ -13,7 +13,9 @@ vi.mock("../../readFileSafe.js", () => ({
 describe("readDefaultsFromReadme", () => {
 	describe("logo", () => {
 		it("defaults to undefined when it cannot be found", async () => {
-			mockReadFileSafe.mockResolvedValue("nothing.");
+			mockReadFileSafe.mockResolvedValue(`
+nothing.
+`);
 
 			const logo = await readDefaultsFromReadme().logo();
 
@@ -21,33 +23,119 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it("parses when found in an unquoted string", async () => {
-			mockReadFileSafe.mockResolvedValue("<img src=abc/def.jpg/>");
+			mockReadFileSafe.mockResolvedValue(`
+<img src=abc/def.jpg/>
+`);
 
 			const logo = await readDefaultsFromReadme().logo();
 
-			expect(logo).toBe("abc/def.jpg");
+			expect(logo).toEqual({
+				alt: "Project logo",
+				src: "abc/def.jpg",
+			});
 		});
 
 		it("parses when found in a single quoted string", async () => {
-			mockReadFileSafe.mockResolvedValue("<img src='abc/def.jpg'/>");
+			mockReadFileSafe.mockResolvedValue(`
+<img src='abc/def.jpg'/>
+`);
 
 			const logo = await readDefaultsFromReadme().logo();
 
-			expect(logo).toBe("abc/def.jpg");
+			expect(logo).toEqual({
+				alt: "Project logo",
+				src: "abc/def.jpg",
+			});
 		});
 
 		it("parses when found in a double quoted string", async () => {
-			mockReadFileSafe.mockResolvedValue('<img src="abc/def.jpg"/>');
+			mockReadFileSafe.mockResolvedValue(`
+<img src="abc/def.jpg"/>
+`);
 
 			const logo = await readDefaultsFromReadme().logo();
 
-			expect(logo).toBe("abc/def.jpg");
+			expect(logo).toEqual({
+				alt: "Project logo",
+				src: "abc/def.jpg",
+			});
+		});
+
+		it("includes alt text when it exists in double quotes", async () => {
+			mockReadFileSafe.mockResolvedValue(`
+<img alt="Project logo: a fancy circle" src="abc/def.jpg"/>
+`);
+
+			const logo = await readDefaultsFromReadme().logo();
+
+			expect(logo).toEqual({
+				alt: "Project logo: a fancy circle",
+				src: "abc/def.jpg",
+			});
+		});
+
+		it("includes alt text when it exists in single quotes", async () => {
+			mockReadFileSafe.mockResolvedValue(`
+<img alt='Project logo: a fancy circle' src='abc/def.jpg'/>,
+`);
+
+			const logo = await readDefaultsFromReadme().logo();
+
+			expect(logo).toEqual({
+				alt: "Project logo: a fancy circle",
+				src: "abc/def.jpg",
+			});
+		});
+
+		it("parses when found after a badge image", async () => {
+			mockReadFileSafe.mockResolvedValue(`
+		<a href="#contributors" target="_blank"><img alt="👪 All Contributors: 48" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-48-21bb42.svg" /></a>
+<img src=abc/def.jpg/>
+`);
+
+			const logo = await readDefaultsFromReadme().logo();
+
+			expect(logo).toEqual({
+				alt: "Project logo",
+				src: "abc/def.jpg",
+			});
+		});
+
+		it("parses when found after an h1 and many badge images", async () => {
+			mockReadFileSafe.mockResolvedValue(`<h1 align="center">Create TypeScript App</h1>
+
+<p align="center">Quickstart-friendly TypeScript template with comprehensive, configurable, opinionated tooling. 💝</p>
+
+<p align="center">
+	<!-- prettier-ignore-start -->
+	<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
+	<a href="#contributors" target="_blank"><img alt="👪 All Contributors: 48" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-48-21bb42.svg" /></a>
+<!-- ALL-CONTRIBUTORS-BADGE:END -->
+	<!-- prettier-ignore-end -->
+	<a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="🤝 Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a>
+	<a href="https://codecov.io/gh/JoshuaKGoldberg/create-typescript-app" target="_blank"><img alt="🧪 Coverage" src="https://img.shields.io/codecov/c/github/JoshuaKGoldberg/create-typescript-app?label=%F0%9F%A7%AA%20coverage" /></a>
+	<a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/LICENSE.md" target="_blank"><img alt="📝 License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg"></a>
+	<a href="http://npmjs.com/package/create-typescript-app"><img alt="📦 npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a>
+	<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
+</p>
+
+<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" src="./docs/create-typescript-app.png">
+`);
+
+			const logo = await readDefaultsFromReadme().logo();
+
+			expect(logo).toEqual({
+				alt: "Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'",
+				src: "./docs/create-typescript-app.png",
+			});
 		});
 	});
 
 	describe("title", () => {
 		it("defaults to undefined when it cannot be found", async () => {
-			mockReadFileSafe.mockResolvedValue("nothing.");
+			mockReadFileSafe.mockResolvedValue(`
+nothing.
+`);
 
 			const title = await readDefaultsFromReadme().title();
 
@@ -55,7 +143,7 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it('reads title as markdown from "README.md" when it exists', async () => {
-			mockReadFileSafe.mockResolvedValue("# My Awesome Package");
+			mockReadFileSafe.mockResolvedValue(`# My Awesome Package`);
 
 			const title = await readDefaultsFromReadme().title();
 
@@ -73,7 +161,7 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it("returns undefined when title does not exist", async () => {
-			mockReadFileSafe.mockResolvedValue("");
+			mockReadFileSafe.mockResolvedValue(`Other text.`);
 
 			const title = await readDefaultsFromReadme().title();
 
diff --git a/src/shared/options/createOptionDefaults/readGitHubEmail.test.ts b/src/shared/options/createOptionDefaults/readGitHubEmail.test.ts
new file mode 100644
index 0000000..f472abe
--- /dev/null
+++ b/src/shared/options/createOptionDefaults/readGitHubEmail.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, it, vi } from "vitest";
+
+import { readGitHubEmail } from "./readGitHubEmail.js";
+
+const mockReadFileSafe = vi.fn();
+
+vi.mock("../../readFileSafe.js", () => ({
+	get readFileSafe() {
+		return mockReadFileSafe;
+	},
+}));
+
+describe("readGitHubEmail", () => {
+	it("returns undefined when it cannot be found", async () => {
+		mockReadFileSafe.mockResolvedValue("nothing.");
+
+		const email = await readGitHubEmail();
+
+		expect(email).toBeUndefined();
+	});
+
+	it("returns undefined when a different template", async () => {
+		mockReadFileSafe.mockResolvedValue(
+			`## Other Code of Conduct
+
+for enforcement at
+invalid@test.com.
+`,
+		);
+
+		const email = await readGitHubEmail();
+
+		expect(email).toBeUndefined();
+	});
+
+	it("returns the email when it matches the template", async () => {
+		const expected = `email@test.com`;
+
+		mockReadFileSafe.mockResolvedValue(
+			`## Contributor Covenant Code of Conduct
+
+reported to the community leaders responsible for enforcement at
+${expected}.
+All complaints will be reviewed and investigated promptly and fairly.
+`,
+		);
+
+		const email = await readGitHubEmail();
+
+		expect(email).toBe(expected);
+	});
+});
diff --git a/src/shared/options/readOptions.test.ts b/src/shared/options/readOptions.test.ts
index 2be4a60..8df87a8 100644
--- a/src/shared/options/readOptions.test.ts
+++ b/src/shared/options/readOptions.test.ts
@@ -112,9 +112,11 @@ vi.mock("./createOptionDefaults/index.js", () => ({
 	createOptionDefaults() {
 		return {
 			author: vi.fn(),
+			bin: vi.fn(),
 			description: vi.fn(),
 			email: vi.fn(),
 			funding: vi.fn(),
+			guide: vi.fn(),
 			logo: vi.fn(),
 			owner: vi.fn(),
 			repository: vi.fn(),