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

Issue:
📝 Documentation: It's "pre-commit", not "precommit"
### Bug Report Checklist

- [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.

### Overview

The file is `.husky/pre-commit`:

https://github.com/JoshuaKGoldberg/create-typescript-app/blob/e65eae3e00d797ce0d88b5a1eb9707c857c66944/docs/FAQs.md?plain=1#L91

This explains why it's showing up in `cspell.json`'s `words`.

### Additional Info

Split out of #1670.

:sparkling

Pull request description:
docs: correct pre(-)commit typo
## PR Checklist

- [x] Addresses an existing open issue: fixes #1713
- [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

Removes `precommit` from `cspell.json`, hooray.

💖 

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dc1a7b4..d831a4c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,6 +1,6 @@
 jobs:
   are_the_types_wrong:
-    name: Are the types wrong?
+    name: Are The Types Wrong?
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
diff --git a/cspell.json b/cspell.json
index 336c862..99ddf58 100644
--- a/cspell.json
+++ b/cspell.json
@@ -27,7 +27,6 @@
 		"npmpackagejsonlintrc",
 		"outro",
 		"packagejson",
-		"precommit",
 		"tada",
 		"tseslint",
 		"tsup",
diff --git a/docs/FAQs.md b/docs/FAQs.md
index 31b1d9c..71eb537 100644
--- a/docs/FAQs.md
+++ b/docs/FAQs.md
@@ -88,7 +88,7 @@ Here we'll outline the steps required to migrate a CTA app to a GitHub Action:
    +dist
    ```
 
-   - Rather than having to remember to compile each time, we'll update our precommit hook in `.husky/precommit` to build for us on each commit:
+   - Rather than having to remember to compile each time, we'll update our pre-commit hook in `.husky/pre-commit` to build for us on each commit:
 
    ```diff
    +pnpm run build
diff --git a/docs/Options.md b/docs/Options.md
index d548a04..9b29b51 100644
--- a/docs/Options.md
+++ b/docs/Options.md
@@ -101,6 +101,7 @@ Alternately, you can bypass that prompt by providing any number of the following
 - `--exclude-lint-yml`: Don't apply linting and sorting to `*.yaml` and `*.yml` files.
 - `--exclude-releases`: Don't add release-it to generate changelogs, package bumps, and publishes based on conventional commits.
 - `--exclude-renovate`: Don't add a Renovate config to dependencies up-to-date with PRs.
+- `--exclude-templated-by`: Don't add a _"This package was templated with create-typescript-app"_ notice at the end of the README.md.
 - `--exclude-tests`: Don't add Vitest tooling for fast unit tests, configured with coverage tracking.
 
 For example, initializing with all tooling except for `package.json` checks and Renovate:
diff --git a/package.json b/package.json
index dc6ec7a..72f251f 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
 		"prettier": "^3.4.1",
 		"replace-in-file": "^8.2.0",
 		"rimraf": "^6.0.1",
+		"sort-package-json": "^2.12.0",
 		"title-case": "^4.3.2",
 		"zod": "^3.23.8",
 		"zod-validation-error": "^3.4.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bd35c7b..6250c44 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -53,6 +53,9 @@ importers:
       rimraf:
         specifier: ^6.0.1
         version: 6.0.1
+      sort-package-json:
+        specifier: ^2.12.0
+        version: 2.12.0
       title-case:
         specifier: ^4.3.2
         version: 4.3.2
diff --git a/src/shared/options/args.ts b/src/shared/options/args.ts
index 3c78474..a4325fe 100644
--- a/src/shared/options/args.ts
+++ b/src/shared/options/args.ts
@@ -174,6 +174,11 @@ export const allArgOptions = {
 		docsSection: "opt-out",
 		type: "boolean",
 	},
+	"exclude-templated-by": {
+		description: `Don't add a _"This package was templated with create-typescript-app"_ notice at the end of the README.md.`,
+		docsSection: "opt-out",
+		type: "boolean",
+	},
 	"exclude-tests": {
 		description: `Don't add Vitest tooling for fast unit tests, configured 
   with coverage tracking.`,
diff --git a/src/shared/options/exclusionKeys.ts b/src/shared/options/exclusionKeys.ts
index a15acf2..013d117 100644
--- a/src/shared/options/exclusionKeys.ts
+++ b/src/shared/options/exclusionKeys.ts
@@ -107,6 +107,12 @@ export const exclusionDescriptions: Record<ExclusionKey, ExclusionDescription> =
 			hint: "--exclude-renovate",
 			label: "Add a Renovate config to keep dependencies up-to-date with PRs.",
 		},
+		excludeTemplatedBy: {
+			hint: "--exclude-templated-by",
+			label:
+				'Add a _"This package was templated with create-typescript-app"_ notice at the end of the README.md.',
+			level: "minimal",
+		},
 		excludeTests: {
 			hint: "--exclude-tests",
 			label:
diff --git a/src/shared/options/optionsSchema.ts b/src/shared/options/optionsSchema.ts
index e6a097a..36c3abc 100644
--- a/src/shared/options/optionsSchema.ts
+++ b/src/shared/options/optionsSchema.ts
@@ -39,6 +39,7 @@ export const optionsSchemaShape = {
 	excludeLintYml: z.boolean().optional(),
 	excludeReleases: z.boolean().optional(),
 	excludeRenovate: z.boolean().optional(),
+	excludeTemplatedBy: z.boolean().optional(),
 	excludeTests: z.boolean().optional(),
 	funding: z.string().optional(),
 	guide: z.string().url().optional(),
diff --git a/src/shared/options/readOptions.ts b/src/shared/options/readOptions.ts
index f637451..6fa0f4b 100644
--- a/src/shared/options/readOptions.ts
+++ b/src/shared/options/readOptions.ts
@@ -81,6 +81,7 @@ export async function readOptions(
 		excludeLintYml: values["exclude-lint-yml"],
 		excludeReleases: values["exclude-releases"],
 		excludeRenovate: values["exclude-renovate"],
+		excludeTemplatedBy: values["exclude-templated-by"],
 		excludeTests: values["unit-tests"],
 		funding: values.funding,
 		guide: values.guide,
diff --git a/src/shared/types.ts b/src/shared/types.ts
index a22f944..af6f3f1 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -71,6 +71,7 @@ export interface Options {
 	excludeLintYml?: boolean;
 	excludeReleases?: boolean;
 	excludeRenovate?: boolean;
+	excludeTemplatedBy?: boolean;
 	excludeTests?: boolean;
 	funding?: string;
 	guide?: OptionsGuide;
diff --git a/src/steps/uninstallPackages.ts b/src/steps/uninstallPackages.ts
index ea22f9c..2c10a23 100644
--- a/src/steps/uninstallPackages.ts
+++ b/src/steps/uninstallPackages.ts
@@ -14,14 +14,15 @@ export async function uninstallPackages(offline: boolean | undefined) {
 			"get-github-auth-token",
 			"git-remote-origin-url",
 			"git-url-parse",
-			"lazy-value",
 			"js-yaml",
+			"lazy-value",
 			"npm-user",
-			"parse-author",
-			"rimraf",
 			"octokit",
+			"parse-author",
 			"prettier",
 			"replace-in-file",
+			"rimraf",
+			"sort-package-json",
 			"title-case",
 			"zod",
 			"zod-validation-error",
diff --git a/src/steps/updateReadme.ts b/src/steps/updateReadme.ts
index 6032020..7eb3dab 100644
--- a/src/steps/updateReadme.ts
+++ b/src/steps/updateReadme.ts
@@ -17,12 +17,14 @@ export const endOfReadmeNotice = [
 export const endOfReadmeMatcher =
 	/💙.+(?:based|built|templated).+(?:from|using|on|with).+create-typescript-app/;
 
-export async function updateReadme(options: Pick<Options, "owner">) {
+export async function updateReadme(
+	options: Pick<Options, "excludeTemplatedBy" | "owner">,
+) {
 	let contents = await readFileSafe("./README.md", "");
 
 	contents = contents.replaceAll("JoshuaKGoldberg", options.owner);
 
-	if (!endOfReadmeMatcher.test(contents)) {
+	if (!options.excludeTemplatedBy && !endOfReadmeMatcher.test(contents)) {
 		contents += endOfReadmeNotice;
 	}
 
diff --git a/src/steps/writing/creation/writePackageJson.ts b/src/steps/writing/creation/writePackageJson.ts
index 05980d2..b6b9285 100644
--- a/src/steps/writing/creation/writePackageJson.ts
+++ b/src/steps/writing/creation/writePackageJson.ts
@@ -1,3 +1,5 @@
+import { sortPackageJson } from "sort-package-json";
+
 import { readFileSafeAsJson } from "../../../shared/readFileSafeAsJson.js";
 import { Options, PartialPackageData } from "../../../shared/types.js";
 import { formatJson } from "./formatters/formatJson.js";
@@ -35,81 +37,83 @@ export async function writePackageJson(options: Options) {
 			"./package.json",
 		)) as null | PartialPackageData) ?? {};
 
-	return await formatJson({
-		// If we didn't already have a version, set it to 0.0.0
-		version: "0.0.0",
+	return sortPackageJson(
+		await formatJson({
+			// If we didn't already have a version, set it to 0.0.0
+			version: "0.0.0",
 
-		// To start, copy over all existing package fields (e.g. "dependencies")
-		...existingPackageJson,
+			// To start, copy over all existing package fields (e.g. "dependencies")
+			...existingPackageJson,
 
-		author: { email: options.email.npm, name: options.author },
-		bin: options.bin,
-		description: options.description,
-		keywords: options.keywords?.length
-			? options.keywords.flatMap((keyword) => keyword.split(/ /))
-			: undefined,
+			author: { email: options.email.npm, name: options.author },
+			bin: options.bin,
+			description: options.description,
+			keywords: options.keywords?.length
+				? options.keywords.flatMap((keyword) => keyword.split(/ /))
+				: undefined,
 
-		// We copy all existing dev dependencies except those we know are not used anymore
-		devDependencies: copyDevDependencies(existingPackageJson),
+			// We copy all existing dev dependencies except those we know are not used anymore
+			devDependencies: copyDevDependencies(existingPackageJson),
 
-		// Remove fields we know we don't want, such as old or redundant configs
-		eslintConfig: undefined,
-		husky: undefined,
-		jest: undefined,
-		mocha: undefined,
-		prettierConfig: undefined,
-		types: undefined,
+			// Remove fields we know we don't want, such as old or redundant configs
+			eslintConfig: undefined,
+			husky: undefined,
+			jest: undefined,
+			mocha: undefined,
+			prettierConfig: undefined,
+			types: undefined,
 
-		// The rest of the fields are ones we know from our template
-		engines: {
-			node: ">=18.3.0",
-		},
-		files: [
-			options.bin?.replace(/^\.\//, ""),
-			"lib/",
-			"package.json",
-			"LICENSE.md",
-			"README.md",
-		].filter(Boolean),
-		license: "MIT",
-		"lint-staged": {
-			"*": "prettier --ignore-unknown --write",
-		},
-		main: "./lib/index.js",
-		name: options.repository,
-		publishConfig: {
-			provenance: true,
-		},
-		repository: {
-			type: "git",
-			url: `https://github.com/${options.owner}/${options.repository}`,
-		},
-		scripts: {
-			...existingPackageJson.scripts,
-			...(!options.excludeBuild && {
-				build: "tsup",
-			}),
-			format: "prettier .",
-			lint: "eslint . --max-warnings 0",
-			...(!options.excludeLintKnip && {
-				"lint:knip": "knip",
-			}),
-			...(!options.excludeLintMd && {
-				"lint:md":
-					'markdownlint "**/*.md" ".github/**/*.md" --rules sentences-per-line',
-			}),
-			...(!options.excludeLintPackages && {
-				"lint:packages": "pnpm dedupe --check",
-			}),
-			...(!options.excludeLintSpelling && {
-				"lint:spelling": 'cspell "**" ".github/**/*"',
-			}),
-			prepare: "husky",
-			...(!options.excludeReleases && { test: "vitest" }),
-			tsc: "tsc",
-		},
-		type: "module",
-	});
+			// The rest of the fields are ones we know from our template
+			engines: {
+				node: ">=18.3.0",
+			},
+			files: [
+				options.bin?.replace(/^\.\//, ""),
+				"lib/",
+				"package.json",
+				"LICENSE.md",
+				"README.md",
+			].filter(Boolean),
+			license: "MIT",
+			"lint-staged": {
+				"*": "prettier --ignore-unknown --write",
+			},
+			main: "./lib/index.js",
+			name: options.repository,
+			publishConfig: {
+				provenance: true,
+			},
+			repository: {
+				type: "git",
+				url: `https://github.com/${options.owner}/${options.repository}`,
+			},
+			scripts: {
+				...existingPackageJson.scripts,
+				...(!options.excludeBuild && {
+					build: "tsup",
+				}),
+				format: "prettier .",
+				lint: "eslint . --max-warnings 0",
+				...(!options.excludeLintKnip && {
+					"lint:knip": "knip",
+				}),
+				...(!options.excludeLintMd && {
+					"lint:md":
+						'markdownlint "**/*.md" ".github/**/*.md" --rules sentences-per-line',
+				}),
+				...(!options.excludeLintPackages && {
+					"lint:packages": "pnpm dedupe --check",
+				}),
+				...(!options.excludeLintSpelling && {
+					"lint:spelling": 'cspell "**" ".github/**/*"',
+				}),
+				prepare: "husky",
+				...(!options.excludeTests && { test: "vitest" }),
+				tsc: "tsc",
+			},
+			type: "module",
+		}),
+	);
 }
 
 function copyDevDependencies(existingPackageJson: object) {


Test patch:
diff --git a/script/__snapshots__/migrate-test-e2e.ts.snap b/script/__snapshots__/migrate-test-e2e.ts.snap
index e394b0f..8fe95da 100644
--- a/script/__snapshots__/migrate-test-e2e.ts.snap
+++ b/script/__snapshots__/migrate-test-e2e.ts.snap
@@ -6,7 +6,7 @@ exports[`expected file changes > .github/workflows/ci.yml 1`] = `
 @@ ... @@
  jobs:
 -  are_the_types_wrong:
--    name: Are the types wrong?
+-    name: Are The Types Wrong?
 -    runs-on: ubuntu-latest
 -    steps:
 -      - uses: actions/checkout@v4
@@ -93,10 +93,9 @@ exports[`expected file changes > cspell.json 1`] = `
  		"markdownlintignore",
  		"mtfoley",
  		"npmignore",
- 		"npmpackagejsonlintrc",
+-		"npmpackagejsonlintrc",
  		"outro",
- 		"packagejson",
--		"precommit",
+-		"packagejson",
  		"tada",
  		"tseslint",
  		"tsup",
diff --git a/src/bin/help.test.ts b/src/bin/help.test.ts
index d0956d3..0ce57c9 100644
--- a/src/bin/help.test.ts
+++ b/src/bin/help.test.ts
@@ -271,6 +271,10 @@ describe("logHelpText", () => {
 			  --exclude-renovate: Don't add a Renovate config to dependencies up-to-date with 
 			  PRs.",
 			  ],
+			  [
+			    "
+			  --exclude-templated-by: Don't add a _"This package was templated with create-typescript-app"_ notice at the end of the README.md.",
+			  ],
 			  [
 			    "
 			  --exclude-tests: Don't add Vitest tooling for fast unit tests, configured 
diff --git a/src/create/createRerunSuggestion.test.ts b/src/create/createRerunSuggestion.test.ts
index 7dfbe20..9854ca3 100644
--- a/src/create/createRerunSuggestion.test.ts
+++ b/src/create/createRerunSuggestion.test.ts
@@ -27,6 +27,7 @@ const options = {
 	excludeLintYml: false,
 	excludeReleases: false,
 	excludeRenovate: undefined,
+	excludeTemplatedBy: undefined,
 	excludeTests: undefined,
 	funding: undefined,
 	keywords: ["abc", "def ghi", "jkl mno pqr"],
diff --git a/src/shared/options/augmentOptionsWithExcludes.test.ts b/src/shared/options/augmentOptionsWithExcludes.test.ts
index 63f2a7e..75dd924 100644
--- a/src/shared/options/augmentOptionsWithExcludes.test.ts
+++ b/src/shared/options/augmentOptionsWithExcludes.test.ts
@@ -44,6 +44,7 @@ const optionsBase = {
 	excludeLintYml: undefined,
 	excludeReleases: undefined,
 	excludeRenovate: undefined,
+	excludeTemplatedBy: undefined,
 	excludeTests: undefined,
 	funding: undefined,
 	logo: undefined,
@@ -102,6 +103,7 @@ describe("augmentOptionsWithExcludes", () => {
 			excludeLintYml: true,
 			excludeReleases: true,
 			excludeRenovate: true,
+			excludeTemplatedBy: true,
 			excludeTests: true,
 		});
 	});
diff --git a/src/shared/options/readOptions.test.ts b/src/shared/options/readOptions.test.ts
index 8df87a8..2c6c26d 100644
--- a/src/shared/options/readOptions.test.ts
+++ b/src/shared/options/readOptions.test.ts
@@ -31,6 +31,7 @@ const emptyOptions = {
 	excludeLintYml: undefined,
 	excludeReleases: undefined,
 	excludeRenovate: undefined,
+	excludeTemplatedBy: undefined,
 	excludeTests: undefined,
 	funding: undefined,
 	guide: undefined,
@@ -751,6 +752,7 @@ describe("readOptions", () => {
 			      "excludeLintYml": undefined,
 			      "excludeReleases": undefined,
 			      "excludeRenovate": undefined,
+			      "excludeTemplatedBy": undefined,
 			      "excludeTests": undefined,
 			      "funding": undefined,
 			      "github": "mock.git",
diff --git a/src/steps/updateReadme.test.ts b/src/steps/updateReadme.test.ts
index 135c456..5247063 100644
--- a/src/steps/updateReadme.test.ts
+++ b/src/steps/updateReadme.test.ts
@@ -23,7 +23,7 @@ const options = {
 };
 
 describe("updateReadme", () => {
-	it("adds a notice when the file does not contain it already", async () => {
+	it("adds a notice when the file does not contain it already and excludeTemplatedBy is not enabled", async () => {
 		mockReadFileSafe.mockResolvedValue(
 			"Existing JoshuaKGoldberg/create-typescript-app content.",
 		);
@@ -44,6 +44,23 @@ describe("updateReadme", () => {
 		`);
 	});
 
+	it("doesn't add a notice when excludeTemplatedBy is enabled", async () => {
+		mockReadFileSafe.mockResolvedValue(
+			"Existing JoshuaKGoldberg/create-typescript-app content.",
+		);
+
+		await updateReadme({ ...options, excludeTemplatedBy: true });
+
+		expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(`
+			[
+			  [
+			    "./README.md",
+			    "Existing NewOwner/create-typescript-app content.",
+			  ],
+			]
+		`);
+	});
+
 	it("doesn't add a notice when the file contains it already", async () => {
 		mockReadFileSafe.mockResolvedValue(`
 			Existing JoshuaKGoldberg/create-typescript-app content.
diff --git a/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.test.ts b/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.test.ts
index 66a0962..acb1471 100644
--- a/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.test.ts
+++ b/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.test.ts
@@ -30,6 +30,7 @@ const createOptions = (exclude: boolean) =>
 		excludeLintYml: exclude,
 		excludeReleases: exclude,
 		excludeRenovate: exclude,
+		excludeTemplatedBy: exclude,
 		excludeTests: exclude,
 		mode: "create",
 		owner: "StubOwner",
diff --git a/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts b/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts
index 6eaa1c0..d348abd 100644
--- a/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts
+++ b/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts
@@ -26,6 +26,7 @@ const createOptions = (exclude: boolean) =>
 		excludeLintYml: exclude,
 		excludeReleases: exclude,
 		excludeRenovate: exclude,
+		excludeTemplatedBy: exclude,
 		excludeTests: exclude,
 		mode: "create",
 		owner: "StubOwner",
diff --git a/src/steps/writing/creation/writePackageJson.test.ts b/src/steps/writing/creation/writePackageJson.test.ts
index 1d0864e..68cfcfc 100644
--- a/src/steps/writing/creation/writePackageJson.test.ts
+++ b/src/steps/writing/creation/writePackageJson.test.ts
@@ -135,6 +135,7 @@ describe("writePackageJson", () => {
 			excludeLintYml: true,
 			excludeReleases: true,
 			excludeRenovate: true,
+			excludeTests: true,
 		});
 
 		expect(JSON.parse(packageJson)).toMatchInlineSnapshot(`