The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    ├── actions
    │   └── commit-binary-to-github
    │   │   └── action.yml
    └── workflows
    │   └── ci.yml
├── .gitignore
├── .npmignore
├── .shellspec
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── SECURITY.md
├── esbuild.js
├── install.sh
├── package-lock.json
├── package.json
├── spec
    ├── install_spec.sh
    ├── spec_helper.sh
    └── tmp
    │   └── .gitkeep
├── src
    ├── cli
    │   ├── actions
    │   │   ├── decrypt.js
    │   │   ├── encrypt.js
    │   │   ├── ext
    │   │   │   ├── genexample.js
    │   │   │   ├── gitignore.js
    │   │   │   ├── prebuild.js
    │   │   │   ├── precommit.js
    │   │   │   └── scan.js
    │   │   ├── get.js
    │   │   ├── keypair.js
    │   │   ├── ls.js
    │   │   ├── rotate.js
    │   │   ├── run.js
    │   │   └── set.js
    │   ├── commands
    │   │   └── ext.js
    │   ├── dotenvx.js
    │   └── examples.js
    ├── lib
    │   ├── config.d.ts
    │   ├── config.js
    │   ├── helpers
    │   │   ├── append.js
    │   │   ├── arrayToTree.js
    │   │   ├── buildEnvs.js
    │   │   ├── catchAndLog.js
    │   │   ├── chomp.js
    │   │   ├── colorDepth.js
    │   │   ├── conventions.js
    │   │   ├── decrypt.js
    │   │   ├── decryptKeyValue.js
    │   │   ├── deprecationNotice.js
    │   │   ├── detectEncoding.js
    │   │   ├── determineEnvs.js
    │   │   ├── dotenvOptionPaths.js
    │   │   ├── dotenvParse.js
    │   │   ├── dotenvPrivateKeyNames.js
    │   │   ├── encrypt.js
    │   │   ├── encryptValue.js
    │   │   ├── errors.js
    │   │   ├── escape.js
    │   │   ├── escapeDollarSigns.js
    │   │   ├── escapeForRegex.js
    │   │   ├── evalKeyValue.js
    │   │   ├── execute.js
    │   │   ├── executeCommand.js
    │   │   ├── executeDynamic.js
    │   │   ├── executeExtension.js
    │   │   ├── findEnvFiles.js
    │   │   ├── findPrivateKey.js
    │   │   ├── findPublicKey.js
    │   │   ├── fsx.js
    │   │   ├── getCommanderVersion.js
    │   │   ├── guessEnvironment.js
    │   │   ├── guessPrivateKeyFilename.js
    │   │   ├── guessPrivateKeyName.js
    │   │   ├── guessPublicKeyName.js
    │   │   ├── installPrecommitHook.js
    │   │   ├── isEncrypted.js
    │   │   ├── isFullyEncrypted.js
    │   │   ├── isIgnoringDotenvKeys.js
    │   │   ├── isPublicKey.js
    │   │   ├── keypair.js
    │   │   ├── packageJson.js
    │   │   ├── parse.js
    │   │   ├── parseEncryptionKeyFromDotenvKey.js
    │   │   ├── parseEnvironmentFromDotenvKey.js
    │   │   ├── pluralize.js
    │   │   ├── proKeypair.js
    │   │   ├── quotes.js
    │   │   ├── removeDynamicHelpSection.js
    │   │   ├── removeOptionsHelpParts.js
    │   │   ├── replace.js
    │   │   ├── resolveEscapeSequences.js
    │   │   ├── resolveHome.js
    │   │   ├── sleep.js
    │   │   ├── smartDotenvPrivateKey.js
    │   │   ├── smartDotenvPublicKey.js
    │   │   └── truncate.js
    │   ├── main.d.ts
    │   ├── main.js
    │   └── services
    │   │   ├── decrypt.js
    │   │   ├── encrypt.js
    │   │   ├── genexample.js
    │   │   ├── get.js
    │   │   ├── keypair.js
    │   │   ├── ls.js
    │   │   ├── prebuild.js
    │   │   ├── precommit.js
    │   │   ├── rotate.js
    │   │   ├── run.js
    │   │   └── sets.js
    └── shared
    │   ├── colors.js
    │   └── logger.js
└── tests
    ├── .env
    ├── .env.eval
    ├── .env.expand
    ├── .env.export
    ├── .env.latin1
    ├── .env.local
    ├── .env.multiline
    ├── .env.utf16le
    ├── .env.vault
    ├── cli
        ├── actions
        │   ├── decrypt.test.js
        │   ├── encrypt.test.js
        │   ├── ext
        │   │   ├── genexample.test.js
        │   │   ├── gitignore.test.js
        │   │   ├── prebuild.test.js
        │   │   ├── precommit.test.js
        │   │   └── scan.test.js
        │   ├── get.test.js
        │   ├── keypair.test.js
        │   ├── ls.test.js
        │   ├── rotate.test.js
        │   ├── run.test.js
        │   └── set.test.js
        └── examples.test.js
    ├── e2e
        ├── decrypt.test.js
        ├── encrypt.test.js
        ├── ext.test.js
        ├── get.test.js
        ├── run.test.js
        └── version.test.js
    ├── lib
        ├── config-expand.test.js
        ├── config-vault.test.js
        ├── config.test.js
        ├── helpers
        │   ├── append.test.js
        │   ├── arrayToTree.test.js
        │   ├── colorDepth.test.js
        │   ├── conventions.test.js
        │   ├── decrypt.test.js
        │   ├── decryptKeyValue.test.js
        │   ├── detectEncoding.test.js
        │   ├── encryptValue.test.js
        │   ├── errors.test.js
        │   ├── executeCommand.test.js
        │   ├── executeDynamic.test.js
        │   ├── executeExtension.test.js
        │   ├── findEnvFiles.test.js
        │   ├── findPrivateKey.test.js
        │   ├── fsx.test.js
        │   ├── guessEnvironment.test.js
        │   ├── guessPrivateKeyFilename.test.js
        │   ├── guessPrivateKeyName.test.js
        │   ├── guessPublicKeyName.test.js
        │   ├── installPrecommitHook.test.js
        │   ├── isEncrypted.test.js
        │   ├── isFullyEncrypted.test.js
        │   ├── isIgnoringDotenvKeys.test.js
        │   ├── isPublicKey.test.js
        │   ├── keypair.test.js
        │   ├── parse.test.js
        │   ├── parseEncryptionKeyFromDotenvKey.test.js
        │   ├── parseEnvironmentFromDotenvKey.test.js
        │   ├── pluralize.test.js
        │   ├── proKeypair.test.js
        │   ├── removeDynamicHelpSection.test.js
        │   ├── removeOptionsHelpParts.test.js
        │   ├── replace.test.js
        │   ├── smartDotenvPrivateKey.test.js
        │   ├── smartDotenvPublicKey.test.js
        │   └── truncate.test.js
        ├── main.test.js
        ├── parse-multiline.test.js
        ├── parse.test.js
        └── services
        │   ├── decrypt.test.js
        │   ├── encrypt.test.js
        │   ├── genexample.test.js
        │   ├── get.test.js
        │   ├── keypair.test.js
        │   ├── ls.test.js
        │   ├── prebuild.test.js
        │   ├── precommit.test.js
        │   ├── rotate.test.js
        │   ├── run.test.js
        │   └── sets.test.js
    ├── monorepo
        ├── .env.keys
        └── apps
        │   ├── app1
        │       ├── .env
        │       └── .env.production
        │   ├── backend
        │       ├── .env
        │       ├── .env.example
        │       ├── .env.keys
        │       ├── .env.previous
        │       ├── .env.untracked
        │       └── .env.vault
        │   ├── encrypted
        │       ├── .env
        │       ├── .env.keys
        │       ├── secrets.ci.txt
        │       └── secrets.txt
        │   ├── frontend
        │       └── .env
        │   ├── multiline
        │       ├── .env
        │       └── .env.crlf
        │   ├── multiple
        │       ├── .env
        │       ├── .env.keys
        │       └── .env.production
        │   ├── shebang
        │       └── .env
        │   └── unencrypted
        │       └── .env
    ├── multiline.txt
    └── shared
        ├── colors.test.js
        └── logger.test.js


/.github/actions/commit-binary-to-github/action.yml:
--------------------------------------------------------------------------------
 1 | name: commit-binary-to-github
 2 | description: commit binary to github repo - which triggers to npm publish
 3 | inputs:
 4 |   version:
 5 |     description: 'choose version'
 6 |     required: true
 7 |     default: 'v0.30.0'
 8 |   platform:
 9 |     description: 'choose platform'
10 |     required: true
11 |     default: 'darwin-arm64'
12 | 
13 | runs:
14 |   using: "composite"
15 |   steps:
16 |     - name: extract version
17 |       shell: bash
18 |       run: |
19 |         VERSION=${{inputs.version}}
20 |         CLEAN_VERSION=${VERSION#v}
21 |         echo "VERSION=$VERSION" >> $GITHUB_ENV
22 |         echo "CLEAN_VERSION=$CLEAN_VERSION" >> $GITHUB_ENV
23 |     - name: node
24 |       uses: actions/setup-node@v4
25 |       with:
26 |         node-version: 16.x
27 | 
28 |     - name: download and extract tarball
29 |       shell: bash
30 |       run: |
31 |         curl -L -o tarball.tar.gz https://github.com/dotenvx/dotenvx/releases/download/${{ env.VERSION }}/dotenvx-${{ env.CLEAN_VERSION }}-${{ inputs.platform }}.tar.gz
32 |         mkdir -p output
33 |         tar -xzvf tarball.tar.gz -C output --strip-components=1
34 | 
35 |     - name: checkout target repo
36 |       uses: actions/checkout@v4
37 |       with:
38 |         repository: "dotenvx/dotenvx-${{ inputs.platform }}"
39 |         token: ${{ env.PERSONAL_ACCESS_TOKEN }}
40 |         path: "dotenvx-${{ inputs.platform }}"
41 | 
42 |     - name: copy dotenvx binary to repo
43 |       shell: bash
44 |       run: |
45 |         mkdir -p dotenvx-${{ inputs.platform }}
46 |         cp -r output/* dotenvx-${{ inputs.platform }}/
47 | 
48 |     - name: set version, commit, and push
49 |       shell: bash
50 |       run: |
51 |         cd dotenvx-${{ inputs.platform }}
52 |         npm version ${{ env.CLEAN_VERSION }} --no-git-tag-version
53 |         git config --global user.name 'motdotenv'
54 |         git config --global user.email 'mot@dotenv.org'
55 |         git add .
56 |         git commit -m "${{ env.VERSION }}"
57 |         git tag "${{ env.VERSION }}"
58 |         git push origin HEAD --tags
59 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | .nyc_output
 3 | .tap
 4 | coverage/
 5 | node_modules/
 6 | build/*
 7 | bin/*
 8 | dist/*
 9 | .env*
10 | !.env.vault
11 | tests/tmp
12 | .vscode
13 | 
14 | # for test purposes
15 | !tests/.env
16 | !tests/.env.expand
17 | !tests/.env.export
18 | !tests/.env.eval
19 | !tests/.env.local
20 | !tests/.env.multiline
21 | !tests/.env.vault
22 | !tests/.env.utf16le
23 | !tests/.env.latin1
24 | !tests/monorepo/**/.env*
25 | spec/tmp/*
26 | !spec/tmp/.gitkeep
27 | 


--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | .nyc_output
 3 | .tap
 4 | coverage/
 5 | node_modules/
 6 | dist/*
 7 | .env*
 8 | !.env.vault
 9 | tests/
10 | spec/
11 | .shellspec
12 | 


--------------------------------------------------------------------------------
/.shellspec:
--------------------------------------------------------------------------------
 1 | --require spec_helper
 2 | 
 3 | ## Default kcov (coverage) options
 4 | # --kcov-options "--include-path=. --path-strip-level=1"
 5 | # --kcov-options "--include-pattern=.sh"
 6 | # --kcov-options "--exclude-pattern=/.shellspec,/spec/,/coverage/,/report/"
 7 | 
 8 | ## Example: Include script "myprog" with no extension
 9 | # --kcov-options "--include-pattern=.sh,myprog"
10 | 
11 | ## Example: Only specified files/directories
12 | # --kcov-options "--include-pattern=myprog,/lib/"
13 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | # Use a base image
 2 | FROM ubuntu:latest
 3 | 
 4 | # Set work directory
 5 | WORKDIR /app
 6 | 
 7 | # Set environment variables for the architecture and OS (change if necessary)
 8 | ENV OS=linux
 9 | 
10 | # Install dependencies
11 | RUN apt-get update && apt-get install -y curl
12 | 
13 | # Extract architecture from TARGETPLATFORM environment variable and fetch dotenvx binary
14 | ARG TARGETPLATFORM
15 | RUN ARCH=$(echo ${TARGETPLATFORM} | cut -f2 -d '/') && \
16 |     echo "Architecture: ${ARCH}" && \
17 |     curl -L https://github.com/dotenvx/dotenvx/releases/latest/download/dotenvx-${OS}-${ARCH}.tar.gz | tar -xz -C /usr/local/bin
18 | 
19 | # Make the binary executable
20 | RUN chmod +x /usr/local/bin/dotenvx
21 | 
22 | # Set the entry point to the dotenvx command (optional)
23 | ENTRYPOINT ["/usr/local/bin/dotenvx"]
24 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | BSD 3-Clause License
 2 | 
 3 | Copyright (c) 2024, Scott Motte
 4 | 
 5 | Redistribution and use in source and binary forms, with or without
 6 | modification, are permitted provided that the following conditions are met:
 7 | 
 8 | 1. Redistributions of source code must retain the above copyright notice, this
 9 |    list of conditions and the following disclaimer.
10 | 
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 |    this list of conditions and the following disclaimer in the documentation
13 |    and/or other materials provided with the distribution.
14 | 
15 | 3. Neither the name of the copyright holder nor the names of its
16 |    contributors may be used to endorse or promote products derived from
17 |    this software without specific prior written permission.
18 | 
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 | 


--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | Please report any security vulnerabilities to security@dotenvx.com. 
2 | 


--------------------------------------------------------------------------------
/esbuild.js:
--------------------------------------------------------------------------------
 1 | const esbuild = require('esbuild')
 2 | const { stat, writeFile, rm, mkdir } = require('fs/promises')
 3 | const pkgJson = require('./package.json')
 4 | 
 5 | const outputDir = 'build'
 6 | 
 7 | function cleanPkgJson (json) {
 8 |   delete json.devDependencies
 9 |   delete json.optionalDependencies
10 |   delete json.dependencies
11 |   delete json.workspaces
12 |   return json
13 | }
14 | 
15 | async function emptyDir (dir) {
16 |   try {
17 |     await rm(dir, { recursive: true })
18 |   } catch (err) {
19 |     if (err.code !== 'ENOENT') {
20 |       throw err
21 |     }
22 |   }
23 |   await mkdir(dir)
24 | }
25 | 
26 | async function printSize (fileName) {
27 |   const stats = await stat(fileName)
28 | 
29 |   // print size in MB
30 |   console.log(`Bundle size: ${Math.round(stats.size / 10000) / 100}MB\n\n`)
31 | }
32 | 
33 | async function main () {
34 |   const start = Date.now()
35 |   // clean build folder
36 |   await emptyDir(outputDir)
37 | 
38 |   const outfile = `${outputDir}/index.js`
39 | 
40 |   /** @type { import('esbuild').BuildOptions } */
41 |   const config = {
42 |     entryPoints: [pkgJson.bin.dotenvx],
43 |     bundle: true,
44 |     platform: 'node',
45 |     target: 'node18',
46 |     sourcemap: true,
47 |     outfile,
48 |     // suppress direct-eval warning
49 |     logOverride: {
50 |       'direct-eval': 'silent'
51 |     }
52 |   }
53 | 
54 |   await esbuild.build(config)
55 | 
56 |   console.log(`Build took ${Date.now() - start}ms`)
57 |   await printSize(outfile)
58 | 
59 |   if (process.argv.includes('--minify')) {
60 |     // minify the file
61 |     await esbuild.build({
62 |       ...config,
63 |       entryPoints: [outfile],
64 |       minify: true,
65 |       keepNames: true,
66 |       allowOverwrite: true,
67 |       outfile
68 |     })
69 | 
70 |     console.log(`Minify took ${Date.now() - start}ms`)
71 |     await printSize(outfile)
72 |   }
73 | 
74 |   // create main patched package.json
75 |   cleanPkgJson(pkgJson)
76 | 
77 |   pkgJson.scripts = {
78 |     start: 'node index.js'
79 |   }
80 | 
81 |   pkgJson.bin = 'index.js'
82 |   pkgJson.pkg = {
83 |     assets: [
84 |       '*.map'
85 |     ]
86 |   }
87 | 
88 |   await writeFile(
89 |     `${outputDir}/package.json`,
90 |     JSON.stringify(pkgJson, null, 2)
91 |   )
92 | }
93 | 
94 | main().catch(err => {
95 |   console.error(err)
96 |   process.exit(1)
97 | })
98 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "version": "1.46.0",
 3 |   "name": "@dotenvx/dotenvx",
 4 |   "description": "a better dotenv–from the creator of `dotenv`",
 5 |   "author": "@motdotla",
 6 |   "keywords": [
 7 |     "dotenv",
 8 |     "env"
 9 |   ],
10 |   "homepage": "https://github.com/dotenvx/dotenvx",
11 |   "repository": {
12 |     "type": "git",
13 |     "url": "git+https://github.com/dotenvx/dotenvx.git"
14 |   },
15 |   "license": "BSD-3-Clause",
16 |   "files": [
17 |     "src/**/*",
18 |     "CHANGELOG.md"
19 |   ],
20 |   "main": "src/lib/main.js",
21 |   "types": "src/lib/main.d.ts",
22 |   "exports": {
23 |     ".": {
24 |       "types": "./src/lib/main.d.ts",
25 |       "require": "./src/lib/main.js",
26 |       "default": "./src/lib/main.js"
27 |     },
28 |     "./config": "./src/lib/config.js",
29 |     "./config.js": "./src/lib/config.js",
30 |     "./package.json": "./package.json"
31 |   },
32 |   "bin": {
33 |     "dotenvx": "./src/cli/dotenvx.js"
34 |   },
35 |   "scripts": {
36 |     "standard": "standard",
37 |     "standard:fix": "standard --fix",
38 |     "test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000",
39 |     "test-coverage": "tap run --show-full-coverage --timeout=60000",
40 |     "testshell": "bash shellspec",
41 |     "prerelease": "npm test && npm run testshell",
42 |     "release": "standard-version"
43 |   },
44 |   "funding": "https://dotenvx.com",
45 |   "dependencies": {
46 |     "commander": "^11.1.0",
47 |     "dotenv": "^16.4.5",
48 |     "eciesjs": "^0.4.10",
49 |     "execa": "^5.1.1",
50 |     "fdir": "^6.2.0",
51 |     "ignore": "^5.3.0",
52 |     "object-treeify": "1.1.33",
53 |     "picomatch": "^4.0.2",
54 |     "which": "^4.0.0"
55 |   },
56 |   "devDependencies": {
57 |     "@yao-pkg/pkg": "^5.14.2",
58 |     "capture-console": "^1.0.2",
59 |     "esbuild": "^0.24.0",
60 |     "proxyquire": "^2.1.3",
61 |     "sinon": "^14.0.1",
62 |     "standard": "^17.1.0",
63 |     "standard-version": "^9.5.0",
64 |     "tap": "^21.0.1"
65 |   },
66 |   "publishConfig": {
67 |     "access": "public",
68 |     "provenance": true
69 |   }
70 | }
71 | 


--------------------------------------------------------------------------------
/spec/spec_helper.sh:
--------------------------------------------------------------------------------
 1 | # shellcheck shell=sh
 2 | 
 3 | # Defining variables and functions here will affect all specfiles.
 4 | # Change shell options inside a function may cause different behavior,
 5 | # so it is better to set them here.
 6 | # set -eu
 7 | 
 8 | # This callback function will be invoked only once before loading specfiles.
 9 | spec_helper_precheck() {
10 |   # Available functions: info, warn, error, abort, setenv, unsetenv
11 |   # Available variables: VERSION, SHELL_TYPE, SHELL_VERSION
12 |   : minimum_version "0.28.1"
13 | }
14 | 
15 | # This callback function will be invoked after a specfile has been loaded.
16 | spec_helper_loaded() {
17 |   :
18 | }
19 | 
20 | # This callback function will be invoked after core modules has been loaded.
21 | spec_helper_configure() {
22 |   # Available functions: import, before_each, after_each, before_all, after_all
23 |   : import 'support/custom_matcher'
24 | }
25 | 


--------------------------------------------------------------------------------
/spec/tmp/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dotenvx/dotenvx/49a78cacab5fa4b758b901f1ac739cccf2bf4562/spec/tmp/.gitkeep


--------------------------------------------------------------------------------
/src/cli/actions/decrypt.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./../../lib/helpers/fsx')
 2 | const { logger } = require('./../../shared/logger')
 3 | 
 4 | const Decrypt = require('./../../lib/services/decrypt')
 5 | 
 6 | const catchAndLog = require('../../lib/helpers/catchAndLog')
 7 | 
 8 | function decrypt () {
 9 |   const options = this.opts()
10 |   logger.debug(`options: ${JSON.stringify(options)}`)
11 | 
12 |   const envs = this.envs
13 | 
14 |   let errorCount = 0
15 | 
16 |   // stdout - should not have a try so that exit codes can surface to stdout
17 |   if (options.stdout) {
18 |     const {
19 |       processedEnvs
20 |     } = new Decrypt(envs, options.key, options.excludeKey, options.envKeysFile).run()
21 | 
22 |     for (const processedEnv of processedEnvs) {
23 |       if (processedEnv.error) {
24 |         errorCount += 1
25 |         logger.error(processedEnv.error.message)
26 |         if (processedEnv.error.help) {
27 |           logger.error(processedEnv.error.help)
28 |         }
29 |       } else {
30 |         console.log(processedEnv.envSrc)
31 |       }
32 |     }
33 | 
34 |     if (errorCount > 0) {
35 |       process.exit(1)
36 |     } else {
37 |       process.exit(0) // exit early
38 |     }
39 |   } else {
40 |     try {
41 |       const {
42 |         processedEnvs,
43 |         changedFilepaths,
44 |         unchangedFilepaths
45 |       } = new Decrypt(envs, options.key, options.excludeKey, options.envKeysFile).run()
46 | 
47 |       for (const processedEnv of processedEnvs) {
48 |         logger.verbose(`decrypting ${processedEnv.envFilepath} (${processedEnv.filepath})`)
49 | 
50 |         if (processedEnv.error) {
51 |           errorCount += 1
52 | 
53 |           if (processedEnv.error.code === 'MISSING_ENV_FILE') {
54 |             logger.error(processedEnv.error.message)
55 |             logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx decrypt]`)
56 |           } else {
57 |             logger.error(processedEnv.error.message)
58 |             if (processedEnv.error.help) {
59 |               logger.error(processedEnv.error.help)
60 |             }
61 |           }
62 |         } else if (processedEnv.changed) {
63 |           fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc)
64 | 
65 |           logger.verbose(`decrypted ${processedEnv.envFilepath} (${processedEnv.filepath})`)
66 |         } else {
67 |           logger.verbose(`no changes ${processedEnv.envFilepath} (${processedEnv.filepath})`)
68 |         }
69 |       }
70 | 
71 |       if (changedFilepaths.length > 0) {
72 |         logger.success(`✔ decrypted (${changedFilepaths.join(',')})`)
73 |       } else if (unchangedFilepaths.length > 0) {
74 |         logger.info(`no changes (${unchangedFilepaths})`)
75 |       } else {
76 |         // do nothing - scenario when no .env files found
77 |       }
78 | 
79 |       if (errorCount > 0) {
80 |         process.exit(1)
81 |       }
82 |     } catch (error) {
83 |       catchAndLog(error)
84 |       process.exit(1)
85 |     }
86 |   }
87 | }
88 | 
89 | module.exports = decrypt
90 | 


--------------------------------------------------------------------------------
/src/cli/actions/encrypt.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./../../lib/helpers/fsx')
 2 | const { logger } = require('./../../shared/logger')
 3 | 
 4 | const Encrypt = require('./../../lib/services/encrypt')
 5 | 
 6 | const catchAndLog = require('../../lib/helpers/catchAndLog')
 7 | const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys')
 8 | 
 9 | function encrypt () {
10 |   const options = this.opts()
11 |   logger.debug(`options: ${JSON.stringify(options)}`)
12 | 
13 |   const envs = this.envs
14 | 
15 |   // stdout - should not have a try so that exit codes can surface to stdout
16 |   if (options.stdout) {
17 |     const {
18 |       processedEnvs
19 |     } = new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile).run()
20 | 
21 |     for (const processedEnv of processedEnvs) {
22 |       console.log(processedEnv.envSrc)
23 |     }
24 |     process.exit(0) // exit early
25 |   } else {
26 |     try {
27 |       const {
28 |         processedEnvs,
29 |         changedFilepaths,
30 |         unchangedFilepaths
31 |       } = new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile).run()
32 | 
33 |       for (const processedEnv of processedEnvs) {
34 |         logger.verbose(`encrypting ${processedEnv.envFilepath} (${processedEnv.filepath})`)
35 |         if (processedEnv.error) {
36 |           if (processedEnv.error.code === 'MISSING_ENV_FILE') {
37 |             logger.warn(processedEnv.error.message)
38 |             logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx encrypt]`)
39 |           } else {
40 |             logger.warn(processedEnv.error.message)
41 |             if (processedEnv.error.help) {
42 |               logger.help(processedEnv.error.help)
43 |             }
44 |           }
45 |         } else if (processedEnv.changed) {
46 |           fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc)
47 | 
48 |           logger.verbose(`encrypted ${processedEnv.envFilepath} (${processedEnv.filepath})`)
49 |         } else {
50 |           logger.verbose(`no changes ${processedEnv.envFilepath} (${processedEnv.filepath})`)
51 |         }
52 |       }
53 | 
54 |       if (changedFilepaths.length > 0) {
55 |         logger.success(`✔ encrypted (${changedFilepaths.join(',')})`)
56 |       } else if (unchangedFilepaths.length > 0) {
57 |         logger.info(`no changes (${unchangedFilepaths})`)
58 |       } else {
59 |         // do nothing - scenario when no .env files found
60 |       }
61 | 
62 |       for (const processedEnv of processedEnvs) {
63 |         if (processedEnv.privateKeyAdded) {
64 |           logger.success(`✔ key added to .env.keys (${processedEnv.privateKeyName})`)
65 | 
66 |           if (!isIgnoringDotenvKeys()) {
67 |             logger.help('⮕  next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys')
68 |           }
69 | 
70 |           logger.help(`⮕  next run [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx run -- yourcommand] to test decryption locally`)
71 |         }
72 |       }
73 |     } catch (error) {
74 |       catchAndLog(error)
75 |       process.exit(1)
76 |     }
77 |   }
78 | }
79 | 
80 | module.exports = encrypt
81 | 


--------------------------------------------------------------------------------
/src/cli/actions/ext/genexample.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./../../../lib/helpers/fsx')
 2 | const main = require('./../../../lib/main')
 3 | const { logger } = require('./../../../shared/logger')
 4 | 
 5 | function genexample (directory) {
 6 |   logger.debug(`directory: ${directory}`)
 7 | 
 8 |   const options = this.opts()
 9 |   logger.debug(`options: ${JSON.stringify(options)}`)
10 | 
11 |   try {
12 |     const {
13 |       envExampleFile,
14 |       envFile,
15 |       exampleFilepath,
16 |       addedKeys
17 |     } = main.genexample(directory, options.envFile)
18 | 
19 |     logger.verbose(`loading env from ${envFile}`)
20 | 
21 |     fsx.writeFileX(exampleFilepath, envExampleFile)
22 | 
23 |     if (addedKeys.length > 0) {
24 |       logger.success(`updated .env.example (${addedKeys.length})`)
25 |     } else {
26 |       logger.info('no changes (.env.example)')
27 |     }
28 |   } catch (error) {
29 |     logger.error(error.message)
30 |     if (error.help) {
31 |       logger.help(error.help)
32 |     }
33 |     if (error.code) {
34 |       logger.debug(`ERROR_CODE: ${error.code}`)
35 |     }
36 |     process.exit(1)
37 |   }
38 | }
39 | 
40 | module.exports = genexample
41 | 


--------------------------------------------------------------------------------
/src/cli/actions/ext/gitignore.js:
--------------------------------------------------------------------------------
  1 | const fsx = require('./../../../lib/helpers/fsx')
  2 | 
  3 | const DEFAULT_PATTERNS = ['.env*']
  4 | const { logger } = require('./../../../shared/logger')
  5 | 
  6 | class Generic {
  7 |   constructor (filename, patterns = DEFAULT_PATTERNS, touchFile = false) {
  8 |     this.filename = filename
  9 |     this.patterns = patterns
 10 |     this.touchFile = touchFile
 11 |   }
 12 | 
 13 |   append (str) {
 14 |     fsx.appendFileSync(this.filename, `\n${str}`)
 15 |   }
 16 | 
 17 |   run () {
 18 |     const changedPatterns = []
 19 |     if (!fsx.existsSync(this.filename)) {
 20 |       if (this.touchFile === true && this.patterns.length > 0) {
 21 |         fsx.writeFileX(this.filename, '')
 22 |       } else {
 23 |         return
 24 |       }
 25 |     }
 26 | 
 27 |     const lines = fsx.readFileX(this.filename).split(/\r?\n/)
 28 |     this.patterns.forEach(pattern => {
 29 |       if (!lines.includes(pattern.trim())) {
 30 |         this.append(pattern)
 31 | 
 32 |         changedPatterns.push(pattern.trim())
 33 |       }
 34 |     })
 35 | 
 36 |     if (changedPatterns.length > 0) {
 37 |       logger.success(`✔ ignored ${this.patterns} (${this.filename})`)
 38 |     } else {
 39 |       logger.info(`no changes (${this.filename})`)
 40 |     }
 41 |   }
 42 | }
 43 | 
 44 | class Git {
 45 |   constructor (patterns = DEFAULT_PATTERNS) {
 46 |     this.patterns = patterns
 47 |   }
 48 | 
 49 |   run () {
 50 |     logger.verbose('add to .gitignore')
 51 |     new Generic('.gitignore', this.patterns, true).run()
 52 |   }
 53 | }
 54 | 
 55 | class Docker {
 56 |   constructor (patterns = DEFAULT_PATTERNS) {
 57 |     this.patterns = patterns
 58 |   }
 59 | 
 60 |   run () {
 61 |     logger.verbose('add to .dockerignore (if exists)')
 62 |     new Generic('.dockerignore', this.patterns).run()
 63 |   }
 64 | }
 65 | 
 66 | class Npm {
 67 |   constructor (patterns = DEFAULT_PATTERNS) {
 68 |     this.patterns = patterns
 69 |   }
 70 | 
 71 |   run () {
 72 |     logger.verbose('add to .npmignore (if existing)')
 73 |     new Generic('.npmignore', this.patterns).run()
 74 |   }
 75 | }
 76 | 
 77 | class Vercel {
 78 |   constructor (patterns = DEFAULT_PATTERNS) {
 79 |     this.patterns = patterns
 80 |   }
 81 | 
 82 |   run () {
 83 |     logger.verbose('add to .vercelignore (if existing)')
 84 |     new Generic('.vercelignore', this.patterns).run()
 85 |   }
 86 | }
 87 | 
 88 | function gitignore () {
 89 |   const options = this.opts()
 90 |   logger.debug(`options: ${JSON.stringify(options)}`)
 91 | 
 92 |   const patterns = options.pattern
 93 | 
 94 |   new Git(patterns).run()
 95 |   new Docker(patterns).run()
 96 |   new Npm(patterns).run()
 97 |   new Vercel(patterns).run()
 98 | }
 99 | 
100 | module.exports = gitignore
101 | module.exports.Git = Git
102 | module.exports.Docker = Docker
103 | module.exports.Npm = Npm
104 | module.exports.Vercel = Vercel
105 | module.exports.Generic = Generic
106 | 


--------------------------------------------------------------------------------
/src/cli/actions/ext/prebuild.js:
--------------------------------------------------------------------------------
 1 | const { logger } = require('./../../../shared/logger')
 2 | 
 3 | const Prebuild = require('./../../../lib/services/prebuild')
 4 | 
 5 | function prebuild (directory) {
 6 |   // debug args
 7 |   logger.debug(`directory: ${directory}`)
 8 | 
 9 |   const options = this.opts()
10 |   logger.debug(`options: ${JSON.stringify(options)}`)
11 | 
12 |   try {
13 |     const {
14 |       successMessage,
15 |       warnings
16 |     } = new Prebuild(directory, options).run()
17 | 
18 |     for (const warning of warnings) {
19 |       logger.warn(warning.message)
20 |       if (warning.help) {
21 |         logger.help(warning.help)
22 |       }
23 |     }
24 | 
25 |     logger.success(successMessage)
26 |   } catch (error) {
27 |     logger.error(error.message)
28 |     if (error.help) {
29 |       logger.help(error.help)
30 |     }
31 | 
32 |     process.exit(1)
33 |   }
34 | }
35 | 
36 | module.exports = prebuild
37 | 


--------------------------------------------------------------------------------
/src/cli/actions/ext/precommit.js:
--------------------------------------------------------------------------------
 1 | const { logger } = require('./../../../shared/logger')
 2 | 
 3 | const Precommit = require('./../../../lib/services/precommit')
 4 | 
 5 | function precommit (directory) {
 6 |   // debug args
 7 |   logger.debug(`directory: ${directory}`)
 8 | 
 9 |   const options = this.opts()
10 |   logger.debug(`options: ${JSON.stringify(options)}`)
11 | 
12 |   try {
13 |     const {
14 |       successMessage,
15 |       warnings
16 |     } = new Precommit(directory, options).run()
17 | 
18 |     for (const warning of warnings) {
19 |       logger.warn(warning.message)
20 |       if (warning.help) {
21 |         logger.help(warning.help)
22 |       }
23 |     }
24 | 
25 |     logger.success(successMessage)
26 |   } catch (error) {
27 |     logger.error(error.message)
28 |     if (error.help) {
29 |       logger.help(error.help)
30 |     }
31 | 
32 |     process.exit(1)
33 |   }
34 | }
35 | 
36 | module.exports = precommit
37 | 


--------------------------------------------------------------------------------
/src/cli/actions/ext/scan.js:
--------------------------------------------------------------------------------
 1 | const childProcess = require('child_process')
 2 | 
 3 | const { logger } = require('./../../../shared/logger')
 4 | const chomp = require('./../../../lib/helpers/chomp')
 5 | 
 6 | function scan () {
 7 |   const options = this.opts()
 8 |   logger.debug(`options: ${JSON.stringify(options)}`)
 9 | 
10 |   try {
11 |     // redirect stderr to stdout to capture and ignore it
12 |     childProcess.execSync('gitleaks version', { stdio: ['ignore', 'pipe', 'ignore'] })
13 |   } catch (error) {
14 |     logger.error('gitleaks: command not found')
15 |     logger.help('? install gitleaks:      [brew install gitleaks]')
16 |     logger.help('? other install options: [https://github.com/gitleaks/gitleaks]')
17 |     process.exit(1)
18 |     return
19 |   }
20 | 
21 |   let output = ''
22 |   try {
23 |     output = childProcess.execSync('gitleaks detect --no-banner --verbose 2>&1').toString() // gitleaks sends exit code 1 but puts data on stdout for failures, so we catch later and resurface the stdout
24 |     logger.info(chomp(output))
25 |   } catch (error) {
26 |     if (error.stdout) {
27 |       logger.error(chomp(error.stdout.toString()))
28 |     }
29 | 
30 |     process.exit(1)
31 |   }
32 | }
33 | 
34 | module.exports = scan
35 | 


--------------------------------------------------------------------------------
/src/cli/actions/get.js:
--------------------------------------------------------------------------------
 1 | const { logger } = require('./../../shared/logger')
 2 | 
 3 | const conventions = require('./../../lib/helpers/conventions')
 4 | const escape = require('./../../lib/helpers/escape')
 5 | 
 6 | const Get = require('./../../lib/services/get')
 7 | 
 8 | function get (key) {
 9 |   if (key) {
10 |     logger.debug(`key: ${key}`)
11 |   }
12 | 
13 |   const options = this.opts()
14 |   logger.debug(`options: ${JSON.stringify(options)}`)
15 | 
16 |   const ignore = options.ignore || []
17 | 
18 |   let envs = []
19 |   // handle shorthand conventions - like --convention=nextjs
20 |   if (options.convention) {
21 |     envs = conventions(options.convention).concat(this.envs)
22 |   } else {
23 |     envs = this.envs
24 |   }
25 | 
26 |   try {
27 |     const { parsed, errors } = new Get(key, envs, options.overload, process.env.DOTENV_KEY, options.all, options.envKeysFile).run()
28 | 
29 |     for (const error of errors || []) {
30 |       if (options.strict) throw error // throw immediately if strict
31 | 
32 |       if (ignore.includes(error.code)) {
33 |         continue // ignore error
34 |       }
35 | 
36 |       logger.error(error.message)
37 |       if (error.help) {
38 |         logger.error(error.help)
39 |       }
40 |     }
41 | 
42 |     if (key) {
43 |       const single = parsed[key]
44 |       if (single === undefined) {
45 |         console.log('')
46 |       } else {
47 |         console.log(single)
48 |       }
49 |     } else {
50 |       if (options.format === 'eval') {
51 |         let inline = ''
52 |         for (const [key, value] of Object.entries(parsed)) {
53 |           inline += `${key}=${escape(value)}\n`
54 |         }
55 |         inline = inline.trim()
56 | 
57 |         console.log(inline)
58 |       } else if (options.format === 'shell') {
59 |         let inline = ''
60 |         for (const [key, value] of Object.entries(parsed)) {
61 |           inline += `${key}=${value} `
62 |         }
63 |         inline = inline.trim()
64 | 
65 |         console.log(inline)
66 |       } else {
67 |         let space = 0
68 |         if (options.prettyPrint) {
69 |           space = 2
70 |         }
71 | 
72 |         console.log(JSON.stringify(parsed, null, space))
73 |       }
74 |     }
75 |   } catch (error) {
76 |     logger.error(error.message)
77 |     if (error.help) {
78 |       logger.error(error.help)
79 |     }
80 |     process.exit(1)
81 |   }
82 | }
83 | 
84 | module.exports = get
85 | 


--------------------------------------------------------------------------------
/src/cli/actions/keypair.js:
--------------------------------------------------------------------------------
 1 | const { logger } = require('./../../shared/logger')
 2 | 
 3 | const main = require('./../../lib/main')
 4 | 
 5 | function keypair (key) {
 6 |   if (key) {
 7 |     logger.debug(`key: ${key}`)
 8 |   }
 9 | 
10 |   const options = this.opts()
11 |   logger.debug(`options: ${JSON.stringify(options)}`)
12 | 
13 |   const results = main.keypair(options.envFile, key, options.envKeysFile)
14 | 
15 |   if (typeof results === 'object' && results !== null) {
16 |     // inline shell format - env $(dotenvx keypair --format=shell) your-command
17 |     if (options.format === 'shell') {
18 |       let inline = ''
19 |       for (const [key, value] of Object.entries(results)) {
20 |         inline += `${key}=${value || ''} `
21 |       }
22 |       inline = inline.trim()
23 | 
24 |       console.log(inline)
25 |     // json format
26 |     } else {
27 |       let space = 0
28 |       if (options.prettyPrint) {
29 |         space = 2
30 |       }
31 | 
32 |       console.log(JSON.stringify(results, null, space))
33 |     }
34 |   } else {
35 |     if (results === undefined) {
36 |       console.log('')
37 |       process.exit(1)
38 |     } else {
39 |       console.log(results)
40 |     }
41 |   }
42 | }
43 | 
44 | module.exports = keypair
45 | 


--------------------------------------------------------------------------------
/src/cli/actions/ls.js:
--------------------------------------------------------------------------------
 1 | const treeify = require('object-treeify')
 2 | 
 3 | const { logger } = require('./../../shared/logger')
 4 | 
 5 | const main = require('./../../lib/main')
 6 | const ArrayToTree = require('./../../lib/helpers/arrayToTree')
 7 | 
 8 | function ls (directory) {
 9 |   // debug args
10 |   logger.debug(`directory: ${directory}`)
11 | 
12 |   const options = this.opts()
13 |   logger.debug(`options: ${JSON.stringify(options)}`)
14 | 
15 |   const filepaths = main.ls(directory, options.envFile, options.excludeEnvFile)
16 |   logger.debug(`filepaths: ${JSON.stringify(filepaths)}`)
17 | 
18 |   const tree = new ArrayToTree(filepaths).run()
19 |   logger.debug(`tree: ${JSON.stringify(tree)}`)
20 | 
21 |   logger.info(treeify(tree))
22 | }
23 | 
24 | module.exports = ls
25 | 


--------------------------------------------------------------------------------
/src/cli/actions/rotate.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./../../lib/helpers/fsx')
 2 | const { logger } = require('./../../shared/logger')
 3 | 
 4 | const Rotate = require('./../../lib/services/rotate')
 5 | 
 6 | const catchAndLog = require('../../lib/helpers/catchAndLog')
 7 | const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys')
 8 | 
 9 | function rotate () {
10 |   const options = this.opts()
11 |   logger.debug(`options: ${JSON.stringify(options)}`)
12 | 
13 |   const envs = this.envs
14 | 
15 |   // stdout - should not have a try so that exit codes can surface to stdout
16 |   if (options.stdout) {
17 |     const {
18 |       processedEnvs
19 |     } = new Rotate(envs, options.key, options.excludeKey, options.envKeysFile).run()
20 | 
21 |     for (const processedEnv of processedEnvs) {
22 |       console.log(processedEnv.envSrc)
23 |       console.log('')
24 |       console.log(processedEnv.envKeysSrc)
25 |     }
26 |     process.exit(0) // exit early
27 |   } else {
28 |     try {
29 |       const {
30 |         processedEnvs,
31 |         changedFilepaths,
32 |         unchangedFilepaths
33 |       } = new Rotate(envs, options.key, options.excludeKey, options.envKeysFile).run()
34 | 
35 |       for (const processedEnv of processedEnvs) {
36 |         logger.verbose(`rotating ${processedEnv.envFilepath} (${processedEnv.filepath})`)
37 |         if (processedEnv.error) {
38 |           if (processedEnv.error.code === 'MISSING_ENV_FILE') {
39 |             logger.warn(processedEnv.error.message)
40 |             logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx rotate]`)
41 |           } else {
42 |             logger.warn(processedEnv.error.message)
43 |             if (processedEnv.error.help) {
44 |               logger.help(processedEnv.error.help)
45 |             }
46 |           }
47 |         } else if (processedEnv.changed) {
48 |           fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc)
49 |           fsx.writeFileX(processedEnv.envKeysFilepath, processedEnv.envKeysSrc)
50 | 
51 |           logger.verbose(`rotated ${processedEnv.envFilepath} (${processedEnv.filepath})`)
52 |         } else {
53 |           logger.verbose(`no changes ${processedEnv.envFilepath} (${processedEnv.filepath})`)
54 |         }
55 |       }
56 | 
57 |       if (changedFilepaths.length > 0) {
58 |         logger.success(`✔ rotated (${changedFilepaths.join(',')})`)
59 |       } else if (unchangedFilepaths.length > 0) {
60 |         logger.info(`no changes (${unchangedFilepaths})`)
61 |       } else {
62 |         // do nothing - scenario when no .env files found
63 |       }
64 | 
65 |       for (const processedEnv of processedEnvs) {
66 |         if (processedEnv.privateKeyAdded) {
67 |           logger.success(`✔ key added to .env.keys (${processedEnv.privateKeyName})`)
68 | 
69 |           if (!isIgnoringDotenvKeys()) {
70 |             logger.help('⮕  next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys')
71 |           }
72 | 
73 |           logger.help(`⮕  next run [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx get] to test decryption locally`)
74 |         }
75 |       }
76 |     } catch (error) {
77 |       catchAndLog(error)
78 |       process.exit(1)
79 |     }
80 |   }
81 | }
82 | 
83 | module.exports = rotate
84 | 


--------------------------------------------------------------------------------
/src/cli/actions/set.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./../../lib/helpers/fsx')
 2 | const { logger } = require('./../../shared/logger')
 3 | 
 4 | const Sets = require('./../../lib/services/sets')
 5 | 
 6 | const catchAndLog = require('../../lib/helpers/catchAndLog')
 7 | const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys')
 8 | 
 9 | function set (key, value) {
10 |   logger.debug(`key: ${key}`)
11 |   logger.debug(`value: ${value}`)
12 | 
13 |   const options = this.opts()
14 |   logger.debug(`options: ${JSON.stringify(options)}`)
15 | 
16 |   // encrypt
17 |   let encrypt = true
18 |   if (options.plain) {
19 |     encrypt = false
20 |   }
21 | 
22 |   try {
23 |     const envs = this.envs
24 |     const envKeysFilepath = options.envKeysFile
25 | 
26 |     const {
27 |       processedEnvs,
28 |       changedFilepaths,
29 |       unchangedFilepaths
30 |     } = new Sets(key, value, envs, encrypt, envKeysFilepath).run()
31 | 
32 |     let withEncryption = ''
33 | 
34 |     if (encrypt) {
35 |       withEncryption = ' with encryption'
36 |     }
37 | 
38 |     for (const processedEnv of processedEnvs) {
39 |       logger.verbose(`setting for ${processedEnv.envFilepath}`)
40 | 
41 |       if (processedEnv.error) {
42 |         if (processedEnv.error.code === 'MISSING_ENV_FILE') {
43 |           logger.warn(processedEnv.error.message)
44 |           logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx set]`)
45 |         } else {
46 |           logger.warn(processedEnv.error.message)
47 |           if (processedEnv.error.help) {
48 |             logger.help(processedEnv.error.help)
49 |           }
50 |         }
51 |       } else {
52 |         fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc)
53 | 
54 |         logger.verbose(`${processedEnv.key} set${withEncryption} (${processedEnv.envFilepath})`)
55 |         logger.debug(`${processedEnv.key} set${withEncryption} to ${processedEnv.value} (${processedEnv.envFilepath})`)
56 |       }
57 |     }
58 | 
59 |     if (changedFilepaths.length > 0) {
60 |       logger.success(`✔ set ${key}${withEncryption} (${changedFilepaths.join(',')})`)
61 |     } else if (unchangedFilepaths.length > 0) {
62 |       logger.info(`no changes (${unchangedFilepaths})`)
63 |     } else {
64 |       // do nothing
65 |     }
66 | 
67 |     for (const processedEnv of processedEnvs) {
68 |       if (processedEnv.privateKeyAdded) {
69 |         logger.success(`✔ key added to ${processedEnv.envKeysFilepath} (${processedEnv.privateKeyName})`)
70 | 
71 |         if (!isIgnoringDotenvKeys()) {
72 |           logger.help('⮕  next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys')
73 |         }
74 | 
75 |         logger.help(`⮕  next run [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx get ${key}] to test decryption locally`)
76 |       }
77 |     }
78 |   } catch (error) {
79 |     catchAndLog(error)
80 |     process.exit(1)
81 |   }
82 | }
83 | 
84 | module.exports = set
85 | 


--------------------------------------------------------------------------------
/src/cli/commands/ext.js:
--------------------------------------------------------------------------------
 1 | const { Command } = require('commander')
 2 | 
 3 | const examples = require('./../examples')
 4 | const executeExtension = require('../../lib/helpers/executeExtension')
 5 | const removeDynamicHelpSection = require('../../lib/helpers/removeDynamicHelpSection')
 6 | 
 7 | const ext = new Command('ext')
 8 | 
 9 | ext
10 |   .description('🔌 extensions')
11 |   .allowUnknownOption()
12 | 
13 | // list known extensions here you want to display
14 | ext.addHelpText('after', '  vault                             🔐 manage .env.vault files')
15 | 
16 | ext
17 |   .argument('[command]', 'dynamic ext command')
18 |   .argument('[args...]', 'dynamic ext command arguments')
19 |   .action((command, args, cmdObj) => {
20 |     const rawArgs = process.argv.slice(3) // adjust the index based on where actual args start
21 |     executeExtension(ext, command, rawArgs)
22 |   })
23 | 
24 | // dotenvx ext ls
25 | ext.command('ls')
26 |   .description('print all .env files in a tree structure')
27 |   .argument('[directory]', 'directory to list .env files from', '.')
28 |   .option('-f, --env-file <filenames...>', 'path(s) to your env file(s)', '.env*')
29 |   .option('-ef, --exclude-env-file <excludeFilenames...>', 'path(s) to exclude from your env file(s) (default: none)')
30 |   .action(require('./../actions/ls'))
31 | 
32 | // dotenvx ext genexample
33 | ext.command('genexample')
34 |   .description('generate .env.example')
35 |   .argument('[directory]', 'directory to generate from', '.')
36 |   .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env')
37 |   .action(require('./../actions/ext/genexample'))
38 | 
39 | // dotenvx ext gitignore
40 | ext.command('gitignore')
41 |   .description('append to .gitignore file (and if existing, .dockerignore, .npmignore, and .vercelignore)')
42 |   .addHelpText('after', examples.gitignore)
43 |   .option('--pattern <patterns...>', 'pattern(s) to gitignore', ['.env*'])
44 |   .action(require('./../actions/ext/gitignore'))
45 | 
46 | // dotenvx ext prebuild
47 | ext.command('prebuild')
48 |   .description('prevent including .env files in docker builds')
49 |   .addHelpText('after', examples.prebuild)
50 |   .argument('[directory]', 'directory to prevent including .env files from', '.')
51 |   .action(require('./../actions/ext/prebuild'))
52 | 
53 | // dotenvx ext precommit
54 | ext.command('precommit')
55 |   .description('prevent committing .env files to code')
56 |   .addHelpText('after', examples.precommit)
57 |   .argument('[directory]', 'directory to prevent committing .env files from', '.')
58 |   .option('-i, --install', 'install to .git/hooks/pre-commit')
59 |   .action(require('./../actions/ext/precommit'))
60 | 
61 | // dotenvx scan
62 | ext.command('scan')
63 |   .description('scan for leaked secrets')
64 |   .action(require('./../actions/ext/scan'))
65 | 
66 | // override helpInformation to hide dynamic commands
67 | ext.helpInformation = function () {
68 |   const originalHelp = Command.prototype.helpInformation.call(this)
69 |   const lines = originalHelp.split('\n')
70 | 
71 |   removeDynamicHelpSection(lines)
72 | 
73 |   return lines.join('\n')
74 | }
75 | 
76 | module.exports = ext
77 | 


--------------------------------------------------------------------------------
/src/cli/examples.js:
--------------------------------------------------------------------------------
  1 | const run = function () {
  2 |   return `
  3 | Examples:
  4 | 
  5 |   \`\`\`
  6 |   $ dotenvx run -- npm run dev
  7 |   $ dotenvx run -- flask --app index run
  8 |   $ dotenvx run -- php artisan serve
  9 |   $ dotenvx run -- bin/rails s
 10 |   \`\`\`
 11 | 
 12 | Try it:
 13 | 
 14 |   \`\`\`
 15 |   $ echo "HELLO=World" > .env
 16 |   $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
 17 | 
 18 |   $ dotenvx run -f .env -- node index.js
 19 |   [dotenvx] injecting env (1) from .env
 20 |   Hello World
 21 |   \`\`\`
 22 |   `
 23 | }
 24 | 
 25 | const precommit = function () {
 26 |   return `
 27 | Examples:
 28 | 
 29 |   \`\`\`
 30 |   $ dotenvx ext precommit
 31 |   $ dotenvx ext precommit --install
 32 |   \`\`\`
 33 | 
 34 | Try it:
 35 | 
 36 |   \`\`\`
 37 |   $ dotenvx ext precommit
 38 |   [dotenvx@0.45.0][precommit] success
 39 |   \`\`\`
 40 |   `
 41 | }
 42 | 
 43 | const prebuild = function () {
 44 |   return `
 45 | Examples:
 46 | 
 47 |   \`\`\`
 48 |   $ dotenvx ext prebuild
 49 |   \`\`\`
 50 | 
 51 | Try it:
 52 | 
 53 |   \`\`\`
 54 |   $ dotenvx ext prebuild
 55 |   [dotenvx@0.10.0][prebuild] success
 56 |   \`\`\`
 57 |   `
 58 | }
 59 | 
 60 | const gitignore = function () {
 61 |   return `
 62 | Examples:
 63 | 
 64 |   \`\`\`
 65 |   $ dotenvx ext gitignore
 66 |   $ dotenvx ext gitignore --pattern .env.keys
 67 |   \`\`\`
 68 | 
 69 | Try it:
 70 | 
 71 |   \`\`\`
 72 |   $ dotenvx ext gitignore
 73 |   ✔ ignored .env* (.gitignore)
 74 |   \`\`\`
 75 |   `
 76 | }
 77 | 
 78 | const set = function () {
 79 |   return `
 80 | Examples:
 81 | 
 82 |   \`\`\`
 83 |   $ dotenvx set KEY value
 84 |   $ dotenvx set KEY "value with spaces"
 85 |   $ dotenvx set KEY -- "---value with a dash---"
 86 |   $ dotenvx set KEY -- "-----BEGIN OPENSSH PRIVATE KEY-----
 87 |                         b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 88 |                         -----END OPENSSH PRIVATE KEY-----"
 89 |   \`\`\`
 90 |   `
 91 | }
 92 | 
 93 | module.exports = {
 94 |   run,
 95 |   precommit,
 96 |   prebuild,
 97 |   gitignore,
 98 |   set
 99 | }
100 | 


--------------------------------------------------------------------------------
/src/lib/config.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | 


--------------------------------------------------------------------------------
/src/lib/config.js:
--------------------------------------------------------------------------------
1 | require('./main.js').config()
2 | 


--------------------------------------------------------------------------------
/src/lib/helpers/append.js:
--------------------------------------------------------------------------------
 1 | const quotes = require('./quotes')
 2 | const dotenvParse = require('./dotenvParse')
 3 | const escapeForRegex = require('./escapeForRegex')
 4 | const escapeDollarSigns = require('./escapeDollarSigns')
 5 | 
 6 | function append (src, key, appendValue) {
 7 |   let output
 8 |   let newPart = ''
 9 | 
10 |   const parsed = dotenvParse(src, true, true) // skip expanding \n and skip converting \r\n
11 |   const _quotes = quotes(src)
12 |   if (Object.prototype.hasOwnProperty.call(parsed, key)) {
13 |     const quote = _quotes[key]
14 |     const originalValue = parsed[key]
15 | 
16 |     newPart += `${key}=${quote}${originalValue},${appendValue}${quote}`
17 | 
18 |     const escapedOriginalValue = escapeForRegex(originalValue)
19 | 
20 |     // conditionally enforce end of line
21 |     let enforceEndOfLine = ''
22 |     if (escapedOriginalValue === '') {
23 |       enforceEndOfLine = '
#39; // EMPTY scenario
24 |     }
25 | 
26 |     const currentPart = new RegExp(
27 |       '^' + // start of line
28 |       '(\\s*)?' + // spaces
29 |       '(export\\s+)?' + // export
30 |       key + // KEY
31 |       '\\s*=\\s*' + // spaces (KEY = value)
32 |       '["\'`]?' + // open quote
33 |       escapedOriginalValue + // escaped value
34 |       '["\'`]?' + // close quote
35 |       enforceEndOfLine
36 |       ,
37 |       'gm' // (g)lobal (m)ultiline
38 |     )
39 | 
40 |     const saferInput = escapeDollarSigns(newPart) // cleanse user inputted capture groups ($1, $2 etc)
41 | 
42 |     // $1 preserves spaces
43 |     // $2 preserves export
44 |     output = src.replace(currentPart, `$1$2${saferInput}`)
45 |   } else {
46 |     newPart += `${key}="${appendValue}"`
47 | 
48 |     // append
49 |     if (src.endsWith('\n')) {
50 |       newPart = newPart + '\n'
51 |     } else {
52 |       newPart = '\n' + newPart
53 |     }
54 | 
55 |     output = src + newPart
56 |   }
57 | 
58 |   return output
59 | }
60 | 
61 | module.exports = append
62 | 


--------------------------------------------------------------------------------
/src/lib/helpers/arrayToTree.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | 
 3 | class ArrayToTree {
 4 |   constructor (arr) {
 5 |     this.arr = arr
 6 |   }
 7 | 
 8 |   run () {
 9 |     const tree = {}
10 | 
11 |     for (let i = 0; i < this.arr.length; i++) {
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
14 |       let current = tree
15 | 
16 |       for (let j = 0; j < parts.length; j++) {
17 |         const part = parts[j]
18 |         current[part] = current[part] || {}
19 |         current = current[part]
20 |       }
21 |     }
22 | 
23 |     return tree
24 |   }
25 | }
26 | 
27 | module.exports = ArrayToTree
28 | 


--------------------------------------------------------------------------------
/src/lib/helpers/buildEnvs.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | 
 3 | const conventions = require('./conventions')
 4 | const dotenvOptionPaths = require('./dotenvOptionPaths')
 5 | const DeprecationNotice = require('./deprecationNotice')
 6 | 
 7 | function buildEnvs (options, DOTENV_KEY = undefined) {
 8 |   // build envs using user set option.path
 9 |   const optionPaths = dotenvOptionPaths(options) // [ '.env' ]
10 | 
11 |   let envs = []
12 |   if (options.convention) { // handle shorthand conventions
13 |     envs = conventions(options.convention).concat(envs)
14 |   }
15 | 
16 |   new DeprecationNotice({ DOTENV_KEY }).dotenvKey() // DEPRECATION NOTICE
17 | 
18 |   for (const optionPath of optionPaths) {
19 |     // if DOTENV_KEY is set then assume we are checking envVaultFile
20 |     if (DOTENV_KEY) {
21 |       envs.push({
22 |         type: 'envVaultFile',
23 |         value: path.join(path.dirname(optionPath), '.env.vault')
24 |       })
25 |     } else {
26 |       envs.push({ type: 'envFile', value: optionPath })
27 |     }
28 |   }
29 | 
30 |   return envs
31 | }
32 | 
33 | module.exports = buildEnvs
34 | 


--------------------------------------------------------------------------------
/src/lib/helpers/catchAndLog.js:
--------------------------------------------------------------------------------
 1 | const { logger } = require('./../../shared/logger')
 2 | 
 3 | function catchAndLog (error) {
 4 |   logger.error(error.message)
 5 |   if (error.help) {
 6 |     logger.help(error.help)
 7 |   }
 8 |   if (error.debug) {
 9 |     logger.debug(error.debug)
10 |   }
11 |   if (error.code) {
12 |     logger.debug(`ERROR_CODE: ${error.code}`)
13 |   }
14 | }
15 | 
16 | module.exports = catchAndLog
17 | 


--------------------------------------------------------------------------------
/src/lib/helpers/chomp.js:
--------------------------------------------------------------------------------
1 | function chomp (value) {
2 |   return value.replace(/[\r\n]+$/, '')
3 | }
4 | 
5 | module.exports = chomp
6 | 


--------------------------------------------------------------------------------
/src/lib/helpers/colorDepth.js:
--------------------------------------------------------------------------------
 1 | const { WriteStream } = require('tty')
 2 | 
 3 | const getColorDepth = () => {
 4 |   try {
 5 |     return WriteStream.prototype.getColorDepth()
 6 |   } catch (error) {
 7 |     const term = process.env.TERM
 8 | 
 9 |     if (term && (term.includes('256color') || term.includes('xterm'))) {
10 |       return 8 // 256 colors
11 |     }
12 | 
13 |     return 4
14 |   }
15 | }
16 | 
17 | module.exports = { getColorDepth }
18 | 


--------------------------------------------------------------------------------
/src/lib/helpers/conventions.js:
--------------------------------------------------------------------------------
 1 | function conventions (convention) {
 2 |   const env = process.env.DOTENV_ENV || process.env.NODE_ENV || 'development'
 3 | 
 4 |   if (convention === 'nextjs') {
 5 |     const canonicalEnv = ['development', 'test', 'production'].includes(env) && env
 6 | 
 7 |     return [
 8 |       canonicalEnv && { type: 'envFile', value: `.env.${canonicalEnv}.local` },
 9 |       canonicalEnv !== 'test' && { type: 'envFile', value: '.env.local' },
10 |       canonicalEnv && { type: 'envFile', value: `.env.${canonicalEnv}` },
11 |       { type: 'envFile', value: '.env' }
12 |     ].filter(Boolean)
13 |   } else if (convention === 'flow') {
14 |     return [
15 |       { type: 'envFile', value: `.env.${env}.local` },
16 |       { type: 'envFile', value: `.env.${env}` },
17 |       { type: 'envFile', value: '.env.local' },
18 |       { type: 'envFile', value: '.env' },
19 |       { type: 'envFile', value: '.env.defaults' }
20 |     ]
21 |   } else {
22 |     throw new Error(`INVALID_CONVENTION: '${convention}'. permitted conventions: ['nextjs', 'flow']`)
23 |   }
24 | }
25 | 
26 | module.exports = conventions
27 | 


--------------------------------------------------------------------------------
/src/lib/helpers/decrypt.js:
--------------------------------------------------------------------------------
 1 | const dotenv = require('dotenv')
 2 | 
 3 | const parseEncryptionKeyFromDotenvKey = require('./parseEncryptionKeyFromDotenvKey')
 4 | 
 5 | function decrypt (ciphertext, dotenvKey) {
 6 |   const key = parseEncryptionKeyFromDotenvKey(dotenvKey)
 7 | 
 8 |   try {
 9 |     return dotenv.decrypt(ciphertext, key)
10 |   } catch (e) {
11 |     if (e.code === 'DECRYPTION_FAILED') {
12 |       const error = new Error('[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.')
13 |       error.code = 'DECRYPTION_FAILED'
14 |       error.help = '[DECRYPTION_FAILED] Run with debug flag [dotenvx run --debug -- yourcommand] or manually run [echo $DOTENV_KEY] to compare it to the one in .env.keys.'
15 |       error.debug = `[DECRYPTION_FAILED] DOTENV_KEY is ${dotenvKey}`
16 |       throw error
17 |     }
18 | 
19 |     if (e.code === 'ERR_CRYPTO_INVALID_AUTH_TAG') {
20 |       const error = new Error('[INVALID_CIPHERTEXT] Unable to decrypt what appears to be invalid ciphertext.')
21 |       error.code = 'INVALID_CIPHERTEXT'
22 |       error.help = '[INVALID_CIPHERTEXT] Run with debug flag [dotenvx run --debug -- yourcommand] or manually check .env.vault.'
23 |       error.debug = `[INVALID_CIPHERTEXT] ciphertext is '${ciphertext}'`
24 |       throw error
25 |     }
26 | 
27 |     throw e
28 |   }
29 | }
30 | 
31 | module.exports = decrypt
32 | 


--------------------------------------------------------------------------------
/src/lib/helpers/decryptKeyValue.js:
--------------------------------------------------------------------------------
 1 | const { decrypt } = require('eciesjs')
 2 | 
 3 | const Errors = require('./errors')
 4 | 
 5 | const PREFIX = 'encrypted:'
 6 | 
 7 | function decryptKeyValue (key, value, privateKeyName, privateKey) {
 8 |   let decryptedValue
 9 |   let decryptionError
10 | 
11 |   if (!value.startsWith(PREFIX)) {
12 |     return value
13 |   }
14 | 
15 |   privateKey = privateKey || ''
16 |   if (privateKey.length <= 0) {
17 |     decryptionError = new Errors({ key, privateKeyName, privateKey }).missingPrivateKey()
18 |   } else {
19 |     const privateKeys = privateKey.split(',')
20 |     for (const privKey of privateKeys) {
21 |       const secret = Buffer.from(privKey, 'hex')
22 |       const encoded = value.substring(PREFIX.length)
23 |       const ciphertext = Buffer.from(encoded, 'base64')
24 | 
25 |       try {
26 |         decryptedValue = decrypt(secret, ciphertext).toString()
27 |         decryptionError = null // reset to null error (scenario for multiple private keys)
28 |         break
29 |       } catch (e) {
30 |         if (e.message === 'Invalid private key') {
31 |           decryptionError = new Errors({ key, privateKeyName, privateKey }).invalidPrivateKey()
32 |         } else if (e.message === 'Unsupported state or unable to authenticate data') {
33 |           decryptionError = new Errors({ key, privateKeyName, privateKey }).looksWrongPrivateKey()
34 |         } else if (e.message === 'Point of length 65 was invalid. Expected 33 compressed bytes or 65 uncompressed bytes') {
35 |           decryptionError = new Errors({ key, privateKeyName, privateKey }).malformedEncryptedData()
36 |         } else {
37 |           decryptionError = new Errors({ key, privateKeyName, privateKey, message: e.message }).decryptionFailed()
38 |         }
39 |       }
40 |     }
41 |   }
42 | 
43 |   if (decryptionError) {
44 |     throw decryptionError
45 |   }
46 | 
47 |   return decryptedValue
48 | }
49 | 
50 | module.exports = decryptKeyValue
51 | 


--------------------------------------------------------------------------------
/src/lib/helpers/deprecationNotice.js:
--------------------------------------------------------------------------------
 1 | const { logger } = require('./../../shared/logger')
 2 | 
 3 | class DeprecationNotice {
 4 |   constructor (options = {}) {
 5 |     this.DOTENV_KEY = options.DOTENV_KEY || process.env.DOTENV_KEY
 6 |   }
 7 | 
 8 |   dotenvKey () {
 9 |     if (this.DOTENV_KEY) {
10 |       logger.warn('[DEPRECATION NOTICE] Setting DOTENV_KEY with .env.vault is deprecated.')
11 |       logger.warn('[DEPRECATION NOTICE] Run [dotenvx ext vault migrate] for instructions on converting your .env.vault file to encrypted .env files (using public key encryption algorithm secp256k1)')
12 |       logger.warn('[DEPRECATION NOTICE] Read more at [https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md#0380]')
13 |     }
14 |   }
15 | }
16 | 
17 | module.exports = DeprecationNotice
18 | 


--------------------------------------------------------------------------------
/src/lib/helpers/detectEncoding.js:
--------------------------------------------------------------------------------
 1 | const fs = require('fs')
 2 | 
 3 | function detectEncoding (filepath) {
 4 |   const buffer = fs.readFileSync(filepath)
 5 | 
 6 |   // check for UTF-16LE BOM (Byte Order Mark)
 7 |   if (buffer.length >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) {
 8 |     return 'utf16le'
 9 |   }
10 | 
11 |   /* c8 ignore start */
12 |   // check for UTF-8 BOM
13 |   if (buffer.length >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
14 |     return 'utf8'
15 |   }
16 | 
17 |   /* c8 ignore stop */
18 | 
19 |   return 'utf8'
20 | }
21 | 
22 | module.exports = detectEncoding
23 | 


--------------------------------------------------------------------------------
/src/lib/helpers/determineEnvs.js:
--------------------------------------------------------------------------------
 1 | const dotenvPrivateKeyNames = require('./dotenvPrivateKeyNames')
 2 | const guessPrivateKeyFilename = require('./guessPrivateKeyFilename')
 3 | 
 4 | const TYPE_ENV_FILE = 'envFile'
 5 | const TYPE_ENV_VAULT_FILE = 'envVaultFile'
 6 | const DEFAULT_ENVS = [{ type: TYPE_ENV_FILE, value: '.env' }]
 7 | const DEFAULT_ENV_VAULTS = [{ type: TYPE_ENV_VAULT_FILE, value: '.env.vault' }]
 8 | 
 9 | function determineEnvsFromDotenvPrivateKey (privateKeyNames) {
10 |   const envs = []
11 | 
12 |   for (const privateKeyName of privateKeyNames) {
13 |     const filename = guessPrivateKeyFilename(privateKeyName)
14 |     envs.push({ type: TYPE_ENV_FILE, value: filename })
15 |   }
16 | 
17 |   return envs
18 | }
19 | 
20 | function determineEnvs (envs = [], processEnv, DOTENV_KEY = '') {
21 |   const privateKeyNames = dotenvPrivateKeyNames(processEnv)
22 |   if (!envs || envs.length <= 0) {
23 |     // if process.env.DOTENV_PRIVATE_KEY or process.env.DOTENV_PRIVATE_KEY_${environment} is set, assume inline encryption methodology
24 |     if (privateKeyNames.length > 0) {
25 |       return determineEnvsFromDotenvPrivateKey(privateKeyNames)
26 |     }
27 | 
28 |     if (DOTENV_KEY.length > 0) {
29 |       // if DOTENV_KEY is set then default to look for .env.vault file
30 |       return DEFAULT_ENV_VAULTS
31 |     } else {
32 |       return DEFAULT_ENVS // default to .env file expectation
33 |     }
34 |   } else {
35 |     let fileAlreadySpecified = false // can be .env or .env.vault type
36 | 
37 |     for (const env of envs) {
38 |       // if DOTENV_KEY set then we are checking if a .env.vault file is already specified
39 |       if (DOTENV_KEY.length > 0 && env.type === TYPE_ENV_VAULT_FILE) {
40 |         fileAlreadySpecified = true
41 |       }
42 | 
43 |       // if DOTENV_KEY not set then we are checking if a .env file is already specified
44 |       if (DOTENV_KEY.length <= 0 && env.type === TYPE_ENV_FILE) {
45 |         fileAlreadySpecified = true
46 |       }
47 |     }
48 | 
49 |     // return early since envs array objects already contain 1 .env.vault or .env file
50 |     if (fileAlreadySpecified) {
51 |       return envs
52 |     }
53 | 
54 |     // no .env.vault or .env file specified as a flag so we assume either .env.vault (if dotenv key is set) or a .env file
55 |     if (DOTENV_KEY.length > 0) {
56 |       // if DOTENV_KEY is set then default to look for .env.vault file
57 |       return [...DEFAULT_ENV_VAULTS, ...envs]
58 |     } else {
59 |       // if no DOTENV_KEY then default to look for .env file
60 |       return [...DEFAULT_ENVS, ...envs]
61 |     }
62 |   }
63 | }
64 | 
65 | module.exports = determineEnvs
66 | 


--------------------------------------------------------------------------------
/src/lib/helpers/dotenvOptionPaths.js:
--------------------------------------------------------------------------------
 1 | const resolveHome = require('./resolveHome')
 2 | 
 3 | function dotenvOptionPaths (options) {
 4 |   let optionPaths = []
 5 | 
 6 |   if (options && options.path) {
 7 |     if (!Array.isArray(options.path)) {
 8 |       optionPaths = [resolveHome(options.path)]
 9 |     } else {
10 |       optionPaths = [] // reset default
11 | 
12 |       for (const filepath of options.path) {
13 |         optionPaths.push(resolveHome(filepath))
14 |       }
15 |     }
16 |   }
17 | 
18 |   return optionPaths
19 | }
20 | 
21 | module.exports = dotenvOptionPaths
22 | 


--------------------------------------------------------------------------------
/src/lib/helpers/dotenvParse.js:
--------------------------------------------------------------------------------
 1 | // historical dotenv.parse - https://github.com/motdotla/dotenv)
 2 | const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
 3 | 
 4 | function dotenvParse (src, skipExpandForDoubleQuotes = false, skipConvertingWindowsNewlines = false, collectAllValues = false) {
 5 |   const obj = {}
 6 | 
 7 |   // Convert buffer to string
 8 |   let lines = src.toString()
 9 | 
10 |   // Convert line breaks to same format
11 |   if (!skipConvertingWindowsNewlines) {
12 |     lines = lines.replace(/\r\n?/mg, '\n')
13 |   }
14 | 
15 |   let match
16 |   while ((match = LINE.exec(lines)) != null) {
17 |     const key = match[1]
18 | 
19 |     // Default undefined or null to empty string
20 |     let value = (match[2] || '')
21 | 
22 |     // Remove whitespace
23 |     value = value.trim()
24 | 
25 |     // Check if double quoted
26 |     const maybeQuote = value[0]
27 | 
28 |     // Remove surrounding quotes
29 |     value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
30 | 
31 |     // Expand newlines if double quoted
32 |     if (maybeQuote === '"' && !skipExpandForDoubleQuotes) {
33 |       value = value.replace(/\\n/g, '\n') // newline
34 |       value = value.replace(/\\r/g, '\r') // carriage return
35 |       value = value.replace(/\\t/g, '\t') // tabs
36 |     }
37 | 
38 |     if (collectAllValues) {
39 |       // handle scenario where user mistakenly includes plaintext duplicate in .env:
40 |       //
41 |       // # .env
42 |       // HELLO="World"
43 |       // HELLO="enrypted:1234"
44 |       obj[key] = obj[key] || []
45 |       obj[key].push(value)
46 |     } else {
47 |       // Add to object
48 |       obj[key] = value
49 |     }
50 |   }
51 | 
52 |   return obj
53 | }
54 | 
55 | module.exports = dotenvParse
56 | 


--------------------------------------------------------------------------------
/src/lib/helpers/dotenvPrivateKeyNames.js:
--------------------------------------------------------------------------------
1 | const PRIVATE_KEY_NAME_SCHEMA = 'DOTENV_PRIVATE_KEY'
2 | 
3 | function dotenvPrivateKeyNames (processEnv) {
4 |   return Object.keys(processEnv).filter(key => key.startsWith(PRIVATE_KEY_NAME_SCHEMA))
5 | }
6 | 
7 | module.exports = dotenvPrivateKeyNames
8 | 


--------------------------------------------------------------------------------
/src/lib/helpers/encrypt.js:
--------------------------------------------------------------------------------
 1 | const crypto = require('crypto')
 2 | 
 3 | const parseEncryptionKeyFromDotenvKey = require('./parseEncryptionKeyFromDotenvKey')
 4 | 
 5 | const NONCE_BYTES = 12
 6 | 
 7 | function encrypt (raw, dotenvKey) {
 8 |   const key = parseEncryptionKeyFromDotenvKey(dotenvKey)
 9 | 
10 |   // set up nonce
11 |   const nonce = crypto.randomBytes(NONCE_BYTES)
12 | 
13 |   // set up cipher
14 |   const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
15 | 
16 |   // generate ciphertext
17 |   let ciphertext = ''
18 |   ciphertext += cipher.update(raw, 'utf8', 'hex')
19 |   ciphertext += cipher.final('hex')
20 |   ciphertext += cipher.getAuthTag().toString('hex')
21 | 
22 |   // prepend nonce
23 |   ciphertext = nonce.toString('hex') + ciphertext
24 | 
25 |   // base64 encode output
26 |   return Buffer.from(ciphertext, 'hex').toString('base64')
27 | }
28 | 
29 | module.exports = encrypt
30 | 


--------------------------------------------------------------------------------
/src/lib/helpers/encryptValue.js:
--------------------------------------------------------------------------------
 1 | const { encrypt } = require('eciesjs')
 2 | 
 3 | const PREFIX = 'encrypted:'
 4 | 
 5 | function encryptValue (value, publicKey) {
 6 |   const ciphertext = encrypt(publicKey, Buffer.from(value))
 7 |   const encoded = Buffer.from(ciphertext, 'hex').toString('base64') // base64 encode ciphertext
 8 | 
 9 |   return `${PREFIX}${encoded}`
10 | }
11 | 
12 | module.exports = encryptValue
13 | 


--------------------------------------------------------------------------------
/src/lib/helpers/errors.js:
--------------------------------------------------------------------------------
  1 | const truncate = require('./truncate')
  2 | 
  3 | class Errors {
  4 |   constructor (options = {}) {
  5 |     this.filepath = options.filepath
  6 |     this.envFilepath = options.envFilepath
  7 | 
  8 |     this.key = options.key
  9 |     this.privateKey = options.privateKey
 10 |     this.privateKeyName = options.privateKeyName
 11 |     this.command = options.command
 12 | 
 13 |     this.message = options.message
 14 |   }
 15 | 
 16 |   missingEnvFile () {
 17 |     const code = 'MISSING_ENV_FILE'
 18 |     const message = `[${code}] missing ${this.envFilepath} file (${this.filepath})`
 19 |     const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/484`
 20 | 
 21 |     const e = new Error(message)
 22 |     e.code = code
 23 |     e.help = help
 24 |     return e
 25 |   }
 26 | 
 27 |   missingKey () {
 28 |     const code = 'MISSING_KEY'
 29 |     const message = `[${code}] missing ${this.key} key`
 30 | 
 31 |     const e = new Error(message)
 32 |     e.code = code
 33 |     return e
 34 |   }
 35 | 
 36 |   missingPrivateKey () {
 37 |     const code = 'MISSING_PRIVATE_KEY'
 38 |     const message = `[${code}] could not decrypt ${this.key} using private key '${this.privateKeyName}=${truncate(this.privateKey)}'`
 39 |     const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/464`
 40 | 
 41 |     const e = new Error(message)
 42 |     e.code = code
 43 |     e.help = help
 44 |     return e
 45 |   }
 46 | 
 47 |   invalidPrivateKey () {
 48 |     const code = 'INVALID_PRIVATE_KEY'
 49 |     const message = `[${code}] could not decrypt ${this.key} using private key '${this.privateKeyName}=${truncate(this.privateKey)}'`
 50 |     const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/465`
 51 | 
 52 |     const e = new Error(message)
 53 |     e.code = code
 54 |     e.help = help
 55 |     return e
 56 |   }
 57 | 
 58 |   looksWrongPrivateKey () {
 59 |     const code = 'WRONG_PRIVATE_KEY'
 60 |     const message = `[${code}] could not decrypt ${this.key} using private key '${this.privateKeyName}=${truncate(this.privateKey)}'`
 61 |     const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/466`
 62 | 
 63 |     const e = new Error(message)
 64 |     e.code = code
 65 |     e.help = help
 66 |     return e
 67 |   }
 68 | 
 69 |   malformedEncryptedData () {
 70 |     const code = 'MALFORMED_ENCRYPTED_DATA'
 71 |     const message = `[${code}] could not decrypt ${this.key} because encrypted data appears malformed`
 72 |     const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/467`
 73 | 
 74 |     const e = new Error(message)
 75 |     e.code = code
 76 |     e.help = help
 77 |     return e
 78 |   }
 79 | 
 80 |   decryptionFailed () {
 81 |     const code = 'DECRYPTION_FAILED'
 82 |     const message = this.message
 83 | 
 84 |     const e = new Error(message)
 85 |     e.code = code
 86 |     return e
 87 |   }
 88 | 
 89 |   commandSubstitutionFailed () {
 90 |     const code = 'COMMAND_SUBSTITUTION_FAILED'
 91 |     const message = `[${code}] could not eval ${this.key} containing command '${this.command}': ${this.message}`
 92 |     const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/532`
 93 | 
 94 |     const e = new Error(message)
 95 |     e.code = code
 96 |     e.help = help
 97 |     return e
 98 |   }
 99 | 
100 |   dangerousDependencyHoist () {
101 |     const code = 'DANGEROUS_DEPENDENCY_HOIST'
102 |     const message = `[${code}] your environment has hoisted an incompatible version of a dotenvx dependency: ${this.message}`
103 |     const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/622`
104 | 
105 |     const e = new Error(message)
106 |     e.code = code
107 |     e.help = help
108 |     return e
109 |   }
110 | }
111 | 
112 | module.exports = Errors
113 | 


--------------------------------------------------------------------------------
/src/lib/helpers/escape.js:
--------------------------------------------------------------------------------
1 | function escape (value) {
2 |   return JSON.stringify(value)
3 | }
4 | 
5 | module.exports = escape
6 | 


--------------------------------------------------------------------------------
/src/lib/helpers/escapeDollarSigns.js:
--------------------------------------------------------------------------------
1 | function escapeDollarSigns (str) {
2 |   return str.replace(/\$/g, '$$')
3 | }
4 | 
5 | module.exports = escapeDollarSigns
6 | 


--------------------------------------------------------------------------------
/src/lib/helpers/escapeForRegex.js:
--------------------------------------------------------------------------------
1 | function escapeForRegex (str) {
2 |   return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\
amp;').replace(/-/g, '\\x2d')
3 | }
4 | 
5 | module.exports = escapeForRegex
6 | 


--------------------------------------------------------------------------------
/src/lib/helpers/evalKeyValue.js:
--------------------------------------------------------------------------------
 1 | const { execSync } = require('child_process')
 2 | const chomp = require('./chomp')
 3 | const Errors = require('./errors')
 4 | 
 5 | function evalKeyValue (key, value, processEnv, runningParsed) {
 6 |   // Match everything between the outermost $() using a regex with non-capturing groups
 7 |   const matches = value.match(/\$\(([^)]+(?:\)[^(]*)*)\)/g) || []
 8 |   return matches.reduce((newValue, match) => {
 9 |     const command = match.slice(2, -1) // Extract command by removing $() wrapper
10 |     let result
11 | 
12 |     try {
13 |       result = execSync(command, { env: { ...processEnv, ...runningParsed } }).toString() // execute command (including runningParsed)
14 |     } catch (e) {
15 |       throw new Errors({ key, command, message: e.message.trim() }).commandSubstitutionFailed()
16 |     }
17 | 
18 |     result = chomp(result) // chomp it
19 |     return newValue.replace(match, result) // Replace match with result
20 |   }, value)
21 | }
22 | 
23 | module.exports = evalKeyValue
24 | 


--------------------------------------------------------------------------------
/src/lib/helpers/execute.js:
--------------------------------------------------------------------------------
 1 | const execa = require('execa')
 2 | /* c8 ignore start */
 3 | const pkgArgs = process.pkg ? { PKG_EXECPATH: '' } : {}
 4 | /* c8 ignore stop */
 5 | 
 6 | const execute = {
 7 |   execa (command, args, options) {
 8 |     return execa(command, args, { ...options, env: { ...options.env, ...pkgArgs } })
 9 |   }
10 | }
11 | 
12 | module.exports = execute
13 | 


--------------------------------------------------------------------------------
/src/lib/helpers/executeCommand.js:
--------------------------------------------------------------------------------
  1 | const path = require('path')
  2 | const which = require('which')
  3 | const execute = require('./../../lib/helpers/execute')
  4 | const { logger } = require('./../../shared/logger')
  5 | 
  6 | async function executeCommand (commandArgs, env) {
  7 |   const signals = [
  8 |     'SIGHUP', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT',
  9 |     'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2'
 10 |   ]
 11 | 
 12 |   logger.debug(`executing process command [${commandArgs.join(' ')}]`)
 13 | 
 14 |   let child
 15 |   let signalSent
 16 | 
 17 |   /* c8 ignore start */
 18 |   const sigintHandler = () => {
 19 |     logger.debug('received SIGINT')
 20 |     logger.debug('checking command process')
 21 |     logger.debug(child)
 22 | 
 23 |     if (child) {
 24 |       logger.debug('sending SIGINT to command process')
 25 |       signalSent = 'SIGINT'
 26 |       child.kill('SIGINT') // Send SIGINT to the command process
 27 |     } else {
 28 |       logger.debug('no command process to send SIGINT to')
 29 |     }
 30 |   }
 31 | 
 32 |   const sigtermHandler = () => {
 33 |     logger.debug('received SIGTERM')
 34 |     logger.debug('checking command process')
 35 |     logger.debug(child)
 36 | 
 37 |     if (child) {
 38 |       logger.debug('sending SIGTERM to command process')
 39 |       signalSent = 'SIGTERM'
 40 |       child.kill('SIGTERM') // Send SIGTERM to the command process
 41 |     } else {
 42 |       logger.debug('no command process to send SIGTERM to')
 43 |     }
 44 |   }
 45 | 
 46 |   const handleOtherSignal = (signal) => {
 47 |     logger.debug(`received ${signal}`)
 48 |     child.kill(signal)
 49 |   }
 50 |   /* c8 ignore stop */
 51 | 
 52 |   try {
 53 |     // ensure the first command is expanded
 54 |     try {
 55 |       commandArgs[0] = path.resolve(which.sync(`${commandArgs[0]}`))
 56 |       logger.debug(`expanding process command to [${commandArgs.join(' ')}]`)
 57 |     } catch (e) {
 58 |       logger.debug(`could not expand process command. using [${commandArgs.join(' ')}]`)
 59 |     }
 60 | 
 61 |     // expand any other commands that follow a --
 62 |     let expandNext = false
 63 |     for (let i = 0; i < commandArgs.length; i++) {
 64 |       if (commandArgs[i] === '--') {
 65 |         expandNext = true
 66 |       } else if (expandNext) {
 67 |         try {
 68 |           commandArgs[i] = path.resolve(which.sync(`${commandArgs[i]}`))
 69 |           logger.debug(`expanding process command to [${commandArgs.join(' ')}]`)
 70 |         } catch (e) {
 71 |           logger.debug(`could not expand process command. using [${commandArgs.join(' ')}]`)
 72 |         }
 73 |         expandNext = false
 74 |       }
 75 |     }
 76 | 
 77 |     child = execute.execa(commandArgs[0], commandArgs.slice(1), {
 78 |       stdio: 'inherit',
 79 |       env: { ...process.env, ...env }
 80 |     })
 81 | 
 82 |     process.on('SIGINT', sigintHandler)
 83 |     process.on('SIGTERM', sigtermHandler)
 84 | 
 85 |     signals.forEach(signal => {
 86 |       process.on(signal, () => handleOtherSignal(signal))
 87 |     })
 88 | 
 89 |     // Wait for the command process to finish
 90 |     const { exitCode } = await child
 91 | 
 92 |     if (exitCode !== 0) {
 93 |       logger.debug(`received exitCode ${exitCode}`)
 94 |       throw new Error(`Command exited with exit code ${exitCode}`)
 95 |     }
 96 |   } catch (error) {
 97 |     // no color on these errors as they can be standard errors for things like jest exiting with exitCode 1 for a single failed test.
 98 |     if (!['SIGINT', 'SIGTERM'].includes(signalSent || error.signal)) {
 99 |       if (error.code === 'ENOENT') {
100 |         logger.error(`Unknown command: ${error.command}`)
101 |       } else {
102 |         logger.error(error.message)
103 |       }
104 |     }
105 | 
106 |     // Exit with the error code from the command process, or 1 if unavailable
107 |     process.exit(error.exitCode || 1)
108 |   } finally {
109 |     // Clean up: Remove the SIGINT handler
110 |     process.removeListener('SIGINT', sigintHandler)
111 |     // Clean up: Remove the SIGTERM handler
112 |     process.removeListener('SIGTERM', sigtermHandler)
113 |   }
114 | }
115 | 
116 | module.exports = executeCommand
117 | 


--------------------------------------------------------------------------------
/src/lib/helpers/executeDynamic.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | const childProcess = require('child_process')
 3 | const { logger } = require('../../shared/logger')
 4 | 
 5 | function executeDynamic (program, command, rawArgs) {
 6 |   if (!command) {
 7 |     program.outputHelp()
 8 |     process.exit(1)
 9 |     return
10 |   }
11 | 
12 |   // construct the full command line manually including flags
13 |   const commandIndex = rawArgs.indexOf(command)
14 |   const forwardedArgs = rawArgs.slice(commandIndex + 1)
15 | 
16 |   logger.debug(`command: ${command}`)
17 |   logger.debug(`args: ${JSON.stringify(forwardedArgs)}`)
18 | 
19 |   const binPath = path.join(process.cwd(), 'node_modules', '.bin')
20 |   const newPath = `${binPath}:${process.env.PATH}`
21 |   const env = { ...process.env, PATH: newPath }
22 | 
23 |   const result = childProcess.spawnSync(`dotenvx-${command}`, forwardedArgs, { stdio: 'inherit', env })
24 |   if (result.error) {
25 |     if (command === 'pro') {
26 |       const pro = `_______________________________________________________________
27 | |                                                             |
28 | |  For small and medium businesses                            |
29 | |                                                             |
30 | |      | |     | |                                            |
31 | |    __| | ___ | |_ ___ _ ____   ____  __  _ __  _ __ ___     |
32 | |   / _\` |/ _ \\| __/ _ \\ '_ \\ \\ / /\\ \\/ / | '_ \\| '__/ _ \\    |
33 | |  | (_| | (_) | ||  __/ | | \\ V /  >  <  | |_) | | | (_) |   |
34 | |   \\__,_|\\___/ \\__\\___|_| |_|\\_/  /_/\\_\\ | .__/|_|  \\___/    |
35 | |                                         | |                 |
36 | |                                         |_|                 |
37 | | ## learn more on dotenvx 🟨                                 |
38 | |                                                             |
39 | | >> https://dotenvx.com/pricing                              |
40 | |                                                             |
41 | | ## subscribe on github to be notified 📣                    |
42 | |                                                             |
43 | | >> https://github.com/dotenvx/dotenvx/issues/259            |
44 | |                                                             |
45 | | ----------------------------------------------------------- |
46 | | - thank you for using dotenvx! - @motdotla                  |
47 | |_____________________________________________________________|`
48 | 
49 |       console.log(pro)
50 |       console.log('')
51 |       logger.warn(`[INSTALLATION_NEEDED] install dotenvx-${command} to use [dotenvx ${command}] commands 🏆`)
52 |       logger.help('? see installation instructions [https://github.com/dotenvx/dotenvx-pro]')
53 |     } else {
54 |       logger.info(`error: unknown command '${command}'`)
55 |     }
56 |   }
57 | 
58 |   if (result.status !== 0) {
59 |     process.exit(result.status)
60 |   }
61 | }
62 | 
63 | module.exports = executeDynamic
64 | 


--------------------------------------------------------------------------------
/src/lib/helpers/executeExtension.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | const childProcess = require('child_process')
 3 | const { logger } = require('../../shared/logger')
 4 | 
 5 | function executeExtension (ext, command, rawArgs) {
 6 |   if (!command) {
 7 |     ext.outputHelp()
 8 |     process.exit(0)
 9 |     return
10 |   }
11 | 
12 |   // construct the full command line manually including flags
13 |   const commandIndex = rawArgs.indexOf(command)
14 |   const forwardedArgs = rawArgs.slice(commandIndex + 1)
15 | 
16 |   logger.debug(`command: ${command}`)
17 |   logger.debug(`args: ${JSON.stringify(forwardedArgs)}`)
18 | 
19 |   const binPath = path.join(process.cwd(), 'node_modules', '.bin')
20 |   const newPath = `${binPath}:${process.env.PATH}`
21 |   const env = { ...process.env, PATH: newPath }
22 | 
23 |   const result = childProcess.spawnSync(`dotenvx-ext-${command}`, forwardedArgs, { stdio: 'inherit', env })
24 |   if (result.error) {
25 |     // list known extension here for convenience to the user
26 |     if (['vault', 'hub'].includes(command)) {
27 |       logger.warn(`[INSTALLATION_NEEDED] install dotenvx-ext-${command} to use [dotenvx ext ${command}] commands`)
28 |       logger.help('? see installation instructions [https://github.com/dotenvx/dotenvx-ext-vault]')
29 |     } else {
30 |       logger.info(`error: unknown command '${command}'`)
31 |     }
32 |   }
33 | 
34 |   if (result.status !== 0) {
35 |     process.exit(result.status)
36 |   }
37 | }
38 | 
39 | module.exports = executeExtension
40 | 


--------------------------------------------------------------------------------
/src/lib/helpers/findEnvFiles.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./fsx')
 2 | 
 3 | const RESERVED_ENV_FILES = ['.env.vault', '.env.project', '.env.keys', '.env.me', '.env.x', '.env.example']
 4 | 
 5 | function findEnvFiles (directory) {
 6 |   try {
 7 |     const files = fsx.readdirSync(directory)
 8 |     const envFiles = files.filter(file =>
 9 |       file.startsWith('.env') &&
10 |       !file.endsWith('.previous') &&
11 |       !RESERVED_ENV_FILES.includes(file)
12 |     )
13 | 
14 |     return envFiles
15 |   } catch (e) {
16 |     if (e.code === 'ENOENT') {
17 |       const error = new Error(`missing directory (${directory})`)
18 |       error.code = 'MISSING_DIRECTORY'
19 | 
20 |       throw error
21 |     } else {
22 |       throw e
23 |     }
24 |   }
25 | }
26 | 
27 | module.exports = findEnvFiles
28 | 


--------------------------------------------------------------------------------
/src/lib/helpers/findPrivateKey.js:
--------------------------------------------------------------------------------
 1 | // helpers
 2 | const guessPrivateKeyName = require('./guessPrivateKeyName')
 3 | const ProKeypair = require('./proKeypair')
 4 | 
 5 | // services
 6 | const Keypair = require('./../services/keypair')
 7 | 
 8 | function findPrivateKey (envFilepath, envKeysFilepath = null) {
 9 |   // use path/to/.env.${environment} to generate privateKeyName
10 |   const privateKeyName = guessPrivateKeyName(envFilepath)
11 | 
12 |   const proKeypairs = new ProKeypair(envFilepath).run() // TODO: implement custom envKeysFilepath
13 |   const keypairs = new Keypair(envFilepath, envKeysFilepath).run()
14 | 
15 |   return proKeypairs[privateKeyName] || keypairs[privateKeyName]
16 | }
17 | 
18 | module.exports = { findPrivateKey }
19 | 


--------------------------------------------------------------------------------
/src/lib/helpers/findPublicKey.js:
--------------------------------------------------------------------------------
 1 | // helpers
 2 | const guessPublicKeyName = require('./guessPublicKeyName')
 3 | const ProKeypair = require('./proKeypair')
 4 | 
 5 | // services
 6 | const Keypair = require('./../services/keypair')
 7 | 
 8 | function findPublicKey (envFilepath) {
 9 |   const publicKeyName = guessPublicKeyName(envFilepath)
10 | 
11 |   const proKeypairs = new ProKeypair(envFilepath).run()
12 |   const keypairs = new Keypair(envFilepath).run()
13 | 
14 |   return proKeypairs[publicKeyName] || keypairs[publicKeyName]
15 | }
16 | 
17 | module.exports = findPublicKey
18 | 


--------------------------------------------------------------------------------
/src/lib/helpers/fsx.js:
--------------------------------------------------------------------------------
 1 | const fs = require('fs')
 2 | 
 3 | const ENCODING = 'utf8'
 4 | 
 5 | function readFileX (filepath, encoding = null) {
 6 |   if (!encoding) {
 7 |     encoding = ENCODING
 8 |   }
 9 | 
10 |   return fs.readFileSync(filepath, encoding) // utf8 default so it returns a string
11 | }
12 | 
13 | function writeFileX (filepath, str) {
14 |   return fs.writeFileSync(filepath, str, ENCODING) // utf8 always
15 | }
16 | 
17 | const fsx = {
18 |   chmodSync: fs.chmodSync,
19 |   existsSync: fs.existsSync,
20 |   readdirSync: fs.readdirSync,
21 |   readFileSync: fs.readFileSync,
22 |   writeFileSync: fs.writeFileSync,
23 |   appendFileSync: fs.appendFileSync,
24 | 
25 |   // fsx special commands
26 |   readFileX,
27 |   writeFileX
28 | }
29 | 
30 | module.exports = fsx
31 | 


--------------------------------------------------------------------------------
/src/lib/helpers/getCommanderVersion.js:
--------------------------------------------------------------------------------
 1 | const fs = require('fs')
 2 | const path = require('path')
 3 | 
 4 | function getCommanderVersion () {
 5 |   const commanderMain = require.resolve('commander')
 6 |   const pkgPath = path.join(commanderMain, '..', 'package.json')
 7 |   return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version
 8 | }
 9 | 
10 | module.exports = getCommanderVersion
11 | 


--------------------------------------------------------------------------------
/src/lib/helpers/guessEnvironment.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | 
 3 | function guessEnvironment (filepath) {
 4 |   const filename = path.basename(filepath)
 5 | 
 6 |   const parts = filename.split('.')
 7 |   const possibleEnvironmentList = [...parts.slice(2)]
 8 | 
 9 |   if (possibleEnvironmentList.length === 0) {
10 |     // handle .env1 -> development1
11 |     const environment = filename.replace('.env', 'development')
12 | 
13 |     return environment
14 |   }
15 | 
16 |   if (possibleEnvironmentList.length === 1) {
17 |     return possibleEnvironmentList[0]
18 |   }
19 | 
20 |   if (
21 |     possibleEnvironmentList.length === 2
22 |   ) {
23 |     return possibleEnvironmentList.join('_')
24 |   }
25 | 
26 |   return possibleEnvironmentList.slice(0, 2).join('_')
27 | }
28 | 
29 | module.exports = guessEnvironment
30 | 


--------------------------------------------------------------------------------
/src/lib/helpers/guessPrivateKeyFilename.js:
--------------------------------------------------------------------------------
 1 | const PREFIX = 'DOTENV_PRIVATE_KEY'
 2 | 
 3 | function guessPrivateKeyFilename (privateKeyName) {
 4 |   // .env
 5 |   if (privateKeyName === PREFIX) {
 6 |     return '.env'
 7 |   }
 8 | 
 9 |   const filenameSuffix = privateKeyName.substring(`${PREFIX}_`.length).split('_').join('.').toLowerCase()
10 |   // .env.ENVIRONMENT
11 | 
12 |   return `.env.${filenameSuffix}`
13 | }
14 | 
15 | module.exports = guessPrivateKeyFilename
16 | 


--------------------------------------------------------------------------------
/src/lib/helpers/guessPrivateKeyName.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | const guessEnvironment = require('./guessEnvironment')
 3 | 
 4 | function guessPrivateKeyName (filepath) {
 5 |   const filename = path.basename(filepath).toLowerCase()
 6 | 
 7 |   // .env
 8 |   if (filename === '.env') {
 9 |     return 'DOTENV_PRIVATE_KEY'
10 |   }
11 | 
12 |   // .env.ENVIRONMENT
13 |   const environment = guessEnvironment(filename)
14 | 
15 |   return `DOTENV_PRIVATE_KEY_${environment.toUpperCase()}`
16 | }
17 | 
18 | module.exports = guessPrivateKeyName
19 | 


--------------------------------------------------------------------------------
/src/lib/helpers/guessPublicKeyName.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | const guessEnvironment = require('./guessEnvironment')
 3 | 
 4 | function guessPublicKeyName (filepath) {
 5 |   const filename = path.basename(filepath).toLowerCase()
 6 | 
 7 |   // .env
 8 |   if (filename === '.env') {
 9 |     return 'DOTENV_PUBLIC_KEY'
10 |   }
11 | 
12 |   // .env.ENVIRONMENT
13 |   const environment = guessEnvironment(filename)
14 | 
15 |   return `DOTENV_PUBLIC_KEY_${environment.toUpperCase()}`
16 | }
17 | 
18 | module.exports = guessPublicKeyName
19 | 


--------------------------------------------------------------------------------
/src/lib/helpers/installPrecommitHook.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./fsx')
 2 | const path = require('path')
 3 | 
 4 | const HOOK_SCRIPT = `#!/bin/sh
 5 | 
 6 | if command -v dotenvx 2>&1 >/dev/null
 7 | then
 8 |   dotenvx ext precommit
 9 | elif npx dotenvx -V >/dev/null 2>&1
10 | then
11 |   npx dotenvx ext precommit
12 | else
13 |   echo "[dotenvx][precommit] 'dotenvx' command not found"
14 |   echo "[dotenvx][precommit] ? install it with [curl -fsS https://dotenvx.sh | sh]"
15 |   echo "[dotenvx][precommit] ? other install options [https://dotenvx.com/docs/install]"
16 |   exit 1
17 | fi
18 | `
19 | 
20 | class InstallPrecommitHook {
21 |   constructor () {
22 |     this.hookPath = path.join('.git', 'hooks', 'pre-commit')
23 |   }
24 | 
25 |   run () {
26 |     let successMessage
27 | 
28 |     try {
29 |       // Check if the pre-commit file already exists
30 |       if (this._exists()) {
31 |         // Check if 'dotenvx precommit' already exists in the file
32 |         if (this._currentHook().includes('dotenvx ext precommit')) {
33 |           // do nothing
34 |           successMessage = `dotenvx ext precommit exists [${this.hookPath}]`
35 |         } else {
36 |           this._appendHook()
37 |           successMessage = `dotenvx ext precommit appended [${this.hookPath}]`
38 |         }
39 |       } else {
40 |         this._createHook()
41 |         successMessage = `dotenvx ext precommit installed [${this.hookPath}]`
42 |       }
43 | 
44 |       return {
45 |         successMessage
46 |       }
47 |     } catch (err) {
48 |       const error = new Error(`failed to modify pre-commit hook: ${err.message}`)
49 |       throw error
50 |     }
51 |   }
52 | 
53 |   _exists () {
54 |     return fsx.existsSync(this.hookPath)
55 |   }
56 | 
57 |   _currentHook () {
58 |     return fsx.readFileX(this.hookPath)
59 |   }
60 | 
61 |   _createHook () {
62 |     // If the pre-commit file doesn't exist, create a new one with the hookScript
63 |     fsx.writeFileX(this.hookPath, HOOK_SCRIPT)
64 |     fsx.chmodSync(this.hookPath, '755') // Make the file executable
65 |   }
66 | 
67 |   _appendHook () {
68 |     // Append 'dotenvx precommit' to the existing file
69 |     fsx.appendFileSync(this.hookPath, '\n' + HOOK_SCRIPT)
70 |   }
71 | }
72 | 
73 | module.exports = InstallPrecommitHook
74 | 


--------------------------------------------------------------------------------
/src/lib/helpers/isEncrypted.js:
--------------------------------------------------------------------------------
1 | const ENCRYPTION_PATTERN = /^encrypted:.+/
2 | 
3 | function isEncrypted (value) {
4 |   return ENCRYPTION_PATTERN.test(value)
5 | }
6 | 
7 | module.exports = isEncrypted
8 | 


--------------------------------------------------------------------------------
/src/lib/helpers/isFullyEncrypted.js:
--------------------------------------------------------------------------------
 1 | const dotenvParse = require('./dotenvParse')
 2 | const isEncrypted = require('./isEncrypted')
 3 | const isPublicKey = require('./isPublicKey')
 4 | 
 5 | function isFullyEncrypted (src) {
 6 |   const parsed = dotenvParse(src, false, false, true) // collect all values
 7 | 
 8 |   for (const [key, values] of Object.entries(parsed)) {
 9 |     // handle scenario where user mistakenly includes plaintext duplicate in .env:
10 |     //
11 |     // # .env
12 |     // HELLO="World"
13 |     // HELLO="enrypted:1234"
14 |     //
15 |     // key => [value1, ...]
16 |     for (const value of values) {
17 |       const result = isEncrypted(value) || isPublicKey(key, value)
18 |       if (!result) {
19 |         return false
20 |       }
21 |     }
22 |   }
23 | 
24 |   return true
25 | }
26 | 
27 | module.exports = isFullyEncrypted
28 | 


--------------------------------------------------------------------------------
/src/lib/helpers/isIgnoringDotenvKeys.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./fsx')
 2 | const ignore = require('ignore')
 3 | 
 4 | function isIgnoringDotenvKeys () {
 5 |   if (!fsx.existsSync('.gitignore')) {
 6 |     return false
 7 |   }
 8 | 
 9 |   const gitignore = fsx.readFileX('.gitignore')
10 |   const ig = ignore(gitignore).add(gitignore)
11 | 
12 |   if (!ig.ignores('.env.keys')) {
13 |     return false
14 |   }
15 | 
16 |   return true
17 | }
18 | 
19 | module.exports = isIgnoringDotenvKeys
20 | 


--------------------------------------------------------------------------------
/src/lib/helpers/isPublicKey.js:
--------------------------------------------------------------------------------
1 | const PUBLIC_KEY_PATTERN = /^DOTENV_PUBLIC_KEY/
2 | 
3 | function isPublicKey (key, value) {
4 |   return PUBLIC_KEY_PATTERN.test(key)
5 | }
6 | 
7 | module.exports = isPublicKey
8 | 


--------------------------------------------------------------------------------
/src/lib/helpers/keypair.js:
--------------------------------------------------------------------------------
 1 | const { PrivateKey } = require('eciesjs')
 2 | 
 3 | function keypair (existingPrivateKey) {
 4 |   let kp
 5 | 
 6 |   if (existingPrivateKey) {
 7 |     kp = new PrivateKey(Buffer.from(existingPrivateKey, 'hex'))
 8 |   } else {
 9 |     kp = new PrivateKey()
10 |   }
11 | 
12 |   const publicKey = kp.publicKey.toHex()
13 |   const privateKey = kp.secret.toString('hex')
14 | 
15 |   return {
16 |     publicKey,
17 |     privateKey
18 |   }
19 | }
20 | 
21 | module.exports = keypair
22 | 


--------------------------------------------------------------------------------
/src/lib/helpers/packageJson.js:
--------------------------------------------------------------------------------
1 | const { name, version, description } = require('../../../package.json')
2 | 
3 | module.exports = { name, version, description }
4 | 


--------------------------------------------------------------------------------
/src/lib/helpers/parseEncryptionKeyFromDotenvKey.js:
--------------------------------------------------------------------------------
 1 | function parseEncryptionKeyFromDotenvKey (dotenvKey) {
 2 |   // Parse DOTENV_KEY. Format is a URI
 3 |   let uri
 4 |   try {
 5 |     uri = new URL(dotenvKey)
 6 |   } catch (e) {
 7 |     throw new Error('INVALID_DOTENV_KEY: Incomplete format. It should be a dotenv uri. (dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development)')
 8 |   }
 9 | 
10 |   // Get decrypt key
11 |   const key = uri.password
12 |   if (!key) {
13 |     throw new Error('INVALID_DOTENV_KEY: Missing key part')
14 |   }
15 | 
16 |   return Buffer.from(key.slice(-64), 'hex')
17 | }
18 | 
19 | module.exports = parseEncryptionKeyFromDotenvKey
20 | 


--------------------------------------------------------------------------------
/src/lib/helpers/parseEnvironmentFromDotenvKey.js:
--------------------------------------------------------------------------------
 1 | function parseEnvironmentFromDotenvKey (dotenvKey) {
 2 |   // Parse DOTENV_KEY. Format is a URI
 3 |   let uri
 4 |   try {
 5 |     uri = new URL(dotenvKey)
 6 |   } catch (e) {
 7 |     throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
 8 |   }
 9 | 
10 |   // Get environment
11 |   const environment = uri.searchParams.get('environment')
12 |   if (!environment) {
13 |     throw new Error('INVALID_DOTENV_KEY: Missing environment part')
14 |   }
15 | 
16 |   return environment
17 | }
18 | 
19 | module.exports = parseEnvironmentFromDotenvKey
20 | 


--------------------------------------------------------------------------------
/src/lib/helpers/pluralize.js:
--------------------------------------------------------------------------------
 1 | function pluralize (word, count) {
 2 |   // simple pluralization: add 's' at the end
 3 |   if (count === 0 || count > 1) {
 4 |     return word + 's'
 5 |   } else {
 6 |     return word
 7 |   }
 8 | }
 9 | 
10 | module.exports = pluralize
11 | 


--------------------------------------------------------------------------------
/src/lib/helpers/proKeypair.js:
--------------------------------------------------------------------------------
 1 | const path = require('path')
 2 | const childProcess = require('child_process')
 3 | 
 4 | const guessPrivateKeyName = require('./guessPrivateKeyName')
 5 | const guessPublicKeyName = require('./guessPublicKeyName')
 6 | 
 7 | class ProKeypair {
 8 |   constructor (envFilepath) {
 9 |     this.envFilepath = envFilepath
10 |   }
11 | 
12 |   run () {
13 |     let result = {}
14 | 
15 |     try {
16 |       // if installed as sibling module
17 |       const projectRoot = path.resolve(process.cwd())
18 |       const dotenvxProPath = require.resolve('@dotenvx/dotenvx-pro', { paths: [projectRoot] })
19 |       const { keypair } = require(dotenvxProPath)
20 | 
21 |       result = keypair(this.envFilepath)
22 |     } catch (_e) {
23 |       try {
24 |         // if installed as binary cli
25 |         const output = childProcess.execSync(`dotenvx-pro keypair -f ${this.envFilepath}`, { stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim()
26 | 
27 |         result = JSON.parse(output)
28 |       } catch (_e) {
29 |         const privateKeyName = guessPrivateKeyName(this.envFilepath)
30 |         const publicKeyName = guessPublicKeyName(this.envFilepath)
31 | 
32 |         // match format of dotenvx-pro
33 |         result[privateKeyName] = null
34 |         result[publicKeyName] = null
35 |       }
36 |     }
37 | 
38 |     return result
39 |   }
40 | }
41 | 
42 | module.exports = ProKeypair
43 | 


--------------------------------------------------------------------------------
/src/lib/helpers/quotes.js:
--------------------------------------------------------------------------------
 1 | const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
 2 | 
 3 | function quotes (src) {
 4 |   const obj = {}
 5 |   // Convert buffer to string
 6 |   let lines = src.toString()
 7 | 
 8 |   // Convert line breaks to same format
 9 |   lines = lines.replace(/\r\n?/mg, '\n')
10 | 
11 |   let match
12 |   while ((match = LINE.exec(lines)) != null) {
13 |     const key = match[1]
14 | 
15 |     // Default undefined or null to empty string
16 |     let value = (match[2] || '')
17 | 
18 |     // Remove whitespace
19 |     value = value.trim()
20 | 
21 |     // Check if double quoted
22 |     const maybeQuote = value[0]
23 | 
24 |     value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
25 | 
26 |     if (maybeQuote === value[0]) {
27 |       obj[key] = ''
28 |     } else {
29 |       obj[key] = maybeQuote
30 |     }
31 |   }
32 | 
33 |   return obj
34 | }
35 | 
36 | module.exports = quotes
37 | 


--------------------------------------------------------------------------------
/src/lib/helpers/removeDynamicHelpSection.js:
--------------------------------------------------------------------------------
 1 | // Remove Arguments section from help text. example:
 2 | // Arguments:
 3 | // command     dynamic command
 4 | // args        dynamic command arguments
 5 | 
 6 | function removeDynamicHelpSection (lines) {
 7 |   let argumentsHelpIndex
 8 |   for (let i = 0; i < lines.length; i++) {
 9 |     if (lines[i] === 'Arguments:') {
10 |       argumentsHelpIndex = i
11 |       break
12 |     }
13 |   }
14 |   if (argumentsHelpIndex) {
15 |     lines.splice(argumentsHelpIndex, 4) // remove Arguments and the following 3 lines
16 |   }
17 | 
18 |   return lines
19 | }
20 | 
21 | module.exports = removeDynamicHelpSection
22 | 


--------------------------------------------------------------------------------
/src/lib/helpers/removeOptionsHelpParts.js:
--------------------------------------------------------------------------------
 1 | // Remove [options] from help text. example:
 2 | 
 3 | function removeOptionsHelpParts (lines) {
 4 |   for (let i = 0; i < lines.length; i++) {
 5 |     lines[i] = lines[i].replace(' [options]', '')
 6 |   }
 7 | 
 8 |   return lines
 9 | }
10 | 
11 | module.exports = removeOptionsHelpParts
12 | 


--------------------------------------------------------------------------------
/src/lib/helpers/replace.js:
--------------------------------------------------------------------------------
 1 | const quotes = require('./quotes')
 2 | const dotenvParse = require('./dotenvParse')
 3 | const escapeForRegex = require('./escapeForRegex')
 4 | const escapeDollarSigns = require('./escapeDollarSigns')
 5 | 
 6 | function replace (src, key, replaceValue) {
 7 |   let output
 8 |   let newPart = ''
 9 | 
10 |   const parsed = dotenvParse(src, true, true) // skip expanding \n and skip converting \r\n
11 |   const _quotes = quotes(src)
12 |   if (Object.prototype.hasOwnProperty.call(parsed, key)) {
13 |     const quote = _quotes[key]
14 |     newPart += `${key}=${quote}${replaceValue}${quote}`
15 | 
16 |     const originalValue = parsed[key]
17 |     const escapedOriginalValue = escapeForRegex(originalValue)
18 | 
19 |     // conditionally enforce end of line
20 |     let enforceEndOfLine = ''
21 |     if (escapedOriginalValue === '') {
22 |       enforceEndOfLine = '
#39; // EMPTY scenario
23 | 
24 |       // if empty quote and consecutive newlines
25 |       const newlineMatch = src.match(new RegExp(`${key}\\s*=\\s*\n\n`, 'm')) // match any consecutive newline scenario for a blank value
26 |       if (quote === '' && newlineMatch) {
27 |         const newlineCount = (newlineMatch[0].match(/\n/g)).length - 1
28 |         for (let i = 0; i < newlineCount; i++) {
29 |           newPart += '\n' // re-append the extra newline to preserve user's format choice
30 |         }
31 |       }
32 |     }
33 | 
34 |     const currentPart = new RegExp(
35 |       '^' + // start of line
36 |       '(\\s*)?' + // spaces
37 |       '(export\\s+)?' + // export
38 |       key + // KEY
39 |       '\\s*=\\s*' + // spaces (KEY = value)
40 |       '["\'`]?' + // open quote
41 |       escapedOriginalValue + // escaped value
42 |       '["\'`]?' + // close quote
43 |       enforceEndOfLine
44 |       ,
45 |       'gm' // (g)lobal (m)ultiline
46 |     )
47 | 
48 |     const saferInput = escapeDollarSigns(newPart) // cleanse user inputted capture groups ($1, $2 etc)
49 | 
50 |     // $1 preserves spaces
51 |     // $2 preserves export
52 |     output = src.replace(currentPart, `$1$2${saferInput}`)
53 |   } else {
54 |     newPart += `${key}="${replaceValue}"`
55 | 
56 |     // append
57 |     if (src.endsWith('\n')) {
58 |       newPart = newPart + '\n'
59 |     } else {
60 |       newPart = '\n' + newPart
61 |     }
62 | 
63 |     output = src + newPart
64 |   }
65 | 
66 |   return output
67 | }
68 | 
69 | module.exports = replace
70 | 


--------------------------------------------------------------------------------
/src/lib/helpers/resolveEscapeSequences.js:
--------------------------------------------------------------------------------
1 | function resolveEscapeSequences (value) {
2 |   return value.replace(/\\\$/g, '
#39;)
3 | }
4 | 
5 | module.exports = resolveEscapeSequences
6 | 


--------------------------------------------------------------------------------
/src/lib/helpers/resolveHome.js:
--------------------------------------------------------------------------------
 1 | const os = require('os')
 2 | const path = require('path')
 3 | 
 4 | function resolveHome (filepath) {
 5 |   if (filepath[0] === '~') {
 6 |     return path.join(os.homedir(), filepath.slice(1))
 7 |   }
 8 | 
 9 |   return filepath
10 | }
11 | 
12 | module.exports = resolveHome
13 | 


--------------------------------------------------------------------------------
/src/lib/helpers/sleep.js:
--------------------------------------------------------------------------------
1 | function sleep (ms) {
2 |   return new Promise(resolve => setTimeout(resolve, ms))
3 | }
4 | 
5 | module.exports = sleep
6 | 


--------------------------------------------------------------------------------
/src/lib/helpers/smartDotenvPrivateKey.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./fsx')
 2 | const path = require('path')
 3 | 
 4 | const PUBLIC_KEY_SCHEMA = 'DOTENV_PUBLIC_KEY'
 5 | const PRIVATE_KEY_SCHEMA = 'DOTENV_PRIVATE_KEY'
 6 | 
 7 | const dotenvParse = require('./dotenvParse')
 8 | const guessPrivateKeyName = require('./guessPrivateKeyName')
 9 | 
10 | function searchProcessEnv (privateKeyName) {
11 |   if (process.env[privateKeyName] && process.env[privateKeyName].length > 0) {
12 |     return process.env[privateKeyName]
13 |   }
14 | }
15 | 
16 | function searchKeysFile (privateKeyName, envFilepath, envKeysFilepath = null) {
17 |   let keysFilepath = path.resolve(path.dirname(envFilepath), '.env.keys') // typical scenario
18 |   if (envKeysFilepath) { // user specified -fk flag
19 |     keysFilepath = path.resolve(envKeysFilepath)
20 |   }
21 | 
22 |   if (fsx.existsSync(keysFilepath)) {
23 |     const keysSrc = fsx.readFileX(keysFilepath)
24 |     const keysParsed = dotenvParse(keysSrc)
25 | 
26 |     if (keysParsed[privateKeyName] && keysParsed[privateKeyName].length > 0) {
27 |       return keysParsed[privateKeyName]
28 |     }
29 |   }
30 | }
31 | 
32 | function invertForPrivateKeyName (envFilepath) {
33 |   if (!fsx.existsSync(envFilepath)) {
34 |     return null
35 |   }
36 | 
37 |   const envSrc = fsx.readFileX(envFilepath)
38 |   const envParsed = dotenvParse(envSrc)
39 | 
40 |   let publicKeyName
41 |   for (const keyName of Object.keys(envParsed)) {
42 |     if (keyName === PUBLIC_KEY_SCHEMA || keyName.startsWith(PUBLIC_KEY_SCHEMA)) {
43 |       publicKeyName = keyName // find DOTENV_PUBLIC_KEY* in filename
44 |     }
45 |   }
46 | 
47 |   if (publicKeyName) {
48 |     return publicKeyName.replace(PUBLIC_KEY_SCHEMA, PRIVATE_KEY_SCHEMA) // return inverted (DOTENV_PUBLIC_KEY* -> DOTENV_PRIVATE_KEY*) if found
49 |   }
50 | 
51 |   return null
52 | }
53 | 
54 | function smartDotenvPrivateKey (envFilepath, envKeysFilepath = null) {
55 |   let privateKey = null
56 |   let privateKeyName = guessPrivateKeyName(envFilepath) // DOTENV_PRIVATE_KEY_${ENVIRONMENT}
57 | 
58 |   // 1. attempt process.env first
59 |   privateKey = searchProcessEnv(privateKeyName)
60 |   if (privateKey) {
61 |     return privateKey
62 |   }
63 | 
64 |   // 2. attempt .env.keys second (path/to/.env.keys)
65 |   privateKey = searchKeysFile(privateKeyName, envFilepath, envKeysFilepath)
66 |   if (privateKey) {
67 |     return privateKey
68 |   }
69 | 
70 |   // 3. attempt inverting `DOTENV_PUBLIC_KEY*` name inside file (unlocks custom filenames not matching .env.${ENVIRONMENT} pattern)
71 |   privateKeyName = invertForPrivateKeyName(envFilepath)
72 |   if (privateKeyName) {
73 |     // 3.1 attempt process.env first
74 |     privateKey = searchProcessEnv(privateKeyName)
75 |     if (privateKey) {
76 |       return privateKey
77 |     }
78 | 
79 |     // 3.2. attempt .env.keys second (path/to/.env.keys)
80 |     privateKey = searchKeysFile(privateKeyName, envFilepath, envKeysFilepath)
81 |     if (privateKey) {
82 |       return privateKey
83 |     }
84 |   }
85 | 
86 |   return null
87 | }
88 | 
89 | module.exports = smartDotenvPrivateKey
90 | 


--------------------------------------------------------------------------------
/src/lib/helpers/smartDotenvPublicKey.js:
--------------------------------------------------------------------------------
 1 | const fsx = require('./fsx')
 2 | const dotenvParse = require('./dotenvParse')
 3 | 
 4 | const guessPublicKeyName = require('./guessPublicKeyName')
 5 | 
 6 | function searchProcessEnv (publicKeyName) {
 7 |   if (process.env[publicKeyName] && process.env[publicKeyName].length > 0) {
 8 |     return process.env[publicKeyName]
 9 |   }
10 | }
11 | 
12 | function searchEnvFile (publicKeyName, envFilepath) {
13 |   if (fsx.existsSync(envFilepath)) {
14 |     const keysSrc = fsx.readFileX(envFilepath)
15 |     const keysParsed = dotenvParse(keysSrc)
16 | 
17 |     if (keysParsed[publicKeyName] && keysParsed[publicKeyName].length > 0) {
18 |       return keysParsed[publicKeyName]
19 |     }
20 |   }
21 | }
22 | 
23 | function smartDotenvPublicKey (envFilepath) {
24 |   let publicKey = null
25 |   const publicKeyName = guessPublicKeyName(envFilepath) // DOTENV_PUBLIC_KEY_${ENVIRONMENT}
26 | 
27 |   // 1. attempt process.env first
28 |   publicKey = searchProcessEnv(publicKeyName)
29 |   if (publicKey) {
30 |     return publicKey
31 |   }
32 | 
33 |   // 2. attempt .env.keys second (path/to/.env.keys)
34 |   publicKey = searchEnvFile(publicKeyName, envFilepath)
35 |   if (publicKey) {
36 |     return publicKey
37 |   }
38 | 
39 |   return null
40 | }
41 | 
42 | module.exports = smartDotenvPublicKey
43 | 


--------------------------------------------------------------------------------
/src/lib/helpers/truncate.js:
--------------------------------------------------------------------------------
 1 | function truncate (str, showChar = 7) {
 2 |   if (str && str.length > 0) {
 3 |     const visiblePart = str.slice(0, showChar)
 4 |     return visiblePart + '…'
 5 |   } else {
 6 |     return ''
 7 |   }
 8 | }
 9 | 
10 | module.exports = truncate
11 | 


--------------------------------------------------------------------------------
/src/lib/services/genexample.js:
--------------------------------------------------------------------------------
  1 | const fsx = require('./../helpers/fsx')
  2 | const path = require('path')
  3 | 
  4 | const Errors = require('../helpers/errors')
  5 | const findEnvFiles = require('../helpers/findEnvFiles')
  6 | const replace = require('../helpers/replace')
  7 | const dotenvParse = require('../helpers/dotenvParse')
  8 | 
  9 | class Genexample {
 10 |   constructor (directory = '.', envFile) {
 11 |     this.directory = directory
 12 |     this.envFile = envFile || findEnvFiles(directory)
 13 | 
 14 |     this.exampleFilename = '.env.example'
 15 |     this.exampleFilepath = path.resolve(this.directory, this.exampleFilename)
 16 |   }
 17 | 
 18 |   run () {
 19 |     if (this.envFile.length < 1) {
 20 |       const code = 'MISSING_ENV_FILES'
 21 |       const message = 'no .env* files found'
 22 |       const help = '? add one with [echo "HELLO=World" > .env] and then run [dotenvx genexample]'
 23 | 
 24 |       const error = new Error(message)
 25 |       error.code = code
 26 |       error.help = help
 27 |       throw error
 28 |     }
 29 | 
 30 |     const keys = new Set()
 31 |     const addedKeys = new Set()
 32 |     const envFilepaths = this._envFilepaths()
 33 |     /** @type {Record<string, string>} */
 34 |     const injected = {}
 35 |     /** @type {Record<string, string>} */
 36 |     const preExisted = {}
 37 | 
 38 |     let exampleSrc = `# ${this.exampleFilename} - generated with dotenvx\n`
 39 | 
 40 |     for (const envFilepath of envFilepaths) {
 41 |       const filepath = path.resolve(this.directory, envFilepath)
 42 |       if (!fsx.existsSync(filepath)) {
 43 |         const error = new Errors({ envFilepath, filepath }).missingEnvFile()
 44 |         error.help = `? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx genexample]`
 45 |         throw error
 46 |       }
 47 | 
 48 |       // get the original src
 49 |       let src = fsx.readFileX(filepath)
 50 |       const parsed = dotenvParse(src)
 51 |       for (const key in parsed) {
 52 |         // used later
 53 |         keys.add(key)
 54 | 
 55 |         // once newSrc is built write it out
 56 |         src = replace(src, key, '') // empty value
 57 |       }
 58 | 
 59 |       exampleSrc += `\n${src}`
 60 |     }
 61 | 
 62 |     if (!fsx.existsSync(this.exampleFilepath)) {
 63 |       // it doesn't exist so just write this first generated one
 64 |       // exampleSrc - already written to from the prior loop
 65 |       for (const key of [...keys]) {
 66 |         // every key is added since it's the first time generating .env.example
 67 |         addedKeys.add(key)
 68 | 
 69 |         injected[key] = ''
 70 |       }
 71 |     } else {
 72 |       // it already exists (which means the user might have it modified a way in which they prefer, so replace exampleSrc with their existing .env.example)
 73 |       exampleSrc = fsx.readFileX(this.exampleFilepath)
 74 | 
 75 |       const parsed = dotenvParse(exampleSrc)
 76 |       for (const key of [...keys]) {
 77 |         if (key in parsed) {
 78 |           preExisted[key] = parsed[key]
 79 |         } else {
 80 |           exampleSrc += `${key}=''\n`
 81 | 
 82 |           addedKeys.add(key)
 83 | 
 84 |           injected[key] = ''
 85 |         }
 86 |       }
 87 |     }
 88 | 
 89 |     return {
 90 |       envExampleFile: exampleSrc,
 91 |       envFile: this.envFile,
 92 |       exampleFilepath: this.exampleFilepath,
 93 |       addedKeys: [...addedKeys],
 94 |       injected,
 95 |       preExisted
 96 |     }
 97 |   }
 98 | 
 99 |   _envFilepaths () {
100 |     if (!Array.isArray(this.envFile)) {
101 |       return [this.envFile]
102 |     }
103 | 
104 |     return this.envFile
105 |   }
106 | }
107 | 
108 | module.exports = Genexample
109 | 


--------------------------------------------------------------------------------
/src/lib/services/get.js:
--------------------------------------------------------------------------------
 1 | const Run = require('./run')
 2 | const Errors = require('./../helpers/errors')
 3 | 
 4 | class Get {
 5 |   constructor (key, envs = [], overload = false, DOTENV_KEY = '', all = false, envKeysFilepath = null) {
 6 |     this.key = key
 7 |     this.envs = envs
 8 |     this.overload = overload
 9 |     this.DOTENV_KEY = DOTENV_KEY
10 |     this.all = all
11 |     this.envKeysFilepath = envKeysFilepath
12 |   }
13 | 
14 |   run () {
15 |     const processEnv = { ...process.env }
16 |     const { processedEnvs } = new Run(this.envs, this.overload, this.DOTENV_KEY, processEnv, this.envKeysFilepath).run()
17 | 
18 |     const errors = []
19 |     for (const processedEnv of processedEnvs) {
20 |       for (const error of processedEnv.errors) {
21 |         errors.push(error)
22 |       }
23 |     }
24 | 
25 |     if (this.key) {
26 |       const parsed = {}
27 |       const value = processEnv[this.key]
28 |       parsed[this.key] = value
29 | 
30 |       if (value === undefined) {
31 |         errors.push(new Errors({ key: this.key }).missingKey())
32 |       }
33 | 
34 |       return { parsed, errors }
35 |     } else {
36 |       // if user wants to return ALL envs (even prior set on machine)
37 |       if (this.all) {
38 |         return { parsed: processEnv, errors }
39 |       }
40 | 
41 |       // typical scenario - return only envs that were identified in the .env file
42 |       // iterate over all processedEnvs.parsed and grab from processEnv
43 |       /** @type {Record<string, string>} */
44 |       const parsed = {}
45 |       for (const processedEnv of processedEnvs) {
46 |         // parsed means we saw the key in a file or --env flag. this effectively filters out any preset machine envs - while still respecting complex evaluating, expansion, and overload. in other words, the value might be the machine value because the key was displayed in a .env file
47 |         if (processedEnv.parsed) {
48 |           for (const key of Object.keys(processedEnv.parsed)) {
49 |             parsed[key] = processEnv[key]
50 |           }
51 |         }
52 |       }
53 | 
54 |       return { parsed, errors }
55 |     }
56 |   }
57 | }
58 | 
59 | module.exports = Get
60 | 


--------------------------------------------------------------------------------
/src/lib/services/keypair.js:
--------------------------------------------------------------------------------
 1 | const guessPublicKeyName = require('./../helpers/guessPublicKeyName')
 2 | const smartDotenvPublicKey = require('./../helpers/smartDotenvPublicKey')
 3 | const guessPrivateKeyName = require('./../helpers/guessPrivateKeyName')
 4 | const smartDotenvPrivateKey = require('./../helpers/smartDotenvPrivateKey')
 5 | 
 6 | class Keypair {
 7 |   constructor (envFile = '.env', envKeysFilepath = null) {
 8 |     this.envFile = envFile
 9 |     this.envKeysFilepath = envKeysFilepath
10 |   }
11 | 
12 |   run () {
13 |     const out = {}
14 | 
15 |     const envFilepaths = this._envFilepaths()
16 |     for (const envFilepath of envFilepaths) {
17 |       // public key
18 |       const publicKeyName = guessPublicKeyName(envFilepath)
19 |       const publicKeyValue = smartDotenvPublicKey(envFilepath)
20 |       out[publicKeyName] = publicKeyValue
21 | 
22 |       // private key
23 |       const privateKeyName = guessPrivateKeyName(envFilepath)
24 |       const privateKeyValue = smartDotenvPrivateKey(envFilepath, this.envKeysFilepath)
25 | 
26 |       out[privateKeyName] = privateKeyValue
27 |     }
28 | 
29 |     return out
30 |   }
31 | 
32 |   _envFilepaths () {
33 |     if (!Array.isArray(this.envFile)) {
34 |       return [this.envFile]
35 |     }
36 | 
37 |     return this.envFile
38 |   }
39 | }
40 | 
41 | module.exports = Keypair
42 | 


--------------------------------------------------------------------------------
/src/lib/services/ls.js:
--------------------------------------------------------------------------------
 1 | const { fdir: Fdir } = require('fdir')
 2 | const path = require('path')
 3 | const picomatch = require('picomatch')
 4 | 
 5 | class Ls {
 6 |   constructor (directory = './', envFile = ['.env*'], excludeEnvFile = []) {
 7 |     this.ignore = ['node_modules/**', '.git/**']
 8 | 
 9 |     this.cwd = path.resolve(directory)
10 |     this.envFile = envFile
11 |     this.excludeEnvFile = excludeEnvFile
12 |   }
13 | 
14 |   run () {
15 |     return this._filepaths()
16 |   }
17 | 
18 |   _filepaths () {
19 |     const exclude = picomatch(this._exclude())
20 |     const include = picomatch(this._patterns(), {
21 |       ignore: this._exclude()
22 |     })
23 | 
24 |     return new Fdir()
25 |       .withRelativePaths()
26 |       .exclude((dir, path) => exclude(path))
27 |       .filter((path) => include(path))
28 |       .crawl(this.cwd)
29 |       .sync()
30 |   }
31 | 
32 |   _patterns () {
33 |     if (!Array.isArray(this.envFile)) {
34 |       return [`**/${this.envFile}`]
35 |     }
36 | 
37 |     return this.envFile.map(part => `**/${part}`)
38 |   }
39 | 
40 |   _excludePatterns () {
41 |     if (!Array.isArray(this.excludeEnvFile)) {
42 |       return [`**/${this.excludeEnvFile}`]
43 |     }
44 | 
45 |     return this.excludeEnvFile.map(part => `**/${part}`)
46 |   }
47 | 
48 |   _exclude () {
49 |     if (this._excludePatterns().length > 0) {
50 |       return this.ignore.concat(this._excludePatterns())
51 |     } else {
52 |       return this.ignore
53 |     }
54 |   }
55 | }
56 | 
57 | module.exports = Ls
58 | 


--------------------------------------------------------------------------------
/src/lib/services/prebuild.js:
--------------------------------------------------------------------------------
 1 | /* istanbul ignore file */
 2 | const fsx = require('./../helpers/fsx')
 3 | const path = require('path')
 4 | const ignore = require('ignore')
 5 | 
 6 | const Ls = require('../services/ls')
 7 | 
 8 | const isFullyEncrypted = require('./../helpers/isFullyEncrypted')
 9 | const packageJson = require('./../helpers/packageJson')
10 | const MISSING_DOCKERIGNORE = '.env.keys' // by default only ignore .env.keys. all other .env* files COULD be included - as long as they are encrypted
11 | 
12 | class Prebuild {
13 |   constructor (directory = './') {
14 |     // args
15 |     this.directory = directory
16 | 
17 |     this.excludeEnvFile = ['test/**', 'tests/**', 'spec/**', 'specs/**', 'pytest/**', 'test_suite/**']
18 |   }
19 | 
20 |   run () {
21 |     let count = 0
22 |     const warnings = []
23 |     let dockerignore = MISSING_DOCKERIGNORE
24 | 
25 |     // 1. check for .dockerignore file
26 |     if (!fsx.existsSync('.dockerignore')) {
27 |       const warning = new Error(`[dotenvx@${packageJson.version}][prebuild] .dockerignore missing`)
28 |       warnings.push(warning)
29 |     } else {
30 |       dockerignore = fsx.readFileX('.dockerignore')
31 |     }
32 | 
33 |     // 2. check .env* files against .dockerignore file
34 |     const ig = ignore().add(dockerignore)
35 |     const lsService = new Ls(this.directory, undefined, this.excludeEnvFile)
36 |     const dotenvFiles = lsService.run()
37 |     dotenvFiles.forEach(_file => {
38 |       count += 1
39 | 
40 |       const file = path.join(this.directory, _file) // to handle when directory argument passed
41 | 
42 |       // check if that file is being ignored
43 |       if (ig.ignores(file)) {
44 |         if (file === '.env.example' || file === '.env.vault') {
45 |           const warning = new Error(`[dotenvx@${packageJson.version}][prebuild] ${file} (currently ignored but should not be)`)
46 |           warning.help = `[dotenvx@${packageJson.version}][prebuild] ⮕  run [dotenvx ext gitignore --pattern !${file}]`
47 |           warnings.push(warning)
48 |         }
49 |       } else {
50 |         if (file !== '.env.example' && file !== '.env.vault') {
51 |           const src = fsx.readFileX(file)
52 |           const encrypted = isFullyEncrypted(src)
53 | 
54 |           // if contents are encrypted don't raise an error
55 |           if (!encrypted) {
56 |             let errorMsg = `[dotenvx@${packageJson.version}][prebuild] ${file} not protected (encrypted or dockerignored)`
57 |             let errorHelp = `[dotenvx@${packageJson.version}][prebuild] ⮕  run [dotenvx encrypt -f ${file}] or [dotenvx ext gitignore --pattern ${file}]`
58 |             if (file.includes('.env.keys')) {
59 |               errorMsg = `[dotenvx@${packageJson.version}][prebuild] ${file} not protected (dockerignored)`
60 |               errorHelp = `[dotenvx@${packageJson.version}][prebuild] ⮕  run [dotenvx ext gitignore --pattern ${file}]`
61 |             }
62 | 
63 |             const error = new Error(errorMsg)
64 |             error.help = errorHelp
65 |             throw error
66 |           }
67 |         }
68 |       }
69 |     })
70 | 
71 |     let successMessage = `[dotenvx@${packageJson.version}][prebuild] .env files (${count}) protected (encrypted or dockerignored)`
72 | 
73 |     if (count === 0) {
74 |       successMessage = `[dotenvx@${packageJson.version}][prebuild] zero .env files`
75 |     }
76 |     if (warnings.length > 0) {
77 |       successMessage += ` with warnings (${warnings.length})`
78 |     }
79 | 
80 |     return {
81 |       successMessage,
82 |       warnings
83 |     }
84 |   }
85 | }
86 | 
87 | module.exports = Prebuild
88 | 


--------------------------------------------------------------------------------
/src/shared/colors.js:
--------------------------------------------------------------------------------
 1 | const depth = require('../lib/helpers/colorDepth')
 2 | 
 3 | const colors16 = new Map([
 4 |   ['blue', 34],
 5 |   ['gray', 37],
 6 |   ['green', 32],
 7 |   ['olive', 33],
 8 |   ['orangered', 31], // mapped to red
 9 |   ['plum', 35], // mapped to magenta
10 |   ['red', 31],
11 |   ['electricblue', 36],
12 |   ['dodgerblue', 36]
13 | ])
14 | 
15 | const colors256 = new Map([
16 |   ['blue', 21],
17 |   ['gray', 244],
18 |   ['green', 34],
19 |   ['olive', 142],
20 |   ['orangered', 202],
21 |   ['plum', 182],
22 |   ['red', 196],
23 |   ['electricblue', 45],
24 |   ['dodgerblue', 33]
25 | ])
26 | 
27 | function getColor (color) {
28 |   const colorDepth = depth.getColorDepth()
29 |   if (!colors256.has(color)) {
30 |     throw new Error(`Invalid color ${color}`)
31 |   }
32 |   if (colorDepth >= 8) {
33 |     const code = colors256.get(color)
34 |     return (message) => `\x1b[38;5;${code}m${message}\x1b[39m`
35 |   }
36 |   if (colorDepth >= 4) {
37 |     const code = colors16.get(color)
38 |     return (message) => `\x1b[${code}m${message}\x1b[39m`
39 |   }
40 |   return (message) => message
41 | }
42 | 
43 | function bold (message) {
44 |   if (depth.getColorDepth() >= 4) {
45 |     return `\x1b[1m${message}\x1b[22m`
46 |   }
47 | 
48 |   return message
49 | }
50 | 
51 | module.exports = {
52 |   getColor,
53 |   bold
54 | }
55 | 


--------------------------------------------------------------------------------
/src/shared/logger.js:
--------------------------------------------------------------------------------
  1 | const packageJson = require('../lib/helpers/packageJson')
  2 | const { getColor, bold } = require('./colors')
  3 | 
  4 | const levels = {
  5 |   error: 0,
  6 |   warn: 1,
  7 |   success: 2,
  8 |   successv: 2,
  9 |   info: 2,
 10 |   help: 2,
 11 |   verbose: 4,
 12 |   debug: 5,
 13 |   silly: 6
 14 | }
 15 | 
 16 | const error = (m) => bold(getColor('red')(m))
 17 | const warn = getColor('orangered')
 18 | const success = getColor('green')
 19 | const successv = getColor('olive') // yellow-ish tint that 'looks' like dotenv
 20 | const help = getColor('dodgerblue')
 21 | const verbose = getColor('plum')
 22 | const debug = getColor('plum')
 23 | 
 24 | let currentLevel = levels.info // default log level
 25 | let currentName = 'dotenvx' // default logger name
 26 | let currentVersion = packageJson.version // default logger version
 27 | 
 28 | function stderr (level, message) {
 29 |   const formattedMessage = formatMessage(level, message)
 30 |   console.error(formattedMessage)
 31 | }
 32 | 
 33 | function stdout (level, message) {
 34 |   if (levels[level] === undefined) {
 35 |     throw new Error(`MISSING_LOG_LEVEL: '${level}'. implement in logger.`)
 36 |   }
 37 | 
 38 |   if (levels[level] <= currentLevel) {
 39 |     const formattedMessage = formatMessage(level, message)
 40 |     console.log(formattedMessage)
 41 |   }
 42 | }
 43 | 
 44 | function formatMessage (level, message) {
 45 |   const formattedMessage = typeof message === 'object' ? JSON.stringify(message) : message
 46 | 
 47 |   switch (level.toLowerCase()) {
 48 |     // errors
 49 |     case 'error':
 50 |       return error(formattedMessage)
 51 |     // warns
 52 |     case 'warn':
 53 |       return warn(formattedMessage)
 54 |     // successes
 55 |     case 'success':
 56 |       return success(formattedMessage)
 57 |     case 'successv': // success with 'version'
 58 |       return successv(`[${currentName}@${currentVersion}] ${formattedMessage}`)
 59 |     // info
 60 |     case 'info':
 61 |       return formattedMessage
 62 |     // help
 63 |     case 'help':
 64 |       return help(formattedMessage)
 65 |     // verbose
 66 |     case 'verbose':
 67 |       return verbose(formattedMessage)
 68 |     // debug
 69 |     case 'debug':
 70 |       return debug(formattedMessage)
 71 |   }
 72 | }
 73 | 
 74 | const logger = {
 75 |   // track level
 76 |   level: 'info',
 77 | 
 78 |   // errors
 79 |   error: (msg) => stderr('error', msg),
 80 |   // warns
 81 |   warn: (msg) => stdout('warn', msg),
 82 |   // success
 83 |   success: (msg) => stdout('success', msg),
 84 |   successv: (msg) => stdout('successv', msg),
 85 |   // info
 86 |   info: (msg) => stdout('info', msg),
 87 |   // help
 88 |   help: (msg) => stdout('help', msg),
 89 |   // verbose
 90 |   verbose: (msg) => stdout('verbose', msg),
 91 |   // debug
 92 |   debug: (msg) => stdout('debug', msg),
 93 |   setLevel: (level) => {
 94 |     if (levels[level] !== undefined) {
 95 |       currentLevel = levels[level]
 96 |       logger.level = level
 97 |     }
 98 |   },
 99 |   setName: (name) => {
100 |     currentName = name
101 |     logger.name = name
102 |   },
103 |   setVersion: (version) => {
104 |     currentVersion = version
105 |     logger.version = version
106 |   }
107 | }
108 | 
109 | function setLogLevel (options) {
110 |   const logLevel = options.debug
111 |     ? 'debug'
112 |     : options.verbose
113 |       ? 'verbose'
114 |       : options.quiet
115 |         ? 'error'
116 |         : options.logLevel
117 | 
118 |   if (!logLevel) return
119 |   logger.setLevel(logLevel)
120 |   // Only log which level it's setting if it's not set to quiet mode
121 |   if (!options.quiet || (options.quiet && logLevel !== 'error')) {
122 |     logger.debug(`Setting log level to ${logLevel}`)
123 |   }
124 | }
125 | 
126 | function setLogName (options) {
127 |   const logName = options.logName
128 |   if (!logName) return
129 |   logger.setName(logName)
130 | }
131 | 
132 | function setLogVersion (options) {
133 |   const logVersion = options.logVersion
134 |   if (!logVersion) return
135 |   logger.setVersion(logVersion)
136 | }
137 | 
138 | module.exports = {
139 |   logger,
140 |   getColor,
141 |   setLogLevel,
142 |   setLogName,
143 |   setLogVersion,
144 |   levels
145 | }
146 | 


--------------------------------------------------------------------------------
/tests/.env:
--------------------------------------------------------------------------------
 1 | BASIC=basic
 2 | 
 3 | # previous line intentionally left blank
 4 | AFTER_LINE=after_line
 5 | EMPTY=
 6 | EMPTY_SINGLE_QUOTES=''
 7 | EMPTY_DOUBLE_QUOTES=""
 8 | EMPTY_BACKTICKS=``
 9 | SINGLE_QUOTES='single_quotes'
10 | SINGLE_QUOTES_SPACED='    single quotes    '
11 | DOUBLE_QUOTES="double_quotes"
12 | DOUBLE_QUOTES_SPACED="    double quotes    "
13 | DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes'
14 | DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET="{ port: $MONGOLAB_PORT}"
15 | SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes"
16 | BACKTICKS_INSIDE_SINGLE='`backticks` work inside single quotes'
17 | BACKTICKS_INSIDE_DOUBLE="`backticks` work inside double quotes"
18 | BACKTICKS=`backticks`
19 | BACKTICKS_SPACED=`    backticks    `
20 | DOUBLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" work inside backticks`
21 | SINGLE_QUOTES_INSIDE_BACKTICKS=`single 'quotes' work inside backticks`
22 | DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" and single 'quotes' work inside backticks`
23 | EXPAND_NEWLINES="expand\nnew\nlines"
24 | DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
25 | DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
26 | # COMMENTS=work
27 | INLINE_COMMENTS=inline comments # work #very #well
28 | INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work
29 | INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work
30 | INLINE_COMMENTS_BACKTICKS=`inline comments outside of #backticks` # work
31 | INLINE_COMMENTS_SPACE=inline comments start with a#number sign. no space required.
32 | EQUAL_SIGNS=equals==
33 | RETAIN_INNER_QUOTES={"foo": "bar"}
34 | RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
35 | RETAIN_INNER_QUOTES_AS_BACKTICKS=`{"foo": "bar's"}`
36 | TRIM_SPACE_FROM_UNQUOTED=    some spaced out string
37 | USERNAME=therealnerdybeast@example.tld
38 |     SPACED_KEY = parsed
39 | 


--------------------------------------------------------------------------------
/tests/.env.eval:
--------------------------------------------------------------------------------
1 | HELLO="$(echo world)"
2 | 


--------------------------------------------------------------------------------
/tests/.env.export:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | export KEY=value
3 | 


--------------------------------------------------------------------------------
/tests/.env.latin1:
--------------------------------------------------------------------------------
1 | HELLO="latin1"


--------------------------------------------------------------------------------
/tests/.env.local:
--------------------------------------------------------------------------------
1 | BASIC=local_basic
2 | LOCAL=local
3 | 


--------------------------------------------------------------------------------
/tests/.env.multiline:
--------------------------------------------------------------------------------
 1 | BASIC=basic
 2 | 
 3 | # previous line intentionally left blank
 4 | AFTER_LINE=after_line
 5 | EMPTY=
 6 | SINGLE_QUOTES='single_quotes'
 7 | SINGLE_QUOTES_SPACED='    single quotes    '
 8 | DOUBLE_QUOTES="double_quotes"
 9 | DOUBLE_QUOTES_SPACED="    double quotes    "
10 | EXPAND_NEWLINES="expand\nnew\nlines"
11 | DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
12 | DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
13 | # COMMENTS=work
14 | EQUAL_SIGNS=equals==
15 | RETAIN_INNER_QUOTES={"foo": "bar"}
16 | 
17 | RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
18 | TRIM_SPACE_FROM_UNQUOTED=    some spaced out string
19 | USERNAME=therealnerdybeast@example.tld
20 |     SPACED_KEY = parsed
21 | 
22 | MULTI_DOUBLE_QUOTED="THIS
23 | IS
24 | A
25 | MULTILINE
26 | STRING"
27 | 
28 | MULTI_SINGLE_QUOTED='THIS
29 | IS
30 | A
31 | MULTILINE
32 | STRING'
33 | 
34 | MULTI_BACKTICKED=`THIS
35 | IS
36 | A
37 | "MULTILINE'S"
38 | STRING`
39 | 
40 | MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY-----
41 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
42 | LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
43 | bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
44 | kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
45 | u4QuUoobAgMBAAE=
46 | -----END PUBLIC KEY-----"
47 | 


--------------------------------------------------------------------------------
/tests/.env.utf16le:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dotenvx/dotenvx/49a78cacab5fa4b758b901f1ac739cccf2bf4562/tests/.env.utf16le


--------------------------------------------------------------------------------
/tests/.env.vault:
--------------------------------------------------------------------------------
1 | DOTENV_VAULT_DEVELOPMENT="s7NYXa809k/bVSPwIAmJhPJmEGTtU0hG58hOZy7I0ix6y5HP8LsHBsZCYC/gw5DDFy5DgOcyd18R"
2 | 


--------------------------------------------------------------------------------
/tests/cli/actions/ext/genexample.test.js:
--------------------------------------------------------------------------------
  1 | const t = require('tap')
  2 | const fsx = require('../../../../src/lib/helpers/fsx')
  3 | const sinon = require('sinon')
  4 | 
  5 | const main = require('../../../../src/lib/main')
  6 | const genexample = require('../../../../src/cli/actions/ext/genexample')
  7 | 
  8 | t.test('genexample calls main.genexample', ct => {
  9 |   const stub = sinon.stub(main, 'genexample')
 10 |   stub.returns({
 11 |     envExampleFile: 'HELLO=""',
 12 |     envFile: '.env.example',
 13 |     exampleFilepath: '.env.example',
 14 |     addedKeys: ['HELLO']
 15 |   })
 16 | 
 17 |   const fsStub = sinon.stub(fsx, 'writeFileX')
 18 | 
 19 |   const optsStub = sinon.stub().returns({})
 20 |   const fakeContext = {
 21 |     opts: optsStub
 22 |   }
 23 | 
 24 |   // Call the genexample function with the fake context
 25 |   genexample.call(fakeContext, '.')
 26 | 
 27 |   t.ok(stub.called, 'main.genexample() called')
 28 |   t.ok(fsStub.called, 'fs.writeFileX() called')
 29 |   stub.restore()
 30 |   fsStub.restore()
 31 | 
 32 |   ct.end()
 33 | })
 34 | 
 35 | t.test('genexample calls main.genexample (no addedKeys changes)', ct => {
 36 |   const stub = sinon.stub(main, 'genexample')
 37 |   stub.returns({
 38 |     envExampleFile: '',
 39 |     envFile: '.env.example',
 40 |     exampleFilepath: '.env.example',
 41 |     addedKeys: []
 42 |   })
 43 | 
 44 |   const fsStub = sinon.stub(fsx, 'writeFileX')
 45 | 
 46 |   const optsStub = sinon.stub().returns({})
 47 |   const fakeContext = {
 48 |     opts: optsStub
 49 |   }
 50 | 
 51 |   // Call the genexample function with the fake context
 52 |   genexample.call(fakeContext, '.')
 53 | 
 54 |   t.ok(stub.called, 'main.genexample() called')
 55 |   t.ok(fsStub.called, 'fsx.writeFileX() called')
 56 |   stub.restore()
 57 |   fsStub.restore()
 58 | 
 59 |   ct.end()
 60 | })
 61 | 
 62 | t.test('genexample calls main.genexample (other error)', ct => {
 63 |   const stub = sinon.stub(main, 'genexample').throws(new Error('other error'))
 64 |   const exitStub = sinon.stub(process, 'exit')
 65 | 
 66 |   const optsStub = sinon.stub().returns({})
 67 |   const fakeContext = {
 68 |     opts: optsStub
 69 |   }
 70 | 
 71 |   // Call the genexample function with the fake context
 72 |   genexample.call(fakeContext, '.')
 73 | 
 74 |   ct.ok(exitStub.calledWith(1), 'process.exit was called with code 1')
 75 | 
 76 |   stub.restore()
 77 |   exitStub.restore()
 78 | 
 79 |   ct.end()
 80 | })
 81 | 
 82 | t.test('genexample calls main.genexample (error with code and help message)', ct => {
 83 |   const error = new Error('message')
 84 |   error.help = 'help message'
 85 |   error.code = 'CODE'
 86 | 
 87 |   const stub = sinon.stub(main, 'genexample').throws(error)
 88 |   const exitStub = sinon.stub(process, 'exit')
 89 | 
 90 |   const optsStub = sinon.stub().returns({})
 91 |   const fakeContext = {
 92 |     opts: optsStub
 93 |   }
 94 | 
 95 |   // Call the genexample function with the fake context
 96 |   genexample.call(fakeContext, '.')
 97 | 
 98 |   ct.ok(exitStub.calledWith(1), 'process.exit was called with code 1')
 99 | 
100 |   stub.restore()
101 |   exitStub.restore()
102 | 
103 |   ct.end()
104 | })
105 | 


--------------------------------------------------------------------------------
/tests/cli/actions/ext/prebuild.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | 
 4 | const prebuild = require('../../../../src/cli/actions/ext/prebuild')
 5 | 
 6 | const { logger } = require('../../../../src/shared/logger')
 7 | const Prebuild = require('../../../../src/lib/services/prebuild')
 8 | 
 9 | const optsStub = sinon.stub().returns({})
10 | const fakeContext = { opts: optsStub }
11 | 
12 | t.beforeEach((ct) => {
13 |   sinon.restore()
14 | })
15 | 
16 | t.test('prebuild - successMessage', (ct) => {
17 |   // Stub the Prebuild service
18 |   sinon.stub(Prebuild.prototype, 'run').returns({
19 |     successMessage: 'success',
20 |     warnings: []
21 |   })
22 | 
23 |   const loggerSuccessStub = sinon.stub(logger, 'success')
24 | 
25 |   prebuild.call(fakeContext)
26 | 
27 |   ct.ok(loggerSuccessStub.calledWith('success'), 'logger.success logs')
28 | 
29 |   ct.end()
30 | })
31 | 
32 | t.test('prebuild - success with warnings', (ct) => {
33 |   const warning = new Error('.dockerignore missing')
34 |   warning.help = '? add it with [touch .dockerignore]'
35 |   // Stub the Prebuild service
36 |   sinon.stub(Prebuild.prototype, 'run').returns({
37 |     successMessage: 'success (with 1 warning)',
38 |     warnings: [warning]
39 |   })
40 | 
41 |   const loggerSuccessStub = sinon.stub(logger, 'success')
42 |   const loggerWarnStub = sinon.stub(logger, 'warn')
43 |   const loggerHelpStub = sinon.stub(logger, 'help')
44 | 
45 |   prebuild.call(fakeContext)
46 | 
47 |   ct.ok(loggerSuccessStub.calledWith('success (with 1 warning)'), 'logger.success logs')
48 |   ct.ok(loggerWarnStub.calledWith('.dockerignore missing'), 'logger.warn logs')
49 |   ct.ok(loggerHelpStub.calledWith('? add it with [touch .dockerignore]'), 'logger.help logs')
50 | 
51 |   ct.end()
52 | })
53 | 
54 | t.test('prebuild - error raised', (ct) => {
55 |   sinon.stub(Prebuild.prototype, 'run').throws({
56 |     message: 'An error occurred',
57 |     help: 'Help message for error'
58 |   })
59 | 
60 |   const processExitStub = sinon.stub(process, 'exit')
61 |   const loggerErrorStub = sinon.stub(logger, 'error')
62 |   const loggerHelpStub = sinon.stub(logger, 'help')
63 | 
64 |   prebuild.call(fakeContext)
65 | 
66 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
67 |   ct.ok(loggerErrorStub.calledWith('An error occurred'), 'logger.success logs')
68 |   ct.ok(loggerHelpStub.calledWith('Help message for error'), 'logger.help logs')
69 | 
70 |   ct.end()
71 | })
72 | 


--------------------------------------------------------------------------------
/tests/cli/actions/ext/precommit.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | 
 4 | const precommit = require('../../../../src/cli/actions/ext/precommit')
 5 | 
 6 | const { logger } = require('../../../../src/shared/logger')
 7 | const Precommit = require('../../../../src/lib/services/precommit')
 8 | 
 9 | const optsStub = sinon.stub().returns({})
10 | const fakeContext = { opts: optsStub }
11 | 
12 | t.beforeEach((ct) => {
13 |   sinon.restore()
14 | })
15 | 
16 | t.test('precommit - successMessage', (ct) => {
17 |   // Stub the Precommit service
18 |   sinon.stub(Precommit.prototype, 'run').returns({
19 |     successMessage: 'success',
20 |     warnings: []
21 |   })
22 | 
23 |   const loggerSuccessStub = sinon.stub(logger, 'success')
24 | 
25 |   precommit.call(fakeContext)
26 | 
27 |   ct.ok(loggerSuccessStub.calledWith('success'), 'logger.success logs')
28 | 
29 |   ct.end()
30 | })
31 | 
32 | t.test('precommit - success with warnings', (ct) => {
33 |   const warning = new Error('.gitignore missing')
34 |   warning.help = '? add it with [touch .gitignore]'
35 |   // Stub the Precommit service
36 |   sinon.stub(Precommit.prototype, 'run').returns({
37 |     successMessage: 'success (with 1 warning)',
38 |     warnings: [warning]
39 |   })
40 | 
41 |   const loggerSuccessStub = sinon.stub(logger, 'success')
42 |   const loggerWarnStub = sinon.stub(logger, 'warn')
43 |   const loggerHelpStub = sinon.stub(logger, 'help')
44 | 
45 |   precommit.call(fakeContext)
46 | 
47 |   ct.ok(loggerSuccessStub.calledWith('success (with 1 warning)'), 'logger.success logs')
48 |   ct.ok(loggerWarnStub.calledWith('.gitignore missing'), 'logger.warn logs')
49 |   ct.ok(loggerHelpStub.calledWith('? add it with [touch .gitignore]'), 'logger.help logs')
50 | 
51 |   ct.end()
52 | })
53 | 
54 | t.test('precommit - error raised', (ct) => {
55 |   sinon.stub(Precommit.prototype, 'run').throws({
56 |     message: 'An error occurred',
57 |     help: 'Help message for error'
58 |   })
59 | 
60 |   const processExitStub = sinon.stub(process, 'exit')
61 |   const loggerErrorStub = sinon.stub(logger, 'error')
62 |   const loggerHelpStub = sinon.stub(logger, 'help')
63 | 
64 |   precommit.call(fakeContext)
65 | 
66 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
67 |   ct.ok(loggerErrorStub.calledWith('An error occurred'), 'logger.success logs')
68 |   ct.ok(loggerHelpStub.calledWith('Help message for error'), 'logger.help logs')
69 | 
70 |   ct.end()
71 | })
72 | 


--------------------------------------------------------------------------------
/tests/cli/actions/ext/scan.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | const childProcess = require('child_process')
 4 | 
 5 | const scan = require('../../../../src/cli/actions/ext/scan')
 6 | 
 7 | const { logger } = require('../../../../src/shared/logger')
 8 | 
 9 | const optsStub = sinon.stub().returns({})
10 | const fakeContext = { opts: optsStub }
11 | const originalExecSync = childProcess.execSync
12 | 
13 | t.beforeEach((ct) => {
14 |   sinon.restore()
15 |   childProcess.execSync = sinon.stub()
16 | })
17 | 
18 | t.afterEach((ct) => {
19 |   childProcess.execSync = originalExecSync // restore the original execSync after each test
20 | })
21 | 
22 | t.test('scan - gitleaks not installed', (ct) => {
23 |   childProcess.execSync.throws(new Error('gitleaks: command not found'))
24 | 
25 |   const processExitStub = sinon.stub(process, 'exit')
26 |   const loggerErrorStub = sinon.stub(logger, 'error')
27 |   const loggerHelpStub = sinon.stub(logger, 'help')
28 | 
29 |   scan.call(fakeContext)
30 | 
31 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
32 |   ct.ok(loggerErrorStub.calledWith('gitleaks: command not found'), 'logger.error logs')
33 |   ct.ok(loggerHelpStub.calledWith('? install gitleaks:      [brew install gitleaks]'), 'logger.help logs')
34 |   ct.ok(loggerHelpStub.calledWith('? other install options: [https://github.com/gitleaks/gitleaks]'), 'logger.help logs')
35 | 
36 |   ct.end()
37 | })
38 | 
39 | t.test('scan - gitleaks installed and works', (ct) => {
40 |   const gitleaksOutput = `
41 |     ○
42 |     │╲
43 |     │ ○
44 |     ○ ░
45 |     ░    gitleaks
46 | 
47 | 10:22AM INF 0 commits scanned.
48 | 10:22AM INF scan completed in 14.4ms
49 | 10:22AM INF no leaks found
50 |   `
51 | 
52 |   childProcess.execSync.onCall(0).returns('8.18.4')
53 |   childProcess.execSync.onCall(1).returns(gitleaksOutput)
54 | 
55 |   const loggerInfoStub = sinon.stub(logger, 'info')
56 | 
57 |   scan.call(fakeContext)
58 | 
59 |   ct.ok(loggerInfoStub.calledWith(gitleaksOutput), 'logger.info logs')
60 | 
61 |   ct.end()
62 | })
63 | 
64 | t.test('scan - gitleaks installed and raises error', (ct) => {
65 |   childProcess.execSync.onCall(0).returns('8.18.4')
66 | 
67 |   // Simulate gitleaks error with stderr output
68 |   const error = new Error('Gitleaks failed')
69 |   error.stdout = Buffer.from('leak: API_KEY=abcd1234')
70 |   childProcess.execSync.onCall(1).throws(error)
71 | 
72 |   const processExitStub = sinon.stub(process, 'exit')
73 |   const loggerErrorStub = sinon.stub(logger, 'error')
74 | 
75 |   scan.call(fakeContext)
76 | 
77 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
78 |   ct.ok(loggerErrorStub.calledWith('leak: API_KEY=abcd1234'), 'logger.error logs')
79 | 
80 |   ct.end()
81 | })
82 | 


--------------------------------------------------------------------------------
/tests/cli/actions/keypair.test.js:
--------------------------------------------------------------------------------
  1 | const t = require('tap')
  2 | const sinon = require('sinon')
  3 | const capcon = require('capture-console')
  4 | 
  5 | const main = require('./../../../src/lib/main')
  6 | 
  7 | const keypair = require('./../../../src/cli/actions/keypair')
  8 | 
  9 | t.beforeEach((ct) => {
 10 |   sinon.restore()
 11 | })
 12 | 
 13 | t.test('keypair', ct => {
 14 |   const optsStub = sinon.stub().returns({})
 15 |   const fakeContext = { opts: optsStub }
 16 |   const stub = sinon.stub(main, 'keypair').returns({ DOTENV_PUBLIC_KEY: '<publicKey>', DOTENV_PRIVATE_KEY: '<privateKey>' })
 17 | 
 18 |   const stdout = capcon.interceptStdout(() => {
 19 |     keypair.call(fakeContext, undefined)
 20 |   })
 21 | 
 22 |   t.ok(stub.called, 'main.keypair() called')
 23 |   t.equal(stdout, `${JSON.stringify({ DOTENV_PUBLIC_KEY: '<publicKey>', DOTENV_PRIVATE_KEY: '<privateKey>' }, null, 0)}\n`)
 24 | 
 25 |   ct.end()
 26 | })
 27 | 
 28 | t.test('keypair KEY', ct => {
 29 |   const optsStub = sinon.stub().returns({})
 30 |   const fakeContext = { opts: optsStub }
 31 |   const stub = sinon.stub(main, 'keypair').returns('<publicKey>')
 32 | 
 33 |   const stdout = capcon.interceptStdout(() => {
 34 |     keypair.call(fakeContext, 'DOTENV_PUBLIC_KEY')
 35 |   })
 36 | 
 37 |   t.ok(stub.called, 'main.keypair() called')
 38 |   t.equal(stdout, '<publicKey>\n')
 39 | 
 40 |   ct.end()
 41 | })
 42 | 
 43 | t.test('keypair --format shell', ct => {
 44 |   const optsStub = sinon.stub().returns({ format: 'shell' })
 45 |   const fakeContext = { opts: optsStub }
 46 |   const stub = sinon.stub(main, 'keypair').returns({ DOTENV_PUBLIC_KEY: '<publicKey>', DOTENV_PRIVATE_KEY: '<privateKey>' })
 47 | 
 48 |   const stdout = capcon.interceptStdout(() => {
 49 |     keypair.call(fakeContext, undefined)
 50 |   })
 51 | 
 52 |   t.ok(stub.called, 'main.keypair() called')
 53 |   t.equal(stdout, 'DOTENV_PUBLIC_KEY=<publicKey> DOTENV_PRIVATE_KEY=<privateKey>\n')
 54 | 
 55 |   ct.end()
 56 | })
 57 | 
 58 | t.test('keypair --format shell (when null value should be empty string for shell format)', ct => {
 59 |   const optsStub = sinon.stub().returns({ format: 'shell' })
 60 |   const fakeContext = { opts: optsStub }
 61 |   const stub = sinon.stub(main, 'keypair').returns({ DOTENV_PUBLIC_KEY: '<publicKey>', DOTENV_PRIVATE_KEY: null })
 62 | 
 63 |   const stdout = capcon.interceptStdout(() => {
 64 |     keypair.call(fakeContext, undefined)
 65 |   })
 66 | 
 67 |   t.ok(stub.called, 'main.keypair() called')
 68 |   t.equal(stdout, 'DOTENV_PUBLIC_KEY=<publicKey> DOTENV_PRIVATE_KEY=\n')
 69 | 
 70 |   ct.end()
 71 | })
 72 | 
 73 | t.test('keypair --pretty-print', ct => {
 74 |   const optsStub = sinon.stub().returns({ prettyPrint: true })
 75 |   const fakeContext = { opts: optsStub }
 76 |   const stub = sinon.stub(main, 'keypair').returns({ DOTENV_PUBLIC_KEY: '<publicKey>' })
 77 | 
 78 |   const stdout = capcon.interceptStdout(() => {
 79 |     keypair.call(fakeContext, undefined)
 80 |   })
 81 | 
 82 |   t.ok(stub.called, 'main.keypair() called')
 83 |   t.equal(stdout, `${JSON.stringify({ DOTENV_PUBLIC_KEY: '<publicKey>' }, null, 2)}\n`)
 84 | 
 85 |   ct.end()
 86 | })
 87 | 
 88 | t.test('keypair KEY (not found)', ct => {
 89 |   const optsStub = sinon.stub().returns({})
 90 |   const fakeContext = { opts: optsStub }
 91 |   const stub = sinon.stub(main, 'keypair').returns(undefined)
 92 |   const processExitStub = sinon.stub(process, 'exit')
 93 | 
 94 |   const stdout = capcon.interceptStdout(() => {
 95 |     keypair.call(fakeContext, 'NOTFOUND')
 96 |   })
 97 | 
 98 |   t.ok(stub.called, 'main.keypair() called')
 99 |   t.ok(processExitStub.calledWith(1), 'process.exit(1)')
100 |   t.equal(stdout, '\n') // send empty string if key's value undefined
101 | 
102 |   ct.end()
103 | })
104 | 


--------------------------------------------------------------------------------
/tests/cli/actions/ls.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | 
 4 | const main = require('../../../src/lib/main')
 5 | const ls = require('../../../src/cli/actions/ls')
 6 | 
 7 | t.test('ls calls main.ls', ct => {
 8 |   const stub = sinon.stub(main, 'ls')
 9 |   stub.returns({})
10 | 
11 |   const optsStub = sinon.stub().returns({})
12 |   const fakeContext = {
13 |     opts: optsStub
14 |   }
15 | 
16 |   // Call the ls function with the fake context
17 |   ls.call(fakeContext, '.')
18 | 
19 |   t.ok(stub.called, 'main.ls() called')
20 |   stub.restore()
21 | 
22 |   ct.end()
23 | })
24 | 


--------------------------------------------------------------------------------
/tests/cli/examples.test.js:
--------------------------------------------------------------------------------
  1 | const tap = require('tap')
  2 | const {
  3 |   run,
  4 |   precommit,
  5 |   prebuild,
  6 |   gitignore,
  7 |   set
  8 | } = require('../../src/cli/examples') // Adjust the path as needed
  9 | 
 10 | tap.test('run function returns expected string', (t) => {
 11 |   const expected = `
 12 | Examples:
 13 | 
 14 |   \`\`\`
 15 |   $ dotenvx run -- npm run dev
 16 |   $ dotenvx run -- flask --app index run
 17 |   $ dotenvx run -- php artisan serve
 18 |   $ dotenvx run -- bin/rails s
 19 |   \`\`\`
 20 | 
 21 | Try it:
 22 | 
 23 |   \`\`\`
 24 |   $ echo "HELLO=World" > .env
 25 |   $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
 26 | 
 27 |   $ dotenvx run -f .env -- node index.js
 28 |   [dotenvx] injecting env (1) from .env
 29 |   Hello World
 30 |   \`\`\`
 31 |   `
 32 |   t.equal(run(), expected)
 33 |   t.end()
 34 | })
 35 | 
 36 | tap.test('precommit function returns expected string', (t) => {
 37 |   const expected = `
 38 | Examples:
 39 | 
 40 |   \`\`\`
 41 |   $ dotenvx ext precommit
 42 |   $ dotenvx ext precommit --install
 43 |   \`\`\`
 44 | 
 45 | Try it:
 46 | 
 47 |   \`\`\`
 48 |   $ dotenvx ext precommit
 49 |   [dotenvx@0.45.0][precommit] success
 50 |   \`\`\`
 51 |   `
 52 |   t.equal(precommit(), expected)
 53 |   t.end()
 54 | })
 55 | 
 56 | tap.test('prebuild function returns expected string', (t) => {
 57 |   const expected = `
 58 | Examples:
 59 | 
 60 |   \`\`\`
 61 |   $ dotenvx ext prebuild
 62 |   \`\`\`
 63 | 
 64 | Try it:
 65 | 
 66 |   \`\`\`
 67 |   $ dotenvx ext prebuild
 68 |   [dotenvx@0.10.0][prebuild] success
 69 |   \`\`\`
 70 |   `
 71 |   t.equal(prebuild(), expected)
 72 |   t.end()
 73 | })
 74 | 
 75 | tap.test('gitignore function returns expected string', (t) => {
 76 |   const expected = `
 77 | Examples:
 78 | 
 79 |   \`\`\`
 80 |   $ dotenvx ext gitignore
 81 |   $ dotenvx ext gitignore --pattern .env.keys
 82 |   \`\`\`
 83 | 
 84 | Try it:
 85 | 
 86 |   \`\`\`
 87 |   $ dotenvx ext gitignore
 88 |   ✔ ignored .env* (.gitignore)
 89 |   \`\`\`
 90 |   `
 91 |   t.equal(gitignore(), expected)
 92 |   t.end()
 93 | })
 94 | 
 95 | tap.test('set function returns expected string', (t) => {
 96 |   const expected = `
 97 | Examples:
 98 | 
 99 |   \`\`\`
100 |   $ dotenvx set KEY value
101 |   $ dotenvx set KEY "value with spaces"
102 |   $ dotenvx set KEY -- "---value with a dash---"
103 |   $ dotenvx set KEY -- "-----BEGIN OPENSSH PRIVATE KEY-----
104 |                         b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
105 |                         -----END OPENSSH PRIVATE KEY-----"
106 |   \`\`\`
107 |   `
108 |   t.equal(set(), expected)
109 |   t.end()
110 | })
111 | 


--------------------------------------------------------------------------------
/tests/e2e/encrypt.test.js:
--------------------------------------------------------------------------------
  1 | const t = require('tap')
  2 | const fs = require('fs')
  3 | const os = require('os')
  4 | const path = require('path')
  5 | const which = require('which')
  6 | const dotenv = require('dotenv')
  7 | const { execSync } = require('child_process')
  8 | 
  9 | const keypair = require('../../src/lib/helpers/keypair')
 10 | 
 11 | let tempDir = ''
 12 | const osTempDir = fs.realpathSync(os.tmpdir())
 13 | const originalDir = process.cwd()
 14 | 
 15 | const node = path.resolve(which.sync('node')) // /opt/homebrew/node
 16 | const dotenvx = `${node} ${path.join(originalDir, 'src/cli/dotenvx.js')}`
 17 | 
 18 | function execShell (commands) {
 19 |   return execSync(commands, {
 20 |     encoding: 'utf8',
 21 |     shell: true
 22 |   }).trim()
 23 | }
 24 | 
 25 | t.beforeEach((ct) => {
 26 |   // important, clear process.env before each test
 27 |   process.env = {}
 28 | 
 29 |   tempDir = fs.mkdtempSync(path.join(osTempDir, 'dotenvx-test-'))
 30 | 
 31 |   // go to tempDir
 32 |   process.chdir(tempDir)
 33 | })
 34 | 
 35 | t.afterEach((ct) => {
 36 |   // cleanup
 37 |   process.chdir(originalDir)
 38 | })
 39 | 
 40 | t.test('#encrypt', ct => {
 41 |   execShell(`
 42 |     echo "HELLO=World" > .env
 43 |   `)
 44 | 
 45 |   const output = execShell(`${dotenvx} encrypt`)
 46 | 
 47 |   const parsedEnvKeys = dotenv.parse(fs.readFileSync(path.join(tempDir, '.env.keys')))
 48 |   const DOTENV_PRIVATE_KEY = parsedEnvKeys.DOTENV_PRIVATE_KEY
 49 | 
 50 |   ct.equal(output, `✔ encrypted (.env)
 51 | ✔ key added to .env.keys (DOTENV_PRIVATE_KEY)
 52 | ⮕  next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys
 53 | ⮕  next run [DOTENV_PRIVATE_KEY='${DOTENV_PRIVATE_KEY}' dotenvx run -- yourcommand] to test decryption locally`)
 54 | 
 55 |   ct.end()
 56 | })
 57 | 
 58 | t.test('#encrypt -k', ct => {
 59 |   ct.plan(4)
 60 | 
 61 |   execShell(`
 62 |     echo "HELLO=World\nHI=thar" > .env
 63 |   `)
 64 | 
 65 |   const output = execShell(`${dotenvx} encrypt -k HI`)
 66 | 
 67 |   const parsedEnvKeys = dotenv.parse(fs.readFileSync(path.join(tempDir, '.env.keys')))
 68 |   const DOTENV_PRIVATE_KEY = parsedEnvKeys.DOTENV_PRIVATE_KEY
 69 | 
 70 |   ct.equal(output, `✔ encrypted (.env)
 71 | ✔ key added to .env.keys (DOTENV_PRIVATE_KEY)
 72 | ⮕  next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys
 73 | ⮕  next run [DOTENV_PRIVATE_KEY='${DOTENV_PRIVATE_KEY}' dotenvx run -- yourcommand] to test decryption locally`)
 74 | 
 75 |   execShell('rm .env.keys')
 76 | 
 77 |   ct.equal(execShell(`${dotenvx} get HELLO`), 'World') // unencrypted still
 78 |   ct.match(execShell(`${dotenvx} get HI`), /^encrypted:/, 'HI should be encrypted')
 79 | 
 80 |   process.env.DOTENV_PRIVATE_KEY = DOTENV_PRIVATE_KEY
 81 |   ct.equal(execShell(`${dotenvx} get HI`), 'thar')
 82 | 
 83 |   ct.end()
 84 | })
 85 | 
 86 | t.test('#run - encrypt -k --stdout', ct => {
 87 |   execShell(`
 88 |     echo "HELLO=World\nHI=thar" > .env
 89 |   `)
 90 | 
 91 |   const output = execShell(`${dotenvx} encrypt -k HI --stdout`)
 92 | 
 93 |   const parsedEnvKeys = dotenv.parse(fs.readFileSync(path.join(tempDir, '.env.keys')))
 94 |   const DOTENV_PRIVATE_KEY = parsedEnvKeys.DOTENV_PRIVATE_KEY
 95 |   const { publicKey } = keypair(DOTENV_PRIVATE_KEY)
 96 | 
 97 |   const expectedFixedPart1 = `#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
 98 | #/            public-key encryption for .env files          /
 99 | #/       [how it works](https://dotenvx.com/encryption)     /
100 | #/----------------------------------------------------------/
101 | DOTENV_PUBLIC_KEY="${publicKey}"
102 | 
103 | # .env
104 | HELLO=World
105 | HI=encrypted:`
106 | 
107 |   const parts = output.split('HI=encrypted:')
108 |   const encryptedPart = parts[1]
109 |   const unencryptedPart = `${parts[0]}HI=encrypted:`
110 | 
111 |   ct.equal(unencryptedPart, expectedFixedPart1, 'The fixed part of the output should match the expected output')
112 |   ct.match(encryptedPart, /.*/, 'The encrypted part should match the expected pattern')
113 | 
114 |   ct.end()
115 | })
116 | 


--------------------------------------------------------------------------------
/tests/e2e/ext.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const path = require('path')
 3 | const which = require('which')
 4 | const { execSync } = require('child_process')
 5 | 
 6 | const originalDir = process.cwd()
 7 | 
 8 | const node = path.resolve(which.sync('node')) // /opt/homebrew/node
 9 | const dotenvx = `${node} ${path.join(originalDir, 'src/cli/dotenvx.js')}`
10 | 
11 | function execShell (commands) {
12 |   return execSync(commands, {
13 |     encoding: 'utf8',
14 |     shell: true
15 |   }).trim()
16 | }
17 | 
18 | t.test('ext', ct => {
19 |   const output = execShell(`${dotenvx} ext`)
20 | 
21 |   t.match(output, /genexample/, 'should say genexample')
22 |   t.match(output, /gitignore/, 'should say gitignore')
23 |   t.match(output, /prebuild/, 'should say prebuild')
24 |   t.match(output, /precommit/, 'should say precommit')
25 | 
26 |   ct.end()
27 | })
28 | 
29 | t.test('ext missing', ct => {
30 |   const output = execShell(`${dotenvx} ext missing`)
31 | 
32 |   t.match(output, "error: unknown command 'missing'", 'should say installation needed')
33 | 
34 |   ct.end()
35 | })
36 | 
37 | t.test('ext vault', ct => {
38 |   const output = execShell(`${dotenvx} ext vault`)
39 | 
40 |   t.match(output, /\[INSTALLATION_NEEDED\] install dotenvx-ext-vault to use \[dotenvx ext vault\] commands/, 'should say installation needed')
41 |   t.match(output, /see installation instructions/, 'should say see installation instructions')
42 | 
43 |   ct.end()
44 | })
45 | 


--------------------------------------------------------------------------------
/tests/e2e/get.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const fs = require('fs')
 3 | const os = require('os')
 4 | const path = require('path')
 5 | const which = require('which')
 6 | const { execSync } = require('child_process')
 7 | 
 8 | let tempDir = ''
 9 | const osTempDir = fs.realpathSync(os.tmpdir())
10 | const originalDir = process.cwd()
11 | 
12 | const node = path.resolve(which.sync('node')) // /opt/homebrew/node
13 | const dotenvx = `${node} ${path.join(originalDir, 'src/cli/dotenvx.js')}`
14 | 
15 | function execShell (commands) {
16 |   return execSync(commands, {
17 |     encoding: 'utf8',
18 |     shell: true
19 |   }).trim()
20 | }
21 | 
22 | t.beforeEach((ct) => {
23 |   // important, clear process.env before each test
24 |   process.env = {}
25 | 
26 |   tempDir = fs.mkdtempSync(path.join(osTempDir, 'dotenvx-test-'))
27 | 
28 |   // go to tempDir
29 |   process.chdir(tempDir)
30 | })
31 | 
32 | t.afterEach((ct) => {
33 |   // cleanup
34 |   process.chdir(originalDir)
35 | })
36 | 
37 | t.test('#get', ct => {
38 |   execShell(`
39 |     echo "HELLO=World" > .env
40 |   `)
41 | 
42 |   ct.equal(execShell(`${dotenvx} get HELLO`), 'World')
43 | 
44 |   ct.end()
45 | })
46 | 
47 | t.test('#get --env', ct => {
48 |   execShell(`
49 |     echo "HELLO=World" > .env
50 |   `)
51 | 
52 |   ct.equal(execShell(`${dotenvx} get HELLO --env HELLO=String`), 'World')
53 |   ct.equal(execShell(`${dotenvx} get HELLO --env HELLO=String -f .env`), 'String')
54 |   ct.equal(execShell(`${dotenvx} get HELLO -f .env --env HELLO=String`), 'World')
55 | 
56 |   ct.end()
57 | })
58 | 
59 | t.test('#get --overload', ct => {
60 |   execShell(`
61 |     echo "HELLO=World" > .env
62 |     echo "HELLO=production" > .env.production
63 |   `)
64 | 
65 |   ct.equal(execShell(`${dotenvx} get HELLO -f .env.production --env HELLO=String -f .env --overload`), 'World')
66 | 
67 |   ct.end()
68 | })
69 | 
70 | t.test('#get (json)', ct => {
71 |   execShell(`
72 |     echo "HELLO=World" > .env
73 |   `)
74 | 
75 |   ct.equal(execShell(`${dotenvx} get`), '{"HELLO":"World"}')
76 | 
77 |   ct.end()
78 | })
79 | 


--------------------------------------------------------------------------------
/tests/e2e/version.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const path = require('path')
 3 | const which = require('which')
 4 | const { execSync } = require('child_process')
 5 | 
 6 | const packageJson = require('../../src/lib/helpers/packageJson')
 7 | const version = packageJson.version
 8 | 
 9 | const originalDir = process.cwd()
10 | 
11 | const node = path.resolve(which.sync('node')) // /opt/homebrew/node
12 | const dotenvx = `${node} ${path.join(originalDir, 'src/cli/dotenvx.js')}`
13 | 
14 | function execShell (commands) {
15 |   return execSync(commands, {
16 |     encoding: 'utf8',
17 |     shell: true
18 |   }).trim()
19 | }
20 | 
21 | t.test('#--version', ct => {
22 |   ct.equal(execShell(`${dotenvx} --version`), version)
23 | 
24 |   ct.end()
25 | })
26 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/arrayToTree.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const ArrayToTree = require('../../../src/lib/helpers/arrayToTree')
 4 | 
 5 | t.test('#run', ct => {
 6 |   const arr = [
 7 |     '.env',
 8 |     'sub1/.env',
 9 |     'sub1/sub2/.env',
10 |     'sub1/sub2/sub3/.env'
11 |   ]
12 |   const tree = new ArrayToTree(arr).run()
13 | 
14 |   ct.same(tree, {
15 |     '.env': {},
16 |     sub1: {
17 |       sub2: {
18 |         sub3: {
19 |           '.env': {}
20 |         },
21 |         '.env': {}
22 |       },
23 |       '.env': {}
24 |     }
25 |   })
26 | 
27 |   ct.end()
28 | })
29 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/colorDepth.test.js:
--------------------------------------------------------------------------------
 1 | const { WriteStream } = require('tty')
 2 | const t = require('tap')
 3 | const sinon = require('sinon')
 4 | 
 5 | const depth = require('../../../src/lib/helpers/colorDepth')
 6 | 
 7 | t.beforeEach((ct) => {
 8 |   // important, clear process.env before each test
 9 |   process.env = {}
10 | })
11 | 
12 | t.test('returns WriteStream.prototype.getColorDepth()', (ct) => {
13 |   const stub = sinon.stub(WriteStream.prototype, 'getColorDepth').returns(8)
14 |   ct.equal(depth.getColorDepth(), 8)
15 | 
16 |   stub.restore()
17 |   ct.end()
18 | })
19 | 
20 | t.test('falls back to process.env.TERM when getColorDepth throws an error (deno scenario)', (ct) => {
21 |   const stub = sinon.stub(WriteStream.prototype, 'getColorDepth').throws(new TypeError('Not a function'))
22 | 
23 |   process.env.TERM = 'xterm-256color'
24 | 
25 |   ct.equal(depth.getColorDepth(), 8, 'should return 8 since TERM is 256')
26 | 
27 |   stub.restore()
28 |   ct.end()
29 | })
30 | 
31 | t.test('falls back to 4 when no process.env.TERM when getColorDepth throws an error (deno scenario)', (ct) => {
32 |   const stub = sinon.stub(WriteStream.prototype, 'getColorDepth').throws(new TypeError('Not a function'))
33 | 
34 |   ct.equal(depth.getColorDepth(), 4, 'should return 4 since TERM is missing')
35 | 
36 |   stub.restore()
37 |   ct.end()
38 | })
39 | 
40 | t.test('falls back to 4 when process.env.TERM neither xterm or 256color when getColorDepth throws an error (deno scenario)', (ct) => {
41 |   process.env.TERM = 'something else'
42 | 
43 |   const stub = sinon.stub(WriteStream.prototype, 'getColorDepth').throws(new TypeError('Not a function'))
44 | 
45 |   ct.equal(depth.getColorDepth(), 4, 'should return 4 since TERM is missing')
46 | 
47 |   stub.restore()
48 |   ct.end()
49 | })
50 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/conventions.test.js:
--------------------------------------------------------------------------------
  1 | const t = require('tap')
  2 | 
  3 | const conventions = require('../../../src/lib/helpers/conventions')
  4 | 
  5 | t.beforeEach((ct) => {
  6 |   // important, clear process.env before each test
  7 |   process.env = {}
  8 | })
  9 | 
 10 | t.test('#conventions', ct => {
 11 |   const envs = conventions('nextjs')
 12 | 
 13 |   ct.same(envs, [
 14 |     { type: 'envFile', value: '.env.development.local' },
 15 |     { type: 'envFile', value: '.env.local' },
 16 |     { type: 'envFile', value: '.env.development' },
 17 |     { type: 'envFile', value: '.env' }
 18 |   ])
 19 | 
 20 |   ct.end()
 21 | })
 22 | 
 23 | t.test('#conventions flow', ct => {
 24 |   const envs = conventions('flow')
 25 | 
 26 |   ct.same(envs, [
 27 |     { type: 'envFile', value: '.env.development.local' },
 28 |     { type: 'envFile', value: '.env.development' },
 29 |     { type: 'envFile', value: '.env.local' },
 30 |     { type: 'envFile', value: '.env' },
 31 |     { type: 'envFile', value: '.env.defaults' }
 32 |   ])
 33 | 
 34 |   ct.end()
 35 | })
 36 | 
 37 | t.test('#conventions (invalid)', ct => {
 38 |   try {
 39 |     conventions('invalid')
 40 | 
 41 |     ct.fail('should have raised an error but did not')
 42 |   } catch (error) {
 43 |     const exampleError = new Error('INVALID_CONVENTION: \'invalid\'. permitted conventions: [\'nextjs\', \'flow\']')
 44 | 
 45 |     ct.same(error, exampleError)
 46 |   }
 47 | 
 48 |   ct.end()
 49 | })
 50 | 
 51 | t.test('#conventions (process.env.NODE_ENV is test)', ct => {
 52 |   process.env.NODE_ENV = 'test'
 53 | 
 54 |   const envs = conventions('nextjs')
 55 | 
 56 |   ct.same(envs, [
 57 |     { type: 'envFile', value: '.env.test.local' },
 58 |     { type: 'envFile', value: '.env.test' },
 59 |     { type: 'envFile', value: '.env' }
 60 |   ])
 61 | 
 62 |   ct.end()
 63 | })
 64 | 
 65 | t.test('#conventions (process.env.DOTENV_ENV is test)', ct => {
 66 |   process.env.DOTENV_ENV = 'test'
 67 | 
 68 |   const envs = conventions('nextjs')
 69 | 
 70 |   ct.same(envs, [
 71 |     { type: 'envFile', value: '.env.test.local' },
 72 |     { type: 'envFile', value: '.env.test' },
 73 |     { type: 'envFile', value: '.env' }
 74 |   ])
 75 | 
 76 |   ct.end()
 77 | })
 78 | 
 79 | t.test('#conventions flow (process.env.NODE_ENV is test)', ct => {
 80 |   process.env.NODE_ENV = 'test'
 81 | 
 82 |   const envs = conventions('flow')
 83 | 
 84 |   ct.same(envs, [
 85 |     { type: 'envFile', value: '.env.test.local' },
 86 |     { type: 'envFile', value: '.env.test' },
 87 |     { type: 'envFile', value: '.env.local' },
 88 |     { type: 'envFile', value: '.env' },
 89 |     { type: 'envFile', value: '.env.defaults' }
 90 |   ])
 91 | 
 92 |   ct.end()
 93 | })
 94 | 
 95 | t.test('#conventions (process.env.NODE_ENV is unrecognized)', ct => {
 96 |   process.env.NODE_ENV = 'unrecognized'
 97 | 
 98 |   const envs = conventions('nextjs')
 99 | 
100 |   ct.same(envs, [
101 |     { type: 'envFile', value: '.env.local' },
102 |     { type: 'envFile', value: '.env' }
103 |   ])
104 | 
105 |   ct.end()
106 | })
107 | 
108 | t.test('#conventions flow (process.env.NODE_ENV is unrecognized)', ct => {
109 |   process.env.NODE_ENV = 'unrecognized'
110 | 
111 |   const envs = conventions('flow')
112 | 
113 |   ct.same(envs, [
114 |     { type: 'envFile', value: '.env.unrecognized.local' },
115 |     { type: 'envFile', value: '.env.unrecognized' },
116 |     { type: 'envFile', value: '.env.local' },
117 |     { type: 'envFile', value: '.env' },
118 |     { type: 'envFile', value: '.env.defaults' }
119 |   ])
120 | 
121 |   ct.end()
122 | })
123 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/decrypt.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | const dotenv = require('dotenv')
 4 | 
 5 | const decrypt = require('../../../src/lib/helpers/decrypt')
 6 | const encrypt = require('../../../src/lib/helpers/encrypt')
 7 | 
 8 | t.test('#decrypt', ct => {
 9 |   const dotenvKey = 'dotenv://:key_ac300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development'
10 | 
11 |   const ciphertext = encrypt('HELLO=World', dotenvKey)
12 |   const decrypted = decrypt(ciphertext, dotenvKey)
13 | 
14 |   ct.same(decrypted, 'HELLO=World')
15 | 
16 |   ct.end()
17 | })
18 | 
19 | t.test('#decrypt (DECRYPTION_FAILED)', ct => {
20 |   const dotenvKey = 'dotenv://:key_ac300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development'
21 | 
22 |   const ciphertext = encrypt('HELLO=World', dotenvKey)
23 | 
24 |   const badDotenvKey = 'dotenv://:key_bc300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development'
25 | 
26 |   try {
27 |     decrypt(ciphertext, badDotenvKey)
28 |     ct.fail('should have raised an error but did not')
29 |   } catch (error) {
30 |     ct.same(error.code, 'DECRYPTION_FAILED')
31 |     ct.same(error.message, '[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.')
32 |     ct.same(error.help, '[DECRYPTION_FAILED] Run with debug flag [dotenvx run --debug -- yourcommand] or manually run [echo $DOTENV_KEY] to compare it to the one in .env.keys.')
33 |     ct.same(error.debug, '[DECRYPTION_FAILED] DOTENV_KEY is dotenv://:key_bc300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development')
34 |   }
35 | 
36 |   ct.end()
37 | })
38 | 
39 | t.test('#decrypt (INVALID_CIPHERTEXT)', ct => {
40 |   const dotenvKey = 'dotenv://:key_ac300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development'
41 |   const ciphertext = 'abc'
42 | 
43 |   try {
44 |     decrypt(ciphertext, dotenvKey)
45 |     ct.fail('should have raised an error but did not')
46 |   } catch (error) {
47 |     ct.same(error.code, 'INVALID_CIPHERTEXT')
48 |     ct.same(error.message, '[INVALID_CIPHERTEXT] Unable to decrypt what appears to be invalid ciphertext.')
49 |     ct.same(error.help, '[INVALID_CIPHERTEXT] Run with debug flag [dotenvx run --debug -- yourcommand] or manually check .env.vault.')
50 |     ct.same(error.debug, '[INVALID_CIPHERTEXT] ciphertext is \'abc\'')
51 |   }
52 | 
53 |   ct.end()
54 | })
55 | 
56 | t.test('#decrypt (other error)', ct => {
57 |   const dotenvKey = 'dotenv://:key_ac300a21c59058c422c18dba8dc9892a537a63e156af14b5c5ef14810dc71f20@dotenvx.com/vault/.env.vault?environment=development'
58 | 
59 |   const ciphertext = encrypt('HELLO=World', dotenvKey)
60 | 
61 |   const decryptStub = sinon.stub(dotenv, 'decrypt').throws(new Error('other error'))
62 | 
63 |   try {
64 |     decrypt(ciphertext, dotenvKey)
65 |     ct.fail('should have raised an error but did not')
66 |   } catch (error) {
67 |     ct.same(error.message, 'other error')
68 |   }
69 | 
70 |   decryptStub.restore()
71 | 
72 |   ct.end()
73 | })
74 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/detectEncoding.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const path = require('path')
 3 | 
 4 | const detectEncoding = require('../../../src/lib/helpers/detectEncoding')
 5 | 
 6 | t.test('#detectEncoding utf8', ct => {
 7 |   const filepath = path.resolve(__dirname, '../../', '.env')
 8 | 
 9 |   ct.equal(detectEncoding(filepath), 'utf8', 'Correctly detected utf8 encoding')
10 | 
11 |   ct.end()
12 | })
13 | 
14 | t.test('#detectEncoding utf16le', ct => {
15 |   const filepath = path.resolve(__dirname, '../../', '.env.utf16le')
16 | 
17 |   ct.equal(detectEncoding(filepath), 'utf16le', 'Correctly detected utf16le encoding')
18 | 
19 |   ct.end()
20 | })
21 | 
22 | t.test('#detectEncoding fallback utf8 (latin1)', ct => {
23 |   const filepath = path.resolve(__dirname, '../../', '.env.latin1')
24 | 
25 |   ct.equal(detectEncoding(filepath), 'utf8', 'Correctly fellback to utf8 encoding')
26 | 
27 |   ct.end()
28 | })
29 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/encryptValue.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const publicKey = '02b106c30579baf896ae1fddf077cbcb4fef5e7d457932974878dcb51f42b45498'
 4 | const privateKey = '1fc1cafa954a7a2bf0a6fbff46189c9e03e3a66b4d1133108ab9fcdb9e154b70'
 5 | 
 6 | const encryptValue = require('../../../src/lib/helpers/encryptValue')
 7 | const decryptKeyValue = require('../../../src/lib/helpers/decryptKeyValue')
 8 | 
 9 | t.test('#encryptValue', ct => {
10 |   const result = encryptValue('hello', publicKey)
11 |   ct.ok(result.startsWith('encrypted:'))
12 | 
13 |   const decrypted = decryptKeyValue('KEY', result, 'DOTENV_PRIVATE_KEY', privateKey)
14 |   ct.same(decrypted, 'hello')
15 | 
16 |   ct.end()
17 | })
18 | 
19 | t.test('#encryptValue - implicit newlines', ct => {
20 |   const value = `line 1
21 | line 2
22 | line 3`
23 | 
24 |   const result = encryptValue(value, publicKey)
25 |   ct.ok(result.startsWith('encrypted:'))
26 | 
27 |   const decrypted = decryptKeyValue('KEY', result, 'DOTENV_PRIVATE_KEY', privateKey)
28 |   ct.same(decrypted, value)
29 | 
30 |   ct.end()
31 | })
32 | 
33 | t.test('#encryptValue - explicit newlines', ct => {
34 |   const value = 'line 1\nline 2\nline 3'
35 | 
36 |   const result = encryptValue(value, publicKey)
37 |   ct.ok(result.startsWith('encrypted:'))
38 | 
39 |   const decrypted = decryptKeyValue('KEY', result, 'DOTENV_PRIVATE_KEY', privateKey)
40 |   ct.same(decrypted, value)
41 | 
42 |   ct.end()
43 | })
44 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/errors.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const Errors = require('../../../src/lib/helpers/errors')
 4 | 
 5 | t.test('#errors', ct => {
 6 |   const result = new Errors({ message: 'hi' }).dangerousDependencyHoist()
 7 | 
 8 |   t.equal(result.code, 'DANGEROUS_DEPENDENCY_HOIST')
 9 |   t.equal(result.message, '[DANGEROUS_DEPENDENCY_HOIST] your environment has hoisted an incompatible version of a dotenvx dependency: hi')
10 |   t.equal(result.help, '[DANGEROUS_DEPENDENCY_HOIST] https://github.com/dotenvx/dotenvx/issues/622')
11 | 
12 |   ct.end()
13 | })
14 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/executeDynamic.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | const childProcess = require('child_process')
 4 | 
 5 | const { logger } = require('../../../src/shared/logger')
 6 | 
 7 | const executeDynamic = require('../../../src/lib/helpers/executeDynamic')
 8 | 
 9 | const program = {
10 |   outputHelp: sinon.stub()
11 | }
12 | 
13 | t.beforeEach((ct) => {
14 |   sinon.restore()
15 | })
16 | 
17 | t.test('executeDynamic - no command', ct => {
18 |   const processExitStub = sinon.stub(process, 'exit')
19 | 
20 |   executeDynamic(program, undefined, [])
21 | 
22 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
23 | 
24 |   ct.end()
25 | })
26 | 
27 | t.test('executeDynamic - pro command missing', ct => {
28 |   const spawnSyncStub = sinon.stub(childProcess, 'spawnSync')
29 |   const mockResult = {
30 |     status: 1,
31 |     error: new Error('Mock Error')
32 |   }
33 |   spawnSyncStub.returns(mockResult)
34 |   const processExitStub = sinon.stub(process, 'exit')
35 |   const consoleLogStub = sinon.stub(console, 'log')
36 | 
37 |   executeDynamic(program, 'pro', ['pro'])
38 | 
39 |   ct.ok(spawnSyncStub.called, 'spawnSync')
40 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
41 |   ct.ok(consoleLogStub.called, 'console.log')
42 | 
43 |   ct.end()
44 | })
45 | 
46 | t.test('executeDynamic - other command missing', ct => {
47 |   const spawnSyncStub = sinon.stub(childProcess, 'spawnSync')
48 |   const mockResult = {
49 |     status: 1,
50 |     error: new Error('Mock Error')
51 |   }
52 |   spawnSyncStub.returns(mockResult)
53 |   const processExitStub = sinon.stub(process, 'exit')
54 |   const loggerHelpStub = sinon.stub(logger, 'help')
55 |   const loggerWarnStub = sinon.stub(logger, 'warn')
56 |   const loggerInfoStub = sinon.stub(logger, 'info')
57 | 
58 |   executeDynamic(program, 'other', ['other'])
59 | 
60 |   ct.ok(spawnSyncStub.called, 'spawnSync')
61 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
62 |   ct.ok(loggerWarnStub.notCalled, 'warn')
63 |   ct.ok(loggerHelpStub.notCalled, 'help')
64 |   ct.ok(loggerInfoStub.calledWith('error: unknown command \'other\''), 'info')
65 | 
66 |   ct.end()
67 | })
68 | 
69 | t.test('executeDynamic - pro found', ct => {
70 |   const spawnSyncStub = sinon.stub(childProcess, 'spawnSync')
71 |   const mockResult = {
72 |     status: 0
73 |   }
74 |   spawnSyncStub.returns(mockResult)
75 |   const processExitStub = sinon.stub(process, 'exit')
76 |   const loggerHelpStub = sinon.stub(logger, 'help')
77 |   const loggerWarnStub = sinon.stub(logger, 'warn')
78 | 
79 |   executeDynamic(program, 'pro', ['pro'])
80 | 
81 |   ct.ok(spawnSyncStub.called, 'spawnSync')
82 |   ct.ok(processExitStub.notCalled, 'process.exit should not be called')
83 |   ct.ok(loggerWarnStub.notCalled, 'warn')
84 |   ct.ok(loggerHelpStub.notCalled, 'help')
85 | 
86 |   ct.end()
87 | })
88 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/executeExtension.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | const childProcess = require('child_process')
 4 | 
 5 | const { logger } = require('../../../src/shared/logger')
 6 | 
 7 | const executeExtension = require('../../../src/lib/helpers/executeExtension')
 8 | 
 9 | const ext = {
10 |   outputHelp: sinon.stub()
11 | }
12 | 
13 | t.beforeEach((ct) => {
14 |   sinon.restore()
15 | })
16 | 
17 | t.test('executeExtension - no command', ct => {
18 |   const processExitStub = sinon.stub(process, 'exit')
19 | 
20 |   executeExtension(ext, undefined, [])
21 | 
22 |   ct.ok(processExitStub.calledWith(0), 'process.exit should be called with code 0 to output help command')
23 | 
24 |   ct.end()
25 | })
26 | 
27 | t.test('executeExtension - vault command missing', ct => {
28 |   const spawnSyncStub = sinon.stub(childProcess, 'spawnSync')
29 |   const mockResult = {
30 |     status: 1,
31 |     error: new Error('Mock Error')
32 |   }
33 |   spawnSyncStub.returns(mockResult)
34 |   const processExitStub = sinon.stub(process, 'exit')
35 |   const loggerHelpStub = sinon.stub(logger, 'help')
36 |   const loggerWarnStub = sinon.stub(logger, 'warn')
37 | 
38 |   executeExtension(ext, 'vault', ['vault'])
39 | 
40 |   ct.ok(spawnSyncStub.called, 'spawnSync')
41 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
42 |   ct.ok(loggerWarnStub.calledWith('[INSTALLATION_NEEDED] install dotenvx-ext-vault to use [dotenvx ext vault] commands'), 'warn')
43 |   ct.ok(loggerHelpStub.calledWith('? see installation instructions [https://github.com/dotenvx/dotenvx-ext-vault]'), 'help')
44 | 
45 |   ct.end()
46 | })
47 | 
48 | t.test('executeExtension - other command missing', ct => {
49 |   const spawnSyncStub = sinon.stub(childProcess, 'spawnSync')
50 |   const mockResult = {
51 |     status: 1,
52 |     error: new Error('Mock Error')
53 |   }
54 |   spawnSyncStub.returns(mockResult)
55 |   const processExitStub = sinon.stub(process, 'exit')
56 |   const loggerHelpStub = sinon.stub(logger, 'help')
57 |   const loggerWarnStub = sinon.stub(logger, 'warn')
58 |   const loggerInfoStub = sinon.stub(logger, 'info')
59 | 
60 |   executeExtension(ext, 'other', ['other'])
61 | 
62 |   ct.ok(spawnSyncStub.called, 'spawnSync')
63 |   ct.ok(processExitStub.calledWith(1), 'process.exit should be called with code 1')
64 |   ct.ok(loggerWarnStub.notCalled, 'warn')
65 |   ct.ok(loggerHelpStub.notCalled, 'help')
66 |   ct.ok(loggerInfoStub.calledWith('error: unknown command \'other\''), 'info')
67 | 
68 |   ct.end()
69 | })
70 | 
71 | t.test('executeExtension - vault found', ct => {
72 |   const spawnSyncStub = sinon.stub(childProcess, 'spawnSync')
73 |   const mockResult = {
74 |     status: 0
75 |   }
76 |   spawnSyncStub.returns(mockResult)
77 |   const processExitStub = sinon.stub(process, 'exit')
78 |   const loggerHelpStub = sinon.stub(logger, 'help')
79 |   const loggerWarnStub = sinon.stub(logger, 'warn')
80 | 
81 |   executeExtension(ext, 'vault', ['vault'])
82 | 
83 |   ct.ok(spawnSyncStub.called, 'spawnSync')
84 |   ct.ok(processExitStub.notCalled, 'process.exit should not be called')
85 |   ct.ok(loggerWarnStub.notCalled, 'warn')
86 |   ct.ok(loggerHelpStub.notCalled, 'help')
87 | 
88 |   ct.end()
89 | })
90 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/findEnvFiles.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const fsx = require('../../../src/lib/helpers/fsx')
 3 | const sinon = require('sinon')
 4 | 
 5 | const findEnvFiles = require('../../../src/lib/helpers/findEnvFiles')
 6 | 
 7 | t.test('#findEnvFiles', ct => {
 8 |   const files = findEnvFiles('.')
 9 | 
10 |   ct.same(files, [])
11 | 
12 |   ct.end()
13 | })
14 | 
15 | t.test('#findEnvFiles (tests/monorepo/apps/frontend)', ct => {
16 |   const files = findEnvFiles('tests/monorepo/apps/frontend')
17 | 
18 |   ct.same(files, ['.env'])
19 | 
20 |   ct.end()
21 | })
22 | 
23 | t.test('#findEnvFiles (bad directory)', ct => {
24 |   try {
25 |     findEnvFiles('tests/does/not/exist')
26 |     ct.fail('should have raised an error but did not')
27 |   } catch (e) {
28 |     ct.same(e.message, 'missing directory (tests/does/not/exist)')
29 |     ct.same(e.code, 'MISSING_DIRECTORY')
30 |   }
31 | 
32 |   ct.end()
33 | })
34 | 
35 | t.test('#findEnvFiles (other error)', ct => {
36 |   const mockError = new Error('Mock Error')
37 |   mockError.code = 'other'
38 | 
39 |   const stub = sinon.stub(fsx, 'readdirSync').throws(mockError)
40 | 
41 |   try {
42 |     findEnvFiles('.')
43 |     ct.fail('should have raised an error but did not')
44 |   } catch (e) {
45 |     ct.same(e.message, 'Mock Error')
46 |   }
47 | 
48 |   stub.restore()
49 | 
50 |   ct.end()
51 | })
52 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/findPrivateKey.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const { findPrivateKey } = require('../../../src/lib/helpers/findPrivateKey')
 3 | 
 4 | t.test('#findPrivateKey', ct => {
 5 |   const envFilepath = 'tests/monorepo/apps/encrypted/.env'
 6 |   const privateKey = findPrivateKey(envFilepath)
 7 | 
 8 |   t.equal(privateKey, 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1')
 9 | 
10 |   ct.end()
11 | })
12 | 
13 | t.test('#findPrivateKey non-standard .env name (secrets.txt)', ct => {
14 |   const envFilepath = 'tests/monorepo/apps/encrypted/secrets.txt'
15 |   const privateKey = findPrivateKey(envFilepath)
16 | 
17 |   t.equal(privateKey, 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1')
18 | 
19 |   ct.end()
20 | })
21 | 
22 | t.test('#findPrivateKey non-standard .env name with no matching private key (secrets.ci.txt)', ct => {
23 |   const envFilepath = 'tests/monorepo/apps/encrypted/secrets.ci.txt'
24 |   const privateKey = findPrivateKey(envFilepath)
25 | 
26 |   t.equal(privateKey, null)
27 | 
28 |   ct.end()
29 | })
30 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/fsx.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const fs = require('fs')
 3 | const sinon = require('sinon')
 4 | 
 5 | const fsx = require('../../../src/lib/helpers/fsx')
 6 | 
 7 | let writeFileSyncStub
 8 | 
 9 | t.beforeEach((ct) => {
10 |   process.env = {}
11 |   writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
12 | })
13 | 
14 | t.afterEach((ct) => {
15 |   writeFileSyncStub.restore()
16 | })
17 | 
18 | t.test('#writeFileX', ct => {
19 |   fsx.writeFileX('tests/somefile.txt')
20 | 
21 |   t.ok(writeFileSyncStub.called, 'fs.writeFileSync() called')
22 | 
23 |   ct.end()
24 | })
25 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/guessEnvironment.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const guessEnvironment = require('../../../src/lib/helpers/guessEnvironment')
 4 | 
 5 | t.test('#guessEnvironment (.env)', ct => {
 6 |   const filepath = '.env'
 7 |   const environment = guessEnvironment(filepath)
 8 | 
 9 |   ct.same(environment, 'development')
10 | 
11 |   ct.end()
12 | })
13 | 
14 | t.test('#guessEnvironment (.env.production)', ct => {
15 |   const filepath = '.env.production'
16 |   const environment = guessEnvironment(filepath)
17 | 
18 |   ct.same(environment, 'production')
19 | 
20 |   ct.end()
21 | })
22 | 
23 | t.test('#guessEnvironment (.env.local)', (ct) => {
24 |   const filepath = '.env.local'
25 |   const environment = guessEnvironment(filepath)
26 | 
27 |   ct.same(environment, 'local')
28 | 
29 |   ct.end()
30 | })
31 | 
32 | t.test('#guessEnvironment (.env.development.local)', (ct) => {
33 |   const filepath = '.env.development.local'
34 |   const environment = guessEnvironment(filepath)
35 | 
36 |   ct.same(environment, 'development_local')
37 | 
38 |   ct.end()
39 | })
40 | 
41 | t.test('#guessEnvironment (.env.development.production)', (ct) => {
42 |   const filepath = '.env.development.production'
43 |   const environment = guessEnvironment(filepath)
44 | 
45 |   ct.same(environment, 'development_production')
46 | 
47 |   ct.end()
48 | })
49 | 
50 | t.test('#guessEnvironment (.env.some.other.thing)', (ct) => {
51 |   const filepath = '.env.some.other.thing'
52 |   const environment = guessEnvironment(filepath)
53 | 
54 |   ct.same(environment, 'some_other')
55 | 
56 |   ct.end()
57 | })
58 | 
59 | t.test('#guessEnvironment (.env1)', ct => {
60 |   const filepath = '.env1'
61 |   const environment = guessEnvironment(filepath)
62 | 
63 |   ct.same(environment, 'development1')
64 | 
65 |   ct.end()
66 | })
67 | 
68 | t.test('#guessEnvironment (secrets.txt)', ct => {
69 |   const filepath = 'secrets.txt'
70 |   const environment = guessEnvironment(filepath)
71 | 
72 |   ct.same(environment, 'secrets.txt') // for now just return the filename (might change in the future depending on user usage)
73 | 
74 |   ct.end()
75 | })
76 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/guessPrivateKeyFilename.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const guessPrivateKeyFilename = require('../../../src/lib/helpers/guessPrivateKeyFilename')
 4 | 
 5 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY)', ct => {
 6 |   const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY')
 7 | 
 8 |   ct.same(result, '.env')
 9 | 
10 |   ct.end()
11 | })
12 | 
13 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY_PRODUCTION)', ct => {
14 |   const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY_PRODUCTION')
15 | 
16 |   ct.same(result, '.env.production')
17 | 
18 |   ct.end()
19 | })
20 | 
21 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY_CI)', ct => {
22 |   const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY_CI')
23 | 
24 |   ct.same(result, '.env.ci')
25 | 
26 |   ct.end()
27 | })
28 | 
29 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL)', ct => {
30 |   const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL')
31 | 
32 |   ct.same(result, '.env.development.local')
33 | 
34 |   ct.end()
35 | })
36 | 
37 | t.test('#guessPrivateKeyFilename (DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL_ME)', ct => {
38 |   const result = guessPrivateKeyFilename('DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL_ME')
39 | 
40 |   ct.same(result, '.env.development.local.me')
41 | 
42 |   ct.end()
43 | })
44 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/guessPrivateKeyName.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const guessPrivateKeyName = require('../../../src/lib/helpers/guessPrivateKeyName')
 4 | 
 5 | t.test('#guessPrivateKeyName (.env)', ct => {
 6 |   const filepath = '.env'
 7 |   const result = guessPrivateKeyName(filepath)
 8 | 
 9 |   ct.same(result, 'DOTENV_PRIVATE_KEY')
10 | 
11 |   ct.end()
12 | })
13 | 
14 | t.test('#guessPrivateKeyName (.env)', ct => {
15 |   const filepath = 'some/path/to/.env'
16 |   const result = guessPrivateKeyName(filepath)
17 | 
18 |   ct.same(result, 'DOTENV_PRIVATE_KEY')
19 | 
20 |   ct.end()
21 | })
22 | 
23 | t.test('#guessPrivateKeyName (.env.env)', ct => {
24 |   const filepath = '.env.env'
25 |   const result = guessPrivateKeyName(filepath)
26 | 
27 |   ct.same(result, 'DOTENV_PRIVATE_KEY_ENV')
28 | 
29 |   ct.end()
30 | })
31 | 
32 | t.test('#guessPrivateKeyName (.env.production)', ct => {
33 |   const filepath = '.env.production'
34 |   const result = guessPrivateKeyName(filepath)
35 | 
36 |   ct.same(result, 'DOTENV_PRIVATE_KEY_PRODUCTION')
37 | 
38 |   ct.end()
39 | })
40 | 
41 | t.test('#guessPrivateKeyName (.env.local)', (ct) => {
42 |   const filepath = '.env.local'
43 |   const result = guessPrivateKeyName(filepath)
44 | 
45 |   ct.same(result, 'DOTENV_PRIVATE_KEY_LOCAL')
46 | 
47 |   ct.end()
48 | })
49 | 
50 | t.test('#guessPrivateKeyName (.env.development.local)', (ct) => {
51 |   const filepath = '.env.development.local'
52 |   const result = guessPrivateKeyName(filepath)
53 | 
54 |   ct.same(result, 'DOTENV_PRIVATE_KEY_DEVELOPMENT_LOCAL')
55 | 
56 |   ct.end()
57 | })
58 | 
59 | t.test('#guessPrivateKeyName (.env.development.production)', (ct) => {
60 |   const filepath = '.env.development.production'
61 |   const result = guessPrivateKeyName(filepath)
62 | 
63 |   ct.same(result, 'DOTENV_PRIVATE_KEY_DEVELOPMENT_PRODUCTION')
64 | 
65 |   ct.end()
66 | })
67 | 
68 | t.test('#guessPrivateKeyName (.env.some.other.thing)', (ct) => {
69 |   const filepath = '.env.some.other.thing'
70 |   const result = guessPrivateKeyName(filepath)
71 | 
72 |   ct.same(result, 'DOTENV_PRIVATE_KEY_SOME_OTHER')
73 | 
74 |   ct.end()
75 | })
76 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/guessPublicKeyName.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const guessPublicKeyName = require('../../../src/lib/helpers/guessPublicKeyName')
 4 | 
 5 | t.test('#guessPublicKeyName (.env)', ct => {
 6 |   const filepath = '.env'
 7 |   const result = guessPublicKeyName(filepath)
 8 | 
 9 |   ct.same(result, 'DOTENV_PUBLIC_KEY')
10 | 
11 |   ct.end()
12 | })
13 | 
14 | t.test('#guessPublicKeyName (.env)', ct => {
15 |   const filepath = 'some/path/to/.env'
16 |   const result = guessPublicKeyName(filepath)
17 | 
18 |   ct.same(result, 'DOTENV_PUBLIC_KEY')
19 | 
20 |   ct.end()
21 | })
22 | 
23 | t.test('#guessPublicKeyName (.env.env)', ct => {
24 |   const filepath = '.env.env'
25 |   const result = guessPublicKeyName(filepath)
26 | 
27 |   ct.same(result, 'DOTENV_PUBLIC_KEY_ENV')
28 | 
29 |   ct.end()
30 | })
31 | 
32 | t.test('#guessPublicKeyName (.env.production)', ct => {
33 |   const filepath = '.env.production'
34 |   const result = guessPublicKeyName(filepath)
35 | 
36 |   ct.same(result, 'DOTENV_PUBLIC_KEY_PRODUCTION')
37 | 
38 |   ct.end()
39 | })
40 | 
41 | t.test('#guessPublicKeyName (.env.local)', (ct) => {
42 |   const filepath = '.env.local'
43 |   const result = guessPublicKeyName(filepath)
44 | 
45 |   ct.same(result, 'DOTENV_PUBLIC_KEY_LOCAL')
46 | 
47 |   ct.end()
48 | })
49 | 
50 | t.test('#guessPublicKeyName (.env.development.local)', (ct) => {
51 |   const filepath = '.env.development.local'
52 |   const result = guessPublicKeyName(filepath)
53 | 
54 |   ct.same(result, 'DOTENV_PUBLIC_KEY_DEVELOPMENT_LOCAL')
55 | 
56 |   ct.end()
57 | })
58 | 
59 | t.test('#guessPublicKeyName (.env.development.production)', (ct) => {
60 |   const filepath = '.env.development.production'
61 |   const result = guessPublicKeyName(filepath)
62 | 
63 |   ct.same(result, 'DOTENV_PUBLIC_KEY_DEVELOPMENT_PRODUCTION')
64 | 
65 |   ct.end()
66 | })
67 | 
68 | t.test('#guessPublicKeyName (.env.some.other.thing)', (ct) => {
69 |   const filepath = '.env.some.other.thing'
70 |   const result = guessPublicKeyName(filepath)
71 | 
72 |   ct.same(result, 'DOTENV_PUBLIC_KEY_SOME_OTHER')
73 | 
74 |   ct.end()
75 | })
76 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/installPrecommitHook.test.js:
--------------------------------------------------------------------------------
  1 | const t = require('tap')
  2 | const fsx = require('../../../src/lib/helpers/fsx')
  3 | const sinon = require('sinon')
  4 | 
  5 | const InstallPrecommitHook = require('../../../src/lib/helpers/installPrecommitHook')
  6 | 
  7 | t.test('#run (exists and already includes dotenvx ext precommit) does nothing', ct => {
  8 |   const installPrecommitHook = new InstallPrecommitHook()
  9 | 
 10 |   const existsStub = sinon.stub(installPrecommitHook, '_exists')
 11 |   const currentHookStub = sinon.stub(installPrecommitHook, '_currentHook')
 12 | 
 13 |   existsStub.returns(true)
 14 |   currentHookStub.returns('dotenvx ext precommit')
 15 | 
 16 |   const { successMessage } = installPrecommitHook.run()
 17 | 
 18 |   ct.same(successMessage, 'dotenvx ext precommit exists [.git/hooks/pre-commit]')
 19 | 
 20 |   // restore stubs
 21 |   existsStub.restore()
 22 |   currentHookStub.restore()
 23 | 
 24 |   ct.end()
 25 | })
 26 | 
 27 | t.test('#run (exists but does not include dotenvx ext precommit) appends', ct => {
 28 |   const installPrecommitHook = new InstallPrecommitHook()
 29 | 
 30 |   const existsStub = sinon.stub(installPrecommitHook, '_exists')
 31 |   const currentHookStub = sinon.stub(installPrecommitHook, '_currentHook')
 32 |   const appendFileSyncStub = sinon.stub(fsx, 'appendFileSync')
 33 | 
 34 |   existsStub.returns(true)
 35 |   currentHookStub.returns('') // empty file
 36 | 
 37 |   const { successMessage } = installPrecommitHook.run()
 38 | 
 39 |   ct.same(successMessage, 'dotenvx ext precommit appended [.git/hooks/pre-commit]')
 40 | 
 41 |   t.ok(appendFileSyncStub.called, 'fsx.appendFileSync should be called')
 42 | 
 43 |   // restore stubs
 44 |   existsStub.restore()
 45 |   currentHookStub.restore()
 46 |   appendFileSyncStub.restore()
 47 | 
 48 |   ct.end()
 49 | })
 50 | 
 51 | t.test('#run (does not exist) creates', ct => {
 52 |   const installPrecommitHook = new InstallPrecommitHook()
 53 | 
 54 |   const existsStub = sinon.stub(installPrecommitHook, '_exists')
 55 |   const writeFileXStub = sinon.stub(fsx, 'writeFileX')
 56 |   const chmodSyncStub = sinon.stub(fsx, 'chmodSync')
 57 | 
 58 |   existsStub.returns(false)
 59 | 
 60 |   const { successMessage } = installPrecommitHook.run()
 61 | 
 62 |   ct.same(successMessage, 'dotenvx ext precommit installed [.git/hooks/pre-commit]')
 63 | 
 64 |   t.ok(writeFileXStub.called, 'fsx.writeFileX should be called')
 65 |   t.ok(chmodSyncStub.called, 'fsx.chomdSyncStub should be called')
 66 | 
 67 |   // restore stubs
 68 |   existsStub.restore()
 69 |   writeFileXStub.restore()
 70 |   chmodSyncStub.restore()
 71 | 
 72 |   ct.end()
 73 | })
 74 | 
 75 | t.test('#run (fs throws an error) logs error', ct => {
 76 |   const installPrecommitHook = new InstallPrecommitHook()
 77 | 
 78 |   const existsStub = sinon.stub(installPrecommitHook, '_exists')
 79 |   const writeFileXStub = sinon.stub(fsx, 'writeFileX').throws(new Error('Mock Error'))
 80 | 
 81 |   existsStub.returns(false)
 82 | 
 83 |   try {
 84 |     installPrecommitHook.run()
 85 |     ct.fail('should have raised an error but did not')
 86 |   } catch (error) {
 87 |     ct.same(error.message, 'failed to modify pre-commit hook: Mock Error')
 88 |   }
 89 | 
 90 |   // restore stubs
 91 |   existsStub.restore()
 92 |   writeFileXStub.restore()
 93 | 
 94 |   ct.end()
 95 | })
 96 | 
 97 | t.test('#_exists true/false', ct => {
 98 |   const installPrecommitHook = new InstallPrecommitHook()
 99 | 
100 |   const existsSyncStub = sinon.stub(fsx, 'existsSync')
101 | 
102 |   existsSyncStub.returns(false)
103 |   let result = installPrecommitHook._exists()
104 |   ct.equal(result, false)
105 | 
106 |   existsSyncStub.returns(true)
107 |   result = installPrecommitHook._exists()
108 |   ct.equal(result, true)
109 | 
110 |   existsSyncStub.restore()
111 | 
112 |   ct.end()
113 | })
114 | 
115 | t.test('#_currentHook', ct => {
116 |   const installPrecommitHook = new InstallPrecommitHook()
117 | 
118 |   const readFileXStub = sinon.stub(fsx, 'readFileX')
119 | 
120 |   readFileXStub.returns('some file')
121 |   const result = installPrecommitHook._currentHook()
122 |   ct.equal(result, 'some file')
123 | 
124 |   readFileXStub.restore()
125 | 
126 |   ct.end()
127 | })
128 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/isEncrypted.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const isEncrypted = require('../../../src/lib/helpers/isEncrypted')
 3 | 
 4 | t.test('#isEncrypted', ct => {
 5 |   const result = isEncrypted('encrypted:1234')
 6 |   ct.same(result, true)
 7 |   ct.end()
 8 | })
 9 | 
10 | t.test('#isEncrypted real world', ct => {
11 |   const result = isEncrypted('encrypted:BHqJQhgpCHY4My3PQ1UE3vSTyfqM/wNaISWSVQgi8eBQze4X7AMkcl0tg4skow5vI7Akhm0UXV43+FeYOvxcXifjjKbHZeXp+hFxKk5zu/N3tB95DiCpXSLA2bbcyeTNLAfTZgXa')
12 |   ct.same(result, true)
13 |   ct.end()
14 | })
15 | 
16 | t.test('#isEncrypted not encrypted', ct => {
17 |   const result = isEncrypted('1234')
18 |   ct.same(result, false)
19 |   ct.end()
20 | })
21 | 
22 | t.test('#isEncrypted passes null', ct => {
23 |   const result = isEncrypted(null)
24 |   ct.same(result, false)
25 |   ct.end()
26 | })
27 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/isFullyEncrypted.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const isFullyEncrypted = require('../../../src/lib/helpers/isFullyEncrypted')
 3 | 
 4 | t.test('#isFullyEncrypted - All values are encrypted', ct => {
 5 |   const str = `
 6 |     # .env
 7 |     HELLO=encrypted:1234
 8 |   `.trim()
 9 | 
10 |   const result = isFullyEncrypted(str)
11 |   ct.same(result, true, 'All values are encrypted')
12 |   ct.end()
13 | })
14 | 
15 | t.test('#isFullyEncrypted - No values are encrypted', ct => {
16 |   const str = `
17 |     # .env
18 |     HELLO=World
19 |   `.trim()
20 | 
21 |   const result = isFullyEncrypted(str)
22 |   ct.same(result, false, 'No values are encrypted')
23 |   ct.end()
24 | })
25 | 
26 | t.test('#isFullyEncrypted - partially encrypted and partially unencrypted', ct => {
27 |   const str = `
28 |     # .env
29 |     HELLO=unencrypted
30 |     HELLO=encrypted:1234
31 |   `.trim()
32 | 
33 |   const result = isFullyEncrypted(str)
34 |   ct.same(result, false, 'Some values are unencrypted')
35 |   ct.end()
36 | })
37 | 
38 | t.test('#isFullyEncrypted - Encrypted values with DOTENV_PUBLIC_KEY', ct => {
39 |   const str = `
40 |     #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
41 |     #/            public-key encryption for .env files          /
42 |     #/       [how it works](https://dotenvx.com/encryption)     /
43 |     #/----------------------------------------------------------/
44 |     DOTENV_PUBLIC_KEY="0395dc734661dfd7a2d6581cd2c8864038028c2570f6586771534525767341d1b2"
45 | 
46 |     # .env
47 |     HELLO="encrypted:BFYjx93CX4d4OIRzYMR+ZT+JR92kCfOJSsivsXxwaQHvA5FJgHa50rUHWhj1t72LLeRkLE2v4GrKpW5w1bjinXEmXtAV28k2audEVW6cBU7YapLVcPvrV2FkNqbMEKRvp78C0wKaMvNarg=="
48 |   `.trim()
49 | 
50 |   const result = isFullyEncrypted(str)
51 |   ct.same(result, true, 'Encrypted values with DOTENV_PUBLIC_KEY')
52 |   ct.end()
53 | })
54 | 
55 | t.test('#isFullyEncrypted - Keys starting with DOTENV_PUBLIC_KEY are considered valid', ct => {
56 |   const str = `
57 |     # .env
58 |     DOTENV_PUBLIC_KEY_ENVIRONMENT="somevalue"
59 |     HELLO="encrypted:BFYjx93CX4d4OIRzYMR+ZT+JR92kCfOJSsivsXxwaQHvA5FJgHa50rUHWhj1t72LLeRkLE2v4GrKpW5w1bjinXEmXtAV28k2audEVW6cBU7YapLVcPvrV2FkNqbMEKRvp78C0wKaMvNarg=="
60 |   `.trim()
61 | 
62 |   const result = isFullyEncrypted(str)
63 |   ct.same(result, true, 'Keys starting with DOTENV_PUBLIC_KEY are considered valid')
64 |   ct.end()
65 | })
66 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/isIgnoringDotenvKeys.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const fsx = require('../../../src/lib/helpers/fsx')
 3 | const sinon = require('sinon')
 4 | 
 5 | const isIgnoringDotenvKeys = require('../../../src/lib/helpers/isIgnoringDotenvKeys')
 6 | 
 7 | t.test('#isIgnoringDotenvKeys - no .gitignore file', ct => {
 8 |   const existsSyncStub = sinon.stub(fsx, 'existsSync')
 9 |   existsSyncStub.returns(false)
10 | 
11 |   const result = isIgnoringDotenvKeys()
12 |   ct.same(result, false)
13 | 
14 |   existsSyncStub.restore()
15 | 
16 |   ct.end()
17 | })
18 | 
19 | t.test('#isIgnoringDotenvKeys - empty .gitignore file', ct => {
20 |   const existsSyncStub = sinon.stub(fsx, 'existsSync')
21 |   existsSyncStub.returns(true)
22 |   const readFileXStub = sinon.stub(fsx, 'readFileX')
23 |   readFileXStub.returns('')
24 | 
25 |   const result = isIgnoringDotenvKeys()
26 | 
27 |   ct.same(result, false)
28 | 
29 |   existsSyncStub.restore()
30 |   readFileXStub.restore()
31 | 
32 |   ct.end()
33 | })
34 | 
35 | t.test('#isIgnoringDotenvKeys - .gitignore file ignores .env*', ct => {
36 |   const existsSyncStub = sinon.stub(fsx, 'existsSync')
37 |   existsSyncStub.returns(true)
38 |   const readFileXStub = sinon.stub(fsx, 'readFileX')
39 |   readFileXStub.returns('.env*')
40 | 
41 |   const result = isIgnoringDotenvKeys()
42 | 
43 |   ct.same(result, true)
44 | 
45 |   existsSyncStub.restore()
46 |   readFileXStub.restore()
47 | 
48 |   ct.end()
49 | })
50 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/isPublicKey.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const isPublicKey = require('../../../src/lib/helpers/isPublicKey')
 3 | 
 4 | t.test('#isPublicKey not encrypted but DOTENV_PUBLIC_KEY', ct => {
 5 |   const result = isPublicKey('DOTENV_PUBLIC_KEY', '1234')
 6 |   ct.same(result, true)
 7 |   ct.end()
 8 | })
 9 | 
10 | t.test('#isPublicKey not encrypted but DOTENV_PUBLIC_KEY_PRODUCTION', ct => {
11 |   const result = isPublicKey('DOTENV_PUBLIC_KEY_PRODUCTION', '1234')
12 |   ct.same(result, true)
13 |   ct.end()
14 | })
15 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/keypair.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const keypair = require('../../../src/lib/helpers/keypair')
 3 | 
 4 | t.test('#keypair', ct => {
 5 |   const { publicKey, privateKey } = keypair()
 6 | 
 7 |   t.ok(publicKey, 'Public key should be defined')
 8 |   t.ok(privateKey, 'Private key should be defined')
 9 |   t.equal(publicKey.length, 66, 'Public key should be 66 characters long')
10 |   t.equal(privateKey.length, 64, 'Private key should be 64 characters long')
11 | 
12 |   ct.end()
13 | })
14 | 
15 | t.test('keypair uses provided private key to generate public key', (t) => {
16 |   const existingPrivateKey = '4c06b1f9ffc4af11d0d206fd43f28bc96b68647158c1666edc4832f19857cef9'
17 |   const { publicKey, privateKey } = keypair(existingPrivateKey)
18 | 
19 |   t.equal(privateKey, existingPrivateKey, 'Private key should match the provided private key')
20 |   t.ok(publicKey, 'Public key should be defined')
21 |   t.equal(publicKey.length, 66, 'Public key should be 66 characters long')
22 | 
23 |   // Generate the public key from the provided private key for comparison
24 |   const { PrivateKey } = require('eciesjs')
25 |   const kp = new PrivateKey(Buffer.from(existingPrivateKey, 'hex'))
26 |   const expectedPublicKey = kp.publicKey.toHex()
27 | 
28 |   t.equal(publicKey, expectedPublicKey, 'Public key should match the expected public key generated from the provided private key')
29 | 
30 |   t.end()
31 | })
32 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/parseEncryptionKeyFromDotenvKey.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const parseEncryptionKeyFromDotenvKey = require('../../../src/lib/helpers/parseEncryptionKeyFromDotenvKey')
 4 | 
 5 | t.test('#parseEncryptionKeyFromDotenvKey', ct => {
 6 |   const dotenvKey = 'dotenv://:key_e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34@dotenvx.com/vault/.env.vault?environment=other'
 7 | 
 8 |   const key = parseEncryptionKeyFromDotenvKey(dotenvKey) // buffer hex
 9 |   const expected = Buffer.from('e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34', 'hex')
10 | 
11 |   ct.same(key, expected)
12 | 
13 |   ct.end()
14 | })
15 | 
16 | t.test('#parseEncryptionKeyFromDotenvKey (not url parseable)', ct => {
17 |   const dotenvKey = 'e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34'
18 | 
19 |   try {
20 |     parseEncryptionKeyFromDotenvKey(dotenvKey) // buffer hex
21 | 
22 |     ct.fail('should have raised an error but did not')
23 |   } catch (error) {
24 |     const exampleError = new Error('INVALID_DOTENV_KEY: Incomplete format. It should be a dotenv uri. (dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development)')
25 | 
26 |     ct.same(error, exampleError)
27 |   }
28 | 
29 |   ct.end()
30 | })
31 | 
32 | t.test('#parseEncryptionKeyFromDotenvKey (missing key/password part)', ct => {
33 |   const dotenvKey = 'dotenv://:@dotenvx.com/vault/.env.vault?environment=other'
34 | 
35 |   try {
36 |     parseEncryptionKeyFromDotenvKey(dotenvKey) // buffer hex
37 | 
38 |     ct.fail('should have raised an error but did not')
39 |   } catch (error) {
40 |     const exampleError = new Error('INVALID_DOTENV_KEY: Missing key part')
41 | 
42 |     ct.same(error, exampleError)
43 |   }
44 | 
45 |   ct.end()
46 | })
47 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/parseEnvironmentFromDotenvKey.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const parseEnvironmentFromDotenvKey = require('../../../src/lib/helpers/parseEnvironmentFromDotenvKey')
 4 | 
 5 | t.test('#parseEnvironmentFromDotenvKey', ct => {
 6 |   const dotenvKey = 'dotenv://:key_e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34@dotenvx.com/vault/.env.vault?environment=development'
 7 | 
 8 |   const environment = parseEnvironmentFromDotenvKey(dotenvKey)
 9 | 
10 |   ct.same(environment, 'development')
11 | 
12 |   ct.end()
13 | })
14 | 
15 | t.test('#parseEnvironmentFromDotenvKey (not url parseable)', ct => {
16 |   const dotenvKey = 'e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34'
17 | 
18 |   try {
19 |     parseEnvironmentFromDotenvKey(dotenvKey)
20 | 
21 |     ct.fail('should have raised an error but did not')
22 |   } catch (error) {
23 |     const exampleError = new Error('INVALID_DOTENV_KEY: Invalid URL')
24 | 
25 |     ct.same(error, exampleError)
26 |   }
27 | 
28 |   ct.end()
29 | })
30 | 
31 | t.test('#parseEnvironmentFromDotenvKey (missing environment part)', ct => {
32 |   const dotenvKey = 'dotenv://:key_e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34@dotenvx.com/vault/.env.vault?environment='
33 | 
34 |   try {
35 |     parseEnvironmentFromDotenvKey(dotenvKey)
36 | 
37 |     ct.fail('should have raised an error but did not')
38 |   } catch (error) {
39 |     const exampleError = new Error('INVALID_DOTENV_KEY: Missing environment part')
40 | 
41 |     ct.same(error, exampleError)
42 |   }
43 | 
44 |   ct.end()
45 | })
46 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/pluralize.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const pluralize = require('../../../src/lib/helpers/pluralize')
 4 | 
 5 | t.test('#pluralize', ct => {
 6 |   const result0 = pluralize('world', 0)
 7 |   const result1 = pluralize('world', 1)
 8 |   const result2 = pluralize('world', 2)
 9 | 
10 |   ct.same(result0, 'worlds')
11 |   ct.same(result1, 'world')
12 |   ct.same(result2, 'worlds')
13 | 
14 |   ct.end()
15 | })
16 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/proKeypair.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | const childProcess = require('child_process')
 4 | 
 5 | const ProKeypair = require('../../../src/lib/helpers/proKeypair')
 6 | 
 7 | t.test('#proKeypair', ct => {
 8 |   const keypairs = new ProKeypair('tests/monorepo/apps/app1/.env').run()
 9 | 
10 |   ct.same(keypairs, { DOTENV_PUBLIC_KEY: null, DOTENV_PRIVATE_KEY: null })
11 | 
12 |   ct.end()
13 | })
14 | 
15 | t.test('#proKeypair when childProcess fails', ct => {
16 |   const stub = sinon.stub(childProcess, 'execSync').throws(new Error('Command failed'))
17 | 
18 |   const keypairs = new ProKeypair('tests/monorepo/apps/app1/.env').run()
19 | 
20 |   ct.same(keypairs, { DOTENV_PUBLIC_KEY: null, DOTENV_PRIVATE_KEY: null })
21 | 
22 |   stub.restore()
23 | 
24 |   ct.end()
25 | })
26 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/removeDynamicHelpSection.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const removeDynamicHelpSection = require('../../../src/lib/helpers/removeDynamicHelpSection')
 4 | 
 5 | t.test('#removeDynamicHelpSection', ct => {
 6 |   const lines = [
 7 |     'Usage: dotenvx [options] [command] [command] [args...]',
 8 |     '',
 9 |     'a better dotenv–from the creator of `dotenv`',
10 |     '',
11 |     'Arguments:',
12 |     '  command                      dynamic command',
13 |     '  args                         dynamic command arguments',
14 |     '',
15 |     'Options:'
16 |   ]
17 | 
18 |   removeDynamicHelpSection(lines)
19 | 
20 |   ct.same(lines, [
21 |     'Usage: dotenvx [options] [command] [command] [args...]',
22 |     '',
23 |     'a better dotenv–from the creator of `dotenv`',
24 |     '',
25 |     'Options:'
26 |   ])
27 | 
28 |   ct.end()
29 | })
30 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/removeOptionsHelpParts.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const removeOptionsHelpParts = require('../../../src/lib/helpers/removeOptionsHelpParts')
 4 | 
 5 | t.test('#removeOptionsHelpParts', ct => {
 6 |   const lines = [
 7 |     'set [options] <KEY> <value>  set a single environment variable'
 8 |   ]
 9 | 
10 |   removeOptionsHelpParts(lines)
11 | 
12 |   ct.same(lines, [
13 |     'set <KEY> <value>  set a single environment variable'
14 |   ])
15 | 
16 |   ct.end()
17 | })
18 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/smartDotenvPrivateKey.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const smartDotenvPrivateKey = require('../../../src/lib/helpers/smartDotenvPrivateKey')
 4 | 
 5 | t.beforeEach((ct) => {
 6 |   // important, clear process.env before each test
 7 |   process.env = {}
 8 | })
 9 | 
10 | let filepath = '.env'
11 | 
12 | t.test('#smartDotenvPrivateKey', ct => {
13 |   const result = smartDotenvPrivateKey(filepath)
14 | 
15 |   ct.same(result, null)
16 | 
17 |   ct.end()
18 | })
19 | 
20 | t.test('#smartDotenvPrivateKey when process.env.DOTENV_PRIVATE_KEY is set', ct => {
21 |   process.env.DOTENV_PRIVATE_KEY = '<privateKey>'
22 | 
23 |   const result = smartDotenvPrivateKey(filepath)
24 | 
25 |   ct.same(result, '<privateKey>')
26 | 
27 |   ct.end()
28 | })
29 | 
30 | t.test('#smartDotenvPrivateKey when process.env.DOTENV_PRIVATE_KEY is set but it is an empty string', ct => {
31 |   process.env.DOTENV_PRIVATE_KEY = ''
32 | 
33 |   const result = smartDotenvPrivateKey(filepath)
34 | 
35 |   ct.same(result, null)
36 | 
37 |   ct.end()
38 | })
39 | 
40 | t.test('#smartDotenvPrivateKey when .env.keys present', ct => {
41 |   filepath = 'tests/monorepo/apps/encrypted/.env'
42 | 
43 |   const result = smartDotenvPrivateKey(filepath)
44 | 
45 |   ct.same(result, 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1')
46 | 
47 |   ct.end()
48 | })
49 | 
50 | t.test('#smartDotenvPrivateKey when .env.keys present but filename is not .env (use invertse of DOTENV_PUBLIC_KEY* in the filename (if exists))', ct => {
51 |   filepath = 'tests/monorepo/apps/encrypted/secrets.txt'
52 | 
53 |   const result = smartDotenvPrivateKey(filepath)
54 | 
55 |   ct.same(result, 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1')
56 | 
57 |   ct.end()
58 | })
59 | 
60 | t.test('#smartDotenvPrivateKey when DOTENV_PRIVATE_KEY passed and custom filename', ct => {
61 |   process.env.DOTENV_PRIVATE_KEY = '<privateKey>'
62 | 
63 |   filepath = 'tests/monorepo/apps/encrypted/secrets.txt'
64 | 
65 |   const result = smartDotenvPrivateKey(filepath)
66 | 
67 |   ct.same(result, '<privateKey>')
68 | 
69 |   ct.end()
70 | })
71 | 
72 | t.test('#smartDotenvPrivateKey when DOTENV_PRIVATE_KEY passed and custom filename but custom filename has a different named DOTENV_PUBLIC_KEY', ct => {
73 |   process.env.DOTENV_PRIVATE_KEY = '<privateKey>'
74 | 
75 |   filepath = 'tests/monorepo/apps/encrypted/secrets.ci.txt'
76 | 
77 |   const result = smartDotenvPrivateKey(filepath)
78 |   ct.same(result, null) // it should not find it because it is instead looking for a DOTENV_PRIVATE_KEY_CI (see secrets.ci.txt contents)
79 | 
80 |   // matching ci key is set - inverse of the ci public key in the secrets.ci.txt file
81 |   process.env.DOTENV_PRIVATE_KEY_CI = '<privateKeyCi>'
82 |   const result2 = smartDotenvPrivateKey(filepath)
83 |   ct.same(result2, '<privateKeyCi>')
84 | 
85 |   // an additional random key is set but still with the dotenv private key schema
86 |   process.env.DOTENV_PRIVATE_KEY_PRODUCTION = '<privateKeyProduction>'
87 |   const result3 = smartDotenvPrivateKey(filepath)
88 |   ct.same(result3, '<privateKeyCi>') // it should still find the CI one first
89 | 
90 |   ct.end()
91 | })
92 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/smartDotenvPublicKey.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const smartDotenvPublicKey = require('../../../src/lib/helpers/smartDotenvPublicKey')
 4 | 
 5 | t.beforeEach((ct) => {
 6 |   // important, clear process.env before each test
 7 |   process.env = {}
 8 | })
 9 | 
10 | let filepath = '.env'
11 | 
12 | t.test('#smartDotenvPublicKey', ct => {
13 |   const result = smartDotenvPublicKey(filepath)
14 | 
15 |   ct.same(result, null)
16 | 
17 |   ct.end()
18 | })
19 | 
20 | t.test('#smartDotenvPublicKey when process.env.DOTENV_PUBLIC_KEY is set', ct => {
21 |   process.env.DOTENV_PUBLIC_KEY = '<publicKey>'
22 | 
23 |   const result = smartDotenvPublicKey(filepath)
24 | 
25 |   ct.same(result, '<publicKey>')
26 | 
27 |   ct.end()
28 | })
29 | 
30 | t.test('#smartDotenvPublicKey when process.env.DOTENV_PUBLIC_KEY is set but it is an empty string', ct => {
31 |   process.env.DOTENV_PUBLIC_KEY = ''
32 | 
33 |   const result = smartDotenvPublicKey(filepath)
34 | 
35 |   ct.same(result, null)
36 | 
37 |   ct.end()
38 | })
39 | 
40 | t.test('#smartDotenvPublicKey when .env.keys present', ct => {
41 |   filepath = 'tests/monorepo/apps/encrypted/.env'
42 | 
43 |   const result = smartDotenvPublicKey(filepath)
44 | 
45 |   ct.same(result, '03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba')
46 | 
47 |   ct.end()
48 | })
49 | 


--------------------------------------------------------------------------------
/tests/lib/helpers/truncate.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | 
 3 | const truncate = require('../../../src/lib/helpers/truncate')
 4 | 
 5 | t.test('#truncate', ct => {
 6 |   const privateKey = '2c93601cba85b3b2474817897826ebef977415c097f0bf57dcbaa3056e5d64d0'
 7 | 
 8 |   const result = truncate(privateKey)
 9 | 
10 |   t.equal(result, '2c93601…')
11 | 
12 |   ct.end()
13 | })
14 | 
15 | t.test('#truncate - 11 characters', ct => {
16 |   const privateKey = 'dxo_123456789'
17 | 
18 |   const result = truncate(privateKey, 11)
19 | 
20 |   t.equal(result, 'dxo_1234567…')
21 | 
22 |   ct.end()
23 | })
24 | 
25 | t.test('#truncate - null privateKey', ct => {
26 |   const privateKey = null
27 | 
28 |   const result = truncate(privateKey)
29 | 
30 |   t.equal(result, '')
31 | 
32 |   ct.end()
33 | })
34 | 


--------------------------------------------------------------------------------
/tests/lib/services/genexample.test.js:
--------------------------------------------------------------------------------
  1 | const t = require('tap')
  2 | const fsx = require('../../../src/lib/helpers/fsx')
  3 | const path = require('path')
  4 | const sinon = require('sinon')
  5 | 
  6 | const Genexample = require('../../../src/lib/services/genexample')
  7 | 
  8 | t.test('#run', ct => {
  9 |   const genexample = new Genexample('tests/monorepo/apps/frontend')
 10 | 
 11 |   const {
 12 |     envExampleFile,
 13 |     injected,
 14 |     preExisted
 15 |   } = genexample.run()
 16 | 
 17 |   const output = `# .env.example - generated with dotenvx
 18 | 
 19 | # for testing purposes only
 20 | HELLO="" # this is a comment
 21 | `
 22 |   ct.same(envExampleFile, output)
 23 |   ct.same(injected, { HELLO: '' })
 24 |   ct.same(preExisted, {})
 25 | 
 26 |   ct.end()
 27 | })
 28 | 
 29 | t.test('#run (.env.example already exists)', ct => {
 30 |   const genexample = new Genexample('tests/monorepo/apps/backend')
 31 | 
 32 |   const {
 33 |     envExampleFile,
 34 |     injected,
 35 |     preExisted
 36 |   } = genexample.run()
 37 | 
 38 |   const output = `# .env.example - generated with dotenvx
 39 | HELLO=''
 40 | `
 41 |   ct.same(envExampleFile, output)
 42 |   ct.same(injected, {})
 43 |   ct.same(preExisted, { HELLO: '' })
 44 | 
 45 |   ct.end()
 46 | })
 47 | 
 48 | t.test('#run (.env.example already exists but with different keys)', ct => {
 49 |   const originalReadFileSync = fsx.readFileX
 50 |   const sandbox = sinon.createSandbox()
 51 |   sandbox.stub(fsx, 'readFileX').callsFake((filepath, options) => {
 52 |     if (filepath === path.resolve('tests/monorepo/apps/backend/.env')) {
 53 |       return 'HELLO=world\nHELLO2=universe'
 54 |     } else {
 55 |       return originalReadFileSync(filepath, options)
 56 |     }
 57 |   })
 58 | 
 59 |   const genexample = new Genexample('tests/monorepo/apps/backend')
 60 | 
 61 |   const {
 62 |     envExampleFile,
 63 |     injected,
 64 |     preExisted
 65 |   } = genexample.run()
 66 | 
 67 |   const output = `# .env.example - generated with dotenvx
 68 | HELLO=''
 69 | HELLO2=''
 70 | `
 71 | 
 72 |   ct.same(envExampleFile, output)
 73 |   ct.same(injected, { HELLO2: '' })
 74 |   ct.same(preExisted, { HELLO: '' })
 75 | 
 76 |   ct.end()
 77 | })
 78 | 
 79 | t.test('#run (string envFile)', ct => {
 80 |   const genexample = new Genexample('tests/monorepo/apps/frontend', '.env')
 81 | 
 82 |   const {
 83 |     envExampleFile,
 84 |     injected,
 85 |     preExisted
 86 |   } = genexample.run()
 87 | 
 88 |   const output = `# .env.example - generated with dotenvx
 89 | 
 90 | # for testing purposes only
 91 | HELLO="" # this is a comment
 92 | `
 93 |   ct.same(envExampleFile, output)
 94 |   ct.same(injected, { HELLO: '' })
 95 |   ct.same(preExisted, {})
 96 | 
 97 |   ct.end()
 98 | })
 99 | 
100 | t.test('#run (cant find directory)', ct => {
101 |   try {
102 |     new Genexample('tests/monorepo/apps/frontendzzzz').run()
103 | 
104 |     ct.fail('should have raised an error but did not')
105 |   } catch (error) {
106 |     ct.equal(error.message, 'missing directory (tests/monorepo/apps/frontendzzzz)')
107 |   }
108 | 
109 |   ct.end()
110 | })
111 | 
112 | t.test('#run (missing env files)', ct => {
113 |   try {
114 |     new Genexample('tests/monorepo/apps/frontend', []).run()
115 |     ct.fail('should have raised an error but did not')
116 |   } catch (error) {
117 |     ct.equal(error.code, 'MISSING_ENV_FILES')
118 |     ct.equal(error.message, 'no .env* files found')
119 |     ct.equal(error.help, '? add one with [echo "HELLO=World" > .env] and then run [dotenvx genexample]')
120 |   }
121 | 
122 |   ct.end()
123 | })
124 | 
125 | t.test('#run (non-existent .env file)', ct => {
126 |   try {
127 |     new Genexample('tests/monorepo/apps/frontend', ['.env.nonexistent']).run()
128 |     ct.fail('should have raised an error but did not')
129 |   } catch (error) {
130 |     ct.equal(error.code, 'MISSING_ENV_FILE')
131 |     ct.equal(error.message, `[MISSING_ENV_FILE] missing .env.nonexistent file (${path.resolve('tests/monorepo/apps/frontend/.env.nonexistent')})`)
132 |     ct.equal(error.help, '? add it with [echo "HELLO=World" > .env.nonexistent] and then run [dotenvx genexample]')
133 |   }
134 | 
135 |   ct.end()
136 | })
137 | 


--------------------------------------------------------------------------------
/tests/lib/services/get.test.js:
--------------------------------------------------------------------------------
  1 | const t = require('tap')
  2 | 
  3 | const Get = require('../../../src/lib/services/get')
  4 | 
  5 | t.beforeEach((ct) => {
  6 |   // important, clear process.env before each test
  7 |   process.env = {}
  8 | })
  9 | 
 10 | t.test('#run (missing key returns the entire processEnv as object)', ct => {
 11 |   const { parsed } = new Get().run()
 12 | 
 13 |   ct.same(parsed, {})
 14 | 
 15 |   ct.end()
 16 | })
 17 | 
 18 | t.test('#run (all object) with preset process.env', ct => {
 19 |   process.env.PRESET_ENV_EXAMPLE = 'something/on/machine'
 20 | 
 21 |   const { parsed } = new Get(null, [], false, '', true).run()
 22 |   ct.same(parsed, { PRESET_ENV_EXAMPLE: 'something/on/machine' })
 23 | 
 24 |   const result = new Get(null, [], false, '', false).run()
 25 |   ct.same(result.parsed, {})
 26 | 
 27 |   ct.end()
 28 | })
 29 | 
 30 | t.test('#run (missing key returns the entire processEnv as object)', ct => {
 31 |   const envs = [
 32 |     { type: 'envFile', value: 'tests/.env.local' }
 33 |   ]
 34 |   const { parsed } = new Get(null, envs).run()
 35 | 
 36 |   ct.same(parsed, { BASIC: 'local_basic', LOCAL: 'local' })
 37 | 
 38 |   ct.end()
 39 | })
 40 | 
 41 | t.test('#run (missing key returns empty string when fetching single key)', ct => {
 42 |   const envs = [
 43 |     { type: 'envFile', value: 'tests/.env.local' }
 44 |   ]
 45 |   const { parsed } = new Get('BAZ', envs).run()
 46 | 
 47 |   ct.same(parsed.BAZ, undefined)
 48 | 
 49 |   ct.end()
 50 | })
 51 | 
 52 | t.test('#run', ct => {
 53 |   const envs = [
 54 |     { type: 'envFile', value: 'tests/.env' }
 55 |   ]
 56 |   const { parsed } = new Get('BASIC', envs).run()
 57 | 
 58 |   ct.same(parsed.BASIC, 'basic')
 59 | 
 60 |   ct.end()
 61 | })
 62 | 
 63 | t.test('#run (as multi-array)', ct => {
 64 |   const envs = [
 65 |     { type: 'envFile', value: 'tests/.env' },
 66 |     { type: 'envFile', value: 'tests/.env.local' }
 67 |   ]
 68 |   const { parsed } = new Get('BASIC', envs).run()
 69 | 
 70 |   ct.same(parsed.BASIC, 'basic')
 71 | 
 72 |   ct.end()
 73 | })
 74 | 
 75 | t.test('#run (as multi-array reversed (first wins))', ct => {
 76 |   const envs = [
 77 |     { type: 'envFile', value: 'tests/.env.local' },
 78 |     { type: 'envFile', value: 'tests/.env' }
 79 |   ]
 80 | 
 81 |   const { parsed } = new Get('BASIC', envs).run()
 82 | 
 83 |   ct.same(parsed.BASIC, 'local_basic')
 84 | 
 85 |   ct.end()
 86 | })
 87 | 
 88 | t.test('#run (as multi-array reversed with overload (second wins))', ct => {
 89 |   const envs = [
 90 |     { type: 'envFile', value: 'tests/.env.local' },
 91 |     { type: 'envFile', value: 'tests/.env' }
 92 |   ]
 93 | 
 94 |   const { parsed } = new Get('BASIC', envs, true).run()
 95 | 
 96 |   ct.same(parsed.BASIC, 'basic')
 97 | 
 98 |   ct.end()
 99 | })
100 | 
101 | t.test('#run (as multi-array - some not found)', ct => {
102 |   const envs = [
103 |     { type: 'envFile', value: 'tests/.env.notfound' },
104 |     { type: 'envFile', value: 'tests/.env' }
105 |   ]
106 | 
107 |   const { parsed } = new Get('BASIC', envs, true).run()
108 | 
109 |   ct.same(parsed.BASIC, 'basic')
110 | 
111 |   ct.end()
112 | })
113 | 
114 | t.test('#run (process.env already exists on machine)', ct => {
115 |   process.env.BASIC = 'existing'
116 | 
117 |   const envs = [
118 |     { type: 'envFile', value: 'tests/.env.local' }
119 |   ]
120 | 
121 |   const { parsed } = new Get('BASIC', envs).run()
122 | 
123 |   ct.same(parsed.BASIC, 'existing')
124 | 
125 |   ct.end()
126 | })
127 | 
128 | t.test('#run (no key and process.env already exists on machine)', ct => {
129 |   process.env.BASIC = 'existing'
130 | 
131 |   const envs = [
132 |     { type: 'envFile', value: 'tests/.env.local' }
133 |   ]
134 | 
135 |   const { parsed } = new Get(null, envs).run()
136 | 
137 |   ct.same(parsed, { BASIC: 'existing', LOCAL: 'local' })
138 | 
139 |   ct.end()
140 | })
141 | 
142 | t.test('#run expansion', ct => {
143 |   const envs = [
144 |     { type: 'envFile', value: 'tests/.env.expand' }
145 |   ]
146 | 
147 |   const { parsed } = new Get('BASIC_EXPAND', envs).run()
148 | 
149 |   ct.same(parsed.BASIC_EXPAND, 'basic')
150 | 
151 |   ct.end()
152 | })
153 | 


--------------------------------------------------------------------------------
/tests/lib/services/keypair.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const fs = require('fs')
 3 | const sinon = require('sinon')
 4 | 
 5 | const Keypair = require('../../../src/lib/services/keypair')
 6 | 
 7 | let writeFileSyncStub
 8 | 
 9 | t.beforeEach((ct) => {
10 |   process.env = {}
11 |   writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
12 | })
13 | 
14 | t.afterEach((ct) => {
15 |   writeFileSyncStub.restore()
16 | })
17 | 
18 | t.test('#run (no arguments)', ct => {
19 |   const result = new Keypair().run()
20 | 
21 |   ct.same(result, { DOTENV_PUBLIC_KEY: null, DOTENV_PRIVATE_KEY: null })
22 | 
23 |   ct.end()
24 | })
25 | 
26 | t.test('#run (finds .env file)', ct => {
27 |   const envFile = 'tests/monorepo/apps/encrypted/.env'
28 |   const result = new Keypair(envFile).run()
29 | 
30 |   ct.same(result, { DOTENV_PUBLIC_KEY: '03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba', DOTENV_PRIVATE_KEY: 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1' })
31 | 
32 |   ct.end()
33 | })
34 | 
35 | t.test('#run (finds .env file as array)', ct => {
36 |   const envFile = 'tests/monorepo/apps/encrypted/.env'
37 |   const result = new Keypair([envFile]).run()
38 | 
39 |   ct.same(result, { DOTENV_PUBLIC_KEY: '03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba', DOTENV_PRIVATE_KEY: 'ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1' })
40 | 
41 |   ct.end()
42 | })
43 | 


--------------------------------------------------------------------------------
/tests/monorepo/.env.keys:
--------------------------------------------------------------------------------
1 | #/------------------!DOTENV_PRIVATE_KEYS!-------------------/
2 | #/ private decryption keys. DO NOT commit to source control /
3 | #/     [how it works](https://dotenvx.com/encryption)       /
4 | #/----------------------------------------------------------/
5 | 
6 | # .env.production
7 | DOTENV_PRIVATE_KEY_PRODUCTION="f96fbb9b3e99b46d891879414e5c800113f0e757adc0bb7fb73c152072de669b"
8 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/app1/.env:
--------------------------------------------------------------------------------
1 | # for testing purposes only
2 | HELLO="app1"
3 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/app1/.env.production:
--------------------------------------------------------------------------------
1 | # for testing purposes only
2 | HELLO="production"
3 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/backend/.env:
--------------------------------------------------------------------------------
1 | # for testing purposes only
2 | HELLO="backend"
3 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/backend/.env.example:
--------------------------------------------------------------------------------
1 | # .env.example - generated with dotenvx
2 | HELLO=''
3 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/backend/.env.keys:
--------------------------------------------------------------------------------
1 | #/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/
2 | #/   DOTENV_KEYs. DO NOT commit to source control   /
3 | #/   [how it works](https://dotenvx.com/env-keys)   /
4 | #/--------------------------------------------------/
5 | DOTENV_KEY_DEVELOPMENT="dotenv://:key_e9e9ef8665b828cf2b64b2bf4237876b9a866da6580777633fba4325648cdd34@dotenvx.com/vault/.env.vault?environment=development"
6 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/backend/.env.previous:
--------------------------------------------------------------------------------
1 | # for testing purposes only
2 | HELLO="previous"
3 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/backend/.env.untracked:
--------------------------------------------------------------------------------
1 | HELLO="untracked"
2 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/backend/.env.vault:
--------------------------------------------------------------------------------
1 | #/-------------------.env.vault---------------------/
2 | #/         cloud-agnostic vaulting standard         /
3 | #/   [how it works](https://dotenvx.com/env-vault)  /
4 | #/--------------------------------------------------/
5 | 
6 | # development
7 | DOTENV_VAULT_DEVELOPMENT="TgaIyXmiLS1ej5LrII+Boz8R8nQ4avEM/pcreOfLUehTMmludeyXn6HMXLu8Jjn9O0yckjXy7kRrNfUvUJ88V8RpTwDP8k7u"
8 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/encrypted/.env:
--------------------------------------------------------------------------------
1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
2 | #/            public-key encryption for .env files          /
3 | #/       [how it works](https://dotenvx.com/encryption)     /
4 | #/----------------------------------------------------------/
5 | DOTENV_PUBLIC_KEY="03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba"
6 | 
7 | # .env
8 | HELLO="encrypted:BG8M6U+GKJGwpGA42ml2erb9+T2NBX6Z2JkBLynDy21poz0UfF5aPxCgRbIyhnQFdWKd0C9GZ7lM5PeL86xghoMcWvvPpkyQ0yaD2pZ64RzoxFGB1lTZYlEgQOxTDJnWxODHfuQcFY10uA=="
9 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/encrypted/.env.keys:
--------------------------------------------------------------------------------
1 | #/------------------!DOTENV_PRIVATE_KEYS!-------------------/
2 | #/ private decryption keys. DO NOT commit to source control /
3 | #/     [how it works](https://dotenvx.com/encryption)       /
4 | #/----------------------------------------------------------/
5 | 
6 | # .env
7 | DOTENV_PRIVATE_KEY="ec9e80073d7ace817d35acb8b7293cbf8e5981b4d2f5708ee5be405122993cd1"
8 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/encrypted/secrets.ci.txt:
--------------------------------------------------------------------------------
1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
2 | #/            public-key encryption for .env files          /
3 | #/       [how it works](https://dotenvx.com/encryption)     /
4 | #/----------------------------------------------------------/
5 | DOTENV_PUBLIC_KEY_CI="03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba"
6 | 
7 | # .env
8 | HELLO="encrypted:BG8M6U+GKJGwpGA42ml2erb9+T2NBX6Z2JkBLynDy21poz0UfF5aPxCgRbIyhnQFdWKd0C9GZ7lM5PeL86xghoMcWvvPpkyQ0yaD2pZ64RzoxFGB1lTZYlEgQOxTDJnWxODHfuQcFY10uA=="
9 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/encrypted/secrets.txt:
--------------------------------------------------------------------------------
1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
2 | #/            public-key encryption for .env files          /
3 | #/       [how it works](https://dotenvx.com/encryption)     /
4 | #/----------------------------------------------------------/
5 | DOTENV_PUBLIC_KEY="03eaf2142ab3d55bdf108962334e06696db798e7412cfc51d75e74b4f87f299bba"
6 | 
7 | # .env
8 | HELLO="encrypted:BG8M6U+GKJGwpGA42ml2erb9+T2NBX6Z2JkBLynDy21poz0UfF5aPxCgRbIyhnQFdWKd0C9GZ7lM5PeL86xghoMcWvvPpkyQ0yaD2pZ64RzoxFGB1lTZYlEgQOxTDJnWxODHfuQcFY10uA=="
9 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/frontend/.env:
--------------------------------------------------------------------------------
1 | # for testing purposes only
2 | HELLO="frontend" # this is a comment
3 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/multiline/.env:
--------------------------------------------------------------------------------
1 | HELLO="-----BEGIN RSA PRIVATE KEY-----
2 | ABCD
3 | EFGH
4 | IJKL
5 | -----END RSA PRIVATE KEY-----"
6 | ALOHA="-----BEGIN RSA PRIVATE KEY-----\nABCD\nEFGH\nIJKL\n-----END RSA PRIVATE KEY-----"
7 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/multiline/.env.crlf:
--------------------------------------------------------------------------------
1 | HELLO="-----BEGIN RSA PRIVATE KEY-----
2 | ABCD
3 | EFGH
4 | IJKL
5 | -----END RSA PRIVATE KEY-----"
6 | ALOHA="-----BEGIN RSA PRIVATE KEY-----\r\nABCD\r\nEFGH\r\nIJKL\r\n-----END RSA PRIVATE KEY-----"
7 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/multiple/.env:
--------------------------------------------------------------------------------
1 | HELLO="one"
2 | HELLO2="two"
3 | HELLO3="three"
4 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/multiple/.env.keys:
--------------------------------------------------------------------------------
1 | #/------------------!DOTENV_PRIVATE_KEYS!-------------------/
2 | #/ private decryption keys. DO NOT commit to source control /
3 | #/     [how it works](https://dotenvx.com/encryption)       /
4 | #/----------------------------------------------------------/
5 | 
6 | # .env.production
7 | DOTENV_PRIVATE_KEY_PRODUCTION="f96fbb9b3e99b46d891879414e5c800113f0e757adc0bb7fb73c152072de669b"
8 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/multiple/.env.production:
--------------------------------------------------------------------------------
 1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
 2 | #/            public-key encryption for .env files          /
 3 | #/       [how it works](https://dotenvx.com/encryption)     /
 4 | #/----------------------------------------------------------/
 5 | DOTENV_PUBLIC_KEY_PRODUCTION="027907f1e455c0e881f12e0e385461df4166dc6324d1a584ce47858485357e16bb"
 6 | 
 7 | # .env.production
 8 | HELLO="encrypted:BOYFQaf+RnJa5/Myo+PbeGpmKnqLQlcl7x1vnoGRHdzlHK9rDFjPpWC5g82If69OPEnbijj8ywWd8QUAdOXLHlYrf8+KGg/QF6AgpyNrMUzMhCHyHDnWilKlywHH677xj1wL9g=="
 9 | HELLO2="encrypted:BDaopY2/icyl9MYMy7WNAdVYnCncLf0eLlloubA9Tki4VaLkiyK+Hj1nlIBAxGzjD/s0PkPIrfa/QXQdQwlHtsFYixikZhT8nDMdfV80cKKjKJJ7oANLFH1Ck+/VAW2tbv+tYQ=="
10 | HELLO3="encrypted:BF4XTKdeAwj4yESsmlFHAm7TIQ9rGK3qDVP0gMUKGSMZ8YX345NJ0QZtCWKVXl0+z+6XrjwlCQe3fSFsYV0H+SMJapMMmq2hIr1GqGX423XCkdS6oMCRpCqsbV0/w7yTEsBRirST"
11 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/shebang/.env:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | HELLO="shebang"
3 | 


--------------------------------------------------------------------------------
/tests/monorepo/apps/unencrypted/.env:
--------------------------------------------------------------------------------
1 | HELLO="unencrypted"
2 | 


--------------------------------------------------------------------------------
/tests/multiline.txt:
--------------------------------------------------------------------------------
1 | one
2 | two
3 | three
4 | 


--------------------------------------------------------------------------------
/tests/shared/colors.test.js:
--------------------------------------------------------------------------------
 1 | const t = require('tap')
 2 | const sinon = require('sinon')
 3 | 
 4 | const { getColor, bold } = require('../../src/shared/colors')
 5 | const depth = require('../../src/lib/helpers/colorDepth')
 6 | 
 7 | t.test('getColor with ansi256 color support', (ct) => {
 8 |   const stub = sinon.stub(depth, 'getColorDepth').returns(8)
 9 | 
10 |   ct.equal(getColor('orangered')('hello'), '\x1b[38;5;202mhello\x1b[39m')
11 | 
12 |   stub.restore()
13 |   ct.end()
14 | })
15 | 
16 | t.test('getColor with ansi16 color support', (ct) => {
17 |   const stub = sinon.stub(depth, 'getColorDepth').returns(4)
18 | 
19 |   ct.equal(getColor('orangered')('hello'), '\x1b[31mhello\x1b[39m')
20 | 
21 |   stub.restore()
22 |   ct.end()
23 | })
24 | 
25 | t.test('getColor without color support', (ct) => {
26 |   const stub = sinon.stub(depth, 'getColorDepth').returns(1)
27 | 
28 |   ct.equal(getColor('orangered')('hello'), 'hello')
29 | 
30 |   stub.restore()
31 |   ct.end()
32 | })
33 | 
34 | t.test('getColor invalid color', (ct) => {
35 |   try {
36 |     getColor('invalid')
37 | 
38 |     ct.fail('getColor should throw error')
39 |   } catch (error) {
40 |     ct.pass(' threw an error')
41 |     ct.equal(error.message, 'Invalid color invalid')
42 |   }
43 | 
44 |   ct.end()
45 | })
46 | 
47 | t.test('bold with ansi16 color support', (ct) => {
48 |   const stub = sinon.stub(depth, 'getColorDepth').returns(4)
49 | 
50 |   ct.equal(bold('hello'), '\x1b[1mhello\x1b[22m')
51 | 
52 |   stub.restore()
53 |   ct.end()
54 | })
55 | 
56 | t.test('bold without color support', (ct) => {
57 |   const stub = sinon.stub(depth, 'getColorDepth').returns(1)
58 | 
59 |   ct.equal(bold('hello'), 'hello')
60 | 
61 |   stub.restore()
62 |   ct.end()
63 | })
64 | 


--------------------------------------------------------------------------------