@effect-x/envault 0.1.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 +53 -0
- package/dist/assets/highlights-CBXJmSNL.scm +205 -0
- package/dist/assets/highlights-D8AwpUmb.scm +284 -0
- package/dist/assets/highlights-Dj7C7yga.scm +150 -0
- package/dist/assets/highlights-_y98o7mL.scm +604 -0
- package/dist/assets/highlights-oYFuV_X8.scm +115 -0
- package/dist/assets/injections-jD0OqTrg.scm +27 -0
- package/dist/assets/tree-sitter-javascript-BIQS8cIx.wasm +0 -0
- package/dist/assets/tree-sitter-markdown-BcT6zdLB.wasm +0 -0
- package/dist/assets/tree-sitter-markdown_inline-C00NH7xW.wasm +0 -0
- package/dist/assets/tree-sitter-typescript-BQpamM5l.wasm +0 -0
- package/dist/assets/tree-sitter-zig-g3m_vtcZ.wasm +0 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +58845 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-2mx7fq49-DWgCruu_.js +5 -0
- package/dist/chunk-2mx7fq49-DWgCruu_.js.map +1 -0
- package/dist/chunk-C-LSD6C2.js +14 -0
- package/dist/chunk-bdqvmfwv-WZRmk9C2.js +10212 -0
- package/dist/chunk-bdqvmfwv-WZRmk9C2.js.map +1 -0
- package/dist/wrapper-BUyLZ9aM.js +1606 -0
- package/dist/wrapper-BUyLZ9aM.js.map +1 -0
- package/package.json +110 -0
- package/src/bin.ts +53 -0
- package/src/domain.ts +56 -0
- package/src/env-file.ts +440 -0
- package/src/tui/App.tsx +1591 -0
- package/src/tui/atoms.ts +227 -0
- package/src/tui/colors.ts +58 -0
- package/src/tui/modal.ts +15 -0
- package/src/tui/primitives.tsx +168 -0
- package/src/tui/runTui.tsx +68 -0
- package/src/tui/types.ts +127 -0
- package/src/workspace.ts +120 -0
package/src/domain.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type EnvVariable = {
|
|
2
|
+
readonly key: string;
|
|
3
|
+
readonly value: string;
|
|
4
|
+
readonly rawValue: string;
|
|
5
|
+
readonly encrypted: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type EnvFileInfo = {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly absolutePath: string;
|
|
11
|
+
readonly variables: ReadonlyArray<EnvVariable>;
|
|
12
|
+
readonly encrypted: boolean;
|
|
13
|
+
readonly hasKeys: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type EnvScanOptions = {
|
|
17
|
+
readonly root: string;
|
|
18
|
+
readonly decrypt?: boolean | undefined;
|
|
19
|
+
readonly maxDepth?: number | undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type EnvFileRequest = {
|
|
23
|
+
readonly root: string;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly decrypt?: boolean | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type SetEnvVariableRequest = {
|
|
29
|
+
readonly root: string;
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly key: string;
|
|
32
|
+
readonly value: string;
|
|
33
|
+
readonly decrypt?: boolean | undefined;
|
|
34
|
+
readonly encrypt?: boolean | undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type CreateEnvFileRequest = {
|
|
38
|
+
readonly root: string;
|
|
39
|
+
readonly path: string;
|
|
40
|
+
readonly decrypt?: boolean | undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type DeleteEnvVariableRequest = {
|
|
44
|
+
readonly root: string;
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly key: string;
|
|
47
|
+
readonly decrypt?: boolean | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type WorkspaceKind = "single" | "pnpm" | "npm" | "moon" | "nx";
|
|
51
|
+
|
|
52
|
+
export type WorkspaceInfo = {
|
|
53
|
+
readonly root: string;
|
|
54
|
+
readonly kind: WorkspaceKind;
|
|
55
|
+
readonly packageDirectories: ReadonlyArray<string>;
|
|
56
|
+
};
|
package/src/env-file.ts
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { config as dotenvConfig, set as dotenvSet } from "@dotenvx/dotenvx";
|
|
2
|
+
import { Effect, Layer, Schema, Context } from "effect";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type {
|
|
6
|
+
DeleteEnvVariableRequest,
|
|
7
|
+
CreateEnvFileRequest,
|
|
8
|
+
EnvFileInfo,
|
|
9
|
+
EnvFileRequest,
|
|
10
|
+
EnvScanOptions,
|
|
11
|
+
EnvVariable,
|
|
12
|
+
SetEnvVariableRequest,
|
|
13
|
+
} from "./domain.ts";
|
|
14
|
+
|
|
15
|
+
export class EnvFileError extends Schema.TaggedErrorClass<EnvFileError>()("EnvFileError", {
|
|
16
|
+
message: Schema.String,
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
export class EnvFileService extends Context.Service<
|
|
20
|
+
EnvFileService,
|
|
21
|
+
{
|
|
22
|
+
readonly scan: (
|
|
23
|
+
options: EnvScanOptions,
|
|
24
|
+
) => Effect.Effect<ReadonlyArray<EnvFileInfo>, EnvFileError>;
|
|
25
|
+
readonly get: (request: EnvFileRequest) => Effect.Effect<EnvFileInfo, EnvFileError>;
|
|
26
|
+
readonly createFile: (
|
|
27
|
+
request: CreateEnvFileRequest,
|
|
28
|
+
) => Effect.Effect<EnvFileInfo, EnvFileError>;
|
|
29
|
+
readonly setVariable: (
|
|
30
|
+
request: SetEnvVariableRequest,
|
|
31
|
+
) => Effect.Effect<EnvFileInfo, EnvFileError>;
|
|
32
|
+
readonly deleteVariable: (
|
|
33
|
+
request: DeleteEnvVariableRequest,
|
|
34
|
+
) => Effect.Effect<EnvFileInfo, EnvFileError>;
|
|
35
|
+
}
|
|
36
|
+
>()("EnvFileService") {}
|
|
37
|
+
|
|
38
|
+
export declare namespace EnvFileService {
|
|
39
|
+
export type Methods = Context.Service.Shape<typeof EnvFileService>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const skipDirectories = new Set([
|
|
43
|
+
".cache",
|
|
44
|
+
".git",
|
|
45
|
+
".jj",
|
|
46
|
+
".moon/cache",
|
|
47
|
+
".next",
|
|
48
|
+
".nx",
|
|
49
|
+
".turbo",
|
|
50
|
+
"build",
|
|
51
|
+
"coverage",
|
|
52
|
+
"dist",
|
|
53
|
+
"node_modules",
|
|
54
|
+
"out",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const keyPattern = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
58
|
+
|
|
59
|
+
export const isEnvFileName = (name: string): boolean =>
|
|
60
|
+
name.startsWith(".env") && name !== ".envrc" && !name.endsWith(".keys");
|
|
61
|
+
|
|
62
|
+
export const isEncryptedValue = (value: string): boolean => value.startsWith("encrypted:");
|
|
63
|
+
|
|
64
|
+
const toEnvFileError = (message: string) => (cause: unknown) =>
|
|
65
|
+
new EnvFileError({ message: `${message}: ${String(cause)}` });
|
|
66
|
+
|
|
67
|
+
const normalizeRoot = (root: string): string => path.resolve(root);
|
|
68
|
+
|
|
69
|
+
const relativeEnvPath = (root: string, absolutePath: string): string =>
|
|
70
|
+
path.relative(root, absolutePath).split(path.sep).join("/");
|
|
71
|
+
|
|
72
|
+
const resolveEnvPath = (root: string, inputPath: string): Effect.Effect<string, EnvFileError> =>
|
|
73
|
+
Effect.sync(() => {
|
|
74
|
+
const normalizedRoot = normalizeRoot(root);
|
|
75
|
+
const target = path.resolve(normalizedRoot, inputPath);
|
|
76
|
+
const relative = path.relative(normalizedRoot, target);
|
|
77
|
+
|
|
78
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
79
|
+
throw new EnvFileError({ message: `Env file path escapes root: ${inputPath}` });
|
|
80
|
+
}
|
|
81
|
+
if (!isEnvFileName(path.basename(target))) {
|
|
82
|
+
throw new EnvFileError({ message: `Not an env file: ${inputPath}` });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return target;
|
|
86
|
+
}).pipe(
|
|
87
|
+
Effect.mapError(
|
|
88
|
+
(cause) => new EnvFileError({ message: `Invalid env file path: ${String(cause)}` }),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const readText = (target: string): Effect.Effect<string, EnvFileError> =>
|
|
93
|
+
Effect.tryPromise({
|
|
94
|
+
try: () => fs.readFile(target, "utf8"),
|
|
95
|
+
catch: toEnvFileError(`Failed to read ${target}`),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const writeText = (target: string, content: string): Effect.Effect<void, EnvFileError> =>
|
|
99
|
+
Effect.tryPromise({
|
|
100
|
+
try: () => fs.writeFile(target, content),
|
|
101
|
+
catch: toEnvFileError(`Failed to write ${target}`),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const createEmptyFile = (target: string): Effect.Effect<void, EnvFileError> =>
|
|
105
|
+
Effect.tryPromise({
|
|
106
|
+
try: async () => {
|
|
107
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
108
|
+
await fs.writeFile(target, "", { flag: "wx" });
|
|
109
|
+
},
|
|
110
|
+
catch: toEnvFileError(`Failed to create ${target}`),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const exists = (target: string): Effect.Effect<boolean> =>
|
|
114
|
+
Effect.tryPromise({
|
|
115
|
+
try: async () => {
|
|
116
|
+
await fs.access(target);
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
119
|
+
catch: () => new EnvFileError({ message: `Path does not exist: ${target}` }),
|
|
120
|
+
}).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
121
|
+
|
|
122
|
+
const parseVariables = (content: string): ReadonlyArray<EnvVariable> => {
|
|
123
|
+
const variables: Array<EnvVariable> = [];
|
|
124
|
+
|
|
125
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
128
|
+
|
|
129
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u);
|
|
130
|
+
if (match === null) continue;
|
|
131
|
+
|
|
132
|
+
const key = match[1] ?? "";
|
|
133
|
+
let rawValue = match[2] ?? "";
|
|
134
|
+
if (
|
|
135
|
+
(rawValue.startsWith('"') && rawValue.endsWith('"')) ||
|
|
136
|
+
(rawValue.startsWith("'") && rawValue.endsWith("'"))
|
|
137
|
+
) {
|
|
138
|
+
rawValue = rawValue.slice(1, -1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
variables.push({
|
|
142
|
+
key,
|
|
143
|
+
value: isEncryptedValue(rawValue) ? "***" : rawValue,
|
|
144
|
+
rawValue,
|
|
145
|
+
encrypted: isEncryptedValue(rawValue),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return variables;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const decryptVariables = ({
|
|
153
|
+
absolutePath,
|
|
154
|
+
keysPath,
|
|
155
|
+
variables,
|
|
156
|
+
}: {
|
|
157
|
+
readonly absolutePath: string;
|
|
158
|
+
readonly keysPath: string;
|
|
159
|
+
readonly variables: ReadonlyArray<EnvVariable>;
|
|
160
|
+
}): ReadonlyArray<EnvVariable> => {
|
|
161
|
+
try {
|
|
162
|
+
const result = dotenvConfig({
|
|
163
|
+
envKeysFile: keysPath,
|
|
164
|
+
ignore: ["MISSING_ENV_FILE"],
|
|
165
|
+
path: [absolutePath],
|
|
166
|
+
processEnv: {},
|
|
167
|
+
quiet: true,
|
|
168
|
+
});
|
|
169
|
+
const parsed = result.parsed ?? {};
|
|
170
|
+
return variables.map((variable) => ({
|
|
171
|
+
...variable,
|
|
172
|
+
value: parsed[variable.key] ?? variable.value,
|
|
173
|
+
}));
|
|
174
|
+
} catch {
|
|
175
|
+
return variables;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const parseEnvFile = ({
|
|
180
|
+
absolutePath,
|
|
181
|
+
root,
|
|
182
|
+
decrypt,
|
|
183
|
+
}: {
|
|
184
|
+
readonly absolutePath: string;
|
|
185
|
+
readonly root: string;
|
|
186
|
+
readonly decrypt: boolean;
|
|
187
|
+
}): Effect.Effect<EnvFileInfo, EnvFileError> =>
|
|
188
|
+
Effect.gen(function* () {
|
|
189
|
+
const content = yield* readText(absolutePath);
|
|
190
|
+
const rootKeysPath = path.join(root, ".env.keys");
|
|
191
|
+
const siblingKeysPath = path.join(path.dirname(absolutePath), ".env.keys");
|
|
192
|
+
const hasRootKeys = yield* exists(rootKeysPath);
|
|
193
|
+
const hasSiblingKeys = hasRootKeys ? false : yield* exists(siblingKeysPath);
|
|
194
|
+
const keysPath = hasRootKeys ? rootKeysPath : siblingKeysPath;
|
|
195
|
+
const hasKeys = hasRootKeys || hasSiblingKeys;
|
|
196
|
+
const parsedVariables = parseVariables(content);
|
|
197
|
+
const encrypted = parsedVariables.some((variable) => variable.encrypted);
|
|
198
|
+
const variables =
|
|
199
|
+
decrypt && encrypted && hasKeys
|
|
200
|
+
? decryptVariables({ absolutePath, keysPath, variables: parsedVariables })
|
|
201
|
+
: parsedVariables;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
absolutePath,
|
|
205
|
+
encrypted,
|
|
206
|
+
hasKeys,
|
|
207
|
+
path: relativeEnvPath(root, absolutePath),
|
|
208
|
+
variables,
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const shouldSkipDirectory = (name: string, relativePath: string): boolean =>
|
|
213
|
+
skipDirectories.has(name) || skipDirectories.has(relativePath);
|
|
214
|
+
|
|
215
|
+
const findEnvFiles = ({
|
|
216
|
+
maxDepth,
|
|
217
|
+
root,
|
|
218
|
+
}: {
|
|
219
|
+
readonly maxDepth: number;
|
|
220
|
+
readonly root: string;
|
|
221
|
+
}): Effect.Effect<ReadonlyArray<string>, EnvFileError> =>
|
|
222
|
+
Effect.tryPromise({
|
|
223
|
+
try: async () => {
|
|
224
|
+
const results: Array<string> = [];
|
|
225
|
+
|
|
226
|
+
const visit = async (directory: string, depth: number): Promise<void> => {
|
|
227
|
+
if (depth > maxDepth) return;
|
|
228
|
+
|
|
229
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
const absolutePath = path.join(directory, entry.name);
|
|
232
|
+
const relativePath = relativeEnvPath(root, absolutePath);
|
|
233
|
+
|
|
234
|
+
if (entry.isDirectory()) {
|
|
235
|
+
if (!shouldSkipDirectory(entry.name, relativePath)) {
|
|
236
|
+
await visit(absolutePath, depth + 1);
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (entry.isFile() && isEnvFileName(entry.name)) {
|
|
242
|
+
results.push(absolutePath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
await visit(root, 0);
|
|
248
|
+
return results.sort((left, right) =>
|
|
249
|
+
relativeEnvPath(root, left).localeCompare(relativeEnvPath(root, right)),
|
|
250
|
+
);
|
|
251
|
+
},
|
|
252
|
+
catch: toEnvFileError(`Failed to scan env files under ${root}`),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const escapeQuotedValue = (value: string, quote: string): string =>
|
|
256
|
+
quote === '"' ? value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"') : value.replace(/'/gu, "\\'");
|
|
257
|
+
|
|
258
|
+
const renderNewValue = (value: string): string =>
|
|
259
|
+
/[\s#"']/u.test(value) ? `"${escapeQuotedValue(value, '"')}"` : value;
|
|
260
|
+
|
|
261
|
+
const splitValueAndComment = (rest: string) => {
|
|
262
|
+
let quote: string | undefined;
|
|
263
|
+
let escaped = false;
|
|
264
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
265
|
+
const char = rest[index];
|
|
266
|
+
if (escaped) {
|
|
267
|
+
escaped = false;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (char === "\\") {
|
|
271
|
+
escaped = true;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (quote !== undefined) {
|
|
275
|
+
if (char === quote) quote = undefined;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (char === '"' || char === "'") {
|
|
279
|
+
quote = char;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (char === "#" && (index === 0 || /\s/u.test(rest[index - 1] ?? ""))) {
|
|
283
|
+
const rawValueWithWhitespace = rest.slice(0, index);
|
|
284
|
+
const trailingWhitespace = rawValueWithWhitespace.match(/\s*$/u)?.[0] ?? "";
|
|
285
|
+
return {
|
|
286
|
+
valuePart: rawValueWithWhitespace.slice(
|
|
287
|
+
0,
|
|
288
|
+
rawValueWithWhitespace.length - trailingWhitespace.length,
|
|
289
|
+
),
|
|
290
|
+
commentPart: `${trailingWhitespace}${rest.slice(index)}`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { valuePart: rest, commentPart: "" };
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const renderReplacementLine = (line: string, key: string, value: string): string | undefined => {
|
|
298
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
299
|
+
const match = line.match(new RegExp(`^(\\s*(?:export\\s+)?${escapedKey}\\s*=)(.*)$`, "u"));
|
|
300
|
+
if (match === null) return undefined;
|
|
301
|
+
const prefix = match[1] ?? "";
|
|
302
|
+
const rest = match[2] ?? "";
|
|
303
|
+
const { valuePart, commentPart } = splitValueAndComment(rest);
|
|
304
|
+
const trimmedValue = valuePart.trim();
|
|
305
|
+
const quote =
|
|
306
|
+
trimmedValue.startsWith('"') && trimmedValue.endsWith('"')
|
|
307
|
+
? '"'
|
|
308
|
+
: trimmedValue.startsWith("'") && trimmedValue.endsWith("'")
|
|
309
|
+
? "'"
|
|
310
|
+
: undefined;
|
|
311
|
+
const rendered =
|
|
312
|
+
quote === undefined
|
|
313
|
+
? renderNewValue(value)
|
|
314
|
+
: `${quote}${escapeQuotedValue(value, quote)}${quote}`;
|
|
315
|
+
return `${prefix}${rendered}${commentPart}`;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const setVariableInContent = ({
|
|
319
|
+
content,
|
|
320
|
+
key,
|
|
321
|
+
value,
|
|
322
|
+
}: {
|
|
323
|
+
readonly content: string;
|
|
324
|
+
readonly key: string;
|
|
325
|
+
readonly value: string;
|
|
326
|
+
}): string => {
|
|
327
|
+
const lines = content.split(/\r?\n/u);
|
|
328
|
+
let replaced = false;
|
|
329
|
+
|
|
330
|
+
const nextLines = lines.map((line) => {
|
|
331
|
+
const replacement = renderReplacementLine(line, key, value);
|
|
332
|
+
if (replacement === undefined) return line;
|
|
333
|
+
replaced = true;
|
|
334
|
+
return replacement;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (replaced) {
|
|
338
|
+
return nextLines.join("\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const suffix = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
342
|
+
return `${content}${suffix}${key}=${renderNewValue(value)}\n`;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const deleteVariableFromContent = ({
|
|
346
|
+
content,
|
|
347
|
+
key,
|
|
348
|
+
}: {
|
|
349
|
+
readonly content: string;
|
|
350
|
+
readonly key: string;
|
|
351
|
+
}): string =>
|
|
352
|
+
content
|
|
353
|
+
.split(/\r?\n/u)
|
|
354
|
+
.filter((line) => renderReplacementLine(line, key, "") === undefined)
|
|
355
|
+
.join("\n");
|
|
356
|
+
|
|
357
|
+
const setEncryptedVariable = ({
|
|
358
|
+
absolutePath,
|
|
359
|
+
key,
|
|
360
|
+
root,
|
|
361
|
+
value,
|
|
362
|
+
}: {
|
|
363
|
+
readonly absolutePath: string;
|
|
364
|
+
readonly key: string;
|
|
365
|
+
readonly root: string;
|
|
366
|
+
readonly value: string;
|
|
367
|
+
}): Effect.Effect<void, EnvFileError> =>
|
|
368
|
+
Effect.try({
|
|
369
|
+
try: () => {
|
|
370
|
+
dotenvSet(key, value, {
|
|
371
|
+
envKeysFile: path.join(root, ".env.keys"),
|
|
372
|
+
noOps: true,
|
|
373
|
+
path: absolutePath,
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
catch: toEnvFileError(`Failed to encrypt ${key} in ${absolutePath}`),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const validateKey = (key: string): Effect.Effect<void, EnvFileError> =>
|
|
380
|
+
keyPattern.test(key)
|
|
381
|
+
? Effect.void
|
|
382
|
+
: Effect.fail(new EnvFileError({ message: `Invalid env variable key: ${key}` }));
|
|
383
|
+
|
|
384
|
+
export const EnvFileServiceLive = Layer.succeed(EnvFileService)(
|
|
385
|
+
EnvFileService.of({
|
|
386
|
+
scan: (options) =>
|
|
387
|
+
Effect.gen(function* () {
|
|
388
|
+
const root = normalizeRoot(options.root);
|
|
389
|
+
const paths = yield* findEnvFiles({ root, maxDepth: options.maxDepth ?? 6 });
|
|
390
|
+
return yield* Effect.all(
|
|
391
|
+
paths.map((absolutePath) =>
|
|
392
|
+
parseEnvFile({ absolutePath, root, decrypt: options.decrypt ?? false }),
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
}),
|
|
396
|
+
get: (request) =>
|
|
397
|
+
Effect.gen(function* () {
|
|
398
|
+
const root = normalizeRoot(request.root);
|
|
399
|
+
const absolutePath = yield* resolveEnvPath(root, request.path);
|
|
400
|
+
return yield* parseEnvFile({ absolutePath, root, decrypt: request.decrypt ?? false });
|
|
401
|
+
}),
|
|
402
|
+
createFile: (request) =>
|
|
403
|
+
Effect.gen(function* () {
|
|
404
|
+
const root = normalizeRoot(request.root);
|
|
405
|
+
const absolutePath = yield* resolveEnvPath(root, request.path);
|
|
406
|
+
yield* createEmptyFile(absolutePath);
|
|
407
|
+
return yield* parseEnvFile({ absolutePath, root, decrypt: request.decrypt ?? false });
|
|
408
|
+
}),
|
|
409
|
+
setVariable: (request) =>
|
|
410
|
+
Effect.gen(function* () {
|
|
411
|
+
yield* validateKey(request.key);
|
|
412
|
+
const root = normalizeRoot(request.root);
|
|
413
|
+
const absolutePath = yield* resolveEnvPath(root, request.path);
|
|
414
|
+
if (request.encrypt === true) {
|
|
415
|
+
yield* setEncryptedVariable({
|
|
416
|
+
absolutePath,
|
|
417
|
+
root,
|
|
418
|
+
key: request.key,
|
|
419
|
+
value: request.value,
|
|
420
|
+
});
|
|
421
|
+
} else {
|
|
422
|
+
const content = yield* readText(absolutePath);
|
|
423
|
+
yield* writeText(
|
|
424
|
+
absolutePath,
|
|
425
|
+
setVariableInContent({ content, key: request.key, value: request.value }),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return yield* parseEnvFile({ absolutePath, root, decrypt: request.decrypt ?? false });
|
|
429
|
+
}),
|
|
430
|
+
deleteVariable: (request) =>
|
|
431
|
+
Effect.gen(function* () {
|
|
432
|
+
yield* validateKey(request.key);
|
|
433
|
+
const root = normalizeRoot(request.root);
|
|
434
|
+
const absolutePath = yield* resolveEnvPath(root, request.path);
|
|
435
|
+
const content = yield* readText(absolutePath);
|
|
436
|
+
yield* writeText(absolutePath, deleteVariableFromContent({ content, key: request.key }));
|
|
437
|
+
return yield* parseEnvFile({ absolutePath, root, decrypt: request.decrypt ?? false });
|
|
438
|
+
}),
|
|
439
|
+
}),
|
|
440
|
+
);
|