@fiodos/cli 0.1.3 → 0.1.4

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 +65 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiodos/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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
@@ -30,24 +30,36 @@ function readJsonSafe(file) {
30
30
  }
31
31
 
32
32
  /**
33
- * Decide which env file + variable names the detected framework uses. Returns
34
- * null when there is no safe .env convention to write (Angular, or unknown).
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.
35
39
  */
36
40
  function resolveEnvPlan(appRoot) {
37
41
  const pkg = readJsonSafe(path.join(appRoot, 'package.json'));
38
42
  const has = (rel) => fs.existsSync(path.join(appRoot, rel));
39
43
  if (!pkg && !has('angular.json')) {
40
- return { framework: null, file: null, keyVar: null, urlVar: null };
44
+ return { framework: null, file: null, candidates: [], keyVar: null, urlVar: null };
41
45
  }
42
46
  const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
43
47
 
44
48
  if (deps.next) {
45
- return { framework: 'next', file: '.env.local', keyVar: 'NEXT_PUBLIC_FYODOS_API_KEY', urlVar: 'NEXT_PUBLIC_FYODOS_API_URL' };
49
+ // Next dev precedence (first wins): .env.development.local > .env.local >
50
+ // .env.development > .env.
51
+ return {
52
+ framework: 'next',
53
+ file: '.env.local',
54
+ candidates: ['.env.development.local', '.env.local', '.env.development', '.env'],
55
+ keyVar: 'NEXT_PUBLIC_FYODOS_API_KEY',
56
+ urlVar: 'NEXT_PUBLIC_FYODOS_API_URL',
57
+ };
46
58
  }
47
59
  if (deps['@angular/core'] || has('angular.json')) {
48
60
  // Angular has no .env convention (values live in environment.ts), so we
49
61
  // never write a file blindly — the caller prints manual guidance instead.
50
- return { framework: 'angular', file: null, keyVar: null, urlVar: null };
62
+ return { framework: 'angular', file: null, candidates: [], keyVar: null, urlVar: null };
51
63
  }
52
64
  if (
53
65
  deps['@sveltejs/kit'] ||
@@ -58,9 +70,33 @@ function resolveEnvPlan(appRoot) {
58
70
  deps['@vitejs/plugin-react'] ||
59
71
  deps['react-scripts']
60
72
  ) {
61
- return { framework: 'vite', file: '.env', keyVar: 'VITE_FYODOS_API_KEY', urlVar: 'VITE_FYODOS_API_URL' };
73
+ // Vite dev precedence (first wins): .env.development.local >
74
+ // .env.development > .env.local > .env.
75
+ return {
76
+ framework: 'vite',
77
+ file: '.env',
78
+ candidates: ['.env.development.local', '.env.development', '.env.local', '.env'],
79
+ keyVar: 'VITE_FYODOS_API_KEY',
80
+ urlVar: 'VITE_FYODOS_API_URL',
81
+ };
62
82
  }
63
- return { framework: null, file: null, keyVar: null, urlVar: null };
83
+ return { framework: null, file: null, candidates: [], keyVar: null, urlVar: null };
84
+ }
85
+
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
+ function findEffectiveVar(appRoot, candidates, name) {
92
+ for (const rel of candidates) {
93
+ const filePath = path.join(appRoot, rel);
94
+ if (!fs.existsSync(filePath)) continue;
95
+ const lines = fs.readFileSync(filePath, 'utf8').replace(/\n$/, '').split('\n');
96
+ const value = readEnvVar(lines, name);
97
+ if (value !== undefined) return { file: rel, value };
98
+ }
99
+ return null;
64
100
  }
65
101
 
66
102
  /** Find the value of VAR in the parsed lines, or undefined if not present. */
@@ -145,14 +181,24 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
145
181
  return 'manual (unknown framework)';
146
182
  }
147
183
 
148
- const filePath = path.join(appRoot, plan.file);
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
+ const candidates = plan.candidates && plan.candidates.length ? plan.candidates : [plan.file];
189
+ const effectiveKey = findEffectiveVar(appRoot, candidates, plan.keyVar);
190
+ const effectiveUrl = findEffectiveVar(appRoot, candidates, plan.urlVar);
191
+ const currentKey = effectiveKey ? effectiveKey.value : undefined;
192
+ 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
+ const targetRel = (effectiveKey && effectiveKey.file) || (effectiveUrl && effectiveUrl.file) || plan.file;
197
+ const filePath = path.join(appRoot, targetRel);
149
198
  const exists = fs.existsSync(filePath);
150
199
  const raw = exists ? fs.readFileSync(filePath, 'utf8') : '';
151
200
  const lines = raw.length ? raw.replace(/\n$/, '').split('\n') : [];
152
201
 
153
- const currentKey = readEnvVar(lines, plan.keyVar);
154
- const currentUrl = readEnvVar(lines, plan.urlVar);
155
-
156
202
  // Only write a URL var when it differs from the public default (the SDK
157
203
  // already defaults to production). But if a STALE url var exists and differs
158
204
  // from what we published against, we must offer to fix it — that is exactly
@@ -163,7 +209,7 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
163
209
  const keyMatches = currentKey === apiKey;
164
210
  const urlOk = !urlNeedsFix && (wantUrl ? currentUrl === wantUrl : true);
165
211
  if (keyMatches && urlOk) {
166
- log(`✓ Your ${plan.file} already uses the same key/URL as the publication. Nothing to change.`);
212
+ log(`✓ Your ${targetRel} already uses the same key/URL as the publication. Nothing to change.`);
167
213
  return 'already aligned';
168
214
  }
169
215
 
@@ -189,7 +235,7 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
189
235
  }
190
236
 
191
237
  if (!changes.length) {
192
- log(`✓ Your ${plan.file} is already aligned.`);
238
+ log(`✓ Your ${targetRel} is already aligned.`);
193
239
  return 'already aligned';
194
240
  }
195
241
 
@@ -197,17 +243,17 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
197
243
  const isOverwrite = (currentKey !== undefined && !keyMatches) || urlNeedsFix;
198
244
  log(
199
245
  `${isOverwrite ? '⚠︎ ' : ''}For the orb to find its manifest, your app should use the SAME ` +
200
- `key/URL you just published with. Proposed in ${plan.file}: ${changes.join('; ')}.`,
246
+ `key/URL you just published with. Proposed in ${targetRel}: ${changes.join('; ')}.`,
201
247
  );
202
248
 
203
249
  let proceed = assumeYes;
204
250
  if (!proceed) {
205
251
  proceed = await askYesNo(
206
- `◉ Fiodos · Write/update ${plan.file} with the published key/URL? [yes/no] `,
252
+ `◉ Fiodos · Write/update ${targetRel} with the published key/URL? [yes/no] `,
207
253
  );
208
254
  }
209
255
  if (!proceed) {
210
- log(`Left your ${plan.file} untouched. Do it by hand: ${changes.join('; ')}.`);
256
+ log(`Left your ${targetRel} untouched. Do it by hand: ${changes.join('; ')}.`);
211
257
  return 'declined by user';
212
258
  }
213
259
 
@@ -219,10 +265,10 @@ async function offerWriteEnv({ appRoot, apiKey, apiUrl, defaultApiUrl, assumeYes
219
265
  out = upsertEnvVar(out, plan.urlVar, apiUrl);
220
266
  }
221
267
  fs.writeFileSync(filePath, `${out.join('\n')}\n`);
222
- log(`✓ ${plan.file} updated (${changes.join('; ')}).`);
268
+ log(`✓ ${targetRel} updated (${changes.join('; ')}).`);
223
269
 
224
- if (!isGitIgnored(appRoot, plan.file)) {
225
- log(`Reminder: add ${plan.file} to .gitignore so you don't commit your API key.`);
270
+ if (!isGitIgnored(appRoot, targetRel)) {
271
+ log(`Reminder: add ${targetRel} to .gitignore so you don't commit your API key.`);
226
272
  }
227
273
  return 'written';
228
274
  }