@savaryna/git-add-account 2.0.1 → 2.1.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/README.md +4 -4
- package/bin/main.js +34 -0
- package/package.json +20 -8
- package/helpers/exec.js +0 -4
- package/helpers/file.js +0 -24
- package/helpers/prompts.js +0 -110
- package/helpers/validate.js +0 -9
- package/index.js +0 -89
package/README.md
CHANGED
|
@@ -44,8 +44,8 @@ After going through all the steps, you will be presented with your public SSH ke
|
|
|
44
44
|
|
|
45
45
|
A simple way to use multiple git accounts on one machine is to use different SSH configs based on the directory you are in. The way [@savaryna/add-git-account](https://www.npmjs.com/package/@savaryna/git-add-account) works is, it asks you for some basic information and then it creates some files under `.config` in the workspace directory you specified. Ex:
|
|
46
46
|
|
|
47
|
-
1. It creates a _(private/public)_ SSH keypair using `ssh-keygen -t ed25519 -C "john@github.com" -f /Users/john/code/work/.config/id_ed25519_git_github_com`. [See code](https://github.com/savaryna/git-add-account/blob/main/index.
|
|
48
|
-
1. It creates a `sshconfig` file. [See code](https://github.com/savaryna/git-add-account/blob/main/index.
|
|
47
|
+
1. It creates a _(private/public)_ SSH keypair using `ssh-keygen -t ed25519 -C "john@github.com" -f /Users/john/code/work/.config/id_ed25519_git_github_com`. [See code](https://github.com/savaryna/git-add-account/blob/main/src/index.ts#L29-L30).
|
|
48
|
+
1. It creates a `sshconfig` file. [See code](https://github.com/savaryna/git-add-account/blob/main/src/index.ts#L40-L48).
|
|
49
49
|
|
|
50
50
|
```ini
|
|
51
51
|
# File at /Users/john/code/work/.config/sshconfig
|
|
@@ -59,7 +59,7 @@ A simple way to use multiple git accounts on one machine is to use different SSH
|
|
|
59
59
|
IdentityFile /Users/john/code/work/.config/id_ed25519_git_github_com
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
1. It creates a `gitconfig` file. [See code](https://github.com/savaryna/git-add-account/blob/main/index.
|
|
62
|
+
1. It creates a `gitconfig` file. [See code](https://github.com/savaryna/git-add-account/blob/main/src/index.ts#L50-L58).
|
|
63
63
|
|
|
64
64
|
```ini
|
|
65
65
|
# File at /Users/john/code/work/.config/gitconfig
|
|
@@ -81,7 +81,7 @@ A simple way to use multiple git accounts on one machine is to use different SSH
|
|
|
81
81
|
signingkey = /Users/john/code/work/.config/id_ed25519_git_github_com
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
1. It runs `git config --global includeIf.gitdir:/Users/john/code/work/.path /Users/john/code/work/.config/gitconfig`, this makes sure that as long as you are in the workspace created earlier, **or any other subdirectory**, git will use the config from step **3** automatically[^3]. [See code](https://github.com/savaryna/git-add-account/blob/main/index.
|
|
84
|
+
1. It runs `git config --global includeIf.gitdir:/Users/john/code/work/.path /Users/john/code/work/.config/gitconfig`, this makes sure that as long as you are in the workspace created earlier, **or any other subdirectory**, git will use the config from step **3** automatically[^3]. [See code](https://github.com/savaryna/git-add-account/blob/main/src/index.ts#L60-L61).
|
|
85
85
|
1. And finally, it presents you with your public SSH key so you can copy it and add it to your GIT provider of choice.
|
|
86
86
|
|
|
87
87
|
## License
|
package/bin/main.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var $=Object.create;var y=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var F=Object.getPrototypeOf,L=Object.prototype.hasOwnProperty;var O=(t,e,s,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of A(e))!L.call(t,r)&&r!==s&&y(t,r,{get:()=>e[r],enumerable:!(o=H(e,r))||o.enumerable});return t};var d=(t,e,s)=>(s=t!=null?$(F(t)):{},O(e||!t||!t.__esModule?y(s,"default",{value:t,enumerable:!0}):s,t));var h=d(require("mustache"));var v=`# Config for GIT account {{{email}}}
|
|
3
|
+
Host {{{host.value}}}
|
|
4
|
+
HostName {{{host.value}}}
|
|
5
|
+
User git
|
|
6
|
+
AddKeysToAgent yes{{#useKeychain}}
|
|
7
|
+
UseKeychain yes{{/useKeychain}}
|
|
8
|
+
IdentitiesOnly yes
|
|
9
|
+
IdentityFile {{{workspace.privateKey}}}
|
|
10
|
+
`;var w=`# Config for GIT account {{{email}}}
|
|
11
|
+
[user]
|
|
12
|
+
name = {{{name.value}}}
|
|
13
|
+
email = {{{email}}}
|
|
14
|
+
[core]
|
|
15
|
+
sshCommand = ssh -F {{{workspace.sshConfig}}}
|
|
16
|
+
{{#signYourWork}}
|
|
17
|
+
[gpg]
|
|
18
|
+
format = ssh
|
|
19
|
+
[commit]
|
|
20
|
+
gpgsign = true
|
|
21
|
+
[push]
|
|
22
|
+
gpgsign = if-asked
|
|
23
|
+
[tag]
|
|
24
|
+
gpgsign = true
|
|
25
|
+
[user]
|
|
26
|
+
signingkey = {{{workspace.privateKey}}}
|
|
27
|
+
{{/signYourWork}}
|
|
28
|
+
`;var k=require("util"),x=require("child_process"),m=(0,k.promisify)(x.exec);var i=require("fs/promises"),n=require("os"),a=require("path"),u=(t,e)=>(0,i.writeFile)(t,e,"utf8"),P=t=>(0,i.rm)(t,{recursive:!0,force:!0}),C=t=>(0,i.access)(t,i.constants.R_OK|i.constants.W_OK).then(()=>!0,()=>!1);var c=require("zod"),G=t=>e=>{let{success:s,error:o}=t.safeParse(e);return s?!0:o.format()._errors[0]},l=G;var K=d(require("prompts")),R="2.34.0",j=()=>m("git --version").then(({stdout:t})=>t.split(" ")[2]),g=(t=0,e=null)=>{console.log(e?`
|
|
29
|
+
${e}
|
|
30
|
+
`:""),console.log(`${t?"\u{1F635} Exited.":"\u2728 Done."} Thanks for using @savaryna/git-add-account!
|
|
31
|
+
`),process.exit(t)},T=(t,e)=>(0,K.default)(t,{onCancel:()=>g(1),...e}),W=t=>T({type:"toggle",name:"overwrite",message:`Path ${t} already exists. Overwrite?`,initial:!1,active:"yes",inactive:"no"}),b=async()=>{let t=await j();return T([{type:"text",name:"name",message:"Your name:",validate:l(c.z.string()),format:e=>({value:e,camel:e.toLowerCase().replace(/[^\w]/g,"_")})},{type:"text",name:"email",message:"Email to use for this account:",validate:l(c.z.string().email())},{type:"text",name:"host",message:"Host to use for this account:",initial:e=>e.split("@")[1],validate:l(c.z.string().transform(e=>"https://"+e).refine(URL.canParse,{message:"Invalid host"})),format:e=>({value:e,camel:e.replace(/[^\w]/g,"_")})},{type:"text",name:"workspace",message:"Workspace to use for this account:",initial:e=>(0,a.resolve)((0,n.homedir)(),"code",e.camel),validate:l(c.z.string().refine(e=>!e.startsWith("~"),{message:'"~" is not supported'})),format:(e,{host:s})=>{let o=(0,a.resolve)((0,n.homedir)(),e),r=(0,a.resolve)(o,".config"),f=`id_ed25519_git_${s.camel}`;return{root:o,config:r,gitConfig:(0,a.resolve)(r,"gitconfig"),sshConfig:(0,a.resolve)(r,"sshconfig"),privateKey:(0,a.resolve)(r,f),publicKey:(0,a.resolve)(r,f+".pub")}}},{type:"toggle",name:"signYourWork",message:"Do you want to sign your work with SSH?",initial:!0,active:"yes",inactive:"no"},{type:e=>e&&t<R?"toggle":null,name:"signYourWork",initial:!0,message:`Your current git version (${t}) does not support SSH signing. Continue without?`,active:"yes",inactive:"no",format:e=>e?!e:g(1)}])};async function E(){let{name:t,email:e,host:s,workspace:o,signYourWork:r}=await b();if(await C(o.config)){let{overwrite:p}=await W(o.config);p?await P(o.config):g(1)}await(0,i.mkdir)(o.config,{recursive:!0}),await m(`ssh-keygen -t ed25519 -C "${e}" -f ${o.privateKey}`);let f=await m(`ssh-keygen -y -P "" -f ${o.privateKey}`).then(()=>!1,()=>!0).then(p=>p&&(0,n.platform)()==="darwin"),I=h.default.render(v,{email:e,host:s,useKeychain:f,workspace:o});await u(o.sshConfig,I);let Y=h.default.render(w,{name:t,email:e,workspace:o,signYourWork:r});await u(o.gitConfig,Y),await m(`git config --global includeIf.gitdir:${o.root}/.path ${o.gitConfig}`);let _=await(0,i.readFile)(o.publicKey).then(p=>p.toString().trim());console.log(`
|
|
32
|
+
Your public SSH key is:
|
|
33
|
+
`,_),console.log(`You can also find it here:
|
|
34
|
+
`,o.publicKey),console.log("Add it to your favorite GIT provider and enjoy!")}E().then(()=>g()).catch(t=>g(1,t.message));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savaryna/git-add-account",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "🔐 A small CLI app that allows you to easily add multiple GIT accounts on one machine. It switches between accounts automatically based on the workspace (directory) you are in.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "🔐 A small CLI app that allows you to easily add multiple GIT accounts on one machine. It switches between accounts automatically based on the workspace (directory/subdirectory) you are in.",
|
|
5
5
|
"homepage": "https://github.com/savaryna/git-add-account#readme",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"add",
|
|
@@ -34,19 +34,31 @@
|
|
|
34
34
|
},
|
|
35
35
|
"type": "commonjs",
|
|
36
36
|
"files": [
|
|
37
|
-
"
|
|
37
|
+
"bin"
|
|
38
38
|
],
|
|
39
39
|
"bin": {
|
|
40
|
-
"gaa": "
|
|
41
|
-
"git-add-account": "
|
|
40
|
+
"gaa": "bin/main.js",
|
|
41
|
+
"git-add-account": "bin/main.js"
|
|
42
42
|
},
|
|
43
|
-
"
|
|
43
|
+
"scripts": {
|
|
44
|
+
"link": "npm unlink -g && npm link",
|
|
45
|
+
"dev": "tsup --watch",
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"pretest": "npm run build",
|
|
48
|
+
"test": "npm publish --dry-run"
|
|
49
|
+
},
|
|
50
|
+
"source": "src/index.ts",
|
|
51
|
+
"main": "bin/main.js",
|
|
44
52
|
"dependencies": {
|
|
53
|
+
"mustache": "^4.2.0",
|
|
45
54
|
"prompts": "^2.4.2",
|
|
46
55
|
"zod": "^3.20.6"
|
|
47
56
|
},
|
|
48
57
|
"devDependencies": {
|
|
49
|
-
"@types/
|
|
50
|
-
"@types/
|
|
58
|
+
"@types/mustache": "^4.2.5",
|
|
59
|
+
"@types/node": "^22.5.2",
|
|
60
|
+
"@types/prompts": "^2.4.2",
|
|
61
|
+
"tsup": "^8.2.4",
|
|
62
|
+
"typescript": "^5.5.4"
|
|
51
63
|
}
|
|
52
64
|
}
|
package/helpers/exec.js
DELETED
package/helpers/file.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const { access, constants, mkdir, readFile, rm, writeFile } = require('fs');
|
|
2
|
-
const { homedir, platform } = require('os');
|
|
3
|
-
const { promisify } = require('util');
|
|
4
|
-
const { resolve } = require('path');
|
|
5
|
-
|
|
6
|
-
module.exports.createFile = (path, data) => promisify(writeFile)(path, data, 'utf8');
|
|
7
|
-
|
|
8
|
-
module.exports.readFile = promisify(readFile);
|
|
9
|
-
|
|
10
|
-
module.exports.home = homedir();
|
|
11
|
-
|
|
12
|
-
module.exports.platform = platform();
|
|
13
|
-
|
|
14
|
-
module.exports.mkdir = promisify(mkdir);
|
|
15
|
-
|
|
16
|
-
module.exports.remove = (path) => promisify(rm)(path, { recursive: true, force: true });
|
|
17
|
-
|
|
18
|
-
module.exports.resolve = resolve;
|
|
19
|
-
|
|
20
|
-
module.exports.hasReadWriteAccess = (path) =>
|
|
21
|
-
promisify(access)(path, constants.R_OK | constants.W_OK).then(
|
|
22
|
-
() => true,
|
|
23
|
-
() => false
|
|
24
|
-
);
|
package/helpers/prompts.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
const { default: exec } = require('./exec');
|
|
2
|
-
const { home, resolve } = require('./file');
|
|
3
|
-
const { default: validate, z } = require('./validate');
|
|
4
|
-
const defaultPrompts = require('prompts');
|
|
5
|
-
|
|
6
|
-
const MIN_GIT_VERSION = '2.34.0'; // Lower versions don't support SSH for GPG signing
|
|
7
|
-
|
|
8
|
-
const getGitVersion = () => exec('git --version').then(({ stdout }) => stdout.split(' ')[2]);
|
|
9
|
-
|
|
10
|
-
const exit = (code = 0, reason = null) => {
|
|
11
|
-
console.log(reason ? `\n${reason}\n` : '');
|
|
12
|
-
console.log(`${code ? '😵 Exited.' : '✨ Done.'} Thanks for using @savaryna/git-add-account!\n`);
|
|
13
|
-
process.exit(code);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Add onCancel handler for all prompts
|
|
17
|
-
const prompts = (questions, options) => defaultPrompts(questions, { onCancel: () => exit(1), ...options });
|
|
18
|
-
|
|
19
|
-
const overwritePathPrompt = (path) =>
|
|
20
|
-
prompts({
|
|
21
|
-
type: 'toggle',
|
|
22
|
-
name: 'overwrite',
|
|
23
|
-
message: `Path ${path} already exists. Overwrite?`,
|
|
24
|
-
initial: false,
|
|
25
|
-
active: 'yes',
|
|
26
|
-
inactive: 'no',
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const cliPrompts = () =>
|
|
30
|
-
prompts([
|
|
31
|
-
{
|
|
32
|
-
type: 'text',
|
|
33
|
-
name: 'name',
|
|
34
|
-
message: 'Your name:',
|
|
35
|
-
validate: validate(z.string()),
|
|
36
|
-
format: (value) => ({
|
|
37
|
-
value,
|
|
38
|
-
camel: value.toLowerCase().replace(/[^\w]/g, '_'),
|
|
39
|
-
}),
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
type: 'text',
|
|
43
|
-
name: 'email',
|
|
44
|
-
message: 'Email to use for this account:',
|
|
45
|
-
validate: validate(z.string().email()),
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: 'text',
|
|
49
|
-
name: 'host',
|
|
50
|
-
message: 'Host to use for this account:',
|
|
51
|
-
initial: (email) => email.split('@')[1],
|
|
52
|
-
validate: validate(
|
|
53
|
-
z
|
|
54
|
-
.string()
|
|
55
|
-
.transform((h) => 'https://' + h)
|
|
56
|
-
.refine(URL.canParse, { message: 'Invalid host' })
|
|
57
|
-
),
|
|
58
|
-
format: (value) => ({
|
|
59
|
-
value,
|
|
60
|
-
camel: value.replace(/[^\w]/g, '_'),
|
|
61
|
-
}),
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
type: 'text',
|
|
65
|
-
name: 'workspace',
|
|
66
|
-
message: 'Workspace to use for this account:',
|
|
67
|
-
initial: (host) => resolve(home, 'code', host.camel),
|
|
68
|
-
validate: validate(z.string().refine((value) => !value.startsWith('~'), { message: '"~" is not supported' })),
|
|
69
|
-
format: (value, { host }) => {
|
|
70
|
-
const root = resolve(home, value);
|
|
71
|
-
const config = resolve(root, '.config');
|
|
72
|
-
const sshKeyFileName = `id_ed25519_git_${host.camel}`;
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
root,
|
|
76
|
-
config,
|
|
77
|
-
gitConfig: resolve(config, 'gitconfig'),
|
|
78
|
-
sshConfig: resolve(config, 'sshconfig'),
|
|
79
|
-
privateKey: resolve(config, sshKeyFileName),
|
|
80
|
-
publicKey: resolve(config, sshKeyFileName + '.pub'),
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
type: 'toggle',
|
|
86
|
-
name: 'signYourWork',
|
|
87
|
-
message: 'Do you want to sign your work with SSH?',
|
|
88
|
-
initial: true,
|
|
89
|
-
active: 'yes',
|
|
90
|
-
inactive: 'no',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
type: async (prev) => {
|
|
94
|
-
this.gitVersion = await getGitVersion();
|
|
95
|
-
return prev && this.gitVersion < MIN_GIT_VERSION ? 'toggle' : null;
|
|
96
|
-
},
|
|
97
|
-
name: 'signYourWork',
|
|
98
|
-
message: () => `Your current git version (${this.gitVersion}) does not support SSH signing. Continue without?`,
|
|
99
|
-
initial: true,
|
|
100
|
-
active: 'yes',
|
|
101
|
-
inactive: 'no',
|
|
102
|
-
format: (value) => (value ? !value : exit(1)),
|
|
103
|
-
},
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
module.exports.exit = exit;
|
|
107
|
-
|
|
108
|
-
module.exports.overwritePathPrompt = overwritePathPrompt;
|
|
109
|
-
|
|
110
|
-
module.exports.default = cliPrompts;
|
package/helpers/validate.js
DELETED
package/index.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { default: exec } = require('./helpers/exec');
|
|
4
|
-
const { createFile, hasReadWriteAccess, platform, mkdir, readFile, remove } = require('./helpers/file');
|
|
5
|
-
const { default: prompts, overwritePathPrompt, exit } = require('./helpers/prompts');
|
|
6
|
-
|
|
7
|
-
async function main() {
|
|
8
|
-
const { name, email, host, workspace, signYourWork } = await prompts();
|
|
9
|
-
|
|
10
|
-
// Check already existing workspace config
|
|
11
|
-
if (await hasReadWriteAccess(workspace.config)) {
|
|
12
|
-
const { overwrite } = await overwritePathPrompt(workspace.config);
|
|
13
|
-
|
|
14
|
-
if (overwrite) {
|
|
15
|
-
await remove(workspace.config);
|
|
16
|
-
} else {
|
|
17
|
-
exit(1);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Create workspace/config dir
|
|
22
|
-
await mkdir(workspace.config, { recursive: true });
|
|
23
|
-
|
|
24
|
-
// Generate ssh key
|
|
25
|
-
await exec(`ssh-keygen -t ed25519 -C "${email}" -f ${workspace.privateKey}`);
|
|
26
|
-
|
|
27
|
-
// Use keychain if the system is MacOS and a passphrase was used
|
|
28
|
-
const useKeychain = await exec(`ssh-keygen -y -P "" -f ${workspace.privateKey}`)
|
|
29
|
-
.then(
|
|
30
|
-
() => false,
|
|
31
|
-
() => true
|
|
32
|
-
)
|
|
33
|
-
.then((hasPassphrase) => hasPassphrase && platform === 'darwin');
|
|
34
|
-
|
|
35
|
-
// Create sshconfig for the workspace
|
|
36
|
-
const sshConfig = `
|
|
37
|
-
# Config for GIT account ${email}
|
|
38
|
-
Host ${host.value}
|
|
39
|
-
HostName ${host.value}
|
|
40
|
-
User git
|
|
41
|
-
AddKeysToAgent yes
|
|
42
|
-
${useKeychain ? 'UseKeychain yes' : ''}
|
|
43
|
-
IdentitiesOnly yes
|
|
44
|
-
IdentityFile ${workspace.privateKey}
|
|
45
|
-
`.replace(/\n\s{4}/g, '\n');
|
|
46
|
-
|
|
47
|
-
await createFile(workspace.sshConfig, sshConfig);
|
|
48
|
-
|
|
49
|
-
// Create gitconfig for the workspace
|
|
50
|
-
const gitConfig = `
|
|
51
|
-
# Config for GIT account ${email}
|
|
52
|
-
[user]
|
|
53
|
-
name = ${name.value}
|
|
54
|
-
email = ${email}
|
|
55
|
-
[core]
|
|
56
|
-
sshCommand = ssh -F ${workspace.sshConfig}
|
|
57
|
-
${
|
|
58
|
-
!signYourWork
|
|
59
|
-
? ''
|
|
60
|
-
: `
|
|
61
|
-
[gpg]
|
|
62
|
-
format = ssh
|
|
63
|
-
[commit]
|
|
64
|
-
gpgsign = true
|
|
65
|
-
[push]
|
|
66
|
-
gpgsign = if-asked
|
|
67
|
-
[tag]
|
|
68
|
-
gpgsign = true
|
|
69
|
-
[user]
|
|
70
|
-
signingkey = ${workspace.privateKey}
|
|
71
|
-
`
|
|
72
|
-
}
|
|
73
|
-
`.replace(/\n\s{4}/g, '\n');
|
|
74
|
-
|
|
75
|
-
await createFile(workspace.gitConfig, gitConfig);
|
|
76
|
-
|
|
77
|
-
// Include workspace config in the global config
|
|
78
|
-
await exec(`git config --global includeIf.gitdir:${workspace.root}/.path ${workspace.gitConfig}`);
|
|
79
|
-
|
|
80
|
-
const publicKey = await readFile(workspace.publicKey).then((buffer) => buffer.toString().trim());
|
|
81
|
-
|
|
82
|
-
console.log('\nYour public SSH key is: ', publicKey);
|
|
83
|
-
console.log('You can also find it here: ', workspace.publicKey);
|
|
84
|
-
console.log('Add it to your favorite GIT provider and enjoy!');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
main()
|
|
88
|
-
.then(() => exit())
|
|
89
|
-
.catch((error) => exit(1, error.message));
|