@fiodos/cli 0.1.4 → 0.1.9

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.
@@ -1360,10 +1360,10 @@ async function runAutoCorrectionLoop(ctx) {
1360
1360
  const {
1361
1361
  appRoot, fyodosDirRel, fyodosDirAbs, manifest, evidence, wiringMap,
1362
1362
  ts, esm, ext, testRunner, corrector, files, framework,
1363
- maxAttempts = 3, tag, dim, reset, registryRel,
1363
+ maxAttempts = 3, tag, dim, reset, registryRel, quiet = false,
1364
1364
  } = ctx;
1365
1365
 
1366
- const log = (m) => console.error(`${tag} · ${dim || ''}${m}${reset || ''}`);
1366
+ const log = quiet ? () => {} : (m) => console.error(`${tag} · ${dim || ''}${m}${reset || ''}`);
1367
1367
  log('Verifying and auto-correcting each action (may take a while)…');
1368
1368
  const baseline = await testRunner(appRoot, { framework });
1369
1369
 
@@ -1455,6 +1455,7 @@ async function wireHandlers(appRoot, opts = {}) {
1455
1455
  colors = {},
1456
1456
  assumeYes = false,
1457
1457
  noWire = false,
1458
+ quiet = false,
1458
1459
  framework = 'web',
1459
1460
  appName,
1460
1461
  registryExt: registryExtOpt,
@@ -1516,13 +1517,15 @@ async function wireHandlers(appRoot, opts = {}) {
1516
1517
  // to consent and let the loop decide what ends up wired.
1517
1518
  const loopEnabled = typeof corrector === 'function' && runTest && testRunner;
1518
1519
  if (!plan.auto.length && !loopEnabled) {
1519
- console.error(
1520
- `\n${tag} · ${dim || ''}Prepared the action wiring, but none of the ` +
1521
- `${plan.manual.length} action(s) can be connected with confidence.${reset || ''}`,
1522
- );
1523
- console.error(
1524
- `${tag} · See what is needed and how to do it by hand in: ${docRel}`,
1525
- );
1520
+ if (!quiet) {
1521
+ console.error(
1522
+ `\n${tag} · ${dim || ''}Prepared the action wiring, but none of the ` +
1523
+ `${plan.manual.length} action(s) can be connected with confidence.${reset || ''}`,
1524
+ );
1525
+ console.error(
1526
+ `${tag} · See what is needed and how to do it by hand in: ${docRel}`,
1527
+ );
1528
+ }
1526
1529
  return { status: 'manual-only', docPath: docAbs, autoCount: 0, manualCount: plan.manual.length, plan };
1527
1530
  }
1528
1531
 
@@ -1538,29 +1541,33 @@ async function wireHandlers(appRoot, opts = {}) {
1538
1541
  `${tag} · Apply these changes after reviewing? [yes/no] `;
1539
1542
 
1540
1543
  if (!assumeYes && !process.stdin.isTTY) {
1541
- console.error(prompt.trimEnd());
1542
- console.error(
1543
- `${tag} · ${dim || ''}Non-interactive terminal: leaving your code untouched. The document ` +
1544
- `is at ${docRel}. Re-run with --wire-yes to apply it.${reset || ''}`,
1545
- );
1544
+ if (!quiet) {
1545
+ console.error(prompt.trimEnd());
1546
+ console.error(
1547
+ `${tag} · ${dim || ''}Non-interactive terminal: leaving your code untouched. The document ` +
1548
+ `is at ${docRel}. Re-run with --wire-yes to apply it.${reset || ''}`,
1549
+ );
1550
+ }
1546
1551
  return { status: 'declined-noninteractive', docPath: docAbs, autoCount: plan.auto.length, manualCount: plan.manual.length, plan };
1547
1552
  }
1548
1553
 
1549
1554
  const yes = assumeYes || (await askYesNo(prompt));
1550
1555
  if (!yes) {
1551
- console.error(
1552
- `${tag} · ${dim || ''}Leaving your code untouched. The document stays at ${docRel} ` +
1553
- `in case you want to wire it by hand or re-run with --wire-yes.${reset || ''}`,
1554
- );
1556
+ if (!quiet) {
1557
+ console.error(
1558
+ `${tag} · ${dim || ''}Leaving your code untouched. The document stays at ${docRel} ` +
1559
+ `in case you want to wire it by hand or re-run with --wire-yes.${reset || ''}`,
1560
+ );
1561
+ }
1555
1562
  return { status: 'declined', docPath: docAbs, autoCount: plan.auto.length, manualCount: plan.manual.length, plan };
1556
1563
  }
1557
1564
 
1558
1565
  // 4) Baseline: BEFORE touching anything, check whether the app already builds.
1559
- // This lets us tell "we broke it" (revert) from "it was already broken"
1560
- // (don't revert a good wiring over a pre-existing failure).
1561
1566
  let baseline = null;
1562
1567
  if (runTest && testRunner) {
1563
- console.error(`${tag} · ${dim || ''}Checking the build baseline BEFORE wiring…${reset || ''}`);
1568
+ if (!quiet) {
1569
+ console.error(`${tag} · ${dim || ''}Checking the build baseline BEFORE wiring…${reset || ''}`);
1570
+ }
1564
1571
  try {
1565
1572
  baseline = await testRunner(appRoot, { framework });
1566
1573
  } catch (err) {
@@ -1579,7 +1586,7 @@ async function wireHandlers(appRoot, opts = {}) {
1579
1586
  const loop = await runAutoCorrectionLoop({
1580
1587
  appRoot, fyodosDirRel, fyodosDirAbs, manifest, evidence, wiringMap,
1581
1588
  ts, esm, ext, testRunner, corrector, files, framework, maxAttempts,
1582
- tag, dim, reset, registryRel, baseline,
1589
+ tag, dim, reset, registryRel, baseline, quiet,
1583
1590
  });
1584
1591
 
1585
1592
  const finalAuto = loop.ready;
@@ -1692,7 +1699,9 @@ async function wireHandlers(appRoot, opts = {}) {
1692
1699
  if (baseline && baseline.ok === false && baseline.stage !== 'skipped-no-deps') {
1693
1700
  return { status: 'applied-untested', ...baseResult, test: baseline, preexistingFailure: true };
1694
1701
  }
1695
- console.error(`${tag} · ${dim || ''}Checking the app still builds after wiring…${reset || ''}`);
1702
+ if (!quiet) {
1703
+ console.error(`${tag} · ${dim || ''}Checking the app still builds after wiring…${reset || ''}`);
1704
+ }
1696
1705
  let test;
1697
1706
  try {
1698
1707
  test = await testRunner(appRoot, { framework });
@@ -1710,50 +1719,26 @@ async function wireHandlers(appRoot, opts = {}) {
1710
1719
  return { status: 'applied', ...baseResult };
1711
1720
  }
1712
1721
 
1713
- function reportHandlerResult(result, colors = {}) {
1722
+ function reportHandlerResult(result, colors = {}, opts = {}) {
1723
+ const quiet = opts.quiet !== false;
1714
1724
  if (!result) return;
1715
1725
  const { blue, cyan, dim, reset } = colors;
1716
1726
  const tag = `${cyan || ''}◉${reset || ''} ${blue || ''}Fiodos${reset || ''}`;
1717
1727
  switch (result.status) {
1718
1728
  case 'applied':
1719
- case 'applied-untested': {
1720
- const rel = result.registryRel || result.registryFile;
1721
- console.error(`${tag} · ✓ Handler wiring applied → ${rel} (${result.autoCount} action(s)).`);
1722
- if (Array.isArray(result.verification) && result.verification.length) {
1723
- const ready = result.verification.filter((v) => v.status === 'ready');
1724
- const corrected = ready.filter((v) => v.attempts > 1).length;
1725
- const firstTry = ready.length - corrected;
1726
- const eff = ready.filter((v) => v.level === 'effect').length;
1727
- console.error(`${tag} · ${dim || ''}Verification: ${ready.length} action(s) ready (${firstTry} on first try, ${corrected} after auto-correction, ${eff} with real effect confirmed).${reset || ''}`);
1728
- }
1729
- if (result.editedFiles && result.editedFiles.length) {
1730
- console.error(`${tag} · ${dim || ''}Bridges added (reversible) in: ${result.editedFiles.join(', ')}.${reset || ''}`);
1731
- }
1732
- if (Array.isArray(result.dropped) && result.dropped.length) {
1733
- console.error(`${tag} · ${dim || ''}${result.dropped.length} action(s) could NOT be wired after retries (reverted, see the document): ${result.dropped.map((d) => d.intent).join(', ')}.${reset || ''}`);
1734
- } else if (result.manualCount) {
1735
- console.error(`${tag} · ${dim || ''}${result.manualCount} action(s) left for review (see the document).${reset || ''}`);
1736
- }
1737
- if (result.test && result.test.ok) {
1738
- console.error(`${tag} · ✓ Post-wiring build check OK (${result.test.stage}).`);
1739
- } else if (result.preexistingFailure) {
1740
- console.error(`${tag} · ${dim || ''}Note: your app was ALREADY not building BEFORE wiring (${result.test && result.test.stage}); not caused by Fiodos, so the wiring is kept but cannot be build-verified.${reset || ''}`);
1741
- } else if (result.status === 'applied-untested') {
1742
- console.error(`${tag} · ${dim || ''}Note: could not verify the build (${result.test && result.test.stage}); please check it.${reset || ''}`);
1743
- }
1744
- console.error(`${tag} · ${dim || ''}Enable it by passing registries={fyodosGeneratedRegistries} to <FiodosAgent/> (snippet in the document).${reset || ''}`);
1729
+ case 'applied-untested':
1730
+ if (quiet) return;
1745
1731
  break;
1746
- }
1747
1732
  case 'reverted':
1748
1733
  console.error(`${tag} · ✗ Post-wiring build check failed (${result.test && result.test.stage}) — reverted all changes, your app is untouched.`);
1749
1734
  console.error(`${tag} · ${dim || ''}Details in the document; nothing was modified in your code.${reset || ''}`);
1750
1735
  break;
1751
1736
  case 'manual-only':
1737
+ if (quiet) return;
1752
1738
  console.error(`${tag} · ${dim || ''}Wiring documented; apply it by hand following ${result.docPath}.${reset || ''}`);
1753
1739
  break;
1754
1740
  case 'declined':
1755
1741
  case 'declined-noninteractive':
1756
- // already reported inline
1757
1742
  break;
1758
1743
  case 'failed':
1759
1744
  console.error(`${tag} · ✗ Could not write the handler registry: ${result.error && result.error.message}`);
package/src/wireWeb.js CHANGED
@@ -222,7 +222,9 @@ async function wireWebOrb(appRoot, opts = {}) {
222
222
  return { status: 'printed', reason: plan.reason };
223
223
  }
224
224
 
225
- printMountPreview(plan, colors);
225
+ if (!assumeYes) {
226
+ printMountPreview(plan, colors);
227
+ }
226
228
 
227
229
  const yes = assumeYes || (await askYesNo(
228
230
  `${colors.cyan || ''}◉${colors.reset || ''} About to add the orb to ${plan.rel}. Proceed? [y/N] `,
@@ -286,22 +288,15 @@ async function wireWebOrb(appRoot, opts = {}) {
286
288
  }
287
289
  }
288
290
 
289
- function reportWireResult(result, colors = {}) {
291
+ function reportWireResult(result, colors = {}, opts = {}) {
292
+ const quiet = opts.quiet !== false;
293
+ if (!result) return;
290
294
  const { blue, cyan, dim, reset } = colors;
291
295
  const tag = `${cyan || ''}◉${reset || ''} ${blue || ''}Fiodos${reset || ''}`;
292
296
  switch (result.status) {
293
297
  case 'added':
294
- console.error(`${tag} · ✓ orb added to ${result.file}. Start your web app (e.g. \`npm run dev\`) to see it.`);
295
- if (result.preexistingFailure) {
296
- console.error(`${tag} · ${dim || ''}Note: your app was ALREADY not building before the orb was added (${result.test && result.test.stage}); not caused by Fiodos — the orb is kept so it appears once your build is fixed.${reset || ''}`);
297
- } else if (result.test && result.test.stage === 'skipped-no-deps') {
298
- console.error(`${tag} · ${dim || ''}build not verified (no node_modules) — run npm install && npm run build locally.${reset || ''}`);
299
- } else if (result.test && result.test.ok) {
300
- console.error(`${tag} · ✓ ${result.test.command} passed after mount.`);
301
- }
302
- break;
303
298
  case 'already':
304
- console.error(`${tag} · ✓ orb already mounted in ${result.file}. Nothing to do.`);
299
+ if (quiet) return;
305
300
  break;
306
301
  case 'declined':
307
302
  console.error(`${tag} · ${dim || ''}did not modify your code. Paste the snippet above to mount the orb.${reset || ''}`);
package/src/writeEnv.js CHANGED
@@ -1,19 +1,10 @@
1
1
  /**
2
- * writeEnv — optional, consent-based convenience that ties the app's RUNTIME
3
- * key/URL to the SAME key/URL the CLI just published with.
2
+ * writeEnv — consent-based API key / URL alignment for the app's environment.
4
3
  *
5
- * The #1 silent-failure cause is a mismatch: you publish with key A against
6
- * production, but your app runs with key B against localhost, so the orb fetches
7
- * its manifest from the wrong place, finds nothing, and renders invisibly. This
8
- * removes the second manual copy of the key by offering to write it into the
9
- * correct env file for the detected framework.
10
- *
11
- * SAFEGUARDS (mirrors the handler-wiring philosophy — never act blindly):
12
- * - Never overwrites silently: if the var exists and DIFFERS, it warns and asks.
13
- * - Always asks for consent (unless --yes), showing the exact file/var/value.
14
- * - Warns to keep the key out of git (suggests .gitignore) for committed files.
15
- * - On unknown framework / no package.json / Angular (no .env convention), it
16
- * does NOT write — it prints manual instructions instead.
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.
17
8
  */
18
9
  'use strict';
19
10
 
@@ -30,12 +21,7 @@ function readJsonSafe(file) {
30
21
  }
31
22
 
32
23
  /**
33
- * Decide which env files + variable names the detected framework uses. Returns
34
- * `file: null` when there is no safe .env convention to write (Angular,
35
- * unknown). `candidates` is the list of env files the framework loads, ordered
36
- * HIGHEST precedence first — so we can fix the value WHERE THE APP ACTUALLY
37
- * READS IT instead of writing a second file that may or may not win. `file` is
38
- * the default file to create when no candidate already defines the vars.
24
+ * Decide which env files + variable names the detected framework uses.
39
25
  */
40
26
  function resolveEnvPlan(appRoot) {
41
27
  const pkg = readJsonSafe(path.join(appRoot, 'package.json'));
@@ -46,8 +32,6 @@ function resolveEnvPlan(appRoot) {
46
32
  const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
47
33
 
48
34
  if (deps.next) {
49
- // Next dev precedence (first wins): .env.development.local > .env.local >
50
- // .env.development > .env.
51
35
  return {
52
36
  framework: 'next',
53
37
  file: '.env.local',
@@ -57,8 +41,6 @@ function resolveEnvPlan(appRoot) {
57
41
  };
58
42
  }
59
43
  if (deps['@angular/core'] || has('angular.json')) {
60
- // Angular has no .env convention (values live in environment.ts), so we
61
- // never write a file blindly — the caller prints manual guidance instead.
62
44
  return { framework: 'angular', file: null, candidates: [], keyVar: null, urlVar: null };
63
45
  }
64
46
  if (
@@ -70,8 +52,6 @@ function resolveEnvPlan(appRoot) {
70
52
  deps['@vitejs/plugin-react'] ||
71
53
  deps['react-scripts']
72
54
  ) {
73
- // Vite dev precedence (first wins): .env.development.local >
74
- // .env.development > .env.local > .env.
75
55
  return {
76
56
  framework: 'vite',
77
57
  file: '.env',
@@ -83,11 +63,6 @@ function resolveEnvPlan(appRoot) {
83
63
  return { framework: null, file: null, candidates: [], keyVar: null, urlVar: null };
84
64
  }
85
65
 
86
- /**
87
- * Scan the framework's candidate env files (highest precedence first) for VAR.
88
- * Returns { file, value } for the first (highest-precedence) file that defines
89
- * it — i.e. the value the running app actually reads — or null if none do.
90
- */
91
66
  function findEffectiveVar(appRoot, candidates, name) {
92
67
  for (const rel of candidates) {
93
68
  const filePath = path.join(appRoot, rel);
@@ -99,7 +74,6 @@ function findEffectiveVar(appRoot, candidates, name) {
99
74
  return null;
100
75
  }
101
76
 
102
- /** Find the value of VAR in the parsed lines, or undefined if not present. */
103
77
  function readEnvVar(lines, name) {
104
78
  const re = new RegExp(`^\\s*${name}\\s*=(.*)$`);
105
79
  for (const line of lines) {
@@ -113,7 +87,6 @@ function stripQuotes(v) {
113
87
  return v.replace(/^['"]/, '').replace(/['"]$/, '');
114
88
  }
115
89
 
116
- /** Upsert VAR=value: replace the existing line, or append a new one. */
117
90
  function upsertEnvVar(lines, name, value) {
118
91
  const re = new RegExp(`^\\s*${name}\\s*=`);
119
92
  const idx = lines.findIndex((l) => re.test(l));
@@ -159,118 +132,162 @@ function isGitIgnored(appRoot, fileName) {
159
132
  }
160
133
 
161
134
  /**
162
- * Offer to write key/URL to the app's env file. `log` is the branded logUser.
163
- * Returns a short status string for the final report.
135
+ * Compute where the key/URL would go and what would change no writes, no prompts.
136
+ * @returns {object|null} null when there is nothing to do (already aligned / no key).
164
137
  */
165
- async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes, log }) {
166
- if (!apiKey) return 'skipped (no API key)';
138
+ function computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl) {
139
+ if (!apiKey) return null;
167
140
 
168
141
  const plan = resolveEnvPlan(appRoot);
169
- if (!plan.file) {
170
- if (plan.framework === 'angular') {
171
- log(
172
- 'Angular does not use .env: put these values in src/environments/environment.ts → ' +
173
- `fyodosApiKey: '${maskKey(apiKey)}', fyodosApiUrl: '${apiUrl}'`,
174
- );
175
- return 'manual (angular)';
176
- }
177
- log(
178
- 'Could not identify the framework with confidence; add by hand to your .env: ' +
179
- `(NEXT_PUBLIC_/VITE_)FYODOS_API_KEY=${maskKey(apiKey)} and the API URL ${apiUrl}`,
180
- );
181
- return 'manual (unknown framework)';
142
+ if (!plan.file && plan.framework !== 'angular') {
143
+ return {
144
+ kind: 'manual-unknown',
145
+ framework: plan.framework,
146
+ keyVar: 'NEXT_PUBLIC_FYODOS_API_KEY or VITE_FYODOS_API_KEY',
147
+ urlVar: 'NEXT_PUBLIC_FYODOS_API_URL or VITE_FYODOS_API_URL',
148
+ apiKey,
149
+ apiUrl,
150
+ };
151
+ }
152
+ if (plan.framework === 'angular') {
153
+ return {
154
+ kind: 'manual-angular',
155
+ file: 'src/environments/environment.ts',
156
+ apiKey,
157
+ apiUrl,
158
+ };
182
159
  }
183
160
 
184
- // Look across ALL the files the framework loads (not just the default one):
185
- // the app may already keep its Fiodos vars in .env while we would otherwise
186
- // create a separate .env.local — leaving the stale value the orb actually
187
- // reads untouched. Fix the value WHERE THE APP READS IT.
188
161
  const candidates = plan.candidates && plan.candidates.length ? plan.candidates : [plan.file];
189
162
  const effectiveKey = findEffectiveVar(appRoot, candidates, plan.keyVar);
190
163
  const effectiveUrl = findEffectiveVar(appRoot, candidates, plan.urlVar);
191
164
  const currentKey = effectiveKey ? effectiveKey.value : undefined;
192
165
  const currentUrl = effectiveUrl ? effectiveUrl.value : undefined;
193
-
194
- // Write in the highest-precedence file that already defines either var (so the
195
- // fix is the value the app loads). If none do, create the framework default.
196
166
  const targetRel = (effectiveKey && effectiveKey.file) || (effectiveUrl && effectiveUrl.file) || plan.file;
197
- const filePath = path.join(appRoot, targetRel);
198
- const exists = fs.existsSync(filePath);
199
- const raw = exists ? fs.readFileSync(filePath, 'utf8') : '';
200
- const lines = raw.length ? raw.replace(/\n$/, '').split('\n') : [];
201
-
202
- // Only write a URL var when it differs from the public default (the SDK
203
- // already defaults to production). But if a STALE url var exists and differs
204
- // from what we published against, we must offer to fix it — that is exactly
205
- // the localhost-vs-production mismatch that hides the orb.
206
167
  const wantUrl = apiUrl && apiUrl !== defaultApiUrl ? apiUrl : null;
207
168
  const urlNeedsFix = currentUrl !== undefined && currentUrl !== apiUrl;
208
-
209
169
  const keyMatches = currentKey === apiKey;
210
170
  const urlOk = !urlNeedsFix && (wantUrl ? currentUrl === wantUrl : true);
171
+
211
172
  if (keyMatches && urlOk) {
212
- log(`✓ Your ${targetRel} already uses the same key/URL as the publication. Nothing to change.`);
213
- return 'already aligned';
173
+ return { kind: 'aligned', targetRel, plan };
214
174
  }
215
175
 
216
- // Build the change description.
217
- const changes = [];
218
- if (!keyMatches) {
219
- changes.push(
220
- currentKey === undefined
221
- ? `add ${plan.keyVar}=${maskKey(apiKey)}`
222
- : `update ${plan.keyVar} (was ${maskKey(currentKey)}) → ${maskKey(apiKey)}`,
176
+ const envLines = [`${plan.keyVar}=${apiKey}`];
177
+ if (wantUrl) envLines.push(`${plan.urlVar}=${wantUrl}`);
178
+ else if (urlNeedsFix) envLines.push(`${plan.urlVar}=${apiUrl}`);
179
+
180
+ return {
181
+ kind: 'writable',
182
+ plan,
183
+ targetRel,
184
+ absPath: path.join(appRoot, targetRel),
185
+ envLines,
186
+ keyMatches,
187
+ urlOk,
188
+ wantUrl,
189
+ urlNeedsFix,
190
+ currentKey,
191
+ currentUrl,
192
+ apiKey,
193
+ apiUrl,
194
+ };
195
+ }
196
+
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;
200
+
201
+ logUser('Add your API key to your environment file (last step)');
202
+
203
+ if (writePlan.kind === 'manual-angular') {
204
+ console.log(
205
+ ` Angular: open ${writePlan.file} and set fyodosApiKey / fyodosApiUrl:`,
223
206
  );
207
+ console.log(` fyodosApiKey: '${writePlan.apiKey}'`);
208
+ console.log(` fyodosApiUrl: '${writePlan.apiUrl}'`);
209
+ return;
224
210
  }
225
- if (wantUrl && currentUrl !== wantUrl) {
226
- changes.push(
227
- currentUrl === undefined
228
- ? `add ${plan.urlVar}=${wantUrl}`
229
- : `update ${plan.urlVar} (was ${currentUrl}) → ${wantUrl}`,
230
- );
231
- } else if (urlNeedsFix && !wantUrl) {
232
- // The app points somewhere (e.g. localhost) but we published against the
233
- // public default; the var is misleading. Offer to align it explicitly.
234
- changes.push(`update ${plan.urlVar} (was ${currentUrl}) → ${apiUrl}`);
211
+
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}`);
217
+ }
218
+ return;
219
+ }
220
+
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}`);
235
227
  }
228
+ logDev(`[write-env] manual reminder for ${rel}`);
229
+ }
236
230
 
237
- if (!changes.length) {
238
- log(`✓ Your ${targetRel} is already aligned.`);
239
- return 'already aligned';
231
+ /**
232
+ * Ask consent and optionally write. Returns { status, writePlan }.
233
+ * `assumeYes` comes from --write-env-yes only (NOT global --yes).
234
+ */
235
+ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes, log, logDev: devLog }) {
236
+ const writePlan = computeEnvWritePlan(appRoot, apiKey, apiUrl, defaultApiUrl);
237
+ if (!writePlan) return { status: 'skipped (no API key)', writePlan: null };
238
+ if (writePlan.kind === 'aligned') {
239
+ log(`Your environment already uses the same API key as this project. Nothing to change.`);
240
+ return { status: 'already aligned', writePlan };
240
241
  }
241
242
 
242
- // Loud warning when we are about to CHANGE an existing, different value.
243
- const isOverwrite = (currentKey !== undefined && !keyMatches) || urlNeedsFix;
244
- log(
245
- `${isOverwrite ? '⚠︎ ' : ''}For the orb to find its manifest, your app should use the SAME ` +
246
- `key/URL you just published with. Proposed in ${targetRel}: ${changes.join('; ')}.`,
247
- );
243
+ const targetLabel =
244
+ writePlan.kind === 'manual-angular'
245
+ ? writePlan.file
246
+ : writePlan.kind === 'manual-unknown'
247
+ ? 'your .env file'
248
+ : writePlan.targetRel;
248
249
 
249
250
  let proceed = assumeYes;
250
251
  if (!proceed) {
251
252
  proceed = await askYesNo(
252
- `◉ Fiodos · Write/update ${targetRel} with the published key/URL? [yes/no] `,
253
+ `◉ Fiodos · Add your API key to ${targetLabel}? [yes/no] `,
253
254
  );
254
255
  }
256
+
255
257
  if (!proceed) {
256
- log(`Left your ${targetRel} untouched. Do it by hand: ${changes.join('; ')}.`);
257
- return 'declined by user';
258
+ return { status: 'declined by user', writePlan };
259
+ }
260
+
261
+ if (writePlan.kind === 'manual-angular' || writePlan.kind === 'manual-unknown') {
262
+ log('Could not write automatically for this framework — see the reminder at the end.');
263
+ return { status: 'manual', writePlan };
258
264
  }
259
265
 
266
+ const { plan, targetRel, keyMatches, wantUrl, urlNeedsFix, apiKey: key, apiUrl: url } = writePlan;
267
+ const filePath = writePlan.absPath;
268
+ const exists = fs.existsSync(filePath);
269
+ const raw = exists ? fs.readFileSync(filePath, 'utf8') : '';
270
+ const lines = raw.length ? raw.replace(/\n$/, '').split('\n') : [];
271
+
260
272
  let out = lines.slice();
261
- if (!keyMatches) out = upsertEnvVar(out, plan.keyVar, apiKey);
273
+ if (!keyMatches) out = upsertEnvVar(out, plan.keyVar, key);
262
274
  if (wantUrl) {
263
275
  out = upsertEnvVar(out, plan.urlVar, wantUrl);
264
276
  } else if (urlNeedsFix) {
265
- out = upsertEnvVar(out, plan.urlVar, apiUrl);
277
+ out = upsertEnvVar(out, plan.urlVar, url);
266
278
  }
267
279
  fs.writeFileSync(filePath, `${out.join('\n')}\n`);
268
- log(`✓ ${targetRel} updated (${changes.join('; ')}).`);
280
+ log(`API key saved to ${targetRel}.`);
269
281
 
270
282
  if (!isGitIgnored(appRoot, targetRel)) {
271
- log(`Reminder: add ${targetRel} to .gitignore so you don't commit your API key.`);
283
+ devLog(`Reminder: add ${targetRel} to .gitignore so you don't commit your API key.`);
272
284
  }
273
- return 'written';
285
+ return { status: 'written', writePlan };
274
286
  }
275
287
 
276
- module.exports = { offerWriteEnv, resolveEnvPlan };
288
+ module.exports = {
289
+ offerWriteEnv,
290
+ resolveEnvPlan,
291
+ computeEnvWritePlan,
292
+ printEnvManualReminder,
293
+ };