squash-only 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Ryszkiewicz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # squash-only
2
+
3
+ A bash script to automatically configure all your GitHub repositories to use squash-only merge strategy.
4
+
5
+ ## What it does
6
+
7
+ This script updates all repositories you own on GitHub to:
8
+ - ✅ Enable squash merges
9
+ - ❌ Disable merge commits
10
+ - ❌ Disable rebase merges
11
+
12
+ It automatically skips repositories you don't own and provides a summary of the results.
13
+
14
+ ## Requirements
15
+
16
+ - `bash` (version 4+)
17
+ - `curl`
18
+ - `jq`
19
+ - GitHub authentication (see below)
20
+
21
+ **Note:** If using the Node.js binary wrapper, you'll also need Node.js installed.
22
+
23
+ ## Authentication
24
+
25
+ The script supports three methods for GitHub authentication (in order of preference):
26
+
27
+ ### 1. Environment Variable
28
+ Set `GITHUB_TOKEN` in your environment:
29
+ ```bash
30
+ export GITHUB_TOKEN=your_token_here
31
+ ./scripts/squash-only.sh
32
+ ```
33
+
34
+ ### 2. GitHub CLI (`gh`)
35
+ If you have the GitHub CLI installed and authenticated:
36
+ ```bash
37
+ gh auth login
38
+ ./scripts/squash-only.sh
39
+ ```
40
+
41
+ ### 3. Personal Access Token (PAT)
42
+ If neither of the above are available, the script will display instructions for creating a PAT with the required permissions.
43
+
44
+ **Required PAT permissions:**
45
+ - `repo` (Full control of private repositories)
46
+
47
+ ## Installation & Usage
48
+
49
+ You can run this tool in several ways:
50
+
51
+ ### Option 1: Using npx (Recommended - No Installation Required)
52
+
53
+ Run directly from the GitHub repository without installing:
54
+ ```bash
55
+ npx github:pRizz/squash-only
56
+ ```
57
+
58
+ With custom sleep interval:
59
+ ```bash
60
+ npx github:pRizz/squash-only --sleep 0.5
61
+ ```
62
+
63
+ **Note:** If the package is published to npm, you can also use:
64
+ ```bash
65
+ npx squash-only
66
+ ```
67
+
68
+ ### Option 2: Using the Bash Script Directly
69
+
70
+ Run the bash script directly:
71
+ ```bash
72
+ ./scripts/squash-only.sh
73
+ ```
74
+
75
+ With custom sleep interval:
76
+ ```bash
77
+ ./scripts/squash-only.sh --sleep 0.5
78
+ # or
79
+ ./scripts/squash-only.sh -s 1.0
80
+ ```
81
+
82
+ ### Option 3: Run Locally with npm (Development)
83
+
84
+ If you've cloned the repository, you can run it locally using npm scripts:
85
+ ```bash
86
+ npm start
87
+ # or
88
+ npm run squash-only
89
+ ```
90
+
91
+ With custom sleep interval:
92
+ ```bash
93
+ npm start -- --sleep 0.5
94
+ ```
95
+
96
+ You can also use `npx` to run the local version:
97
+ ```bash
98
+ npx .
99
+ ```
100
+
101
+ ### Option 4: Install Globally via npm/pnpm
102
+
103
+ Install the package globally:
104
+ ```bash
105
+ npm install -g squash-only
106
+ # or
107
+ pnpm install -g squash-only
108
+ ```
109
+
110
+ Then run it from anywhere:
111
+ ```bash
112
+ squash-only
113
+ ```
114
+
115
+ ## Usage Examples
116
+
117
+ ### Basic usage
118
+ ```bash
119
+ # Using npx (recommended)
120
+ npx github:pRizz/squash-only
121
+
122
+ # Or run locally with npm
123
+ npm start
124
+
125
+ # Or using the bash script directly
126
+ ./scripts/squash-only.sh
127
+ ```
128
+
129
+ ### Custom sleep interval
130
+ Control the delay between API requests (default: 0.2 seconds):
131
+ ```bash
132
+ # Using npx
133
+ npx github:pRizz/squash-only --sleep 0.5
134
+
135
+ # Or run locally with npm
136
+ npm start -- --sleep 0.5
137
+
138
+ # Or using the bash script
139
+ ./scripts/squash-only.sh --sleep 0.5
140
+ # or
141
+ ./scripts/squash-only.sh -s 1.0
142
+ ```
143
+
144
+ ## Options
145
+
146
+ - `-s, --sleep SECONDS` - Set the sleep interval between API requests (must be a number)
147
+
148
+ ## Features
149
+
150
+ - 🔐 **Automatic authentication** - Tries multiple authentication methods
151
+ - 📄 **Pagination support** - Handles users with 100+ repositories
152
+ - 🔍 **Ownership filtering** - Only updates repositories you own
153
+ - 📊 **Progress tracking** - Shows success, skipped, and failed counts
154
+ - ⏱️ **Performance metrics** - Displays elapsed time
155
+ - 🛡️ **Error handling** - Validates inputs and provides clear error messages
156
+
157
+ ## Output
158
+
159
+ The script provides:
160
+ - Real-time progress updates for each repository
161
+ - Success/failure status for each update
162
+ - A summary at the end showing:
163
+ - Number of successfully updated repositories
164
+ - Number of skipped repositories (not owned by you)
165
+ - Number of failed updates (if any)
166
+ - Total elapsed time
167
+
168
+ Example output:
169
+ ```
170
+ ────────────────────────────────────
171
+ Summary:
172
+ ✅ Successfully updated: 15
173
+ ⏭️ Skipped: 3
174
+ ⏱️ Elapsed time: 2m 30s
175
+ ```
176
+
177
+ ## Examples
178
+
179
+ Update all your repos with default settings:
180
+ ```bash
181
+ # Using npx (recommended)
182
+ npx github:pRizz/squash-only
183
+
184
+ # Or run locally with npm
185
+ npm start
186
+
187
+ # Or using the bash script
188
+ ./scripts/squash-only.sh
189
+ ```
190
+
191
+ Update with a longer delay between requests:
192
+ ```bash
193
+ # Using npx
194
+ npx github:pRizz/squash-only --sleep 1.0
195
+
196
+ # Or using the bash script
197
+ ./scripts/squash-only.sh --sleep 1.0
198
+ ```
199
+
200
+ Use with environment variable:
201
+ ```bash
202
+ # Using npx
203
+ GITHUB_TOKEN=ghp_xxxxx npx github:pRizz/squash-only
204
+
205
+ # Or run locally with npm
206
+ GITHUB_TOKEN=ghp_xxxxx npm start
207
+
208
+ # Or using the bash script
209
+ GITHUB_TOKEN=ghp_xxxxx ./scripts/squash-only.sh
210
+ ```
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const scriptPath = path.join(__dirname, '..', 'scripts', 'squash-only.sh');
8
+
9
+ // Check if script exists
10
+ if (!fs.existsSync(scriptPath)) {
11
+ console.error(`❌ Error: Script not found at ${scriptPath}`);
12
+ process.exit(1);
13
+ }
14
+
15
+ // Get all arguments except node and script name
16
+ const args = process.argv.slice(2);
17
+
18
+ // Spawn the bash script with all arguments
19
+ const child = spawn('bash', [scriptPath, ...args], {
20
+ stdio: 'inherit',
21
+ cwd: path.join(__dirname, '..'),
22
+ });
23
+
24
+ let isExiting = false;
25
+
26
+ // Handle termination signals (Ctrl-C, etc.)
27
+ const handleSignal = (signal) => {
28
+ if (isExiting) {
29
+ return;
30
+ }
31
+ isExiting = true;
32
+
33
+ // Forward the signal to the child process
34
+ if (child && !child.killed) {
35
+ child.kill(signal);
36
+ }
37
+ };
38
+
39
+ process.on('SIGINT', handleSignal);
40
+ process.on('SIGTERM', handleSignal);
41
+
42
+ // Cleanup on process exit
43
+ process.on('exit', () => {
44
+ if (child && !child.killed) {
45
+ child.kill('SIGTERM');
46
+ }
47
+ });
48
+
49
+ child.on('error', (error) => {
50
+ console.error(`❌ Error executing script: ${error.message}`);
51
+ process.exit(1);
52
+ });
53
+
54
+ child.on('exit', (code, signal) => {
55
+ // If child was killed by a signal, exit with appropriate code
56
+ if (signal) {
57
+ process.exit(signal === 'SIGINT' ? 130 : 143);
58
+ } else {
59
+ process.exit(code || 0);
60
+ }
61
+ });
62
+
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "squash-only",
3
+ "version": "1.0.0",
4
+ "description": "Scripts for making all your repos on GitHub Squash Only!",
5
+ "bin": {
6
+ "squash-only": "./bin/squash-only.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "scripts",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=12.0.0"
16
+ },
17
+ "preferGlobal": true,
18
+ "scripts": {
19
+ "start": "node bin/squash-only.js",
20
+ "squash-only": "node bin/squash-only.js",
21
+ "test": "echo \"Error: no test specified\" && exit 1"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/pRizz/squash-only.git"
26
+ },
27
+ "keywords": [
28
+ "squash",
29
+ "github",
30
+ "merge",
31
+ "squash-merge",
32
+ "cli",
33
+ "automation",
34
+ "git"
35
+ ],
36
+ "author": "Peter Ryszkiewicz",
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/pRizz/squash-only/issues"
40
+ },
41
+ "homepage": "https://github.com/pRizz/squash-only#readme"
42
+ }
@@ -0,0 +1,260 @@
1
+ #!/bin/bash
2
+ set -uo pipefail
3
+
4
+ SLEEP_SECS=0.2
5
+ SHOULD_EXIT=0
6
+
7
+ # Temporary files for tracking counts across subshells
8
+ SUCCESS_COUNT_FILE=$(mktemp)
9
+ SKIP_COUNT_FILE=$(mktemp)
10
+ FAILED_COUNT_FILE=$(mktemp)
11
+
12
+ cleanup() {
13
+ rm -f "$SUCCESS_COUNT_FILE" "$SKIP_COUNT_FILE" "$FAILED_COUNT_FILE"
14
+ }
15
+ trap cleanup EXIT
16
+
17
+ # Handle termination signals
18
+ handle_exit() {
19
+ SHOULD_EXIT=1
20
+ echo ""
21
+ echo "⚠️ Interrupted. Cleaning up..."
22
+ cleanup
23
+ exit 130
24
+ }
25
+ trap handle_exit INT TERM
26
+
27
+ get_github_token() {
28
+ # Check if GITHUB_TOKEN is already in environment
29
+ if [ -n "${GITHUB_TOKEN:-}" ]; then
30
+ echo "$GITHUB_TOKEN"
31
+ return 0
32
+ fi
33
+
34
+ # Check if gh CLI is installed
35
+ if ! command -v gh &> /dev/null; then
36
+ print_pat_instructions
37
+ return 1
38
+ fi
39
+
40
+ # Try to get token from gh CLI
41
+ local maybeToken
42
+ maybeToken=$(gh auth token 2>/dev/null)
43
+ if [ -n "$maybeToken" ]; then
44
+ echo "$maybeToken"
45
+ return 0
46
+ fi
47
+
48
+ # gh is installed but no token available
49
+ print_pat_instructions
50
+ return 1
51
+ }
52
+
53
+ print_pat_instructions() {
54
+ cat << 'EOF'
55
+
56
+ To use this script, you need a GitHub Personal Access Token (PAT).
57
+
58
+ 1. Go to https://github.com/settings/tokens/new
59
+ 2. Give your token a descriptive name (e.g., "Squash Only Script")
60
+ 3. Set an expiration (recommended: 90 days or custom)
61
+ 4. Select the following permissions:
62
+ - repo (Full control of private repositories)
63
+ - This includes: repo:status, repo_deployment, public_repo, repo:invite, security_events
64
+ 5. Click "Generate token"
65
+ 6. Copy the token and run this script with:
66
+ GITHUB_TOKEN=your_token_here ./scripts/squash-only.sh
67
+
68
+ Alternatively, you can install the GitHub CLI (gh) and authenticate:
69
+ brew install gh
70
+ gh auth login
71
+
72
+ EOF
73
+ }
74
+
75
+ increment_counter() {
76
+ local counter_file=$1
77
+ local current
78
+ current=$(cat "$counter_file")
79
+ echo $((current + 1)) > "$counter_file"
80
+ }
81
+
82
+ process_repo() {
83
+ local repo=$1
84
+ local owner=$2
85
+
86
+ # Check if we should exit
87
+ [ "$SHOULD_EXIT" -eq 1 ] && return 1
88
+
89
+ if [ -z "$repo" ] || [ "$repo" = "null" ]; then
90
+ return 0
91
+ fi
92
+
93
+ if [ "$owner" != "$GITHUB_USER" ]; then
94
+ echo "────────────────────────────────────"
95
+ echo "⏭️ Skipping repo: $repo (owned by $owner)"
96
+ increment_counter "$SKIP_COUNT_FILE"
97
+ return 0
98
+ fi
99
+
100
+ echo "────────────────────────────────────"
101
+ echo "Updating repo: $repo"
102
+
103
+ local http_code
104
+ http_code=$(curl -s --max-time 30 -o /dev/null -w "%{http_code}" \
105
+ -X PATCH \
106
+ -H "Authorization: token $GITHUB_TOKEN" \
107
+ -H "Accept: application/vnd.github+json" \
108
+ https://api.github.com/repos/$repo \
109
+ -d '{
110
+ "allow_merge_commit": false,
111
+ "allow_rebase_merge": false,
112
+ "allow_squash_merge": true
113
+ }')
114
+
115
+ # Check if we should exit after curl
116
+ [ "$SHOULD_EXIT" -eq 1 ] && return 1
117
+
118
+ if [ "$http_code" -eq 200 ]; then
119
+ echo "✅ Success ($http_code)"
120
+ echo " https://github.com/$repo"
121
+ increment_counter "$SUCCESS_COUNT_FILE"
122
+ else
123
+ echo "❌ Failed ($http_code)"
124
+ increment_counter "$FAILED_COUNT_FILE"
125
+ fi
126
+
127
+ # Check again before sleeping
128
+ [ "$SHOULD_EXIT" -eq 1 ] && return 1
129
+
130
+ echo "Sleeping ${SLEEP_SECS}s…"
131
+ sleep "$SLEEP_SECS"
132
+ }
133
+
134
+ fetch_all_repos() {
135
+ local page=1
136
+ local per_page=100
137
+
138
+ while true; do
139
+ # Check if we should exit
140
+ [ "$SHOULD_EXIT" -eq 1 ] && break
141
+
142
+ local body
143
+ body=$(curl -s --max-time 30 -H "Authorization: token $GITHUB_TOKEN" \
144
+ "https://api.github.com/user/repos?per_page=$per_page&page=$page")
145
+
146
+ # Check if we should exit after curl
147
+ [ "$SHOULD_EXIT" -eq 1 ] && break
148
+
149
+ local repo_count
150
+ repo_count=$(echo "$body" | jq '. | length')
151
+ if [ -z "$repo_count" ] || [ "$repo_count" = "0" ] || [ "$repo_count" = "null" ]; then
152
+ break
153
+ fi
154
+
155
+ echo "$body" | jq -r '.[] | "\(.full_name)|\(.owner.login)"' | while IFS='|' read -r repo owner; do
156
+ [ "$SHOULD_EXIT" -eq 1 ] && break
157
+ process_repo "$repo" "$owner" || break
158
+ done
159
+
160
+ [ "$SHOULD_EXIT" -eq 1 ] && break
161
+
162
+ if [ "$repo_count" -lt "$per_page" ]; then
163
+ break
164
+ fi
165
+
166
+ page=$((page + 1))
167
+ done
168
+ }
169
+
170
+ is_number() {
171
+ local value=$1
172
+ if [[ "$value" =~ ^[0-9]+\.?[0-9]*$ ]]; then
173
+ return 0
174
+ fi
175
+ return 1
176
+ }
177
+
178
+ parse_args() {
179
+ while [[ $# -gt 0 ]]; do
180
+ case $1 in
181
+ -s|--sleep)
182
+ if [ -z "${2:-}" ]; then
183
+ echo "❌ Error: --sleep requires a value"
184
+ exit 1
185
+ fi
186
+ if ! is_number "$2"; then
187
+ echo "❌ Error: --sleep value must be a number (got: $2)"
188
+ exit 1
189
+ fi
190
+ SLEEP_SECS="$2"
191
+ shift 2
192
+ ;;
193
+ *)
194
+ echo "❌ Error: Unknown option: $1"
195
+ echo "Usage: $0 [--sleep SECONDS]"
196
+ exit 1
197
+ ;;
198
+ esac
199
+ done
200
+ }
201
+
202
+ main() {
203
+ parse_args "$@"
204
+
205
+ # Initialize counters
206
+ echo "0" > "$SUCCESS_COUNT_FILE"
207
+ echo "0" > "$SKIP_COUNT_FILE"
208
+ echo "0" > "$FAILED_COUNT_FILE"
209
+
210
+ echo "Updating all your repos to Squash Only!"
211
+ echo "→ Disables merge commits"
212
+ echo "→ Disables rebase merges"
213
+ echo "→ Enables squash merges"
214
+
215
+ echo "→ Logging in to GitHub…"
216
+ GITHUB_TOKEN=$(get_github_token)
217
+ if [ $? -ne 0 ]; then
218
+ exit 1
219
+ fi
220
+
221
+ echo "Fetching your username…"
222
+ GITHUB_USER=$(curl -s --max-time 30 -H "Authorization: token $GITHUB_TOKEN" \
223
+ https://api.github.com/user | jq -r '.login')
224
+
225
+ # Check if we should exit
226
+ [ "$SHOULD_EXIT" -eq 1 ] && exit 130
227
+
228
+ if [ -z "$GITHUB_USER" ] || [ "$GITHUB_USER" = "null" ]; then
229
+ echo "❌ Failed to get GitHub username"
230
+ exit 1
231
+ fi
232
+
233
+ echo "Fetching your repos…"
234
+ START_TIME=$(date +%s)
235
+ fetch_all_repos
236
+ END_TIME=$(date +%s)
237
+
238
+ ELAPSED=$((END_TIME - START_TIME))
239
+ ELAPSED_MIN=$((ELAPSED / 60))
240
+ ELAPSED_SEC=$((ELAPSED % 60))
241
+
242
+ echo ""
243
+ echo "────────────────────────────────────"
244
+ echo "Summary:"
245
+ SUCCESS_COUNT=$(cat "$SUCCESS_COUNT_FILE")
246
+ SKIP_COUNT=$(cat "$SKIP_COUNT_FILE")
247
+ FAILED_COUNT=$(cat "$FAILED_COUNT_FILE")
248
+ echo " ✅ Successfully updated: $SUCCESS_COUNT"
249
+ echo " ⏭️ Skipped: $SKIP_COUNT"
250
+ if [ "$FAILED_COUNT" -gt 0 ]; then
251
+ echo " ❌ Failed: $FAILED_COUNT"
252
+ fi
253
+ if [ "$ELAPSED_MIN" -gt 0 ]; then
254
+ echo " ⏱️ Elapsed time: ${ELAPSED_MIN}m ${ELAPSED_SEC}s"
255
+ else
256
+ echo " ⏱️ Elapsed time: ${ELAPSED}s"
257
+ fi
258
+ }
259
+
260
+ main "$@"