@saasak/tool-env 1.1.0 → 1.2.1
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/bin/index.js +157 -115
- package/package.json +2 -1
- package/src/core.js +120 -72
- package/src/index.d.ts +82 -0
- package/src/utils-env.js +23 -18
package/bin/index.js
CHANGED
|
@@ -1,65 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import minimist from
|
|
4
|
-
import fs from
|
|
5
|
-
import path from
|
|
6
|
-
import { execa } from
|
|
7
|
-
|
|
8
|
-
import { encrypt, isEncrypted } from
|
|
9
|
-
import { buildEnv } from
|
|
10
|
-
import { findMonorepoPackages } from
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
import minimist from "minimist";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { execa } from "execa";
|
|
7
|
+
|
|
8
|
+
import { encrypt, isEncrypted } from "../src/utils-crypto.js";
|
|
9
|
+
import { buildEnv } from "../src/utils-env.js";
|
|
10
|
+
import { findMonorepoPackages } from "../src/utils-pkg.js";
|
|
11
|
+
import {
|
|
12
|
+
resolveTarget,
|
|
13
|
+
resolveSecret,
|
|
14
|
+
verifyCanDecrypt,
|
|
15
|
+
getOutputName,
|
|
16
|
+
load,
|
|
17
|
+
} from "../src/core.js";
|
|
18
|
+
|
|
19
|
+
const args = minimist(process.argv.slice(2), { "--": true });
|
|
14
20
|
const __root = process.cwd();
|
|
15
21
|
|
|
16
|
-
const command = args._[0] ||
|
|
22
|
+
const command = args._[0] || "write";
|
|
17
23
|
|
|
18
|
-
const secret = args.secret
|
|
19
|
-
? (args.secret === 'stdin' ? fs.readFileSync(0, 'utf8').trim() : fs.readFileSync(args.secret, 'utf8').trim())
|
|
20
|
-
: process.env.WRENV_SECRET || process.env.TARGET_SECRET || '';
|
|
24
|
+
const secret = resolveSecret(args.secret);
|
|
21
25
|
|
|
22
|
-
const envPath = args.env ||
|
|
26
|
+
const envPath = args.env || ".env.json";
|
|
23
27
|
const envFile = path.resolve(__root, envPath);
|
|
24
28
|
|
|
25
29
|
// Check env file exists for commands that need it
|
|
26
|
-
const commandsRequiringEnvFile = [
|
|
30
|
+
const commandsRequiringEnvFile = ["write", "show", "encrypt", "besafe", "add"];
|
|
27
31
|
if (commandsRequiringEnvFile.includes(command) && !fs.existsSync(envFile)) {
|
|
28
|
-
console.error(
|
|
32
|
+
console.error("No env file found.");
|
|
29
33
|
process.exit(1);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
async function showCommand() {
|
|
33
37
|
if (!secret) {
|
|
34
|
-
console.log(
|
|
38
|
+
console.log(
|
|
39
|
+
"Secret not provided. Use --secret flag or WRENV_SECRET env variable.",
|
|
40
|
+
);
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
const target = resolveTarget(args.target);
|
|
38
44
|
|
|
39
|
-
const envContent = fs.readFileSync(envFile,
|
|
45
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
40
46
|
const env = JSON.parse(envContent);
|
|
41
47
|
|
|
42
|
-
const rootPkgPath = path.resolve(__root,
|
|
43
|
-
const rootPkgContent = fs.readFileSync(rootPkgPath,
|
|
48
|
+
const rootPkgPath = path.resolve(__root, "package.json");
|
|
49
|
+
const rootPkgContent = fs.readFileSync(rootPkgPath, "utf8");
|
|
44
50
|
const rootPkg = JSON.parse(rootPkgContent);
|
|
45
51
|
|
|
46
|
-
const scope = rootPkg.name.split(
|
|
52
|
+
const scope = rootPkg.name.split("/")[0];
|
|
47
53
|
const [_, ...foundPackages] = findMonorepoPackages(__root, scope);
|
|
48
54
|
|
|
49
55
|
const deps = [
|
|
50
56
|
...foundPackages.filter(Boolean).map((pkg) => ({
|
|
51
|
-
name: pkg.name.replace(`${scope}/`,
|
|
57
|
+
name: pkg.name.replace(`${scope}/`, ""),
|
|
52
58
|
dir: pkg.dir,
|
|
53
59
|
})),
|
|
54
60
|
{
|
|
55
|
-
name:
|
|
61
|
+
name: "root",
|
|
56
62
|
dir: __root,
|
|
57
63
|
},
|
|
58
64
|
];
|
|
59
65
|
|
|
60
66
|
const depsName = deps.map((dep) => dep.name);
|
|
61
67
|
|
|
62
|
-
const allowedEnvs =
|
|
68
|
+
const allowedEnvs =
|
|
69
|
+
env && env.envs && Array.isArray(env.envs) ? env.envs : ["dev"];
|
|
63
70
|
const targetEnv = allowedEnvs.find((env) => env === target);
|
|
64
71
|
if (!targetEnv) {
|
|
65
72
|
console.error(`Target env "${target}" is not allowed.`);
|
|
@@ -71,46 +78,50 @@ async function showCommand() {
|
|
|
71
78
|
for await (const dep of deps) {
|
|
72
79
|
const pkgEnv = Object.entries(allEnvs[dep.name] || {})
|
|
73
80
|
.map(([key, value]) => `${key}=${value}`)
|
|
74
|
-
.join(
|
|
75
|
-
console.log(
|
|
76
|
-
console.log(
|
|
81
|
+
.join("\n");
|
|
82
|
+
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
|
|
83
|
+
console.log(
|
|
84
|
+
`=== ENV for ${dep.name} in ${dep.dir} with target ${targetEnv}`,
|
|
85
|
+
);
|
|
77
86
|
console.log(pkgEnv);
|
|
78
87
|
}
|
|
79
|
-
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
async function writeCommand() {
|
|
83
91
|
if (!secret) {
|
|
84
|
-
console.log(
|
|
92
|
+
console.log(
|
|
93
|
+
"Secret not provided. Use --secret flag or WRENV_SECRET env variable.",
|
|
94
|
+
);
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
const target = resolveTarget(args.target);
|
|
88
98
|
|
|
89
|
-
const envContent = fs.readFileSync(envFile,
|
|
99
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
90
100
|
const env = JSON.parse(envContent);
|
|
91
101
|
|
|
92
|
-
const rootPkgPath = path.resolve(__root,
|
|
93
|
-
const rootPkgContent = fs.readFileSync(rootPkgPath,
|
|
102
|
+
const rootPkgPath = path.resolve(__root, "package.json");
|
|
103
|
+
const rootPkgContent = fs.readFileSync(rootPkgPath, "utf8");
|
|
94
104
|
const rootPkg = JSON.parse(rootPkgContent);
|
|
95
|
-
const scope = rootPkg.name.split(
|
|
105
|
+
const scope = rootPkg.name.split("/")[0];
|
|
96
106
|
|
|
97
107
|
const [_, ...foundPackages] = await findMonorepoPackages(__root, scope);
|
|
98
108
|
const deps = [
|
|
99
109
|
...foundPackages.map((pkg) => ({
|
|
100
|
-
name: pkg.name.replace(`${scope}/`,
|
|
110
|
+
name: pkg.name.replace(`${scope}/`, ""),
|
|
101
111
|
dir: pkg.dir,
|
|
102
112
|
})),
|
|
103
113
|
{
|
|
104
|
-
name:
|
|
114
|
+
name: "root",
|
|
105
115
|
dir: __root,
|
|
106
116
|
},
|
|
107
117
|
];
|
|
108
118
|
const depsName = deps.map((dep) => dep.name);
|
|
109
119
|
|
|
110
|
-
const allowedEnvs =
|
|
120
|
+
const allowedEnvs =
|
|
121
|
+
env && env.envs && Array.isArray(env.envs) ? env.envs : ["dev"];
|
|
111
122
|
|
|
112
123
|
// Handle 'all' target: write all environments with suffixed filenames
|
|
113
|
-
const isAllTarget = target ===
|
|
124
|
+
const isAllTarget = target === "all";
|
|
114
125
|
const targetEnvs = isAllTarget ? allowedEnvs : [target];
|
|
115
126
|
|
|
116
127
|
for (const targetEnv of targetEnvs) {
|
|
@@ -124,9 +135,11 @@ async function writeCommand() {
|
|
|
124
135
|
for await (const dep of deps) {
|
|
125
136
|
const pkgEnv = Object.entries(allEnvs[dep.name] || {})
|
|
126
137
|
.map(([key, value]) => `${key}=${value}`)
|
|
127
|
-
.join(
|
|
138
|
+
.join("\n");
|
|
128
139
|
// Use suffixed filename (.env.{outputName}) for 'all', plain .env otherwise
|
|
129
|
-
const filename = isAllTarget
|
|
140
|
+
const filename = isAllTarget
|
|
141
|
+
? `.env.${getOutputName(targetEnv)}`
|
|
142
|
+
: ".env";
|
|
130
143
|
console.log(`Writing ${filename} for ${dep.name} in ${dep.dir}`);
|
|
131
144
|
await fs.writeFile(path.join(dep.dir, filename), pkgEnv);
|
|
132
145
|
}
|
|
@@ -134,27 +147,35 @@ async function writeCommand() {
|
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
async function encryptCommand() {
|
|
137
|
-
const envContent = fs.readFileSync(envFile,
|
|
150
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
138
151
|
const env = JSON.parse(envContent);
|
|
139
152
|
|
|
140
153
|
if (!secret) {
|
|
141
|
-
console.error(
|
|
154
|
+
console.error(
|
|
155
|
+
"Cannot encrypt env file: secret not provided. Doing nothing",
|
|
156
|
+
);
|
|
142
157
|
return;
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
// Verify we can decrypt all existing encrypted values before encrypting new ones
|
|
146
161
|
const verification = verifyCanDecrypt(env, secret);
|
|
147
162
|
if (!verification.success) {
|
|
148
|
-
console.error(
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
console.error(
|
|
164
|
+
`Cannot encrypt: the provided secret cannot decrypt existing encrypted value at "${verification.path}".`,
|
|
165
|
+
);
|
|
166
|
+
console.error(
|
|
167
|
+
"This would result in a file with mixed encryption passwords.",
|
|
168
|
+
);
|
|
169
|
+
console.error(
|
|
170
|
+
"Please provide the correct secret that was used for existing encrypted values.",
|
|
171
|
+
);
|
|
151
172
|
process.exit(1);
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
if (env.variables) {
|
|
155
176
|
for (const [varName, varValue] of Object.entries(env.variables)) {
|
|
156
177
|
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
157
|
-
if (typeof envVal ===
|
|
178
|
+
if (typeof envVal === "string" && !isEncrypted(envVal)) {
|
|
158
179
|
env.variables[varName][envKey] = encrypt(envVal, secret);
|
|
159
180
|
}
|
|
160
181
|
}
|
|
@@ -165,7 +186,7 @@ async function encryptCommand() {
|
|
|
165
186
|
for (const [pkgName, pkgOverrides] of Object.entries(env.overrides)) {
|
|
166
187
|
for (const [varName, varValue] of Object.entries(pkgOverrides)) {
|
|
167
188
|
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
168
|
-
if (typeof envVal ===
|
|
189
|
+
if (typeof envVal === "string" && !isEncrypted(envVal)) {
|
|
169
190
|
env.overrides[pkgName][varName][envKey] = encrypt(envVal, secret);
|
|
170
191
|
}
|
|
171
192
|
}
|
|
@@ -178,7 +199,11 @@ async function encryptCommand() {
|
|
|
178
199
|
|
|
179
200
|
async function besafeCommand() {
|
|
180
201
|
// Find git root to ensure we're in a repository and use correct base path
|
|
181
|
-
const { stdout: gitRoot, exitCode: gitCheck } = await execa(
|
|
202
|
+
const { stdout: gitRoot, exitCode: gitCheck } = await execa(
|
|
203
|
+
"git",
|
|
204
|
+
["rev-parse", "--show-toplevel"],
|
|
205
|
+
{ cwd: __root, reject: false },
|
|
206
|
+
);
|
|
182
207
|
if (gitCheck !== 0) {
|
|
183
208
|
return;
|
|
184
209
|
}
|
|
@@ -186,9 +211,21 @@ async function besafeCommand() {
|
|
|
186
211
|
const relativeEnvPath = path.relative(gitRoot, envFile);
|
|
187
212
|
|
|
188
213
|
// Check if file has any changes (staged, unstaged, or untracked)
|
|
189
|
-
const { stdout: staged } = await execa(
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
const { stdout: staged } = await execa(
|
|
215
|
+
"git",
|
|
216
|
+
["diff", "--cached", "--name-only", "--", relativeEnvPath],
|
|
217
|
+
{ cwd: gitRoot, reject: false },
|
|
218
|
+
);
|
|
219
|
+
const { stdout: unstaged } = await execa(
|
|
220
|
+
"git",
|
|
221
|
+
["diff", "--name-only", "--", relativeEnvPath],
|
|
222
|
+
{ cwd: gitRoot, reject: false },
|
|
223
|
+
);
|
|
224
|
+
const { stdout: untracked } = await execa(
|
|
225
|
+
"git",
|
|
226
|
+
["ls-files", "--others", "--exclude-standard", "--", relativeEnvPath],
|
|
227
|
+
{ cwd: gitRoot, reject: false },
|
|
228
|
+
);
|
|
192
229
|
|
|
193
230
|
if (!staged?.trim() && !unstaged?.trim() && !untracked?.trim()) {
|
|
194
231
|
return;
|
|
@@ -197,24 +234,28 @@ async function besafeCommand() {
|
|
|
197
234
|
await encryptCommand();
|
|
198
235
|
|
|
199
236
|
if (!secret) {
|
|
200
|
-
console.error(
|
|
237
|
+
console.error(
|
|
238
|
+
"Warning: potential security risk: secret not provided. Variables WERE NOT encrypted.",
|
|
239
|
+
);
|
|
201
240
|
}
|
|
202
241
|
|
|
203
|
-
await execa(
|
|
242
|
+
await execa("git", ["add", relativeEnvPath], { cwd: gitRoot });
|
|
204
243
|
}
|
|
205
244
|
|
|
206
245
|
async function addCommand() {
|
|
207
246
|
const varName = args._[1];
|
|
208
247
|
if (!varName) {
|
|
209
|
-
console.error(
|
|
248
|
+
console.error("Variable name is required.");
|
|
210
249
|
process.exit(1);
|
|
211
250
|
}
|
|
212
251
|
|
|
213
252
|
if (!secret) {
|
|
214
|
-
console.error(
|
|
253
|
+
console.error(
|
|
254
|
+
"Warning: potential security risk: secret not provided. Variable WILL NOT be encrypted.",
|
|
255
|
+
);
|
|
215
256
|
}
|
|
216
257
|
|
|
217
|
-
const envContent = fs.readFileSync(envFile,
|
|
258
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
218
259
|
const env = JSON.parse(envContent);
|
|
219
260
|
|
|
220
261
|
if (!env.variables) {
|
|
@@ -226,10 +267,10 @@ async function addCommand() {
|
|
|
226
267
|
}
|
|
227
268
|
|
|
228
269
|
for (const arg of args._.slice(2)) {
|
|
229
|
-
if (!arg.startsWith(
|
|
230
|
-
const [key, ...valueParts] = arg.slice(1).split(
|
|
231
|
-
const value = valueParts.join(
|
|
232
|
-
const envKey = key ===
|
|
270
|
+
if (!arg.startsWith("+")) continue;
|
|
271
|
+
const [key, ...valueParts] = arg.slice(1).split("=");
|
|
272
|
+
const value = valueParts.join("=");
|
|
273
|
+
const envKey = key === "fallback" ? "@@" : key;
|
|
233
274
|
env.variables[varName][envKey] = secret ? encrypt(value, secret) : value;
|
|
234
275
|
}
|
|
235
276
|
|
|
@@ -237,40 +278,23 @@ async function addCommand() {
|
|
|
237
278
|
}
|
|
238
279
|
|
|
239
280
|
async function runCommand() {
|
|
240
|
-
const commandParts = args[
|
|
281
|
+
const commandParts = args["--"] || [];
|
|
241
282
|
const [cmd, ...cmdArgs] = commandParts;
|
|
242
283
|
|
|
243
284
|
if (!cmd) {
|
|
244
|
-
console.error(
|
|
285
|
+
console.error("Usage: wrenv run [options] -- <command> [args...]");
|
|
245
286
|
process.exit(1);
|
|
246
287
|
}
|
|
247
288
|
|
|
248
|
-
// Load environment variables
|
|
289
|
+
// Load environment variables (without applying to current process.env)
|
|
249
290
|
let envVars = {};
|
|
250
291
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
? (args.secret === 'stdin' ? fs.readFileSync(0, 'utf8').trim() : fs.readFileSync(args.secret, 'utf8').trim())
|
|
255
|
-
: process.env.WRENV_SECRET || process.env.TARGET_SECRET || null;
|
|
256
|
-
|
|
257
|
-
const target = resolveTarget(args.target);
|
|
258
|
-
|
|
259
|
-
envVars = loadEnvJson({
|
|
260
|
-
secret: resolvedSecret,
|
|
261
|
-
targetEnv: target,
|
|
292
|
+
envVars = load({
|
|
293
|
+
secret: resolveSecret(args.secret),
|
|
294
|
+
targetEnv: args.target,
|
|
262
295
|
envPath: args.env,
|
|
296
|
+
applyToProcess: false,
|
|
263
297
|
});
|
|
264
|
-
|
|
265
|
-
// Apply .env.local overrides only in dev mode (security: prevent local overrides in production)
|
|
266
|
-
if (target === 'dev') {
|
|
267
|
-
const localOverrides = loadLocalOverrides();
|
|
268
|
-
for (const [key, value] of Object.entries(localOverrides)) {
|
|
269
|
-
if (Object.hasOwn(envVars, key)) {
|
|
270
|
-
envVars[key] = value;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
298
|
} catch (error) {
|
|
275
299
|
console.error(`wrenv: ${error.message}`);
|
|
276
300
|
process.exit(1);
|
|
@@ -278,48 +302,66 @@ async function runCommand() {
|
|
|
278
302
|
|
|
279
303
|
// Use execa for cross-platform compatibility with minimal overhead
|
|
280
304
|
const subprocess = execa(cmd, cmdArgs, {
|
|
281
|
-
stdio:
|
|
305
|
+
stdio: "inherit",
|
|
282
306
|
env: { ...process.env, ...envVars },
|
|
283
307
|
reject: false,
|
|
284
308
|
});
|
|
285
309
|
|
|
286
310
|
// Forward signals to child process
|
|
287
311
|
const forwardSignal = (signal) => subprocess.kill(signal);
|
|
288
|
-
process.on(
|
|
289
|
-
process.on(
|
|
290
|
-
process.on(
|
|
312
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
313
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
314
|
+
process.on("SIGHUP", () => forwardSignal("SIGHUP"));
|
|
291
315
|
|
|
292
316
|
const result = await subprocess;
|
|
293
317
|
process.exit(result.exitCode ?? 0);
|
|
294
318
|
}
|
|
295
319
|
|
|
296
320
|
function helpCommand() {
|
|
297
|
-
console.log(
|
|
298
|
-
console.log(
|
|
299
|
-
console.log(
|
|
300
|
-
console.log(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
console.log(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
console.log(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
console.log(
|
|
310
|
-
console.log(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
console.log(
|
|
314
|
-
console.log(
|
|
315
|
-
console.log(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.log(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.log(
|
|
322
|
-
|
|
321
|
+
console.log("Usage: wrenv [command] [options]\n");
|
|
322
|
+
console.log("Commands:");
|
|
323
|
+
console.log(" write Write .env files for all packages (default)");
|
|
324
|
+
console.log(
|
|
325
|
+
" show Show environment variables without writing files",
|
|
326
|
+
);
|
|
327
|
+
console.log(
|
|
328
|
+
" run Run a command with injected environment variables",
|
|
329
|
+
);
|
|
330
|
+
console.log(
|
|
331
|
+
" encrypt Encrypt all unencrypted variables in .env.json",
|
|
332
|
+
);
|
|
333
|
+
console.log(" besafe Encrypt and stage .env.json if it has changes");
|
|
334
|
+
console.log(
|
|
335
|
+
" add <var> Add a new variable with environment-specific values",
|
|
336
|
+
);
|
|
337
|
+
console.log(" help Show this help message\n");
|
|
338
|
+
console.log("Options:");
|
|
339
|
+
console.log(
|
|
340
|
+
' --secret <path|stdin> Path to secret file or "stdin" to read from stdin',
|
|
341
|
+
);
|
|
342
|
+
console.log(
|
|
343
|
+
" --target <env> Target environment (dev, staging, prod, all)",
|
|
344
|
+
);
|
|
345
|
+
console.log(
|
|
346
|
+
" --env <path> Path to env file (default: .env.json)\n",
|
|
347
|
+
);
|
|
348
|
+
console.log("Environment Variables:");
|
|
349
|
+
console.log(" WRENV_SECRET Secret for encryption/decryption");
|
|
350
|
+
console.log(" WRENV_TARGET Target environment");
|
|
351
|
+
console.log(" TARGET_SECRET Alias for WRENV_SECRET");
|
|
352
|
+
console.log(" TARGET_ENV Alias for WRENV_TARGET\n");
|
|
353
|
+
console.log("Examples:");
|
|
354
|
+
console.log(" wrenv --secret ~/.big-secret write --target dev");
|
|
355
|
+
console.log(" wrenv --secret stdin write < ~/.big-secret");
|
|
356
|
+
console.log(" wrenv show --target prod");
|
|
357
|
+
console.log(" wrenv run --target prod -- node server.js");
|
|
358
|
+
console.log(" wrenv run -- npm test --coverage");
|
|
359
|
+
console.log(
|
|
360
|
+
" wrenv add NEW_VAR +fallback=value +dev=dev_value +production=prod_value",
|
|
361
|
+
);
|
|
362
|
+
console.log(
|
|
363
|
+
" wrenv write --target all # writes .env.development, .env.staging, .env.production",
|
|
364
|
+
);
|
|
323
365
|
}
|
|
324
366
|
|
|
325
367
|
const commands = {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasak/tool-env",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.1
|
|
4
|
+
"version": "1.2.1",
|
|
5
5
|
"author": "dev@saasak.studio",
|
|
6
6
|
"description": "A small util to manage environment variables for your monorepo",
|
|
7
7
|
"keywords": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "src/index.js",
|
|
14
|
+
"types": "src/index.d.ts",
|
|
14
15
|
"files": [
|
|
15
16
|
"bin",
|
|
16
17
|
"src"
|
package/src/core.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { decrypt, isEncrypted } from
|
|
4
|
-
import { buildEnv } from
|
|
5
|
-
import { findMonorepoPackages } from
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { decrypt, isEncrypted } from "./utils-crypto.js";
|
|
4
|
+
import { buildEnv } from "./utils-env.js";
|
|
5
|
+
import { findMonorepoPackages } from "./utils-pkg.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {Object.<string, string>} EnvRecord
|
|
@@ -34,7 +34,19 @@ import { findMonorepoPackages } from './utils-pkg.js';
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
/** @type {string} */
|
|
37
|
-
const DEFAULT_ENV =
|
|
37
|
+
const DEFAULT_ENV = "dev";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Environment variables that must never be injected by .env.json content.
|
|
41
|
+
* These control wrenv's own behavior and must not be overridden.
|
|
42
|
+
* @type {Set<string>}
|
|
43
|
+
*/
|
|
44
|
+
export const RESERVED_ENV_VARS = new Set([
|
|
45
|
+
"WRENV_TARGET",
|
|
46
|
+
"WRENV_SECRET",
|
|
47
|
+
"TARGET_ENV",
|
|
48
|
+
"TARGET_SECRET",
|
|
49
|
+
]);
|
|
38
50
|
|
|
39
51
|
/**
|
|
40
52
|
* Mapping of environment name aliases to canonical names
|
|
@@ -42,14 +54,14 @@ const DEFAULT_ENV = 'dev';
|
|
|
42
54
|
* @type {Object.<string, string>}
|
|
43
55
|
*/
|
|
44
56
|
const targets = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
development: "dev",
|
|
58
|
+
dev: "dev",
|
|
59
|
+
staging: "preprod",
|
|
60
|
+
pp: "preprod",
|
|
61
|
+
preprod: "preprod",
|
|
62
|
+
prod: "production",
|
|
63
|
+
production: "production",
|
|
64
|
+
all: "all",
|
|
53
65
|
};
|
|
54
66
|
|
|
55
67
|
/**
|
|
@@ -58,9 +70,9 @@ const targets = {
|
|
|
58
70
|
* @type {Object.<string, string>}
|
|
59
71
|
*/
|
|
60
72
|
const outputNames = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
dev: "development",
|
|
74
|
+
preprod: "staging",
|
|
75
|
+
production: "production",
|
|
64
76
|
};
|
|
65
77
|
|
|
66
78
|
/**
|
|
@@ -104,39 +116,55 @@ export function resolveTarget(targetParam) {
|
|
|
104
116
|
* @returns {string[]} - Resolved target sliases environment
|
|
105
117
|
*/
|
|
106
118
|
export function resolveTargetAliases(allowedEnvs) {
|
|
107
|
-
const allowedAliases = allowedEnvs.flatMap(
|
|
108
|
-
|
|
119
|
+
const allowedAliases = allowedEnvs.flatMap((env) =>
|
|
120
|
+
Object.entries(targets)
|
|
109
121
|
.find(([_, val]) => val === env)
|
|
110
|
-
.map(([alias]) => alias)
|
|
111
|
-
|
|
112
|
-
)
|
|
122
|
+
.map(([alias]) => alias),
|
|
123
|
+
);
|
|
113
124
|
|
|
114
125
|
return Array.from(new Set(allowedAliases));
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
|
|
118
128
|
/**
|
|
119
|
-
* Resolve secret with
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
129
|
+
* Resolve secret with unified fallback chain:
|
|
130
|
+
* param (stdin | file:// | file path | literal) -> WRENV_SECRET -> TARGET_SECRET -> null
|
|
131
|
+
*
|
|
132
|
+
* If reading from stdin or a file:// path fails, falls back to env vars.
|
|
133
|
+
* A bare string that is not a readable file is treated as a literal secret.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} [secretParam] - Secret source: "stdin", "file://path", file path, or literal string
|
|
136
|
+
* @returns {string} - Resolved secret or empty string if not provided
|
|
124
137
|
*/
|
|
125
138
|
export function resolveSecret(secretParam) {
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
const getDefault = (v = '') => v || process.env.WRENV_SECRET || process.env.TARGET_SECRET || "";
|
|
140
|
+
const getFromStream = (stream) => {
|
|
141
|
+
try {
|
|
142
|
+
const value = fs.readFileSync(stream, "utf8").trim();
|
|
143
|
+
return getDefault(value);
|
|
144
|
+
} catch (_) {
|
|
145
|
+
return getDefault();
|
|
146
|
+
}
|
|
128
147
|
}
|
|
129
|
-
|
|
130
|
-
if (secretParam.startsWith('file://')) {
|
|
131
|
-
const filePath = secretParam.slice(7);
|
|
148
|
+
const isFile = (f) => {
|
|
132
149
|
try {
|
|
133
|
-
return fs.
|
|
134
|
-
} catch (
|
|
135
|
-
|
|
150
|
+
return fs.statSync(f).isFile();
|
|
151
|
+
} catch (_) {
|
|
152
|
+
return false
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
|
|
156
|
+
if (!secretParam)
|
|
157
|
+
return getDefault();
|
|
158
|
+
|
|
159
|
+
if (secretParam === "stdin")
|
|
160
|
+
return getFromStream(0);
|
|
161
|
+
|
|
162
|
+
if (secretParam.startsWith("file://"))
|
|
163
|
+
return getFromStream(secretParam.slice(7));
|
|
164
|
+
|
|
165
|
+
return isFile(secretParam)
|
|
166
|
+
? getFromStream(secretParam)
|
|
167
|
+
: getDefault(secretParam); // Treat as literal if not a file
|
|
140
168
|
}
|
|
141
169
|
|
|
142
170
|
/**
|
|
@@ -148,18 +176,18 @@ export function resolveSecret(secretParam) {
|
|
|
148
176
|
export function parseEnvFile(content) {
|
|
149
177
|
/** @type {EnvRecord} */
|
|
150
178
|
const result = {};
|
|
151
|
-
const lines = content.split(
|
|
179
|
+
const lines = content.split("\n");
|
|
152
180
|
|
|
153
181
|
for (const line of lines) {
|
|
154
182
|
const trimmed = line.trim();
|
|
155
183
|
|
|
156
184
|
// Skip empty lines and comments
|
|
157
|
-
if (!trimmed || trimmed.startsWith(
|
|
185
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
158
186
|
continue;
|
|
159
187
|
}
|
|
160
188
|
|
|
161
189
|
// Find the first = sign
|
|
162
|
-
const eqIndex = trimmed.indexOf(
|
|
190
|
+
const eqIndex = trimmed.indexOf("=");
|
|
163
191
|
if (eqIndex === -1) {
|
|
164
192
|
continue;
|
|
165
193
|
}
|
|
@@ -168,8 +196,10 @@ export function parseEnvFile(content) {
|
|
|
168
196
|
let value = trimmed.slice(eqIndex + 1).trim();
|
|
169
197
|
|
|
170
198
|
// Remove surrounding quotes if present
|
|
171
|
-
if (
|
|
172
|
-
(value.startsWith("'
|
|
199
|
+
if (
|
|
200
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
201
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
202
|
+
) {
|
|
173
203
|
value = value.slice(1, -1);
|
|
174
204
|
}
|
|
175
205
|
|
|
@@ -190,14 +220,15 @@ export function parseEnvFile(content) {
|
|
|
190
220
|
function findNearestPackageName(cwd, scope) {
|
|
191
221
|
let currentDir = cwd;
|
|
192
222
|
|
|
193
|
-
while (currentDir !== path.dirname(currentDir)) {
|
|
194
|
-
|
|
223
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
224
|
+
// Stop at filesystem root
|
|
225
|
+
const pkgPath = path.join(currentDir, "package.json");
|
|
195
226
|
if (fs.existsSync(pkgPath)) {
|
|
196
227
|
try {
|
|
197
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath,
|
|
228
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
198
229
|
if (pkg.name) {
|
|
199
230
|
// Return name without scope prefix
|
|
200
|
-
return pkg.name.replace(`${scope}/`,
|
|
231
|
+
return pkg.name.replace(`${scope}/`, "");
|
|
201
232
|
}
|
|
202
233
|
} catch (e) {
|
|
203
234
|
// Continue traversing upward
|
|
@@ -206,7 +237,7 @@ function findNearestPackageName(cwd, scope) {
|
|
|
206
237
|
currentDir = path.dirname(currentDir);
|
|
207
238
|
}
|
|
208
239
|
|
|
209
|
-
return
|
|
240
|
+
return "root"; // fallback
|
|
210
241
|
}
|
|
211
242
|
|
|
212
243
|
/**
|
|
@@ -215,14 +246,14 @@ function findNearestPackageName(cwd, scope) {
|
|
|
215
246
|
* @returns {EnvRecord} - Local overrides or empty object
|
|
216
247
|
*/
|
|
217
248
|
export function loadLocalOverrides(cwd = process.cwd()) {
|
|
218
|
-
const localEnvPath = path.join(cwd,
|
|
249
|
+
const localEnvPath = path.join(cwd, ".env.local");
|
|
219
250
|
|
|
220
251
|
if (!fs.existsSync(localEnvPath)) {
|
|
221
252
|
return {};
|
|
222
253
|
}
|
|
223
254
|
|
|
224
255
|
try {
|
|
225
|
-
const content = fs.readFileSync(localEnvPath,
|
|
256
|
+
const content = fs.readFileSync(localEnvPath, "utf8");
|
|
226
257
|
return parseEnvFile(content);
|
|
227
258
|
} catch (error) {
|
|
228
259
|
return {};
|
|
@@ -260,8 +291,8 @@ export function loadEnvJson(options = {}) {
|
|
|
260
291
|
const {
|
|
261
292
|
secret: secretParam,
|
|
262
293
|
targetEnv: targetParam,
|
|
263
|
-
envPath =
|
|
264
|
-
cwd = process.cwd()
|
|
294
|
+
envPath = ".env.json",
|
|
295
|
+
cwd = process.cwd(),
|
|
265
296
|
} = options;
|
|
266
297
|
|
|
267
298
|
const secret = resolveSecret(secretParam);
|
|
@@ -274,32 +305,35 @@ export function loadEnvJson(options = {}) {
|
|
|
274
305
|
}
|
|
275
306
|
|
|
276
307
|
const envFile = path.join(monorepoRoot, envPath);
|
|
277
|
-
const envContent = fs.readFileSync(envFile,
|
|
308
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
278
309
|
const env = JSON.parse(envContent);
|
|
279
310
|
|
|
280
|
-
const allowedEnvs =
|
|
311
|
+
const allowedEnvs =
|
|
312
|
+
env && env.envs && Array.isArray(env.envs) ? env.envs : [DEFAULT_ENV];
|
|
281
313
|
const resolvedTarget = allowedEnvs.find((e) => e === targetEnv);
|
|
282
314
|
|
|
283
315
|
if (!resolvedTarget) {
|
|
284
|
-
throw new Error(
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Target env "${targetEnv}" is not allowed. Allowed: ${allowedEnvs.join(", ")}`,
|
|
318
|
+
);
|
|
285
319
|
}
|
|
286
320
|
|
|
287
321
|
// Get scope from monorepo root package.json
|
|
288
|
-
const rootPkgPath = path.join(monorepoRoot,
|
|
289
|
-
let scope =
|
|
322
|
+
const rootPkgPath = path.join(monorepoRoot, "package.json");
|
|
323
|
+
let scope = "";
|
|
290
324
|
/** @type {string[]} */
|
|
291
|
-
let depsName = [
|
|
325
|
+
let depsName = ["root"];
|
|
292
326
|
|
|
293
327
|
if (fs.existsSync(rootPkgPath)) {
|
|
294
328
|
try {
|
|
295
|
-
const rootPkgContent = fs.readFileSync(rootPkgPath,
|
|
329
|
+
const rootPkgContent = fs.readFileSync(rootPkgPath, "utf8");
|
|
296
330
|
const rootPkg = JSON.parse(rootPkgContent);
|
|
297
|
-
scope = rootPkg.name.split(
|
|
331
|
+
scope = rootPkg.name.split("/")[0];
|
|
298
332
|
|
|
299
333
|
const foundPackages = findMonorepoPackages(monorepoRoot, scope);
|
|
300
334
|
depsName = [
|
|
301
|
-
...foundPackages.map((pkg) => pkg.name.replace(`${scope}/`,
|
|
302
|
-
|
|
335
|
+
...foundPackages.map((pkg) => pkg.name.replace(`${scope}/`, "")),
|
|
336
|
+
"root",
|
|
303
337
|
];
|
|
304
338
|
} catch (error) {
|
|
305
339
|
// Not a monorepo or error parsing, use default
|
|
@@ -339,7 +373,7 @@ export function load(options = {}) {
|
|
|
339
373
|
secret,
|
|
340
374
|
envPath,
|
|
341
375
|
cwd = process.cwd(),
|
|
342
|
-
applyToProcess = true
|
|
376
|
+
applyToProcess = true,
|
|
343
377
|
} = options;
|
|
344
378
|
|
|
345
379
|
// Resolve target first to determine if we're in dev mode
|
|
@@ -350,13 +384,12 @@ export function load(options = {}) {
|
|
|
350
384
|
secret,
|
|
351
385
|
targetEnv: resolvedTarget,
|
|
352
386
|
envPath,
|
|
353
|
-
cwd
|
|
387
|
+
cwd,
|
|
354
388
|
});
|
|
355
389
|
|
|
356
390
|
// Load .env.local overrides only in dev mode (security: prevent local overrides in production)
|
|
357
|
-
const localOverrides =
|
|
358
|
-
? loadLocalOverrides(cwd)
|
|
359
|
-
: {};
|
|
391
|
+
const localOverrides =
|
|
392
|
+
resolvedTarget === "dev" ? loadLocalOverrides(cwd) : {};
|
|
360
393
|
|
|
361
394
|
// Merge: env.json values, then .env.local overrides (local takes precedence)
|
|
362
395
|
// Note: .env.local only overrides env.json values, not existing process.env
|
|
@@ -369,6 +402,15 @@ export function load(options = {}) {
|
|
|
369
402
|
}
|
|
370
403
|
}
|
|
371
404
|
|
|
405
|
+
// - Reserved vars (WRENV_*, TARGET_*) are never injected
|
|
406
|
+
// - NODE_ENV is only injected if not already set
|
|
407
|
+
for (const key of RESERVED_ENV_VARS) {
|
|
408
|
+
delete finalConfig[key];
|
|
409
|
+
}
|
|
410
|
+
if (process.env.NODE_ENV) {
|
|
411
|
+
delete finalConfig.NODE_ENV;
|
|
412
|
+
}
|
|
413
|
+
|
|
372
414
|
// Apply to process.env if requested
|
|
373
415
|
if (applyToProcess) {
|
|
374
416
|
for (const [key, value] of Object.entries(finalConfig)) {
|
|
@@ -397,15 +439,18 @@ export function verifyCanDecrypt(env, secret) {
|
|
|
397
439
|
const encryptedValues = [];
|
|
398
440
|
|
|
399
441
|
if (!secret) {
|
|
400
|
-
return { success: false, path: null, error:
|
|
442
|
+
return { success: false, path: null, error: "No secret provided" };
|
|
401
443
|
}
|
|
402
444
|
|
|
403
445
|
// Collect all encrypted values from variables
|
|
404
446
|
if (env.variables) {
|
|
405
447
|
for (const [varName, varValue] of Object.entries(env.variables)) {
|
|
406
448
|
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
407
|
-
if (typeof envVal ===
|
|
408
|
-
encryptedValues.push({
|
|
449
|
+
if (typeof envVal === "string" && isEncrypted(envVal)) {
|
|
450
|
+
encryptedValues.push({
|
|
451
|
+
path: `variables.${varName}.${envKey}`,
|
|
452
|
+
value: envVal,
|
|
453
|
+
});
|
|
409
454
|
}
|
|
410
455
|
}
|
|
411
456
|
}
|
|
@@ -416,8 +461,11 @@ export function verifyCanDecrypt(env, secret) {
|
|
|
416
461
|
for (const [pkgName, pkgOverrides] of Object.entries(env.overrides)) {
|
|
417
462
|
for (const [varName, varValue] of Object.entries(pkgOverrides)) {
|
|
418
463
|
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
419
|
-
if (typeof envVal ===
|
|
420
|
-
encryptedValues.push({
|
|
464
|
+
if (typeof envVal === "string" && isEncrypted(envVal)) {
|
|
465
|
+
encryptedValues.push({
|
|
466
|
+
path: `overrides.${pkgName}.${varName}.${envKey}`,
|
|
467
|
+
value: envVal,
|
|
468
|
+
});
|
|
421
469
|
}
|
|
422
470
|
}
|
|
423
471
|
}
|
|
@@ -437,7 +485,7 @@ export function verifyCanDecrypt(env, secret) {
|
|
|
437
485
|
return {
|
|
438
486
|
success: false,
|
|
439
487
|
path,
|
|
440
|
-
error: error.message
|
|
488
|
+
error: error.message,
|
|
441
489
|
};
|
|
442
490
|
}
|
|
443
491
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Key-value pairs of environment variable name to value */
|
|
2
|
+
export type EnvRecord = Record<string, string>;
|
|
3
|
+
|
|
4
|
+
export interface LoadOptions {
|
|
5
|
+
/** Target environment (e.g. 'dev', 'preprod', 'production') */
|
|
6
|
+
targetEnv?: string;
|
|
7
|
+
/** Secret for decryption: "stdin", "file://path", file path, or literal string */
|
|
8
|
+
secret?: string;
|
|
9
|
+
/** Path to env.json file (default: '.env.json') */
|
|
10
|
+
envPath?: string;
|
|
11
|
+
/** Working directory (default: process.cwd()) */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Whether to apply resolved vars to process.env (default: true) */
|
|
14
|
+
applyToProcess?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load environment variables from .env.json with optional .env.local overrides.
|
|
19
|
+
* By default, applies resolved variables to process.env.
|
|
20
|
+
*/
|
|
21
|
+
export function load(options?: LoadOptions): EnvRecord;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load environment configuration from .env.json without .env.local overrides.
|
|
25
|
+
*/
|
|
26
|
+
export function loadEnvJson(options?: {
|
|
27
|
+
secret?: string;
|
|
28
|
+
targetEnv?: string;
|
|
29
|
+
envPath?: string;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}): EnvRecord;
|
|
32
|
+
|
|
33
|
+
/** Load .env.local overrides if the file exists */
|
|
34
|
+
export function loadLocalOverrides(cwd?: string): EnvRecord;
|
|
35
|
+
|
|
36
|
+
/** Parse .env file content into key-value pairs */
|
|
37
|
+
export function parseEnvFile(content: string): EnvRecord;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve target environment with fallback chain:
|
|
41
|
+
* param -> WRENV_TARGET -> TARGET_ENV -> NODE_ENV (mapped) -> 'dev'
|
|
42
|
+
*/
|
|
43
|
+
export function resolveTarget(targetParam?: string): string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve secret with fallback chain:
|
|
47
|
+
* param (stdin | file:// | file path | literal) -> WRENV_SECRET -> TARGET_SECRET -> ''
|
|
48
|
+
*/
|
|
49
|
+
export function resolveSecret(secretParam?: string): string;
|
|
50
|
+
|
|
51
|
+
/** Get the conventional output file name for an internal environment name */
|
|
52
|
+
export function getOutputName(internalName: string): string;
|
|
53
|
+
|
|
54
|
+
/** Verify that all encrypted values can be decrypted with the provided secret */
|
|
55
|
+
export function verifyCanDecrypt(env: object, secret: string): {
|
|
56
|
+
success: boolean;
|
|
57
|
+
path?: string | null;
|
|
58
|
+
error?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Encrypt a plaintext value with the given secret */
|
|
62
|
+
export function encrypt(value: string, secret: string): string;
|
|
63
|
+
|
|
64
|
+
/** Decrypt an encrypted value with the given secret */
|
|
65
|
+
export function decrypt(value: string, secret: string): string;
|
|
66
|
+
|
|
67
|
+
/** Check if a value is encrypted */
|
|
68
|
+
export function isEncrypted(value: string): boolean;
|
|
69
|
+
|
|
70
|
+
/** Build environment variables for all packages from an env config */
|
|
71
|
+
export function buildEnv(
|
|
72
|
+
secret: string,
|
|
73
|
+
target: string,
|
|
74
|
+
env: object,
|
|
75
|
+
names: string[],
|
|
76
|
+
): Record<string, EnvRecord>;
|
|
77
|
+
|
|
78
|
+
/** Find all packages in a monorepo */
|
|
79
|
+
export function findMonorepoPackages(
|
|
80
|
+
root: string,
|
|
81
|
+
scope: string,
|
|
82
|
+
): Array<{ name: string; dir: string }>;
|
package/src/utils-env.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { decrypt, isEncrypted } from
|
|
1
|
+
import { decrypt, isEncrypted } from "./utils-crypto.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @
|
|
7
|
-
* @property {string} [preprod] - Value for preprod environment
|
|
8
|
-
* @property {string} [production] - Value for production environment
|
|
4
|
+
* Environment-specific values for a single variable.
|
|
5
|
+
* Keys are environment names (e.g. 'dev', 'production') or '@@' for the fallback.
|
|
6
|
+
* @typedef {Object.<string, string>} EnvVariableValue
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
/**
|
|
@@ -47,13 +45,16 @@ export function buildEnv(secret, target, env, names) {
|
|
|
47
45
|
|
|
48
46
|
// For each variable, extract the value for the target env
|
|
49
47
|
/** @type {EnvRecord} */
|
|
50
|
-
const variables = Object.entries(variablesForAllTargets).reduce(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
const variables = Object.entries(variablesForAllTargets).reduce(
|
|
49
|
+
(acc, entry) => {
|
|
50
|
+
const [key, value] = entry;
|
|
51
|
+
return {
|
|
52
|
+
...acc,
|
|
53
|
+
[key]: extractValue(secret, value[target] || value["@@"] || ""),
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
{},
|
|
57
|
+
);
|
|
57
58
|
|
|
58
59
|
// For each package, get all variables
|
|
59
60
|
// compute the overrides and merge them
|
|
@@ -64,7 +65,7 @@ export function buildEnv(secret, target, env, names) {
|
|
|
64
65
|
const allVars = names.reduce((acc, name) => {
|
|
65
66
|
acc[name] = {
|
|
66
67
|
...variables,
|
|
67
|
-
...parseOverrides(secret, target, overrides[name], variables)
|
|
68
|
+
...parseOverrides(secret, target, overrides[name], variables),
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
return acc;
|
|
@@ -77,7 +78,7 @@ export function buildEnv(secret, target, env, names) {
|
|
|
77
78
|
* Parse package-specific overrides for a target environment
|
|
78
79
|
* @param {string} secret - Secret for decrypting encrypted values
|
|
79
80
|
* @param {string} target - Target environment
|
|
80
|
-
* @param {Object.<string, EnvVariableValue>}
|
|
81
|
+
* @param {Object.<string, EnvVariableValue>} overrides - Package overrides (may be undefined)
|
|
81
82
|
* @param {EnvRecord} vars - Base variables for reference substitution
|
|
82
83
|
* @returns {EnvRecord} - Parsed override values
|
|
83
84
|
*/
|
|
@@ -86,12 +87,16 @@ function parseOverrides(secret, target, overrides, vars) {
|
|
|
86
87
|
|
|
87
88
|
return Object.entries(overrides).reduce((acc, entry) => {
|
|
88
89
|
const [key, val] = entry;
|
|
89
|
-
const value = extractValue(secret, val[target] || val[
|
|
90
|
+
const value = extractValue(secret, val[target] || val["@@"] || "");
|
|
90
91
|
|
|
91
92
|
if (value === null) return acc;
|
|
92
93
|
|
|
93
94
|
acc[key] = Array.isArray(value)
|
|
94
|
-
? value
|
|
95
|
+
? value
|
|
96
|
+
.map((v) =>
|
|
97
|
+
Object.prototype.hasOwnProperty.call(vars, v) ? vars[v] : v,
|
|
98
|
+
)
|
|
99
|
+
.join("")
|
|
95
100
|
: value;
|
|
96
101
|
return acc;
|
|
97
102
|
}, {});
|
|
@@ -105,6 +110,6 @@ function parseOverrides(secret, target, overrides, vars) {
|
|
|
105
110
|
*/
|
|
106
111
|
function extractValue(secret, value) {
|
|
107
112
|
if (!isEncrypted(value)) return value;
|
|
108
|
-
if (!secret) return
|
|
113
|
+
if (!secret) return "";
|
|
109
114
|
return decrypt(value, secret);
|
|
110
115
|
}
|