envsetter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zain Afzal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # ⚡ EnvSetter
2
+
3
+ > Stop manually writing `KEY=VALUE`. Scan your codebase, paste values, done.
4
+
5
+ EnvSetter scans your entire codebase for environment variable references (`process.env.X`,
6
+ `import.meta.env.X`, `.env.example` files, etc.), shows you which ones are
7
+ missing, and gives you a clean interactive interface to fill them in.
8
+
9
+ ---
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g envsetter
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ cd your-project
21
+ envsetter
22
+ ```
23
+
24
+ That's it. It will:
25
+
26
+ 1. 🔍 **Scan** your codebase for every env variable reference
27
+ 2. 📂 **Discover** all folders with `.env` files (monorepo support)
28
+ 3. 📋 **Show** which are already set and which are missing
29
+ 4. ✏️ **Prompt** you with a clean interactive interface
30
+ 5. 💾 **Save** everything to your `.env` file with proper formatting
31
+ 6. 🔄 **Sync** keys to `.env.example` automatically
32
+
33
+ ---
34
+
35
+ ## Features
36
+
37
+ ### 🎯 Smart Scanning
38
+ - Deep scanning across **20+ file types** and all major languages
39
+ - Detects patterns like `process.env.VAR`, `os.environ`, `env('VAR')` and more
40
+ - Reads `.env.example` files to find expected variables
41
+
42
+ ### 📂 Multi-Folder Support
43
+ - Automatically discovers all folders containing `.env` files
44
+ - Navigate between folders in monorepos and multi-service projects
45
+ - Process individual folders or edit all at once
46
+
47
+ ### ⬆ Bulk Paste
48
+ - Paste your entire `.env` content in one go
49
+ - Automatically parses `KEY=VALUE` pairs, handles quotes, comments, messy formatting
50
+ - Preview what was parsed before confirming
51
+
52
+ ### 🔐 Security Aware
53
+ - Masks sensitive values (passwords, tokens, API keys) in the UI
54
+ - Warns if `.env` is not in `.gitignore`
55
+ - Auto-detects secret keys by pattern
56
+
57
+ ### ✨ Smart Hints
58
+ - Categorizes variables (Database, AWS, Auth, Email, etc.)
59
+ - Shows type hints (URL, Secret, Number, Flag)
60
+ - Displays where each variable is used in your code
61
+
62
+ ### 📝 Auto-Sync `.env.example`
63
+ - Automatically syncs new keys to `.env.example` (without values)
64
+ - Creates `.env.example` if it doesn't exist
65
+ - Team-friendly — everyone knows what keys are needed
66
+
67
+ ### 🎨 Beautiful UI
68
+ - Progress bars, category grouping, colored output
69
+ - Interactive prompts with commands: `skip`, `back`, `clear`, `list`, `exit`, `skipall`
70
+ - Clean boxed layouts and visual hierarchy
71
+
72
+ ---
73
+
74
+ ## What It Detects
75
+
76
+ | Pattern | Language/Framework |
77
+ | ----------------------- | ------------------ |
78
+ | `process.env.VAR` | Node.js |
79
+ | `process.env['VAR']` | Node.js |
80
+ | `import.meta.env.VAR` | Vite |
81
+ | `NEXT_PUBLIC_*` | Next.js |
82
+ | `REACT_APP_*` | Create React App |
83
+ | `VITE_*` | Vite |
84
+ | `NUXT_*` | Nuxt |
85
+ | `EXPO_PUBLIC_*` | Expo |
86
+ | `os.environ.get('VAR')` | Python |
87
+ | `ENV["VAR"]` | Ruby |
88
+ | `env('VAR')` | Laravel |
89
+ | `System.getenv("VAR")` | Java |
90
+ | `os.Getenv("VAR")` | Go |
91
+ | `std::env::var("VAR")` | Rust |
92
+ | `${VAR}` | Docker/YAML |
93
+ | `.env.example` entries | Any |
94
+
95
+ ---
96
+
97
+ ## Modes
98
+
99
+ ### Fill Missing
100
+ Only prompts for variables that don't have a value yet.
101
+
102
+ ### Edit All
103
+ Re-prompts for every variable, letting you update existing values.
104
+
105
+ ### Bulk Paste
106
+ Paste a whole `.env` file content at once — perfect for migrating from Vercel, Railway, or another project.
107
+
108
+ ---
109
+
110
+ ## Commands (during prompts)
111
+
112
+ | Command | Action |
113
+ | --------- | ------------------------------- |
114
+ | `skip` | Skip this variable |
115
+ | `back` | Go to the previous variable |
116
+ | `clear` | Set value to empty string |
117
+ | `list` | Show all variables and status |
118
+ | `skipall` | Skip all remaining variables |
119
+ | `exit` | End session (with confirmation) |
120
+ | `?` | Show help panel |
121
+
122
+ ---
123
+
124
+ ## How It Works
125
+
126
+ ```
127
+ ╭─────────────────────────────────────────────────────╮
128
+ │ │
129
+ │ _____ _ ___ __ │
130
+ │ | ____| \ | \ \ / / │
131
+ │ | _| | \| |\ \ / / │
132
+ │ | |___| |\ | \ V / │
133
+ │ |_____|_| \_| \_/ │
134
+ │ │
135
+ │ --- S E T T E R --- │
136
+ │ │
137
+ │ Scan > Fill > Save | Interactive .env manager │
138
+ │ v1.0.0 │
139
+ │ │
140
+ ╰─────────────────────────────────────────────────────╯
141
+
142
+ ◆ ✔ Found env files in 3 folders
143
+
144
+ >> Project Folders ───────────────────────────
145
+
146
+ ? Select folder
147
+ > ./ (root) 3 env files
148
+ backend/ 1 env file
149
+ frontend/ 2 env files
150
+ Edit all folders
151
+
152
+ ◆ Working in: backend
153
+
154
+ ╭────────────────────────────────────────────────╮
155
+ │ ◆ Scan Results │
156
+ │ ● Variables found 8 │
157
+ │ ● Already set 3 │
158
+ │ ● Missing 5 │
159
+ │ Coverage ████████░░░░░░░░░░░░ 38% │
160
+ ╰────────────────────────────────────────────────╯
161
+
162
+ ? Select mode
163
+ > ▸ Fill missing only 5
164
+ ✎ Edit all variables 8
165
+ ⬆ Bulk paste (paste whole .env content)
166
+ ✖ Exit
167
+
168
+ ╭──────────────────────────────────────────────╮
169
+ │ ◆ DATABASE_URL [1/5] 0% │
170
+ │ ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
171
+ │ URL Database connection string │
172
+ │ ● Not set │
173
+ ╰──────────────────────────────────────────────╯
174
+
175
+ ▸ Value: postgresql://user:pass@localhost:5432/db
176
+
177
+ ╭──────────────────────────────────────────────────╮
178
+ │ ✔ Complete │
179
+ │ Saved 5 variables │
180
+ │ Target .env │
181
+ ╰──────────────────────────────────────────────────╯
182
+ ```
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ MIT © [Zain Afzal](https://zainafzal.dev)
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict"
4
+
5
+ const {main} = require("../src/index")
6
+
7
+ main().catch(err => {
8
+ console.error(err)
9
+ process.exit(1)
10
+ })
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "envsetter",
3
+ "version": "1.0.0",
4
+ "description": "Interactive CLI to scan your codebase for environment variables and set their values — no more manually writing KEY=VALUE.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "envsetter": "./bin/envsetter.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "env",
17
+ "dotenv",
18
+ "cli",
19
+ "environment",
20
+ "variables",
21
+ "setter",
22
+ "scanner",
23
+ "interactive",
24
+ "env-manager",
25
+ "bulk-paste",
26
+ "monorepo",
27
+ "multi-folder",
28
+ "env-example"
29
+ ],
30
+ "author": "Zain Afzal (https://zainafzal.dev)",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/sheikhmuhammadzain/envsetter.git"
35
+ },
36
+ "homepage": "https://github.com/sheikhmuhammadzain/envsetter#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/sheikhmuhammadzain/envsetter/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=14.0.0"
42
+ },
43
+ "dependencies": {
44
+ "boxen": "^5.1.2",
45
+ "chalk": "^4.1.2",
46
+ "figures": "^3.2.0",
47
+ "glob": "^8.1.0",
48
+ "inquirer": "^8.2.6",
49
+ "ora": "^5.4.1"
50
+ }
51
+ }
package/src/index.js ADDED
@@ -0,0 +1,271 @@
1
+ "use strict"
2
+
3
+ const path = require("path")
4
+ const ora = require("ora")
5
+ const chalk = require("chalk")
6
+
7
+ const {scanCodebase, scanEnvFilesOnly, parseExistingEnv, discoverEnvFolders} = require("./scanner")
8
+ const {
9
+ showBanner,
10
+ showScanResult,
11
+ askMode,
12
+ askEnvFile,
13
+ askFolder,
14
+ promptForValues,
15
+ askBulkPaste,
16
+ showSummary,
17
+ } = require("./ui")
18
+ const {writeEnvFile, ensureGitignore, syncToEnvExample} = require("./writer")
19
+
20
+ // Theme colors matching ui.js
21
+ const T = {
22
+ accent: "#3B82F6",
23
+ cyan: "#22D3EE",
24
+ green: "#10B981",
25
+ yellow: "#F59E0B",
26
+ red: "#EF4444",
27
+ purple: "#A78BFA",
28
+ text: "#F4F4F5",
29
+ textSecondary: "#A1A1AA",
30
+ textMuted: "#71717A",
31
+ textSubtle: "#52525B",
32
+ }
33
+
34
+ /**
35
+ * Process a single folder — scan, select env file, fill values
36
+ */
37
+ async function processFolder(folderPath, isDeepScan, folderLabel) {
38
+ const hasUsableValue = (envMap, key) => {
39
+ if (!envMap.has(key)) return false
40
+ const value = envMap.get(key)
41
+ return typeof value === "string" && value.trim().length > 0
42
+ }
43
+
44
+ // Show which folder we're working on
45
+ if (folderLabel) {
46
+ console.log("")
47
+ console.log(
48
+ chalk.hex(T.accent)(` › `) +
49
+ chalk.bold.hex(T.text)(`Working in: `) +
50
+ chalk.bold.hex(T.accent)(folderLabel),
51
+ )
52
+ console.log(chalk.hex(T.textSubtle)(` ${"─".repeat(50)}`))
53
+ console.log("")
54
+ }
55
+
56
+ // ── Scanning Phase ──────────────────────────────────────────────────────────
57
+ const scanLabel = isDeepScan
58
+ ? "Deep scanning for environment variables"
59
+ : "Scanning env files for environment variables"
60
+
61
+ const spinner = ora({
62
+ text: chalk.hex(T.textSecondary)(scanLabel),
63
+ spinner: "dots12",
64
+ color: "cyan",
65
+ prefixText: chalk.hex(T.accent)(" ›"),
66
+ }).start()
67
+
68
+ let foundVars
69
+ try {
70
+ foundVars = isDeepScan ? scanCodebase(folderPath) : scanEnvFilesOnly(folderPath)
71
+ } catch (err) {
72
+ spinner.fail(chalk.hex(T.red)("Failed to scan for environment variables"))
73
+ console.error(err)
74
+ return {saved: 0, skipped: true}
75
+ }
76
+
77
+ spinner.succeed(chalk.hex(T.green)("Scan complete"))
78
+ console.log("")
79
+
80
+ // ── No Variables Found ──────────────────────────────────────────────────────
81
+ if (foundVars.size === 0) {
82
+ console.log(
83
+ chalk.hex(T.yellow)(
84
+ ` ⚠ No environment variables found.\n` +
85
+ chalk.hex(T.textMuted)(` Try: envsetter --deep\n`),
86
+ ),
87
+ )
88
+ return {saved: 0, skipped: true}
89
+ }
90
+
91
+ // ── Select Target File ─────────────────────────────────────────────────────
92
+ const envFilePath = await askEnvFile(folderPath)
93
+ const fullEnvPath = path.resolve(folderPath, envFilePath)
94
+ const existingEnv = parseExistingEnv(fullEnvPath)
95
+ console.log("")
96
+
97
+ // ── Show Scan Summary ──────────────────────────────────────────────────────
98
+ const {missing, alreadySet} = showScanResult(foundVars, existingEnv)
99
+ const mode = await askMode(missing, alreadySet)
100
+
101
+ if (mode === "exit") {
102
+ console.log(chalk.hex(T.textMuted)("\n Skipped this folder.\n"))
103
+ return {saved: 0, skipped: true}
104
+ }
105
+
106
+ // ── Bulk Paste Mode ────────────────────────────────────────────────────────
107
+ if (mode === "bulk") {
108
+ const bulkVars = await askBulkPaste()
109
+ if (!bulkVars || bulkVars.size === 0) {
110
+ showSummary(0, envFilePath)
111
+ return {saved: 0, skipped: false}
112
+ }
113
+
114
+ const savedCount = writeEnvFile(fullEnvPath, bulkVars, existingEnv)
115
+
116
+ // Sync to .env.example
117
+ const savedKeys = [...bulkVars.keys()]
118
+ if (savedKeys.length > 0) {
119
+ const synced = syncToEnvExample(savedKeys)
120
+ if (synced > 0) {
121
+ console.log(
122
+ chalk.hex(T.green)(
123
+ ` ✔ Synced ${synced} new ${synced > 1 ? "keys" : "key"} to .env.example`,
124
+ ),
125
+ )
126
+ }
127
+ }
128
+
129
+ showSummary(savedCount, envFilePath)
130
+ return {saved: savedCount, skipped: false}
131
+ }
132
+
133
+ // ── Determine Variables to Fill ────────────────────────────────────────────
134
+ let varsToFill
135
+ if (mode === "missing") {
136
+ varsToFill = [...foundVars.keys()].filter(k => !hasUsableValue(existingEnv, k))
137
+ } else {
138
+ varsToFill = [...foundVars.keys()]
139
+ }
140
+
141
+ if (varsToFill.length === 0) {
142
+ console.log(
143
+ chalk.hex(T.green)(`\n ✔ All environment variables are already set!\n`),
144
+ )
145
+ return {saved: 0, skipped: false}
146
+ }
147
+
148
+ // ── Interactive Fill ───────────────────────────────────────────────────────
149
+ let savedCount = 0
150
+ const newValues = await promptForValues(
151
+ varsToFill,
152
+ existingEnv,
153
+ foundVars,
154
+ async (key, value) => {
155
+ const written = writeEnvFile(fullEnvPath, new Map([[key, value]]), existingEnv)
156
+ savedCount += written
157
+ existingEnv.set(key, value)
158
+ },
159
+ )
160
+
161
+ if (newValues.size === 0) {
162
+ showSummary(0, envFilePath)
163
+ return {saved: 0, skipped: false}
164
+ }
165
+
166
+ // ── Gitignore Check ────────────────────────────────────────────────────────
167
+ const isIgnored = ensureGitignore(envFilePath)
168
+ if (isIgnored === false) {
169
+ console.log("")
170
+ console.log(
171
+ chalk.hex(T.yellow)(
172
+ ` ⚠ ${chalk.bold(envFilePath)} is ${chalk.bold("NOT")} in .gitignore!`,
173
+ ),
174
+ )
175
+ console.log(
176
+ chalk.hex(T.textMuted)(
177
+ ` Run: ${chalk.hex(T.accent)(`echo "${path.basename(envFilePath)}" >> .gitignore`)}`,
178
+ ),
179
+ )
180
+ }
181
+
182
+ // ── Sync keys to .env.example ──────────────────────────────────────────────
183
+ const savedKeys = [...newValues.keys()]
184
+ if (savedKeys.length > 0) {
185
+ const synced = syncToEnvExample(savedKeys)
186
+ if (synced > 0) {
187
+ console.log(
188
+ chalk.hex(T.green)(
189
+ ` ✔ Synced ${synced} new ${synced > 1 ? "keys" : "key"} to .env.example`,
190
+ ),
191
+ )
192
+ }
193
+ }
194
+
195
+ showSummary(savedCount, envFilePath)
196
+ return {saved: savedCount, skipped: false}
197
+ }
198
+
199
+ async function main() {
200
+ const cwd = process.cwd()
201
+ const isDeepScan = process.argv.includes("--deep")
202
+
203
+ showBanner()
204
+
205
+ // ── Discover Folders with Env Files ────────────────────────────────────────
206
+ const discoverSpinner = ora({
207
+ text: chalk.hex(T.textSecondary)("Discovering folders with env files..."),
208
+ spinner: "dots12",
209
+ color: "cyan",
210
+ prefixText: chalk.hex(T.accent)(" ›"),
211
+ }).start()
212
+
213
+ const folders = discoverEnvFolders(cwd)
214
+
215
+ if (folders.length === 0) {
216
+ discoverSpinner.warn(chalk.hex(T.yellow)("No env files found in any folder"))
217
+ console.log("")
218
+
219
+ if (!isDeepScan) {
220
+ console.log(
221
+ chalk.hex(T.textMuted)(` Try deep scanning to find env references in code:\n`) +
222
+ chalk.hex(T.accent)(` $ envsetter --deep\n`),
223
+ )
224
+ }
225
+
226
+ // Fall back to running on cwd directly
227
+ discoverSpinner.stop()
228
+ await processFolder(cwd, isDeepScan, null)
229
+ return
230
+ }
231
+
232
+ discoverSpinner.succeed(
233
+ chalk.hex(T.green)(
234
+ `Found env files in ${folders.length} folder${folders.length > 1 ? "s" : ""}`,
235
+ ),
236
+ )
237
+ console.log("")
238
+
239
+ // ── Folder Selection ───────────────────────────────────────────────────────
240
+ const selected = await askFolder(folders)
241
+
242
+ if (selected === "all") {
243
+ // Process all folders sequentially
244
+ let totalSaved = 0
245
+ for (let i = 0; i < folders.length; i++) {
246
+ const folder = folders[i]
247
+ const folderLabel = `${folder.relPath === "." ? "./ (root)" : folder.relPath} [${i + 1}/${folders.length}]`
248
+ const result = await processFolder(folder.absPath, isDeepScan, folderLabel)
249
+ totalSaved += result.saved
250
+ }
251
+
252
+ if (totalSaved > 0) {
253
+ console.log("")
254
+ console.log(
255
+ chalk.hex(T.green)(
256
+ ` ✔ Total: ${totalSaved} variable${totalSaved > 1 ? "s" : ""} saved across ${folders.length} folders`,
257
+ ),
258
+ )
259
+ console.log("")
260
+ }
261
+ } else if (selected) {
262
+ // Process single selected folder
263
+ const folderLabel = selected.relPath === "." ? null : selected.relPath
264
+ await processFolder(selected.absPath, isDeepScan, folderLabel)
265
+ } else {
266
+ // No folder selected (shouldn't happen)
267
+ await processFolder(cwd, isDeepScan, null)
268
+ }
269
+ }
270
+
271
+ module.exports = {main}