@saasak/tool-env 1.4.0 → 1.5.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/bin/index.js +145 -82
- package/package.json +1 -1
- package/src/core.js +2 -2
- package/src/utils-env.js +18 -0
package/bin/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import fs from "fs-extra";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { execa } from "execa";
|
|
7
7
|
|
|
8
|
-
import { encrypt, isEncrypted } from "../src/utils-crypto.js";
|
|
9
|
-
import { buildEnv } from "../src/utils-env.js";
|
|
8
|
+
import { encrypt, decrypt, isEncrypted } from "../src/utils-crypto.js";
|
|
9
|
+
import { buildEnv, walkEnvValues } from "../src/utils-env.js";
|
|
10
10
|
import { findMonorepoPackages } from "../src/utils-pkg.js";
|
|
11
11
|
import {
|
|
12
12
|
resolveTarget,
|
|
@@ -14,24 +14,27 @@ import {
|
|
|
14
14
|
verifyCanDecrypt,
|
|
15
15
|
getOutputName,
|
|
16
16
|
load,
|
|
17
|
+
findMonorepoRoot,
|
|
17
18
|
} from "../src/core.js";
|
|
18
19
|
|
|
19
20
|
const args = minimist(process.argv.slice(2), {
|
|
20
21
|
"--": true,
|
|
21
22
|
boolean: ["strict", "expose", "help", "h"],
|
|
23
|
+
string: ["format", "package"],
|
|
22
24
|
});
|
|
23
|
-
const
|
|
25
|
+
const __cwd = process.cwd();
|
|
26
|
+
const envPath = args.env || ".env.json";
|
|
27
|
+
const __root = findMonorepoRoot(__cwd, envPath) || __cwd;
|
|
24
28
|
|
|
25
29
|
let command = args._[0] || "help";
|
|
26
30
|
if (args.help || args.h) command = "help";
|
|
27
31
|
|
|
28
32
|
const secret = resolveSecret(args.secret ? `file://${args.secret}` : '');
|
|
29
33
|
|
|
30
|
-
const envPath = args.env || ".env.json";
|
|
31
34
|
const envFile = path.resolve(__root, envPath);
|
|
32
35
|
|
|
33
36
|
// Check env file exists for commands that need it
|
|
34
|
-
const commandsRequiringEnvFile = ["write", "show", "encrypt", "besafe", "add"];
|
|
37
|
+
const commandsRequiringEnvFile = ["write", "show", "encrypt", "decrypt", "besafe", "add", "get"];
|
|
35
38
|
if (commandsRequiringEnvFile.includes(command) && !fs.existsSync(envFile)) {
|
|
36
39
|
console.error("No env file found.");
|
|
37
40
|
process.exit(1);
|
|
@@ -46,102 +49,113 @@ if (args.strict && commandsRequiringEnvFile.includes(command)) {
|
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
if (!secret) {
|
|
51
|
-
console.log(
|
|
52
|
-
"Secret not provided. Use --secret flag or WRENV_SECRET env variable.",
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
52
|
+
function loadWorkspace() {
|
|
56
53
|
const target = resolveTarget(args.target);
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const rootPkgPath = path.resolve(__root, "package.json");
|
|
62
|
-
const rootPkgContent = fs.readFileSync(rootPkgPath, "utf8");
|
|
63
|
-
const rootPkg = JSON.parse(rootPkgContent);
|
|
64
|
-
|
|
54
|
+
const env = JSON.parse(fs.readFileSync(envFile, "utf8"));
|
|
55
|
+
const rootPkg = JSON.parse(
|
|
56
|
+
fs.readFileSync(path.resolve(__root, "package.json"), "utf8"),
|
|
57
|
+
);
|
|
65
58
|
const scope = rootPkg.name.split("/")[0];
|
|
66
59
|
const [_, ...foundPackages] = findMonorepoPackages(__root, scope);
|
|
67
|
-
|
|
68
60
|
const deps = [
|
|
69
61
|
...foundPackages.filter(Boolean).map((pkg) => ({
|
|
70
62
|
name: pkg.name.replace(`${scope}/`, ""),
|
|
71
63
|
dir: pkg.dir,
|
|
72
64
|
})),
|
|
73
|
-
{
|
|
74
|
-
name: "root",
|
|
75
|
-
dir: __root,
|
|
76
|
-
},
|
|
65
|
+
{ name: "root", dir: __root },
|
|
77
66
|
];
|
|
67
|
+
const depsName = deps.map((d) => d.name);
|
|
68
|
+
const allowedEnvs =
|
|
69
|
+
env?.envs && Array.isArray(env.envs) ? env.envs : ["dev"];
|
|
70
|
+
return { env, deps, depsName, allowedEnvs, target };
|
|
71
|
+
}
|
|
78
72
|
|
|
79
|
-
|
|
73
|
+
function detectCurrentPackage(deps) {
|
|
74
|
+
for (const dep of deps) {
|
|
75
|
+
if (dep.name === "root") continue;
|
|
76
|
+
const depDir = path.resolve(dep.dir);
|
|
77
|
+
if (__cwd === depDir || __cwd.startsWith(depDir + path.sep)) {
|
|
78
|
+
return dep.name;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const targetEnv = allowedEnvs.find((env) => env === target);
|
|
84
|
-
if (!targetEnv) {
|
|
84
|
+
function validateTarget(target, allowedEnvs) {
|
|
85
|
+
if (!allowedEnvs.includes(target)) {
|
|
85
86
|
console.error(`Target env "${target}" is not allowed.`);
|
|
86
87
|
process.exit(1);
|
|
87
88
|
}
|
|
89
|
+
return target;
|
|
90
|
+
}
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
function printJson(allEnvs, deps, isSinglePackage) {
|
|
93
|
+
if (isSinglePackage) {
|
|
94
|
+
console.log(JSON.stringify(allEnvs[deps[0].name] || {}, null, 2));
|
|
95
|
+
} else {
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const dep of deps) out[dep.name] = allEnvs[dep.name] || {};
|
|
98
|
+
console.log(JSON.stringify(out, null, 2));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
function printKeyValue(allEnvs, deps, targetEnv) {
|
|
103
|
+
for (const dep of deps) {
|
|
92
104
|
const pkgEnv = Object.entries(allEnvs[dep.name] || {})
|
|
93
105
|
.map(([key, value]) => `${key}=${value}`)
|
|
94
106
|
.join("\n");
|
|
95
|
-
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
|
|
107
|
+
console.log("# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
|
|
96
108
|
console.log(
|
|
97
|
-
|
|
109
|
+
`# ===> ${dep.name} : in ${dep.dir} with target ${targetEnv}`,
|
|
98
110
|
);
|
|
111
|
+
console.log("# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
|
|
99
112
|
console.log(pkgEnv);
|
|
100
113
|
}
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
async function
|
|
116
|
+
async function showCommand() {
|
|
104
117
|
if (!secret) {
|
|
105
|
-
console.
|
|
118
|
+
console.error(
|
|
106
119
|
"Secret not provided. Use --secret flag or WRENV_SECRET env variable.",
|
|
107
120
|
);
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
const target =
|
|
123
|
+
const { env, deps, depsName, allowedEnvs, target } = loadWorkspace();
|
|
124
|
+
const targetEnv = validateTarget(target, allowedEnvs);
|
|
111
125
|
|
|
112
|
-
const
|
|
113
|
-
const
|
|
126
|
+
const selectedPkg = args.package || detectCurrentPackage(deps);
|
|
127
|
+
const filteredDeps = selectedPkg
|
|
128
|
+
? deps.filter((d) => d.name === selectedPkg)
|
|
129
|
+
: deps;
|
|
130
|
+
if (selectedPkg && filteredDeps.length === 0) {
|
|
131
|
+
console.error(`Package "${selectedPkg}" not found.`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
114
134
|
|
|
115
|
-
const
|
|
116
|
-
const rootPkgContent = fs.readFileSync(rootPkgPath, "utf8");
|
|
117
|
-
const rootPkg = JSON.parse(rootPkgContent);
|
|
118
|
-
const scope = rootPkg.name.split("/")[0];
|
|
135
|
+
const allEnvs = buildEnv(secret, targetEnv, env, depsName);
|
|
119
136
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
name: "root",
|
|
128
|
-
dir: __root,
|
|
129
|
-
},
|
|
130
|
-
];
|
|
131
|
-
const depsName = deps.map((dep) => dep.name);
|
|
137
|
+
if (args.format === "json") {
|
|
138
|
+
printJson(allEnvs, filteredDeps, !!selectedPkg);
|
|
139
|
+
} else {
|
|
140
|
+
printKeyValue(allEnvs, filteredDeps, targetEnv);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
132
143
|
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
async function writeCommand() {
|
|
145
|
+
if (!secret) {
|
|
146
|
+
console.log(
|
|
147
|
+
"Secret not provided. Use --secret flag or WRENV_SECRET env variable.",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { env, deps, depsName, allowedEnvs, target } = loadWorkspace();
|
|
135
152
|
|
|
136
153
|
// Handle 'all' target: write all environments with suffixed filenames
|
|
137
154
|
const isAllTarget = target === "all";
|
|
138
155
|
const targetEnvs = isAllTarget ? allowedEnvs : [target];
|
|
139
156
|
|
|
140
157
|
for (const targetEnv of targetEnvs) {
|
|
141
|
-
|
|
142
|
-
console.error(`Target env "${targetEnv}" is not allowed.`);
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
158
|
+
validateTarget(targetEnv, allowedEnvs);
|
|
145
159
|
|
|
146
160
|
const allEnvs = buildEnv(secret, targetEnv, env, depsName);
|
|
147
161
|
|
|
@@ -160,8 +174,7 @@ async function writeCommand() {
|
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
async function encryptCommand() {
|
|
163
|
-
const
|
|
164
|
-
const env = JSON.parse(envContent);
|
|
177
|
+
const env = JSON.parse(fs.readFileSync(envFile, "utf8"));
|
|
165
178
|
|
|
166
179
|
if (!secret) {
|
|
167
180
|
console.error(
|
|
@@ -185,31 +198,65 @@ async function encryptCommand() {
|
|
|
185
198
|
process.exit(1);
|
|
186
199
|
}
|
|
187
200
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
walkEnvValues(
|
|
202
|
+
env,
|
|
203
|
+
(v) => typeof v === "string" && !isEncrypted(v),
|
|
204
|
+
(v) => encrypt(v, secret),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
await fs.writeFile(envFile, JSON.stringify(env, null, 2));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function decryptCommand() {
|
|
211
|
+
const env = JSON.parse(fs.readFileSync(envFile, "utf8"));
|
|
212
|
+
|
|
213
|
+
if (!secret) {
|
|
214
|
+
console.error(
|
|
215
|
+
"Cannot decrypt env file: secret not provided. Doing nothing",
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
196
218
|
}
|
|
197
219
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
220
|
+
const verification = verifyCanDecrypt(env, secret);
|
|
221
|
+
if (!verification.success) {
|
|
222
|
+
console.error(
|
|
223
|
+
`Cannot decrypt: the provided secret cannot decrypt encrypted value at "${verification.path}".`,
|
|
224
|
+
);
|
|
225
|
+
process.exit(1);
|
|
208
226
|
}
|
|
209
227
|
|
|
228
|
+
walkEnvValues(env, isEncrypted, (v) => decrypt(v, secret));
|
|
229
|
+
|
|
210
230
|
await fs.writeFile(envFile, JSON.stringify(env, null, 2));
|
|
211
231
|
}
|
|
212
232
|
|
|
233
|
+
async function getCommand() {
|
|
234
|
+
const varName = args._[1];
|
|
235
|
+
if (!varName) {
|
|
236
|
+
console.error("Variable name is required.");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { env, deps, depsName, allowedEnvs, target } = loadWorkspace();
|
|
241
|
+
const targetEnv = validateTarget(target, allowedEnvs);
|
|
242
|
+
|
|
243
|
+
const selectedPkg = args.package || detectCurrentPackage(deps) || "root";
|
|
244
|
+
if (!depsName.includes(selectedPkg)) {
|
|
245
|
+
console.error(`Package "${selectedPkg}" not found.`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const allEnvs = buildEnv(secret, targetEnv, env, depsName);
|
|
250
|
+
const pkgEnv = allEnvs[selectedPkg] || {};
|
|
251
|
+
|
|
252
|
+
if (!Object.prototype.hasOwnProperty.call(pkgEnv, varName)) {
|
|
253
|
+
console.error(`Variable "${varName}" not found.`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
process.stdout.write(pkgEnv[varName]);
|
|
258
|
+
}
|
|
259
|
+
|
|
213
260
|
async function besafeCommand() {
|
|
214
261
|
// Find git root to ensure we're in a repository and use correct base path
|
|
215
262
|
const { stdout: gitRoot, exitCode: gitCheck } = await execa(
|
|
@@ -346,10 +393,16 @@ function helpCommand() {
|
|
|
346
393
|
console.log(
|
|
347
394
|
" encrypt Encrypt all unencrypted variables in .env.json",
|
|
348
395
|
);
|
|
396
|
+
console.log(
|
|
397
|
+
" decrypt Decrypt all encrypted variables in .env.json",
|
|
398
|
+
);
|
|
349
399
|
console.log(" besafe Encrypt and stage .env.json if it has changes");
|
|
350
400
|
console.log(
|
|
351
401
|
" add <var> Add a new variable with environment-specific values",
|
|
352
402
|
);
|
|
403
|
+
console.log(
|
|
404
|
+
" get <var> Get the value of a specific variable",
|
|
405
|
+
);
|
|
353
406
|
console.log(" help Show this help message\n");
|
|
354
407
|
console.log("Options:");
|
|
355
408
|
console.log(
|
|
@@ -362,7 +415,13 @@ function helpCommand() {
|
|
|
362
415
|
" --env <path> Path to env file (default: .env.json)",
|
|
363
416
|
);
|
|
364
417
|
console.log(
|
|
365
|
-
" --strict Error if encrypted values cannot be decrypted
|
|
418
|
+
" --strict Error if encrypted values cannot be decrypted",
|
|
419
|
+
);
|
|
420
|
+
console.log(
|
|
421
|
+
" --format <fmt> Output format for show (json)",
|
|
422
|
+
);
|
|
423
|
+
console.log(
|
|
424
|
+
" --package <name> Show/get variables for a specific package\n",
|
|
366
425
|
);
|
|
367
426
|
console.log("Environment Variables:");
|
|
368
427
|
console.log(" WRENV_SECRET Secret for encryption/decryption");
|
|
@@ -373,6 +432,8 @@ function helpCommand() {
|
|
|
373
432
|
console.log(" wrenv --secret ~/.big-secret write --target dev");
|
|
374
433
|
console.log(" wrenv --secret stdin write < ~/.big-secret");
|
|
375
434
|
console.log(" wrenv show --target prod");
|
|
435
|
+
console.log(" wrenv show --format json --package root");
|
|
436
|
+
console.log(" wrenv get API_URL --target prod --package pkg-a");
|
|
376
437
|
console.log(" wrenv run --target prod -- node server.js");
|
|
377
438
|
console.log(" wrenv run -- npm test --coverage");
|
|
378
439
|
console.log(
|
|
@@ -388,7 +449,9 @@ const commands = {
|
|
|
388
449
|
show: showCommand,
|
|
389
450
|
besafe: besafeCommand,
|
|
390
451
|
encrypt: encryptCommand,
|
|
452
|
+
decrypt: decryptCommand,
|
|
391
453
|
add: addCommand,
|
|
454
|
+
get: getCommand,
|
|
392
455
|
run: runCommand,
|
|
393
456
|
help: helpCommand,
|
|
394
457
|
};
|
package/package.json
CHANGED
package/src/core.js
CHANGED
|
@@ -265,7 +265,7 @@ export function loadLocalOverrides(cwd = process.cwd()) {
|
|
|
265
265
|
* @param {string} envPath - Name of the env file (e.g. '.env.json')
|
|
266
266
|
* @returns {string|null} - Path to monorepo root, or null if not found
|
|
267
267
|
*/
|
|
268
|
-
function findMonorepoRoot(cwd, envPath) {
|
|
268
|
+
export function findMonorepoRoot(cwd, envPath) {
|
|
269
269
|
let currentDir = cwd;
|
|
270
270
|
|
|
271
271
|
while (currentDir !== path.dirname(currentDir)) {
|
|
@@ -338,7 +338,7 @@ export function loadEnvJson(options = {}) {
|
|
|
338
338
|
const rootPkg = JSON.parse(rootPkgContent);
|
|
339
339
|
scope = rootPkg.name.split("/")[0];
|
|
340
340
|
|
|
341
|
-
const foundPackages = findMonorepoPackages(monorepoRoot, scope);
|
|
341
|
+
const [, ...foundPackages] = findMonorepoPackages(monorepoRoot, scope);
|
|
342
342
|
depsName = [
|
|
343
343
|
...foundPackages.map((pkg) => pkg.name.replace(`${scope}/`, "")),
|
|
344
344
|
"root",
|
package/src/utils-env.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { decrypt, isEncrypted } from "./utils-crypto.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Walk all string values in env.variables and env.overrides,
|
|
5
|
+
* applying transform when predicate matches. Mutates env in place.
|
|
6
|
+
* @param {EnvConfig} env
|
|
7
|
+
* @param {(value: string) => boolean} predicate
|
|
8
|
+
* @param {(value: string) => string} transform
|
|
9
|
+
*/
|
|
10
|
+
export function walkEnvValues(env, predicate, transform) {
|
|
11
|
+
for (const varValue of Object.values(env.variables || {}))
|
|
12
|
+
for (const [envKey, envVal] of Object.entries(varValue))
|
|
13
|
+
if (predicate(envVal)) varValue[envKey] = transform(envVal);
|
|
14
|
+
|
|
15
|
+
for (const pkgOverrides of Object.values(env.overrides || {}))
|
|
16
|
+
for (const varValue of Object.values(pkgOverrides))
|
|
17
|
+
for (const [envKey, envVal] of Object.entries(varValue))
|
|
18
|
+
if (predicate(envVal)) varValue[envKey] = transform(envVal);
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
/**
|
|
4
22
|
* Environment-specific values for a single variable.
|
|
5
23
|
* Keys are environment names (e.g. 'dev', 'production') or '@@' for the fallback.
|