@fiodos/cli 0.1.2 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiodos/cli",
3
- "version": "0.1.2",
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/index.js CHANGED
@@ -396,6 +396,7 @@ async function main() {
396
396
  colors: fyodosTermColors(),
397
397
  assumeYes,
398
398
  noWire,
399
+ runTest: !process.argv.includes('--no-wire-test'),
399
400
  });
400
401
  reportWireResult(result, fyodosTermColors());
401
402
  }
package/src/wireWeb.js CHANGED
@@ -206,7 +206,7 @@ function printMountPreview(plan, colors) {
206
206
  }
207
207
 
208
208
  async function wireWebOrb(appRoot, opts = {}) {
209
- const { colors = {}, assumeYes = false, noWire = false } = opts;
209
+ const { colors = {}, assumeYes = false, noWire = false, runTest = true } = opts;
210
210
  if (noWire) return { status: 'skipped' };
211
211
 
212
212
  const framework = detectFramework(appRoot);
@@ -232,20 +232,53 @@ async function wireWebOrb(appRoot, opts = {}) {
232
232
  return { status: 'declined', file: plan.rel };
233
233
  }
234
234
 
235
+ const build = async () => {
236
+ try {
237
+ return await runPostWireTest(appRoot, { timeoutMs: 180000 });
238
+ } catch (e) {
239
+ return { ok: false, stage: 'runner', command: '(runner)', output: String((e && e.message) || e) };
240
+ }
241
+ };
242
+
235
243
  const backups = backupFiles(plan.files);
236
244
  try {
237
245
  applyMount(plan);
238
246
  if (!verifyMounted(plan)) {
239
- throw new Error('post-edit verification failed — FiodosAgent not found in edited files');
247
+ revertFiles(backups);
248
+ printSnippet(target, framework, colors, appRoot, 'post-edit verification failed — FiodosAgent not found in edited files');
249
+ return { status: 'failed', file: plan.rel };
240
250
  }
241
- const test = await runPostWireTest(appRoot, { timeoutMs: 180000 });
242
- if (!test.ok && test.stage !== 'skipped-no-deps') {
251
+
252
+ // The orb is a single, trivial component mount — it must APPEAR. We therefore
253
+ // only revert when WE are demonstrably the cause: the app built fine on its
254
+ // own but breaks once the orb is mounted (e.g. the SDK package isn't installed
255
+ // yet). A pre-existing build failure, a timeout, or a missing toolchain must
256
+ // NOT make the orb vanish — that was the silent "orb never shows up" bug.
257
+ // Fast path: one build. Only when it fails do we spend a second build (on the
258
+ // reverted original) to tell "we broke it" from "it was already broken".
259
+ if (runTest) {
260
+ const test = await build();
261
+ if (test.ok || test.stage === 'skipped-no-deps') {
262
+ writeConsentDoc(appRoot, plan);
263
+ return { status: 'added', file: plan.rel, test };
264
+ }
265
+ // Build failed with the orb mounted — was the app building WITHOUT it?
243
266
  revertFiles(backups);
244
- printSnippet(target, framework, colors, appRoot, `build/typecheck failed after mount — reverted (${test.command})`);
245
- return { status: 'reverted', file: plan.rel, test, reason: test.output };
267
+ const baseline = await build();
268
+ if (baseline.ok) {
269
+ // Built before, fails now → the mount is the regression. Keep it reverted.
270
+ printSnippet(target, framework, colors, appRoot, `mount caused a build regression — reverted (${test.command})`);
271
+ return { status: 'reverted', file: plan.rel, test, reason: test.output };
272
+ }
273
+ // It was already not building (or not verifiable) before us → keep the orb.
274
+ applyMount(plan);
275
+ verifyMounted(plan);
276
+ writeConsentDoc(appRoot, plan);
277
+ return { status: 'added', file: plan.rel, test, preexistingFailure: true };
246
278
  }
279
+
247
280
  writeConsentDoc(appRoot, plan);
248
- return { status: 'added', file: plan.rel, test };
281
+ return { status: 'added', file: plan.rel };
249
282
  } catch (err) {
250
283
  revertFiles(backups);
251
284
  printSnippet(target, framework, colors, appRoot, err.message || String(err));
@@ -259,7 +292,9 @@ function reportWireResult(result, colors = {}) {
259
292
  switch (result.status) {
260
293
  case 'added':
261
294
  console.error(`${tag} · ✓ orb added to ${result.file}. Start your web app (e.g. \`npm run dev\`) to see it.`);
262
- if (result.test && result.test.stage === 'skipped-no-deps') {
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') {
263
298
  console.error(`${tag} · ${dim || ''}build not verified (no node_modules) — run npm install && npm run build locally.${reset || ''}`);
264
299
  } else if (result.test && result.test.ok) {
265
300
  console.error(`${tag} · ✓ ${result.test.command} passed after mount.`);
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
  }