@fiodos/cli 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -43,6 +43,23 @@ npx @fiodos/cli analyze . --publish --api-key fyd_xxx --no-llm
43
43
  npx @fiodos/cli analyze . --publish --api-key fyd_xxx --api-url http://localhost:8000
44
44
  ```
45
45
 
46
+ ### Tras publicar: que el orbe NO falle en silencio
47
+
48
+ El error más común es publicar con una key/URL y correr la app con OTRA: el orbe
49
+ pide su manifest al sitio equivocado, no lo encuentra y se renderiza invisible.
50
+ Para evitarlo, tras `--publish` el CLI:
51
+
52
+ 1. **Re-lee el manifest** con la MISMA key+URL con la que publicó y confirma que
53
+ el orbe lo encontrará, imprimiendo la key+URL exactas que tu app debe usar.
54
+ 2. En web, **ofrece escribir esa key+URL en tu `.env`** (`.env.local` en Next,
55
+ `.env` en Vite) para que runtime y publicación coincidan. Nunca sobrescribe
56
+ un valor distinto en silencio: avisa y pregunta. Desactívalo con
57
+ `--no-write-env`.
58
+
59
+ Y en **desarrollo**, si aun así el orbe no puede montarse (key rechazada, sin
60
+ manifest, backend inalcanzable), el SDK pinta un aviso visible con el motivo
61
+ exacto donde iría el orbe (en producción sigue invisible).
62
+
46
63
  > Alternativa SIN IA — instalación manual: el flujo base de cualquier SDK
47
64
  > siempre funciona y no requiere IA ni clave de proveedor:
48
65
  > `npm install @fiodos/react @fiodos/core` y montar `<FiodosAgent/>` (el
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiodos/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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
@@ -31,8 +31,18 @@
31
31
  * After analysis, a web target is OFFERED automatic wiring of <FiodosAgent/>
32
32
  * into the app's root layout. It asks before editing your code; on "no" (or a
33
33
  * non-interactive shell) it prints the ready-to-paste snippet instead.
34
- * --yes accept the wiring without the prompt (CI / express installer)
35
- * --no-wire skip wiring entirely (analysis/publish unaffected)
34
+ * --yes accept the wiring/env without the prompt (CI / express)
35
+ * --no-wire skip wiring entirely (analysis/publish unaffected)
36
+ * --no-write-env skip the offer to align the app's .env with the published
37
+ * key/URL (see below)
38
+ *
39
+ * Avoiding the silent "orb fetches the wrong backend" failure:
40
+ * After --publish the CLI (1) re-fetches the manifest with the SAME key+URL to
41
+ * confirm the orb will find it, printing the exact key/URL your app must use,
42
+ * and (2) on web, OFFERS to write that key/URL into the app's env file
43
+ * (.env.local for Next, .env for Vite) so the runtime matches what you
44
+ * published. It never overwrites a differing value silently — it warns and
45
+ * asks first. Disable with --no-write-env.
36
46
  *
37
47
  * AI key — HYBRID model:
38
48
  * DEFAULT (hosted): the AI analysis runs on the Fiodos backend with FIODOS'S
@@ -82,6 +92,7 @@ const { analyzeWithAI, correctActionWiring, DEFAULT_MODEL, resolveModel } = requ
82
92
  const { verifyManifest } = require('./verify');
83
93
  const { wireWebOrb, reportWireResult } = require('./wireWeb');
84
94
  const { wireHandlers, reportHandlerResult } = require('./wireHandlers');
95
+ const { offerWriteEnv } = require('./writeEnv');
85
96
 
86
97
  function arg(flag, fallback) {
87
98
  const i = process.argv.indexOf(flag);
@@ -129,7 +140,7 @@ function fyodosTermColors() {
129
140
 
130
141
  /** Internal diagnostics (tokens, cost, paths…) — only with --verbose. */
131
142
  function isVerbose() {
132
- return process.argv.includes('--verbose') || process.argv.includes('-v');
143
+ return process.argv.includes('--verbose') || process.argv.includes('-v') || process.argv.includes('--debug');
133
144
  }
134
145
 
135
146
  function logDev(...args) {
@@ -356,9 +367,27 @@ async function main() {
356
367
  // their app, so the onboarding never needs a separate manual step. Mobile is
357
368
  // wired by its own installer and is intentionally left untouched here.
358
369
  if (platform === 'web') {
359
- const { apiUrl } = resolveApiTarget();
370
+ const { apiKey, apiUrl } = resolveApiTarget();
360
371
  const assumeYes = process.argv.includes('--yes') || process.argv.includes('--wire-yes');
361
372
  const noWire = process.argv.includes('--no-wire');
373
+
374
+ // ── 5b. Offer to align the app's .env with the published key/URL (consent).
375
+ // Removes the second manual copy of the key — the root cause of the silent
376
+ // "orb fetches the wrong backend" failure. Skipped with --no-write-env.
377
+ if (process.argv.includes('--publish') && !process.argv.includes('--no-write-env')) {
378
+ try {
379
+ await offerWriteEnv({
380
+ appRoot: path.resolve(appRoot),
381
+ apiKey,
382
+ apiUrl,
383
+ defaultApiUrl: DEFAULT_API_URL,
384
+ assumeYes,
385
+ log: logUser,
386
+ });
387
+ } catch (e) {
388
+ logDev(`[write-env] skipped: ${e.message}`);
389
+ }
390
+ }
362
391
  // --no-orb-wire skips ONLY the orb mount (useful to test handler wiring in
363
392
  // isolation without modifying the app's entrypoint / package.json).
364
393
  if (!process.argv.includes('--no-orb-wire')) {
@@ -658,9 +687,57 @@ async function publishManifest(manifest, meta, analysisToken = null) {
658
687
  const body = await res.json();
659
688
  logUser(`Published to your project — ${body.routes} routes, ${body.actions} actions`);
660
689
  logDev(`[publish] OK — ${body.routes} routes, ${body.actions} actions received at ${body.receivedAt}`);
690
+
691
+ // ── Self-check: confirm the orb will actually FIND this manifest ───────────
692
+ // Re-fetch it the way the SDK does at runtime (GET /v1/client/manifest with
693
+ // the SAME key + URL). This catches the silent-failure setup BEFORE the
694
+ // developer is left wondering why the orb never appears, and prints the exact
695
+ // key/URL their app must use so they can compare with their .env.
696
+ await verifyOrbCanFetch({ apiKey, apiUrl });
697
+
661
698
  logUser('Open the dashboard → Agent settings → "Reload analysis" to review');
662
699
  }
663
700
 
701
+ /**
702
+ * Verify the just-published manifest is retrievable through the public client
703
+ * endpoint with the same credentials, and tell the developer EXACTLY which key
704
+ * and URL their running app must use. Never hard-fails the publish (the publish
705
+ * already succeeded); a failed re-fetch is surfaced as an actionable warning.
706
+ */
707
+ async function verifyOrbCanFetch({ apiKey, apiUrl }) {
708
+ try {
709
+ const res = await fetch(`${apiUrl}/v1/client/manifest`, {
710
+ method: 'GET',
711
+ headers: { 'x-api-key': apiKey },
712
+ });
713
+ if (res.ok) {
714
+ const data = await res.json().catch(() => null);
715
+ if (data && data.manifest) {
716
+ logUser(
717
+ `✓ Verified: your orb will find this manifest using API key ${apiKey.slice(0, 12)}… ` +
718
+ `against ${apiUrl}.`,
719
+ );
720
+ logUser(
721
+ 'Make sure your app uses this SAME key and URL in its .env ' +
722
+ '(NEXT_PUBLIC_FYODOS_API_KEY / VITE_FYODOS_API_KEY, and the API URL).',
723
+ );
724
+ return true;
725
+ }
726
+ }
727
+ logUser(
728
+ `⚠︎ Published, but could not re-read the manifest from ${apiUrl} (HTTP ${res.status}). ` +
729
+ 'Check that your app uses this same key and URL.',
730
+ );
731
+ } catch (e) {
732
+ logDev(`[verify] re-fetch failed: ${e.message}`);
733
+ logUser(
734
+ `⚠︎ Published, but could not reach ${apiUrl} to verify. ` +
735
+ 'Confirm your app points to this same URL and key.',
736
+ );
737
+ }
738
+ return false;
739
+ }
740
+
664
741
  main().catch((e) => {
665
742
  console.error(e);
666
743
  process.exit(1);
@@ -61,13 +61,13 @@ async function runPostWireTest(appRoot, opts = {}) {
61
61
  ok: true,
62
62
  stage: 'skipped-no-deps',
63
63
  command: '(none)',
64
- output: 'node_modules ausente: no se pudo ejecutar el build (instala dependencias para verificarlo).',
64
+ output: 'node_modules missing: could not run the build (install dependencies to verify it).',
65
65
  };
66
66
  }
67
67
  const pkg = readPkg(appRoot);
68
68
  const chosen = chooseCommand(appRoot, pkg);
69
69
  if (!chosen) {
70
- return { ok: true, stage: 'skipped-no-deps', command: '(none)', output: 'no hay script de build/typecheck.' };
70
+ return { ok: true, stage: 'skipped-no-deps', command: '(none)', output: 'no build/typecheck script.' };
71
71
  }
72
72
 
73
73
  const res = spawnSync('npm', chosen.args, {
@@ -79,7 +79,7 @@ async function runPostWireTest(appRoot, opts = {}) {
79
79
  });
80
80
 
81
81
  if (res.error && res.error.code === 'ETIMEDOUT') {
82
- return { ok: false, stage: 'timeout', command: chosen.label, output: `build excedió ${Math.round(timeoutMs / 1000)}s` };
82
+ return { ok: false, stage: 'timeout', command: chosen.label, output: `build exceeded ${Math.round(timeoutMs / 1000)}s` };
83
83
  }
84
84
  if (res.error) {
85
85
  return { ok: false, stage: 'error', command: chosen.label, output: String(res.error.message || res.error) };
@@ -43,6 +43,39 @@ function loadJsdom() {
43
43
  try { return require('jsdom'); } catch { return null; }
44
44
  }
45
45
 
46
+ /** Detailed technical output (jsdom/React internals) only when asked. */
47
+ function isVerboseProbe() {
48
+ return process.argv.includes('--verbose') || process.argv.includes('-v') || process.argv.includes('--debug');
49
+ }
50
+
51
+ /**
52
+ * Capture (swallow) noisy global console output during a probe — React error
53
+ * boundary logs ("The above error occurred in <X>", "Consider adding an error
54
+ * boundary…"), uncaught-error dumps, etc. These come from rendering the user's
55
+ * real pages in a bare jsdom (no router/providers) and are internal noise, not
56
+ * actionable user information. The captured lines are returned for diagnostics.
57
+ *
58
+ * No-op under --verbose/--debug so power users can still see the raw detail.
59
+ */
60
+ function captureConsole() {
61
+ if (isVerboseProbe()) return { captured: [], restore() {} };
62
+ const captured = [];
63
+ const saved = {
64
+ log: console.log, error: console.error, warn: console.warn, info: console.info, debug: console.debug,
65
+ };
66
+ const sink = (...a) => {
67
+ try { captured.push(a.map((x) => (typeof x === 'string' ? x : String(x))).join(' ')); } catch { /* ignore */ }
68
+ };
69
+ console.log = sink; console.error = sink; console.warn = sink; console.info = sink; console.debug = sink;
70
+ return {
71
+ captured,
72
+ restore() {
73
+ console.log = saved.log; console.error = saved.error; console.warn = saved.warn;
74
+ console.info = saved.info; console.debug = saved.debug;
75
+ },
76
+ };
77
+ }
78
+
46
79
  /** esbuild plugin that empties style/asset imports (works on old esbuild that
47
80
  * lacks the 'empty' loader). Side-effect CSS imports become no-ops. */
48
81
  function emptyAssetsPlugin() {
@@ -100,7 +133,7 @@ async function probeReactEffect(appRoot, entry, ctx) {
100
133
  const esbuild = loadEsbuild(appRoot);
101
134
  const jsdomMod = loadJsdom();
102
135
  if (!esbuild || !jsdomMod) {
103
- return { status: 'unverifiable', detail: `no se pudo montar el render headless (${!esbuild ? 'esbuild' : 'jsdom'} no disponible); cableado aplicado, efecto real no verificable automáticamenteprobar a mano` };
136
+ return { status: 'unverifiable', detail: `could not set up the headless render (${!esbuild ? 'esbuild' : 'jsdom'} not available); wiring applied, real effect not automatically verifiable test by hand` };
104
137
  }
105
138
  const fyodosDirAbs = path.join(appRoot, ctx.fyodosDirRel);
106
139
  const componentAbs = path.join(appRoot, entry.bridge.file);
@@ -118,6 +151,7 @@ async function probeReactEffect(appRoot, entry, ctx) {
118
151
  `export const probe = { React: ReactNS.default || ReactNS, createRoot, flushSync, Comp, registry: fyodosGeneratedRegistries };\n`;
119
152
 
120
153
  let restore = () => {};
154
+ let cap = { restore() {} };
121
155
  try {
122
156
  fs.writeFileSync(harnessAbs, harness);
123
157
  await esbuild.build({
@@ -134,12 +168,14 @@ async function probeReactEffect(appRoot, entry, ctx) {
134
168
  absWorkingDir: fyodosDirAbs,
135
169
  });
136
170
 
171
+ // Silence React/jsdom render noise from here through unmount (see captureConsole).
172
+ cap = captureConsole();
137
173
  restore = installDom(jsdomMod, { storageKeys: ctx.storageKeys, kind: ctx.kind });
138
174
  delete require.cache[outAbs];
139
175
  const mod = require(outAbs);
140
176
  const { React, createRoot, flushSync, Comp, registry } = mod.probe;
141
177
  const Component = pickComponent(Comp);
142
- if (!Component) return { status: 'unverifiable', detail: `no encontré un componente React montable exportado por '${entry.bridge.file}' — efecto real no verificable a mano` };
178
+ if (!Component) return { status: 'unverifiable', detail: `did not find a mountable React component exported by '${entry.bridge.file}' — real effect not verifiable, test by hand` };
143
179
 
144
180
  const container = document.createElement('div');
145
181
  document.body.appendChild(container);
@@ -150,7 +186,7 @@ async function probeReactEffect(appRoot, entry, ctx) {
150
186
  await settle();
151
187
  flushSync(() => {});
152
188
  } catch (err) {
153
- return { status: 'unverifiable', detail: `el componente no se pudo renderizar en aislamiento (${short(err)}); efecto real no verificable automáticamenteprobar a mano` };
189
+ return { status: 'unverifiable', detail: `the component could not be rendered in isolation (${short(err)}); real effect not automatically verifiable test by hand` };
154
190
  }
155
191
 
156
192
  const beforeHTML = captureSnapshot(container);
@@ -162,22 +198,23 @@ async function probeReactEffect(appRoot, entry, ctx) {
162
198
  // Do NOT fire the effect; we proved it mounts and the handler resolves to a
163
199
  // live bridge function. Verify the bridge has the method, nothing more.
164
200
  try { root.unmount(); } catch {}
165
- return { status: 'unverifiable', detail: 'acción con confirmación: no se dispara su efecto en el render de prueba (seguridad); cableado verificado hasta el punto de confirmación' };
201
+ return { status: 'unverifiable', detail: 'confirmation action: its effect is not fired in the test render (security); wiring verified up to the confirmation point' };
166
202
  }
167
203
 
168
204
  let invokeErr = null;
169
205
  try { await handler(ctx.params); } catch (e) { invokeErr = e; }
170
206
  await settle();
171
207
  try { flushSync(() => {}); } catch {}
172
- if (invokeErr) { try { root.unmount(); } catch {} return { status: 'fail', detail: `al invocar el handler la app real lanzó: ${short(invokeErr)}` }; }
208
+ if (invokeErr) { try { root.unmount(); } catch {} return { status: 'fail', detail: `invoking the handler, the real app threw: ${short(invokeErr)}` }; }
173
209
 
174
210
  const afterHTML = captureSnapshot(container);
175
211
  const afterText = container.textContent || '';
176
212
  try { root.unmount(); } catch {}
177
213
  return decide(ctx.kind, { beforeHTML, beforeText, afterHTML, afterText });
178
214
  } catch (err) {
179
- return { status: 'unverifiable', detail: `no se pudo preparar el render de prueba (${short(err)}); efecto real no verificable automáticamenteprobar a mano` };
215
+ return { status: 'unverifiable', detail: `could not prepare the test render (${short(err)}); real effect not automatically verifiable test by hand` };
180
216
  } finally {
217
+ try { cap.restore(); } catch {}
181
218
  try { fs.existsSync(harnessAbs) && fs.rmSync(harnessAbs); } catch {}
182
219
  try { fs.existsSync(outAbs) && fs.rmSync(outAbs); } catch {}
183
220
  try { restore(); } catch {}
@@ -212,22 +249,22 @@ function decide(kind, snap) {
212
249
  const sentinelDisappeared = beforeText.includes(SENTINEL) && !afterText.includes(SENTINEL);
213
250
 
214
251
  if (kind === 'add') {
215
- if (sentinelAppeared) return { status: 'effect-pass', detail: `tras invocar la acción, "${SENTINEL}" aparece en la UI renderizada (el item se añadió de verdad)` };
216
- if (htmlChanged) return { status: 'effect-pass', detail: 'la UI cambió tras invocar la acción (efecto observado en el render)' };
217
- return { status: 'fail', detail: 'la acción se invocó pero la UI no cambió ni apareció el valor enviado (no produce su efecto)' };
252
+ if (sentinelAppeared) return { status: 'effect-pass', detail: `after invoking the action, "${SENTINEL}" appears in the rendered UI (the item was really added)` };
253
+ if (htmlChanged) return { status: 'effect-pass', detail: 'the UI changed after invoking the action (effect observed in the render)' };
254
+ return { status: 'fail', detail: 'the action was invoked but the UI did not change nor did the sent value appear (it produces no effect)' };
218
255
  }
219
256
  if (kind === 'delete') {
220
- if (sentinelDisappeared) return { status: 'effect-pass', detail: 'el item sembrado desapareció de la UI tras invocar la acción (se borró de verdad)' };
221
- if (htmlChanged) return { status: 'effect-pass', detail: 'la UI cambió tras invocar la acción de borrado' };
222
- return { status: 'unverifiable', detail: 'no se observó cambio al borrar (quizá no había item sembrado en aislamiento); cableado aplicado, efecto no confirmadoprobar a mano' };
257
+ if (sentinelDisappeared) return { status: 'effect-pass', detail: 'the seeded item disappeared from the UI after invoking the action (it was really deleted)' };
258
+ if (htmlChanged) return { status: 'effect-pass', detail: 'the UI changed after invoking the delete action' };
259
+ return { status: 'unverifiable', detail: 'no change observed on delete (maybe there was no seeded item in isolation); wiring applied, effect not confirmedtest by hand' };
223
260
  }
224
261
  if (kind === 'toggle') {
225
- if (htmlChanged) return { status: 'effect-pass', detail: 'la UI cambió tras invocar la acción (estado del item alternado)' };
226
- return { status: 'unverifiable', detail: 'no se observó cambio al alternar (quizá sin item sembrado en aislamiento); cableado aplicado, efecto no confirmadoprobar a mano' };
262
+ if (htmlChanged) return { status: 'effect-pass', detail: 'the UI changed after invoking the action (item state toggled)' };
263
+ return { status: 'unverifiable', detail: 'no change observed on toggle (maybe no seeded item in isolation); wiring applied, effect not confirmedtest by hand' };
227
264
  }
228
265
  // ui / other
229
- if (htmlChanged) return { status: 'effect-pass', detail: 'la UI del componente cambió tras invocar la acción' };
230
- return { status: 'unverifiable', detail: 'la acción no produjo un cambio observable en el render aislado; cableado aplicado, efecto no confirmadoprobar a mano' };
266
+ if (htmlChanged) return { status: 'effect-pass', detail: 'the component UI changed after invoking the action' };
267
+ return { status: 'unverifiable', detail: 'the action produced no observable change in the isolated render; wiring applied, effect not confirmedtest by hand' };
231
268
  }
232
269
 
233
270
  function short(err) {
@@ -240,8 +277,17 @@ function short(err) {
240
277
  * toggle actions have something to act on.
241
278
  */
242
279
  function installDom(jsdomMod, { storageKeys = [], kind } = {}) {
243
- const { JSDOM } = jsdomMod;
244
- const dom = new JSDOM('<!doctype html><html><body></body></html>', { url: 'http://localhost/', pretendToBeVisual: true });
280
+ const { JSDOM, VirtualConsole } = jsdomMod;
281
+ // A VirtualConsole that does NOT forward to the real console: jsdom's default
282
+ // pipes page errors (uncaught exceptions → "reportException", long stack
283
+ // traces) to our stderr. That is internal noise from rendering the user's
284
+ // pages in a bare DOM. We swallow it (or forward only under --verbose).
285
+ let virtualConsole;
286
+ try {
287
+ virtualConsole = new VirtualConsole();
288
+ if (isVerboseProbe()) virtualConsole.sendTo(console);
289
+ } catch { virtualConsole = undefined; }
290
+ const dom = new JSDOM('<!doctype html><html><body></body></html>', { url: 'http://localhost/', pretendToBeVisual: true, virtualConsole });
245
291
  const { window } = dom;
246
292
  // Seed storage BEFORE the component hydrates (only useful for delete/toggle).
247
293
  if (kind === 'delete' || kind === 'toggle') {
@@ -319,15 +365,15 @@ async function probeComponentEffect(appRoot, entry, opts = {}) {
319
365
 
320
366
  const withTimeout = (p) => Promise.race([
321
367
  p,
322
- new Promise((resolve) => setTimeout(() => resolve({ status: 'unverifiable', detail: 'el render de prueba excedió el tiempo límite (posible bucle/timer en el componente); efecto real no verificable automáticamenteprobar a mano' }), opts.timeoutMs || 25000)),
368
+ new Promise((resolve) => setTimeout(() => resolve({ status: 'unverifiable', detail: 'the test render exceeded the time limit (possible loop/timer in the component); real effect not automatically verifiable test by hand' }), opts.timeoutMs || 25000)),
323
369
  ]);
324
370
 
325
371
  if (framework.includes('react')) return withTimeout(probeReactEffect(appRoot, entry, ctx));
326
372
  if (framework.includes('vue')) return withTimeout(require('./renderProbeVue').probeVueEffect(appRoot, entry, ctx));
327
373
  if (framework.includes('angular')) {
328
- return { status: 'unverifiable', detail: 'Angular: el render headless de componentes con TestBed no es fiable fuera de ng test; cableado aplicado y verificado por compilación, efecto real no verificable automáticamente aquíprobar con `ng test` o a mano' };
374
+ return { status: 'unverifiable', detail: 'Angular: headless component rendering with TestBed is not reliable outside ng test; wiring applied and verified by compilation, real effect not automatically verifiable here test with `ng test` or by hand' };
329
375
  }
330
- return { status: 'unverifiable', detail: `framework '${framework}' sin render-probe; efecto real no verificable automáticamente` };
376
+ return { status: 'unverifiable', detail: `framework '${framework}' has no render-probe; real effect not automatically verifiable` };
331
377
  }
332
378
 
333
- module.exports = { probeComponentEffect, actionKind, decide, SENTINEL, SEED_ID, installDom, pickComponent, detectStorageKeys, probeParams, captureSnapshot, short, loadEsbuild, loadJsdom, reqFrom, emptyAssetsPlugin };
379
+ module.exports = { probeComponentEffect, actionKind, decide, SENTINEL, SEED_ID, installDom, pickComponent, detectStorageKeys, probeParams, captureSnapshot, short, loadEsbuild, loadJsdom, reqFrom, emptyAssetsPlugin, captureConsole, isVerboseProbe };
@@ -17,7 +17,7 @@ const os = require('os');
17
17
  const path = require('path');
18
18
  const crypto = require('crypto');
19
19
 
20
- const { decide, installDom, captureSnapshot, short, loadEsbuild, loadJsdom, reqFrom, emptyAssetsPlugin } = require('./renderProbe');
20
+ const { decide, installDom, captureSnapshot, short, loadEsbuild, loadJsdom, reqFrom, emptyAssetsPlugin, captureConsole } = require('./renderProbe');
21
21
 
22
22
  function vuePlugin(compilerSfc) {
23
23
  return {
@@ -52,7 +52,7 @@ async function probeVueEffect(appRoot, entry, ctx) {
52
52
  const compilerSfc = reqFrom(appRoot, '@vue/compiler-sfc');
53
53
  if (!esbuild || !jsdomMod || !compilerSfc) {
54
54
  const miss = !esbuild ? 'esbuild' : !jsdomMod ? 'jsdom' : '@vue/compiler-sfc';
55
- return { status: 'unverifiable', detail: `no se pudo montar el render headless de Vue (${miss} no disponible); cableado aplicado, efecto real no verificable automáticamenteprobar a mano` };
55
+ return { status: 'unverifiable', detail: `could not set up the Vue headless render (${miss} not available); wiring applied, real effect not automatically verifiable test by hand` };
56
56
  }
57
57
  const fyodosDirAbs = path.join(appRoot, ctx.fyodosDirRel);
58
58
  const componentAbs = path.join(appRoot, entry.bridge.file);
@@ -66,6 +66,7 @@ async function probeVueEffect(appRoot, entry, ctx) {
66
66
  `export const probe = { createApp, nextTick, Component, registry: fyodosGeneratedRegistries };\n`;
67
67
 
68
68
  let restore = () => {};
69
+ let cap = { restore() {} };
69
70
  try {
70
71
  fs.writeFileSync(harnessAbs, harness);
71
72
  await esbuild.build({
@@ -86,11 +87,13 @@ async function probeVueEffect(appRoot, entry, ctx) {
86
87
  absWorkingDir: fyodosDirAbs,
87
88
  });
88
89
 
90
+ // Silence Vue/jsdom render noise from here through unmount.
91
+ cap = captureConsole();
89
92
  restore = installDom(jsdomMod, { storageKeys: ctx.storageKeys, kind: ctx.kind });
90
93
  delete require.cache[outAbs];
91
94
  const mod = require(outAbs);
92
95
  const { createApp, nextTick, Component, registry } = mod.probe;
93
- if (!Component) return { status: 'unverifiable', detail: `'${entry.bridge.file}' no exporta un componente Vue montableefecto real no verificable a mano` };
96
+ if (!Component) return { status: 'unverifiable', detail: `'${entry.bridge.file}' does not export a mountable Vue component — real effect not verifiable, test by hand` };
94
97
 
95
98
  const container = document.createElement('div');
96
99
  document.body.appendChild(container);
@@ -102,31 +105,32 @@ async function probeVueEffect(appRoot, entry, ctx) {
102
105
  await nextTick();
103
106
  } catch (err) {
104
107
  try { app && app.unmount(); } catch {}
105
- return { status: 'unverifiable', detail: `el componente Vue no se pudo montar en aislamiento (${short(err)}); efecto real no verificable automáticamenteprobar a mano` };
108
+ return { status: 'unverifiable', detail: `the Vue component could not be mounted in isolation (${short(err)}); real effect not automatically verifiable test by hand` };
106
109
  }
107
110
 
108
111
  const beforeHTML = captureSnapshot(container);
109
112
  const beforeText = container.textContent || '';
110
113
  const handler = registry.handlers[entry.handler];
111
- if (typeof handler !== 'function') { try { app.unmount(); } catch {} return { status: 'fail', detail: `el registro no expone el handler '${entry.handler}'` }; }
114
+ if (typeof handler !== 'function') { try { app.unmount(); } catch {} return { status: 'fail', detail: `the registry does not expose the handler '${entry.handler}'` }; }
112
115
 
113
116
  if (ctx.sensitive) {
114
117
  try { app.unmount(); } catch {}
115
- return { status: 'unverifiable', detail: 'acción con confirmación: no se dispara su efecto en el render de prueba (seguridad); cableado verificado hasta el punto de confirmación' };
118
+ return { status: 'unverifiable', detail: 'confirmation action: its effect is not fired in the test render (security); wiring verified up to the confirmation point' };
116
119
  }
117
120
 
118
121
  let invokeErr = null;
119
122
  try { await handler(ctx.params); } catch (e) { invokeErr = e; }
120
123
  await nextTick();
121
- if (invokeErr) { try { app.unmount(); } catch {} return { status: 'fail', detail: `al invocar el handler la app real lanzó: ${short(invokeErr)}` }; }
124
+ if (invokeErr) { try { app.unmount(); } catch {} return { status: 'fail', detail: `invoking the handler, the real app threw: ${short(invokeErr)}` }; }
122
125
 
123
126
  const afterHTML = captureSnapshot(container);
124
127
  const afterText = container.textContent || '';
125
128
  try { app.unmount(); } catch {}
126
129
  return decide(ctx.kind, { beforeHTML, beforeText, afterHTML, afterText });
127
130
  } catch (err) {
128
- return { status: 'unverifiable', detail: `no se pudo preparar el render de prueba de Vue (${short(err)}); efecto real no verificable automáticamenteprobar a mano` };
131
+ return { status: 'unverifiable', detail: `could not prepare the Vue test render (${short(err)}); real effect not automatically verifiable test by hand` };
129
132
  } finally {
133
+ try { cap.restore(); } catch {}
130
134
  try { fs.existsSync(harnessAbs) && fs.rmSync(harnessAbs); } catch {}
131
135
  try { fs.existsSync(outAbs) && fs.rmSync(outAbs); } catch {}
132
136
  try { restore(); } catch {}
package/src/verifyWire.js CHANGED
@@ -145,11 +145,11 @@ async function probeDexie(appRoot, entry, fyodosDirRel) {
145
145
  const op = dbMatch[3];
146
146
  const grew = after > before;
147
147
  const changed = after !== before || /update|put|delete/.test(op);
148
- if (op === 'delete') return { status: 'effect-pass', detail: `delete ejecutó sobre la tabla '${table}'` };
149
- if (grew || changed) return { status: 'effect-pass', detail: `'${table}' cambió (${before}→${after}) ejecutando la expresión real` };
150
- return { status: 'fail', detail: `la expresión ejecutó pero '${table}' no cambió (${before}→${after})` };
148
+ if (op === 'delete') return { status: 'effect-pass', detail: `delete ran on the table '${table}'` };
149
+ if (grew || changed) return { status: 'effect-pass', detail: `'${table}' changed (${before}→${after}) running the real expression` };
150
+ return { status: 'fail', detail: `the expression ran but '${table}' did not change (${before}→${after})` };
151
151
  } catch (err) {
152
- return { status: 'fail', detail: `la expresión lanzó: ${err && err.message ? err.message : String(err)}` };
152
+ return { status: 'fail', detail: `the expression threw: ${err && err.message ? err.message : String(err)}` };
153
153
  }
154
154
  }
155
155
 
@@ -200,16 +200,16 @@ async function probeEffect(appRoot, entry, opts = {}) {
200
200
  sensitive: opts.sensitive || entry.requireConfirmation,
201
201
  });
202
202
  } catch (err) {
203
- return { status: 'unverifiable', detail: `render-probe no disponible (${err && err.message}); efecto real no verificable automáticamenteprobar a mano` };
203
+ return { status: 'unverifiable', detail: `render-probe not available (${err && err.message}); real effect not automatically verifiable test by hand` };
204
204
  }
205
205
  }
206
206
  if (opts.sensitive || entry.requireConfirmation) {
207
- return { status: 'skipped', detail: 'acción con confirmación: no se dispara su efecto en la simulación (seguridad)' };
207
+ return { status: 'skipped', detail: 'confirmation action: its effect is not fired in the simulation (security)' };
208
208
  }
209
209
  // Module strategy: try the Dexie data-layer probe.
210
210
  const dexie = await probeDexie(appRoot, entry, opts.fyodosDirRel);
211
211
  if (dexie) return dexie;
212
- return { status: 'skipped', detail: 'objetivo de módulo no simulable de forma segura en proceso; verificado por compilación' };
212
+ return { status: 'skipped', detail: 'module target not safely simulable in-process; verified by compilation' };
213
213
  }
214
214
 
215
215
  module.exports = { probeEffect, compileRegressed, extractErrorLines, readDexieStores };