Repository: JoshuaKGoldberg/bingo (ts)

Issue:
🚀 Feature: Default to --offline if template doesn't define options.owner and options.repository
### 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

I'm going through the _Your First Template_ guide in #246 and am hitting a snag with the first run:

```plaintext
$ npx bingo template.js --directory generated
┌  ✨ bingo@0.5.15 ✨
│
◇  Imported ./template.js
│
◇  Running with mode --setup using the template:
│    bingo template.js
│
◇  Inferred default options from system
│
■  Error: To run with --mode setup and not --offline, options.owner and options.repository must be string-like schemas.
│
└  Operation cancelled. Exiting - maybe another time? 👋
```

Instead of erroring about not being `--offline`, the Bingo CLI should just set `--offline` and warn.

### Additional Info

💝 
🚀 Feature: Simplify starting log text
### 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

Running `npx bingo template.js` or `npx .` per #246's guide shows some unnecessary text:

```plaintext
┌  ✨ my-template@0.0.0 ✨
│
│  Running with mode --transition using the template:
│    my-template
...
```

We don't need a second, duplicate log of the template. It's a holdover from the days of `npx bingo typescript-app` being the intended design for end-users.

### Additional Info

I suppose users may eventually want an explicit indication of a `create-*.config.js` file being used. I'm hoping that's not a common request anytime soon, given how much work is going into making those files unnecessary.

💝 

Pull request description:
feat: simplified CLI logs and allowed implicit --offline for new templates
## PR Checklist

- [x] Addresses an existing open issue: fixes #356; fixes #357
- [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

💝 

Patch:
diff --git a/packages/bingo/src/cli/loggers/logStartText.ts b/packages/bingo/src/cli/loggers/logStartText.ts
index 6b5554c..42a74c7 100644
--- a/packages/bingo/src/cli/loggers/logStartText.ts
+++ b/packages/bingo/src/cli/loggers/logStartText.ts
@@ -5,16 +5,9 @@ import { ProductionMode } from "../../types/modes.js";
 
 export function logStartText(
 	mode: ProductionMode,
-	from: string,
-	type: string,
 	offline: boolean | undefined,
 ) {
-	prompts.log.step(
-		[
-			`Running with mode --${mode} using the ${type}:`,
-			`  ${chalk.green(from)}`,
-		].join("\n"),
-	);
+	prompts.log.step(`Running with mode --${mode}`);
 
 	if (offline) {
 		prompts.log.message(
diff --git a/packages/bingo/src/cli/runBingoCLI.ts b/packages/bingo/src/cli/runBingoCLI.ts
index 6f34661..2db2508 100644
--- a/packages/bingo/src/cli/runBingoCLI.ts
+++ b/packages/bingo/src/cli/runBingoCLI.ts
@@ -36,7 +36,7 @@ export async function runBingoCLI() {
 		const result = await runCLI({
 			argv,
 			display,
-			from: `bingo ${from}`,
+			from: "bingo",
 			template,
 			values,
 		});
diff --git a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
index c2cdd2d..4396e51 100644
--- a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
+++ b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
@@ -16,45 +16,52 @@ export interface PartialRepositoryLocator {
 	repository: string;
 }
 
+export interface RepositoryCreationResult {
+	offline?: boolean;
+	remote?: Error | RepositoryLocator;
+	warning?: string;
+}
+
 export interface RepositoryLocator extends PartialRepositoryLocator {
 	owner: string;
 }
 
+const offlinePrefix = "Running in local-only mode.";
+
 export async function createRepositoryOnGitHub<
 	OptionsShape extends AnyShape,
 	Refinements,
 >(
 	display: ClackDisplay,
 	{ owner: requestedOwner, repository }: PartialRepositoryLocator,
+	requestedOffline: boolean | undefined,
 	octokit: Octokit,
 	runner: SystemRunner,
 	template: Template<OptionsShape, Refinements>,
-): Promise<Error | RepositoryLocator | undefined> {
+): Promise<RepositoryCreationResult> {
 	const hasStringLikeOwner = hasStringLikeOption(template.options, "owner");
 	const hasStringLikeRepository = hasStringLikeOption(
 		template.options,
 		"repository",
 	);
 
-	if (!hasStringLikeOwner) {
-		return new Error(
-			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 (!hasStringLikeRepository) {
-		return new Error(
-			`To run with --mode setup and not --offline, options.repository must be a string-like schema.`,
-		);
+	if (!hasStringLikeOwner || !hasStringLikeRepository) {
+		return {
+			offline: true,
+			warning: requestedOffline
+				? undefined
+				: `${offlinePrefix} Add string-like options.owner and options.repository schemas to enable creating a repository on GitHub.`,
+		};
 	}
 
 	// 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;
+		return {
+			offline: true,
+			warning: `${offlinePrefix} To push to GitHub, log in with the GitHub CLI (cli.github.com) or run with a GH_TOKEN process environment variable.`,
+		};
 	}
 
 	const ownerRaw =
@@ -82,7 +89,15 @@ export async function createRepositoryOnGitHub<
 		},
 	);
 
-	return error || { owner, repository };
+	return error
+		? {
+				offline: true,
+				remote: error,
+			}
+		: {
+				offline: false,
+				remote: { owner, repository },
+			};
 }
 
 function hasStringLikeOption(options: AnyShape, key: string): boolean {
diff --git a/packages/bingo/src/cli/setup/runModeSetup.ts b/packages/bingo/src/cli/setup/runModeSetup.ts
index ac54eee..34a3be7 100644
--- a/packages/bingo/src/cli/setup/runModeSetup.ts
+++ b/packages/bingo/src/cli/setup/runModeSetup.ts
@@ -43,7 +43,7 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 	display,
 	from,
 	help,
-	offline,
+	offline: requestedOffline,
 	repository: requestedRepository,
 	template,
 
@@ -55,7 +55,7 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 		return logHelpText("setup", from, template);
 	}
 
-	logStartText("setup", from, "template", offline);
+	logStartText("setup", requestedOffline);
 
 	const directory = await promptForDirectory({
 		requestedDirectory,
@@ -69,7 +69,7 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 	const system = await createSystemContextWithAuth({
 		directory,
 		display,
-		offline,
+		offline: requestedOffline,
 	});
 
 	const providedOptions = parseZodArgs(argv, {
@@ -87,7 +87,7 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 			return await prepareOptions(template, {
 				...system,
 				existing: { ...providedOptions, directory },
-				offline,
+				offline: requestedOffline,
 			});
 		},
 	);
@@ -111,12 +111,13 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 		return { status: CLIStatus.Cancelled };
 	}
 
-	const remote =
-		offline || !system.fetchers.octokit
-			? undefined
+	const { offline, remote, warning } =
+		requestedOffline || !system.fetchers.octokit
+			? { offline: true, remote: undefined }
 			: await createRepositoryOnGitHub(
 					display,
 					{ repository, ...baseOptions.completed },
+					requestedOffline,
 					system.fetchers.octokit,
 					system.runner,
 					template,
@@ -127,10 +128,8 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 		return { error: remote, status: CLIStatus.Error };
 	}
 
-	if (!offline && !remote) {
-		prompts.log.warn(
-			"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.",
-		);
+	if (warning) {
+		prompts.log.warn(warning);
 	}
 
 	const descriptor = template.about?.name ?? from;
diff --git a/packages/bingo/src/cli/transition/runModeTransition.ts b/packages/bingo/src/cli/transition/runModeTransition.ts
index 95bb566..92e0b62 100644
--- a/packages/bingo/src/cli/transition/runModeTransition.ts
+++ b/packages/bingo/src/cli/transition/runModeTransition.ts
@@ -52,9 +52,7 @@ export async function runModeTransition<
 		return logHelpText("transition", from, template);
 	}
 
-	const transitionType = configFile ? "config file" : "template";
-
-	logStartText("transition", from, transitionType, offline);
+	logStartText("transition", offline);
 
 	const system = await createSystemContextWithAuth({
 		directory,


Test patch:
diff --git a/packages/bingo/src/cli/loggers/logStartText.test.ts b/packages/bingo/src/cli/loggers/logStartText.test.ts
index 6d8d12d..52a7a57 100644
--- a/packages/bingo/src/cli/loggers/logStartText.test.ts
+++ b/packages/bingo/src/cli/loggers/logStartText.test.ts
@@ -20,20 +20,19 @@ vi.mock("@clack/prompts", () => ({
 
 describe("logStartText", () => {
 	it("only logs an initial message when offline is falsy", () => {
-		logStartText("transition", "from", "type", false);
+		logStartText("transition", false);
 
 		expect(mockStep.mock.calls).toMatchInlineSnapshot(`
 			[
 			  [
-			    "Running with mode --transition using the type:
-			  from",
+			    "Running with mode --transition",
 			  ],
 			]
 		`);
 	});
 
 	it("additionally logs an offline when offline is true", () => {
-		logStartText("transition", "from", "type", true);
+		logStartText("transition", true);
 
 		expect(mockMessage.mock.calls).toMatchInlineSnapshot(`
 			[
@@ -45,8 +44,7 @@ describe("logStartText", () => {
 		expect(mockStep.mock.calls).toMatchInlineSnapshot(`
 			[
 			  [
-			    "Running with mode --transition using the type:
-			  from",
+			    "Running with mode --transition",
 			  ],
 			]
 		`);
diff --git a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.test.ts b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.test.ts
index bdd3bd5..982346e 100644
--- a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.test.ts
+++ b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.test.ts
@@ -50,10 +50,6 @@ const options = {
 	repository: "mock-repository",
 };
 
-const templateWithoutOptions = createTemplate({
-	produce: vi.fn(),
-});
-
 const templateWithOwnerAndRepository = createTemplate({
 	options: { owner: z.string(), repository: z.string() },
 	produce: vi.fn(),
@@ -70,73 +66,53 @@ const templateWithRepository = createTemplate({
 });
 
 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 () => {
+	it("returns a warning when owner is not a string-like", async () => {
 		const mockRunner = vi.fn();
 
 		const actual = await createRepositoryOnGitHub(
 			display,
 			options,
+			false,
 			mockOctokit,
 			mockRunner,
 			templateWithRepository,
 		);
 
-		expect(actual).toEqual(
-			new Error(
-				`To run with --mode setup and not --offline, options.owner must be a string-like schema.`,
-			),
-		);
+		expect(actual).toEqual({
+			offline: true,
+			warning:
+				"Running in local-only mode. Add string-like options.owner and options.repository schemas to enable creating a repository on GitHub.",
+		});
 		expect(mockGetGitHubAuthToken).not.toHaveBeenCalled();
 		expect(mockRunner).not.toHaveBeenCalled();
 		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
 		expect(mockNewGitHubRepository).not.toHaveBeenCalled();
 	});
 
-	it("returns an error when repository is not a string-like", async () => {
+	it("returns a warning when repository is not a string-like", async () => {
 		const mockRunner = vi.fn();
 
 		const actual = await createRepositoryOnGitHub(
 			display,
 			options,
+			false,
 			mockOctokit,
 			mockRunner,
 			templateWithOwner,
 		);
 
-		expect(actual).toEqual(
-			new Error(
-				`To run with --mode setup and not --offline, options.repository must be a string-like schema.`,
-			),
-		);
+		expect(actual).toEqual({
+			offline: true,
+			warning:
+				"Running in local-only mode. Add string-like options.owner and options.repository schemas to enable creating a repository on GitHub.",
+		});
 		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 () => {
+	it("returns an offline warning when getGitHubAuthToken does not retrieve a token", async () => {
 		const mockRunner = vi.fn();
 
 		mockGetGitHubAuthToken.mockResolvedValueOnce({ succeeded: false });
@@ -144,12 +120,17 @@ describe("createRepositoryOnGitHub", () => {
 		const actual = await createRepositoryOnGitHub(
 			display,
 			options,
+			false,
 			mockOctokit,
 			mockRunner,
 			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toBeUndefined();
+		expect(actual).toEqual({
+			offline: true,
+			warning:
+				"Running in local-only mode. To push to GitHub, log in with the GitHub CLI (cli.github.com) or run with a GH_TOKEN process environment variable.",
+		});
 		expect(mockRunner).not.toHaveBeenCalled();
 		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
 		expect(mockNewGitHubRepository).not.toHaveBeenCalled();
@@ -165,12 +146,16 @@ describe("createRepositoryOnGitHub", () => {
 		const actual = await createRepositoryOnGitHub(
 			display,
 			options,
+			false,
 			mockOctokit,
 			mockRunner,
 			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toEqual(options);
+		expect(actual).toEqual({
+			offline: false,
+			remote: { owner, repository },
+		});
 		expect(mockRunner).not.toHaveBeenCalled();
 		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
 		expect(mockNewGitHubRepository).toHaveBeenCalledWith({
@@ -191,12 +176,16 @@ describe("createRepositoryOnGitHub", () => {
 		const actual = await createRepositoryOnGitHub(
 			display,
 			{ owner: undefined, repository },
+			false,
 			mockOctokit,
 			mockRunner,
 			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toEqual(options);
+		expect(actual).toEqual({
+			offline: false,
+			remote: { owner, repository },
+		});
 		expect(mockRunner).toHaveBeenCalledWith("gh config get user -h github.com");
 		expect(mockPromptForOptionSchema).not.toHaveBeenCalled();
 		expect(mockNewGitHubRepository).toHaveBeenCalledWith({
@@ -218,12 +207,16 @@ describe("createRepositoryOnGitHub", () => {
 		const actual = await createRepositoryOnGitHub(
 			display,
 			{ owner: undefined, repository },
+			false,
 			mockOctokit,
 			mockRunner,
 			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toEqual(options);
+		expect(actual).toEqual({
+			offline: false,
+			remote: { owner, repository },
+		});
 		expect(mockRunner).toHaveBeenCalledWith("gh config get user -h github.com");
 		expect(mockPromptForOptionSchema).toHaveBeenCalledWith(
 			"owner",
@@ -248,11 +241,15 @@ describe("createRepositoryOnGitHub", () => {
 		const actual = await createRepositoryOnGitHub(
 			display,
 			options,
+			false,
 			mockOctokit,
 			vi.fn(),
 			templateWithOwnerAndRepository,
 		);
 
-		expect(actual).toBe(error);
+		expect(actual).toEqual({
+			offline: true,
+			remote: error,
+		});
 	});
 });
diff --git a/packages/bingo/src/cli/transition/runModeTransition.test.ts b/packages/bingo/src/cli/transition/runModeTransition.test.ts
index f3e8303..c19a5bd 100644
--- a/packages/bingo/src/cli/transition/runModeTransition.test.ts
+++ b/packages/bingo/src/cli/transition/runModeTransition.test.ts
@@ -287,12 +287,7 @@ describe("runModeTransition", () => {
 			outro: "Done. Enjoy your new repository! 💝",
 			status: CLIStatus.Success,
 		});
-		expect(mockLogStartText).toHaveBeenCalledWith(
-			"transition",
-			from,
-			"template",
-			undefined,
-		);
+		expect(mockLogStartText).toHaveBeenCalledWith("transition", undefined);
 		expect(mockClearTemplateFiles).toHaveBeenCalled();
 		expect(mockClearLocalGitTags).toHaveBeenCalled();
 		expect(mockCreateInitialCommit).toHaveBeenCalledWith(mockSystem.runner, {
@@ -324,12 +319,7 @@ describe("runModeTransition", () => {
 			outro: "Done. Enjoy your new repository! 💝",
 			status: CLIStatus.Success,
 		});
-		expect(mockLogStartText).toHaveBeenCalledWith(
-			"transition",
-			from,
-			"template",
-			true,
-		);
+		expect(mockLogStartText).toHaveBeenCalledWith("transition", true);
 		expect(mockClearTemplateFiles).toHaveBeenCalled();
 		expect(mockClearLocalGitTags).toHaveBeenCalled();
 		expect(mockCreateInitialCommit).toHaveBeenCalledWith(mockSystem.runner, {