@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.
- package/package.json +1 -1
- 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.
|
|
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 —
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
lines.push(`
|
|
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('
|
|
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('
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
432
|
+
const deploy = planDeployReminder(appRoot, writePlan);
|
|
462
433
|
return { status: 'written', writePlan, deploy };
|
|
463
434
|
}
|
|
464
435
|
|
|
465
436
|
/**
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|