@fiodos/cli 0.1.15 → 0.1.17
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 +6 -5
- package/src/writeEnv.js +221 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Fiodos CLI — analyzes your app's source code and generates the in-app voice-agent manifest, then wires the orb. Powers `npx @fiodos/cli analyze`.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
package/src/aiAnalyze.js
CHANGED
|
@@ -76,7 +76,7 @@ AppManifest (all fields required unless noted):
|
|
|
76
76
|
"requireConfirmation": boolean,
|
|
77
77
|
"confirmationMessageTemplate": string (REQUIRED if requireConfirmation true),
|
|
78
78
|
"parameters": { "<name>": {"type": "string"|"number"|"boolean", "required": boolean, "description": string (non-empty)} },
|
|
79
|
-
"contextRequirements": {"requiresVisibleContent"?: bool, "requiresAuth"?: bool} (optional),
|
|
79
|
+
"contextRequirements": {"requiresVisibleContent"?: bool, "requiresAuth"?: bool} (optional; set requiresAuth: true for actions that need a signed-in user — see the REQUIRES-AUTH rule below),
|
|
80
80
|
"examples": string[] (3-4 natural voice phrases, REQUIRED non-empty)
|
|
81
81
|
}],
|
|
82
82
|
"policies": {"requireConfirmationForDestructive": true, "allowedWithoutAuth": string[], "contextRetentionTurns": 5}
|
|
@@ -164,6 +164,12 @@ ACTIONS: things a user could trigger by voice. ${actionHintLine} CRITICAL RULES:
|
|
|
164
164
|
- Destructive/sensitive operations (delete data, logout, place/cancel orders, spend money) need requireConfirmation + confirmationMessageTemplate.
|
|
165
165
|
- "parameters" only when the user must supply a value by voice.
|
|
166
166
|
|
|
167
|
+
REQUIRES-AUTH — mark which actions need a signed-in user (set contextRequirements.requiresAuth):
|
|
168
|
+
This drives a real safety gate: an action marked requiresAuth: true is NOT executed unless the host app reports the user is signed in, so the agent never attempts inside-the-app operations from a logged-out state (e.g. the user is on the login screen). Decide per action by reading the code:
|
|
169
|
+
- Set requiresAuth: true for actions that only make sense for a signed-in user — i.e. anything that reads or changes data tied to a personal session/account: viewing or editing the user's own data, profile/settings, orders/cart history, balances, posting/saving content, in-app operations, or navigating-by-doing into private/authenticated areas. A strong signal: the handler reads a session/token/user id, calls an authenticated API, or lives behind an auth guard/middleware/redirect-to-login. If the app has a global auth gate (a root middleware/layout that redirects unauthenticated users), treat its inside-app actions as requiresAuth: true by default.
|
|
170
|
+
- Leave requiresAuth false/absent for genuinely public actions that work with no session: public navigation, marketing/landing LINK actions (download/get-the-app, social links, contact/email/phone, pricing, legal, in-page anchors), changing language/theme, viewing openly public information, and the sign-in/sign-up flow itself.
|
|
171
|
+
- Be conservative AND fail-safe: do not over-mark clearly public actions (that would needlessly block them when logged out), but when you genuinely cannot tell whether an action is public or requires a session, prefer requiresAuth: true — it is safer to ask the user to sign in than to attempt a private action without a session. requiresAuth never replaces requireConfirmation; an action can need both.
|
|
172
|
+
|
|
167
173
|
THE MANUAL (description/preconditions/effect/relatedIntents per action; description per screen; appType/appFlow):
|
|
168
174
|
This is the context the agent reads to understand the app, so it can reason instead of guessing. Derive every field from the REAL code:
|
|
169
175
|
- description: what the action actually does (read the handler's body).
|
|
@@ -511,4 +517,14 @@ async function correctActionWiring({
|
|
|
511
517
|
return { wiring, usage, costUSD };
|
|
512
518
|
}
|
|
513
519
|
|
|
514
|
-
module.exports = {
|
|
520
|
+
module.exports = {
|
|
521
|
+
analyzeWithAI,
|
|
522
|
+
correctActionWiring,
|
|
523
|
+
DEFAULT_MODEL,
|
|
524
|
+
PRICES,
|
|
525
|
+
resolveModel,
|
|
526
|
+
isAnthropicModel,
|
|
527
|
+
// Exported for offline tests of the analysis guidance (no API key needed).
|
|
528
|
+
buildSystemPrompt,
|
|
529
|
+
manifestSchemaDoc,
|
|
530
|
+
};
|
package/src/index.js
CHANGED
|
@@ -629,11 +629,12 @@ async function main() {
|
|
|
629
629
|
log: logUser,
|
|
630
630
|
logDev,
|
|
631
631
|
});
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
)
|
|
636
|
-
|
|
632
|
+
// The reminder decides what to show: the local-env copy-paste block (only
|
|
633
|
+
// when the developer declined the local write) AND/OR the deploy-provider
|
|
634
|
+
// notice for the build-time public key (so the orb is never invisible in
|
|
635
|
+
// production for a missing var). No-ops when there is nothing to show.
|
|
636
|
+
if (envWriteResult) {
|
|
637
|
+
printEnvManualReminder(envWriteResult, { logUser, logDev });
|
|
637
638
|
}
|
|
638
639
|
} catch (e) {
|
|
639
640
|
logDev(`[write-env] skipped: ${e.message}`);
|
package/src/writeEnv.js
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
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 — for the build-time public key (VITE_/NEXT_PUBLIC_),
|
|
13
|
+
* which is inlined at BUILD time: if it is only in the (gitignored,
|
|
14
|
+
* never-pushed) local env and NOT in the provider, the production build
|
|
15
|
+
* ships WITHOUT it and the orb never appears. A CLI cannot (and must not)
|
|
16
|
+
* authenticate into the user's hosting account, so we do NOT prompt and we
|
|
17
|
+
* do NOT touch their provider. Instead we DETECT the stack and print a
|
|
18
|
+
* clear, numbered step-by-step at the END (copy key → paste in the
|
|
19
|
+
* provider's env panel → redeploy) so they do it themselves, safely.
|
|
20
|
+
*
|
|
21
|
+
* SECURITY: no secret is ever hardcoded into committed source. The orb key
|
|
22
|
+
* (`fyd_…`) is a PUBLISHABLE client key (safe inside the frontend bundle), but it
|
|
23
|
+
* still must exist in the build environment — which is why it goes through env
|
|
24
|
+
* vars and the deploy-provider step, never into the repo.
|
|
8
25
|
*/
|
|
9
26
|
'use strict';
|
|
10
27
|
|
|
@@ -63,6 +80,11 @@ function resolveEnvPlan(appRoot) {
|
|
|
63
80
|
return { framework: null, file: null, candidates: [], keyVar: null, urlVar: null };
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
/** Frameworks whose public env vars are INLINED at build time (need provider env). */
|
|
84
|
+
function isBuildTimePublicFramework(framework) {
|
|
85
|
+
return framework === 'vite' || framework === 'next';
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
function findEffectiveVar(appRoot, candidates, name) {
|
|
67
89
|
for (const rel of candidates) {
|
|
68
90
|
const filePath = path.join(appRoot, rel);
|
|
@@ -117,20 +139,78 @@ function askYesNo(question) {
|
|
|
117
139
|
});
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
/** True if `.gitignore` already keeps `fileName` (or a glob covering it) out of git. */
|
|
120
143
|
function isGitIgnored(appRoot, fileName) {
|
|
121
144
|
const gi = path.join(appRoot, '.gitignore');
|
|
122
145
|
if (!fs.existsSync(gi)) return false;
|
|
123
146
|
try {
|
|
124
147
|
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
|
-
);
|
|
148
|
+
return gitignoreCovers(content, fileName);
|
|
129
149
|
} catch {
|
|
130
150
|
return false;
|
|
131
151
|
}
|
|
132
152
|
}
|
|
133
153
|
|
|
154
|
+
/** Pure: does this .gitignore content keep `fileName` out of git? */
|
|
155
|
+
function gitignoreCovers(content, fileName) {
|
|
156
|
+
const patterns = content.split('\n').map((l) => l.trim());
|
|
157
|
+
const glob = fileName.endsWith('.local') ? '*.local' : '.env*';
|
|
158
|
+
return patterns.some(
|
|
159
|
+
(p) =>
|
|
160
|
+
p === fileName ||
|
|
161
|
+
p === `/${fileName}` ||
|
|
162
|
+
p === '.env*' ||
|
|
163
|
+
p === '.env.*' ||
|
|
164
|
+
p === '*.local' ||
|
|
165
|
+
p === glob ||
|
|
166
|
+
p === fileName.replace(/^\.env/, '.env*'),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Guarantee the secret-bearing env file is git-ignored (never pushed to GitHub).
|
|
172
|
+
* Appends a covering pattern to `.gitignore` (creating it when absent) when the
|
|
173
|
+
* file is not already ignored. Idempotent. Returns what happened.
|
|
174
|
+
*/
|
|
175
|
+
function ensureEnvGitIgnored(appRoot, fileName) {
|
|
176
|
+
const gi = path.join(appRoot, '.gitignore');
|
|
177
|
+
const exists = fs.existsSync(gi);
|
|
178
|
+
let content = '';
|
|
179
|
+
try {
|
|
180
|
+
content = exists ? fs.readFileSync(gi, 'utf8') : '';
|
|
181
|
+
} catch {
|
|
182
|
+
content = '';
|
|
183
|
+
}
|
|
184
|
+
if (exists && gitignoreCovers(content, fileName)) {
|
|
185
|
+
return { changed: false, created: false, alreadyIgnored: true, pattern: null };
|
|
186
|
+
}
|
|
187
|
+
const pattern = fileName.endsWith('.local') ? '*.local' : fileName;
|
|
188
|
+
const header = '# Fiodos: keep local secrets out of git';
|
|
189
|
+
const prefix = content.length === 0 ? '' : content.endsWith('\n') ? '' : '\n';
|
|
190
|
+
const block = `${prefix}${content.length ? '\n' : ''}${header}\n${pattern}\n`;
|
|
191
|
+
try {
|
|
192
|
+
fs.writeFileSync(gi, content + block);
|
|
193
|
+
return { changed: true, created: !exists, alreadyIgnored: false, pattern };
|
|
194
|
+
} catch {
|
|
195
|
+
return { changed: false, created: false, alreadyIgnored: false, pattern, error: true };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Detect a deploy provider (and whether a Vercel project is already linked). */
|
|
200
|
+
function detectDeployProvider(appRoot) {
|
|
201
|
+
const has = (rel) => fs.existsSync(path.join(appRoot, rel));
|
|
202
|
+
if (has('.vercel/project.json') || has('vercel.json')) {
|
|
203
|
+
return { provider: 'vercel', linked: has('.vercel/project.json') };
|
|
204
|
+
}
|
|
205
|
+
if (has('netlify.toml') || has('.netlify')) {
|
|
206
|
+
return { provider: 'netlify', linked: false };
|
|
207
|
+
}
|
|
208
|
+
if (has('wrangler.toml')) {
|
|
209
|
+
return { provider: 'cloudflare', linked: false };
|
|
210
|
+
}
|
|
211
|
+
return { provider: null, linked: false };
|
|
212
|
+
}
|
|
213
|
+
|
|
134
214
|
/**
|
|
135
215
|
* Compute where the key/URL would go and what would change — no writes, no prompts.
|
|
136
216
|
* @returns {object|null} null when there is nothing to do (already aligned / no key).
|
|
@@ -170,7 +250,7 @@ function computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl) {
|
|
|
170
250
|
const urlOk = !urlNeedsFix && (wantUrl ? currentUrl === wantUrl : true);
|
|
171
251
|
|
|
172
252
|
if (keyMatches && urlOk) {
|
|
173
|
-
return { kind: 'aligned', targetRel, plan };
|
|
253
|
+
return { kind: 'aligned', targetRel, plan, framework: plan.framework, keyVar: plan.keyVar, apiKey };
|
|
174
254
|
}
|
|
175
255
|
|
|
176
256
|
const envLines = [`${plan.keyVar}=${apiKey}`];
|
|
@@ -180,6 +260,9 @@ function computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl) {
|
|
|
180
260
|
return {
|
|
181
261
|
kind: 'writable',
|
|
182
262
|
plan,
|
|
263
|
+
framework: plan.framework,
|
|
264
|
+
keyVar: plan.keyVar,
|
|
265
|
+
urlVar: plan.urlVar,
|
|
183
266
|
targetRel,
|
|
184
267
|
absPath: path.join(appRoot, targetRel),
|
|
185
268
|
envLines,
|
|
@@ -194,50 +277,107 @@ function computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl) {
|
|
|
194
277
|
};
|
|
195
278
|
}
|
|
196
279
|
|
|
197
|
-
/**
|
|
198
|
-
function
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
280
|
+
/** Where, per provider, the Environment Variables panel lives + how to redeploy. */
|
|
281
|
+
function providerEnvHint(provider) {
|
|
282
|
+
if (provider === 'vercel') {
|
|
283
|
+
return {
|
|
284
|
+
label: 'Vercel',
|
|
285
|
+
where: 'Project → Settings → Environment Variables (tick Production, Preview, Development)',
|
|
286
|
+
redeploy: 'Deployments → ⋯ → Redeploy (or push a new commit)',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (provider === 'netlify') {
|
|
290
|
+
return {
|
|
291
|
+
label: 'Netlify',
|
|
292
|
+
where: 'Site configuration → Environment variables → Add a variable',
|
|
293
|
+
redeploy: 'Deploys → Trigger deploy → Deploy site',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (provider === 'cloudflare') {
|
|
297
|
+
return {
|
|
298
|
+
label: 'Cloudflare Pages',
|
|
299
|
+
where: 'Workers & Pages → your project → Settings → Variables and Secrets',
|
|
300
|
+
redeploy: 'Deployments → Retry deployment (or push a new commit)',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
202
305
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Clear, numbered step-by-step for setting the build-time public key in the
|
|
308
|
+
* deploy provider. Step 2 always speaks generically ("your hosting provider");
|
|
309
|
+
* when we detect a provider we append an optional tip in parentheses — we never
|
|
310
|
+
* assume the user's host is Vercel/Netlify/etc.
|
|
311
|
+
*/
|
|
312
|
+
function deployReminderLines(deploy) {
|
|
313
|
+
const { keyVar, apiKey, provider } = deploy;
|
|
314
|
+
const hint = providerEnvHint(provider);
|
|
315
|
+
const lines = [];
|
|
316
|
+
lines.push(' Step 1 — Copy your orb key (this exact value):');
|
|
317
|
+
lines.push(` ${apiKey}`);
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push(' Step 2 — In your hosting provider, open Environment Variables and add:');
|
|
320
|
+
lines.push(` Name: ${keyVar}`);
|
|
321
|
+
lines.push(` Value: ${apiKey}`);
|
|
322
|
+
if (hint) {
|
|
323
|
+
lines.push(` (e.g. ${hint.label}: ${hint.where})`);
|
|
324
|
+
} else {
|
|
325
|
+
lines.push(' (usually Project or Site → Settings → Environment Variables)');
|
|
210
326
|
}
|
|
327
|
+
lines.push('');
|
|
328
|
+
lines.push(' Step 3 — Redeploy so the variable is built in:');
|
|
329
|
+
lines.push(` ${hint ? hint.redeploy : 'Trigger a new deploy (or push a commit)'}`);
|
|
330
|
+
return lines;
|
|
331
|
+
}
|
|
211
332
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
333
|
+
/** Branded reminder shown at the END (declined local write and/or deploy step). */
|
|
334
|
+
function printEnvManualReminder(result, { logUser, logDev }) {
|
|
335
|
+
if (!result) return;
|
|
336
|
+
const writePlan = result.writePlan || (result.kind ? result : null);
|
|
337
|
+
const deploy = result.deploy || null;
|
|
338
|
+
const declinedLocal =
|
|
339
|
+
result.status === 'declined by user' || result.status === 'manual';
|
|
340
|
+
|
|
341
|
+
if (writePlan && writePlan.kind !== 'aligned' && declinedLocal) {
|
|
342
|
+
logUser('Add your API key to your environment file (last step)');
|
|
343
|
+
|
|
344
|
+
if (writePlan.kind === 'manual-angular') {
|
|
345
|
+
console.log(` Angular: open ${writePlan.file} and set fyodosApiKey / fyodosApiUrl:`);
|
|
346
|
+
console.log(` fyodosApiKey: '${writePlan.apiKey}'`);
|
|
347
|
+
console.log(` fyodosApiUrl: '${writePlan.apiUrl}'`);
|
|
348
|
+
} else if (writePlan.kind === 'manual-unknown') {
|
|
349
|
+
console.log(' Add to your project .env (use NEXT_PUBLIC_ for Next, VITE_ for Vite):');
|
|
350
|
+
console.log(` ${writePlan.keyVar}=${writePlan.apiKey}`);
|
|
351
|
+
if (writePlan.apiUrl && writePlan.apiUrl !== 'https://api.fyodos.com') {
|
|
352
|
+
console.log(` ${writePlan.urlVar}=${writePlan.apiUrl}`);
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
console.log(` File: ${writePlan.absPath}`);
|
|
356
|
+
console.log(' Add these lines:');
|
|
357
|
+
for (const line of writePlan.envLines) console.log(` ${line}`);
|
|
217
358
|
}
|
|
218
|
-
|
|
359
|
+
if (logDev) logDev(`[write-env] manual reminder for ${writePlan.targetRel || writePlan.file || 'env'}`);
|
|
219
360
|
}
|
|
220
361
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
for (const line of writePlan.envLines) {
|
|
226
|
-
console.log(` ${line}`);
|
|
362
|
+
if (deploy && deploy.reminder) {
|
|
363
|
+
logUser('Make the orb appear in PRODUCTION — add the key to your host (3 steps)');
|
|
364
|
+
for (const line of deployReminderLines(deploy)) console.log(line ? ` ${line}` : '');
|
|
365
|
+
if (logDev) logDev(`[write-env] deploy reminder for provider=${deploy.provider || 'unknown'}`);
|
|
227
366
|
}
|
|
228
|
-
logDev(`[write-env] manual reminder for ${rel}`);
|
|
229
367
|
}
|
|
230
368
|
|
|
231
369
|
/**
|
|
232
|
-
* Ask consent and optionally write. Returns { status, writePlan }.
|
|
370
|
+
* Ask consent and optionally write. Returns { status, writePlan, deploy }.
|
|
233
371
|
* `assumeYes` comes from --write-env-yes only (NOT global --yes).
|
|
234
372
|
*/
|
|
235
373
|
async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes, log, logDev: devLog }) {
|
|
236
374
|
const writePlan = computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl);
|
|
237
|
-
if (!writePlan) return { status: 'skipped (no API key)', writePlan: null };
|
|
375
|
+
if (!writePlan) return { status: 'skipped (no API key)', writePlan: null, deploy: null };
|
|
238
376
|
if (writePlan.kind === 'aligned') {
|
|
239
377
|
log(`Your environment already uses the same API key as this project. Nothing to change.`);
|
|
240
|
-
|
|
378
|
+
// Even when the local env is aligned, production may still be missing the key.
|
|
379
|
+
const deploy = planDeployReminder(appRoot, writePlan);
|
|
380
|
+
return { status: 'already aligned', writePlan, deploy };
|
|
241
381
|
}
|
|
242
382
|
|
|
243
383
|
const targetLabel =
|
|
@@ -249,18 +389,18 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
249
389
|
|
|
250
390
|
let proceed = assumeYes;
|
|
251
391
|
if (!proceed) {
|
|
252
|
-
proceed = await askYesNo(
|
|
253
|
-
`◉ Fiodos · Add your API key to ${targetLabel}? [yes/no] `,
|
|
254
|
-
);
|
|
392
|
+
proceed = await askYesNo(`◉ Fiodos · Add your API key to ${targetLabel}? [yes/no] `);
|
|
255
393
|
}
|
|
256
394
|
|
|
257
395
|
if (!proceed) {
|
|
258
|
-
|
|
396
|
+
const deploy = planDeployReminder(appRoot, writePlan);
|
|
397
|
+
return { status: 'declined by user', writePlan, deploy };
|
|
259
398
|
}
|
|
260
399
|
|
|
261
400
|
if (writePlan.kind === 'manual-angular' || writePlan.kind === 'manual-unknown') {
|
|
262
401
|
log('Could not write automatically for this framework — see the reminder at the end.');
|
|
263
|
-
|
|
402
|
+
const deploy = planDeployReminder(appRoot, writePlan);
|
|
403
|
+
return { status: 'manual', writePlan, deploy };
|
|
264
404
|
}
|
|
265
405
|
|
|
266
406
|
const { plan, targetRel, keyMatches, wantUrl, urlNeedsFix, apiKey: key, apiUrl: url } = writePlan;
|
|
@@ -279,10 +419,40 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
|
|
|
279
419
|
fs.writeFileSync(filePath, `${out.join('\n')}\n`);
|
|
280
420
|
log(`API key saved to ${targetRel}.`);
|
|
281
421
|
|
|
282
|
-
|
|
283
|
-
|
|
422
|
+
// SECURITY: guarantee the secret-bearing file is git-ignored (never pushed).
|
|
423
|
+
const gi = ensureEnvGitIgnored(appRoot, targetRel);
|
|
424
|
+
if (gi.changed) {
|
|
425
|
+
log(`Added ${gi.pattern} to .gitignore so your key is never pushed to GitHub.`);
|
|
426
|
+
} else if (gi.alreadyIgnored) {
|
|
427
|
+
if (devLog) devLog(`[write-env] ${targetRel} already covered by .gitignore`);
|
|
428
|
+
} else if (gi.error) {
|
|
429
|
+
log(`Heads up: could not update .gitignore — make sure ${targetRel} is git-ignored.`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const deploy = planDeployReminder(appRoot, writePlan);
|
|
433
|
+
return { status: 'written', writePlan, deploy };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Plan the deploy-provider reminder for the build-time PUBLIC key
|
|
438
|
+
* (VITE_/NEXT_PUBLIC_). We do NOT prompt and we do NOT touch the user's hosting
|
|
439
|
+
* provider — a CLI cannot safely authenticate into someone's Vercel/Netlify
|
|
440
|
+
* account, and we never want to access a production provider on their behalf.
|
|
441
|
+
*
|
|
442
|
+
* Instead we DETECT the stack (provider + var name) and queue a clear, numbered
|
|
443
|
+
* step-by-step that is printed at the very end of the run (see
|
|
444
|
+
* `deployReminderLines`). The developer copies the key and pastes it into their
|
|
445
|
+
* provider's Environment Variables panel themselves, then redeploys.
|
|
446
|
+
*/
|
|
447
|
+
function planDeployReminder(appRoot, writePlan) {
|
|
448
|
+
const framework = writePlan.framework;
|
|
449
|
+
const keyVar = writePlan.keyVar;
|
|
450
|
+
const apiKey = writePlan.apiKey;
|
|
451
|
+
if (!apiKey || !keyVar || !isBuildTimePublicFramework(framework)) {
|
|
452
|
+
return null; // Angular / unknown handle their own manual reminders.
|
|
284
453
|
}
|
|
285
|
-
|
|
454
|
+
const det = detectDeployProvider(appRoot);
|
|
455
|
+
return { provider: det.provider, linked: det.linked, keyVar, apiKey, reminder: true };
|
|
286
456
|
}
|
|
287
457
|
|
|
288
458
|
module.exports = {
|
|
@@ -290,4 +460,9 @@ module.exports = {
|
|
|
290
460
|
resolveEnvPlan,
|
|
291
461
|
computeEnvWritePlan,
|
|
292
462
|
printEnvManualReminder,
|
|
463
|
+
ensureEnvGitIgnored,
|
|
464
|
+
gitignoreCovers,
|
|
465
|
+
detectDeployProvider,
|
|
466
|
+
isBuildTimePublicFramework,
|
|
467
|
+
deployReminderLines,
|
|
293
468
|
};
|