@willbooster/wb 14.0.0 → 14.2.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/dotenv.js +96 -23
- package/dist/index.js +38 -31
- package/package.json +13 -12
package/bin/dotenv.js
CHANGED
|
@@ -39,14 +39,12 @@ export function runDotenvCommand(args) {
|
|
|
39
39
|
stdio: 'inherit',
|
|
40
40
|
});
|
|
41
41
|
const signalHandlers = new Map();
|
|
42
|
-
let forwardedShutdownSignal;
|
|
43
42
|
child.on('error', (error) => {
|
|
44
43
|
console.error(error);
|
|
45
44
|
process.exit(1);
|
|
46
45
|
});
|
|
47
46
|
for (const signal of shutdownSignals) {
|
|
48
47
|
const signalHandler = () => {
|
|
49
|
-
forwardedShutdownSignal = signal;
|
|
50
48
|
child.kill(signal);
|
|
51
49
|
};
|
|
52
50
|
signalHandlers.set(signal, signalHandler);
|
|
@@ -56,10 +54,9 @@ export function runDotenvCommand(args) {
|
|
|
56
54
|
for (const [shutdownSignal, signalHandler] of signalHandlers) {
|
|
57
55
|
process.off(shutdownSignal, signalHandler);
|
|
58
56
|
}
|
|
59
|
-
if (signal && signal === forwardedShutdownSignal) {
|
|
60
|
-
process.exit(0);
|
|
61
|
-
}
|
|
62
57
|
if (signal) {
|
|
58
|
+
// Re-raise even for forwarded shutdown signals so callers observe the conventional
|
|
59
|
+
// signal exit status (e.g. 130 for SIGINT) instead of a misleading success.
|
|
63
60
|
process.kill(process.pid, signal);
|
|
64
61
|
return;
|
|
65
62
|
}
|
|
@@ -67,59 +64,135 @@ export function runDotenvCommand(args) {
|
|
|
67
64
|
});
|
|
68
65
|
}
|
|
69
66
|
|
|
67
|
+
// Mirrors src/commands/dotenv.ts (readAndApplyEnvironmentVariables) for this startup fast path.
|
|
70
68
|
function readAndApplyEnvironmentVariables(cwd) {
|
|
69
|
+
const mode = process.env.WB_ENV;
|
|
70
|
+
// Mode-specific sources drive the forced-mode override below.
|
|
71
|
+
const modeVars = mode
|
|
72
|
+
? { ...readEnvFile(path.join(cwd, `.env.${mode}`)), ...readEnvFile(path.join(cwd, `.env.${mode}.local`)) }
|
|
73
|
+
: {};
|
|
74
|
+
// Mirror the shared cascade's precedence: .env.<mode>.local > .env.local > .env.<mode> > .env.
|
|
71
75
|
const dotenvVars = {
|
|
72
76
|
...readEnvFile(path.join(cwd, '.env')),
|
|
73
|
-
...(
|
|
77
|
+
...(mode ? readEnvFile(path.join(cwd, `.env.${mode}`)) : {}),
|
|
78
|
+
...readEnvFile(path.join(cwd, '.env.local')),
|
|
79
|
+
...(mode ? readEnvFile(path.join(cwd, `.env.${mode}.local`)) : {}),
|
|
74
80
|
};
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
// WB_ENV in process.env means the mode is explicitly forced, so values from the mode's own
|
|
82
|
+
// sources (.env.<mode>[.local] and the fnox profile) win over variables inherited from the
|
|
83
|
+
// parent shell — except on CI, where injected env vars must keep overriding committed files
|
|
84
|
+
// (see https://github.com/WillBooster/shared/issues/930).
|
|
85
|
+
const modeFileOverridesProcessEnv = !!mode && !isCI(process.env.CI);
|
|
86
|
+
// fnox.toml is the committed, encrypted equivalent of .env files; local .env files still win over it.
|
|
87
|
+
const fnoxVars = readFnoxEnvironmentVariables(cwd, mode, dotenvVars, modeFileOverridesProcessEnv);
|
|
88
|
+
const parsed = { ...fnoxVars, ...dotenvVars };
|
|
89
|
+
// Expand ${...} references against exported variables whose FILE value loses to the shell
|
|
90
|
+
// (mirroring readEnvironmentVariables' effective-value semantics): a reference must resolve to
|
|
91
|
+
// the value the child will actually see, so a shell-shadowed key expands to the shell value
|
|
92
|
+
// while a winning file/override value expands to the file value.
|
|
93
|
+
const fileValueWins = (key) =>
|
|
94
|
+
!(key in process.env) || (modeFileOverridesProcessEnv && (key in modeVars || key in fnoxVars));
|
|
95
|
+
const referenceEnv = {};
|
|
96
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
97
|
+
// Escape dollar signs so exported values substitute literally (pa$word stays pa$word).
|
|
98
|
+
if (value !== undefined && !(key in parsed && fileValueWins(key))) {
|
|
99
|
+
referenceEnv[key] = value.replaceAll('$', String.raw`\$`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const envVars = expand({ parsed, processEnv: referenceEnv }).parsed ?? parsed;
|
|
79
103
|
for (const [key, value] of Object.entries(envVars)) {
|
|
80
104
|
if (!(key in process.env)) {
|
|
81
105
|
process.env[key] = value;
|
|
106
|
+
continue;
|
|
82
107
|
}
|
|
108
|
+
// readFnoxEnvironmentVariables returns a process.env-shadowed key only when the forced
|
|
109
|
+
// profile overrides it, so fnox-provided keys count as mode-specific here.
|
|
110
|
+
if (!(key in modeVars || key in fnoxVars) || process.env[key] === value) continue;
|
|
111
|
+
|
|
112
|
+
if (modeFileOverridesProcessEnv) {
|
|
113
|
+
console.warn(
|
|
114
|
+
`Warning: ${key} in the "${mode}" mode's env sources overrides the value inherited from the parent environment because WB_ENV is explicitly set.`
|
|
115
|
+
);
|
|
116
|
+
process.env[key] = value;
|
|
117
|
+
}
|
|
118
|
+
// On CI, inherited variables intentionally win; no warning is emitted.
|
|
83
119
|
}
|
|
84
120
|
}
|
|
85
121
|
|
|
86
122
|
// Mirrors readFnoxEnvironmentVariables in @willbooster/shared-lib-node for this startup fast path.
|
|
87
|
-
function readFnoxEnvironmentVariables(cwd, cascade, currentEnvVars) {
|
|
123
|
+
function readFnoxEnvironmentVariables(cwd, cascade, currentEnvVars, modeFileOverridesProcessEnv) {
|
|
88
124
|
if (!hasProjectFnoxConfig(cwd)) return {};
|
|
89
125
|
|
|
126
|
+
const secrets = runFnoxExport(cwd, cascade, { quiet: false });
|
|
127
|
+
if (!secrets) return {};
|
|
128
|
+
// A key is profile-specific (and may override process.env off CI) when the profile export's
|
|
129
|
+
// value differs from the base export's; when the base export fails, no override is applied.
|
|
130
|
+
// The base export runs lazily, only when a process.env collision needs adjudicating.
|
|
131
|
+
let cachedBaseSecrets = false;
|
|
132
|
+
const getBaseSecrets = () => {
|
|
133
|
+
if (cachedBaseSecrets === false) {
|
|
134
|
+
cachedBaseSecrets = runFnoxExport(cwd, undefined, { quiet: true, ignoreProfileEnvVar: true });
|
|
135
|
+
}
|
|
136
|
+
return cachedBaseSecrets;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const envVars = {};
|
|
140
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
141
|
+
if (typeof value !== 'string' || key in currentEnvVars) continue;
|
|
142
|
+
if (key in process.env) {
|
|
143
|
+
const baseSecrets = modeFileOverridesProcessEnv && cascade ? getBaseSecrets() : undefined;
|
|
144
|
+
const overridesProcessEnv = baseSecrets !== undefined && baseSecrets[key] !== value;
|
|
145
|
+
if (!overridesProcessEnv) continue;
|
|
146
|
+
}
|
|
147
|
+
envVars[key] = value;
|
|
148
|
+
}
|
|
149
|
+
return envVars;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function runFnoxExport(cwd, cascade, options) {
|
|
90
153
|
// `--if-missing error`: fnox otherwise exits 0 and silently omits secrets it fails to resolve.
|
|
91
154
|
// `--non-interactive`: prompts or browser auth flows would hang forever because stdin is ignored.
|
|
92
155
|
const args = ['export', '--format', 'json', '--no-color', '--if-missing', 'error', '--non-interactive'];
|
|
156
|
+
const env = { ...process.env };
|
|
93
157
|
if (cascade) {
|
|
94
158
|
args.push('--profile', cascade);
|
|
95
159
|
}
|
|
160
|
+
if (options.ignoreProfileEnvVar) {
|
|
161
|
+
// Without `--profile`, fnox falls back to FNOX_PROFILE; the base-adjudication export must
|
|
162
|
+
// read the BASE secrets, so the inherited profile selection is cleared for it — and only for
|
|
163
|
+
// it: a profile-less PRIMARY export (e.g. `wb dotenv` without WB_ENV) keeps honoring
|
|
164
|
+
// FNOX_PROFILE.
|
|
165
|
+
delete env.FNOX_PROFILE;
|
|
166
|
+
}
|
|
96
167
|
const result = childProcess.spawnSync('fnox', args, {
|
|
97
168
|
cwd,
|
|
169
|
+
env,
|
|
98
170
|
encoding: 'utf8',
|
|
99
171
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
172
|
});
|
|
101
173
|
if (result.error || result.status !== 0 || !result.stdout?.trim()) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
174
|
+
if (!options.quiet) {
|
|
175
|
+
console.warn(
|
|
176
|
+
`Failed to read fnox secrets: ${result.error?.message || result.stderr?.trim() || `fnox exited with status ${result.status}`}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
106
180
|
}
|
|
107
181
|
|
|
108
182
|
let parsed;
|
|
109
183
|
try {
|
|
110
184
|
parsed = JSON.parse(result.stdout);
|
|
111
185
|
} catch {
|
|
112
|
-
return
|
|
186
|
+
return;
|
|
113
187
|
}
|
|
114
188
|
const secrets = parsed?.secrets;
|
|
115
|
-
if (!secrets || typeof secrets !== 'object' || Array.isArray(secrets)) return
|
|
189
|
+
if (!secrets || typeof secrets !== 'object' || Array.isArray(secrets)) return;
|
|
190
|
+
return secrets;
|
|
191
|
+
}
|
|
116
192
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
envVars[key] = value;
|
|
121
|
-
}
|
|
122
|
-
return envVars;
|
|
193
|
+
// Mirrors src/utils/ci.ts for this startup fast path.
|
|
194
|
+
function isCI(ciEnv) {
|
|
195
|
+
return !!ciEnv && ciEnv !== '0' && ciEnv !== 'false';
|
|
123
196
|
}
|
|
124
197
|
|
|
125
198
|
function hasProjectFnoxConfig(cwd) {
|