Repository: JoshuaKGoldberg/template-typescript-node-package (ts)

Issue:
🚀 Feature: If the user running initialize isn't already in allcontributors, add them
### 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/template-typescript-node-package/issues?q=is%3Aissue) and found none that matched my issue.

### Overview

Right now, `addOwnerAsContributor` wipes out the `.all-contributorsrc` contributors _except_ for `"JoshuaKGoldberg"` and the currently running user. But what if that user isn't yet in the contributors list? They should be added for `"tool"` if so.

### Additional Info

_No response_

Pull request description:
feat: ensure running user is included in .all-contributorsrc
## PR Checklist

- [x] Addresses an existing open issue: fixes #676
- [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/template-typescript-node-package/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/template-typescript-node-package/blob/main/.github/CONTRIBUTING.md) were taken

## Overview

Adds some nice test coverage too.

Patch:
diff --git a/package.json b/package.json
index 9dba8f6..fc87ac9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "template-typescript-node-package",
-	"version": "1.28.58",
+	"version": "1.28.59",
 	"description": "A quickstart-friendly TypeScript template with comprehensive formatting, linting, releases, testing, and other great tooling built-in. ✨",
 	"repository": {
 		"type": "git",
diff --git a/src/initialize/settings/addOwnerAsAllContributor.ts b/src/initialize/settings/addOwnerAsAllContributor.ts
index 5c32616..a8d0444 100644
--- a/src/initialize/settings/addOwnerAsAllContributor.ts
+++ b/src/initialize/settings/addOwnerAsAllContributor.ts
@@ -43,18 +43,27 @@ export async function addOwnerAsAllContributor(owner: string) {
 		);
 	}
 
+	const contributors = existingContributors.contributors
+		.filter(({ login }) => ["JoshuaKGoldberg", user].includes(login))
+		.map((contributor) =>
+			contributor.login === "JoshuaKGoldberg"
+				? { ...contributor, contributions: ["tool"] }
+				: contributor,
+		);
+
+	if (!contributors.some((contributor) => contributor.login === user)) {
+		contributors.push({
+			contributions: ["tool"],
+			login: user,
+		});
+	}
+
 	await fs.writeFile(
 		"./.all-contributorsrc",
 		await prettier.format(
 			JSON.stringify({
 				...existingContributors,
-				contributors: existingContributors.contributors
-					.filter(({ login }) => ["JoshuaKGoldberg", user].includes(login))
-					.map((contributor) =>
-						contributor.login === "JoshuaKGoldberg"
-							? { ...contributor, contributions: ["tool"] }
-							: contributor,
-					),
+				contributors,
 			}),
 			{ parser: "json" },
 		),
diff --git a/src/shared/types.ts b/src/shared/types.ts
index a7b1246..75502fb 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -1,3 +1,8 @@
+export interface AllContributorContributor {
+	contributions: string[];
+	login: string;
+}
+
 export interface AllContributorsData {
-	contributors: { contributions: string[]; login: string }[];
+	contributors: AllContributorContributor[];
 }


Test patch:
diff --git a/src/initialize/settings/addOwnerAsAllContributor.test.ts b/src/initialize/settings/addOwnerAsAllContributor.test.ts
new file mode 100644
index 0000000..0ab9b0a
--- /dev/null
+++ b/src/initialize/settings/addOwnerAsAllContributor.test.ts
@@ -0,0 +1,143 @@
+import chalk from "chalk";
+import prettier from "prettier";
+import { SpyInstance, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { addOwnerAsAllContributor } from "./addOwnerAsAllContributor.js";
+
+const mock$ = vi.fn();
+
+vi.mock("execa", () => ({
+	get $() {
+		return mock$;
+	},
+}));
+
+const mockWriteFile = vi.fn();
+
+vi.mock("node:fs/promises", () => ({
+	get default() {
+		return {
+			get writeFile() {
+				return mockWriteFile;
+			},
+		};
+	},
+}));
+
+const mockReadFileAsJson = vi.fn();
+
+vi.mock("../../shared/readFileAsJson.js", () => ({
+	get readFileAsJson() {
+		return mockReadFileAsJson;
+	},
+}));
+
+let mockConsoleWarn: SpyInstance;
+
+describe("addOwnerAsAllContributor", () => {
+	beforeEach(() => {
+		mockConsoleWarn = vi
+			.spyOn(console, "warn")
+			.mockImplementation(() => undefined);
+	});
+
+	it("throws an error when the .all-contributorsrc fails to read", async () => {
+		mock$.mockResolvedValueOnce({
+			stdout: JSON.stringify({ login: "user" }),
+		});
+		mockReadFileAsJson.mockResolvedValue("invalid");
+
+		await expect(async () => {
+			await addOwnerAsAllContributor("owner");
+		}).rejects.toMatchInlineSnapshot(
+			'[Error: Invalid .all-contributorsrc: "invalid"]',
+		);
+	});
+
+	it("throws an error when the .all-contributorsrc is missing expected properties", async () => {
+		mock$.mockResolvedValueOnce({
+			stdout: JSON.stringify({ login: "user" }),
+		});
+		mockReadFileAsJson.mockResolvedValue({});
+
+		await expect(async () => {
+			await addOwnerAsAllContributor("owner");
+		}).rejects.toMatchInlineSnapshot(
+			"[Error: Invalid .all-contributorsrc: {}]",
+		);
+	});
+
+	it("adds in the user from gh api user when it succeeds and contributors is empty", async () => {
+		const login = "gh-api-user";
+
+		mock$.mockResolvedValueOnce({
+			stdout: JSON.stringify({ login }),
+		});
+		mockReadFileAsJson.mockResolvedValue({ contributors: [] });
+
+		await addOwnerAsAllContributor("owner");
+
+		expect(mockWriteFile).toHaveBeenCalledWith(
+			"./.all-contributorsrc",
+			await prettier.format(
+				JSON.stringify({
+					contributors: [{ contributions: ["tool"], login }],
+				}),
+				{ parser: "json" },
+			),
+		);
+	});
+
+	it("adds in the provided owner when gh api user fails and contributors is empty", async () => {
+		const owner = "owner";
+
+		mock$.mockRejectedValueOnce({});
+		mockReadFileAsJson.mockResolvedValue({ contributors: [] });
+
+		await addOwnerAsAllContributor(owner);
+
+		expect(mockWriteFile).toHaveBeenCalledWith(
+			"./.all-contributorsrc",
+			await prettier.format(
+				JSON.stringify({
+					contributors: [{ contributions: ["tool"], login: owner }],
+				}),
+				{ parser: "json" },
+			),
+		);
+		expect(mockConsoleWarn).toHaveBeenCalledWith(
+			chalk.gray(
+				`Couldn't authenticate GitHub user, falling back to the provided owner name '${owner}'.`,
+			),
+		);
+	});
+
+	it("resets JoshuaKGoldberg to just tool and adds in the running user when both exist", async () => {
+		const login = "gh-api-user";
+
+		mock$.mockResolvedValueOnce({
+			stdout: JSON.stringify({ login }),
+		});
+		mockReadFileAsJson.mockResolvedValue({
+			contributors: [
+				{ contributions: ["bug", "fix"], login },
+				{ contributions: ["bug", "fix"], login: "JoshuaKGoldberg" },
+			],
+		});
+
+		await addOwnerAsAllContributor("owner");
+
+		expect(mockWriteFile).toHaveBeenCalledWith(
+			"./.all-contributorsrc",
+			await prettier.format(
+				JSON.stringify({
+					contributors: [
+						{ contributions: ["bug", "fix"], login },
+						{ contributions: ["tool"], login: "JoshuaKGoldberg" },
+					],
+				}),
+				{ parser: "json" },
+			),
+		);
+	});
+});