@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.
Files changed (2) hide show
  1. package/index.js +215 -0
  2. 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
+ }