@anhducmata/git-manager 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/index.js +215 -0
- package/package.json +21 -0
package/index.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
const configDir = path.join(os.homedir(), '.config', 'git-manager');
|
|
12
|
+
const accountsFile = path.join(configDir, 'accounts.json');
|
|
13
|
+
|
|
14
|
+
// Ensure config dir exists
|
|
15
|
+
if (!fs.existsSync(configDir)) {
|
|
16
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Load accounts
|
|
20
|
+
function loadAccounts() {
|
|
21
|
+
if (!fs.existsSync(accountsFile)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return JSON.parse(fs.readFileSync(accountsFile, 'utf-8'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Save accounts
|
|
28
|
+
function saveAccounts(accounts) {
|
|
29
|
+
fs.writeFileSync(accountsFile, JSON.stringify(accounts, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check current config
|
|
33
|
+
function getCurrentGitUser() {
|
|
34
|
+
try {
|
|
35
|
+
const name = execSync('git config --global user.name').toString().trim();
|
|
36
|
+
const email = execSync('git config --global user.email').toString().trim();
|
|
37
|
+
return { name, email };
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return { name: 'Not set', email: 'Not set' };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.name('gm')
|
|
45
|
+
.description('Git Account Manager CLI')
|
|
46
|
+
.version('1.0.0');
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command('list')
|
|
50
|
+
.description('List all available git accounts')
|
|
51
|
+
.action(() => {
|
|
52
|
+
const accounts = loadAccounts();
|
|
53
|
+
if (accounts.length === 0) {
|
|
54
|
+
console.log(chalk.yellow('No accounts found. Add one with `gm new`.'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.log(chalk.bold('\nAvailable Git Accounts:'));
|
|
58
|
+
accounts.forEach(acc => {
|
|
59
|
+
console.log(`- ${chalk.green(acc.profileName)} (${acc.name} <${acc.email}>)`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const current = getCurrentGitUser();
|
|
63
|
+
console.log(`\n${chalk.bold('Current Global Git User:')} ${current.name} <${current.email}>`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
program
|
|
67
|
+
.command('new')
|
|
68
|
+
.description('Add a new git account')
|
|
69
|
+
.action(async () => {
|
|
70
|
+
console.log(chalk.cyan('Creating a new Git profile...\n'));
|
|
71
|
+
|
|
72
|
+
const profileName = await input({ message: 'Enter a profile name (e.g. Work, Personal):' });
|
|
73
|
+
const name = await input({ message: 'Enter your Git user.name (e.g. John Doe):' });
|
|
74
|
+
const email = await input({ message: 'Enter your Git user.email (e.g. john@example.com):' });
|
|
75
|
+
|
|
76
|
+
const sshDir = path.join(os.homedir(), '.ssh');
|
|
77
|
+
if (!fs.existsSync(sshDir)) {
|
|
78
|
+
fs.mkdirSync(sshDir, { recursive: true, mode: 0o700 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const safeName = profileName.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
82
|
+
const defaultKeyName = `id_ed25519_gm_${safeName}`;
|
|
83
|
+
const defaultKeyPath = path.join(sshDir, defaultKeyName);
|
|
84
|
+
|
|
85
|
+
const keyPathInput = await input({
|
|
86
|
+
message: 'Enter SSH key path (or press enter to generate a new ed25519 key):',
|
|
87
|
+
default: defaultKeyPath
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const isNewKey = keyPathInput === defaultKeyPath && !fs.existsSync(keyPathInput);
|
|
91
|
+
|
|
92
|
+
if (isNewKey) {
|
|
93
|
+
console.log(chalk.blue(`Generating new SSH key at ${keyPathInput}...`));
|
|
94
|
+
try {
|
|
95
|
+
execSync(`ssh-keygen -t ed25519 -C "${email}" -f "${keyPathInput}" -N ""`, { stdio: 'inherit' });
|
|
96
|
+
console.log(chalk.green('SSH key generated successfully!'));
|
|
97
|
+
console.log(chalk.yellow('\nMake sure to add the following public key to your Git provider (GitHub/GitLab/etc):\n'));
|
|
98
|
+
console.log(fs.readFileSync(`${keyPathInput}.pub`, 'utf-8'));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.log(chalk.red('Error generating SSH key. Continuing with path setup.'));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const accounts = loadAccounts();
|
|
105
|
+
accounts.push({ profileName, name, email, sshKeyPath: keyPathInput });
|
|
106
|
+
saveAccounts(accounts);
|
|
107
|
+
|
|
108
|
+
console.log(chalk.green(`\nProfile '${profileName}' added successfully!`));
|
|
109
|
+
|
|
110
|
+
const switchNow = await confirm({ message: 'Do you want to switch to this profile now?' });
|
|
111
|
+
if (switchNow) {
|
|
112
|
+
switchAccount(profileName, accounts);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function switchAccount(profileName, accounts) {
|
|
117
|
+
const account = accounts.find(a => a.profileName === profileName);
|
|
118
|
+
if (!account) {
|
|
119
|
+
console.log(chalk.red(`Profile '${profileName}' not found.`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
execSync(`git config --global user.name "${account.name}"`);
|
|
125
|
+
execSync(`git config --global user.email "${account.email}"`);
|
|
126
|
+
// Crucial: Use core.sshCommand to override SSH completely for this git global context!
|
|
127
|
+
execSync(`git config --global core.sshCommand "ssh -i ${account.sshKeyPath} -o IdentitiesOnly=yes"`);
|
|
128
|
+
|
|
129
|
+
console.log(chalk.green(`\nSuccessfully switched to profile '${account.profileName}'!`));
|
|
130
|
+
console.log(chalk.cyan(`User: ${account.name}`));
|
|
131
|
+
console.log(chalk.cyan(`Email: ${account.email}`));
|
|
132
|
+
console.log(chalk.cyan(`SSH: ${account.sshKeyPath}`));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.log(chalk.red('Failed to switch account:', error.message));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
program
|
|
139
|
+
.command('switch')
|
|
140
|
+
.description('Switch between git accounts')
|
|
141
|
+
.action(async () => {
|
|
142
|
+
const accounts = loadAccounts();
|
|
143
|
+
if (accounts.length === 0) {
|
|
144
|
+
console.log(chalk.yellow('No accounts found. Add one with `gm new`.'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const current = getCurrentGitUser();
|
|
149
|
+
console.log(chalk.blue(`Current active user: ${current.name} <${current.email}>\n`));
|
|
150
|
+
|
|
151
|
+
const choices = accounts.map(acc => ({
|
|
152
|
+
name: `${acc.profileName} (${acc.name} <${acc.email}>)`,
|
|
153
|
+
value: acc.profileName
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
const selectedProfile = await select({
|
|
157
|
+
message: 'Select an account to switch to:',
|
|
158
|
+
choices
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
switchAccount(selectedProfile, accounts);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
program
|
|
165
|
+
.command('remove')
|
|
166
|
+
.description('Remove a git account')
|
|
167
|
+
.action(async () => {
|
|
168
|
+
const accounts = loadAccounts();
|
|
169
|
+
if (accounts.length === 0) {
|
|
170
|
+
console.log(chalk.yellow('No accounts found.'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const choices = accounts.map(acc => ({
|
|
175
|
+
name: `${acc.profileName} (${acc.name} <${acc.email}>)`,
|
|
176
|
+
value: acc.profileName
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
const selectedProfile = await select({
|
|
180
|
+
message: 'Select an account to remove:',
|
|
181
|
+
choices
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const account = accounts.find(a => a.profileName === selectedProfile);
|
|
185
|
+
|
|
186
|
+
const confirmDelete = await confirm({
|
|
187
|
+
message: chalk.red(`Are you sure you want to remove '${selectedProfile}'?`)
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (confirmDelete) {
|
|
191
|
+
const remaining = accounts.filter(a => a.profileName !== selectedProfile);
|
|
192
|
+
saveAccounts(remaining);
|
|
193
|
+
console.log(chalk.green(`Profile '${selectedProfile}' removed.`));
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
if (fs.existsSync(account.sshKeyPath)) {
|
|
197
|
+
const deleteKey = await confirm({
|
|
198
|
+
message: `Do you also want to delete the SSH key at ${account.sshKeyPath}?`,
|
|
199
|
+
default: false
|
|
200
|
+
});
|
|
201
|
+
if (deleteKey) {
|
|
202
|
+
fs.unlinkSync(account.sshKeyPath);
|
|
203
|
+
if (fs.existsSync(`${account.sshKeyPath}.pub`)) {
|
|
204
|
+
fs.unlinkSync(`${account.sshKeyPath}.pub`);
|
|
205
|
+
}
|
|
206
|
+
console.log(chalk.green('SSH key deleted.'));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// Ignored
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anhducmata/git-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to easily manage and switch between multiple Git accounts",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gm": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@inquirer/prompts": "^8.3.2",
|
|
18
|
+
"chalk": "^5.6.2",
|
|
19
|
+
"commander": "^14.0.3"
|
|
20
|
+
}
|
|
21
|
+
}
|