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/src/writer.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const path = require("path")
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine if a value needs to be quoted
|
|
8
|
+
*/
|
|
9
|
+
function needsQuotes(value) {
|
|
10
|
+
if (!value) return false
|
|
11
|
+
// Quote if it contains spaces, #, =, newlines, or special chars
|
|
12
|
+
return /[\s#=\\$"'`!]/.test(value) || value.includes("\n")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Escape a value for .env format
|
|
17
|
+
*/
|
|
18
|
+
function formatValue(value) {
|
|
19
|
+
if (!value && value !== "") return '""'
|
|
20
|
+
|
|
21
|
+
if (needsQuotes(value)) {
|
|
22
|
+
// Use double quotes and escape internal double quotes and backslashes
|
|
23
|
+
const escaped = value
|
|
24
|
+
.replace(/\\/g, "\\\\")
|
|
25
|
+
.replace(/"/g, '\\"')
|
|
26
|
+
.replace(/\n/g, "\\n")
|
|
27
|
+
return `"${escaped}"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Write env variables to file
|
|
35
|
+
* - Preserves existing comments and structure
|
|
36
|
+
* - Updates existing keys in place
|
|
37
|
+
* - Appends new keys at the end
|
|
38
|
+
*/
|
|
39
|
+
function writeEnvFile(envFilePath, newVars, existingEnv) {
|
|
40
|
+
const fullPath = path.resolve(envFilePath)
|
|
41
|
+
let lines = []
|
|
42
|
+
const updatedKeys = new Set()
|
|
43
|
+
|
|
44
|
+
// Read existing file if it exists
|
|
45
|
+
if (fs.existsSync(fullPath)) {
|
|
46
|
+
const content = fs.readFileSync(fullPath, "utf-8")
|
|
47
|
+
lines = content.split("\n")
|
|
48
|
+
|
|
49
|
+
// Update existing lines in place
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
const line = lines[i].trim()
|
|
52
|
+
if (!line || line.startsWith("#")) continue
|
|
53
|
+
|
|
54
|
+
const eqIndex = line.indexOf("=")
|
|
55
|
+
if (eqIndex === -1) continue
|
|
56
|
+
|
|
57
|
+
const key = line.substring(0, eqIndex).trim()
|
|
58
|
+
|
|
59
|
+
if (newVars.has(key)) {
|
|
60
|
+
lines[i] = `${key}=${formatValue(newVars.get(key))}`
|
|
61
|
+
updatedKeys.add(key)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Collect new keys that weren't in the file already
|
|
67
|
+
const appendKeys = []
|
|
68
|
+
for (const [key, value] of newVars) {
|
|
69
|
+
if (!updatedKeys.has(key)) {
|
|
70
|
+
appendKeys.push({key, value})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If we have new keys to append, add a separator
|
|
75
|
+
if (appendKeys.length > 0) {
|
|
76
|
+
// Don't add blank line if file is empty
|
|
77
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
78
|
+
lines.push("")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const {key, value} of appendKeys) {
|
|
82
|
+
lines.push(`${key}=${formatValue(value)}`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Ensure file ends with newline
|
|
87
|
+
const finalContent = lines.join("\n").replace(/\n+$/, "") + "\n"
|
|
88
|
+
|
|
89
|
+
// Write file
|
|
90
|
+
fs.writeFileSync(fullPath, finalContent, "utf-8")
|
|
91
|
+
|
|
92
|
+
return updatedKeys.size + appendKeys.length
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Ensure .gitignore includes the env file
|
|
97
|
+
*/
|
|
98
|
+
function ensureGitignore(envFilePath) {
|
|
99
|
+
const gitignorePath = path.resolve(".gitignore")
|
|
100
|
+
const envBasename = path.basename(envFilePath)
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(gitignorePath)) return
|
|
103
|
+
|
|
104
|
+
const content = fs.readFileSync(gitignorePath, "utf-8")
|
|
105
|
+
const lines = content.split("\n").map(l => l.trim())
|
|
106
|
+
|
|
107
|
+
// Check if already ignored
|
|
108
|
+
const isIgnored = lines.some(line => {
|
|
109
|
+
if (line.startsWith("#")) return false
|
|
110
|
+
return (
|
|
111
|
+
line === envBasename ||
|
|
112
|
+
line === envFilePath ||
|
|
113
|
+
line === ".env*" ||
|
|
114
|
+
line === ".env"
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (!isIgnored) {
|
|
119
|
+
// Don't auto-modify, just warn — handled in UI
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Sync keys (without values) to .env.example
|
|
128
|
+
* - Creates .env.example if it doesn't exist
|
|
129
|
+
* - Only appends keys that are not already present
|
|
130
|
+
* - Preserves existing comments and structure
|
|
131
|
+
*/
|
|
132
|
+
function syncToEnvExample(keys) {
|
|
133
|
+
const examplePath = path.resolve(".env.example")
|
|
134
|
+
let existingKeys = new Set()
|
|
135
|
+
let lines = []
|
|
136
|
+
|
|
137
|
+
if (fs.existsSync(examplePath)) {
|
|
138
|
+
const content = fs.readFileSync(examplePath, "utf-8")
|
|
139
|
+
lines = content.split("\n")
|
|
140
|
+
|
|
141
|
+
// Parse existing keys
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const trimmed = line.trim()
|
|
144
|
+
if (!trimmed || trimmed.startsWith("#")) continue
|
|
145
|
+
|
|
146
|
+
const eqIndex = trimmed.indexOf("=")
|
|
147
|
+
if (eqIndex === -1) continue
|
|
148
|
+
|
|
149
|
+
const key = trimmed.substring(0, eqIndex).trim()
|
|
150
|
+
if (key) existingKeys.add(key)
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// Create new file with a header
|
|
154
|
+
lines = [
|
|
155
|
+
"# Environment Variables",
|
|
156
|
+
"# Copy this file to .env and fill in the values",
|
|
157
|
+
"",
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Collect keys that need to be added
|
|
162
|
+
const keysToAdd = keys.filter(k => !existingKeys.has(k))
|
|
163
|
+
if (keysToAdd.length === 0) return 0
|
|
164
|
+
|
|
165
|
+
// Add blank line separator if file doesn't end with one
|
|
166
|
+
if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
|
|
167
|
+
lines.push("")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const key of keysToAdd) {
|
|
171
|
+
lines.push(`${key}=`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Write file
|
|
175
|
+
const finalContent = lines.join("\n").replace(/\n+$/, "") + "\n"
|
|
176
|
+
fs.writeFileSync(examplePath, finalContent, "utf-8")
|
|
177
|
+
|
|
178
|
+
return keysToAdd.length
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {writeEnvFile, ensureGitignore, syncToEnvExample}
|