git-userhub 3.0.0 → 3.0.1
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/README.md +1 -1
- package/bin/LICENSE +21 -0
- package/bin/README.md +514 -0
- package/bin/git-user +0 -0
- package/bin/git-user.js +150 -11
- package/package.json +1 -2
- package/install.js +0 -169
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<br />
|
|
3
|
-
<img src="https://raw.githubusercontent.com/divyo-argha/git-user/main/img/git-user-logo-clean.png" alt="git-user" width="100" height="100" style="border-radius:
|
|
3
|
+
<img src="https://raw.githubusercontent.com/divyo-argha/git-user/main/img/git-user-logo-clean.png" alt="git-user" width="100" height="100" style="border-radius:28px" />
|
|
4
4
|
<h1>git-user</h1>
|
|
5
5
|
|
|
6
6
|
<p>
|
package/bin/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Divyo Argha
|
|
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/bin/README.md
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<br />
|
|
3
|
+
<img src="img/git-user-logo-clean.png" alt="git-user" width="120" height="120" style="border-radius:26px" />
|
|
4
|
+
<!-- <br /><br /> -->
|
|
5
|
+
<h1>git-user</h1>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<strong>One command to rule all your Git identities.</strong><br />
|
|
9
|
+
Stop committing as the wrong person. Stop juggling SSH keys. Stop editing config files.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p>
|
|
13
|
+
<a href="https://github.com/divyo-argha/git-user/releases"><img src="https://img.shields.io/github/v/release/divyo-argha/git-user?style=flat-square&color=00FFAA&label=latest" alt="Latest Release" /></a>
|
|
14
|
+
<a href="https://github.com/divyo-argha/git-user/releases"><img src="https://img.shields.io/github/downloads/divyo-argha/git-user/total?style=flat-square&color=00FFAA&label=gh%20downloads" alt="GitHub Downloads" /></a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/git-userhub"><img src="https://img.shields.io/npm/v/git-userhub?style=flat-square&color=CB3837&logo=npm&logoColor=white&label=npm" alt="npm" /></a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/git-userhub"><img src="https://img.shields.io/npm/dt/git-userhub?style=flat-square&color=CB3837&logo=npm&logoColor=white&label=total%20downloads" alt="total downloads" /></a>
|
|
17
|
+
<a href="https://github.com/divyo-argha/git-user"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/divyo-argha/git-user/main/badges/total-downloads.json&style=flat-square" alt="Combined Downloads" /></a>
|
|
18
|
+
<a href="https://pkg.go.dev/github.com/divyo-argha/git-user"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat-square&logo=go&logoColor=white" alt="Go" /></a>
|
|
19
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square" alt="MIT" /></a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p>
|
|
23
|
+
<a href="#-the-problem">The Problem</a> ·
|
|
24
|
+
<a href="#-install">Install</a> ·
|
|
25
|
+
<a href="#-quick-start">Quick Start</a> ·
|
|
26
|
+
<a href="#-why-git-user">Why git-user</a> ·
|
|
27
|
+
<a href="#-features">Features</a> ·
|
|
28
|
+
<a href="#-commands">Commands</a> ·
|
|
29
|
+
<a href="#-security">Security</a> ·
|
|
30
|
+
<a href="#-contributing">Contributing</a>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<br />
|
|
34
|
+
|
|
35
|
+
<img src="https://img.shields.io/badge/GitHub-supported-181717?style=for-the-badge&logo=github&logoColor=white" alt="GitHub" />
|
|
36
|
+
<img src="https://img.shields.io/badge/GitLab-supported-FC6D26?style=for-the-badge&logo=gitlab&logoColor=white" alt="GitLab" />
|
|
37
|
+
<img src="https://img.shields.io/badge/Bitbucket-supported-0052CC?style=for-the-badge&logo=bitbucket&logoColor=white" alt="Bitbucket" />
|
|
38
|
+
<img src="https://img.shields.io/badge/macOS-supported-000000?style=for-the-badge&logo=apple&logoColor=white" alt="macOS" />
|
|
39
|
+
<img src="https://img.shields.io/badge/Linux-supported-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Linux" />
|
|
40
|
+
<img src="https://img.shields.io/badge/Windows-supported-0078D4?style=for-the-badge&logo=windows&logoColor=white" alt="Windows" />
|
|
41
|
+
|
|
42
|
+
<br /><br />
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 😤 The Problem
|
|
49
|
+
|
|
50
|
+
You're a developer with multiple lives — work, personal, freelance, open source. Each one has its own Git account, its own SSH key, its own email.
|
|
51
|
+
|
|
52
|
+
And every few weeks, this happens:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
# You just pushed 3 commits to your client's repo.
|
|
56
|
+
# Then you check the author.
|
|
57
|
+
|
|
58
|
+
Author: you@personal.com ← 💀 wrong account. again.
|
|
59
|
+
|
|
60
|
+
# Or your work email leaked onto your public GitHub profile.
|
|
61
|
+
# Or your client can see your personal email in their repo history.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
You've tried everything:
|
|
65
|
+
|
|
66
|
+
| Attempt | Result |
|
|
67
|
+
|---------|--------|
|
|
68
|
+
| Editing `~/.gitconfig` manually | You forget. Every time. |
|
|
69
|
+
| Per-repo `.git/config` overrides | Works until you clone a new repo |
|
|
70
|
+
| Multiple terminal profiles | Still mix them up |
|
|
71
|
+
| SSH config `Host` aliases | Breaks half your existing remotes |
|
|
72
|
+
| Remembering which key goes where | Not a real solution |
|
|
73
|
+
|
|
74
|
+
**git-user is the permanent fix.** Register your identities once. Switch with one command. Everything — git config, SSH key, remote verification — updates automatically in under a second.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 📦 Install
|
|
79
|
+
|
|
80
|
+
<table>
|
|
81
|
+
<tr>
|
|
82
|
+
<td width="33%" valign="top">
|
|
83
|
+
|
|
84
|
+
### One-line
|
|
85
|
+
```bash
|
|
86
|
+
curl -sSfL https://raw.githubusercontent.com/divyo-argha/git-user/main/install.sh | bash
|
|
87
|
+
```
|
|
88
|
+
Restart your terminal. PATH is configured automatically.
|
|
89
|
+
|
|
90
|
+
</td>
|
|
91
|
+
<td width="33%" valign="top">
|
|
92
|
+
|
|
93
|
+
### npm
|
|
94
|
+
```bash
|
|
95
|
+
npm install -g git-userhub
|
|
96
|
+
```
|
|
97
|
+
> Published as `git-userhub` on npm.
|
|
98
|
+
> After install, the command is `git-user`.
|
|
99
|
+
|
|
100
|
+
</td>
|
|
101
|
+
<td width="33%" valign="top">
|
|
102
|
+
|
|
103
|
+
### Go
|
|
104
|
+
```bash
|
|
105
|
+
go install github.com/divyo-argha/git-user@latest
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Self-update
|
|
109
|
+
```bash
|
|
110
|
+
git-user --update
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
</td>
|
|
114
|
+
</tr>
|
|
115
|
+
</table>
|
|
116
|
+
|
|
117
|
+
**Requirements:**  · ssh-keygen (optional, for SSH key generation)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## ⚡ Quick Start
|
|
122
|
+
|
|
123
|
+
Two minutes to set up. One second to switch forever after.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Step 1 — register your identities (guided, interactive)
|
|
127
|
+
git-user register # → name: work, email: you@company.com
|
|
128
|
+
git-user register # → name: personal, email: you@gmail.com
|
|
129
|
+
git-user register # → name: client-a, email: you@client.com
|
|
130
|
+
|
|
131
|
+
# Step 2 — switch
|
|
132
|
+
git-user switch work
|
|
133
|
+
|
|
134
|
+
# Step 3 — push. that's it.
|
|
135
|
+
git push # ← commits as you@company.com ✓
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Create and switch in one command
|
|
140
|
+
git-user switch -c freelance me@freelance.com
|
|
141
|
+
|
|
142
|
+
# Always know who you are
|
|
143
|
+
git-user current
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 🏆 Why git-user?
|
|
149
|
+
|
|
150
|
+
There are other tools that try to solve this. Here's how git-user is different:
|
|
151
|
+
|
|
152
|
+
| Feature | git-user | direnv / per-dir config | SSH `Host` aliases | Manual `~/.gitconfig` |
|
|
153
|
+
|---------|:--------:|:----------------------:|:------------------:|:---------------------:|
|
|
154
|
+
| One command to switch everything | ✅ | ❌ | ❌ | ❌ |
|
|
155
|
+
| SSH key managed automatically | ✅ | ❌ | ⚠️ partial | ❌ |
|
|
156
|
+
| Works across all repos, not just one | ✅ | ❌ | ✅ | ✅ |
|
|
157
|
+
| SSH connection verified on switch | ✅ | ❌ | ❌ | ❌ |
|
|
158
|
+
| Clean logout/sign-out to void state | ✅ | ❌ | ❌ | ❌ |
|
|
159
|
+
| Encrypted export/import | ✅ | ❌ | ❌ | ❌ |
|
|
160
|
+
| Pre-commit identity guard | ✅ | ❌ | ❌ | ❌ |
|
|
161
|
+
| Security audit built-in | ✅ | ❌ | ❌ | ❌ |
|
|
162
|
+
| Interactive TUI | ✅ | ❌ | ❌ | ❌ |
|
|
163
|
+
| Shell completions | ✅ | ❌ | ❌ | ❌ |
|
|
164
|
+
| Zero config files to edit manually | ✅ | ❌ | ❌ | ❌ |
|
|
165
|
+
|
|
166
|
+
> **The key difference:** git-user manages the *whole identity* — name, email, SSH key, and passphrase protection — as a single atomic unit. Other approaches only solve part of the problem, leaving you to manually wire the rest.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## ✨ Features
|
|
171
|
+
|
|
172
|
+
<table>
|
|
173
|
+
<tr>
|
|
174
|
+
<td width="50%" valign="top">
|
|
175
|
+
|
|
176
|
+
### 🔑 Identity Management
|
|
177
|
+
- Register unlimited identities — name, email, SSH key
|
|
178
|
+
- Switch in one command, git config updates instantly
|
|
179
|
+
- `switch -c <name>` — create and switch in one step
|
|
180
|
+
- Edit email without re-registering
|
|
181
|
+
- Remove identities safely, with active-identity guard
|
|
182
|
+
|
|
183
|
+
</td>
|
|
184
|
+
<td width="50%" valign="top">
|
|
185
|
+
|
|
186
|
+
### 🔐 SSH Key Handling
|
|
187
|
+
- Auto-generate ed25519 keys per identity
|
|
188
|
+
- `pubkey` — print active identity's public key (add to GitHub, GitLab, Bitbucket)
|
|
189
|
+
- Bind any existing key to any identity
|
|
190
|
+
- `rekey` rotates keys with automatic backup and rollback
|
|
191
|
+
- `IdentitiesOnly yes` — SSH never leaks the wrong key
|
|
192
|
+
|
|
193
|
+
</td>
|
|
194
|
+
</tr>
|
|
195
|
+
<tr>
|
|
196
|
+
<td width="50%" valign="top">
|
|
197
|
+
|
|
198
|
+
### 🛡️ Security & Passphrases
|
|
199
|
+
- Passphrase-protected keys enforced by default
|
|
200
|
+
- `security` audits every identity: permissions, passphrase, key existence
|
|
201
|
+
- `passphrase` changes passphrase only for the active, unlocked identity
|
|
202
|
+
- All config writes are atomic (temp file + rename) — crash-safe
|
|
203
|
+
- All files stored at `0600` permissions
|
|
204
|
+
|
|
205
|
+
</td>
|
|
206
|
+
<td width="50%" valign="top">
|
|
207
|
+
|
|
208
|
+
### 🔒 Passphrase-Gated Switching
|
|
209
|
+
- Gated switch: switching to a passphrase-protected profile requires entering the passphrase to unlock the SSH key
|
|
210
|
+
- Seamless ssh-agent management: the SSH key is added automatically on switch
|
|
211
|
+
- Security by default: you cannot act as an identity without verifying the passphrase first
|
|
212
|
+
- Clean logout: sign out at any time to clear active user config completely
|
|
213
|
+
|
|
214
|
+
</td>
|
|
215
|
+
</tr>
|
|
216
|
+
<tr>
|
|
217
|
+
<td width="50%" valign="top">
|
|
218
|
+
|
|
219
|
+
### 🚀 Passwordless Push
|
|
220
|
+
- Detects HTTPS remotes on `switch` and offers to convert
|
|
221
|
+
- `fix-remote` converts all remotes HTTPS → SSH instantly
|
|
222
|
+
- Works with GitHub, GitLab, Bitbucket, and any Git host
|
|
223
|
+
|
|
224
|
+
</td>
|
|
225
|
+
<td width="50%" valign="top">
|
|
226
|
+
|
|
227
|
+
### 🖥️ Developer Experience
|
|
228
|
+
- Interactive TUI menu (`git-user tui`)
|
|
229
|
+
- Shell completions for bash, zsh, fish
|
|
230
|
+
- Pre-commit hooks to block wrong-identity commits
|
|
231
|
+
- `doctor` diagnoses your entire setup in one command
|
|
232
|
+
- Encrypted export/import for moving to a new machine
|
|
233
|
+
|
|
234
|
+
</td>
|
|
235
|
+
</tr>
|
|
236
|
+
</table>
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 🔄 How It Works
|
|
241
|
+
|
|
242
|
+
### Under the hood — one switch
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
git-user switch work
|
|
246
|
+
│
|
|
247
|
+
▼
|
|
248
|
+
1. Looks up "work" in ~/.git-users/config.json
|
|
249
|
+
2. Sets ~/.gitconfig → user.name, user.email
|
|
250
|
+
3. Sets ~/.gitconfig → core.sshCommand (points to your key)
|
|
251
|
+
4. Verifies SSH connection to GitHub/GitLab/Bitbucket
|
|
252
|
+
5. ✅ Switched to "work" (you@company.com)
|
|
253
|
+
|
|
254
|
+
git push ← just works, every time
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### A real day with multiple accounts
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
9:00 AM — starting work
|
|
261
|
+
──────────────────────────────────────────────────────────
|
|
262
|
+
$ git-user switch work
|
|
263
|
+
✅ Switched to work (you@company.com)
|
|
264
|
+
$ git push ← commits as you@company.com ✓
|
|
265
|
+
|
|
266
|
+
1:00 PM — open source on lunch break
|
|
267
|
+
──────────────────────────────────────────────────────────
|
|
268
|
+
$ git-user switch personal
|
|
269
|
+
✅ Switched to personal (you@gmail.com)
|
|
270
|
+
$ git push ← commits as you@gmail.com ✓
|
|
271
|
+
|
|
272
|
+
5:00 PM — freelance client work
|
|
273
|
+
──────────────────────────────────────────────────────────
|
|
274
|
+
$ git-user switch client-a
|
|
275
|
+
✅ Switched to client-a (you@client-a.com)
|
|
276
|
+
$ git push ← commits as you@client-a.com ✓
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Each switch: under one second. No config editing. No SSH juggling.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 🚪 Logout / Void State
|
|
284
|
+
|
|
285
|
+
When you are done with your work or leaving a shared machine, you can sign out to clear your active Git identity completely:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
git-user logout
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
What happens:
|
|
292
|
+
- Unloads the active SSH key from `ssh-agent`
|
|
293
|
+
- Clears the global `user.name` and `user.email` from `~/.gitconfig`
|
|
294
|
+
- Clears `core.sshCommand` from `~/.gitconfig`
|
|
295
|
+
- Puts the terminal into a clean "void" state (no git user configured), preventing accidental commits under your identity by other users.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 📋 Commands
|
|
300
|
+
|
|
301
|
+
| Command | Description |
|
|
302
|
+
|---------|-------------|
|
|
303
|
+
| `register` | Create a new identity (guided setup with SSH) |
|
|
304
|
+
| `switch <name>` | Switch to an identity |
|
|
305
|
+
| `switch -c <name> [email]` | Create and switch in one command |
|
|
306
|
+
| `list` | Show all identities |
|
|
307
|
+
| `current` | Show active identity |
|
|
308
|
+
| `remove <name>` | Delete an identity |
|
|
309
|
+
| `edit <name> <email>` | Update email |
|
|
310
|
+
| `bind <name> [--ssh-key <path>]` | Link an SSH key to an identity |
|
|
311
|
+
| `pubkey` | Show the public key of the active identity |
|
|
312
|
+
| `passphrase` | Add or change passphrase for the active, unlocked identity |
|
|
313
|
+
| `rekey <name>` | Rotate SSH key (with rollback safety) |
|
|
314
|
+
| `fix-remote` | Convert HTTPS remotes to SSH |
|
|
315
|
+
| `logout` | Sign out, clearing the active identity and restoring a void state |
|
|
316
|
+
| `security` | Audit all identities for security issues |
|
|
317
|
+
| `export --all` | Export all identities + SSH keys (AES-256 encrypted) |
|
|
318
|
+
| `export <name> [name...]` | Export specific identities |
|
|
319
|
+
| `import <file>` | Import from an encrypted bundle |
|
|
320
|
+
| `doctor` | Run a full health check |
|
|
321
|
+
| `tui` | Interactive menu |
|
|
322
|
+
| `completion <shell>` | Shell completions (bash/zsh/fish) |
|
|
323
|
+
| `hook <install\|uninstall>` | Pre-commit hook to verify identity |
|
|
324
|
+
| `--update` | Update to the latest version |
|
|
325
|
+
| `--version` / `-v` | Show version |
|
|
326
|
+
|
|
327
|
+
**Aliases:** `ls` → `list` · `sw` → `switch` · `rm` → `remove`
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 🛡️ Security
|
|
332
|
+
|
|
333
|
+
<table>
|
|
334
|
+
<tr>
|
|
335
|
+
<td width="50%" valign="top">
|
|
336
|
+
|
|
337
|
+
**What git-user does**
|
|
338
|
+
- Private keys stay on your machine at `0600` permissions
|
|
339
|
+
- Config writes are atomic (temp file + rename) — crash-safe
|
|
340
|
+
- `IdentitiesOnly yes` in SSH config — no key leakage
|
|
341
|
+
- Passphrase protection audited by `security` command
|
|
342
|
+
- Export bundles encrypted with AES-256-GCM, passphrase stretched with scrypt (N=2¹⁷)
|
|
343
|
+
- Passphrases are never passed as CLI arguments — entered directly into the terminal
|
|
344
|
+
- `pubkey` only shows the active identity's key — other identities' keys are never exposed
|
|
345
|
+
|
|
346
|
+
</td>
|
|
347
|
+
<td width="50%" valign="top">
|
|
348
|
+
|
|
349
|
+
**What git-user never does**
|
|
350
|
+
- Never stores passphrases
|
|
351
|
+
- Never sends keys or config anywhere
|
|
352
|
+
- Never modifies your repositories
|
|
353
|
+
- Never overwrites existing identities on import
|
|
354
|
+
- `logout` command cleanly clears all gitconfig references and unloads loaded keys
|
|
355
|
+
|
|
356
|
+
</td>
|
|
357
|
+
</tr>
|
|
358
|
+
</table>
|
|
359
|
+
|
|
360
|
+
### Run a security audit
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
git-user security
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
✔ Config file permissions OK (0600)
|
|
368
|
+
|
|
369
|
+
ℹ work (you@company.com)
|
|
370
|
+
✔ Permissions OK: git_work
|
|
371
|
+
✔ Passphrase protected
|
|
372
|
+
|
|
373
|
+
ℹ personal (you@gmail.com)
|
|
374
|
+
✔ Permissions OK: git_personal
|
|
375
|
+
⚠ No passphrase detected
|
|
376
|
+
Fix: ssh-keygen -p -f ~/.ssh/git_personal
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 🚚 Moving to a New Machine
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
# On your current machine
|
|
385
|
+
git-user export --all
|
|
386
|
+
# → ~/git-user-export-2026-05-29.bundle (AES-256 encrypted)
|
|
387
|
+
|
|
388
|
+
# Transfer the file, then on the new machine
|
|
389
|
+
git-user import ~/git-user-export-2026-05-29.bundle
|
|
390
|
+
# ✅ Imported: work (you@company.com) → ~/.ssh/git_work
|
|
391
|
+
# ✅ Imported: personal (you@gmail.com) → ~/.ssh/git_personal
|
|
392
|
+
|
|
393
|
+
git-user switch work
|
|
394
|
+
# Ready to push immediately
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## 🔧 Troubleshooting
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
git-user doctor
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
✅ git installed (2.43.0)
|
|
407
|
+
✅ ssh-keygen available
|
|
408
|
+
✅ Active identity: work (you@company.com)
|
|
409
|
+
✅ SSH key exists at ~/.ssh/git_work
|
|
410
|
+
✅ Key permissions OK (0600)
|
|
411
|
+
✅ GitHub connection verified — Hi alice-corp!
|
|
412
|
+
──────────────────────────────────────────────
|
|
413
|
+
Everything looks good.
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Common issues:**
|
|
417
|
+
|
|
418
|
+
| Symptom | Fix |
|
|
419
|
+
|---------|-----|
|
|
420
|
+
| `git-user: command not found` | Restart terminal or `source ~/.zshrc` |
|
|
421
|
+
| SSH verification failed | Key not added to platform yet — run `git-user pubkey` to copy the public key |
|
|
422
|
+
| `Permission denied` during install | Expected — installer needs sudo for `/usr/local/bin` |
|
|
423
|
+
| Git asks for credentials on push | Run `git-user fix-remote` to convert HTTPS → SSH |
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 🐚 Shell Completions
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
# Bash
|
|
431
|
+
git-user completion bash | sudo tee /etc/bash_completion.d/git-user
|
|
432
|
+
|
|
433
|
+
# Zsh
|
|
434
|
+
git-user completion zsh > "${fpath[1]}/_git-user"
|
|
435
|
+
|
|
436
|
+
# Fish
|
|
437
|
+
git-user completion fish > ~/.config/fish/completions/git-user.fish
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
git-user sw<TAB> # → git-user switch
|
|
442
|
+
git-user switch <TAB> # → work personal client-a
|
|
443
|
+
git-user remove <TAB> # → your identity names
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## 🪝 Pre-commit Hooks
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
git-user hook install # in any repo where identity matters
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
git commit -m "Add feature"
|
|
456
|
+
|
|
457
|
+
# ✖ Identity mismatch!
|
|
458
|
+
# Expected: work (you@company.com)
|
|
459
|
+
# Git config: you@gmail.com
|
|
460
|
+
# Run: git-user switch work
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## 📁 What Gets Modified
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
~/.git-users/
|
|
469
|
+
└── config.json ← your identities (names, emails, key paths)
|
|
470
|
+
|
|
471
|
+
~/.gitconfig ← updated on every switch/logout (name, email, sshCommand)
|
|
472
|
+
~/.ssh/git_<name> ← private key (never leaves your machine)
|
|
473
|
+
~/.ssh/git_<name>.pub ← public key (what you add to GitHub/GitLab)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Your repositories are never touched. Only global git config changes.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## 🤝 Contributing
|
|
481
|
+
|
|
482
|
+
Issues and pull requests are welcome. If something's broken, open an issue. If something's confusing — even just "I didn't understand what this command does" — that's worth filing too.
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
git clone https://github.com/divyo-argha/git-user.git
|
|
486
|
+
cd git-user
|
|
487
|
+
make build # build binary
|
|
488
|
+
make test # run tests
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 📄 License
|
|
496
|
+
|
|
497
|
+
MIT — see [LICENSE](LICENSE).
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
<div align="center">
|
|
502
|
+
|
|
503
|
+
**Made for developers who just want their Git to work.**
|
|
504
|
+
|
|
505
|
+
<br />
|
|
506
|
+
|
|
507
|
+
[](https://github.com/divyo-argha/git-user)
|
|
508
|
+
[](https://www.npmjs.com/package/git-userhub)
|
|
509
|
+
|
|
510
|
+
<br />
|
|
511
|
+
|
|
512
|
+
<sub>If git-user saved you from a wrong-account commit, consider giving it a ⭐</sub>
|
|
513
|
+
|
|
514
|
+
</div>
|
package/bin/git-user
ADDED
|
Binary file
|
package/bin/git-user.js
CHANGED
|
@@ -1,18 +1,157 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { spawn } = require('child_process');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const fs = require('fs');
|
|
4
6
|
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const tar = require('tar');
|
|
9
|
+
const pkg = require('../package.json');
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const binPath = path.join(__dirname, '..', 'bin', binaryName);
|
|
11
|
+
const REPO = pkg.config.repo;
|
|
12
|
+
const BIN_DIR = path.join(__dirname, '..', 'bin');
|
|
9
13
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// Detect platform and architecture
|
|
15
|
+
function getPlatform() {
|
|
16
|
+
const platform = os.platform();
|
|
17
|
+
const arch = os.arch();
|
|
18
|
+
|
|
19
|
+
const platformMap = {
|
|
20
|
+
'darwin': 'darwin',
|
|
21
|
+
'linux': 'linux',
|
|
22
|
+
'win32': 'windows'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const archMap = {
|
|
26
|
+
'x64': 'amd64',
|
|
27
|
+
'arm64': 'arm64'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
os: platformMap[platform],
|
|
32
|
+
arch: archMap[arch],
|
|
33
|
+
ext: platform === 'win32' ? '.exe' : ''
|
|
34
|
+
};
|
|
35
|
+
}
|
|
15
36
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
37
|
+
// Download file from URL
|
|
38
|
+
function download(url, dest) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const file = fs.createWriteStream(dest);
|
|
41
|
+
|
|
42
|
+
https.get(url, (response) => {
|
|
43
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
44
|
+
return download(response.headers.location, dest).then(resolve).catch(reject);
|
|
45
|
+
}
|
|
46
|
+
if (response.statusCode !== 200) {
|
|
47
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
response.pipe(file);
|
|
51
|
+
file.on('finish', () => {
|
|
52
|
+
file.close();
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
}).on('error', (err) => {
|
|
56
|
+
fs.unlink(dest, () => {});
|
|
57
|
+
reject(err);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Get release info matching this npm package version.
|
|
63
|
+
function getRelease() {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const options = {
|
|
66
|
+
hostname: 'api.github.com',
|
|
67
|
+
path: `/repos/${REPO}/releases/tags/v${pkg.version}`,
|
|
68
|
+
headers: {
|
|
69
|
+
'User-Agent': 'git-user-cli'
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
https.get(options, (res) => {
|
|
74
|
+
let data = '';
|
|
75
|
+
res.on('data', (chunk) => data += chunk);
|
|
76
|
+
res.on('end', () => {
|
|
77
|
+
try {
|
|
78
|
+
resolve(JSON.parse(data));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
reject(err);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}).on('error', reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function installAndRun() {
|
|
88
|
+
const { os: osName, arch, ext } = getPlatform();
|
|
89
|
+
const binaryPath = path.join(BIN_DIR, `git-user${ext}`);
|
|
90
|
+
|
|
91
|
+
if (!fs.existsSync(binaryPath)) {
|
|
92
|
+
console.log('📦 First run detected. Downloading git-user binary...');
|
|
93
|
+
|
|
94
|
+
if (!osName || !arch) {
|
|
95
|
+
console.error('❌ Unsupported platform:', os.platform(), os.arch());
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
if (!fs.existsSync(BIN_DIR)) {
|
|
101
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`🔍 Fetching release v${pkg.version}...`);
|
|
105
|
+
const release = await getRelease();
|
|
106
|
+
|
|
107
|
+
const asset = release.assets?.find(a => {
|
|
108
|
+
const name = a.name.toLowerCase();
|
|
109
|
+
if (!name.includes(osName)) return false;
|
|
110
|
+
if (arch === 'arm64') return name.includes('arm64');
|
|
111
|
+
return name.includes('x86_64') || name.includes('amd64') || name.includes('x64');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!asset) {
|
|
115
|
+
console.error('❌ No binary found for your platform');
|
|
116
|
+
console.error(` Looking for a ${osName} binary matching architecture: ${arch}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`⬇️ Downloading ${asset.name}...`);
|
|
121
|
+
const archivePath = path.join(BIN_DIR, asset.name);
|
|
122
|
+
await download(asset.browser_download_url, archivePath);
|
|
123
|
+
|
|
124
|
+
console.log('📂 Extracting...');
|
|
125
|
+
await tar.extract({ file: archivePath, cwd: BIN_DIR });
|
|
126
|
+
fs.unlinkSync(archivePath);
|
|
127
|
+
|
|
128
|
+
if (fs.existsSync(binaryPath)) {
|
|
129
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
130
|
+
console.log('✅ git-user installed successfully!\n');
|
|
131
|
+
} else {
|
|
132
|
+
console.error('❌ Binary not found after extraction');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('❌ Installation failed:', err.message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Forward all arguments to the binary
|
|
142
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
143
|
+
stdio: 'inherit',
|
|
144
|
+
shell: false
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
child.on('exit', (code) => {
|
|
148
|
+
process.exit(code || 0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
child.on('error', (err) => {
|
|
152
|
+
console.error('❌ Failed to start git-user:', err.message);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
installAndRun();
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-userhub",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Switch Git accounts in one command. No config editing. No SSH key chaos.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"git-user": "bin/git-user.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"postinstall": "node install.js",
|
|
10
9
|
"test": "echo \"No tests yet\" && exit 0"
|
|
11
10
|
},
|
|
12
11
|
"dependencies": {
|
package/install.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const https = require('https');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const tar = require('tar');
|
|
8
|
-
const pkg = require('./package.json');
|
|
9
|
-
|
|
10
|
-
const REPO = pkg.config.repo;
|
|
11
|
-
const BIN_DIR = path.join(__dirname, 'bin');
|
|
12
|
-
|
|
13
|
-
// Detect platform and architecture
|
|
14
|
-
function getPlatform() {
|
|
15
|
-
const platform = os.platform();
|
|
16
|
-
const arch = os.arch();
|
|
17
|
-
|
|
18
|
-
const platformMap = {
|
|
19
|
-
'darwin': 'darwin',
|
|
20
|
-
'linux': 'linux',
|
|
21
|
-
'win32': 'windows'
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const archMap = {
|
|
25
|
-
'x64': 'amd64',
|
|
26
|
-
'arm64': 'arm64'
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
os: platformMap[platform],
|
|
31
|
-
arch: archMap[arch],
|
|
32
|
-
ext: platform === 'win32' ? '.exe' : ''
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Download file from URL
|
|
37
|
-
function download(url, dest) {
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
const file = fs.createWriteStream(dest);
|
|
40
|
-
|
|
41
|
-
https.get(url, (response) => {
|
|
42
|
-
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
43
|
-
// Follow redirect
|
|
44
|
-
return download(response.headers.location, dest).then(resolve).catch(reject);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (response.statusCode !== 200) {
|
|
48
|
-
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
response.pipe(file);
|
|
53
|
-
|
|
54
|
-
file.on('finish', () => {
|
|
55
|
-
file.close();
|
|
56
|
-
resolve();
|
|
57
|
-
});
|
|
58
|
-
}).on('error', (err) => {
|
|
59
|
-
fs.unlink(dest, () => {});
|
|
60
|
-
reject(err);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Get release info matching this npm package version.
|
|
66
|
-
function getRelease() {
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
const options = {
|
|
69
|
-
hostname: 'api.github.com',
|
|
70
|
-
path: `/repos/${REPO}/releases/tags/v${pkg.version}`,
|
|
71
|
-
headers: {
|
|
72
|
-
'User-Agent': 'git-user-npm-installer'
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
https.get(options, (res) => {
|
|
77
|
-
let data = '';
|
|
78
|
-
|
|
79
|
-
res.on('data', (chunk) => {
|
|
80
|
-
data += chunk;
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
res.on('end', () => {
|
|
84
|
-
try {
|
|
85
|
-
const release = JSON.parse(data);
|
|
86
|
-
resolve(release);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
reject(err);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}).on('error', reject);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function install() {
|
|
96
|
-
console.log('📦 Installing git-user...');
|
|
97
|
-
|
|
98
|
-
const { os: osName, arch, ext } = getPlatform();
|
|
99
|
-
|
|
100
|
-
if (!osName || !arch) {
|
|
101
|
-
console.error('❌ Unsupported platform:', os.platform(), os.arch());
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
// Create bin directory
|
|
107
|
-
if (!fs.existsSync(BIN_DIR)) {
|
|
108
|
-
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Get matching release
|
|
112
|
-
console.log(`🔍 Fetching release v${pkg.version}...`);
|
|
113
|
-
const release = await getRelease();
|
|
114
|
-
|
|
115
|
-
// Find the right asset dynamically
|
|
116
|
-
const asset = release.assets.find(a => {
|
|
117
|
-
const name = a.name.toLowerCase();
|
|
118
|
-
if (!name.includes(osName)) return false;
|
|
119
|
-
|
|
120
|
-
if (arch === 'arm64') {
|
|
121
|
-
return name.includes('arm64');
|
|
122
|
-
} else {
|
|
123
|
-
return name.includes('x86_64') || name.includes('amd64') || name.includes('x64');
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (!asset) {
|
|
128
|
-
console.error('❌ No binary found for your platform');
|
|
129
|
-
console.error(` Looking for a ${osName} binary matching architecture: ${arch}`);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Download
|
|
134
|
-
console.log(`⬇️ Downloading ${asset.name}...`);
|
|
135
|
-
const archivePath = path.join(BIN_DIR, asset.name);
|
|
136
|
-
await download(asset.browser_download_url, archivePath);
|
|
137
|
-
|
|
138
|
-
// Extract
|
|
139
|
-
console.log('📂 Extracting...');
|
|
140
|
-
await tar.extract({
|
|
141
|
-
file: archivePath,
|
|
142
|
-
cwd: BIN_DIR
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Cleanup archive
|
|
146
|
-
fs.unlinkSync(archivePath);
|
|
147
|
-
|
|
148
|
-
// Make executable
|
|
149
|
-
const binaryPath = path.join(BIN_DIR, `git-user${ext}`);
|
|
150
|
-
if (fs.existsSync(binaryPath)) {
|
|
151
|
-
fs.chmodSync(binaryPath, 0o755);
|
|
152
|
-
console.log('✅ git-user installed successfully!');
|
|
153
|
-
console.log('');
|
|
154
|
-
console.log('Quick start:');
|
|
155
|
-
console.log(' npx git-user register # Create your first identity');
|
|
156
|
-
console.log(' npx git-user switch <n> # Switch between identities');
|
|
157
|
-
console.log(' npx git-user --help # Show all commands');
|
|
158
|
-
} else {
|
|
159
|
-
console.error('❌ Binary not found after extraction');
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
} catch (err) {
|
|
164
|
-
console.error('❌ Installation failed:', err.message);
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
install();
|