@fiodos/cli 0.1.11 → 0.1.12
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 +2 -2
- package/src/index.js +19 -4
- package/src/renderProbe.js +69 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"node": ">=18"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@fiodos/core": "^0.1.
|
|
22
|
+
"@fiodos/core": "^0.1.2",
|
|
23
23
|
"esbuild": "^0.28.1",
|
|
24
24
|
"jsdom": "^24.0.0",
|
|
25
25
|
"typescript": "^5.6.0"
|
package/src/index.js
CHANGED
|
@@ -525,7 +525,20 @@ async function main() {
|
|
|
525
525
|
apiKey &&
|
|
526
526
|
(result.status === 'added' || result.status === 'already')
|
|
527
527
|
) {
|
|
528
|
-
await postOrbWired({ apiKey, apiUrl, file: result.file || '' });
|
|
528
|
+
const proof = await postOrbWired({ apiKey, apiUrl, file: result.file || '' });
|
|
529
|
+
// The dashboard's onboarding orb check completes on THIS install proof
|
|
530
|
+
// (no need to run the app). If the backend could not record it (e.g. an
|
|
531
|
+
// outdated deployment without the orb-wired endpoint), say so plainly so
|
|
532
|
+
// the developer knows the check will otherwise wait for a runtime ping.
|
|
533
|
+
if (proof && proof.ok === false && proof.reason !== 'no-api-key') {
|
|
534
|
+
const why = proof.status === 404
|
|
535
|
+
? 'the backend does not expose /v1/developer/orb-wired yet (update the backend deployment)'
|
|
536
|
+
: `the backend rejected the install proof (HTTP ${proof.status || '?'})`;
|
|
537
|
+
logUser(
|
|
538
|
+
`Note: the orb is wired into your app, but ${why}. ` +
|
|
539
|
+
'The dashboard install check will complete once the orb connects from your running app.',
|
|
540
|
+
);
|
|
541
|
+
}
|
|
529
542
|
}
|
|
530
543
|
}
|
|
531
544
|
|
|
@@ -846,7 +859,7 @@ async function publishManifest(manifest, meta, analysisToken = null, opts = {})
|
|
|
846
859
|
* already succeeded); a failed re-fetch is surfaced as an actionable warning.
|
|
847
860
|
*/
|
|
848
861
|
async function postOrbWired({ apiKey, apiUrl, file }) {
|
|
849
|
-
if (!apiKey) return;
|
|
862
|
+
if (!apiKey) return { ok: false, reason: 'no-api-key' };
|
|
850
863
|
try {
|
|
851
864
|
const res = await fetch(`${apiUrl}/v1/developer/orb-wired`, {
|
|
852
865
|
method: 'POST',
|
|
@@ -855,11 +868,13 @@ async function postOrbWired({ apiKey, apiUrl, file }) {
|
|
|
855
868
|
});
|
|
856
869
|
if (res.ok) {
|
|
857
870
|
logDev(`[orb-wired] install proof recorded (${file || 'web'})`);
|
|
858
|
-
|
|
859
|
-
logDev(`[orb-wired] could not record install proof: HTTP ${res.status}`);
|
|
871
|
+
return { ok: true };
|
|
860
872
|
}
|
|
873
|
+
logDev(`[orb-wired] could not record install proof: HTTP ${res.status}`);
|
|
874
|
+
return { ok: false, status: res.status };
|
|
861
875
|
} catch (e) {
|
|
862
876
|
logDev(`[orb-wired] skipped: ${e.message || e}`);
|
|
877
|
+
return { ok: false, reason: e.message || String(e) };
|
|
863
878
|
}
|
|
864
879
|
}
|
|
865
880
|
async function verifyOrbCanFetch({ apiKey, apiUrl }) {
|
package/src/renderProbe.js
CHANGED
|
@@ -48,6 +48,46 @@ function isVerboseProbe() {
|
|
|
48
48
|
return process.argv.includes('--verbose') || process.argv.includes('-v') || process.argv.includes('--debug');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Render-probe teardown guard.
|
|
53
|
+
*
|
|
54
|
+
* The probe mounts the user's real React component in jsdom, then unmounts and
|
|
55
|
+
* tears the DOM down. React 18 schedules passive-effect cleanup ASYNCHRONOUSLY
|
|
56
|
+
* (a scheduler timer/microtask). If any cleanup fires AFTER we removed the
|
|
57
|
+
* jsdom globals, React throws "The `document` global was defined when React was
|
|
58
|
+
* initialized, but is not defined anymore". On Node ≥ 21/25 that async throw is
|
|
59
|
+
* an uncaughtException that kills the ENTIRE CLI — long after the probe returned
|
|
60
|
+
* (e.g. while the CLI is awaiting the ".env" prompt). The probe is an OPTIONAL
|
|
61
|
+
* effect check; it must never crash an otherwise-successful install.
|
|
62
|
+
*
|
|
63
|
+
* We install (once) a narrow process-level filter that swallows ONLY this jsdom/
|
|
64
|
+
* React teardown error class and lets every other error crash as usual.
|
|
65
|
+
*/
|
|
66
|
+
let teardownGuardInstalled = false;
|
|
67
|
+
function isProbeTeardownError(err) {
|
|
68
|
+
const msg = String((err && (err.stack || err.message)) || err || '');
|
|
69
|
+
return (
|
|
70
|
+
/`document` global was defined when React was initialized/.test(msg) ||
|
|
71
|
+
/\bdocument is not defined\b/.test(msg) ||
|
|
72
|
+
/Cannot read properties of (?:null|undefined) \(reading '(?:document|ownerDocument|defaultView|createElement|body)'\)/.test(msg) ||
|
|
73
|
+
(/jsdom/i.test(msg) && /not defined|closed|null/i.test(msg))
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function installProbeTeardownGuard() {
|
|
77
|
+
if (teardownGuardInstalled) return;
|
|
78
|
+
teardownGuardInstalled = true;
|
|
79
|
+
process.on('uncaughtException', (err) => {
|
|
80
|
+
if (isProbeTeardownError(err)) return; // late probe passive-effect cleanup — safe to drop
|
|
81
|
+
// Not ours: preserve Node's default fail-fast behavior for real errors.
|
|
82
|
+
console.error(err && err.stack ? err.stack : err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
85
|
+
process.on('unhandledRejection', (reason) => {
|
|
86
|
+
if (isProbeTeardownError(reason)) return;
|
|
87
|
+
throw reason;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
51
91
|
/**
|
|
52
92
|
* Capture (swallow) noisy global console output during a probe — React error
|
|
53
93
|
* boundary logs ("The above error occurred in <X>", "Consider adding an error
|
|
@@ -150,8 +190,18 @@ async function probeReactEffect(appRoot, entry, ctx) {
|
|
|
150
190
|
`import { flushSync } from 'react-dom';\n` +
|
|
151
191
|
`export const probe = { React: ReactNS.default || ReactNS, createRoot, flushSync, Comp, registry: fyodosGeneratedRegistries };\n`;
|
|
152
192
|
|
|
193
|
+
// A late React passive-effect cleanup that fires after we tear down jsdom must
|
|
194
|
+
// never crash the CLI (see installProbeTeardownGuard).
|
|
195
|
+
installProbeTeardownGuard();
|
|
196
|
+
|
|
153
197
|
let restore = () => {};
|
|
154
198
|
let cap = { restore() {} };
|
|
199
|
+
// Hoisted so the finally block can drain React (synchronously unmount + settle)
|
|
200
|
+
// BEFORE the DOM globals are removed — the only reliable way to stop React from
|
|
201
|
+
// running cleanup against a closed jsdom.
|
|
202
|
+
let root = null;
|
|
203
|
+
let flushSync = null;
|
|
204
|
+
const settle = () => new Promise((r) => setTimeout(r, 0));
|
|
155
205
|
try {
|
|
156
206
|
fs.writeFileSync(harnessAbs, harness);
|
|
157
207
|
await esbuild.build({
|
|
@@ -173,14 +223,13 @@ async function probeReactEffect(appRoot, entry, ctx) {
|
|
|
173
223
|
restore = installDom(jsdomMod, { storageKeys: ctx.storageKeys, kind: ctx.kind });
|
|
174
224
|
delete require.cache[outAbs];
|
|
175
225
|
const mod = require(outAbs);
|
|
176
|
-
const { React, createRoot,
|
|
226
|
+
const { React, createRoot, Comp, registry } = mod.probe;
|
|
227
|
+
flushSync = mod.probe.flushSync;
|
|
177
228
|
const Component = pickComponent(Comp);
|
|
178
229
|
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` };
|
|
179
230
|
|
|
180
231
|
const container = document.createElement('div');
|
|
181
232
|
document.body.appendChild(container);
|
|
182
|
-
let root;
|
|
183
|
-
const settle = () => new Promise((r) => setTimeout(r, 0));
|
|
184
233
|
try {
|
|
185
234
|
flushSync(() => { root = createRoot(container); root.render(React.createElement(Component)); });
|
|
186
235
|
await settle();
|
|
@@ -192,12 +241,11 @@ async function probeReactEffect(appRoot, entry, ctx) {
|
|
|
192
241
|
const beforeHTML = captureSnapshot(container);
|
|
193
242
|
const beforeText = container.textContent || '';
|
|
194
243
|
const handler = registry.handlers[entry.handler];
|
|
195
|
-
if (typeof handler !== 'function')
|
|
244
|
+
if (typeof handler !== 'function') return { status: 'fail', detail: `el registro no expone el handler '${entry.handler}'` };
|
|
196
245
|
|
|
197
246
|
if (ctx.sensitive) {
|
|
198
247
|
// Do NOT fire the effect; we proved it mounts and the handler resolves to a
|
|
199
248
|
// live bridge function. Verify the bridge has the method, nothing more.
|
|
200
|
-
try { root.unmount(); } catch {}
|
|
201
249
|
return { status: 'unverifiable', detail: 'confirmation action: its effect is not fired in the test render (security); wiring verified up to the confirmation point' };
|
|
202
250
|
}
|
|
203
251
|
|
|
@@ -205,15 +253,25 @@ async function probeReactEffect(appRoot, entry, ctx) {
|
|
|
205
253
|
try { await handler(ctx.params); } catch (e) { invokeErr = e; }
|
|
206
254
|
await settle();
|
|
207
255
|
try { flushSync(() => {}); } catch {}
|
|
208
|
-
if (invokeErr)
|
|
256
|
+
if (invokeErr) return { status: 'fail', detail: `invoking the handler, the real app threw: ${short(invokeErr)}` };
|
|
209
257
|
|
|
210
258
|
const afterHTML = captureSnapshot(container);
|
|
211
259
|
const afterText = container.textContent || '';
|
|
212
|
-
try { root.unmount(); } catch {}
|
|
213
260
|
return decide(ctx.kind, { beforeHTML, beforeText, afterHTML, afterText });
|
|
214
261
|
} catch (err) {
|
|
215
262
|
return { status: 'unverifiable', detail: `could not prepare the test render (${short(err)}); real effect not automatically verifiable — test by hand` };
|
|
216
263
|
} finally {
|
|
264
|
+
// Drain React WHILE the DOM is still alive: a synchronous unmount runs every
|
|
265
|
+
// pending passive-effect cleanup now, so nothing fires after we remove the
|
|
266
|
+
// jsdom globals below. Two settle ticks flush the scheduler's macro/microtasks.
|
|
267
|
+
try {
|
|
268
|
+
if (root) {
|
|
269
|
+
if (flushSync) flushSync(() => { try { root.unmount(); } catch { /* ignore */ } });
|
|
270
|
+
else { try { root.unmount(); } catch { /* ignore */ } }
|
|
271
|
+
await settle();
|
|
272
|
+
await settle();
|
|
273
|
+
}
|
|
274
|
+
} catch { /* ignore */ }
|
|
217
275
|
try { cap.restore(); } catch {}
|
|
218
276
|
try { fs.existsSync(harnessAbs) && fs.rmSync(harnessAbs); } catch {}
|
|
219
277
|
try { fs.existsSync(outAbs) && fs.rmSync(outAbs); } catch {}
|
|
@@ -357,6 +415,9 @@ function installDom(jsdomMod, { storageKeys = [], kind } = {}) {
|
|
|
357
415
|
* `sources` are the project's source files (for storage-key detection, etc.).
|
|
358
416
|
*/
|
|
359
417
|
async function probeComponentEffect(appRoot, entry, opts = {}) {
|
|
418
|
+
// Defense-in-depth: a late headless-render teardown error (React/Vue passive
|
|
419
|
+
// cleanup firing after jsdom is closed) must never crash the CLI.
|
|
420
|
+
installProbeTeardownGuard();
|
|
360
421
|
const framework = (opts.framework || 'web').toLowerCase();
|
|
361
422
|
const kind = actionKind(entry);
|
|
362
423
|
const params = probeParams(entry, kind);
|
|
@@ -376,4 +437,4 @@ async function probeComponentEffect(appRoot, entry, opts = {}) {
|
|
|
376
437
|
return { status: 'unverifiable', detail: `framework '${framework}' has no render-probe; real effect not automatically verifiable` };
|
|
377
438
|
}
|
|
378
439
|
|
|
379
|
-
module.exports = { probeComponentEffect, actionKind, decide, SENTINEL, SEED_ID, installDom, pickComponent, detectStorageKeys, probeParams, captureSnapshot, short, loadEsbuild, loadJsdom, reqFrom, emptyAssetsPlugin, captureConsole, isVerboseProbe };
|
|
440
|
+
module.exports = { probeComponentEffect, actionKind, decide, SENTINEL, SEED_ID, installDom, pickComponent, detectStorageKeys, probeParams, captureSnapshot, short, loadEsbuild, loadJsdom, reqFrom, emptyAssetsPlugin, captureConsole, isVerboseProbe, installProbeTeardownGuard, isProbeTeardownError };
|