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

Issue:
🐛 Bug: All Contributors table not created in setup mode
### 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

When running `npx create-typescript-app@beta` on a new repository, a `## Contributors` README section with a contributors table should be created.

### Actual

It isn't right now because there are no existing detected contributors.

https://github.com/JoshuaKGoldberg/create-typescript-app/blob/cec96c0fcbc65a8391e1b4b3a9cc11c090733944/src/blocks/blockAllContributors.ts#L25-L27

### Additional Info

Previously solved in 1.x: https://github.com/JoshuaKGoldberg/create-typescript-app/issues/676

🎁 

Pull request:
fix: add README.md contributors section in setup
## PR Checklist

- [x] Addresses an existing open issue: fixes #1956
- [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

The previous `pnpx all-contributors-cli` command was only enough for transition mode to pick up on existing contributors and create a README.md section. For setup mode, there needed to be a default in options if `.all-contributorsrc` doesn't yet exist. That's now pulled from the GitHub API.

🎁 

Patch:
diff --git a/src/base.ts b/src/base.ts
index b78a23b..530748b 100644
--- a/src/base.ts
+++ b/src/base.ts
@@ -3,7 +3,6 @@ import { execaCommand } from "execa";
 import gitRemoteOriginUrl from "git-remote-origin-url";
 import gitUrlParse from "git-url-parse";
 import { inputFromFile } from "input-from-file";
-import { inputFromFileJSON } from "input-from-file-json";
 import { inputFromScript } from "input-from-script";
 import lazyValue from "lazy-value";
 import npmUser from "npm-user";
@@ -11,6 +10,7 @@ import { z } from "zod";
 
 import { inputFromOctokit } from "./inputs/inputFromOctokit.js";
 import { parsePackageAuthor } from "./options/parsePackageAuthor.js";
+import { readAllContributors } from "./options/readAllContributors.js";
 import { readDefaultsFromReadme } from "./options/readDefaultsFromReadme.js";
 import { readDescription } from "./options/readDescription.js";
 import { readDocumentation } from "./options/readDocumentation.js";
@@ -19,7 +19,6 @@ import { readFileSafe } from "./options/readFileSafe.js";
 import { readFunding } from "./options/readFunding.js";
 import { readGuide } from "./options/readGuide.js";
 import { readPackageData } from "./options/readPackageData.js";
-import { AllContributorsData } from "./types.js";
 import { swallowError } from "./utils/swallowError.js";
 import { tryCatchLazyValueAsync } from "./utils/tryCatchLazyValueAsync.js";
 
@@ -141,14 +140,7 @@ export const base = createBase({
 			.describe("package version to publish as and store in `package.json`"),
 	},
 	prepare({ options, take }) {
-		const allContributors = lazyValue(async () => {
-			const contributions = (await take(inputFromFileJSON, {
-				filePath: ".all-contributorsrc",
-			})) as AllContributorsData;
-
-			return contributions.contributors;
-		});
-
+		const allContributors = lazyValue(async () => readAllContributors(take));
 		const documentation = lazyValue(async () => readDocumentation(take));
 
 		const nvmrc = lazyValue(
@@ -232,6 +224,8 @@ export const base = createBase({
 				options.directory,
 		);
 
+		const email = lazyValue(async () => readEmails(npmDefaults, packageAuthor));
+
 		return {
 			access: "public" as const,
 			author,
@@ -239,7 +233,7 @@ export const base = createBase({
 			contributors: allContributors,
 			description: async () => await readDescription(packageData, readme),
 			documentation,
-			email: async () => readEmails(npmDefaults, packageAuthor),
+			email,
 			funding: readFunding,
 			guide: readGuide,
 			login: author,
diff --git a/src/blocks/blockAllContributors.ts b/src/blocks/blockAllContributors.ts
index 2257e63..908b0ea 100644
--- a/src/blocks/blockAllContributors.ts
+++ b/src/blocks/blockAllContributors.ts
@@ -1,6 +1,7 @@
 import _ from "lodash";
 
 import { base, Contributor } from "../base.js";
+import { ownerContributions } from "../data/contributions.js";
 import { blockREADME } from "./blockREADME.js";
 import { blockRepositorySecrets } from "./blockRepositorySecrets.js";
 import { createSoloWorkflowFile } from "./files/createSoloWorkflowFile.js";
@@ -68,7 +69,7 @@ export const blockAllContributors = base.createBlock({
 			scripts: [
 				{
 					commands: [
-						`pnpx all-contributors-cli add ${options.owner} code,content,doc,ideas,infra,maintenance,projectManagement,tool`,
+						`pnpx all-contributors-cli add ${options.owner} ${ownerContributions.join(",")}`,
 					],
 					phase: CommandPhase.Process,
 				},
diff --git a/src/data/contributions.ts b/src/data/contributions.ts
new file mode 100644
index 0000000..8d4a135
--- /dev/null
+++ b/src/data/contributions.ts
@@ -0,0 +1,10 @@
+export const ownerContributions = [
+	"code",
+	"content",
+	"doc",
+	"ideas",
+	"infra",
+	"maintenance",
+	"projectManagement",
+	"tool",
+];
diff --git a/src/inputs/inputFromOctokit.ts b/src/inputs/inputFromOctokit.ts
index 494e1de..eb63956 100644
--- a/src/inputs/inputFromOctokit.ts
+++ b/src/inputs/inputFromOctokit.ts
@@ -4,7 +4,7 @@ import { z } from "zod";
 export const inputFromOctokit = createInput({
 	args: {
 		endpoint: z.string(),
-		options: z.record(z.string(), z.unknown()),
+		options: z.record(z.string(), z.unknown()).optional(),
 	},
 	// TODO: Strongly type this, then push it upstream to Bingo
 	// This will require smart types around GitHub endpoints, similar to:
diff --git a/src/options/readAllContributors.ts b/src/options/readAllContributors.ts
new file mode 100644
index 0000000..2d5e037
--- /dev/null
+++ b/src/options/readAllContributors.ts
@@ -0,0 +1,30 @@
+import { TakeInput } from "bingo";
+import { inputFromFileJSON } from "input-from-file-json";
+
+import { ownerContributions } from "../data/contributions.js";
+import { inputFromOctokit } from "../inputs/inputFromOctokit.js";
+import { AllContributorsData } from "../types.js";
+
+export async function readAllContributors(take: TakeInput) {
+	const contributions = (await take(inputFromFileJSON, {
+		filePath: ".all-contributorsrc",
+	})) as AllContributorsData | Error;
+
+	if (!(contributions instanceof Error)) {
+		return contributions.contributors;
+	}
+
+	const user = (await take(inputFromOctokit, {
+		endpoint: "GET /user",
+	})) as { avatar_url: string; blog: string; login: string; name: string };
+
+	return [
+		{
+			avatar_url: user.avatar_url,
+			contributions: ownerContributions,
+			login: user.login,
+			name: user.name,
+			profile: user.blog,
+		},
+	];
+}


Test patch:
diff --git a/src/options/readAllContributors.test.ts b/src/options/readAllContributors.test.ts
new file mode 100644
index 0000000..f76ac22
--- /dev/null
+++ b/src/options/readAllContributors.test.ts
@@ -0,0 +1,57 @@
+import { createMockSystems } from "bingo-testers";
+import { describe, expect, it, vi } from "vitest";
+
+import { ownerContributions } from "../data/contributions.js";
+import { readAllContributors } from "./readAllContributors.js";
+
+const mockInputFromFileJSON = vi.fn();
+
+vi.mock("input-from-file-json", () => ({
+	get inputFromFileJSON() {
+		return mockInputFromFileJSON;
+	},
+}));
+
+const mockInputFromOctokit = vi.fn();
+
+vi.mock("../inputs/inputFromOctokit.js", () => ({
+	get inputFromOctokit() {
+		return mockInputFromOctokit;
+	},
+}));
+
+const { take } = createMockSystems();
+
+describe("readAllContributors", () => {
+	it("returns contributors from .all-contributorsrc when it can be read", async () => {
+		const contributors = ["a", "b", "c"];
+		mockInputFromFileJSON.mockResolvedValueOnce({ contributors });
+
+		const actual = await readAllContributors(take);
+
+		expect(actual).toEqual(contributors);
+		expect(mockInputFromOctokit).not.toHaveBeenCalled();
+	});
+
+	it("returns the current user as a contributor when .all-contributorsrc cannot be read", async () => {
+		mockInputFromFileJSON.mockResolvedValueOnce(new Error("Oh no!"));
+		mockInputFromOctokit.mockResolvedValueOnce({
+			avatar_url: "avatar_url",
+			blog: "blog",
+			login: "login",
+			name: "name",
+		});
+
+		const actual = await readAllContributors(take);
+
+		expect(actual).toEqual([
+			{
+				avatar_url: "avatar_url",
+				contributions: ownerContributions,
+				login: "login",
+				name: "name",
+				profile: "blog",
+			},
+		]);
+	});
+});