@fiodos/cli 0.1.14 → 0.1.16
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/package.json +1 -1
- package/src/aiAnalyze.js +18 -2
- package/src/index.js +99 -13
- package/src/wireHandlers.js +61 -197
- package/src/wireWeb.js +154 -9
- package/src/wireWebMount.js +205 -18
- package/src/writeEnv.js +278 -46
package/src/writeEnv.js
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* writeEnv — consent-based
|
|
2
|
+
* writeEnv — consent-based handling of the orb's environment variables.
|
|
3
3
|
*
|
|
4
|
-
* The orb must read the SAME key/URL the CLI published with. We always ASK
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The orb must read the SAME key/URL the CLI published with. We always ASK before
|
|
5
|
+
* writing (independent of --yes, which only skips orb/handler prompts). Sensitive
|
|
6
|
+
* values follow ONE consistent flow:
|
|
7
|
+
*
|
|
8
|
+
* 1. Local env file — "add it automatically, or add it yourself?" (yes/no).
|
|
9
|
+
* · yes → written to the framework's env file, which we ALSO ensure is in
|
|
10
|
+
* .gitignore so the secret never reaches GitHub.
|
|
11
|
+
* · no → a copy-paste reminder is shown at the very END of the run.
|
|
12
|
+
* 2. Deploy provider — the SAME yes/no flow for the build-time public key
|
|
13
|
+
* (VITE_/NEXT_PUBLIC_), because that value is inlined at BUILD time: if it is
|
|
14
|
+
* only in the (gitignored, never-pushed) local env and NOT in the provider,
|
|
15
|
+
* the production build ships WITHOUT it and the orb never appears. We detect
|
|
16
|
+
* a linked Vercel project and offer to push it automatically; otherwise we
|
|
17
|
+
* warn clearly and print exactly what to add where.
|
|
18
|
+
*
|
|
19
|
+
* SECURITY: no secret is ever hardcoded into committed source. The orb key
|
|
20
|
+
* (`fyd_…`) is a PUBLISHABLE client key (safe inside the frontend bundle), but it
|
|
21
|
+
* still must exist in the build environment — which is why it goes through env
|
|
22
|
+
* vars and the deploy-provider step, never into the repo.
|
|
8
23
|
*/
|
|
9
24
|
'use strict';
|
|
10
25
|
|
|
11
26
|
const fs = require('fs');
|
|
12
27
|
const path = require('path');
|
|
13
28
|
const readline = require('readline');
|
|
29
|
+
const { execFileSync } = require('child_process');
|
|
14
30
|
|
|
15
31
|
function readJsonSafe(file) {
|
|
16
32
|
try {
|
|
@@ -63,6 +79,11 @@ function resolveEnvPlan(appRoot) {
|
|
|
63
79
|
return { framework: null, file: null, candidates: [], keyVar: null, urlVar: null };
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
/** Frameworks whose public env vars are INLINED at build time (need provider env). */
|
|
83
|
+
function isBuildTimePublicFramework(framework) {
|
|
84
|
+
return framework === 'vite' || framework === 'next';
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
function findEffectiveVar(appRoot, candidates, name) {
|
|
67
88
|
for (const rel of candidates) {
|
|
68
89
|
const filePath = path.join(appRoot, rel);
|
|
@@ -117,20 +138,127 @@ function askYesNo(question) {
|
|
|
117
138
|
});
|
|
118
139
|
}
|
|
119
140
|
|
|
141
|
+
/** True if `.gitignore` already keeps `fileName` (or a glob covering it) out of git. */
|
|
120
142
|
function isGitIgnored(appRoot, fileName) {
|
|
121
143
|
const gi = path.join(appRoot, '.gitignore');
|
|
122
144
|
if (!fs.existsSync(gi)) return false;
|
|
123
145
|
try {
|
|
124
146
|
const content = fs.readFileSync(gi, 'utf8');
|
|
125
|
-
|
|
126
|
-
return patterns.some(
|
|
127
|
-
(p) => p === fileName || p === `/${fileName}` || p === '.env*' || p === '*.local' || p === '.env.local',
|
|
128
|
-
);
|
|
147
|
+
return gitignoreCovers(content, fileName);
|
|
129
148
|
} catch {
|
|
130
149
|
return false;
|
|
131
150
|
}
|
|
132
151
|
}
|
|
133
152
|
|
|
153
|
+
/** Pure: does this .gitignore content keep `fileName` out of git? */
|
|
154
|
+
function gitignoreCovers(content, fileName) {
|
|
155
|
+
const patterns = content.split('\n').map((l) => l.trim());
|
|
156
|
+
const glob = fileName.endsWith('.local') ? '*.local' : '.env*';
|
|
157
|
+
return patterns.some(
|
|
158
|
+
(p) =>
|
|
159
|
+
p === fileName ||
|
|
160
|
+
p === `/${fileName}` ||
|
|
161
|
+
p === '.env*' ||
|
|
162
|
+
p === '.env.*' ||
|
|
163
|
+
p === '*.local' ||
|
|
164
|
+
p === glob ||
|
|
165
|
+
p === fileName.replace(/^\.env/, '.env*'),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Guarantee the secret-bearing env file is git-ignored (never pushed to GitHub).
|
|
171
|
+
* Appends a covering pattern to `.gitignore` (creating it when absent) when the
|
|
172
|
+
* file is not already ignored. Idempotent. Returns what happened.
|
|
173
|
+
*/
|
|
174
|
+
function ensureEnvGitIgnored(appRoot, fileName) {
|
|
175
|
+
const gi = path.join(appRoot, '.gitignore');
|
|
176
|
+
const exists = fs.existsSync(gi);
|
|
177
|
+
let content = '';
|
|
178
|
+
try {
|
|
179
|
+
content = exists ? fs.readFileSync(gi, 'utf8') : '';
|
|
180
|
+
} catch {
|
|
181
|
+
content = '';
|
|
182
|
+
}
|
|
183
|
+
if (exists && gitignoreCovers(content, fileName)) {
|
|
184
|
+
return { changed: false, created: false, alreadyIgnored: true, pattern: null };
|
|
185
|
+
}
|
|
186
|
+
const pattern = fileName.endsWith('.local') ? '*.local' : fileName;
|
|
187
|
+
const header = '# Fiodos: keep local secrets out of git';
|
|
188
|
+
const prefix = content.length === 0 ? '' : content.endsWith('\n') ? '' : '\n';
|
|
189
|
+
const block = `${prefix}${content.length ? '\n' : ''}${header}\n${pattern}\n`;
|
|
190
|
+
try {
|
|
191
|
+
fs.writeFileSync(gi, content + block);
|
|
192
|
+
return { changed: true, created: !exists, alreadyIgnored: false, pattern };
|
|
193
|
+
} catch {
|
|
194
|
+
return { changed: false, created: false, alreadyIgnored: false, pattern, error: true };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Detect a deploy provider (and whether a Vercel project is already linked). */
|
|
199
|
+
function detectDeployProvider(appRoot) {
|
|
200
|
+
const has = (rel) => fs.existsSync(path.join(appRoot, rel));
|
|
201
|
+
if (has('.vercel/project.json') || has('vercel.json')) {
|
|
202
|
+
return { provider: 'vercel', linked: has('.vercel/project.json') };
|
|
203
|
+
}
|
|
204
|
+
if (has('netlify.toml') || has('.netlify')) {
|
|
205
|
+
return { provider: 'netlify', linked: false };
|
|
206
|
+
}
|
|
207
|
+
if (has('wrangler.toml')) {
|
|
208
|
+
return { provider: 'cloudflare', linked: false };
|
|
209
|
+
}
|
|
210
|
+
return { provider: null, linked: false };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function commandExists(cmd) {
|
|
214
|
+
try {
|
|
215
|
+
execFileSync(process.platform === 'win32' ? 'where' : 'which', [cmd], { stdio: 'ignore' });
|
|
216
|
+
return true;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Best-effort push of a single var to a LINKED Vercel project, non-interactively,
|
|
224
|
+
* across the given environments. Never hangs: only runs when the project is
|
|
225
|
+
* already linked and the CLI resolves, each call bounded by a timeout. Returns a
|
|
226
|
+
* per-environment result; failures (incl. "already exists") fall back to the
|
|
227
|
+
* manual reminder.
|
|
228
|
+
*/
|
|
229
|
+
function attemptVercelEnvAdd(appRoot, name, value, environments) {
|
|
230
|
+
if (!fs.existsSync(path.join(appRoot, '.vercel', 'project.json'))) {
|
|
231
|
+
return { attempted: false, reason: 'not-linked' };
|
|
232
|
+
}
|
|
233
|
+
let bin = null;
|
|
234
|
+
let pre = [];
|
|
235
|
+
if (commandExists('vercel')) {
|
|
236
|
+
bin = 'vercel';
|
|
237
|
+
} else if (commandExists('npx')) {
|
|
238
|
+
bin = 'npx';
|
|
239
|
+
pre = ['--no-install', 'vercel'];
|
|
240
|
+
} else {
|
|
241
|
+
return { attempted: false, reason: 'no-cli' };
|
|
242
|
+
}
|
|
243
|
+
const results = [];
|
|
244
|
+
for (const env of environments) {
|
|
245
|
+
try {
|
|
246
|
+
execFileSync(bin, [...pre, 'env', 'add', name, env], {
|
|
247
|
+
cwd: appRoot,
|
|
248
|
+
input: `${value}\n`,
|
|
249
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
250
|
+
timeout: 60_000,
|
|
251
|
+
});
|
|
252
|
+
results.push({ env, ok: true });
|
|
253
|
+
} catch (e) {
|
|
254
|
+
const msg = String((e && (e.stderr || e.message)) || '').split('\n')[0];
|
|
255
|
+
results.push({ env, ok: false, msg });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const okCount = results.filter((r) => r.ok).length;
|
|
259
|
+
return { attempted: true, ok: okCount > 0, okCount, results };
|
|
260
|
+
}
|
|
261
|
+
|
|
134
262
|
/**
|
|
135
263
|
* Compute where the key/URL would go and what would change — no writes, no prompts.
|
|
136
264
|
* @returns {object|null} null when there is nothing to do (already aligned / no key).
|
|
@@ -170,7 +298,7 @@ function computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl) {
|
|
|
170
298
|
const urlOk = !urlNeedsFix && (wantUrl ? currentUrl === wantUrl : true);
|
|
171
299
|
|
|
172
300
|
if (keyMatches && urlOk) {
|
|
173
|
-
return { kind: 'aligned', targetRel, plan };
|
|
301
|
+
return { kind: 'aligned', targetRel, plan, framework: plan.framework, keyVar: plan.keyVar, apiKey };
|
|
174
302
|
}
|
|
175
303
|
|
|
176
304
|
const envLines = [`${plan.keyVar}=${apiKey}`];
|
|
@@ -180,6 +308,9 @@ function computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl) {
|
|
|
180
308
|
return {
|
|
181
309
|
kind: 'writable',
|
|
182
310
|
plan,
|
|
311
|
+
framework: plan.framework,
|
|
312
|
+
keyVar: plan.keyVar,
|
|
313
|
+
urlVar: plan.urlVar,
|
|
183
314
|
targetRel,
|
|
184
315
|
absPath: path.join(appRoot, targetRel),
|
|
185
316
|
envLines,
|
|
@@ -194,50 +325,88 @@ function computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl) {
|
|
|
194
325
|
};
|
|
195
326
|
}
|
|
196
327
|
|
|
197
|
-
/**
|
|
198
|
-
function
|
|
199
|
-
|
|
328
|
+
/** Lines explaining how to set the build-time public key in the deploy provider. */
|
|
329
|
+
function deployReminderLines(deploy) {
|
|
330
|
+
const { keyVar, apiKey, provider } = deploy;
|
|
331
|
+
const lines = [];
|
|
332
|
+
lines.push(
|
|
333
|
+
`${keyVar} is a PUBLISHABLE client key (safe in your frontend bundle), but it is`,
|
|
334
|
+
);
|
|
335
|
+
lines.push(
|
|
336
|
+
"read at BUILD time — it must also live in your deploy provider's env, or the",
|
|
337
|
+
);
|
|
338
|
+
lines.push('production build ships without it and the orb will NOT appear.');
|
|
339
|
+
lines.push('');
|
|
340
|
+
if (provider === 'vercel') {
|
|
341
|
+
lines.push('Vercel — add it to Production, Preview and Development:');
|
|
342
|
+
lines.push(` vercel env add ${keyVar} production`);
|
|
343
|
+
lines.push(` vercel env add ${keyVar} preview`);
|
|
344
|
+
lines.push(` vercel env add ${keyVar} development`);
|
|
345
|
+
lines.push(` (value: ${apiKey})`);
|
|
346
|
+
lines.push(' …or Project → Settings → Environment Variables in the dashboard,');
|
|
347
|
+
lines.push(' then redeploy.');
|
|
348
|
+
} else if (provider === 'netlify') {
|
|
349
|
+
lines.push('Netlify — Site settings → Environment variables, then redeploy:');
|
|
350
|
+
lines.push(` ${keyVar}=${apiKey}`);
|
|
351
|
+
} else if (provider === 'cloudflare') {
|
|
352
|
+
lines.push('Cloudflare Pages — Settings → Environment variables, then redeploy:');
|
|
353
|
+
lines.push(` ${keyVar}=${apiKey}`);
|
|
354
|
+
} else {
|
|
355
|
+
lines.push('Your deploy provider (Vercel / Netlify / Cloudflare / …) — add it in');
|
|
356
|
+
lines.push('its Environment Variables panel and redeploy:');
|
|
357
|
+
lines.push(` ${keyVar}=${apiKey}`);
|
|
358
|
+
}
|
|
359
|
+
return lines;
|
|
360
|
+
}
|
|
200
361
|
|
|
201
|
-
|
|
362
|
+
/** Branded reminder shown at the END (declined local write and/or deploy step). */
|
|
363
|
+
function printEnvManualReminder(result, { logUser, logDev }) {
|
|
364
|
+
if (!result) return;
|
|
365
|
+
const writePlan = result.writePlan || (result.kind ? result : null);
|
|
366
|
+
const deploy = result.deploy || null;
|
|
367
|
+
const declinedLocal =
|
|
368
|
+
result.status === 'declined by user' || result.status === 'manual';
|
|
202
369
|
|
|
203
|
-
if (writePlan.kind
|
|
204
|
-
|
|
205
|
-
` Angular: open ${writePlan.file} and set fyodosApiKey / fyodosApiUrl:`,
|
|
206
|
-
);
|
|
207
|
-
console.log(` fyodosApiKey: '${writePlan.apiKey}'`);
|
|
208
|
-
console.log(` fyodosApiUrl: '${writePlan.apiUrl}'`);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
370
|
+
if (writePlan && writePlan.kind !== 'aligned' && declinedLocal) {
|
|
371
|
+
logUser('Add your API key to your environment file (last step)');
|
|
211
372
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
373
|
+
if (writePlan.kind === 'manual-angular') {
|
|
374
|
+
console.log(` Angular: open ${writePlan.file} and set fyodosApiKey / fyodosApiUrl:`);
|
|
375
|
+
console.log(` fyodosApiKey: '${writePlan.apiKey}'`);
|
|
376
|
+
console.log(` fyodosApiUrl: '${writePlan.apiUrl}'`);
|
|
377
|
+
} else if (writePlan.kind === 'manual-unknown') {
|
|
378
|
+
console.log(' Add to your project .env (use NEXT_PUBLIC_ for Next, VITE_ for Vite):');
|
|
379
|
+
console.log(` ${writePlan.keyVar}=${writePlan.apiKey}`);
|
|
380
|
+
if (writePlan.apiUrl && writePlan.apiUrl !== 'https://api.fyodos.com') {
|
|
381
|
+
console.log(` ${writePlan.urlVar}=${writePlan.apiUrl}`);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
console.log(` File: ${writePlan.absPath}`);
|
|
385
|
+
console.log(' Add these lines:');
|
|
386
|
+
for (const line of writePlan.envLines) console.log(` ${line}`);
|
|
217
387
|
}
|
|
218
|
-
|
|
388
|
+
if (logDev) logDev(`[write-env] manual reminder for ${writePlan.targetRel || writePlan.file || 'env'}`);
|
|
219
389
|
}
|
|
220
390
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
for (const line of writePlan.envLines) {
|
|
226
|
-
console.log(` ${line}`);
|
|
391
|
+
if (deploy && deploy.reminder) {
|
|
392
|
+
logUser('Set the orb key in your deploy provider (or the orb is invisible in production)');
|
|
393
|
+
for (const line of deployReminderLines(deploy)) console.log(line ? ` ${line}` : '');
|
|
394
|
+
if (logDev) logDev(`[write-env] deploy reminder for provider=${deploy.provider || 'unknown'}`);
|
|
227
395
|
}
|
|
228
|
-
logDev(`[write-env] manual reminder for ${rel}`);
|
|
229
396
|
}
|
|
230
397
|
|
|
231
398
|
/**
|
|
232
|
-
* Ask consent and optionally write. Returns { status, writePlan }.
|
|
399
|
+
* Ask consent and optionally write. Returns { status, writePlan, deploy }.
|
|
233
400
|
* `assumeYes` comes from --write-env-yes only (NOT global --yes).
|
|
234
401
|
*/
|
|
235
402
|
async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes, log, logDev: devLog }) {
|
|
236
403
|
const writePlan = computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl);
|
|
237
|
-
if (!writePlan) return { status: 'skipped (no API key)', writePlan: null };
|
|
404
|
+
if (!writePlan) return { status: 'skipped (no API key)', writePlan: null, deploy: null };
|
|
238
405
|
if (writePlan.kind === 'aligned') {
|
|
239
406
|
log(`Your environment already uses the same API key as this project. Nothing to change.`);
|
|
240
|
-
|
|
407
|
+
// Even when the local env is aligned, production may still be missing the key.
|
|
408
|
+
const deploy = await offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog });
|
|
409
|
+
return { status: 'already aligned', writePlan, deploy };
|
|
241
410
|
}
|
|
242
411
|
|
|
243
412
|
const targetLabel =
|
|
@@ -249,18 +418,18 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
249
418
|
|
|
250
419
|
let proceed = assumeYes;
|
|
251
420
|
if (!proceed) {
|
|
252
|
-
proceed = await askYesNo(
|
|
253
|
-
`◉ Fiodos · Add your API key to ${targetLabel}? [yes/no] `,
|
|
254
|
-
);
|
|
421
|
+
proceed = await askYesNo(`◉ Fiodos · Add your API key to ${targetLabel}? [yes/no] `);
|
|
255
422
|
}
|
|
256
423
|
|
|
257
424
|
if (!proceed) {
|
|
258
|
-
|
|
425
|
+
const deploy = await offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog });
|
|
426
|
+
return { status: 'declined by user', writePlan, deploy };
|
|
259
427
|
}
|
|
260
428
|
|
|
261
429
|
if (writePlan.kind === 'manual-angular' || writePlan.kind === 'manual-unknown') {
|
|
262
430
|
log('Could not write automatically for this framework — see the reminder at the end.');
|
|
263
|
-
|
|
431
|
+
const deploy = await offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog });
|
|
432
|
+
return { status: 'manual', writePlan, deploy };
|
|
264
433
|
}
|
|
265
434
|
|
|
266
435
|
const { plan, targetRel, keyMatches, wantUrl, urlNeedsFix, apiKey: key, apiUrl: url } = writePlan;
|
|
@@ -279,10 +448,68 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
279
448
|
fs.writeFileSync(filePath, `${out.join('\n')}\n`);
|
|
280
449
|
log(`API key saved to ${targetRel}.`);
|
|
281
450
|
|
|
282
|
-
|
|
283
|
-
|
|
451
|
+
// SECURITY: guarantee the secret-bearing file is git-ignored (never pushed).
|
|
452
|
+
const gi = ensureEnvGitIgnored(appRoot, targetRel);
|
|
453
|
+
if (gi.changed) {
|
|
454
|
+
log(`Added ${gi.pattern} to .gitignore so your key is never pushed to GitHub.`);
|
|
455
|
+
} else if (gi.alreadyIgnored) {
|
|
456
|
+
if (devLog) devLog(`[write-env] ${targetRel} already covered by .gitignore`);
|
|
457
|
+
} else if (gi.error) {
|
|
458
|
+
log(`Heads up: could not update .gitignore — make sure ${targetRel} is git-ignored.`);
|
|
284
459
|
}
|
|
285
|
-
|
|
460
|
+
|
|
461
|
+
const deploy = await offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog });
|
|
462
|
+
return { status: 'written', writePlan, deploy };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Deploy-provider step for the build-time PUBLIC key (VITE_/NEXT_PUBLIC_). Same
|
|
467
|
+
* yes/no flow as the local write. When a linked Vercel project is detected and
|
|
468
|
+
* the user consents, the key is pushed automatically; otherwise (declined, no
|
|
469
|
+
* provider, non-TTY, or failure) a clear reminder is queued for the end.
|
|
470
|
+
*/
|
|
471
|
+
async function offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog }) {
|
|
472
|
+
const framework = writePlan.framework;
|
|
473
|
+
const keyVar = writePlan.keyVar;
|
|
474
|
+
const apiKey = writePlan.apiKey;
|
|
475
|
+
if (!apiKey || !keyVar || !isBuildTimePublicFramework(framework)) {
|
|
476
|
+
return null; // Angular / unknown handle their own manual reminders.
|
|
477
|
+
}
|
|
478
|
+
const det = detectDeployProvider(appRoot);
|
|
479
|
+
const base = { provider: det.provider, linked: det.linked, keyVar, apiKey };
|
|
480
|
+
|
|
481
|
+
// Only Vercel can be automated here, and only when the project is already
|
|
482
|
+
// linked (so the CLI never blocks on an interactive link/login).
|
|
483
|
+
if (det.provider === 'vercel' && det.linked) {
|
|
484
|
+
let proceed = assumeYes;
|
|
485
|
+
if (!proceed) {
|
|
486
|
+
proceed = await askYesNo(
|
|
487
|
+
`◉ Fiodos · Add ${keyVar} to your linked Vercel project (production, preview, development) now? [yes/no] `,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
if (proceed) {
|
|
491
|
+
const res = attemptVercelEnvAdd(appRoot, keyVar, apiKey, [
|
|
492
|
+
'production',
|
|
493
|
+
'preview',
|
|
494
|
+
'development',
|
|
495
|
+
]);
|
|
496
|
+
if (res.attempted && res.ok) {
|
|
497
|
+
log(`Pushed ${keyVar} to Vercel (${res.okCount}/3 environments). Redeploy to apply.`);
|
|
498
|
+
if (res.okCount < 3) {
|
|
499
|
+
if (devLog) devLog('[write-env] some Vercel envs failed (often already set) — reminder kept');
|
|
500
|
+
return { ...base, reminder: true, partial: true };
|
|
501
|
+
}
|
|
502
|
+
return { ...base, reminder: false, automated: true };
|
|
503
|
+
}
|
|
504
|
+
if (devLog) devLog(`[write-env] vercel automation failed: ${res.reason || 'cli-error'}`);
|
|
505
|
+
log('Could not push to Vercel automatically — see the reminder at the end.');
|
|
506
|
+
return { ...base, reminder: true };
|
|
507
|
+
}
|
|
508
|
+
return { ...base, reminder: true };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Any other provider / not linked / non-TTY: warn clearly at the end.
|
|
512
|
+
return { ...base, reminder: true };
|
|
286
513
|
}
|
|
287
514
|
|
|
288
515
|
module.exports = {
|
|
@@ -290,4 +517,9 @@ module.exports = {
|
|
|
290
517
|
resolveEnvPlan,
|
|
291
518
|
computeEnvWritePlan,
|
|
292
519
|
printEnvManualReminder,
|
|
520
|
+
ensureEnvGitIgnored,
|
|
521
|
+
gitignoreCovers,
|
|
522
|
+
detectDeployProvider,
|
|
523
|
+
isBuildTimePublicFramework,
|
|
524
|
+
deployReminderLines,
|
|
293
525
|
};
|