Repository: JoshuaKGoldberg/bingo (ts)

Issue:
🐛 Bug: Script merging loses `silent` attributes on mixed-silence phases
### 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/bingo/issues?q=is%3Aissue) and found none that matched my issue.

### Expected

If a phase contains both `silent` and non-`silent` scripts, they should preserve their `silent` attribute.

### Actual

Running `npx create-typescript-app@beta` on an existing repo:

```plaintext
◐  Running create-typescript-appError in script rm CONTRIBUTING.md ExecaError: Command failed with exit code 1: rm CONTRIBUTING.md

rm: CONTRIBUTING.md: No such file or directory
    at getFinalError (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/execa@9.5.2/node_modules/execa/lib/return/final-error.js:6:9)
    at makeError (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/execa@9.5.2/node_modules/execa/lib/return/result.js:108:16)
    at getAsyncResult (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/execa@9.5.2/node_modules/execa/lib/methods/main-async.js:167:4)
    at handlePromise (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/execa@9.5.2/node_modules/execa/lib/methods/main-async.js:150:17)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async runCommand (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/bingo@0.5.1/node_modules/bingo/lib/runners/applyScriptsToSystem.js:10:24)
    at async file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/bingo@0.5.1/node_modules/bingo/lib/runners/applyScriptsToSystem.js:22:17
    at async Promise.all (index 0)
    at async applyScriptsToSystem (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/bingo@0.5.1/node_modules/bingo/lib/runners/applyScriptsToSystem.js:20:9)
    at async Promise.all (index 0) {
  shortMessage: 'Command failed with exit code 1: rm CONTRIBUTING.md',
  command: 'rm CONTRIBUTING.md',
  escapedCommand: 'rm CONTRIBUTING.md',
  cwd: '/Users/josh/repos/my-typescript-app',
  durationMs: 16.854666,
  failed: true,
  timedOut: false,
  isCanceled: false,
  isGracefullyCanceled: false,
  isTerminated: false,
  isMaxBuffer: false,
  isForcefullyTerminated: false,
  exitCode: 1,
  stdout: '',
  stderr: 'rm: CONTRIBUTING.md: No such file or directory',
  stdio: [ undefined, '', 'rm: CONTRIBUTING.md: No such file or directory' ],
  ipcOutput: [],
  pipedFrom: []
}
Error in script rm CODE_OF_CONDUCT.md ExecaError: Command failed with exit code 1: rm CODE_OF_CONDUCT.md
```

### Additional Info

💝 

Pull request description:
fix(bingo): preserve silent mode when merging script commands
## PR Checklist

- [x] Addresses an existing open issue: fixes #239
- [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

Previously, the `silent` attribute was preserved at the `phase` level. If any script in a phase wasn't `silent` then the entire `phase` wouldn't be either (`nonSilentPhases`). In other words, _all_ scripts in a `phase` needed to be `silent` for the `phase` to be.

Now, when multiple script objects with the same `phase` are merged, the `silent` attribute factors in `phase` for its caching. 

💝 

Patch:
diff --git a/packages/bingo/src/mergers/mergeScripts.ts b/packages/bingo/src/mergers/mergeScripts.ts
index 44f62d9..0c47e33 100644
--- a/packages/bingo/src/mergers/mergeScripts.ts
+++ b/packages/bingo/src/mergers/mergeScripts.ts
@@ -1,6 +1,6 @@
 import hashObject from "hash-object";
 
-import { CreatedScript } from "../types/creations.js";
+import { CreatedScript, CreatedScriptWithOptions } from "../types/creations.js";
 
 export function mergeScripts(
 	first: CreatedScript[],
@@ -8,7 +8,7 @@ export function mergeScripts(
 ): CreatedScript[] {
 	const commandsByPhase = new Map<number | undefined, string[][]>();
 	const commandsWithoutPhase: string[] = [];
-	const nonSilentPhases = new Set<number | undefined>();
+	const nonSilentPhaseScripts = new Set<string>();
 
 	for (const command of [...first, ...second]) {
 		if (typeof command === "string") {
@@ -17,7 +17,7 @@ export function mergeScripts(
 		}
 
 		if (!command.silent) {
-			nonSilentPhases.add(command.phase);
+			nonSilentPhaseScripts.add(makeSilenceHash(command));
 		}
 
 		const byPhase = commandsByPhase.get(command.phase);
@@ -47,7 +47,11 @@ export function mergeScripts(
 				.map((commands) => ({
 					commands,
 					phase,
-					...(!nonSilentPhases.has(phase) && { silent: true }),
+					...(!nonSilentPhaseScripts.has(
+						makeSilenceHash({ commands, phase }),
+					) && {
+						silent: true,
+					}),
 				}))
 				.filter((phaseCommand) => {
 					const hash = hashObject(phaseCommand);
@@ -66,3 +70,7 @@ export function mergeScripts(
 function firstHasSameStart(first: string[], second: string[]) {
 	return first.every((entry, i) => entry === second[i]);
 }
+
+function makeSilenceHash(command: CreatedScriptWithOptions) {
+	return hashObject([command.phase, command.commands]);
+}


Test patch:
diff --git a/packages/bingo/src/mergers/mergeScripts.test.ts b/packages/bingo/src/mergers/mergeScripts.test.ts
index 6162d96..1271e3a 100644
--- a/packages/bingo/src/mergers/mergeScripts.test.ts
+++ b/packages/bingo/src/mergers/mergeScripts.test.ts
@@ -4,19 +4,6 @@ import { mergeScripts } from "./mergeScripts.js";
 
 describe("mergeScripts", () => {
 	test.each([
-		[
-			[
-				{ commands: ["rm CONTRIBUTING.md"], phase: 0 },
-				{ commands: ["rm CODE_OF_CONDUCT.md"], phase: 0 },
-				{ commands: ["rm DEVELOPMENT.md"], phase: 0 },
-			],
-			[{ commands: ["rm DEVELOPMENT.md"], phase: 0 }],
-			[
-				{ commands: ["rm CONTRIBUTING.md"], phase: 0 },
-				{ commands: ["rm CODE_OF_CONDUCT.md"], phase: 0 },
-				{ commands: ["rm DEVELOPMENT.md"], phase: 0 },
-			],
-		],
 		[[], [], []],
 		[[], ["a"], ["a"]],
 		[["a"], [], ["a"]],
@@ -364,6 +351,72 @@ describe("mergeScripts", () => {
 				},
 			],
 		],
+		[
+			[
+				{ commands: ["rm CODE_OF_CONDUCT.md"], phase: 0 },
+				{ commands: ["rm DEVELOPMENT.md"], phase: 0 },
+			],
+			[{ commands: ["rm DEVELOPMENT.md"], phase: 0 }],
+			[
+				{ commands: ["rm CODE_OF_CONDUCT.md"], phase: 0 },
+				{ commands: ["rm DEVELOPMENT.md"], phase: 0 },
+			],
+		],
+		[
+			[
+				{ commands: ["rm CONTRIBUTING.md"], phase: 0 },
+				{ commands: ["rm CODE_OF_CONDUCT.md"], phase: 0 },
+				{ commands: ["rm DEVELOPMENT.md"], phase: 0 },
+			],
+			[{ commands: ["rm DEVELOPMENT.md"], phase: 0 }],
+			[
+				{ commands: ["rm CONTRIBUTING.md"], phase: 0 },
+				{ commands: ["rm CODE_OF_CONDUCT.md"], phase: 0 },
+				{ commands: ["rm DEVELOPMENT.md"], phase: 0 },
+			],
+		],
+		[
+			[
+				{ commands: ["rm CONTRIBUTING.md"], phase: 0, silent: true },
+				{ commands: ["pnpm install"], phase: 1 },
+			],
+			[{ commands: ["rm .prettierrc*"], phase: 0 }],
+			[
+				{ commands: ["rm CONTRIBUTING.md"], phase: 0, silent: true },
+				{ commands: ["rm .prettierrc*"], phase: 0 },
+				{ commands: ["pnpm install"], phase: 1 },
+			],
+		],
+		[
+			[
+				{ commands: ["rm CONTRIBUTING.md"], phase: 0, silent: true },
+				{ commands: ["rm .eslintrc*"], phase: 0, silent: true },
+			],
+			[{ commands: ["rm .prettierrc* prettier.config*"], phase: 0 }],
+			[
+				{ commands: ["rm CONTRIBUTING.md"], phase: 0, silent: true },
+				{ commands: ["rm .eslintrc*"], phase: 0, silent: true },
+				{ commands: ["rm .prettierrc* prettier.config*"], phase: 0 },
+			],
+		],
+		[
+			[
+				{ commands: ["rm CONTRIBUTING.md"], phase: 0, silent: true },
+				{ commands: ["rm .eslintrc*"], phase: 0, silent: true },
+				{ commands: ["pnpm install", "pnpm dedupe --offline"], phase: 1 },
+			],
+			[
+				{ commands: ["pnpm format --write"], phase: 2 },
+				{ commands: ["rm .prettierrc* prettier.config*"], phase: 0 },
+			],
+			[
+				{ commands: ["rm CONTRIBUTING.md"], phase: 0, silent: true },
+				{ commands: ["rm .eslintrc*"], phase: 0, silent: true },
+				{ commands: ["rm .prettierrc* prettier.config*"], phase: 0 },
+				{ commands: ["pnpm install", "pnpm dedupe --offline"], phase: 1 },
+				{ commands: ["pnpm format --write"], phase: 2 },
+			],
+		],
 	])("%j and %j", (first, second, expected) => {
 		const actual = mergeScripts(first, second);