Repository: JoshuaKGoldberg/bingo (ts)

Issue:
🚀 Feature: Give better messaging if not logged into GitHub
### Feature Request Checklist

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

### Overview

With the initial changes in #259, trying to create a new repo without being logged into GitHub gives:

```plaintext
┌  ✨ create-typescript-app@2.4.1 ✨
│
●  Learn more on:
│    https://github.com/JoshuaKGoldberg/create-typescript-app
│
◇  Running with mode --setup using the template:
│    create-typescript-app
│
◇  Inferred default options from system
│
◇  What will the GitHub organization or user the repository is underneath be? (--owner)
│  asdf
│
◇  What will the starting set of tooling to use be? (--preset)
│  common
│
■  Error creating repository on GitHub:
│
■  HttpError: Requires authentication - https://docs.github.com/rest/repos/repos#create-a-repository-using-a-template
│      at fetchWrapper (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/@octokit+request@9.2.2/node_modules/@octokit/request/dist-bundle/index.js:122:11)
│      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
│      at async requestWithGraphqlErrorHandling (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/@octokit+plugin-retry@7.1.4_@octokit+core@6.1.4/node_modules/@octokit/plugin-retry/dist-bundle/index.js:36:20)
│      at async Job.doExecute (/Users/josh/repos/create-typescript-app/node_modules/.pnpm/bottleneck@2.19.5/node_modules/bottleneck/light.js:405:18)
│
◇  Ran the Create TypeScript App template
│
```

That's not a great experience. Users should instead be given more friendly messaging that because they're not logged into GitHub, the repo will stay offline.

### Additional Info

There'll probably be a followup issue after this around running transition mode in a repo that's offline - nothing should crash if there's no upstream...

💝 

Pull request description:
feat: don't attempt to create repository if not authenticated
<!-- 👋 Hi, thanks for sending a PR to Bingo! 💖.
Please fill out all fields below and make sure each item is true and [x] checked.
Otherwise we may not be able to review your PR. -->

## PR Checklist

- [x] Addresses an existing open issue: fixes #261
- [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/bingo/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/bingo/blob/main/.github/CONTRIBUTING.md) were taken

## Overview

Checks whether the user seems to be logged into GitHub with [`getGitHubAuthToken`](https://github.com/JoshuaKGoldberg/get-github-auth-token), which intentionally is what Bingo elsewhere uses to set up its Octokit instances.

An alternate design could have been to just not have a `system.fetchers.octokit` Octokit instance without auth. I think that's a design that will more likely happen with #275 - and I've noted it there too with https://github.com/JoshuaKGoldberg/bingo/issues/275#issuecomment-2715524221.

Refactors a bit of the Zod schema stuffs to be more shared and tested. One day I will split out a library...

💝 

Patch:
diff --git a/packages/bingo/src/cli/prompts/promptForOptionSchema.ts b/packages/bingo/src/cli/prompts/promptForOptionSchema.ts
index f4718d7..bf3fdf2 100644
--- a/packages/bingo/src/cli/prompts/promptForOptionSchema.ts
+++ b/packages/bingo/src/cli/prompts/promptForOptionSchema.ts
@@ -1,27 +1,18 @@
 import * as prompts from "@clack/prompts";
 import * as z from "zod";
 
+import { isZodDefaultDef } from "../schemas/isZodDefaultDef.js";
+import { PromptFriendlyZodDef } from "../schemas/types.js";
 import { validateNumber, validatorFromSchema } from "./validators.js";
 
-type ZodDef =
-	| z.ZodBooleanDef
-	| (z.ZodDefault<z.ZodTypeAny> & {
-			innerType: z.ZodTypeAny;
-			typeName: z.ZodFirstPartyTypeKind.ZodDefault;
-	  })
-	| z.ZodUnionDef
-	| ZodStringLikeDef;
-
-type ZodStringLikeDef = z.ZodNumberDef | z.ZodStringDef;
-
 export async function promptForOptionSchema(
 	key: string,
 	schema: z.ZodTypeAny,
 	description: string | undefined,
 	defaultValue: unknown,
 ) {
-	const def = schema._def as ZodDef;
-	if (def.typeName === z.ZodFirstPartyTypeKind.ZodDefault) {
+	const def = schema._def as PromptFriendlyZodDef;
+	if (isZodDefaultDef(def)) {
 		return await promptForOptionSchema(
 			key,
 			def.innerType,
@@ -29,6 +20,7 @@ export async function promptForOptionSchema(
 			defaultValue,
 		);
 	}
+
 	const message = description
 		? `What will the ${description} be? (--${key})`
 		: `What will the --${key} be?`;
@@ -44,6 +36,17 @@ export async function promptForOptionSchema(
 				break;
 			}
 
+			case z.ZodFirstPartyTypeKind.ZodEnum: {
+				const options = def.values.map((value) => ({ value }));
+				const text = await prompts.select({
+					initialValue: defaultValue as string,
+					message,
+					options,
+				});
+
+				return cancelOrParse(schema, text);
+			}
+
 			case z.ZodFirstPartyTypeKind.ZodNumber:
 				value = Number(
 					await prompts.text({
@@ -66,11 +69,7 @@ export async function promptForOptionSchema(
 					options,
 				});
 
-				if (prompts.isCancel(text)) {
-					return text;
-				}
-
-				return schema.parse(text) as unknown;
+				return cancelOrParse(schema, text);
 			}
 
 			default: {
@@ -80,14 +79,14 @@ export async function promptForOptionSchema(
 					validate: validatorFromSchema(schema),
 				});
 
-				if (prompts.isCancel(text)) {
-					return text;
-				}
-
-				return schema.parse(text) as unknown;
+				return cancelOrParse(schema, text);
 			}
 		}
 	}
 
 	return value;
 }
+
+function cancelOrParse(schema: z.ZodTypeAny, text: unknown) {
+	return prompts.isCancel(text) ? text : (schema.parse(text) as unknown);
+}
diff --git a/packages/bingo/src/cli/schemas/isStringLikeSchema.ts b/packages/bingo/src/cli/schemas/isStringLikeSchema.ts
new file mode 100644
index 0000000..6f51981
--- /dev/null
+++ b/packages/bingo/src/cli/schemas/isStringLikeSchema.ts
@@ -0,0 +1,26 @@
+import { z } from "zod";
+
+import { PromptFriendlyZodDef } from "./types.js";
+
+export function isStringLikeSchema(schema: z.ZodTypeAny): boolean {
+	const def = schema._def as PromptFriendlyZodDef;
+
+	switch (def.typeName) {
+		case z.ZodFirstPartyTypeKind.ZodBoolean:
+		case z.ZodFirstPartyTypeKind.ZodEnum:
+		case z.ZodFirstPartyTypeKind.ZodLiteral:
+		case z.ZodFirstPartyTypeKind.ZodNumber:
+		case z.ZodFirstPartyTypeKind.ZodString:
+			return true;
+
+		case z.ZodFirstPartyTypeKind.ZodDefault:
+		case z.ZodFirstPartyTypeKind.ZodOptional:
+			return isStringLikeSchema(def.innerType);
+
+		case z.ZodFirstPartyTypeKind.ZodUnion:
+			return def.options.every((option) => isStringLikeSchema(option));
+
+		default:
+			return false;
+	}
+}
diff --git a/packages/bingo/src/cli/schemas/isZodDefaultDef.ts b/packages/bingo/src/cli/schemas/isZodDefaultDef.ts
new file mode 100644
index 0000000..02a23e2
--- /dev/null
+++ b/packages/bingo/src/cli/schemas/isZodDefaultDef.ts
@@ -0,0 +1,9 @@
+// eslint-disable-next-line @eslint-community/eslint-comments/disable-enable-pair
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
+import { z } from "zod";
+
+import { ZodDefaultDefWithType } from "./types.js";
+
+export function isZodDefaultDef(def: any): def is ZodDefaultDefWithType {
+	return def.typeName === z.ZodFirstPartyTypeKind.ZodDefault;
+}
diff --git a/packages/bingo/src/cli/schemas/types.ts b/packages/bingo/src/cli/schemas/types.ts
new file mode 100644
index 0000000..7552ca8
--- /dev/null
+++ b/packages/bingo/src/cli/schemas/types.ts
@@ -0,0 +1,22 @@
+import { z } from "zod";
+
+/**
+ * The known Zod schema types that we know how to prompt for with Clack.
+ */
+export type PromptFriendlyZodDef =
+	| z.ZodBooleanDef
+	| z.ZodEnumDef
+	| z.ZodLiteralDef
+	| z.ZodNumberDef
+	| z.ZodOptionalDef
+	| z.ZodStringDef
+	| z.ZodUnionDef
+	| ZodDefaultDefWithType;
+
+/**
+ * Our view of default defs: we access the private innerType and typeName properties.
+ */
+export type ZodDefaultDefWithType = z.ZodDefaultDef & {
+	innerType: z.ZodTypeAny;
+	typeName: z.ZodFirstPartyTypeKind.ZodDefault;
+};
diff --git a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
index 8ab1a53..8d51497 100644
--- a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
+++ b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
@@ -1,12 +1,15 @@
 import { SystemRunner } from "bingo-systems";
-import { Result as ExecaResult } from "execa";
+import { ExecaError, Result as ExecaResult } from "execa";
+import { getGitHubAuthToken } from "get-github-auth-token";
 import { newGitHubRepository } from "new-github-repository";
 import { Octokit } from "octokit";
-import { z } from "zod";
 
+import { AnyShape } from "../../types/shapes.js";
+import { Template } from "../../types/templates.js";
 import { ClackDisplay } from "../display/createClackDisplay.js";
 import { runSpinnerTask } from "../display/runSpinnerTask.js";
 import { promptForOptionSchema } from "../prompts/promptForOptionSchema.js";
+import { isStringLikeSchema } from "../schemas/isStringLikeSchema.js";
 
 export interface PartialRepositoryLocator {
 	owner?: string;
@@ -17,55 +20,75 @@ export interface RepositoryLocator extends PartialRepositoryLocator {
 	owner: string;
 }
 
-export async function createRepositoryOnGitHub(
+export async function createRepositoryOnGitHub<
+	OptionsShape extends AnyShape,
+	Refinements,
+>(
 	display: ClackDisplay,
 	{ owner: requestedOwner, repository }: PartialRepositoryLocator,
 	octokit: Octokit,
 	runner: SystemRunner,
-	template?: RepositoryLocator,
+	template: Template<OptionsShape, Refinements>,
 ): Promise<Error | RepositoryLocator | undefined> {
-	const owner =
-		requestedOwner ??
-		stdoutIfNotError(await runner("gh config get user -h github.com")) ??
-		(await promptForOptionSchema(
-			"owner",
-			z.string(),
-			"GitHub organization or user the repository is underneath",
-			undefined,
-		));
+	const hasStringLikeOwner = hasStringLikeOption(template.options, "owner");
+	const hasStringLikeRepository = hasStringLikeOption(
+		template.options,
+		"repository",
+	);
 
-	if (!isStringLike(owner)) {
+	if (!hasStringLikeOwner) {
 		return new Error(
-			`To run with --mode setup, --owner must be a string-like, not ${typeof owner}.`,
+			hasStringLikeRepository
+				? `To run with --mode setup and not --offline, options.owner must be a string-like schema.`
+				: `To run with --mode setup and not --offline, options.owner and options.repository must be string-like schemas.`,
 		);
 	}
 
-	if (!isStringLike(repository)) {
+	if (!hasStringLikeRepository) {
 		return new Error(
-			`To run with --mode setup, --repository must be a string-like, not ${typeof repository}.`,
+			`To run with --mode setup and not --offline, options.repository must be a string-like schema.`,
 		);
 	}
 
-	if (typeof owner !== "string") {
+	// We'll only want to create a repository if we're logged in.
+	// getGitHubAuthToken is intentionally what Bingo uses to create an Octokit.
+	const authToken = await getGitHubAuthToken();
+	if (!authToken.succeeded) {
 		return undefined;
 	}
 
+	const ownerRaw =
+		requestedOwner ??
+		stdoutIfNotError(await runner("gh config get user -h github.com")) ??
+		(await promptForOptionSchema(
+			"owner",
+			template.options.owner,
+			"GitHub organization or user the repository is underneath",
+			undefined,
+		));
+
+	const owner = String(ownerRaw);
 	const error = await runSpinnerTask(
 		display,
 		"Creating repository on GitHub",
 		"Created repository on GitHub",
 		async () => {
-			await newGitHubRepository({ octokit, owner, repository, template });
+			await newGitHubRepository({
+				octokit,
+				owner,
+				repository,
+				template: template.about?.repository,
+			});
 		},
 	);
 
-	return error ? undefined : { owner, repository };
+	return error || { owner, repository };
 }
 
-function isStringLike(value: unknown): value is boolean | number | string {
-	return ["boolean", "number", "string", "undefined"].includes(typeof value);
+function hasStringLikeOption(options: AnyShape, key: string): boolean {
+	return key in options && isStringLikeSchema(options[key]);
 }
 
-function stdoutIfNotError(value: Error | ExecaResult) {
+function stdoutIfNotError(value: ExecaError | ExecaResult) {
 	return value instanceof Error ? undefined : value.stdout;
 }
diff --git a/packages/bingo/src/cli/setup/runModeSetup.ts b/packages/bingo/src/cli/setup/runModeSetup.ts
index da31ada..0b281b0 100644
--- a/packages/bingo/src/cli/setup/runModeSetup.ts
+++ b/packages/bingo/src/cli/setup/runModeSetup.ts
@@ -119,17 +119,17 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 				{ repository, ...baseOptions.completed },
 				system.fetchers.octokit,
 				system.runner,
-				template.about?.repository,
+				template,
 			);
 
 	if (remote instanceof Error) {
-		logRerunSuggestion(argv, baseOptions.completed);
+		logRerunSuggestion(argv, baseOptions.prompted);
 		return { error: remote, status: CLIStatus.Error };
 	}
 
 	if (!offline && !remote) {
 		prompts.log.warn(
-			"Running in local-only mode without a repository on GitHub",
+			"Running in local-only mode without a repository on GitHub. To push to GitHub, log in with the GitHub CLI (cli.github.com) or run with a GH_TOKEN process environment variable.",
 		);
 	}
 
@@ -156,7 +156,7 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 		};
 	}
 
-	const prepared = await runSpinnerTask(
+	const preparationError = await runSpinnerTask(
 		display,
 		"Preparing local repository",
 		"Prepared local repository",
@@ -167,22 +167,23 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 		},
 	);
 
+	prompts.log.message(
+		[
+			"You've got a new repository ready to use in:",
+			`  ${chalk.green(makeRelative(directory))}`,
+			...(remote
+				? [
+						"",
+						"It's also pushed to GitHub on:",
+						`  ${chalk.green(`https://github.com/${remote.owner}/${remote.repository}`)}`,
+					]
+				: []),
+		].join("\n"),
+	);
+
 	logRerunSuggestion(argv, baseOptions.prompted);
 
-	if (!prepared) {
-		prompts.log.message(
-			[
-				"Great, you've got a new repository ready to use in:",
-				`  ${chalk.green(makeRelative(directory))}`,
-				...(remote
-					? [
-							"",
-							"It's also pushed to GitHub on:",
-							`  ${chalk.green(`https://github.com/${remote.owner}/${remote.repository}`)}`,
-						]
-					: []),
-			].join("\n"),
-		);
+	if (preparationError) {
 		return {
 			outro: CLIMessage.Leaving,
 			status: CLIStatus.Error,


Test patch:
diff --git a/packages/bingo/src/cli/schemas/isStringLikeSchema.test.ts b/packages/bingo/src/cli/schemas/isStringLikeSchema.test.ts
new file mode 100644
index 0000000..c1a77d4
--- /dev/null
+++ b/packages/bingo/src/cli/schemas/isStringLikeSchema.test.ts
@@ -0,0 +1,86 @@
+import { describe, expect, test } from "vitest";
+import { z } from "zod";
+
+import { isStringLikeSchema } from "./isStringLikeSchema.js";
+
+enum Value {
+	A,
+	B,
+	C,
+}
+
+describe("isStringLikeSchema", () => {
+	test.each([
+		["boolean", z.boolean(), true],
+		["boolean literal", z.literal(false), true],
+		["number", z.number(), true],
+		["number literal", z.literal(0), true],
+		["string", z.string(), true],
+		["string literal", z.literal(""), true],
+		[
+			"union of string-likes",
+			z.union([z.boolean(), z.number(), z.string()]),
+			true,
+		],
+		[
+			"union of primitives",
+			z.union([z.literal(false), z.literal(0), z.literal("")]),
+			true,
+		],
+		["default boolean", z.boolean().default(false), true],
+		["default boolean literal", z.literal(false).default(false), true],
+		["default number", z.number().default(0), true],
+		["default number literal", z.literal(0).default(0), true],
+		["default string", z.string().default(""), true],
+		["default string literal", z.literal("").default(""), true],
+		[
+			"default union of string-likes",
+			z.union([z.boolean(), z.number(), z.string()]).default(""),
+			true,
+		],
+		["optional boolean", z.boolean().optional(), true],
+		["optional boolean literal", z.literal(false).optional(), true],
+		["optional number", z.number().optional(), true],
+		["optional number literal", z.literal(0).optional(), true],
+		["optional string", z.string().optional(), true],
+		["optional string literal", z.literal("").optional(), true],
+		[
+			"optional union of string-likes",
+			z.union([z.boolean(), z.number(), z.string()]).optional(),
+			true,
+		],
+		["default optional boolean", z.boolean().default(false).optional(), true],
+		["default optional number", z.number().default(0).optional(), true],
+		["default optional string", z.string().default("").optional(), true],
+		[
+			"default optional union of string-likes",
+			z.union([z.boolean(), z.number(), z.string()]).default("").optional(),
+			true,
+		],
+		["optional default boolean", z.boolean().optional().default(false), true],
+		["optional default number", z.number().optional().default(0), true],
+		["optional default string", z.string().optional().default(""), true],
+		[
+			"optional default union of string-likes",
+			z
+				.union([z.boolean(), z.number(), z.string()])
+				.default("")
+				.optional()
+				.default(""),
+			true,
+		],
+		["enum", z.enum(["a", "b", "c"]), true],
+		["object", z.object({}), false],
+		["default object", z.object({}).default({}), false],
+		["optional object", z.object({}).optional(), false],
+		["default optional object", z.object({}).default({}).optional(), false],
+		["optional default object", z.object({}).optional().default({}), false],
+		["intersection", z.intersection(z.object({}), z.object({})), false],
+		["array", z.array(z.string()), false],
+		["tuple", z.tuple([z.string()]), false],
+		["date", z.date(), false],
+		["native enum", z.nativeEnum(Value), false],
+	])("%s", (_, schema, expected) => {
+		expect(isStringLikeSchema(schema)).toBe(expected);
+	});
+});
diff --git a/packages/bingo/src/cli/schemas/isZodDefaultDef.test.ts b/packages/bingo/src/cli/schemas/isZodDefaultDef.test.ts
new file mode 100644
index 0000000..2b567d8
--- /dev/null
+++ b/packages/bingo/src/cli/schemas/isZodDefaultDef.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, test } from "vitest";
+import { z } from "zod";
+
+import { isZodDefaultDef } from "./isZodDefaultDef.js";
+
+enum Value {
+	A,
+	B,
+	C,
+}
+
+describe("isZodDefaultDef", () => {
+	test.each([
+		["boolean", z.boolean(), false],
+		["default boolean", z.boolean().default(false), true],
+		["boolean literal", z.literal(false), false],
+		["default boolean literal", z.literal(false).default(false), true],
+		["number", z.number(), false],
+		["default number", z.number().default(0), true],
+		["number literal", z.literal(0), false],
+		["default number literal", z.literal(0).default(0), true],
+		["string", z.string(), false],
+		["default string", z.string().default(""), true],
+		["string literal", z.literal(""), false],
+		["default string literal", z.literal("").default(""), true],
+		["enum", z.enum(["a", "b", "c"]), false],
+		["default enum", z.enum(["a", "b", "c"]).default("a"), true],
+		["object", z.object({}), false],
+		["default object", z.object({}).default({}), true],
+		["array", z.array(z.string()), false],
+		["default array", z.array(z.string()).default([]), true],
+		["tuple", z.tuple([z.string()]), false],
+		["default tuple", z.tuple([z.string()]).default([""]), true],
+		["date", z.date(), false],
+		["default date", z.date().default(new Date()), true],
+		["native enum", z.nativeEnum(Value), false],
+		["default native enum", z.nativeEnum(Value).default(Value.A), true],
+	])("%s", (_, schema, expected) => {
+		expect(isZodDefaultDef(schema._def)).toBe(expected);
+	});
+});
diff --git a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.test.ts b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.test.ts
index 26b707f..bdd3bd5 100644
--- a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.test.ts
+++ b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.test.ts
@@ -2,11 +2,20 @@ import { Octokit } from "octokit";
 import { describe, expect, it, vi } from "vitest";
 import { z } from "zod";
 
+import { createTemplate } from "../../creators/createTemplate.js";
 import { ClackDisplay } from "../display/createClackDisplay.js";
 import { createRepositoryOnGitHub } from "./createRepositoryOnGitHub.js";
 
 vi.mock("@clack/prompts");
 
+const mockGetGitHubAuthToken = vi.fn();
+
+vi.mock("get-github-auth-token", () => ({
+	get getGitHubAuthToken() {
+		return mockGetGitHubAuthToken;
+	},
+}));
+
 const mockNewGitHubRepository = vi.fn();
 
 vi.mock("new-github-repository", () => ({
@@ -36,23 +45,70 @@ const display: ClackDisplay = {
 
 const mockOctokit = {} as Octokit;
 
+const options = {
+	owner: "mock-owner",
+	repository: "mock-repository",
+};
+
+const templateWithoutOptions = createTemplate({
+	produce: vi.fn(),
+});
+
+const templateWithOwnerAndRepository = createTemplate({
+	options: { owner: z.string(), repository: z.string() },
+	produce: vi.fn(),
+});
+
+const templateWithOwner = createTemplate({
+	options: { owner: z.string() },
+	produce: vi.fn(),
+});
+
+const templateWithRepository = createTemplate({
+	options: { repository: z.string() },
+	produce: vi.fn(),
+});
+
 describe("createRepositoryOnGitHub", () => {
+	it("returns an error when both owner and repository are not string-likes", async () => {
+		const mockRunner = vi.fn();
+
+		const actual = await createRepositoryOnGitHub(
+			display,
+			options,
+			mockOctokit,
+			mockRunner,
+			templateWithoutOptions,
+		);
+
+		expect(actual).toEqual(
+			new Error(
+				`To run with --mode setup and not --offline, options.owner and options.repository must be string-like schemas.`,
+			),
+		);
+		expect(mockGetGitHubAuthToken).not.toHaveBeenCalled();
+		expect(mockRunner).not.toHaveBeenCalled();
+		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
+		expect(mockNewGitHubRepository).not.toHaveBeenCalled();
+	});
+
 	it("returns an error when owner is not a string-like", async () => {
 		const mockRunner = vi.fn();
 
 		const actual = await createRepositoryOnGitHub(
 			display,
-			// eslint-disable-next-line @typescript-eslint/no-explicit-any
-			{ owner: { invalid: true } as any, repository: "mock-repository" },
+			options,
 			mockOctokit,
 			mockRunner,
+			templateWithRepository,
 		);
 
 		expect(actual).toEqual(
 			new Error(
-				`To run with --mode setup, --owner must be a string-like, not object.`,
+				`To run with --mode setup and not --offline, options.owner must be a string-like schema.`,
 			),
 		);
+		expect(mockGetGitHubAuthToken).not.toHaveBeenCalled();
 		expect(mockRunner).not.toHaveBeenCalled();
 		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
 		expect(mockNewGitHubRepository).not.toHaveBeenCalled();
@@ -63,17 +119,37 @@ describe("createRepositoryOnGitHub", () => {
 
 		const actual = await createRepositoryOnGitHub(
 			display,
-			// eslint-disable-next-line @typescript-eslint/no-explicit-any
-			{ owner: "mock-owner", repository: { invalid: true } as any },
+			options,
 			mockOctokit,
 			mockRunner,
+			templateWithOwner,
 		);
 
 		expect(actual).toEqual(
 			new Error(
-				`To run with --mode setup, --repository must be a string-like, not object.`,
+				`To run with --mode setup and not --offline, options.repository must be a string-like schema.`,
 			),
 		);
+		expect(mockGetGitHubAuthToken).not.toHaveBeenCalled();
+		expect(mockRunner).not.toHaveBeenCalled();
+		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
+		expect(mockNewGitHubRepository).not.toHaveBeenCalled();
+	});
+
+	it("returns undefined when getGitHubAuthToken does not retrieve a token", async () => {
+		const mockRunner = vi.fn();
+
+		mockGetGitHubAuthToken.mockResolvedValueOnce({ succeeded: false });
+
+		const actual = await createRepositoryOnGitHub(
+			display,
+			options,
+			mockOctokit,
+			mockRunner,
+			templateWithOwnerAndRepository,
+		);
+
+		expect(actual).toBeUndefined();
 		expect(mockRunner).not.toHaveBeenCalled();
 		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
 		expect(mockNewGitHubRepository).not.toHaveBeenCalled();
@@ -84,14 +160,17 @@ describe("createRepositoryOnGitHub", () => {
 		const repository = "mock-repository";
 		const mockRunner = vi.fn();
 
+		mockGetGitHubAuthToken.mockResolvedValueOnce({ succeeded: true });
+
 		const actual = await createRepositoryOnGitHub(
 			display,
-			{ owner, repository },
+			options,
 			mockOctokit,
 			mockRunner,
+			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toEqual({ owner, repository });
+		expect(actual).toEqual(options);
 		expect(mockRunner).not.toHaveBeenCalled();
 		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
 		expect(mockNewGitHubRepository).toHaveBeenCalledWith({
@@ -107,14 +186,17 @@ describe("createRepositoryOnGitHub", () => {
 		const repository = "mock-repository";
 		const mockRunner = vi.fn().mockResolvedValue({ stdout: owner });
 
+		mockGetGitHubAuthToken.mockResolvedValueOnce({ succeeded: true });
+
 		const actual = await createRepositoryOnGitHub(
 			display,
 			{ owner: undefined, repository },
 			mockOctokit,
 			mockRunner,
+			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toEqual({ owner, repository });
+		expect(actual).toEqual(options);
 		expect(mockRunner).toHaveBeenCalledWith("gh config get user -h github.com");
 		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
 		expect(mockNewGitHubRepository).toHaveBeenCalledWith({
@@ -129,6 +211,8 @@ describe("createRepositoryOnGitHub", () => {
 		const owner = "mock-owner";
 		const repository = "mock-repository";
 		const mockRunner = vi.fn().mockResolvedValue(new Error("Oh no!"));
+
+		mockGetGitHubAuthToken.mockResolvedValueOnce({ succeeded: true });
 		mockPromptForOptionSchema.mockResolvedValueOnce(owner);
 
 		const actual = await createRepositoryOnGitHub(
@@ -136,9 +220,10 @@ describe("createRepositoryOnGitHub", () => {
 			{ owner: undefined, repository },
 			mockOctokit,
 			mockRunner,
+			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toEqual({ owner, repository });
+		expect(actual).toEqual(options);
 		expect(mockRunner).toHaveBeenCalledWith("gh config get user -h github.com");
 		expect(mockPromptForOptionSchema).toHaveBeenCalledWith(
 			"owner",
@@ -154,18 +239,20 @@ describe("createRepositoryOnGitHub", () => {
 		});
 	});
 
-	it("returns undefined when creating a repository errors", async () => {
-		const owner = "mock-owner";
-		const repository = "mock-repository";
-		mockNewGitHubRepository.mockRejectedValueOnce(new Error("Oh no!"));
+	it("returns the error when creating a repository errors", async () => {
+		const error = new Error("Oh no!");
+
+		mockGetGitHubAuthToken.mockResolvedValueOnce({ succeeded: true });
+		mockNewGitHubRepository.mockRejectedValueOnce(error);
 
 		const actual = await createRepositoryOnGitHub(
 			display,
-			{ owner, repository },
+			options,
 			mockOctokit,
 			vi.fn(),
+			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toBeUndefined();
+		expect(actual).toBe(error);
 	});
 });