@fiodos/cli 0.1.16 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/writeEnv.js +68 -125
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiodos/cli",
3
- "version": "0.1.16",
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/writeEnv.js CHANGED
@@ -9,12 +9,14 @@
9
9
  * · yes → written to the framework's env file, which we ALSO ensure is in
10
10
  * .gitignore so the secret never reaches GitHub.
11
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.
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.
18
20
  *
19
21
  * SECURITY: no secret is ever hardcoded into committed source. The orb key
20
22
  * (`fyd_…`) is a PUBLISHABLE client key (safe inside the frontend bundle), but it
@@ -26,7 +28,6 @@
26
28
  const fs = require('fs');
27
29
  const path = require('path');
28
30
  const readline = require('readline');
29
- const { execFileSync } = require('child_process');
30
31
 
31
32
  function readJsonSafe(file) {
32
33
  try {
@@ -210,55 +211,6 @@ function detectDeployProvider(appRoot) {
210
211
  return { provider: null, linked: false };
211
212
  }
212
213
 
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
-
262
214
  /**
263
215
  * Compute where the key/URL would go and what would change — no writes, no prompts.
264
216
  * @returns {object|null} null when there is nothing to do (already aligned / no key).
@@ -325,37 +277,56 @@ function computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl) {
325
277
  };
326
278
  }
327
279
 
328
- /** Lines explaining how to set the build-time public key in the deploy provider. */
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
+ }
305
+
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
+ */
329
312
  function deployReminderLines(deploy) {
330
313
  const { keyVar, apiKey, provider } = deploy;
314
+ const hint = providerEnvHint(provider);
331
315
  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.');
316
+ lines.push(' Step 1 — Copy your orb key (this exact value):');
317
+ lines.push(` ${apiKey}`);
339
318
  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}`);
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})`);
354
324
  } 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}`);
325
+ lines.push(' (usually Project or Site Settings Environment Variables)');
358
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)'}`);
359
330
  return lines;
360
331
  }
361
332
 
@@ -389,7 +360,7 @@ function printEnvManualReminder(result, { logUser, logDev }) {
389
360
  }
390
361
 
391
362
  if (deploy && deploy.reminder) {
392
- logUser('Set the orb key in your deploy provider (or the orb is invisible in production)');
363
+ logUser('Make the orb appear in PRODUCTION add the key to your host (3 steps)');
393
364
  for (const line of deployReminderLines(deploy)) console.log(line ? ` ${line}` : '');
394
365
  if (logDev) logDev(`[write-env] deploy reminder for provider=${deploy.provider || 'unknown'}`);
395
366
  }
@@ -405,7 +376,7 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
405
376
  if (writePlan.kind === 'aligned') {
406
377
  log(`Your environment already uses the same API key as this project. Nothing to change.`);
407
378
  // Even when the local env is aligned, production may still be missing the key.
408
- const deploy = await offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog });
379
+ const deploy = planDeployReminder(appRoot, writePlan);
409
380
  return { status: 'already aligned', writePlan, deploy };
410
381
  }
411
382
 
@@ -422,13 +393,13 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
422
393
  }
423
394
 
424
395
  if (!proceed) {
425
- const deploy = await offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog });
396
+ const deploy = planDeployReminder(appRoot, writePlan);
426
397
  return { status: 'declined by user', writePlan, deploy };
427
398
  }
428
399
 
429
400
  if (writePlan.kind === 'manual-angular' || writePlan.kind === 'manual-unknown') {
430
401
  log('Could not write automatically for this framework — see the reminder at the end.');
431
- const deploy = await offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog });
402
+ const deploy = planDeployReminder(appRoot, writePlan);
432
403
  return { status: 'manual', writePlan, deploy };
433
404
  }
434
405
 
@@ -458,17 +429,22 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
458
429
  log(`Heads up: could not update .gitignore — make sure ${targetRel} is git-ignored.`);
459
430
  }
460
431
 
461
- const deploy = await offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog });
432
+ const deploy = planDeployReminder(appRoot, writePlan);
462
433
  return { status: 'written', writePlan, deploy };
463
434
  }
464
435
 
465
436
  /**
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.
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.
470
446
  */
471
- async function offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog }) {
447
+ function planDeployReminder(appRoot, writePlan) {
472
448
  const framework = writePlan.framework;
473
449
  const keyVar = writePlan.keyVar;
474
450
  const apiKey = writePlan.apiKey;
@@ -476,40 +452,7 @@ async function offerDeployEnv({ appRoot, writePlan, assumeYes, log, devLog }) {
476
452
  return null; // Angular / unknown handle their own manual reminders.
477
453
  }
478
454
  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 };
455
+ return { provider: det.provider, linked: det.linked, keyVar, apiKey, reminder: true };
513
456
  }
514
457
 
515
458
  module.exports = {