@fiodos/cli 0.1.28 → 0.1.29
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 +1 -1
- package/src/aiAnalyze.js +40 -0
- package/src/verify.js +94 -1
- package/src/verifyWire.js +8 -1
- package/src/wireHandlers.js +92 -1
- package/src/wireWeb.js +2 -2
- package/src/wireWebMount.js +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
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/aiAnalyze.js
CHANGED
|
@@ -326,6 +326,34 @@ async function analyzeViaProxy({ apiKey, apiUrl, model, user, task = 'analysis',
|
|
|
326
326
|
'Or pass --own-ai-key to analyze with your own AI key (code never reaches Fiodos).',
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
|
+
// PRE-FLIGHT platform check (analysis only, non-web platforms): the deployed
|
|
330
|
+
// backend may be OLDER than this CLI and not know the platform yet — its
|
|
331
|
+
// prompt builder then silently falls back to the generic web prompt and the
|
|
332
|
+
// resulting manifest misses the platform spec entirely (e.g. Shopify's
|
|
333
|
+
// execution blocks). The system-prompt endpoint echoes the platform it
|
|
334
|
+
// resolved, so a stale backend is detectable BEFORE spending any AI tokens.
|
|
335
|
+
// Fail-open on network/endpoint errors: the post-call echo guard still runs.
|
|
336
|
+
if (task === 'analysis' && platform && platform !== 'web') {
|
|
337
|
+
try {
|
|
338
|
+
const probe = await fetch(
|
|
339
|
+
`${apiUrl}/v1/developer/analysis/system-prompt?task=analysis&platform=${encodeURIComponent(platform)}`,
|
|
340
|
+
{ headers: { 'x-api-key': apiKey } },
|
|
341
|
+
);
|
|
342
|
+
if (probe.ok) {
|
|
343
|
+
const meta = await probe.json();
|
|
344
|
+
if (meta && meta.platform && meta.platform !== platform) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`The Fiodos backend at ${apiUrl} does not support --platform ${platform} yet ` +
|
|
347
|
+
`(it would analyze as "${meta.platform}", missing the ${platform} platform spec). ` +
|
|
348
|
+
'Update/redeploy the backend and re-run the analysis.',
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
if (String(e && e.message).includes('does not support --platform')) throw e;
|
|
354
|
+
/* preflight is best-effort — the post-call echo guard still protects */
|
|
355
|
+
}
|
|
356
|
+
}
|
|
329
357
|
const res = await fetch(`${apiUrl}/v1/developer/analyze`, {
|
|
330
358
|
method: 'POST',
|
|
331
359
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
@@ -351,6 +379,18 @@ async function analyzeViaProxy({ apiKey, apiUrl, model, user, task = 'analysis',
|
|
|
351
379
|
throw new Error(`Hosted analysis failed: ${res.status} ${detail}`);
|
|
352
380
|
}
|
|
353
381
|
const data = await res.json();
|
|
382
|
+
// Platform degradation guard: an outdated backend that does not know the
|
|
383
|
+
// requested platform silently builds the generic WEB prompt instead — the
|
|
384
|
+
// manifest then LOOKS fine but misses the platform's spec (e.g. Shopify's
|
|
385
|
+
// declarative execution blocks, without which no commerce action can run).
|
|
386
|
+
// New backends echo the platform they actually used; a mismatch is fatal.
|
|
387
|
+
if (data.platform && platform && data.platform !== platform) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`The Fiodos backend at ${apiUrl} does not support --platform ${platform} yet ` +
|
|
390
|
+
`(it analyzed as "${data.platform}"). The manifest it would produce is missing ` +
|
|
391
|
+
`the ${platform} platform spec. Update/redeploy the backend and re-run the analysis.`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
354
394
|
return {
|
|
355
395
|
parsed: parseJsonContent(data.text || '{}'),
|
|
356
396
|
usage: {
|
package/src/verify.js
CHANGED
|
@@ -122,6 +122,79 @@ function sanitizeBubblePrompt(raw) {
|
|
|
122
122
|
return s || undefined;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** "es-ES" / "ES" → "es" (base language code, lowercased); undefined when unusable. */
|
|
126
|
+
function sanitizeLanguageCode(raw) {
|
|
127
|
+
const code = String(raw || '').split(/[-_]/)[0].trim().toLowerCase();
|
|
128
|
+
return /^[a-z]{2,3}$/.test(code) ? code : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Sanitize a route's `bubblePrompts` translation map (fail-open, item never
|
|
133
|
+
* dropped): keeps only valid base-language keys whose values survive the same
|
|
134
|
+
* bubblePrompt hygiene. Returns undefined when nothing usable remains, so the
|
|
135
|
+
* SDK falls back to the single `bubblePrompt`.
|
|
136
|
+
*/
|
|
137
|
+
function sanitizeBubblePrompts(raw) {
|
|
138
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
|
139
|
+
const out = {};
|
|
140
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
141
|
+
const lang = sanitizeLanguageCode(key);
|
|
142
|
+
const prompt = sanitizeBubblePrompt(value);
|
|
143
|
+
if (lang && prompt && !(lang in out)) out[lang] = prompt;
|
|
144
|
+
}
|
|
145
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Max length (chars) of a dashboard `labels` translation — it renders as a
|
|
150
|
+
* short UI label (route/action name), never a sentence.
|
|
151
|
+
*/
|
|
152
|
+
const MAX_LABEL = 60;
|
|
153
|
+
|
|
154
|
+
/** Sanitize one dashboard label translation string (see sanitizeBubblePrompt). */
|
|
155
|
+
function sanitizeLabelText(raw) {
|
|
156
|
+
if (typeof raw !== 'string') return undefined;
|
|
157
|
+
let s = raw.replace(/\s+/g, ' ').trim();
|
|
158
|
+
s = s.replace(/^["'«»“”]+|["'«»“”]+$/g, '').trim();
|
|
159
|
+
if (!s) return undefined;
|
|
160
|
+
if (s.length > MAX_LABEL) s = s.slice(0, MAX_LABEL).trim();
|
|
161
|
+
return s || undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sanitize a route/action's `labels` translation map (fail-open, item never
|
|
166
|
+
* dropped): keeps only valid base-language keys whose values survive label
|
|
167
|
+
* hygiene. Returns undefined when nothing usable remains, so the dashboard
|
|
168
|
+
* falls back to the primary `label`. Unlike `bubblePrompts`, this map is
|
|
169
|
+
* REQUIRED by the analyzer prompt (es/en, unconditional) — but the CLI still
|
|
170
|
+
* never blocks a manifest over a missing/malformed translation.
|
|
171
|
+
*/
|
|
172
|
+
function sanitizeLabels(raw) {
|
|
173
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
|
174
|
+
const out = {};
|
|
175
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
176
|
+
const lang = sanitizeLanguageCode(key);
|
|
177
|
+
const label = sanitizeLabelText(value);
|
|
178
|
+
if (lang && label && !(lang in out)) out[lang] = label;
|
|
179
|
+
}
|
|
180
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sanitize the manifest-level `appLanguages` list (fail-open): dedupes and
|
|
185
|
+
* normalizes to base codes, preserving the model's order (primary first).
|
|
186
|
+
* Returns undefined when nothing usable remains (monolingual apps).
|
|
187
|
+
*/
|
|
188
|
+
function sanitizeAppLanguages(raw) {
|
|
189
|
+
if (!Array.isArray(raw)) return undefined;
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const entry of raw) {
|
|
192
|
+
const lang = sanitizeLanguageCode(entry);
|
|
193
|
+
if (lang && !out.includes(lang)) out.push(lang);
|
|
194
|
+
}
|
|
195
|
+
return out.length > 0 ? out : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
125
198
|
/** Normalize an expo-router href for comparison: strip (groups), trailing /index. */
|
|
126
199
|
function normalizeHref(href) {
|
|
127
200
|
return ('/' + String(href || '')
|
|
@@ -399,6 +472,15 @@ function verifyManifest(manifest, evidence, sentFiles, staticRoutes, opts = {})
|
|
|
399
472
|
}
|
|
400
473
|
if (bubble) route.bubblePrompt = bubble;
|
|
401
474
|
else delete route.bubblePrompt;
|
|
475
|
+
// Multilingual bubble translations (fail-open): keep only clean entries so
|
|
476
|
+
// the orb can follow the app's active language; drop the map when empty.
|
|
477
|
+
const bubbles = sanitizeBubblePrompts(route.bubblePrompts);
|
|
478
|
+
if (bubbles) route.bubblePrompts = bubbles;
|
|
479
|
+
else delete route.bubblePrompts;
|
|
480
|
+
// Dashboard label translations (es/en) — see sanitizeLabels doc.
|
|
481
|
+
const routeLabels = sanitizeLabels(route.labels);
|
|
482
|
+
if (routeLabels) route.labels = routeLabels;
|
|
483
|
+
else delete route.labels;
|
|
402
484
|
}
|
|
403
485
|
|
|
404
486
|
// Schema coercion the model sometimes needs (same as the old engine).
|
|
@@ -414,10 +496,21 @@ function verifyManifest(manifest, evidence, sentFiles, staticRoutes, opts = {})
|
|
|
414
496
|
action.confirmationMessageTemplate = `Confirm: ${action.label || action.intent}?`;
|
|
415
497
|
provenance.notes.push(`action '${action.intent}': confirmation template filled mechanically`);
|
|
416
498
|
}
|
|
499
|
+
// Dashboard label translations (es/en) — see sanitizeLabels doc.
|
|
500
|
+
const actionLabels = sanitizeLabels(action.labels);
|
|
501
|
+
if (actionLabels) action.labels = actionLabels;
|
|
502
|
+
else delete action.labels;
|
|
417
503
|
}
|
|
418
504
|
|
|
505
|
+
// App-level supported languages (fail-open): normalized base codes, primary
|
|
506
|
+
// first; dropped entirely when the analysis found no i18n evidence.
|
|
507
|
+
const appLanguages = sanitizeAppLanguages(manifest.appLanguages);
|
|
508
|
+
const cleanManifest = { ...manifest, routes: dedupedRoutes, actions: dedupedActions };
|
|
509
|
+
if (appLanguages) cleanManifest.appLanguages = appLanguages;
|
|
510
|
+
else delete cleanManifest.appLanguages;
|
|
511
|
+
|
|
419
512
|
return {
|
|
420
|
-
manifest:
|
|
513
|
+
manifest: cleanManifest,
|
|
421
514
|
provenance,
|
|
422
515
|
};
|
|
423
516
|
}
|
package/src/verifyWire.js
CHANGED
|
@@ -29,7 +29,14 @@ function normRel(p) {
|
|
|
29
29
|
/** Extract "path(line,col): error" / "ERROR ... path:line" style lines. */
|
|
30
30
|
function extractErrorLines(output) {
|
|
31
31
|
const lines = String(output || '').split('\n');
|
|
32
|
-
return lines.filter((l) =>
|
|
32
|
+
return lines.filter((l) => {
|
|
33
|
+
// npm's failure banner ("npm error path …", "npm ERR! command failed") is
|
|
34
|
+
// the WRAPPER around a failed script, not a diagnostic. Counting it as an
|
|
35
|
+
// error made an action fail with "compilation: npm error path <dir>" even
|
|
36
|
+
// when every real tsc line had been classified as style-only.
|
|
37
|
+
if (/^\s*npm\s+(error|ERR!)/i.test(l)) return false;
|
|
38
|
+
return /error\s+TS\d+|: error|ERROR\b|is not assignable|Cannot find|does not exist/i.test(l);
|
|
39
|
+
});
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
/**
|
package/src/wireHandlers.js
CHANGED
|
@@ -576,6 +576,36 @@ function deriveBridgeAnchor(content, invoke) {
|
|
|
576
576
|
return null;
|
|
577
577
|
}
|
|
578
578
|
|
|
579
|
+
/**
|
|
580
|
+
* "Once wired, never regressed": when the file ALREADY contains a generated
|
|
581
|
+
* block registering this intent (from a previous verified run), reuse that
|
|
582
|
+
* position instead of re-rolling the AI's anchor. Re-running the wiring is
|
|
583
|
+
* otherwise nondeterministic — a bad anchor + an unlucky AI correction can
|
|
584
|
+
* un-wire an action that worked yesterday. Returns the nearest line ABOVE the
|
|
585
|
+
* existing block that survives the strip and is unique in the stripped
|
|
586
|
+
* content (a deterministic anchor for the same spot), or null.
|
|
587
|
+
*/
|
|
588
|
+
function reuseExistingBlockAnchor(rawContent, strippedContent, intent) {
|
|
589
|
+
const blockRe = new RegExp(
|
|
590
|
+
`(${esc(EDIT_START_PREFIX)}[^\\n]*)\\n[\\s\\S]*?${esc(EDIT_END)}`,
|
|
591
|
+
'g',
|
|
592
|
+
);
|
|
593
|
+
let m;
|
|
594
|
+
while ((m = blockRe.exec(rawContent))) {
|
|
595
|
+
if (!GENERATED_HEADER_RE.test(m[1])) continue;
|
|
596
|
+
if (!new RegExp(`"${esc(intent)}"\\s*:`).test(m[0])) continue;
|
|
597
|
+
const lines = rawContent.slice(0, m.index).split('\n');
|
|
598
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
599
|
+
const line = lines[i];
|
|
600
|
+
const t = line.trim();
|
|
601
|
+
if (!t || t.startsWith('//') || t.startsWith('/*') || t.startsWith('*')) continue;
|
|
602
|
+
if (line.includes('FYODOS:')) continue;
|
|
603
|
+
if (countOccurrences(strippedContent, line) === 1) return line;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
579
609
|
/**
|
|
580
610
|
* True when an anchor that appears several times is HARMLESS: every occurrence
|
|
581
611
|
* resolves to the same actual insertion point (the nearest JSX `return`), so
|
|
@@ -634,7 +664,13 @@ function buildAiBridgeEntry({ appRoot, fyodosDirRel, base, aiW }) {
|
|
|
634
664
|
const occ = anchor ? countOccurrences(content, anchor) : 0;
|
|
635
665
|
if (occ !== 1) {
|
|
636
666
|
const scope = b.scope === 'class-field' ? 'class-field' : 'statement';
|
|
637
|
-
|
|
667
|
+
const reused = reuseExistingBlockAnchor(rawContent, content, base.intent);
|
|
668
|
+
if (reused) {
|
|
669
|
+
// A previous run already wired this intent here and verified it; the
|
|
670
|
+
// proven position beats re-rolling an ambiguous AI anchor.
|
|
671
|
+
anchor = reused;
|
|
672
|
+
anchorDerived = true;
|
|
673
|
+
} else if (occ > 1 && scope === 'statement' && ambiguousAnchorSameInsertion(content, anchor)) {
|
|
638
674
|
// Keep the AI anchor: every occurrence lands on the same insertion point.
|
|
639
675
|
} else {
|
|
640
676
|
const derived = deriveBridgeAnchor(content, invoke);
|
|
@@ -675,6 +711,13 @@ function buildAiBridgeEntry({ appRoot, fyodosDirRel, base, aiW }) {
|
|
|
675
711
|
const rawFrom = normRel(imp && imp.from);
|
|
676
712
|
const kind = (imp && imp.kind) || 'named';
|
|
677
713
|
if (!name || !rawFrom) return { reason: 'a bridge import is incomplete (missing name/from)' };
|
|
714
|
+
// The component often ALREADY imports the symbol (useApp, a store hook…).
|
|
715
|
+
// Re-importing it produces "Duplicate identifier" and fails the candidate,
|
|
716
|
+
// so an already-in-scope name is accepted without adding a second import.
|
|
717
|
+
if (new RegExp(`^\\s*import\\b[^;\\n]*[^\\w$]${esc(name)}[^\\w$][^;\\n]*from`, 'm').test(content)) {
|
|
718
|
+
importedNames.add(name);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
678
721
|
const from = resolveSourceFile(appRoot, rawFrom);
|
|
679
722
|
if (!from) return { reason: `the bridge import points to a non-existent file: '${rawFrom}'` };
|
|
680
723
|
if (kind === 'named' && !fileHasSymbol(appRoot, from, name)) return { reason: `named import '${name}' does not exist in '${from}'` };
|
|
@@ -1351,6 +1394,26 @@ function insertRegisterBlock(content, file, anchor, block, scope) {
|
|
|
1351
1394
|
return content.slice(0, endIdx) + '\n' + block + content.slice(endIdx);
|
|
1352
1395
|
}
|
|
1353
1396
|
|
|
1397
|
+
// When the anchor's own line is unmistakably a body STATEMENT (a declaration
|
|
1398
|
+
// or a bare call ending in `;`), insert right AFTER it. Snapping to the
|
|
1399
|
+
// nearest `return` instead can land the registration inside a conditional
|
|
1400
|
+
// early-return branch (`if (authLoading) { … return <Spinner/> }`): it
|
|
1401
|
+
// compiles, but in normal use that branch never runs and the bridge is never
|
|
1402
|
+
// registered — the action silently dies at runtime.
|
|
1403
|
+
const anchorLineStart = content.lastIndexOf('\n', at) + 1;
|
|
1404
|
+
const anchorLineEnd = content.indexOf('\n', at);
|
|
1405
|
+
const anchorLine = content.slice(anchorLineStart, anchorLineEnd === -1 ? content.length : anchorLineEnd).trim();
|
|
1406
|
+
const isStatementLine =
|
|
1407
|
+
/^(const|let|var)\b.*;\s*$/.test(anchorLine) || /^[\w$][\w$.]*\((?:[^()]|\([^()]*\))*\);\s*$/.test(anchorLine);
|
|
1408
|
+
if (isStatementLine) {
|
|
1409
|
+
// A multi-line declaration (`const { a, b } =\n useApp();`) ends at the
|
|
1410
|
+
// first `;` at/after the anchor, not at the anchor's own line end.
|
|
1411
|
+
const semi = content.indexOf(';', at);
|
|
1412
|
+
const stmtEnd = semi === -1 ? (anchorLineEnd === -1 ? content.length : anchorLineEnd) : content.indexOf('\n', semi);
|
|
1413
|
+
const endIdx = stmtEnd === -1 ? content.length : stmtEnd;
|
|
1414
|
+
return content.slice(0, endIdx) + '\n' + block + content.slice(endIdx);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1354
1417
|
const ret = nearestJsxReturn(content, at);
|
|
1355
1418
|
if (ret !== -1) {
|
|
1356
1419
|
const lineStart = content.lastIndexOf('\n', ret) + 1;
|
|
@@ -1500,6 +1563,34 @@ function pickRelevantFiles(files, evidence, candidate, intent) {
|
|
|
1500
1563
|
for (const imp of (candidate.bridge && candidate.bridge.imports) || []) if (imp && imp.from) wanted.add(normRel(imp.from));
|
|
1501
1564
|
for (const r of candidate.requiredSymbols || []) if (r && r.file) wanted.add(normRel(r.file));
|
|
1502
1565
|
}
|
|
1566
|
+
// The most common unfixable failure is "symbol X is not available in the
|
|
1567
|
+
// proposed file": the fix lives in ANOTHER component that consumes X. Give
|
|
1568
|
+
// the corrector every file that mentions the invoked symbols (capped) so it
|
|
1569
|
+
// can retarget the bridge instead of blindly retrying the same file.
|
|
1570
|
+
const invokeSrc = String(
|
|
1571
|
+
(candidate && ((candidate.bridge && candidate.bridge.invoke) || candidate.call)) || '',
|
|
1572
|
+
);
|
|
1573
|
+
const symbols = new Set();
|
|
1574
|
+
let sm;
|
|
1575
|
+
const symRe = /(?:^|[^.\w$])([A-Za-z_$][\w$]{2,})\s*\(/g;
|
|
1576
|
+
while ((sm = symRe.exec(invokeSrc))) {
|
|
1577
|
+
if (!JS_KEYWORDS.has(sm[1]) && !SAFE_GLOBALS.has(sm[1])) symbols.add(sm[1]);
|
|
1578
|
+
}
|
|
1579
|
+
if (symbols.size) {
|
|
1580
|
+
let extra = 0;
|
|
1581
|
+
for (const f of files) {
|
|
1582
|
+
if (extra >= 4) break;
|
|
1583
|
+
if (wanted.has(normRel(f.rel))) continue;
|
|
1584
|
+
const content = String(f.content || '');
|
|
1585
|
+
for (const sym of symbols) {
|
|
1586
|
+
if (new RegExp(`(^|[^\\w$])${esc(sym)}([^\\w$]|$)`).test(content)) {
|
|
1587
|
+
wanted.add(normRel(f.rel));
|
|
1588
|
+
extra += 1;
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1503
1594
|
const picked = files.filter((f) => wanted.has(normRel(f.rel)));
|
|
1504
1595
|
return picked.length ? picked : files.slice(0, 4);
|
|
1505
1596
|
}
|
package/src/wireWeb.js
CHANGED
|
@@ -134,7 +134,7 @@ function agentSnippetVue() {
|
|
|
134
134
|
`</script>\n\n` +
|
|
135
135
|
`<template>\n` +
|
|
136
136
|
` <!-- ...your app... -->\n` +
|
|
137
|
-
` <FiodosAgent :api-key="fyodosApiKey" :base-url="fyodosApiUrl" />\n` +
|
|
137
|
+
` <FiodosAgent :api-key="fyodosApiKey" :base-url="fyodosApiUrl" locale-detection="document" />\n` +
|
|
138
138
|
`</template>`
|
|
139
139
|
);
|
|
140
140
|
}
|
|
@@ -144,7 +144,7 @@ function agentSnippetSvelte() {
|
|
|
144
144
|
`<script lang="ts">\n` +
|
|
145
145
|
` import FiodosAgent from '@fiodos/svelte/FiodosAgent.svelte';\n` +
|
|
146
146
|
`</script>\n\n` +
|
|
147
|
-
`<FiodosAgent apiKey={import.meta.env.VITE_FYODOS_API_KEY} baseUrl={import.meta.env.VITE_FYODOS_API_URL} />`
|
|
147
|
+
`<FiodosAgent apiKey={import.meta.env.VITE_FYODOS_API_KEY} baseUrl={import.meta.env.VITE_FYODOS_API_URL} localeDetection="document" />`
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
150
|
|
package/src/wireWebMount.js
CHANGED
|
@@ -122,6 +122,10 @@ function buildEntryBootstrapBlock(opts = {}) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
if (screenImportPath) lines.push(` getCurrentRoute: ${SCREEN_ROUTE_EXPORT},`);
|
|
125
|
+
// Language link (never left disconnected): the orb follows the page's live
|
|
126
|
+
// <html lang>, so analyzer bubble translations track the app's language
|
|
127
|
+
// switcher automatically.
|
|
128
|
+
lines.push(" localeDetection: 'document',");
|
|
125
129
|
lines.push(' mount: true,');
|
|
126
130
|
lines.push('});');
|
|
127
131
|
lines.push(ORB_SCRIPT_END);
|
|
@@ -243,6 +247,10 @@ function agentJsx(framework, ts, indent = ' ') {
|
|
|
243
247
|
`${indent}<FiodosAgent\n` +
|
|
244
248
|
`${indent} apiKey={${keyExpr}}\n` +
|
|
245
249
|
`${indent} apiUrl={${urlExpr}}\n` +
|
|
250
|
+
// Language link (never left disconnected): the orb follows the page's live
|
|
251
|
+
// <html lang>, so analyzer bubble translations track the app's language
|
|
252
|
+
// switcher automatically.
|
|
253
|
+
`${indent} localeDetection="document"\n` +
|
|
246
254
|
`${indent}/>`
|
|
247
255
|
);
|
|
248
256
|
}
|
|
@@ -644,7 +652,7 @@ function injectVueSfc(plan) {
|
|
|
644
652
|
}
|
|
645
653
|
const block = [
|
|
646
654
|
ORB_HTML_START,
|
|
647
|
-
' <FiodosAgent :api-key="fyodosApiKey" :base-url="fyodosApiUrl" />',
|
|
655
|
+
' <FiodosAgent :api-key="fyodosApiKey" :base-url="fyodosApiUrl" locale-detection="document" />',
|
|
648
656
|
ORB_HTML_END,
|
|
649
657
|
].join('\n');
|
|
650
658
|
source = source.replace('</template>', `${block}\n</template>`);
|
|
@@ -662,7 +670,7 @@ function injectSvelteSfc(plan) {
|
|
|
662
670
|
}
|
|
663
671
|
const block = [
|
|
664
672
|
ORB_HTML_START,
|
|
665
|
-
'<FiodosAgent apiKey={import.meta.env.VITE_FYODOS_API_KEY} baseUrl={import.meta.env.VITE_FYODOS_API_URL} />',
|
|
673
|
+
'<FiodosAgent apiKey={import.meta.env.VITE_FYODOS_API_KEY} baseUrl={import.meta.env.VITE_FYODOS_API_URL} localeDetection="document" />',
|
|
666
674
|
ORB_HTML_END,
|
|
667
675
|
].join('\n');
|
|
668
676
|
if (source.includes('</main>')) {
|
|
@@ -699,7 +707,7 @@ function injectAngularComponent(plan) {
|
|
|
699
707
|
);
|
|
700
708
|
}
|
|
701
709
|
|
|
702
|
-
const block = [ORB_HTML_START, '<fyodos-agent [apiKey]="fyodosApiKey" [baseUrl]="fyodosApiUrl" />', ORB_HTML_END].join('\n');
|
|
710
|
+
const block = [ORB_HTML_START, '<fyodos-agent [apiKey]="fyodosApiKey" [baseUrl]="fyodosApiUrl" localeDetection="document" />', ORB_HTML_END].join('\n');
|
|
703
711
|
html = `${html.trimEnd()}\n${block}\n`;
|
|
704
712
|
writeFile(target.file, ts);
|
|
705
713
|
writeFile(templatePath, html);
|