@dotenvx/dotenvx 0.7.4 → 0.8.1

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 CHANGED
@@ -112,6 +112,16 @@ More examples
112
112
  Hello World
113
113
  ```
114
114
 
115
+ </details>
116
+ * <details><summary>Bash 🖥️</summary><br>
117
+
118
+ ```sh
119
+ $ echo "HELLO=World" > .env
120
+
121
+ $ dotenvx run --quiet -- sh -c 'echo $HELLO'
122
+ World
123
+ ```
124
+
115
125
  </details>
116
126
  * <details><summary>Frameworks ▲</summary><br>
117
127
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.4",
2
+ "version": "0.8.1",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -24,8 +24,17 @@
24
24
  "test": "jest --verbose"
25
25
  },
26
26
  "dependencies": {
27
+ "@inquirer/prompts": "^3.3.0",
28
+ "axios": "^1.6.2",
29
+ "chalk": "^4.1.2",
30
+ "clipboardy": "^2.3.0",
27
31
  "commander": "^11.1.0",
32
+ "conf": "^10.2.0",
28
33
  "dotenv": "^16.3.1",
34
+ "open": "^8.4.2",
35
+ "ora": "^5.4.1",
36
+ "qrcode-terminal": "^0.12.0",
37
+ "update-notifier": "^5.1.0",
29
38
  "execa": "^5.1.1",
30
39
  "winston": "^3.11.0",
31
40
  "xxhashjs": "^0.2.2"
@@ -0,0 +1,168 @@
1
+ const fs = require('fs')
2
+
3
+ const main = require('./../../lib/main')
4
+ const logger = require('./../../shared/logger')
5
+ const helpers = require('./../helpers')
6
+ const createSpinner = require('./../../shared/createSpinner')
7
+
8
+ const spinner = createSpinner('encrypting')
9
+
10
+ // constants
11
+ const ENCODING = 'utf8'
12
+
13
+ async function encrypt () {
14
+ spinner.start()
15
+ await helpers.sleep(500) // better dx
16
+
17
+ const options = this.opts()
18
+ logger.debug(`options: ${JSON.stringify(options)}`)
19
+
20
+ let optionEnvFile = options.envFile
21
+ if (!Array.isArray(optionEnvFile)) {
22
+ optionEnvFile = [optionEnvFile]
23
+ }
24
+
25
+ const addedKeys = new Set()
26
+ const addedVaults = new Set()
27
+ const addedEnvFilepaths = new Set()
28
+
29
+ // must be at least one .env* file
30
+ if (optionEnvFile.length < 1) {
31
+ spinner.fail('no .env* files found')
32
+ logger.help('? add one with [echo "HELLO=World" > .env] and then run [dotenvx encrypt]')
33
+ process.exit(1)
34
+ }
35
+
36
+ try {
37
+ logger.verbose(`generating .env.keys from ${optionEnvFile}`)
38
+
39
+ const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
40
+
41
+ for (const envFilepath of optionEnvFile) {
42
+ const filepath = helpers.resolvePath(envFilepath)
43
+ if (!fs.existsSync(filepath)) {
44
+ spinner.fail(`file does not exist at [${filepath}]`)
45
+ logger.help(`? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx encrypt]`)
46
+ process.exit(1)
47
+ }
48
+
49
+ const environment = helpers.guessEnvironment(filepath)
50
+ const key = `DOTENV_KEY_${environment.toUpperCase()}`
51
+
52
+ let value = dotenvKeys[key]
53
+
54
+ // first time seeing new DOTENV_KEY_${environment}
55
+ if (!value || value.length === 0) {
56
+ logger.verbose(`generating ${key}`)
57
+ value = helpers.generateDotenvKey(environment)
58
+ logger.debug(`generating ${key} as ${value}`)
59
+
60
+ dotenvKeys[key] = value
61
+
62
+ addedKeys.add(key) // for info logging to user
63
+ } else {
64
+ logger.verbose(`existing ${key}`)
65
+ logger.debug(`existing ${key} as ${value}`)
66
+ }
67
+ }
68
+
69
+ let keysData = `#/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/
70
+ #/ DOTENV_KEYs. DO NOT commit to source control /
71
+ #/ [how it works](https://dotenv.org/env-keys) /
72
+ #/--------------------------------------------------/\n`
73
+
74
+ for (const key in dotenvKeys) {
75
+ const value = dotenvKeys[key]
76
+ keysData += `${key}="${value}"\n`
77
+ }
78
+
79
+ fs.writeFileSync('.env.keys', keysData)
80
+ } catch (error) {
81
+ spinner.fail(error.message)
82
+ process.exit(1)
83
+ }
84
+
85
+ // used later in logging to user
86
+ const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
87
+
88
+ try {
89
+ logger.verbose(`generating .env.vault from ${optionEnvFile}`)
90
+
91
+ const dotenvVaults = (main.configDotenv({ path: '.env.vault' }).parsed || {})
92
+
93
+ for (const envFilepath of optionEnvFile) {
94
+ const filepath = helpers.resolvePath(envFilepath)
95
+ const environment = helpers.guessEnvironment(filepath)
96
+ const vault = `DOTENV_VAULT_${environment.toUpperCase()}`
97
+
98
+ let ciphertext = dotenvVaults[vault]
99
+ const dotenvKey = dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
100
+
101
+ if (!ciphertext || ciphertext.length === 0 || helpers.changed(ciphertext, dotenvKey, filepath, ENCODING)) {
102
+ logger.verbose(`encrypting ${vault}`)
103
+ ciphertext = helpers.encryptFile(filepath, dotenvKey, ENCODING)
104
+ logger.verbose(`encrypting ${vault} as ${ciphertext}`)
105
+
106
+ dotenvVaults[vault] = ciphertext
107
+
108
+ addedVaults.add(vault) // for info logging to user
109
+ addedEnvFilepaths.add(envFilepath) // for info logging to user
110
+ } else {
111
+ logger.verbose(`existing ${vault}`)
112
+ logger.debug(`existing ${vault} as ${ciphertext}`)
113
+ }
114
+ }
115
+
116
+ let vaultData = `#/-------------------.env.vault---------------------/
117
+ #/ cloud-agnostic vaulting standard /
118
+ #/ [how it works](https://dotenv.org/env-vault) /
119
+ #/--------------------------------------------------/\n\n`
120
+
121
+ for (const vault in dotenvVaults) {
122
+ const value = dotenvVaults[vault]
123
+ const environment = vault.replace('DOTENV_VAULT_', '').toLowerCase()
124
+ vaultData += `# ${environment}\n`
125
+ vaultData += `${vault}="${value}"\n\n`
126
+ }
127
+
128
+ fs.writeFileSync('.env.vault', vaultData)
129
+ } catch (e) {
130
+ spinner.fail(e.message)
131
+ process.exit(1)
132
+ }
133
+
134
+ if (addedEnvFilepaths.size > 0) {
135
+ spinner.succeed(`encrypted to .env.vault (${[...addedEnvFilepaths]})`)
136
+ logger.help2('ℹ commit .env.vault to code: [git commit -am ".env.vault"]')
137
+ } else {
138
+ spinner.done(`no changes (${optionEnvFile})`)
139
+ }
140
+
141
+ if (addedKeys.size > 0) {
142
+ spinner.succeed(`${helpers.pluralize('key', addedKeys.size)} added to .env.keys (${[...addedKeys]})`)
143
+ logger.help2('ℹ push .env.keys up to hub: [dotenvx hub push]')
144
+ }
145
+
146
+ if (addedVaults.size > 0) {
147
+ const DOTENV_VAULT_X = [...addedVaults][addedVaults.size - 1]
148
+ const DOTENV_KEY_X = DOTENV_VAULT_X.replace('_VAULT_', '_KEY_')
149
+ const tryKey = dotenvKeys[DOTENV_KEY_X] || '<dotenv_key_environment>'
150
+
151
+ logger.help2(`ℹ run [DOTENV_KEY='${tryKey}' dotenvx run -- yourcommand] to test decryption locally`)
152
+ }
153
+
154
+ // logger.verbose('')
155
+ // logger.verbose('next:')
156
+ // logger.verbose('')
157
+ // logger.verbose(' 1. commit .env.vault safely to code')
158
+ // logger.verbose(' 2. set DOTENV_KEY on server (or ci)')
159
+ // logger.verbose(' 3. push your code')
160
+ // logger.verbose('')
161
+ // logger.verbose('protips:')
162
+ // logger.verbose('')
163
+ // logger.verbose(' * .env.keys file holds your decryption DOTENV_KEYs')
164
+ // logger.verbose(' * DO NOT commit .env.keys to code')
165
+ // logger.verbose(' * share .env.keys file over secure channels only')
166
+ }
167
+
168
+ module.exports = encrypt
@@ -0,0 +1,105 @@
1
+ const open = require('open')
2
+ const axios = require('axios')
3
+ const clipboardy = require('clipboardy')
4
+ const { confirm } = require('@inquirer/prompts')
5
+
6
+ const createSpinner = require('./../../../shared/createSpinner')
7
+ const store = require('./../../../shared/store')
8
+ const logger = require('./../../../shared/logger')
9
+ const helpers = require('./../../helpers')
10
+
11
+ const OAUTH_CLIENT_ID = 'oac_dotenvxcli'
12
+
13
+ const spinner = createSpinner('waiting on user authorization')
14
+
15
+ async function pollTokenUrl (tokenUrl, deviceCode, interval) {
16
+ logger.http(`POST ${tokenUrl} with deviceCode ${deviceCode} at interval ${interval}`)
17
+
18
+ try {
19
+ const response = await axios.post(tokenUrl, {
20
+ client_id: OAUTH_CLIENT_ID,
21
+ device_code: deviceCode,
22
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
23
+ })
24
+
25
+ logger.http(response.data)
26
+
27
+ if (response.data.access_token) {
28
+ spinner.start()
29
+ store.setToken(response.data.full_username, response.data.access_token)
30
+ store.setHostname(response.data.hostname)
31
+ spinner.succeed(`logged in as ${response.data.username}`)
32
+ process.exit(0)
33
+ } else {
34
+ // continue polling if no access_token. shouldn't ever get here it server is implemented correctly
35
+ setTimeout(() => pollTokenUrl(tokenUrl, deviceCode, interval), interval * 1000)
36
+ }
37
+ } catch (error) {
38
+ if (error.response && error.response.data) {
39
+ logger.http(error.response.data)
40
+
41
+ // continue polling if authorization_pending
42
+ if (error.response.data.error === 'authorization_pending') {
43
+ setTimeout(() => pollTokenUrl(tokenUrl, deviceCode, interval), interval * 1000)
44
+ } else {
45
+ spinner.start()
46
+ spinner.fail(error.response.data.error_description)
47
+ process.exit(1)
48
+ }
49
+ } else {
50
+ spinner.start()
51
+ spinner.fail(error)
52
+ process.exit(1)
53
+ }
54
+ }
55
+ }
56
+
57
+ async function login () {
58
+ const options = this.opts()
59
+ logger.debug(`options: ${JSON.stringify(options)}`)
60
+
61
+ const hostname = options.hostname
62
+ const deviceCodeUrl = `${hostname}/oauth/device/code`
63
+ const tokenUrl = `${hostname}/oauth/token`
64
+
65
+ try {
66
+ const response = await axios.post(deviceCodeUrl, {
67
+ client_id: OAUTH_CLIENT_ID
68
+ })
69
+ const data = response.data
70
+ const deviceCode = data.device_code
71
+ const userCode = data.user_code
72
+ const verificationUri = data.verification_uri
73
+ const interval = data.interval
74
+
75
+ try { clipboardy.writeSync(userCode) } catch (_e) {}
76
+
77
+ // qrcode.generate(verificationUri, { small: true }) // too verbose
78
+
79
+ // begin polling
80
+ pollTokenUrl(tokenUrl, deviceCode, interval)
81
+
82
+ // optionally allow user to open browser
83
+ const answer = await confirm({ message: `press Enter to open [${verificationUri}] and enter code [${helpers.formatCode(userCode)}]...` })
84
+
85
+ if (answer) {
86
+ await open(verificationUri)
87
+
88
+ spinner.start()
89
+ }
90
+ } catch (error) {
91
+ if (error.response && error.response.data) {
92
+ logger.http(error.response.data)
93
+
94
+ spinner.start()
95
+ spinner.fail(error.response.data.error_description)
96
+ process.exit(1)
97
+ } else {
98
+ spinner.start()
99
+ spinner.fail(error.toString())
100
+ process.exit(1)
101
+ }
102
+ }
103
+ }
104
+
105
+ module.exports = login
@@ -0,0 +1,33 @@
1
+ const openBrowser = require('open')
2
+ const { confirm } = require('@inquirer/prompts')
3
+
4
+ const createSpinner = require('./../../../shared/createSpinner')
5
+ const logger = require('./../../../shared/logger')
6
+ const helpers = require('./../../helpers')
7
+
8
+ async function open () {
9
+ const options = this.opts()
10
+ logger.debug(`options: ${JSON.stringify(options)}`)
11
+
12
+ const hostname = options.hostname
13
+ const remoteOriginUrl = helpers.getRemoteOriginUrl()
14
+ const usernameName = helpers.extractUsernameName(remoteOriginUrl)
15
+
16
+ const openUrl = `${hostname}/gh/${usernameName}`
17
+
18
+ // optionally allow user to open browser
19
+ const answer = await confirm({ message: `press Enter to open [${openUrl}]...` })
20
+
21
+ if (answer) {
22
+ const spinner = createSpinner('opening')
23
+ spinner.start()
24
+
25
+ await helpers.sleep(500) // better dx
26
+
27
+ await openBrowser(openUrl)
28
+
29
+ spinner.succeed(`opened [${usernameName}]`)
30
+ }
31
+ }
32
+
33
+ module.exports = open
@@ -0,0 +1,111 @@
1
+ const fs = require('fs')
2
+ const { execSync } = require('child_process')
3
+ const axios = require('axios')
4
+
5
+ const store = require('./../../../shared/store')
6
+ const logger = require('./../../../shared/logger')
7
+ const helpers = require('./../../helpers')
8
+ const createSpinner = require('./../../../shared/createSpinner')
9
+
10
+ const spinner = createSpinner('pushing')
11
+
12
+ // constants
13
+ const ENCODING = 'utf8'
14
+
15
+ function isGitRepository () {
16
+ try {
17
+ // Redirect standard error to null to suppress Git error messages
18
+ const result = execSync('git rev-parse --is-inside-work-tree 2> /dev/null').toString().trim()
19
+ return result === 'true'
20
+ } catch (_error) {
21
+ return false
22
+ }
23
+ }
24
+
25
+ function isGithub (url) {
26
+ return url.includes('github.com')
27
+ }
28
+
29
+ // Create a simple-git instance for the current directory
30
+ async function push () {
31
+ spinner.start()
32
+ await helpers.sleep(500) // better dx
33
+
34
+ const options = this.opts()
35
+ logger.debug(`options: ${JSON.stringify(options)}`)
36
+
37
+ const hostname = options.hostname
38
+ const pushUrl = `${hostname}/v1/push`
39
+ const keysFilename = '.env.keys'
40
+ const vaultFilename = '.env.vault'
41
+
42
+ if (!isGitRepository()) {
43
+ spinner.fail('oops, must be a git repository')
44
+ logger.help('? create one with [git init .]')
45
+ process.exit(1)
46
+ }
47
+
48
+ const remoteOriginUrl = helpers.getRemoteOriginUrl()
49
+ if (!remoteOriginUrl) {
50
+ spinner.fail('oops, must have a remote origin (git remote -v)')
51
+ logger.help('? create it at [github.com/new] and then run [git remote add origin git@github.com:username/repository.git]')
52
+ process.exit(1)
53
+ }
54
+
55
+ if (!isGithub(remoteOriginUrl)) {
56
+ spinner.fail('oops, must be a github.com remote origin (git remote -v)')
57
+ logger.help('? create it at [github.com/new] and then run [git remote add origin git@github.com:username/repository.git]')
58
+ logger.help2('ℹ need support for other origins? [please tell us](https://github.com/dotenvx/dotenvx/issues)')
59
+ process.exit(1)
60
+ }
61
+
62
+ if (!fs.existsSync(keysFilename)) {
63
+ spinner.fail('oops, missing .env.keys file')
64
+ logger.help('? generate one with [dotenvx encrypt]')
65
+ logger.help2('ℹ a .env.keys file holds decryption keys for a .env.vault file')
66
+ process.exit(1)
67
+ }
68
+
69
+ if (!fs.existsSync(vaultFilename)) {
70
+ spinner.fail('oops, missing .env.vault file')
71
+ logger.help('? generate one with [dotenvx encrypt]')
72
+ logger.help2('ℹ a .env.vault file holds encrypted secrets per environment')
73
+ process.exit(1)
74
+ }
75
+
76
+ const oauthToken = store.getToken()
77
+ const dotenvKeysContent = fs.readFileSync(keysFilename, ENCODING)
78
+ const dotenvVaultContent = fs.readFileSync(vaultFilename, ENCODING)
79
+ const usernameName = helpers.extractUsernameName(remoteOriginUrl)
80
+
81
+ try {
82
+ const postData = {
83
+ username_name: usernameName,
84
+ DOTENV_KEYS: dotenvKeysContent,
85
+ DOTENV_VAULT: dotenvVaultContent
86
+ }
87
+ const options = {
88
+ headers: {
89
+ Authorization: `Bearer ${oauthToken}`
90
+ }
91
+ }
92
+ await axios.post(pushUrl, postData, options)
93
+ } catch (error) {
94
+ if (error.response && error.response.data) {
95
+ logger.http(error.response.data)
96
+ spinner.fail(error.response.data.error.message)
97
+ if (error.response.status === 404) {
98
+ logger.help(`? try visiting [${hostname}gh/${usernameName}] in your browser`)
99
+ }
100
+ process.exit(1)
101
+ } else {
102
+ spinner.fail(error.toString())
103
+ process.exit(1)
104
+ }
105
+ }
106
+
107
+ spinner.succeed(`pushed [${usernameName}]`)
108
+ logger.help2('ℹ run [dotenvx hub open] to view on hub')
109
+ }
110
+
111
+ module.exports = push
@@ -0,0 +1,8 @@
1
+ const store = require('./../../../shared/store')
2
+ const logger = require('./../../../shared/logger')
3
+
4
+ async function status () {
5
+ logger.info(`logged in to ${store.getHostname()} as ${store.getUsername()}`)
6
+ }
7
+
8
+ module.exports = status
@@ -0,0 +1,9 @@
1
+ const store = require('./../../../shared/store')
2
+ const logger = require('./../../../shared/logger')
3
+
4
+ async function token () {
5
+ logger.debug(store.configPath())
6
+ logger.blank(store.getToken())
7
+ }
8
+
9
+ module.exports = token
@@ -0,0 +1,108 @@
1
+ const fs = require('fs')
2
+ const logger = require('./../../shared/logger')
3
+ const helpers = require('./../helpers')
4
+ const main = require('./../../lib/main')
5
+
6
+ const ENCODING = 'utf8'
7
+
8
+ async function run () {
9
+ const options = this.opts()
10
+ logger.debug(`options: ${JSON.stringify(options)}`)
11
+
12
+ // load from .env.vault file
13
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
14
+ const filepath = helpers.resolvePath('.env.vault')
15
+
16
+ if (!fs.existsSync(filepath)) {
17
+ logger.error(`you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`)
18
+ } else {
19
+ logger.verbose(`loading env from encrypted ${filepath}`)
20
+
21
+ try {
22
+ const src = fs.readFileSync(filepath, { encoding: ENCODING })
23
+ const parsedVault = main.parse(src)
24
+
25
+ logger.debug(`decrypting encrypted env from ${filepath}`)
26
+ // handle scenario for comma separated keys - for use with key rotation
27
+ // example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
28
+ const dotenvKeys = process.env.DOTENV_KEY.split(',')
29
+ const length = dotenvKeys.length
30
+
31
+ let decrypted
32
+ for (let i = 0; i < length; i++) {
33
+ try {
34
+ // Get full dotenvKey
35
+ const dotenvKey = dotenvKeys[i].trim()
36
+
37
+ const key = helpers._parseEncryptionKeyFromDotenvKey(dotenvKey)
38
+ const ciphertext = helpers._parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
39
+
40
+ // Decrypt
41
+ decrypted = main.decrypt(ciphertext, key)
42
+
43
+ break
44
+ } catch (error) {
45
+ // last key
46
+ if (i + 1 >= length) {
47
+ throw error
48
+ }
49
+ // try next key
50
+ }
51
+ }
52
+ logger.debug(decrypted)
53
+ const parsed = main.parse(decrypted)
54
+ const result = main.inject(process.env, parsed, options.overload)
55
+
56
+ logger.successv(`injecting env (${result.injected.size}) from encrypted .env.vault`)
57
+ } catch (e) {
58
+ logger.error(e)
59
+ }
60
+ }
61
+ } else {
62
+ // convert to array if needed
63
+ let optionEnvFile = options.envFile
64
+ if (!Array.isArray(optionEnvFile)) {
65
+ optionEnvFile = [optionEnvFile]
66
+ }
67
+
68
+ const readableFilepaths = new Set()
69
+ const injected = new Set()
70
+
71
+ for (const envFilepath of optionEnvFile) {
72
+ const filepath = helpers.resolvePath(envFilepath)
73
+
74
+ logger.verbose(`loading env from ${filepath}`)
75
+
76
+ try {
77
+ const src = fs.readFileSync(filepath, { encoding: ENCODING })
78
+ const parsed = main.parse(src)
79
+ const result = main.inject(process.env, parsed, options.overload)
80
+
81
+ readableFilepaths.add(envFilepath)
82
+ result.injected.forEach(key => injected.add(key))
83
+ } catch (e) {
84
+ logger.warn(e)
85
+ }
86
+ }
87
+
88
+ if (readableFilepaths.size > 0) {
89
+ logger.successv(`injecting env (${injected.size}) from ${[...readableFilepaths]}`)
90
+ }
91
+ }
92
+
93
+ // Extract command and arguments after '--'
94
+ const commandIndex = process.argv.indexOf('--')
95
+ if (commandIndex === -1 || commandIndex === process.argv.length - 1) {
96
+ logger.error('missing command after [dotenvx run --]')
97
+ logger.error('')
98
+ logger.error(' get help: [dotenvx help run]')
99
+ logger.error(' or try: [dotenvx run -- npm run dev]')
100
+ process.exit(1)
101
+ } else {
102
+ const subCommand = process.argv.slice(commandIndex + 1)
103
+
104
+ await helpers.executeCommand(subCommand, process.env)
105
+ }
106
+ }
107
+
108
+ module.exports = run
@@ -0,0 +1,39 @@
1
+ const { Command } = require('commander')
2
+
3
+ const store = require('./../../shared/store')
4
+
5
+ const hub = new Command('hub')
6
+
7
+ hub
8
+ .description('Interact with the hub')
9
+
10
+ hub
11
+ .command('login')
12
+ .description('authenticate to dotenvx hub')
13
+ .option('-h, --hostname <url>', 'set hostname', store.getHostname())
14
+ .action(require('./../actions/hub/login'))
15
+
16
+ hub
17
+ .command('push')
18
+ .description('push .env.keys to dotenvx hub')
19
+ .option('-h, --hostname <url>', 'set hostname', store.getHostname())
20
+ .action(require('./../actions/hub/push'))
21
+
22
+ hub
23
+ .command('open')
24
+ .description('view repository on dotenvx hub')
25
+ .option('-h, --hostname <url>', 'set hostname', store.getHostname())
26
+ .action(require('./../actions/hub/open'))
27
+
28
+ hub
29
+ .command('token')
30
+ .description('print the auth token dotenvx hub is configured to use')
31
+ .option('-h, --hostname <url>', 'set hostname', 'https://hub.dotenvx.com')
32
+ .action(require('./../actions/hub/token'))
33
+
34
+ hub
35
+ .command('status')
36
+ .description('display logged in user')
37
+ .action(require('./../actions/hub/status'))
38
+
39
+ module.exports = hub
@@ -1,18 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs')
3
+ const updateNotifier = require('update-notifier')
4
4
  const { Command } = require('commander')
5
5
  const program = new Command()
6
6
 
7
- // constants
8
- const ENCODING = 'utf8'
9
-
10
7
  const logger = require('./../shared/logger')
11
8
  const helpers = require('./helpers')
12
9
  const examples = require('./examples')
13
10
  const { AppendToIgnores } = require('./ignores')
14
11
  const packageJson = require('./../shared/packageJson')
15
- const main = require('./../lib/main')
12
+
13
+ // once a day check for any updates
14
+ const notifier = updateNotifier({ pkg: packageJson })
15
+ if (notifier.update) {
16
+ logger.warn(`Update available ${notifier.update.current} → ${notifier.update.latest} [see changelog](dotenvx.com/changelog)`)
17
+ }
16
18
 
17
19
  // global log levels
18
20
  program
@@ -58,266 +60,16 @@ program.command('run')
58
60
  .addHelpText('after', examples.run)
59
61
  .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env')
60
62
  .option('-o, --overload', 'override existing env variables')
61
- .action(async function () {
62
- const options = this.opts()
63
- logger.debug('configuring options')
64
- logger.debug(options)
65
-
66
- // load from .env.vault file
67
- if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
68
- const filepath = helpers.resolvePath('.env.vault')
69
-
70
- if (!fs.existsSync(filepath)) {
71
- logger.error(`you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`)
72
- } else {
73
- logger.verbose(`loading env from encrypted ${filepath}`)
74
-
75
- try {
76
- logger.debug(`reading encrypted env from ${filepath}`)
77
- const src = fs.readFileSync(filepath, { encoding: ENCODING })
78
-
79
- logger.debug(`parsing encrypted env from ${filepath}`)
80
- const parsedVault = main.parse(src)
81
-
82
- logger.debug(`decrypting encrypted env from ${filepath}`)
83
- // handle scenario for comma separated keys - for use with key rotation
84
- // example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
85
- const dotenvKeys = process.env.DOTENV_KEY.split(',')
86
- const length = dotenvKeys.length
87
-
88
- let decrypted
89
- for (let i = 0; i < length; i++) {
90
- try {
91
- // Get full dotenvKey
92
- const dotenvKey = dotenvKeys[i].trim()
93
-
94
- const key = helpers._parseEncryptionKeyFromDotenvKey(dotenvKey)
95
- const ciphertext = helpers._parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
96
-
97
- // Decrypt
98
- decrypted = main.decrypt(ciphertext, key)
99
-
100
- break
101
- } catch (error) {
102
- // last key
103
- if (i + 1 >= length) {
104
- throw error
105
- }
106
- // try next key
107
- }
108
- }
109
- logger.debug(decrypted)
110
-
111
- logger.debug(`parsing decrypted env from ${filepath}`)
112
- const parsed = main.parse(decrypted)
113
-
114
- logger.debug(`writing decrypted env from ${filepath}`)
115
- const result = main.write(process.env, parsed, options.overload)
116
-
117
- logger.info(`loading env (${result.written.size}) from encrypted .env.vault`)
118
- } catch (e) {
119
- logger.error(e)
120
- }
121
- }
122
- } else {
123
- // convert to array if needed
124
- let optionEnvFile = options.envFile
125
- if (!Array.isArray(optionEnvFile)) {
126
- optionEnvFile = [optionEnvFile]
127
- }
128
-
129
- const readableFilepaths = new Set()
130
- const written = new Set()
131
-
132
- for (const envFilepath of optionEnvFile) {
133
- const filepath = helpers.resolvePath(envFilepath)
134
-
135
- logger.verbose(`loading env from ${filepath}`)
136
-
137
- try {
138
- logger.debug(`reading env from ${filepath}`)
139
- const src = fs.readFileSync(filepath, { encoding: ENCODING })
140
-
141
- logger.debug(`parsing env from ${filepath}`)
142
- const parsed = main.parse(src)
143
-
144
- logger.debug(`writing env from ${filepath}`)
145
- const result = main.write(process.env, parsed, options.overload)
146
-
147
- readableFilepaths.add(envFilepath)
148
- result.written.forEach(key => written.add(key))
149
- } catch (e) {
150
- logger.warn(e)
151
- }
152
- }
153
-
154
- if (readableFilepaths.size > 0) {
155
- logger.info(`loading env (${written.size}) from ${[...readableFilepaths]}`)
156
- }
157
- }
158
-
159
- // Extract command and arguments after '--'
160
- const commandIndex = process.argv.indexOf('--')
161
- if (commandIndex === -1 || commandIndex === process.argv.length - 1) {
162
- logger.error('missing command after [dotenvx run --]')
163
- logger.error('')
164
- logger.error(' get help: [dotenvx help run]')
165
- logger.error(' or try: [dotenvx run -- npm run dev]')
166
- process.exit(1)
167
- } else {
168
- const subCommand = process.argv.slice(commandIndex + 1)
169
-
170
- await helpers.executeCommand(subCommand, process.env)
171
- }
172
- })
63
+ .action(require('./actions/run'))
173
64
 
174
65
  // dotenvx encrypt
175
66
  program.command('encrypt')
176
67
  .description('encrypt .env.* to .env.vault')
177
68
  .addHelpText('after', examples.encrypt)
178
69
  .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', helpers.findEnvFiles('./'))
179
- .action(function () {
180
- const options = this.opts()
181
- logger.debug('configuring options')
182
- logger.debug(options)
183
-
184
- let optionEnvFile = options.envFile
185
- if (!Array.isArray(optionEnvFile)) {
186
- optionEnvFile = [optionEnvFile]
187
- }
188
-
189
- const addedKeys = new Set()
190
- const addedVaults = new Set()
191
- const addedEnvFilepaths = new Set()
192
-
193
- try {
194
- logger.verbose(`generating .env.keys from ${optionEnvFile}`)
70
+ .action(require('./actions/encrypt'))
195
71
 
196
- const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
197
-
198
- for (const envFilepath of optionEnvFile) {
199
- const filepath = helpers.resolvePath(envFilepath)
200
- if (!fs.existsSync(filepath)) {
201
- throw new Error(`file does not exist: ${filepath}`)
202
- }
203
-
204
- const environment = helpers.guessEnvironment(filepath)
205
- const key = `DOTENV_KEY_${environment.toUpperCase()}`
206
-
207
- let value = dotenvKeys[key]
208
-
209
- // first time seeing new DOTENV_KEY_${environment}
210
- if (!value || value.length === 0) {
211
- logger.verbose(`generating ${key}`)
212
- value = helpers.generateDotenvKey(environment)
213
- logger.debug(`generating ${key} as ${value}`)
214
-
215
- dotenvKeys[key] = value
216
-
217
- addedKeys.add(key) // for info logging to user
218
- } else {
219
- logger.verbose(`existing ${key}`)
220
- logger.debug(`existing ${key} as ${value}`)
221
- }
222
- }
223
-
224
- let keysData = `#/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/
225
- #/ DOTENV_KEYs. DO NOT commit to source control /
226
- #/ [how it works](https://dotenv.org/env-keys) /
227
- #/--------------------------------------------------/\n`
228
-
229
- for (const key in dotenvKeys) {
230
- const value = dotenvKeys[key]
231
- keysData += `${key}="${value}"\n`
232
- }
233
-
234
- fs.writeFileSync('.env.keys', keysData)
235
- } catch (e) {
236
- logger.error(e)
237
- process.exit(1)
238
- }
239
-
240
- // used later in logging to user
241
- const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
242
-
243
- try {
244
- logger.verbose(`generating .env.vault from ${optionEnvFile}`)
245
-
246
- const dotenvVaults = (main.configDotenv({ path: '.env.vault' }).parsed || {})
247
-
248
- for (const envFilepath of optionEnvFile) {
249
- const filepath = helpers.resolvePath(envFilepath)
250
- const environment = helpers.guessEnvironment(filepath)
251
- const vault = `DOTENV_VAULT_${environment.toUpperCase()}`
252
-
253
- let ciphertext = dotenvVaults[vault]
254
- const dotenvKey = dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
255
-
256
- if (!ciphertext || ciphertext.length === 0 || helpers.changed(ciphertext, dotenvKey, filepath, ENCODING)) {
257
- logger.verbose(`encrypting ${vault}`)
258
- ciphertext = helpers.encryptFile(filepath, dotenvKey, ENCODING)
259
- logger.verbose(`encrypting ${vault} as ${ciphertext}`)
260
-
261
- dotenvVaults[vault] = ciphertext
262
-
263
- addedVaults.add(vault) // for info logging to user
264
- addedEnvFilepaths.add(envFilepath) // for info logging to user
265
- } else {
266
- logger.verbose(`existing ${vault}`)
267
- logger.debug(`existing ${vault} as ${ciphertext}`)
268
- }
269
- }
270
-
271
- let vaultData = `#/-------------------.env.vault---------------------/
272
- #/ cloud-agnostic vaulting standard /
273
- #/ [how it works](https://dotenv.org/env-vault) /
274
- #/--------------------------------------------------/\n\n`
275
-
276
- for (const vault in dotenvVaults) {
277
- const value = dotenvVaults[vault]
278
- const environment = vault.replace('DOTENV_VAULT_', '').toLowerCase()
279
- vaultData += `# ${environment}\n`
280
- vaultData += `${vault}="${value}"\n\n`
281
- }
282
-
283
- fs.writeFileSync('.env.vault', vaultData)
284
- } catch (e) {
285
- logger.error(e)
286
- process.exit(1)
287
- }
288
-
289
- if (addedEnvFilepaths.size > 0) {
290
- logger.info(`encrypted to .env.vault (${[...addedEnvFilepaths]})`)
291
- } else {
292
- logger.info(`no changes (${optionEnvFile})`)
293
- }
294
- if (addedKeys.size > 0) {
295
- logger.info(`${helpers.pluralize('key', addedKeys.size)} added to .env.keys (${[...addedKeys]})`)
296
- }
297
-
298
- if (addedVaults.size > 0) {
299
- const DOTENV_VAULT_X = [...addedVaults][addedVaults.size - 1]
300
- const DOTENV_KEY_X = DOTENV_VAULT_X.replace('_VAULT_', '_KEY_')
301
- const tryKey = dotenvKeys[DOTENV_KEY_X] || '<dotenv_key_environment>'
302
-
303
- logger.info('')
304
- logger.info('next, try it:')
305
- logger.info('')
306
- logger.info(` [DOTENV_KEY='${tryKey}' dotenvx run -- your-cmd]`)
307
- }
308
-
309
- logger.verbose('')
310
- logger.verbose('next:')
311
- logger.verbose('')
312
- logger.verbose(' 1. commit .env.vault safely to code')
313
- logger.verbose(' 2. set DOTENV_KEY on server (or ci)')
314
- logger.verbose(' 3. push your code')
315
- logger.verbose('')
316
- logger.verbose('tips:')
317
- logger.verbose('')
318
- logger.verbose(' * .env.keys file holds your decryption DOTENV_KEYs')
319
- logger.verbose(' * DO NOT commit .env.keys to code')
320
- logger.verbose(' * share .env.keys file over secure channels only')
321
- })
72
+ // dotenvx hub
73
+ program.addCommand(require('./commands/hub'))
322
74
 
323
75
  program.parse(process.argv)
@@ -16,7 +16,7 @@ Try it:
16
16
  $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
17
17
 
18
18
  $ dotenvx run -- node index.js
19
- [dotenvx][info] loading env (1) from .env
19
+ [dotenvx] injecting env (1) from .env
20
20
  Hello World
21
21
  \`\`\`
22
22
  `
@@ -38,11 +38,11 @@ Try it:
38
38
  $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
39
39
 
40
40
  $ dotenvx encrypt
41
- [dotenvx][info] encrypted to .env.vault (.env,.env.production)
42
- [dotenvx][info] keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION)
41
+ encrypted to .env.vault (.env,.env.production)
42
+ keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION)
43
43
 
44
44
  $ DOTENV_KEY='<dotenv_key_production>' dotenvx run -- node index.js
45
- [dotenvx][info] loading env (1) from encrypted .env.vault
45
+ [dotenvx] injecting env (1) from encrypted .env.vault
46
46
  Hello production
47
47
  \`\`\`
48
48
  `
@@ -2,6 +2,7 @@ const fs = require('fs')
2
2
  const path = require('path')
3
3
  const execa = require('execa')
4
4
  const crypto = require('crypto')
5
+ const { execSync } = require('child_process')
5
6
  const xxhash = require('xxhashjs')
6
7
 
7
8
  const XXHASH_SEED = 0xABCD
@@ -13,6 +14,10 @@ const logger = require('./../shared/logger')
13
14
  const RESERVED_ENV_FILES = ['.env.vault', '.env.projects', '.env.keys', '.env.me', '.env.x']
14
15
  const REPORT_ISSUE_LINK = 'https://github.com/dotenvx/dotenvx/issues/new'
15
16
 
17
+ const sleep = function (ms) {
18
+ return new Promise(resolve => setTimeout(resolve, ms))
19
+ }
20
+
16
21
  // resolve path based on current running process location
17
22
  const resolvePath = function (filepath) {
18
23
  return path.resolve(process.cwd(), filepath)
@@ -205,7 +210,36 @@ const _parseCipherTextFromDotenvKeyAndParsedVault = function (dotenvKey, parsedV
205
210
  return ciphertext
206
211
  }
207
212
 
213
+ const formatCode = function (str) {
214
+ const parts = []
215
+
216
+ for (let i = 0; i < str.length; i += 4) {
217
+ parts.push(str.substring(i, i + 4))
218
+ }
219
+
220
+ return parts.join('-')
221
+ }
222
+
223
+ const getRemoteOriginUrl = function () {
224
+ try {
225
+ const url = execSync('git remote get-url origin 2> /dev/null').toString().trim()
226
+ return url
227
+ } catch (_error) {
228
+ return null
229
+ }
230
+ }
231
+
232
+ const extractUsernameName = function (url) {
233
+ // Removing the protocol part and splitting by slashes and colons
234
+ // Removing the protocol part and .git suffix, then splitting by slashes and colons
235
+ const parts = url.replace(/(^\w+:|^)\/\//, '').replace(/\.git$/, '').split(/[/:]/)
236
+
237
+ // Extract the 'username/repository' part
238
+ return parts.slice(-2).join('/')
239
+ }
240
+
208
241
  module.exports = {
242
+ sleep,
209
243
  resolvePath,
210
244
  executeCommand,
211
245
  pluralize,
@@ -216,6 +250,9 @@ module.exports = {
216
250
  encrypt,
217
251
  changed,
218
252
  hash,
253
+ formatCode,
254
+ getRemoteOriginUrl,
255
+ extractUsernameName,
219
256
  _parseEncryptionKeyFromDotenvKey,
220
257
  _parseCipherTextFromDotenvKeyAndParsedVault
221
258
  }
package/src/lib/main.js CHANGED
@@ -21,12 +21,12 @@ const parse = function (src) {
21
21
  return result
22
22
  }
23
23
 
24
- const write = function (processEnv = {}, parsed = {}, overload = false) {
24
+ const inject = function (processEnv = {}, parsed = {}, overload = false) {
25
25
  if (typeof parsed !== 'object') {
26
- throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to write')
26
+ throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to inject')
27
27
  }
28
28
 
29
- const written = new Set()
29
+ const injected = new Set()
30
30
  const preExisting = new Set()
31
31
 
32
32
  // set processEnv
@@ -34,19 +34,19 @@ const write = function (processEnv = {}, parsed = {}, overload = false) {
34
34
  if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
35
35
  if (overload === true) {
36
36
  processEnv[key] = parsed[key]
37
- written.add(key)
37
+ injected.add(key)
38
38
 
39
39
  logger.verbose(`${key} set`)
40
40
  logger.debug(`${key} set to ${parsed[key]}`)
41
41
  } else {
42
42
  preExisting.add(key)
43
43
 
44
- logger.verbose(`${key} pre-exists`)
45
- logger.debug(`${key} pre-exists as ${processEnv[key]}`)
44
+ logger.verbose(`${key} pre-exists (protip: use --overload to override)`)
45
+ logger.debug(`${key} pre-exists as ${processEnv[key]} (protip: use --overload to override)`)
46
46
  }
47
47
  } else {
48
48
  processEnv[key] = parsed[key]
49
- written.add(key)
49
+ injected.add(key)
50
50
 
51
51
  logger.verbose(`${key} set`)
52
52
  logger.debug(`${key} set to ${parsed[key]}`)
@@ -54,7 +54,7 @@ const write = function (processEnv = {}, parsed = {}, overload = false) {
54
54
  }
55
55
 
56
56
  return {
57
- written,
57
+ injected,
58
58
  preExisting
59
59
  }
60
60
  }
@@ -64,5 +64,5 @@ module.exports = {
64
64
  configDotenv,
65
65
  decrypt,
66
66
  parse,
67
- write
67
+ inject
68
68
  }
@@ -0,0 +1,15 @@
1
+ const ora = require('ora')
2
+ const chalk = require('chalk')
3
+
4
+ const createSpinner = function (initialMessage = '') {
5
+ const spinner = ora(initialMessage)
6
+
7
+ return {
8
+ start: (message) => spinner.start(message),
9
+ succeed: (message) => spinner.succeed(chalk.keyword('green')(message)),
10
+ done: (message) => spinner.succeed(message),
11
+ fail: (message) => spinner.fail(chalk.bold.red(message))
12
+ }
13
+ }
14
+
15
+ module.exports = createSpinner
@@ -1,4 +1,5 @@
1
1
  const winston = require('winston')
2
+ const chalk = require('chalk')
2
3
 
3
4
  const printf = winston.format.printf
4
5
  const combine = winston.format.combine
@@ -7,14 +8,65 @@ const transports = winston.transports
7
8
 
8
9
  const packageJson = require('./packageJson')
9
10
 
11
+ const levels = {
12
+ error: 0,
13
+ warn: 1,
14
+ success: 2,
15
+ successv: 2,
16
+ info: 2,
17
+ help: 2,
18
+ help2: 2,
19
+ blank: 2,
20
+ http: 3,
21
+ verbose: 4,
22
+ debug: 5,
23
+ silly: 6
24
+ }
25
+
26
+ const error = chalk.bold.red
27
+ const warn = chalk.keyword('orangered')
28
+ const success = chalk.keyword('green')
29
+ const successv = chalk.keyword('olive') // yellow-ish tint that 'looks' like dotenv
30
+ const help = chalk.keyword('blue')
31
+ const help2 = chalk.keyword('gray')
32
+ const http = chalk.keyword('green')
33
+ const verbose = chalk.keyword('plum')
34
+ const debug = chalk.keyword('plum')
35
+
10
36
  const dotenvxFormat = printf(({ level, message, label, timestamp }) => {
11
37
  const formattedMessage = typeof message === 'object' ? JSON.stringify(message) : message
12
38
 
13
- return `[dotenvx@${packageJson.version}][${level.toLowerCase()}] ${formattedMessage}`
39
+ switch (level.toLowerCase()) {
40
+ case 'error':
41
+ return error(formattedMessage)
42
+ case 'warn':
43
+ return warn(formattedMessage)
44
+ case 'success':
45
+ return success(formattedMessage)
46
+ case 'successv': // success with 'version'
47
+ return successv(`[dotenvx@${packageJson.version}] ${formattedMessage}`)
48
+ case 'info':
49
+ return formattedMessage
50
+ case 'help':
51
+ return help(formattedMessage)
52
+ case 'help2':
53
+ return help2(formattedMessage)
54
+ case 'http':
55
+ return http(formattedMessage)
56
+ case 'verbose':
57
+ return verbose(formattedMessage)
58
+ case 'debug':
59
+ return debug(formattedMessage)
60
+ case 'blank': // custom
61
+ return formattedMessage
62
+ default: // handle uncaught
63
+ return formattedMessage
64
+ }
14
65
  })
15
66
 
16
67
  const logger = createLogger({
17
68
  level: 'info',
69
+ levels,
18
70
  format: combine(
19
71
  dotenvxFormat
20
72
  ),
@@ -0,0 +1,101 @@
1
+ const Conf = require('conf')
2
+ const main = require('./../lib/main')
3
+
4
+ function jsonToEnv (json) {
5
+ return Object.entries(json).map(function ([key, value]) {
6
+ return key + '=' + `"${value}"`
7
+ }).join('\n')
8
+ }
9
+
10
+ function convertFullUsernameToEnvFormat (fullUsername) {
11
+ // gh/motdotla => GH_MOTDOTLA_DOTENVX_TOKEN
12
+ return fullUsername
13
+ .toUpperCase()
14
+ .replace(/\//g, '_') // Replace all slashes with underscores
15
+ .concat('_DOTENVX_TOKEN') // Append '_DOTENVX_TOKEN' at the end
16
+ }
17
+
18
+ function findFirstMatchingKey (data) {
19
+ const dotenvxTokenValue = data.DOTENVX_TOKEN
20
+
21
+ for (const [key, value] of Object.entries(data)) {
22
+ if (key !== 'DOTENVX_TOKEN' && value === dotenvxTokenValue) {
23
+ return key
24
+ }
25
+ }
26
+
27
+ return null // Return null if no matching key is found
28
+ }
29
+
30
+ function parseUsernameFromTokenKey (key) {
31
+ // Remove the leading GH_/GL_ and trailing '_DOTENVX_TOKEN'
32
+ const modifiedKey = key.replace(/^(GH_|GL_)/, '').replace(/_DOTENVX_TOKEN$/, '')
33
+
34
+ // Convert to lowercase
35
+ return modifiedKey.toLowerCase()
36
+ }
37
+
38
+ const confStore = new Conf({
39
+ projectName: 'dotenvx',
40
+ configName: '.env',
41
+ // looks better on user's machine
42
+ // https://github.com/sindresorhus/conf/tree/v10.2.0#projectsuffix.
43
+ projectSuffix: '',
44
+ fileExtension: '',
45
+ // in the spirit of dotenv and format inherently puts limits on config complexity
46
+ serialize: function (json) {
47
+ return jsonToEnv(json)
48
+ },
49
+ // Convert .env format to an object
50
+ deserialize: function (env) {
51
+ return main.parse(env)
52
+ }
53
+ })
54
+
55
+ const getHostname = function () {
56
+ return confStore.get('DOTENVX_HOSTNAME') || 'https://hub.dotenvx.com'
57
+ }
58
+
59
+ const getUsername = function () {
60
+ const key = findFirstMatchingKey(confStore.store)
61
+
62
+ if (key) {
63
+ return parseUsernameFromTokenKey(key)
64
+ } else {
65
+ return null
66
+ }
67
+ }
68
+
69
+ const getToken = function () {
70
+ return confStore.get('DOTENVX_TOKEN')
71
+ }
72
+
73
+ const setToken = function (fullUsername, accessToken) {
74
+ // current logged in user
75
+ confStore.set('DOTENVX_TOKEN', accessToken)
76
+
77
+ // for future use to switch between accounts locally
78
+ const memory = convertFullUsernameToEnvFormat(fullUsername)
79
+ confStore.set(memory, accessToken)
80
+
81
+ return accessToken
82
+ }
83
+
84
+ const setHostname = function (hostname) {
85
+ confStore.set('DOTENVX_HOSTNAME', hostname)
86
+
87
+ return hostname
88
+ }
89
+
90
+ const configPath = function () {
91
+ return confStore.path
92
+ }
93
+
94
+ module.exports = {
95
+ getHostname,
96
+ getToken,
97
+ getUsername,
98
+ setHostname,
99
+ setToken,
100
+ configPath
101
+ }