├── .czrc ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── LICENSE ├── README.md ├── commitlint.config.js ├── jest.config.js ├── package.json ├── src ├── classes │ └── cli.ts └── index.ts ├── tsconfig.json └── yarn.lock /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec < /dev/tty && yarn commitlint --edit "$1" 3 | 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules # You don't need to publish your dependencies, npm handles this. 2 | /src # In your case, you're compiling your TypeScript files in a different directory, so you don't need to publish the source. 3 | **/*.spec.ts # Ignore any typescript test files too 4 | **/*.test.js # Ignore any compiled javascript test files 5 | .gitignore # No reason to include this in the package 6 | /package-lock.json # It's generally a good practice to allow your users to handle their own dependency trees 7 | README.md # Usually you wouldn't ignore this, unless you have a different README for your source code and your npm package -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ਇੰਦਰਦੀਪ ਸਿੰਘ Inderdeep Singh Bajwa 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitID - Manage Multiple Git Identities Easily 2 | 3 | GitID is a convenient command-line interface (CLI) that allows you seamlessly manage and switch between multiple git SSH identities on a single user account. 4 | 5 | **Caution:** While this program works well, it is still work in progress. I recommend backing up your ~/.ssh directory before using this. 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install -g gitid 11 | ``` 12 | 13 | ## Usage 14 | 15 | Here's how you can use the different commands of this CLI: 16 | 17 | - **Create new identity:** 18 | 19 | This will create a new `ed25519` SSH identity. 20 | 21 | ``` 22 | gitid new 23 | ``` 24 | 25 | ``` 26 | # example 27 | gitid new personal 28 | ``` 29 | 30 | Replace `` with the desired name for your new identity. 31 | 32 | - **List identities:** 33 | 34 | This will list all available identities. 35 | 36 | ``` 37 | gitid list 38 | ``` 39 | 40 | - **Check current identity:** 41 | 42 | This will output the current identity. 43 | 44 | ``` 45 | gitid current 46 | ``` 47 | 48 | - **Use identity:** 49 | 50 | This will change the Git identity for the repository in the current directory to a specified identity. 51 | 52 | ``` 53 | gitid use 54 | ``` 55 | 56 | ``` 57 | # example 58 | gitid use personal 59 | ``` 60 | 61 | Replace `` with the name of the identity you want to use. 62 | 63 | - **Show public key:** 64 | 65 | This command fetches and displays the public key of a specified identity. 66 | 67 | ``` 68 | gitid show 69 | ``` 70 | 71 | ``` 72 | # example 73 | gitid show personal 74 | ``` 75 | 76 | Replace `` with the name of the identity you want to show the public key for. 77 | 78 | This command reads the SSH config file, extracts the path of the corresponding `IdentityFile` for the specified identity, and then reads and prints the contents of the file. If the identity or the key file is not found, it will print an appropriate error message. 79 | 80 | ## TODO 81 | 82 | [ ] Option to set user.name and user.email in an identity 83 | 84 | [ ] Optionally exclude user.name and user.email settings from an identity 85 | 86 | ## Do I need GitID? 87 | 88 | GitID is your solution if you are: 89 | 90 | - Having a hard time managing multiple Git identity files on a single user account 91 | - Struggling with permission issues when accidentally pushing from a wrong identity file 92 | - Tired of having to modify git URLs every time you clone or add a new remote 93 | 94 | ## Manual Installation and Contribution 95 | 96 | First, clone the repository: 97 | 98 | ``` 99 | git clone https://github.com/inderdeepbajwa/gitid.git 100 | cd gitid 101 | ``` 102 | 103 | Then install the dependencies: 104 | 105 | ``` 106 | yarn install 107 | ``` 108 | 109 | Finally, build the code: 110 | 111 | ``` 112 | yarn run build 113 | ``` 114 | 115 | --- 116 | 117 | ## Note 118 | 119 | This CLI is meant for managing SSH identities on a single machine, the identity names you use are local to your machine and do not have to correspond to your actual GitHub username. 120 | 121 | ## License 122 | 123 | [MIT](LICENSE) 124 | 125 | --- 126 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitid", 3 | "version": "0.1.10", 4 | "description": "Manage multiple git accounts easily", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/InderdeepBajwa/gitid", 7 | "author": "Inderdeep Singh Bajwa", 8 | "license": "MIT", 9 | "private": false, 10 | "bin": { 11 | "gitid": "./dist/index.js" 12 | }, 13 | "scripts": { 14 | "start": "node dist/index.js", 15 | "build": "tsc", 16 | "test": "jest", 17 | "postinstall": "husky install", 18 | "prepack": "pinst --disable", 19 | "postpack": "pinst --enable", 20 | "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true", 21 | "commit": "git-cz" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "prepare-commit-msg": "exec < /dev/tty && yarn prepare-commit-msg" 26 | } 27 | }, 28 | "devDependencies": { 29 | "@commitlint/cli": "^17.6.7", 30 | "@commitlint/config-conventional": "^17.6.7", 31 | "@types/jest": "^29.5.3", 32 | "@types/node": "^20.4.2", 33 | "commitizen": "^4.3.0", 34 | "husky": "^8.0.3", 35 | "jest": "^29.6.1", 36 | "pinst": "^3.0.0", 37 | "ts-jest": "^29.1.1", 38 | "typescript": "^5.1.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/classes/cli.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import os from "os"; 3 | import { execSync } from "child_process"; 4 | 5 | const SSH_FOLDER_PATH = `${os.homedir()}/.ssh`; 6 | const SSH_CONFIG_FILE_PATH = `${SSH_FOLDER_PATH}/config`; 7 | const GIT_REGEX = /git@(.*):(.*)\/(.*).git/; 8 | 9 | export class CLI { 10 | public async createNewKey(keyAlias: string = "") { 11 | try { 12 | await this.createSSHKey(keyAlias); 13 | 14 | fs.existsSync(SSH_CONFIG_FILE_PATH) 15 | ? this.addHostToConfig(keyAlias) 16 | : this.createSSHConfigFile(keyAlias); 17 | } catch (error: any) { 18 | if (error instanceof Error) { 19 | console.error( 20 | "An error occurred while creating a new key:", 21 | error.message 22 | ); 23 | } else { 24 | console.error(error); 25 | } 26 | } 27 | } 28 | 29 | public printCurrentIdentity() { 30 | if (!this.isGitRepo()) { 31 | console.error("Current directory is not a git repository."); 32 | return; 33 | } 34 | 35 | const gitRepoUrl = this.getGitRepoUrl(); 36 | 37 | const match = gitRepoUrl.match(GIT_REGEX); 38 | 39 | match && match[1] 40 | ? console.log(`Current identity: ${match[1]}`) 41 | : console.error( 42 | "Could not find an established identity for this repository." 43 | ); 44 | } 45 | 46 | public listAllIdentities(): void { 47 | if (!fs.existsSync(SSH_CONFIG_FILE_PATH)) { 48 | console.error("SSH config file does not exist."); 49 | return; 50 | } 51 | 52 | const data = fs.readFileSync(SSH_CONFIG_FILE_PATH, "utf8"); 53 | const identities = data.match(/Host (.*)\n/g); 54 | 55 | identities && identities.length > 0 56 | ? identities.map((identity: string) => { 57 | console.log(`- ${identity.replace("Host ", "").replace("\n", "")}`); 58 | }) 59 | : console.error("No identities found."); 60 | } 61 | 62 | public changeIdentity(identity: string = ""): void { 63 | if (!this.isGitRepo()) { 64 | console.error("This directory is not a git repository."); 65 | return; 66 | } 67 | 68 | if (!this.isIdentityAvaialble(identity)) { 69 | console.error(`Requested identity '${identity}' is not available.`); 70 | return; 71 | } 72 | 73 | const originalRepoUrl = this.getGitRepoUrl(); 74 | const match = originalRepoUrl.match(GIT_REGEX); 75 | 76 | if (!match || !match[1] || !match[3]) { 77 | console.error( 78 | "Could not find an established identity for this repository." 79 | ); 80 | return; 81 | } else if (match[1] === identity) { 82 | console.error(`Identity '${identity}' is already in use.`); 83 | return; 84 | } 85 | 86 | const newRepoUrl = `git@${identity}:${match[2]}/${match[3]}.git`; 87 | execSync(`git remote set-url origin ${newRepoUrl}`, { stdio: "inherit" }); 88 | console.log(`Identity changed to '${identity}' successfully.`); 89 | } 90 | 91 | public showPublicKey(identity: string = ""): void { 92 | if (!this.isIdentityAvaialble(identity)) { 93 | console.error(`Requested identity '${identity}' is not available.`); 94 | return; 95 | } 96 | 97 | if (!fs.existsSync(SSH_CONFIG_FILE_PATH)) { 98 | console.error("SSH config file does not exist."); 99 | return; 100 | } 101 | 102 | const data = fs.readFileSync(SSH_CONFIG_FILE_PATH, "utf8"); 103 | 104 | const hosts = data.split("Host "); 105 | const host = hosts.find((host) => host.startsWith(identity)); 106 | 107 | if (!host) { 108 | console.error(`Requested identity '${identity}' is not available.`); 109 | return; 110 | } 111 | 112 | const identityFileMatch = host.match(/IdentityFile (.*)\n/); 113 | 114 | if (identityFileMatch && identityFileMatch[1]) { 115 | let publicKeyPath = identityFileMatch[1].trim(); 116 | 117 | publicKeyPath = publicKeyPath.replace("~", os.homedir()); 118 | if (!publicKeyPath.endsWith(".pub")) { 119 | publicKeyPath += ".pub"; 120 | } 121 | 122 | if (fs.existsSync(publicKeyPath)) { 123 | const publicKey = fs.readFileSync(publicKeyPath, "utf8"); 124 | console.log(publicKey); 125 | } else { 126 | console.error("Could not find public key."); 127 | } 128 | } 129 | } 130 | 131 | private async createSSHKey(keyAlias: string) { 132 | try { 133 | execSync( 134 | `ssh-keygen -t ed25519 -C "${os.hostname()}" -f "${SSH_FOLDER_PATH}/gitid_${keyAlias}"`, 135 | { stdio: "inherit" } 136 | ); 137 | console.log("SSH key created successfully."); 138 | } catch (err) { 139 | console.error( 140 | "An error has occured while creating the SSH key: ", 141 | (err as any).message 142 | ); 143 | } 144 | } 145 | 146 | private async addHostToConfig(keyAlias: string) { 147 | const data = fs.readFileSync(SSH_CONFIG_FILE_PATH, "utf8"); 148 | 149 | if (data.indexOf(keyAlias) > -1) { 150 | console.error( 151 | `A host name already exists with the same name. ${keyAlias}. \ 152 | Please try removing it or try a different name.` 153 | ); 154 | } else { 155 | const newHost = this.buildHostString(keyAlias); 156 | await fs.appendFileSync(SSH_CONFIG_FILE_PATH, newHost); 157 | console.log(`New host '${keyAlias}' added successfully.`); 158 | } 159 | } 160 | 161 | private buildHostString(keyAlias: string): string { 162 | return ( 163 | "\n" + 164 | `Host ${keyAlias}\n` + 165 | ` HostName github.com\n` + 166 | ` User git\n` + 167 | ` IdentityFile ${SSH_FOLDER_PATH}/gitid_${keyAlias}\n` + 168 | ` IdentitiesOnly yes\n` 169 | ); 170 | } 171 | 172 | private createSSHConfigFile(keyAlias: string): void { 173 | const newHost = this.buildHostString(keyAlias); 174 | 175 | fs.writeFileSync(SSH_CONFIG_FILE_PATH, newHost); 176 | console.log("New Host added successfully."); 177 | } 178 | 179 | private isGitRepo(): boolean { 180 | try { 181 | return ( 182 | execSync("git rev-parse --is-inside-work-tree", { 183 | encoding: "utf8", 184 | stdio: "pipe", 185 | }) 186 | .toString() 187 | .trim() === "true" 188 | ); 189 | } catch { 190 | return false; 191 | } 192 | } 193 | 194 | private isIdentityAvaialble(identity: string): boolean { 195 | if (!fs.existsSync(SSH_CONFIG_FILE_PATH)) { 196 | return false; 197 | } 198 | 199 | const data = fs.readFileSync(SSH_CONFIG_FILE_PATH, "utf8"); 200 | return data.includes(identity); 201 | } 202 | 203 | private getGitRepoUrl(): string { 204 | return execSync("git remote get-url origin", { 205 | encoding: "utf8", 206 | stdio: "pipe", 207 | }).toString(); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { CLI } from "./classes/cli"; 4 | 5 | const cli = new CLI(); 6 | 7 | const indices = { 8 | COMMAND_INDEX: 2, 9 | OPTION_INDEX: 3, 10 | }; 11 | 12 | const command = process.argv[indices.COMMAND_INDEX] || ""; 13 | const option = process.argv[indices.OPTION_INDEX] || ""; 14 | 15 | const commandsMap: { 16 | [key: string]: (arg?: string) => void | Promise; 17 | } = { 18 | new: cli.createNewKey.bind(cli), 19 | status: cli.printCurrentIdentity.bind(cli), 20 | list: cli.listAllIdentities.bind(cli), 21 | current: cli.printCurrentIdentity.bind(cli), 22 | use: cli.changeIdentity.bind(cli), 23 | show: cli.showPublicKey.bind(cli), 24 | }; 25 | 26 | commandsMap[command] 27 | ? commandsMap[command](option) 28 | : console.log( 29 | "Usage: gitid