@enactprotocol/secrets 2.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/README.md +73 -0
- package/package.json +30 -0
- package/src/dagger/index.ts +19 -0
- package/src/dagger/secret-object.ts +126 -0
- package/src/dagger/uri-parser.ts +202 -0
- package/src/env/index.ts +54 -0
- package/src/env/manager.ts +251 -0
- package/src/env/parser.ts +240 -0
- package/src/env/reader.ts +105 -0
- package/src/env/writer.ts +118 -0
- package/src/index.ts +120 -0
- package/src/keyring.ts +136 -0
- package/src/resolver.ts +184 -0
- package/src/types.ts +194 -0
- package/tests/dagger/secret-object.test.ts +217 -0
- package/tests/dagger/uri-parser.test.ts +194 -0
- package/tests/env/manager.test.ts +220 -0
- package/tests/env/parser.test.ts +352 -0
- package/tests/env/reader-writer.test.ts +257 -0
- package/tests/fixtures/complex.env +26 -0
- package/tests/fixtures/empty.env +0 -0
- package/tests/fixtures/simple.env +4 -0
- package/tests/keyring.test.ts +250 -0
- package/tests/mocks/keyring.ts +164 -0
- package/tests/resolver.test.ts +287 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified environment variable manager
|
|
3
|
+
*
|
|
4
|
+
* Provides high-level functions for managing environment variables
|
|
5
|
+
* with priority resolution: local → global → default
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EnvResolution, EnvironmentVariable } from "../types";
|
|
9
|
+
import {
|
|
10
|
+
getGlobalEnvPath,
|
|
11
|
+
getLocalEnvPath,
|
|
12
|
+
globalEnvExists,
|
|
13
|
+
loadGlobalEnv,
|
|
14
|
+
loadLocalEnv,
|
|
15
|
+
localEnvExists,
|
|
16
|
+
} from "./reader";
|
|
17
|
+
import { deleteGlobalEnvVar, deleteLocalEnvVar, setGlobalEnvVar, setLocalEnvVar } from "./writer";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set an environment variable
|
|
21
|
+
*
|
|
22
|
+
* @param key - Variable key
|
|
23
|
+
* @param value - Variable value
|
|
24
|
+
* @param scope - Where to set: 'local' or 'global' (default: 'global')
|
|
25
|
+
* @param cwd - Current working directory for local scope
|
|
26
|
+
*/
|
|
27
|
+
export function setEnv(
|
|
28
|
+
key: string,
|
|
29
|
+
value: string,
|
|
30
|
+
scope: "local" | "global" = "global",
|
|
31
|
+
cwd?: string
|
|
32
|
+
): void {
|
|
33
|
+
if (scope === "local") {
|
|
34
|
+
setLocalEnvVar(key, value, cwd);
|
|
35
|
+
} else {
|
|
36
|
+
setGlobalEnvVar(key, value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get an environment variable with priority resolution
|
|
42
|
+
*
|
|
43
|
+
* Priority: local → global → default
|
|
44
|
+
*
|
|
45
|
+
* @param key - Variable key
|
|
46
|
+
* @param defaultValue - Default value if not found
|
|
47
|
+
* @param cwd - Current working directory for local scope
|
|
48
|
+
* @returns Resolution result with value and source
|
|
49
|
+
*/
|
|
50
|
+
export function getEnv(key: string, defaultValue?: string, cwd?: string): EnvResolution | null {
|
|
51
|
+
// Check local first
|
|
52
|
+
const localVars = loadLocalEnv(cwd);
|
|
53
|
+
const localValue = localVars[key];
|
|
54
|
+
if (localValue !== undefined) {
|
|
55
|
+
return {
|
|
56
|
+
key,
|
|
57
|
+
value: localValue,
|
|
58
|
+
source: "local",
|
|
59
|
+
filePath: getLocalEnvPath(cwd),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check global
|
|
64
|
+
const globalVars = loadGlobalEnv();
|
|
65
|
+
const globalValue = globalVars[key];
|
|
66
|
+
if (globalValue !== undefined) {
|
|
67
|
+
return {
|
|
68
|
+
key,
|
|
69
|
+
value: globalValue,
|
|
70
|
+
source: "global",
|
|
71
|
+
filePath: getGlobalEnvPath(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Use default
|
|
76
|
+
if (defaultValue !== undefined) {
|
|
77
|
+
return {
|
|
78
|
+
key,
|
|
79
|
+
value: defaultValue,
|
|
80
|
+
source: "default",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get just the value of an environment variable
|
|
89
|
+
*
|
|
90
|
+
* @param key - Variable key
|
|
91
|
+
* @param defaultValue - Default value if not found
|
|
92
|
+
* @param cwd - Current working directory for local scope
|
|
93
|
+
* @returns The value, or default/undefined if not found
|
|
94
|
+
*/
|
|
95
|
+
export function getEnvValue(key: string, defaultValue?: string, cwd?: string): string | undefined {
|
|
96
|
+
const result = getEnv(key, defaultValue, cwd);
|
|
97
|
+
return result?.value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Delete an environment variable
|
|
102
|
+
*
|
|
103
|
+
* @param key - Variable key
|
|
104
|
+
* @param scope - Where to delete from: 'local' or 'global' (default: 'global')
|
|
105
|
+
* @param cwd - Current working directory for local scope
|
|
106
|
+
* @returns true if variable existed and was deleted
|
|
107
|
+
*/
|
|
108
|
+
export function deleteEnv(
|
|
109
|
+
key: string,
|
|
110
|
+
scope: "local" | "global" = "global",
|
|
111
|
+
cwd?: string
|
|
112
|
+
): boolean {
|
|
113
|
+
if (scope === "local") {
|
|
114
|
+
return deleteLocalEnvVar(key, cwd);
|
|
115
|
+
}
|
|
116
|
+
return deleteGlobalEnvVar(key);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List all environment variables from a scope
|
|
121
|
+
*
|
|
122
|
+
* @param scope - Which scope to list: 'local', 'global', or 'all'
|
|
123
|
+
* @param cwd - Current working directory for local scope
|
|
124
|
+
* @returns Array of environment variables with sources
|
|
125
|
+
*/
|
|
126
|
+
export function listEnv(
|
|
127
|
+
scope: "local" | "global" | "all" = "all",
|
|
128
|
+
cwd?: string
|
|
129
|
+
): EnvironmentVariable[] {
|
|
130
|
+
const results: EnvironmentVariable[] = [];
|
|
131
|
+
|
|
132
|
+
if (scope === "global" || scope === "all") {
|
|
133
|
+
const globalVars = loadGlobalEnv();
|
|
134
|
+
for (const [key, value] of Object.entries(globalVars)) {
|
|
135
|
+
results.push({ key, value, source: "global" });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (scope === "local" || scope === "all") {
|
|
140
|
+
const localVars = loadLocalEnv(cwd);
|
|
141
|
+
for (const [key, value] of Object.entries(localVars)) {
|
|
142
|
+
// If 'all' scope, we might already have the key from global
|
|
143
|
+
// In that case, replace with local (higher priority)
|
|
144
|
+
if (scope === "all") {
|
|
145
|
+
const existingIndex = results.findIndex((v) => v.key === key);
|
|
146
|
+
if (existingIndex !== -1) {
|
|
147
|
+
results[existingIndex] = { key, value, source: "local" };
|
|
148
|
+
} else {
|
|
149
|
+
results.push({ key, value, source: "local" });
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
results.push({ key, value, source: "local" });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get all environment variables with priority resolution
|
|
162
|
+
* Returns the effective value for each key (local overrides global)
|
|
163
|
+
*
|
|
164
|
+
* @param defaults - Default values for keys
|
|
165
|
+
* @param cwd - Current working directory for local scope
|
|
166
|
+
* @returns Map of key to resolution result
|
|
167
|
+
*/
|
|
168
|
+
export function resolveAllEnv(
|
|
169
|
+
defaults: Record<string, string> = {},
|
|
170
|
+
cwd?: string
|
|
171
|
+
): Map<string, EnvResolution> {
|
|
172
|
+
const results = new Map<string, EnvResolution>();
|
|
173
|
+
|
|
174
|
+
// Start with defaults
|
|
175
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
176
|
+
results.set(key, { key, value, source: "default" });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Add global (overrides defaults)
|
|
180
|
+
const globalVars = loadGlobalEnv();
|
|
181
|
+
for (const [key, value] of Object.entries(globalVars)) {
|
|
182
|
+
results.set(key, {
|
|
183
|
+
key,
|
|
184
|
+
value,
|
|
185
|
+
source: "global",
|
|
186
|
+
filePath: getGlobalEnvPath(),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Add local (overrides global and defaults)
|
|
191
|
+
const localVars = loadLocalEnv(cwd);
|
|
192
|
+
for (const [key, value] of Object.entries(localVars)) {
|
|
193
|
+
results.set(key, {
|
|
194
|
+
key,
|
|
195
|
+
value,
|
|
196
|
+
source: "local",
|
|
197
|
+
filePath: getLocalEnvPath(cwd),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve environment variables for a tool manifest
|
|
206
|
+
* Checks that all required vars are present
|
|
207
|
+
*
|
|
208
|
+
* @param envDeclarations - Env declarations from manifest
|
|
209
|
+
* @param cwd - Current working directory
|
|
210
|
+
* @returns Object with resolved vars and any missing required vars
|
|
211
|
+
*/
|
|
212
|
+
export function resolveToolEnv(
|
|
213
|
+
envDeclarations: Record<string, { description?: string; default?: string; secret?: boolean }>,
|
|
214
|
+
cwd?: string
|
|
215
|
+
): {
|
|
216
|
+
resolved: Map<string, EnvResolution>;
|
|
217
|
+
missing: string[];
|
|
218
|
+
} {
|
|
219
|
+
const resolved = new Map<string, EnvResolution>();
|
|
220
|
+
const missing: string[] = [];
|
|
221
|
+
|
|
222
|
+
for (const [key, declaration] of Object.entries(envDeclarations)) {
|
|
223
|
+
// Skip secrets - they're handled by the keyring
|
|
224
|
+
if (declaration.secret) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = getEnv(key, declaration.default, cwd);
|
|
229
|
+
if (result) {
|
|
230
|
+
resolved.set(key, result);
|
|
231
|
+
} else {
|
|
232
|
+
missing.push(key);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { resolved, missing };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if local .env exists
|
|
241
|
+
*/
|
|
242
|
+
export function hasLocalEnv(cwd?: string): boolean {
|
|
243
|
+
return localEnvExists(cwd);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if global .env exists
|
|
248
|
+
*/
|
|
249
|
+
export function hasGlobalEnv(): boolean {
|
|
250
|
+
return globalEnvExists();
|
|
251
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .env file parser
|
|
3
|
+
*
|
|
4
|
+
* Parses KEY=VALUE format with support for:
|
|
5
|
+
* - Comments (lines starting with #)
|
|
6
|
+
* - Empty lines
|
|
7
|
+
* - Quoted values (single and double quotes)
|
|
8
|
+
* - Inline comments
|
|
9
|
+
* - Values with = signs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { EnvFileLine, ParsedEnvFile } from "../types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a single line from an .env file
|
|
16
|
+
*/
|
|
17
|
+
function parseLine(line: string): EnvFileLine {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
|
|
20
|
+
// Empty line
|
|
21
|
+
if (trimmed === "") {
|
|
22
|
+
return { type: "empty", raw: line };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Comment line
|
|
26
|
+
if (trimmed.startsWith("#")) {
|
|
27
|
+
return { type: "comment", raw: line };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Variable line - find the first =
|
|
31
|
+
const eqIndex = trimmed.indexOf("=");
|
|
32
|
+
if (eqIndex === -1) {
|
|
33
|
+
// No = sign, treat as comment (invalid line)
|
|
34
|
+
return { type: "comment", raw: line };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
38
|
+
let value = trimmed.slice(eqIndex + 1);
|
|
39
|
+
|
|
40
|
+
// Handle inline comments (but not inside quotes)
|
|
41
|
+
value = parseValue(value);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
type: "variable",
|
|
45
|
+
raw: line,
|
|
46
|
+
key,
|
|
47
|
+
value,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a value, handling quotes and inline comments
|
|
53
|
+
*/
|
|
54
|
+
function parseValue(rawValue: string): string {
|
|
55
|
+
let value = rawValue.trim();
|
|
56
|
+
|
|
57
|
+
// Handle quoted values
|
|
58
|
+
if (value.length > 1) {
|
|
59
|
+
const firstChar = value[0];
|
|
60
|
+
if (firstChar === '"' || firstChar === "'") {
|
|
61
|
+
const quote = firstChar;
|
|
62
|
+
// Find the end quote, accounting for escaped quotes
|
|
63
|
+
let endQuote = -1;
|
|
64
|
+
for (let i = 1; i < value.length; i++) {
|
|
65
|
+
if (value[i] === quote && value[i - 1] !== "\\") {
|
|
66
|
+
endQuote = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (endQuote !== -1) {
|
|
72
|
+
// Extract value between quotes
|
|
73
|
+
value = value.slice(1, endQuote);
|
|
74
|
+
// Handle escape sequences in double-quoted strings
|
|
75
|
+
if (quote === '"') {
|
|
76
|
+
value = value
|
|
77
|
+
.replace(/\\n/g, "\n")
|
|
78
|
+
.replace(/\\r/g, "\r")
|
|
79
|
+
.replace(/\\t/g, "\t")
|
|
80
|
+
.replace(/\\"/g, '"')
|
|
81
|
+
.replace(/\\\\/g, "\\");
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle inline comments for unquoted values
|
|
89
|
+
const commentIndex = value.indexOf(" #");
|
|
90
|
+
if (commentIndex !== -1) {
|
|
91
|
+
value = value.slice(0, commentIndex).trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse .env file content
|
|
99
|
+
*
|
|
100
|
+
* @param content - The file content to parse
|
|
101
|
+
* @returns Parsed env file with vars and preserved lines
|
|
102
|
+
*/
|
|
103
|
+
export function parseEnvFile(content: string): ParsedEnvFile {
|
|
104
|
+
const lines = content.split("\n");
|
|
105
|
+
const parsedLines: EnvFileLine[] = [];
|
|
106
|
+
const vars: Record<string, string> = {};
|
|
107
|
+
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const parsed = parseLine(line);
|
|
110
|
+
parsedLines.push(parsed);
|
|
111
|
+
|
|
112
|
+
if (parsed.type === "variable" && parsed.key && parsed.value !== undefined) {
|
|
113
|
+
vars[parsed.key] = parsed.value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
vars,
|
|
119
|
+
lines: parsedLines,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse .env file content to simple key-value object
|
|
125
|
+
*
|
|
126
|
+
* @param content - The file content to parse
|
|
127
|
+
* @returns Object with key-value pairs
|
|
128
|
+
*/
|
|
129
|
+
export function parseEnvContent(content: string): Record<string, string> {
|
|
130
|
+
return parseEnvFile(content).vars;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Serialize an env file back to string format
|
|
135
|
+
* Preserves comments and formatting
|
|
136
|
+
*
|
|
137
|
+
* @param parsed - The parsed env file
|
|
138
|
+
* @returns String content for .env file
|
|
139
|
+
*/
|
|
140
|
+
export function serializeEnvFile(parsed: ParsedEnvFile): string {
|
|
141
|
+
return parsed.lines
|
|
142
|
+
.map((line) => {
|
|
143
|
+
if (line.type === "variable" && line.key) {
|
|
144
|
+
const value = parsed.vars[line.key] ?? line.value ?? "";
|
|
145
|
+
// Quote values with spaces or special characters
|
|
146
|
+
if (value.includes(" ") || value.includes("=") || value.includes("#")) {
|
|
147
|
+
return `${line.key}="${value.replace(/"/g, '\\"')}"`;
|
|
148
|
+
}
|
|
149
|
+
return `${line.key}=${value}`;
|
|
150
|
+
}
|
|
151
|
+
return line.raw;
|
|
152
|
+
})
|
|
153
|
+
.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a new env file content from a vars object
|
|
158
|
+
*
|
|
159
|
+
* @param vars - Key-value pairs to serialize
|
|
160
|
+
* @returns String content for .env file
|
|
161
|
+
*/
|
|
162
|
+
export function createEnvContent(vars: Record<string, string>): string {
|
|
163
|
+
const lines: string[] = [];
|
|
164
|
+
|
|
165
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
166
|
+
// Quote values with spaces or special characters
|
|
167
|
+
if (value.includes(" ") || value.includes("=") || value.includes("#")) {
|
|
168
|
+
lines.push(`${key}="${value.replace(/"/g, '\\"')}"`);
|
|
169
|
+
} else {
|
|
170
|
+
lines.push(`${key}=${value}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Update a single variable in parsed env file
|
|
179
|
+
*
|
|
180
|
+
* @param parsed - The parsed env file
|
|
181
|
+
* @param key - The variable key
|
|
182
|
+
* @param value - The new value
|
|
183
|
+
* @returns Updated parsed env file
|
|
184
|
+
*/
|
|
185
|
+
export function updateEnvVar(parsed: ParsedEnvFile, key: string, value: string): ParsedEnvFile {
|
|
186
|
+
// Update the vars
|
|
187
|
+
const newVars = { ...parsed.vars, [key]: value };
|
|
188
|
+
|
|
189
|
+
// Check if key exists in lines
|
|
190
|
+
const existingIndex = parsed.lines.findIndex(
|
|
191
|
+
(line) => line.type === "variable" && line.key === key
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
let newLines: EnvFileLine[];
|
|
195
|
+
if (existingIndex !== -1) {
|
|
196
|
+
// Update existing line
|
|
197
|
+
newLines = [...parsed.lines];
|
|
198
|
+
newLines[existingIndex] = {
|
|
199
|
+
type: "variable",
|
|
200
|
+
raw: `${key}=${value}`,
|
|
201
|
+
key,
|
|
202
|
+
value,
|
|
203
|
+
};
|
|
204
|
+
} else {
|
|
205
|
+
// Add new line at end
|
|
206
|
+
newLines = [
|
|
207
|
+
...parsed.lines,
|
|
208
|
+
{
|
|
209
|
+
type: "variable",
|
|
210
|
+
raw: `${key}=${value}`,
|
|
211
|
+
key,
|
|
212
|
+
value,
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
vars: newVars,
|
|
219
|
+
lines: newLines,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Remove a variable from parsed env file
|
|
225
|
+
*
|
|
226
|
+
* @param parsed - The parsed env file
|
|
227
|
+
* @param key - The variable key to remove
|
|
228
|
+
* @returns Updated parsed env file
|
|
229
|
+
*/
|
|
230
|
+
export function removeEnvVar(parsed: ParsedEnvFile, key: string): ParsedEnvFile {
|
|
231
|
+
const newVars = { ...parsed.vars };
|
|
232
|
+
delete newVars[key];
|
|
233
|
+
|
|
234
|
+
const newLines = parsed.lines.filter((line) => !(line.type === "variable" && line.key === key));
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
vars: newVars,
|
|
238
|
+
lines: newLines,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .env file reader
|
|
3
|
+
*
|
|
4
|
+
* Reads .env files from global (~/.enact/.env) and local (.enact/.env) locations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import type { ParsedEnvFile } from "../types";
|
|
11
|
+
import { parseEnvContent, parseEnvFile } from "./parser";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the path to the global .env file
|
|
15
|
+
*/
|
|
16
|
+
export function getGlobalEnvPath(): string {
|
|
17
|
+
return join(homedir(), ".enact", ".env");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the path to the local (project) .env file
|
|
22
|
+
*
|
|
23
|
+
* @param cwd - Current working directory (defaults to process.cwd())
|
|
24
|
+
*/
|
|
25
|
+
export function getLocalEnvPath(cwd?: string): string {
|
|
26
|
+
return join(cwd ?? process.cwd(), ".enact", ".env");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read and parse an .env file
|
|
31
|
+
*
|
|
32
|
+
* @param path - Path to the .env file
|
|
33
|
+
* @returns Parsed env file, or empty if file doesn't exist
|
|
34
|
+
*/
|
|
35
|
+
export function readEnvFile(path: string): ParsedEnvFile {
|
|
36
|
+
if (!existsSync(path)) {
|
|
37
|
+
return { vars: {}, lines: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = readFileSync(path, "utf-8");
|
|
41
|
+
return parseEnvFile(content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read .env file to simple key-value object
|
|
46
|
+
*
|
|
47
|
+
* @param path - Path to the .env file
|
|
48
|
+
* @returns Object with key-value pairs, empty object if file doesn't exist
|
|
49
|
+
*/
|
|
50
|
+
export function readEnvVars(path: string): Record<string, string> {
|
|
51
|
+
if (!existsSync(path)) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const content = readFileSync(path, "utf-8");
|
|
56
|
+
return parseEnvContent(content);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load global environment variables from ~/.enact/.env
|
|
61
|
+
*/
|
|
62
|
+
export function loadGlobalEnv(): Record<string, string> {
|
|
63
|
+
return readEnvVars(getGlobalEnvPath());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load local (project) environment variables from .enact/.env
|
|
68
|
+
*
|
|
69
|
+
* @param cwd - Current working directory (defaults to process.cwd())
|
|
70
|
+
*/
|
|
71
|
+
export function loadLocalEnv(cwd?: string): Record<string, string> {
|
|
72
|
+
return readEnvVars(getLocalEnvPath(cwd));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load global environment as parsed file (for updates)
|
|
77
|
+
*/
|
|
78
|
+
export function loadGlobalEnvFile(): ParsedEnvFile {
|
|
79
|
+
return readEnvFile(getGlobalEnvPath());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load local environment as parsed file (for updates)
|
|
84
|
+
*
|
|
85
|
+
* @param cwd - Current working directory (defaults to process.cwd())
|
|
86
|
+
*/
|
|
87
|
+
export function loadLocalEnvFile(cwd?: string): ParsedEnvFile {
|
|
88
|
+
return readEnvFile(getLocalEnvPath(cwd));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if global .env file exists
|
|
93
|
+
*/
|
|
94
|
+
export function globalEnvExists(): boolean {
|
|
95
|
+
return existsSync(getGlobalEnvPath());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if local .env file exists
|
|
100
|
+
*
|
|
101
|
+
* @param cwd - Current working directory (defaults to process.cwd())
|
|
102
|
+
*/
|
|
103
|
+
export function localEnvExists(cwd?: string): boolean {
|
|
104
|
+
return existsSync(getLocalEnvPath(cwd));
|
|
105
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .env file writer
|
|
3
|
+
*
|
|
4
|
+
* Writes .env files while preserving comments and formatting
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import type { ParsedEnvFile } from "../types";
|
|
10
|
+
import { createEnvContent, removeEnvVar, serializeEnvFile, updateEnvVar } from "./parser";
|
|
11
|
+
import { getGlobalEnvPath, getLocalEnvPath, readEnvFile } from "./reader";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Ensure directory exists for a file path
|
|
15
|
+
*/
|
|
16
|
+
function ensureDirectory(filePath: string): void {
|
|
17
|
+
const dir = dirname(filePath);
|
|
18
|
+
if (!existsSync(dir)) {
|
|
19
|
+
mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Write a parsed env file to disk
|
|
25
|
+
*
|
|
26
|
+
* @param path - Path to write to
|
|
27
|
+
* @param parsed - Parsed env file to write
|
|
28
|
+
*/
|
|
29
|
+
export function writeEnvFile(path: string, parsed: ParsedEnvFile): void {
|
|
30
|
+
ensureDirectory(path);
|
|
31
|
+
const content = serializeEnvFile(parsed);
|
|
32
|
+
writeFileSync(path, content, "utf-8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write a vars object to an .env file
|
|
37
|
+
*
|
|
38
|
+
* @param path - Path to write to
|
|
39
|
+
* @param vars - Key-value pairs to write
|
|
40
|
+
*/
|
|
41
|
+
export function writeEnvVars(path: string, vars: Record<string, string>): void {
|
|
42
|
+
ensureDirectory(path);
|
|
43
|
+
const content = createEnvContent(vars);
|
|
44
|
+
writeFileSync(path, content, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set an environment variable in a file
|
|
49
|
+
* Creates file if it doesn't exist, preserves existing content
|
|
50
|
+
*
|
|
51
|
+
* @param path - Path to the .env file
|
|
52
|
+
* @param key - Variable key
|
|
53
|
+
* @param value - Variable value
|
|
54
|
+
*/
|
|
55
|
+
export function setEnvVar(path: string, key: string, value: string): void {
|
|
56
|
+
const parsed = readEnvFile(path);
|
|
57
|
+
const updated = updateEnvVar(parsed, key, value);
|
|
58
|
+
writeEnvFile(path, updated);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Delete an environment variable from a file
|
|
63
|
+
*
|
|
64
|
+
* @param path - Path to the .env file
|
|
65
|
+
* @param key - Variable key to delete
|
|
66
|
+
* @returns true if variable existed and was deleted
|
|
67
|
+
*/
|
|
68
|
+
export function deleteEnvVar(path: string, key: string): boolean {
|
|
69
|
+
const parsed = readEnvFile(path);
|
|
70
|
+
if (!(key in parsed.vars)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const updated = removeEnvVar(parsed, key);
|
|
74
|
+
writeEnvFile(path, updated);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set a global environment variable (~/.enact/.env)
|
|
80
|
+
*
|
|
81
|
+
* @param key - Variable key
|
|
82
|
+
* @param value - Variable value
|
|
83
|
+
*/
|
|
84
|
+
export function setGlobalEnvVar(key: string, value: string): void {
|
|
85
|
+
setEnvVar(getGlobalEnvPath(), key, value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set a local environment variable (.enact/.env)
|
|
90
|
+
*
|
|
91
|
+
* @param key - Variable key
|
|
92
|
+
* @param value - Variable value
|
|
93
|
+
* @param cwd - Current working directory (defaults to process.cwd())
|
|
94
|
+
*/
|
|
95
|
+
export function setLocalEnvVar(key: string, value: string, cwd?: string): void {
|
|
96
|
+
setEnvVar(getLocalEnvPath(cwd), key, value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Delete a global environment variable
|
|
101
|
+
*
|
|
102
|
+
* @param key - Variable key to delete
|
|
103
|
+
* @returns true if variable existed and was deleted
|
|
104
|
+
*/
|
|
105
|
+
export function deleteGlobalEnvVar(key: string): boolean {
|
|
106
|
+
return deleteEnvVar(getGlobalEnvPath(), key);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Delete a local environment variable
|
|
111
|
+
*
|
|
112
|
+
* @param key - Variable key to delete
|
|
113
|
+
* @param cwd - Current working directory (defaults to process.cwd())
|
|
114
|
+
* @returns true if variable existed and was deleted
|
|
115
|
+
*/
|
|
116
|
+
export function deleteLocalEnvVar(key: string, cwd?: string): boolean {
|
|
117
|
+
return deleteEnvVar(getLocalEnvPath(cwd), key);
|
|
118
|
+
}
|