@fiodos/cli 0.1.15 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiodos/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
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 = { analyzeWithAI, correctActionWiring, DEFAULT_MODEL, PRICES, resolveModel, isAnthropicModel };
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
- if (
633
- envWriteResult &&
634
- (envWriteResult.status === 'declined by user' || envWriteResult.status === 'manual')
635
- ) {
636
- printEnvManualReminder(envWriteResult.writePlan, { logUser, logDev });
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,16 +1,32 @@
1
1
  /**
2
- * writeEnv — consent-based API key / URL alignment for the app's environment.
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
- * before writing (independent of --yes, which only skips orb/handler prompts).
6
- * If the developer declines, a copy-paste reminder is shown at the very END of
7
- * the run — after analysis, publish, orb mount and handler wiring are done.
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
- const patterns = content.split('\n').map((l) => l.trim());
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
- /** Branded reminder shown at the END when the developer declined the write. */
198
- function printEnvManualReminder(writePlan, { logUser, logDev }) {
199
- if (!writePlan || writePlan.kind === 'aligned') return;
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
- logUser('Add your API key to your environment file (last step)');
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 === 'manual-angular') {
204
- console.log(
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
- if (writePlan.kind === 'manual-unknown') {
213
- console.log(' Add to your project .env (use NEXT_PUBLIC_ for Next, VITE_ for Vite):');
214
- console.log(` ${writePlan.keyVar}=${writePlan.apiKey}`);
215
- if (writePlan.apiUrl && writePlan.apiUrl !== 'https://api.fyodos.com') {
216
- console.log(` ${writePlan.urlVar}=${writePlan.apiUrl}`);
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
- return;
388
+ if (logDev) logDev(`[write-env] manual reminder for ${writePlan.targetRel || writePlan.file || 'env'}`);
219
389
  }
220
390
 
221
- const rel = writePlan.targetRel;
222
- const displayPath = writePlan.absPath;
223
- console.log(` File: ${displayPath}`);
224
- console.log(' Add these lines:');
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
- return { status: 'already aligned', writePlan };
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
- return { status: 'declined by user', writePlan };
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
- return { status: 'manual', writePlan };
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
- if (!isGitIgnored(appRoot, targetRel)) {
283
- devLog(`Reminder: add ${targetRel} to .gitignore so you don't commit your API key.`);
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
- return { status: 'written', writePlan };
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
  };