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/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}