@savaryna/git-add-account 1.0.2 → 2.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/helpers/file.js CHANGED
@@ -1,19 +1,19 @@
1
- const { access, appendFile, constants, mkdir, readFile, unlink, writeFile } = require('fs');
2
- const { homedir } = require('os');
1
+ const { access, constants, mkdir, readFile, rm, writeFile } = require('fs');
2
+ const { homedir, platform } = require('os');
3
3
  const { promisify } = require('util');
4
4
  const { resolve } = require('path');
5
5
 
6
- module.exports.append = promisify(appendFile);
6
+ module.exports.createFile = (path, data) => promisify(writeFile)(path, data, 'utf8');
7
7
 
8
- module.exports.createEmptyFile = (path) => promisify(writeFile)(path, '');
8
+ module.exports.readFile = promisify(readFile);
9
9
 
10
10
  module.exports.home = homedir();
11
11
 
12
- module.exports.mkdir = promisify(mkdir);
12
+ module.exports.platform = platform();
13
13
 
14
- module.exports.readFile = promisify(readFile);
14
+ module.exports.mkdir = promisify(mkdir);
15
15
 
16
- module.exports.remove = promisify(unlink);
16
+ module.exports.remove = (path) => promisify(rm)(path, { recursive: true, force: true });
17
17
 
18
18
  module.exports.resolve = resolve;
19
19
 
@@ -1,4 +1,11 @@
1
- const prompts = require('prompts');
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]);
2
9
 
3
10
  const exit = (code = 0, reason = null) => {
4
11
  console.log(reason ? `\n${reason}\n` : '');
@@ -6,6 +13,98 @@ const exit = (code = 0, reason = null) => {
6
13
  process.exit(code);
7
14
  };
8
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
+
9
106
  module.exports.exit = exit;
10
107
 
11
- module.exports.default = (questions, options) => prompts(questions, { onCancel: () => exit(1), ...options });
108
+ module.exports.overwritePathPrompt = overwritePathPrompt;
109
+
110
+ module.exports.default = cliPrompts;
package/index.js CHANGED
@@ -1,149 +1,85 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { default: exec } = require('./helpers/exec');
4
- const { append, createEmptyFile, hasReadWriteAccess, home, mkdir, readFile, remove, resolve } = require('./helpers/file');
5
- const { default: prompts, exit } = require('./helpers/prompts');
6
- const { default: validate, z } = require('./helpers/validate');
7
-
8
- const sshDirPath = resolve(home, '.ssh');
9
- const sshConfigPath = resolve(sshDirPath, 'config');
10
-
11
- const overwriteFilePrompt = (path) =>
12
- prompts({
13
- type: 'toggle',
14
- name: 'overwrite',
15
- message: `File ${path} already exists. Overwrite?`,
16
- initial: false,
17
- active: 'yes',
18
- inactive: 'no',
19
- });
4
+ const { createFile, hasReadWriteAccess, platform, mkdir, readFile, remove } = require('./helpers/file');
5
+ const { default: prompts, overwritePathPrompt, exit } = require('./helpers/prompts');
20
6
 
21
7
  async function main() {
22
- const { name, email, workspace } = await prompts([
23
- {
24
- type: 'text',
25
- name: 'name',
26
- message: 'Name to use for this account:',
27
- validate: validate(z.string()),
28
- format: (value) => ({
29
- original: value,
30
- camel: value.toLowerCase().replace(/[^\w]/g, '_'),
31
- }),
32
- },
33
- {
34
- type: 'text',
35
- name: 'email',
36
- message: 'Email to use for this account:',
37
- validate: validate(z.string().email()),
38
- format: (value) => ({
39
- address: value,
40
- domain: value.match(/(?<=@).+(?=\.)/)[0].replace(/[^\w]/g, '_'),
41
- }),
42
- },
43
- {
44
- type: 'text',
45
- name: 'workspace',
46
- message: 'Workspace to use for this account:',
47
- initial: (email) => resolve(home, `code/${email.domain}`),
48
- validate: validate(z.string()),
49
- },
50
- ]);
51
-
52
- const workspaceGitConfigPath = resolve(workspace, '.gitconfig');
8
+ const { name, email, host, workspace, signYourWork } = await prompts();
53
9
 
54
- if (await hasReadWriteAccess(workspaceGitConfigPath)) {
55
- const { overwrite } = await overwriteFilePrompt(workspaceGitConfigPath);
10
+ // Check already existing workspace config
11
+ if (await hasReadWriteAccess(workspace.config)) {
12
+ const { overwrite } = await overwritePathPrompt(workspace.config);
56
13
 
57
14
  if (overwrite) {
58
- await remove(workspaceGitConfigPath);
15
+ await remove(workspace.config);
59
16
  } else {
60
17
  exit(1);
61
18
  }
62
19
  }
63
20
 
64
- const { sshKeyFileName } = await prompts([
65
- {
66
- type: 'text',
67
- name: 'sshKeyFileName',
68
- message: 'Name to use for SSH keys:',
69
- initial: `${email.domain}_${name.camel}`,
70
- validate: validate(z.string()),
71
- format: (value) => `git_${value}`,
72
- },
73
- ]);
74
-
75
- const sshKeyPath = resolve(sshDirPath, sshKeyFileName);
76
-
77
- if (await hasReadWriteAccess(sshKeyPath)) {
78
- const { overwrite } = await overwriteFilePrompt(sshKeyPath);
79
-
80
- if (overwrite) {
81
- await remove(sshKeyPath);
82
- } else {
83
- exit(1);
84
- }
85
- }
21
+ // Create workspace/config dir
22
+ await mkdir(workspace.config, { recursive: true });
86
23
 
87
24
  // Generate ssh key
88
- await exec(`ssh-keygen -t ed25519 -C "${email.address}" -f ${sshKeyPath}`);
25
+ await exec(`ssh-keygen -t ed25519 -C "${email}" -f ${workspace.privateKey}`);
89
26
 
90
- // Check to see if the user entered a passphrase
91
- const hasPassphrase = await exec(`ssh-keygen -y -P "" -f ${sshKeyPath}`).then(
92
- () => false,
93
- () => true
94
- );
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');
95
34
 
35
+ // Create sshconfig for the workspace
96
36
  const sshConfig = `
97
- # Config for GIT account ${email.address}
98
- Host *
37
+ # Config for GIT account ${email}
38
+ Host ${host.value}
39
+ HostName ${host.value}
40
+ User git
99
41
  AddKeysToAgent yes
100
- ${hasPassphrase ? 'UseKeychain yes' : ''}
101
- IdentityFile ${sshKeyPath}
42
+ ${useKeychain ? 'UseKeychain yes' : ''}
43
+ IdentitiesOnly yes
44
+ IdentityFile ${workspace.privateKey}
102
45
  `.replace(/\n\s{4}/g, '\n');
103
46
 
104
- // Add account to the ssh config
105
- await append(sshConfigPath, sshConfig);
106
-
107
- // Create workspace dir if it does not exist
108
- await mkdir(workspace, { recursive: true });
109
-
110
- // Create .gitconfig for the workspace
111
- await createEmptyFile(workspaceGitConfigPath);
112
-
113
- // Set user details
114
- await exec(`git config --file ${workspaceGitConfigPath} user.name "${name.original}"`);
115
- await exec(`git config --file ${workspaceGitConfigPath} user.email "${email.address}"`);
116
-
117
- // Set default ssh command
118
- await exec(`git config --file ${workspaceGitConfigPath} core.sshCommand "ssh -i ${sshKeyPath}"`);
119
-
120
- const { signYourWork } = await prompts({
121
- type: 'toggle',
122
- name: 'signYourWork',
123
- message: 'Do you want to sign your work?',
124
- initial: true,
125
- active: 'yes',
126
- inactive: 'no',
127
- });
47
+ await createFile(workspace.sshConfig, sshConfig);
48
+
49
+ // Create gitconfig for the workspace
50
+ const gitConfig = `
51
+ [user]
52
+ name = ${name.value}
53
+ email = ${email}
54
+ [core]
55
+ sshCommand = ssh -F ${workspace.sshConfig}
56
+ ${
57
+ !signYourWork
58
+ ? ''
59
+ : `
60
+ [gpg]
61
+ format = ssh
62
+ [commit]
63
+ gpgsign = true
64
+ [push]
65
+ gpgsign = if-asked
66
+ [tag]
67
+ gpgsign = true
68
+ [user]
69
+ signingkey = ${workspace.privateKey}
70
+ `
71
+ }
72
+ `.replace(/\n\s{4}/g, '\n');
128
73
 
129
- // Enable signing
130
- if (signYourWork) {
131
- await exec(`git config --file ${workspaceGitConfigPath} gpg.format ssh`);
132
- await exec(`git config --file ${workspaceGitConfigPath} commit.gpgsign true`);
133
- await exec(`git config --file ${workspaceGitConfigPath} push.gpgsign if-asked`);
134
- await exec(`git config --file ${workspaceGitConfigPath} tag.gpgsign true`);
135
- await exec(`git config --file ${workspaceGitConfigPath} user.signingkey ${sshKeyPath}`);
136
- }
74
+ await createFile(workspace.gitConfig, gitConfig);
137
75
 
138
76
  // Include workspace config in the global config
139
- await exec(`git config --global includeIf.gitdir:${workspace}/.path ${workspaceGitConfigPath}`);
77
+ await exec(`git config --global includeIf.gitdir:${workspace.root}/.path ${workspace.gitConfig}`);
140
78
 
141
- const publicSshKeyPath = resolve(sshDirPath, `${sshKeyFileName}.pub`);
142
- // await exec(`pbcopy < ${publicSshKeyPath}`);
143
- const publicSshKey = await readFile(publicSshKeyPath).then((buffer) => buffer.toString().trim());
79
+ const publicKey = await readFile(workspace.publicKey).then((buffer) => buffer.toString().trim());
144
80
 
145
- console.log('\nYour public SSH key is: ', publicSshKey);
146
- console.log('You can also find it here: ', publicSshKeyPath);
81
+ console.log('\nYour public SSH key is: ', publicKey);
82
+ console.log('You can also find it here: ', workspace.publicKey);
147
83
  console.log('Add it to your favorite GIT provider and enjoy!');
148
84
  }
149
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savaryna/git-add-account",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
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.",
5
5
  "homepage": "https://github.com/savaryna/git-add-account#readme",
6
6
  "keywords": [
@@ -33,6 +33,9 @@
33
33
  "url": "git+https://github.com/savaryna/git-add-account.git"
34
34
  },
35
35
  "type": "commonjs",
36
+ "files": [
37
+ "helpers"
38
+ ],
36
39
  "bin": {
37
40
  "gaa": "index.js",
38
41
  "git-add-account": "index.js"
@@ -1,19 +0,0 @@
1
- # To get started with Dependabot version updates, you'll need to specify which
2
- # package ecosystems to update and where the package manifests are located.
3
- # Please see the documentation for all configuration options:
4
- # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5
-
6
- version: 2
7
- updates:
8
-
9
- # Maintain dependencies for GitHub Actions
10
- - package-ecosystem: "github-actions"
11
- directory: "/"
12
- schedule:
13
- interval: "monthly"
14
-
15
- # Maintain dependencies for npm
16
- - package-ecosystem: "npm"
17
- directory: "/"
18
- schedule:
19
- interval: "monthly"
@@ -1,21 +0,0 @@
1
- name: Publish package
2
-
3
- on:
4
- release:
5
- types: [created]
6
- workflow_dispatch:
7
-
8
- jobs:
9
- publish-npm:
10
- runs-on: macos-latest
11
- steps:
12
- - uses: actions/checkout@v3
13
- - uses: actions/setup-node@v3
14
- with:
15
- node-version: 16
16
- registry-url: https://registry.npmjs.org/
17
- scope: '@savaryna'
18
- - run: npm ci
19
- - run: npm publish --access=public
20
- env:
21
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/.prettierrc.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "arrowParens": "always",
3
- "bracketSameLine": false,
4
- "bracketSpacing": true,
5
- "embeddedLanguageFormatting": "auto",
6
- "htmlWhitespaceSensitivity": "css",
7
- "insertPragma": false,
8
- "jsxSingleQuote": false,
9
- "printWidth": 120,
10
- "proseWrap": "preserve",
11
- "quoteProps": "as-needed",
12
- "requirePragma": false,
13
- "semi": true,
14
- "singleAttributePerLine": false,
15
- "singleQuote": true,
16
- "tabWidth": 2,
17
- "trailingComma": "es5",
18
- "useTabs": false,
19
- "vueIndentScriptAndStyle": false
20
- }