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

Issue:
🚀 Feature: Allow hydration/migration to run on an empty directory
### 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

"Migration" (before #668, "hydration") right now requires a directory to at least have a `package.json` and be a Git repo (i.e. initialized with `git init` or `git clone`). But it wouldn't be much work to be able to run on an empty directory. That'll make testing the hydration/migration script out easier.

### Additional Info

Splitting out of the changes from #668.

Pull request description:
feat: allowed hydration directory to be empty
## PR Checklist

- [x] Addresses an existing open issue: fixes #669
- [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

Changes split out of #668:

* If `git status` fails, runs `git init` to make the directory a repo
* Allows `package.json` to be empty, defaulting its contents to `{}`

Patch:
diff --git a/src/hydrate/steps/writing/creation/writePackageJson.ts b/src/hydrate/steps/writing/creation/writePackageJson.ts
index 00169ac..eaef89d 100644
--- a/src/hydrate/steps/writing/creation/writePackageJson.ts
+++ b/src/hydrate/steps/writing/creation/writePackageJson.ts
@@ -1,4 +1,4 @@
-import { readFileAsJson } from "../../../../shared/readFileAsJson.js";
+import { readFileSafeAsJson } from "../../../../shared/readFileSafeAsJson.js";
 import { HydrationInputValues } from "../../../values/types.js";
 import { formatJson } from "./formatters/formatJson.js";
 
@@ -41,9 +41,8 @@ export async function writePackageJson({
 	| "repository"
 	| "unitTests"
 >) {
-	const existingPackageJson = (await readFileAsJson(
-		"./package.json",
-	)) as object;
+	const existingPackageJson =
+		((await readFileSafeAsJson("./package.json")) as null | object) ?? {};
 
 	return await formatJson({
 		// To start, copy over all existing package fields (e.g. "dependencies")
diff --git a/src/shared/ensureGitRepository.ts b/src/shared/ensureGitRepository.ts
new file mode 100644
index 0000000..c43f502
--- /dev/null
+++ b/src/shared/ensureGitRepository.ts
@@ -0,0 +1,17 @@
+import chalk from "chalk";
+import { $ } from "execa";
+
+import { logLine } from "./cli/lines.js";
+
+export async function ensureGitRepository() {
+	try {
+		await $`git status`;
+	} catch {
+		logLine();
+		logLine(
+			chalk.gray("Running `git init` to turn this into a Git repository."),
+		);
+
+		await $`git init`;
+	}
+}
diff --git a/src/shared/getDefaultSettings.ts b/src/shared/getDefaultSettings.ts
index ccc6ff1..d752ddc 100644
--- a/src/shared/getDefaultSettings.ts
+++ b/src/shared/getDefaultSettings.ts
@@ -14,7 +14,7 @@ export async function getDefaultSettings() {
 		logLine();
 		logLine(
 			chalk.gray(
-				"Could not populate default owner and repository. Did not detect a Git repository with an origin. ",
+				"Could not populate default owner and repository. Did not detect an 'origin' remote.",
 			),
 		);
 
diff --git a/src/shared/runOrRestore.ts b/src/shared/runOrRestore.ts
index f433aea..b98d29e 100644
--- a/src/shared/runOrRestore.ts
+++ b/src/shared/runOrRestore.ts
@@ -2,6 +2,7 @@ import * as prompts from "@clack/prompts";
 import chalk from "chalk";
 import { $ } from "execa";
 
+import { ensureGitRepository } from "./ensureGitRepository.js";
 import {
 	GetterDefaultInputValues,
 	InputValuesAndOctokit,
@@ -39,6 +40,8 @@ export async function runOrRestore({
 			),
 		);
 
+		await ensureGitRepository();
+
 		const { octokit, values } = await getInputValuesAndOctokit(args, defaults);
 
 		skipRestore = values.skipRestore;


Test patch:
diff --git a/src/hydrate/steps/writing/creation/writePackageJson.test.ts b/src/hydrate/steps/writing/creation/writePackageJson.test.ts
index 01ea060..b20a864 100644
--- a/src/hydrate/steps/writing/creation/writePackageJson.test.ts
+++ b/src/hydrate/steps/writing/creation/writePackageJson.test.ts
@@ -2,11 +2,11 @@ import { describe, expect, it, vi } from "vitest";
 
 import { writePackageJson } from "./writePackageJson.js";
 
-const mockReadFileAsJson = vi.fn();
+const mockReadFileSafeAsJson = vi.fn();
 
-vi.mock("../../../../shared/readFileAsJson.js", () => ({
-	get readFileAsJson() {
-		return mockReadFileAsJson;
+vi.mock("../../../../shared/readFileSafeAsJson.js", () => ({
+	get readFileSafeAsJson() {
+		return mockReadFileSafeAsJson;
 	},
 }));
 
@@ -23,7 +23,7 @@ const values = {
 describe("writePackageJson", () => {
 	it("preserves existing dependencies when they exist", async () => {
 		const dependencies = { abc: "1.2.3" };
-		mockReadFileAsJson.mockResolvedValue({ dependencies });
+		mockReadFileSafeAsJson.mockResolvedValue({ dependencies });
 
 		const packageJson = await writePackageJson(values);
 
@@ -34,7 +34,7 @@ describe("writePackageJson", () => {
 
 	it("preserves existing devDependencies that aren't known to be unnecessary when they exist", async () => {
 		const devDependencies = { abc: "1.2.3", jest: "4.5.6" };
-		mockReadFileAsJson.mockResolvedValue({ devDependencies });
+		mockReadFileSafeAsJson.mockResolvedValue({ devDependencies });
 
 		const packageJson = await writePackageJson(values);
 
@@ -44,7 +44,7 @@ describe("writePackageJson", () => {
 	});
 
 	it("includes a release script when releases is true", async () => {
-		mockReadFileAsJson.mockResolvedValue({});
+		mockReadFileSafeAsJson.mockResolvedValue({});
 
 		const packageJson = await writePackageJson({
 			...values,
@@ -61,7 +61,7 @@ describe("writePackageJson", () => {
 	});
 
 	it("includes a test script when unitTests is true", async () => {
-		mockReadFileAsJson.mockResolvedValue({});
+		mockReadFileSafeAsJson.mockResolvedValue({});
 
 		const packageJson = await writePackageJson({
 			...values,
diff --git a/src/shared/ensureGitRepository.test.ts b/src/shared/ensureGitRepository.test.ts
new file mode 100644
index 0000000..9ebacd6
--- /dev/null
+++ b/src/shared/ensureGitRepository.test.ts
@@ -0,0 +1,43 @@
+import chalk from "chalk";
+import { describe, expect, it, vi } from "vitest";
+
+import { ensureGitRepository } from "./ensureGitRepository.js";
+
+const mock$ = vi.fn();
+
+vi.mock("execa", () => ({
+	get $() {
+		return mock$;
+	},
+}));
+
+const mockLogLine = vi.fn();
+
+vi.mock("./cli/lines.js", () => ({
+	get logLine() {
+		return mockLogLine;
+	},
+}));
+
+describe("ensureGitRepository", () => {
+	it("does not run git init when git status succeeds", async () => {
+		mock$.mockResolvedValue(0);
+
+		await ensureGitRepository();
+
+		expect(mock$).toHaveBeenCalledTimes(1);
+		expect(mockLogLine).not.toHaveBeenCalled();
+	});
+
+	it("runs git init when git status fails", async () => {
+		mock$.mockRejectedValueOnce(1);
+
+		await ensureGitRepository();
+
+		expect(mock$).toHaveBeenCalledWith(["git init"]);
+		expect(mockLogLine).toHaveBeenCalledWith();
+		expect(mockLogLine).toHaveBeenCalledWith(
+			chalk.gray("Running `git init` to turn this into a Git repository."),
+		);
+	});
+});
diff --git a/src/shared/runOrRestore.test.ts b/src/shared/runOrRestore.test.ts
index 5dd2227..6ee8f1b 100644
--- a/src/shared/runOrRestore.test.ts
+++ b/src/shared/runOrRestore.test.ts
@@ -67,7 +67,8 @@ describe("runOrRestore", () => {
 
 		expect(actual).toEqual(1);
 
-		expect(mock$).not.toHaveBeenCalled();
+		expect(mock$).toHaveBeenCalledWith(["git status"]);
+		expect(mock$).toHaveBeenCalledTimes(1);
 	});
 
 	it("returns 1 and restores the repository when run rejects, skipRestore is false, and shouldRestore is confirmed", async () => {
@@ -87,6 +88,8 @@ describe("runOrRestore", () => {
 
 		expect(actual).toEqual(1);
 
+		expect(mock$).toHaveBeenCalledWith(["git status"]);
 		expect(mock$).toHaveBeenCalledWith(["git restore ."]);
+		expect(mock$).toHaveBeenCalledTimes(2);
 	});
 });