@dotenvx/dotenvx 0.35.1 → 0.36.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
@@ -732,6 +732,7 @@ $ dotenvx hub push
732
732
  * [`dotenvx get`](https://dotenvx.com/docs/features/get) – return a single environment variable
733
733
  * [`dotenvx set`](https://dotenvx.com/docs/features/set) – set a single environment variable
734
734
  * [`dotenvx ls`](https://dotenvx.com/docs/features/ls) – list all .env files in your repo
735
+ * [`dotenvx status`](https://dotenvx.com/docs/features/status) – compare your .env* content(s) to your .env.vault decrypted content(s)
735
736
  * [`dotenvx settings`](https://dotenvx.com/docs/features/settings) – print current dotenvx settings
736
737
 
737
738
   
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.35.1",
2
+ "version": "0.36.1",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -32,6 +32,7 @@
32
32
  "chalk": "^4.1.2",
33
33
  "commander": "^11.1.0",
34
34
  "conf": "^10.2.0",
35
+ "diff": "^5.2.0",
35
36
  "dotenv": "^16.4.5",
36
37
  "dotenv-expand": "^11.0.6",
37
38
  "execa": "^5.1.1",
@@ -1,94 +1,70 @@
1
1
  const fs = require('fs')
2
- const dotenv = require('dotenv')
3
2
 
4
3
  const logger = require('./../../shared/logger')
5
4
  const createSpinner = require('./../../shared/createSpinner')
6
-
7
- const libDecrypt = require('./../../lib/helpers/decrypt')
8
5
  const sleep = require('./../../lib/helpers/sleep')
9
- const resolvePath = require('./../../lib/helpers/resolvePath')
10
6
 
11
- const spinner = createSpinner('decrypting')
7
+ const Decrypt = require('./../../lib/services/decrypt')
12
8
 
13
- // constants
14
- const ENCODING = 'utf8'
9
+ const spinner = createSpinner('decrypting')
15
10
 
16
- async function decrypt () {
11
+ async function decrypt (directory) {
17
12
  spinner.start()
18
13
  await sleep(500) // better dx
19
14
 
15
+ logger.debug(`directory: ${directory}`)
16
+
20
17
  const options = this.opts()
21
18
  logger.debug(`options: ${JSON.stringify(options)}`)
22
19
 
23
- const vaultFilepath = resolvePath('.env.vault')
24
- const keysFilepath = resolvePath('.env.keys')
25
- const changedEnvFilenames = new Set()
26
- const unchangedEnvFilenames = new Set()
27
-
28
- // logger.verbose(`checking for .env.vault`)
29
- if (!fs.existsSync(vaultFilepath)) {
30
- spinner.fail(`.env.vault file missing: ${vaultFilepath}`)
31
- logger.help('? generate one with [dotenvx encrypt]')
32
- process.exit(1)
33
- }
34
-
35
- // logger.verbose(`checking for .env.keys`)
36
- if (!fs.existsSync(keysFilepath)) {
37
- spinner.fail(`.env.keys file missing: ${keysFilepath}`)
38
- logger.help('? a .env.keys file must be present in order to decrypt your .env.vault contents to .env file(s)')
39
- process.exit(1)
40
- }
41
-
42
- const dotenvKeys = dotenv.configDotenv({ path: keysFilepath }).parsed
43
- const dotenvVault = dotenv.configDotenv({ path: vaultFilepath }).parsed
44
-
45
- Object.entries(dotenvKeys).forEach(([dotenvKey, value]) => {
46
- // determine environment
47
- const environment = dotenvKey.replace('DOTENV_KEY_', '').toLowerCase()
48
- // determine corresponding vault key
49
- const vaultKey = `DOTENV_VAULT_${environment.toUpperCase()}`
50
-
51
- // attempt to find ciphertext
52
- const ciphertext = dotenvVault[vaultKey]
53
-
54
- // give warning if not found
55
- if (ciphertext && ciphertext.length >= 1) {
56
- // Decrypt
57
- const decrypted = libDecrypt(ciphertext, value.trim())
58
-
59
- // envFilename
60
- // replace _ with . to support filenames like .env.development.local
61
- let envFilename = `.env.${environment.replace('_', '.')}`
62
- if (environment === 'development') {
63
- envFilename = '.env'
64
- }
65
-
66
- // check if exists
67
- if (fs.existsSync(envFilename) && (fs.readFileSync(envFilename, { encoding: ENCODING }).toString() === decrypted)) {
68
- unchangedEnvFilenames.add(envFilename)
20
+ try {
21
+ const {
22
+ processedEnvs,
23
+ changedFilenames,
24
+ unchangedFilenames
25
+ } = new Decrypt(directory, options.environment).run()
26
+
27
+ for (const env of processedEnvs) {
28
+ if (env.warning) {
29
+ const warning = env.warning
30
+ logger.warn(warning.message)
69
31
  } else {
70
- changedEnvFilenames.add(envFilename)
71
- fs.writeFileSync(envFilename, decrypted)
32
+ if (env.shouldWrite) {
33
+ logger.debug(`writing ${env.filepath}`)
34
+ fs.writeFileSync(env.filepath, env.decrypted)
35
+ } else {
36
+ logger.debug(`no changes for ${env.filename}`)
37
+ }
72
38
  }
73
- } else {
74
- logger.warn(`${vaultKey} missing in .env.vault: ${vaultFilepath}`)
75
39
  }
76
- })
77
40
 
78
- let changedMsg = ''
79
- if (changedEnvFilenames.size > 0) {
80
- changedMsg = `decrypted (${Array.from(changedEnvFilenames).join(',')})`
81
- }
41
+ let changedMsg = ''
42
+ if (changedFilenames.length > 0) {
43
+ changedMsg = `decrypted (${changedFilenames.join(', ')})`
44
+ }
82
45
 
83
- let unchangedMsg = ''
84
- if (unchangedEnvFilenames.size > 0) {
85
- unchangedMsg = `no changes (${Array.from(unchangedEnvFilenames).join(',')})`
86
- }
46
+ let unchangedMsg = ''
47
+ if (unchangedFilenames.length > 0) {
48
+ unchangedMsg = `no changes (${unchangedFilenames.join(', ')})`
49
+ }
87
50
 
88
- if (changedMsg.length > 0) {
89
- spinner.succeed(`${changedMsg} ${unchangedMsg}`)
90
- } else {
91
- spinner.done(`${unchangedMsg}`)
51
+ if (changedMsg.length > 0) {
52
+ spinner.succeed(`${changedMsg} ${unchangedMsg}`)
53
+ } else {
54
+ spinner.done(`${unchangedMsg}`)
55
+ }
56
+ } catch (error) {
57
+ spinner.fail(error.message)
58
+ if (error.help) {
59
+ logger.help(error.help)
60
+ }
61
+ if (error.debug) {
62
+ logger.debug(error.debug)
63
+ }
64
+ if (error.code) {
65
+ logger.debug(`ERROR_CODE: ${error.code}`)
66
+ }
67
+ process.exit(1)
92
68
  }
93
69
  }
94
70
 
@@ -8,7 +8,7 @@ const Run = require('./../../lib/services/run')
8
8
  const executeCommand = async function (commandArgs, env) {
9
9
  const signals = [
10
10
  'SIGHUP', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT',
11
- 'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM'
11
+ 'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2'
12
12
  ]
13
13
 
14
14
  logger.debug(`executing process command [${commandArgs.join(' ')}]`)
@@ -27,6 +27,19 @@ const executeCommand = async function (commandArgs, env) {
27
27
  logger.debug('no command process to send SIGINT to')
28
28
  }
29
29
  }
30
+ // handler for SIGTERM
31
+ const sigtermHandler = () => {
32
+ logger.debug('received SIGTERM')
33
+ logger.debug('checking command process')
34
+ logger.debug(commandProcess)
35
+
36
+ if (commandProcess) {
37
+ logger.debug('sending SIGTERM to command process')
38
+ commandProcess.kill('SIGTERM') // Send SIGTEM to the command process
39
+ } else {
40
+ logger.debug('no command process to send SIGTERM to')
41
+ }
42
+ }
30
43
 
31
44
  const handleOtherSignal = (signal) => {
32
45
  logger.debug(`received ${signal}`)
@@ -47,6 +60,7 @@ const executeCommand = async function (commandArgs, env) {
47
60
  })
48
61
 
49
62
  process.on('SIGINT', sigintHandler)
63
+ process.on('SIGTERM', sigtermHandler)
50
64
 
51
65
  signals.forEach(signal => {
52
66
  process.on(signal, () => handleOtherSignal(signal))
@@ -61,7 +75,7 @@ const executeCommand = async function (commandArgs, env) {
61
75
  }
62
76
  } catch (error) {
63
77
  // no color on these errors as they can be standard errors for things like jest exiting with exitCode 1 for a single failed test.
64
- if (error.signal !== 'SIGINT') {
78
+ if (error.signal !== 'SIGINT' && error.signal !== 'SIGTERM') {
65
79
  if (error.code === 'ENOENT') {
66
80
  logger.errornocolor(`Unknown command: ${error.command}`)
67
81
  } else if (error.message.includes('Command failed with exit code 1')) {
@@ -76,6 +90,8 @@ const executeCommand = async function (commandArgs, env) {
76
90
  } finally {
77
91
  // Clean up: Remove the SIGINT handler
78
92
  process.removeListener('SIGINT', sigintHandler)
93
+ // Clean up: Remove the SIGTERM handler
94
+ process.removeListener('SIGTERM', sigtermHandler)
79
95
  }
80
96
  }
81
97
 
@@ -0,0 +1,49 @@
1
+ const logger = require('./../../shared/logger')
2
+
3
+ const main = require('./../../lib/main')
4
+
5
+ function status (directory) {
6
+ // debug args
7
+ logger.debug(`directory: ${directory}`)
8
+
9
+ const options = this.opts()
10
+ logger.debug(`options: ${JSON.stringify(options)}`)
11
+
12
+ const { changes, nochanges } = main.status(directory)
13
+
14
+ const changeFilenames = []
15
+ const nochangeFilenames = []
16
+
17
+ for (const row of nochanges) {
18
+ nochangeFilenames.push(row.filename)
19
+ }
20
+
21
+ if (nochangeFilenames.length > 0) {
22
+ logger.blank(`no changes (${nochangeFilenames.join(', ')})`)
23
+ }
24
+
25
+ for (const row of changes) {
26
+ changeFilenames.push(row.filename)
27
+ }
28
+
29
+ if (changeFilenames.length > 0) {
30
+ logger.warn(`changes (${changeFilenames.join(', ')})`)
31
+ }
32
+
33
+ for (const row of changes) {
34
+ logger.blank('')
35
+ const padding = ' '
36
+ logger.blank(`${padding}\`\`\`${row.filename}`)
37
+ const paddedResult = row.coloredDiff.split('\n').map(line => padding + line).join('\n')
38
+ console.log(paddedResult)
39
+ logger.blank(`${padding}\`\`\``)
40
+ }
41
+
42
+ if (changeFilenames.length > 0) {
43
+ logger.blank('')
44
+ const optionalDirectory = directory === '.' ? '' : ` ${directory}`
45
+ logger.blank(`run [dotenvx encrypt${optionalDirectory}] to apply changes to .env.vault`)
46
+ }
47
+ }
48
+
49
+ module.exports = status
@@ -111,8 +111,16 @@ program.command('encrypt')
111
111
  // dotenvx decrypt
112
112
  program.command('decrypt')
113
113
  .description('decrypt .env.vault to .env*')
114
+ .argument('[directory]', 'directory to decrypt', '.')
115
+ .option('-e, --environment <environments...>', 'environment(s) to decrypt')
114
116
  .action(require('./actions/decrypt'))
115
117
 
118
+ // dotenvx status
119
+ program.command('status')
120
+ .description('compare your .env* content(s) to your .env.vault decrypted content(s)')
121
+ .argument('[directory]', 'directory to check status against', '.')
122
+ .action(require('./actions/status'))
123
+
116
124
  // dotenvx genexample
117
125
  program.command('genexample')
118
126
  .description('generate .env.example')
@@ -1,3 +1,5 @@
1
+ const path = require('path')
2
+
1
3
  class ArrayToTree {
2
4
  constructor (arr) {
3
5
  this.arr = arr
@@ -7,7 +9,8 @@ class ArrayToTree {
7
9
  const tree = {}
8
10
 
9
11
  for (let i = 0; i < this.arr.length; i++) {
10
- const parts = this.arr[i].split('/')
12
+ const normalizedPath = path.normalize(this.arr[i]) // normalize any strange paths
13
+ const parts = normalizedPath.split(path.sep) // use the platform-specific path segment separator
11
14
  let current = tree
12
15
 
13
16
  for (let j = 0; j < parts.length; j++) {
@@ -0,0 +1,7 @@
1
+ const path = require('path')
2
+
3
+ function containsDirectory (filepath) {
4
+ return filepath.includes(path.sep)
5
+ }
6
+
7
+ module.exports = containsDirectory
package/src/lib/main.js CHANGED
@@ -7,6 +7,7 @@ const Encrypt = require('./services/encrypt')
7
7
  const Ls = require('./services/ls')
8
8
  const Get = require('./services/get')
9
9
  const Sets = require('./services/sets')
10
+ const Status = require('./services/status')
10
11
  const Genexample = require('./services/genexample')
11
12
  const Settings = require('./services/settings')
12
13
 
@@ -61,6 +62,10 @@ const set = function (key, value, envFile) {
61
62
  return new Sets(key, value, envFile).run()
62
63
  }
63
64
 
65
+ const status = function (directory) {
66
+ return new Status(directory).run()
67
+ }
68
+
64
69
  const settings = function (key = null) {
65
70
  return new Settings(key).run()
66
71
  }
@@ -94,6 +99,7 @@ module.exports = {
94
99
  ls,
95
100
  get,
96
101
  set,
102
+ status,
97
103
  genexample,
98
104
  // settings
99
105
  settings,
@@ -0,0 +1,126 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const dotenv = require('dotenv')
4
+
5
+ const ENCODING = 'utf8'
6
+
7
+ const libDecrypt = require('./../../lib/helpers/decrypt')
8
+
9
+ class Decrypt {
10
+ constructor (directory = '.', environment) {
11
+ this.directory = directory
12
+ this.environment = environment
13
+
14
+ this.envKeysFilepath = path.resolve(this.directory, '.env.keys')
15
+ this.envVaultFilepath = path.resolve(this.directory, '.env.vault')
16
+
17
+ this.processedEnvs = []
18
+ this.changedFilenames = new Set()
19
+ this.unchangedFilenames = new Set()
20
+ }
21
+
22
+ run () {
23
+ if (!fs.existsSync(this.envVaultFilepath)) {
24
+ const code = 'MISSING_ENV_VAULT_FILE'
25
+ const message = `missing .env.vault (${this.envVaultFilepath})`
26
+ const help = `? generate one with [dotenvx encrypt ${this.directory}]`
27
+
28
+ const error = new Error(message)
29
+ error.code = code
30
+ error.help = help
31
+ throw error
32
+ }
33
+
34
+ if (!fs.existsSync(this.envKeysFilepath)) {
35
+ const code = 'MISSING_ENV_KEYS_FILE'
36
+ const message = `missing .env.keys (${this.envKeysFilepath})`
37
+ const help = '? a .env.keys file must be present in order to decrypt your .env.vault contents to .env file(s)'
38
+
39
+ const error = new Error(message)
40
+ error.code = code
41
+ error.help = help
42
+ throw error
43
+ }
44
+
45
+ const dotenvKeys = dotenv.configDotenv({ path: this.envKeysFilepath }).parsed
46
+ const dotenvVault = dotenv.configDotenv({ path: this.envVaultFilepath }).parsed
47
+ const environments = this._environments()
48
+
49
+ if (environments.length > 0) {
50
+ // iterate over the environments
51
+ for (const environment of environments) {
52
+ const value = dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
53
+ const row = this._processRow(value, dotenvVault, environment)
54
+
55
+ this.processedEnvs.push(row)
56
+ }
57
+ } else {
58
+ for (const [dotenvKey, value] of Object.entries(dotenvKeys)) {
59
+ const environment = dotenvKey.replace('DOTENV_KEY_', '').toLowerCase()
60
+ const row = this._processRow(value, dotenvVault, environment)
61
+
62
+ this.processedEnvs.push(row)
63
+ }
64
+ }
65
+
66
+ return {
67
+ processedEnvs: this.processedEnvs,
68
+ changedFilenames: [...this.changedFilenames],
69
+ unchangedFilenames: [...this.unchangedFilenames]
70
+ }
71
+ }
72
+
73
+ _processRow (value, dotenvVault, environment) {
74
+ environment = environment.toLowerCase() // important so we don't later write .env.DEVELOPMENT for example
75
+ const vaultKey = `DOTENV_VAULT_${environment.toUpperCase()}`
76
+ const ciphertext = dotenvVault[vaultKey] // attempt to find ciphertext
77
+
78
+ const row = {}
79
+ row.environment = environment
80
+ row.dotenvKey = value ? value.trim() : value
81
+ row.ciphertext = ciphertext
82
+
83
+ if (ciphertext && ciphertext.length >= 1) {
84
+ // Decrypt
85
+ const decrypted = libDecrypt(ciphertext, value.trim())
86
+ row.decrypted = decrypted
87
+
88
+ // envFilename
89
+ // replace _ with . to support filenames like .env.development.local
90
+ let envFilename = `.env.${environment.replace('_', '.')}`
91
+ if (environment === 'development') {
92
+ envFilename = '.env'
93
+ }
94
+ row.filename = envFilename
95
+ row.filepath = path.resolve(this.directory, envFilename)
96
+
97
+ // check if exists and is changing
98
+ if (fs.existsSync(row.filepath) && (fs.readFileSync(row.filepath, { encoding: ENCODING }).toString() === decrypted)) {
99
+ this.unchangedFilenames.add(envFilename)
100
+ } else {
101
+ row.shouldWrite = true
102
+ this.changedFilenames.add(envFilename)
103
+ }
104
+ } else {
105
+ const message = `${vaultKey} missing in .env.vault: ${this.envVaultFilepath}`
106
+ const warning = new Error(message)
107
+ row.warning = warning
108
+ }
109
+
110
+ return row
111
+ }
112
+
113
+ _environments () {
114
+ if (this.environment === undefined) {
115
+ return []
116
+ }
117
+
118
+ if (!Array.isArray(this.environment)) {
119
+ return [this.environment]
120
+ }
121
+
122
+ return this.environment
123
+ }
124
+ }
125
+
126
+ module.exports = Decrypt
@@ -0,0 +1,105 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const diff = require('diff')
4
+ const chalk = require('chalk')
5
+
6
+ const Ls = require('./ls')
7
+ const Decrypt = require('./decrypt')
8
+
9
+ const containsDirectory = require('./../helpers/containsDirectory')
10
+ const guessEnvironment = require('./../helpers/guessEnvironment')
11
+
12
+ const ENCODING = 'utf8'
13
+
14
+ class Status {
15
+ constructor (directory = '.') {
16
+ this.directory = directory
17
+ this.changes = []
18
+ this.nochanges = []
19
+ }
20
+
21
+ run () {
22
+ // get list of .env files
23
+ const files = new Ls(this.directory).run()
24
+ // iterate over each one
25
+ for (const filename of files) {
26
+ // skip file if directory
27
+ if (containsDirectory(filename)) {
28
+ continue
29
+ }
30
+
31
+ // skip file if .env.keys
32
+ if (filename.endsWith('.env.keys')) {
33
+ continue
34
+ }
35
+
36
+ // skip file if .env.vault
37
+ if (filename.endsWith('.env.vault')) {
38
+ continue
39
+ }
40
+
41
+ // skip file if .env.example
42
+ if (filename.endsWith('.env.example')) {
43
+ continue
44
+ }
45
+
46
+ // skip file if *.previous
47
+ if (filename.endsWith('.previous')) {
48
+ continue
49
+ }
50
+
51
+ const filepath = path.resolve(this.directory, filename)
52
+
53
+ const row = {}
54
+ row.filename = filename
55
+ row.filepath = filepath
56
+ row.environment = guessEnvironment(filepath)
57
+
58
+ // grab raw
59
+ row.raw = fs.readFileSync(filepath, { encoding: ENCODING })
60
+
61
+ // grab decrypted
62
+ const { processedEnvs } = new Decrypt(this.directory, row.environment).run()
63
+ row.decrypted = processedEnvs[0].decrypted
64
+
65
+ // differences
66
+ row.differences = diff.diffWords(row.decrypted, row.raw)
67
+
68
+ // any changes?
69
+ const hasChanges = this._hasChanges(row.differences)
70
+
71
+ if (hasChanges) {
72
+ row.coloredDiff = row.differences.map(this._colorizeDiff).join('')
73
+ this.changes.push(row)
74
+ } else {
75
+ this.nochanges.push(row)
76
+ }
77
+ }
78
+
79
+ return {
80
+ changes: this.changes,
81
+ nochanges: this.nochanges
82
+ }
83
+ }
84
+
85
+ _colorizeDiff (part) {
86
+ // If the part was added, color it green
87
+ if (part.added) {
88
+ return chalk.green(part.value)
89
+ }
90
+
91
+ // If the part was removed, color it red
92
+ if (part.removed) {
93
+ return chalk.red(part.value)
94
+ }
95
+
96
+ // No color for unchanged parts
97
+ return part.value
98
+ }
99
+
100
+ _hasChanges (differences) {
101
+ return differences.some(part => part.added || part.removed)
102
+ }
103
+ }
104
+
105
+ module.exports = Status