@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.
@@ -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
+ }