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 +21 -0
- package/README.md +188 -0
- package/bin/envsetter.js +10 -0
- package/package.json +51 -0
- package/src/index.js +271 -0
- package/src/scanner.js +411 -0
- package/src/ui.js +773 -0
- package/src/writer.js +181 -0
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)
|
package/bin/envsetter.js
ADDED
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}
|