@fiodos/cli 0.1.14 → 0.1.16

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/src/wireWeb.js CHANGED
@@ -24,6 +24,10 @@ const {
24
24
  writeConsentDoc,
25
25
  agentJsx,
26
26
  envExpr,
27
+ registryRelImport,
28
+ addRegistriesToMountSource,
29
+ addRegistriesToBootstrapSource,
30
+ buildEntryBootstrapBlock,
27
31
  IMPORT_NAME,
28
32
  WRAPPER_BASENAME,
29
33
  } = require('./wireWebMount');
@@ -60,17 +64,24 @@ function firstExisting(appRoot, rels) {
60
64
 
61
65
  function detectTarget(appRoot, framework = detectFramework(appRoot)) {
62
66
  if (framework === 'vue') {
63
- const f = firstExisting(appRoot, ['src/App.vue', 'src/main.ts', 'src/main.js']);
67
+ // Prefer the entry module (deterministic anchor → bootstrap injection). Fall
68
+ // back to the SFC <template> strategy only when there is no entry file.
69
+ const entry = firstExisting(appRoot, ['src/main.ts', 'src/main.js']);
70
+ if (entry) return { kind: 'vue-entry', file: entry, ext: path.extname(entry), framework };
71
+ const f = firstExisting(appRoot, ['src/App.vue']);
64
72
  return f ? { kind: 'vue', file: f, ext: path.extname(f), framework } : { kind: 'vue', file: null, framework };
65
73
  }
66
74
  if (framework === 'sveltekit') {
67
- const f = firstExisting(appRoot, [
68
- 'src/routes/+layout.svelte', 'src/routes/+page.svelte', 'src/App.svelte',
69
- ]);
70
- return f ? { kind: 'sveltekit', file: f, ext: '.svelte', framework } : { kind: 'sveltekit', file: null, framework };
75
+ // SvelteKit owns the bootstrap (no src/main.ts), but src/hooks.client.ts runs
76
+ // once in the browser — the ideal, template-free spot. Create it if missing.
77
+ const existing = firstExisting(appRoot, ['src/hooks.client.ts', 'src/hooks.client.js']);
78
+ const file = existing || path.join(appRoot, 'src/hooks.client.ts');
79
+ return { kind: 'sveltekit-hooks', file, ext: path.extname(file), framework };
71
80
  }
72
81
  if (framework === 'svelte') {
73
- const f = firstExisting(appRoot, ['src/App.svelte', 'src/routes/+layout.svelte', 'src/main.ts']);
82
+ const entry = firstExisting(appRoot, ['src/main.ts', 'src/main.js']);
83
+ if (entry) return { kind: 'svelte-entry', file: entry, ext: path.extname(entry), framework };
84
+ const f = firstExisting(appRoot, ['src/App.svelte']);
74
85
  return f ? { kind: 'svelte', file: f, ext: '.svelte', framework } : { kind: 'svelte', file: null, framework };
75
86
  }
76
87
  if (framework === 'angular') {
@@ -160,6 +171,15 @@ function printSnippet(target, framework, colors, appRoot, reason) {
160
171
  console.error(`${dim}Reason: ${reason}${reset}\n`);
161
172
  }
162
173
 
174
+ // Entry-file bootstrap frameworks: the safe fallback is the same one-shot call
175
+ // we would have auto-injected into the entry module — no <template> edit.
176
+ const entryKind = target && /(-entry|-hooks)$/.test(target.kind || '');
177
+ if (entryKind) {
178
+ const where = path.relative(appRoot, target.file);
179
+ console.error(`${dim}In ${where}, after your imports:${reset}\n`);
180
+ console.error(buildEntryBootstrapBlock());
181
+ return;
182
+ }
163
183
  if (framework === 'vue') {
164
184
  const where = target && target.file ? path.relative(appRoot, target.file) : 'src/App.vue';
165
185
  console.error(`${dim}In ${where}:${reset}\n`);
@@ -243,10 +263,23 @@ async function wireWebOrb(appRoot, opts = {}) {
243
263
  };
244
264
 
245
265
  const backups = backupFiles(plan.files);
266
+ // Files the installer creates from scratch (e.g. SvelteKit src/hooks.client.ts)
267
+ // are not in `backups`; a revert must DELETE them, not leave an orphan behind.
268
+ const createdFiles = plan.files.filter((f) => !(f in backups));
269
+ const revertAll = () => {
270
+ revertFiles(backups);
271
+ for (const f of createdFiles) {
272
+ try {
273
+ if (fs.existsSync(f)) fs.unlinkSync(f);
274
+ } catch {
275
+ /* best-effort */
276
+ }
277
+ }
278
+ };
246
279
  try {
247
280
  applyMount(plan);
248
281
  if (!verifyMounted(plan)) {
249
- revertFiles(backups);
282
+ revertAll();
250
283
  printSnippet(target, framework, colors, appRoot, 'post-edit verification failed — FiodosAgent not found in edited files');
251
284
  return { status: 'failed', file: plan.rel };
252
285
  }
@@ -265,7 +298,7 @@ async function wireWebOrb(appRoot, opts = {}) {
265
298
  return { status: 'added', file: plan.rel, test };
266
299
  }
267
300
  // Build failed with the orb mounted — was the app building WITHOUT it?
268
- revertFiles(backups);
301
+ revertAll();
269
302
  const baseline = await build();
270
303
  if (baseline.ok) {
271
304
  // Built before, fails now → the mount is the regression. Keep it reverted.
@@ -282,7 +315,7 @@ async function wireWebOrb(appRoot, opts = {}) {
282
315
  writeConsentDoc(appRoot, plan);
283
316
  return { status: 'added', file: plan.rel };
284
317
  } catch (err) {
285
- revertFiles(backups);
318
+ revertAll();
286
319
  printSnippet(target, framework, colors, appRoot, err.message || String(err));
287
320
  return { status: 'failed', file: plan.rel, error: err };
288
321
  }
@@ -317,9 +350,121 @@ function reportWireResult(result, colors = {}, opts = {}) {
317
350
  }
318
351
  }
319
352
 
353
+ /**
354
+ * Second pass (after handler wiring): connect the generated registry to the
355
+ * already-mounted orb so it EXECUTES actions, not just navigates. This is what
356
+ * removes the manual "paste registries={…}" step. Idempotent and reversible:
357
+ * the prop lives inside the FYODOS:ORB block. React-family mounts only —
358
+ * Vue/Svelte/Angular SDKs receive handlers differently and are left to the doc.
359
+ * Nav-only apps (no auto-wired handlers) never reach here (the caller gates on
360
+ * autoCount > 0), so they are not given a needless registries prop.
361
+ */
362
+ function connectOrbRegistries(appRoot, opts = {}) {
363
+ const { registryFileAbs, exportName = 'fyodosGeneratedRegistries' } = opts;
364
+ if (!registryFileAbs || !fs.existsSync(registryFileAbs)) return { status: 'no-registry' };
365
+
366
+ const framework = detectFramework(appRoot);
367
+ const target = detectTarget(appRoot, framework);
368
+ if (!target || !target.file) return { status: 'no-target' };
369
+
370
+ // Entry-file bootstrap (Vue / Svelte / SvelteKit): the registries go INTO the
371
+ // createFiodosAgent({…}) call we injected into the entry module — same safe,
372
+ // idempotent, reversible block. No manual "pass registries" step anywhere.
373
+ const bootstrapKinds = new Set(['vue-entry', 'svelte-entry', 'sveltekit-hooks']);
374
+ if (bootstrapKinds.has(target.kind)) {
375
+ let source;
376
+ try {
377
+ source = fs.readFileSync(target.file, 'utf8');
378
+ } catch {
379
+ return { status: 'not-mounted' };
380
+ }
381
+ const importPath = registryRelImport(target.file, registryFileAbs);
382
+ const fileRel = path.relative(appRoot, target.file);
383
+ const backups = backupFiles([target.file]);
384
+ try {
385
+ const res = addRegistriesToBootstrapSource(source, importPath);
386
+ if (!res.changed) {
387
+ return { status: res.reason === 'already' ? 'already' : 'not-mounted', file: fileRel };
388
+ }
389
+ fs.writeFileSync(target.file, res.source);
390
+ return { status: 'connected', file: fileRel };
391
+ } catch (e) {
392
+ revertFiles(backups);
393
+ return { status: 'failed', file: fileRel, error: e };
394
+ }
395
+ }
396
+
397
+ // Only React-family mounts accept a `registries={...}` prop.
398
+ const reactKinds = new Set(['vite', 'next-pages', 'next-app']);
399
+ if (!reactKinds.has(target.kind)) return { status: 'unsupported-framework', framework };
400
+
401
+ // <FiodosAgent/> lives in the client wrapper for the Next App Router, in the
402
+ // entry/root file otherwise.
403
+ let mountFile = target.file;
404
+ if (target.kind === 'next-app') {
405
+ const dir = path.dirname(target.file);
406
+ mountFile =
407
+ [path.join(dir, `${WRAPPER_BASENAME}.tsx`), path.join(dir, `${WRAPPER_BASENAME}.jsx`)].find((f) =>
408
+ fs.existsSync(f),
409
+ ) || null;
410
+ if (!mountFile) return { status: 'not-mounted' };
411
+ }
412
+
413
+ let source;
414
+ try {
415
+ source = fs.readFileSync(mountFile, 'utf8');
416
+ } catch {
417
+ return { status: 'not-mounted' };
418
+ }
419
+ if (!/<FiodosAgent\b/.test(source)) return { status: 'not-mounted' };
420
+
421
+ const importPath = registryRelImport(mountFile, registryFileAbs);
422
+ const fileRel = path.relative(appRoot, mountFile);
423
+ const backups = backupFiles([mountFile]);
424
+ try {
425
+ const res = addRegistriesToMountSource(source, exportName, importPath);
426
+ if (!res.changed) {
427
+ return { status: res.reason === 'already' ? 'already' : 'not-mounted', file: fileRel };
428
+ }
429
+ fs.writeFileSync(mountFile, res.source);
430
+ return { status: 'connected', file: fileRel };
431
+ } catch (e) {
432
+ revertFiles(backups);
433
+ return { status: 'failed', file: fileRel, error: e };
434
+ }
435
+ }
436
+
437
+ function reportRegistriesResult(result, colors = {}, opts = {}) {
438
+ const quiet = opts.quiet !== false;
439
+ if (!result) return;
440
+ const { blue, cyan, dim, reset } = colors;
441
+ const tag = `${cyan || ''}◉${reset || ''} ${blue || ''}Fiodos${reset || ''}`;
442
+ switch (result.status) {
443
+ case 'connected':
444
+ console.error(`${tag} · ${dim || ''}connected actions to the orb (registries=) in ${result.file}${reset || ''}`);
445
+ break;
446
+ case 'already':
447
+ if (quiet) return;
448
+ console.error(`${tag} · ${dim || ''}orb already wired to actions in ${result.file}${reset || ''}`);
449
+ break;
450
+ case 'unsupported-framework':
451
+ console.error(`${tag} · ${dim || ''}pass the generated registry to your orb manually (see FYODOS_HANDLERS.md)${reset || ''}`);
452
+ break;
453
+ case 'failed':
454
+ console.error(`${tag} · ✗ could not auto-connect actions to the orb${result.file ? ` in ${result.file}` : ''}. See FYODOS_HANDLERS.md.`);
455
+ break;
456
+ default:
457
+ // not-mounted / no-registry / no-target: silent. The orb still navigates,
458
+ // and the doc shows how to connect actions by hand if needed.
459
+ break;
460
+ }
461
+ }
462
+
320
463
  module.exports = {
321
464
  wireWebOrb,
322
465
  reportWireResult,
466
+ connectOrbRegistries,
467
+ reportRegistriesResult,
323
468
  detectFramework,
324
469
  detectTarget,
325
470
  };
@@ -60,6 +60,75 @@ function insertImportAfterLastImport(source, importLine) {
60
60
  return lines.join('\n');
61
61
  }
62
62
 
63
+ // Insert a multi-line block right after the last top-level import. The block may
64
+ // itself contain import statements — placed after the existing ones they stay
65
+ // valid top-level imports (ESM hoists them anyway). Used by the entry-file
66
+ // bootstrap so the orb starts before the app mounts.
67
+ function insertBlockAfterLastImport(source, block) {
68
+ const lines = source.split('\n');
69
+ let lastImport = -1;
70
+ for (let i = 0; i < lines.length; i += 1) {
71
+ if (/^\s*import\b/.test(lines[i])) lastImport = i;
72
+ }
73
+ if (lastImport === -1) return `${block}\n\n${source}`;
74
+ lines.splice(lastImport + 1, 0, '', block);
75
+ return lines.join('\n');
76
+ }
77
+
78
+ // ── Entry-file bootstrap (Vue / Svelte / SvelteKit) ───────────────────────────
79
+ // Instead of editing a framework <template> (no deterministic anchor, fragile),
80
+ // we inject a one-shot createFiodosAgent() call into the app's entry module
81
+ // (src/main.ts, hooks.client.ts). The orb mounts itself to document.body, so it
82
+ // never needs to live in the component tree. The whole call lives between
83
+ // FYODOS:ORB markers → idempotent (re-runs strip & rewrite) and reversible.
84
+ const BOOTSTRAP_ALIAS = '__fiodosCreateAgent';
85
+ const BOOTSTRAP_REGISTRY_EXPORT = 'fyodosGeneratedRegistries';
86
+
87
+ function buildEntryBootstrapBlock(opts = {}) {
88
+ const { registryImportPath } = opts;
89
+ const lines = [];
90
+ lines.push(ORB_SCRIPT_START);
91
+ lines.push(`import { createFiodosAgent as ${BOOTSTRAP_ALIAS} } from '@fiodos/web-core';`);
92
+ if (registryImportPath) {
93
+ lines.push(`import { ${BOOTSTRAP_REGISTRY_EXPORT} } from '${registryImportPath}';`);
94
+ }
95
+ lines.push(`${BOOTSTRAP_ALIAS}({`);
96
+ lines.push(' apiKey: import.meta.env.VITE_FYODOS_API_KEY,');
97
+ lines.push(' baseUrl: import.meta.env.VITE_FYODOS_API_URL,');
98
+ if (registryImportPath) lines.push(` registries: ${BOOTSTRAP_REGISTRY_EXPORT},`);
99
+ lines.push(' mount: true,');
100
+ lines.push('});');
101
+ lines.push(ORB_SCRIPT_END);
102
+ return lines.join('\n');
103
+ }
104
+
105
+ function injectEntryBootstrap(plan, opts = {}) {
106
+ const file = plan.target.file;
107
+ let source = fs.existsSync(file) ? readFile(file) : '';
108
+ source = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
109
+ const block = buildEntryBootstrapBlock(opts);
110
+ let next;
111
+ if (!source.trim()) {
112
+ next = `${block}\n`;
113
+ } else {
114
+ next = insertBlockAfterLastImport(source, block);
115
+ }
116
+ writeFile(file, next.endsWith('\n') ? next : `${next}\n`);
117
+ }
118
+
119
+ // Idempotently rewrite an existing entry-bootstrap block to carry the generated
120
+ // registries (second pass, after handler wiring). Returns { changed, source, reason? }.
121
+ function addRegistriesToBootstrapSource(source, registryImportPath) {
122
+ if (!source.includes(ORB_SCRIPT_START)) return { changed: false, source, reason: 'not-mounted' };
123
+ if (/\n\s*registries:\s/.test(source.slice(source.indexOf(ORB_SCRIPT_START)))) {
124
+ return { changed: false, source, reason: 'already' };
125
+ }
126
+ const stripped = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
127
+ const block = buildEntryBootstrapBlock({ registryImportPath });
128
+ const next = insertBlockAfterLastImport(stripped.trimEnd(), block);
129
+ return { changed: true, source: next.endsWith('\n') ? next : `${next}\n` };
130
+ }
131
+
63
132
  function envExpr(framework, kind, ts) {
64
133
  if (framework === 'vite' || framework === 'vue' || framework === 'svelte' || framework === 'sveltekit') {
65
134
  if (kind === 'key') {
@@ -87,6 +156,42 @@ function isAlreadyMounted(source) {
87
156
  return /FiodosAgent|fyodos-agent|FYODOS:ORB:START/.test(source);
88
157
  }
89
158
 
159
+ // Import specifier (no extension, normalized slashes) from the file holding the
160
+ // orb mount to the generated registry module.
161
+ function registryRelImport(fromFile, registryAbs) {
162
+ let rel = path
163
+ .relative(path.dirname(fromFile), registryAbs)
164
+ .replace(/\\/g, '/')
165
+ .replace(/\.(tsx?|jsx?|mjs|cjs)$/, '');
166
+ if (!rel.startsWith('.')) rel = `./${rel}`;
167
+ return rel;
168
+ }
169
+
170
+ // Idempotently add `registries={EXPORT}` to the mounted <FiodosAgent/> and
171
+ // import EXPORT from the generated registry. React-family mounts only. The prop
172
+ // lands INSIDE the FYODOS:ORB block, so removing that block reverts it; the
173
+ // import follows the existing FiodosAgent import convention (added once,
174
+ // harmless if the block is later removed). Returns { changed, source, reason? }.
175
+ function addRegistriesToMountSource(source, exportName, importPath) {
176
+ if (new RegExp(`registries=\\{${escapeRe(exportName)}\\}`).test(source)) {
177
+ return { changed: false, source, reason: 'already' };
178
+ }
179
+ const tagRe = /([ \t]*)<FiodosAgent\b[\s\S]*?\/>/;
180
+ const m = source.match(tagRe);
181
+ if (!m) return { changed: false, source, reason: 'not-mounted' };
182
+ const tag = m[0];
183
+ const baseIndent = m[1] || '';
184
+ const propMatch = tag.match(/\n([ \t]*)(?:apiKey|api-key|apiUrl|baseUrl)/);
185
+ const propIndent = propMatch ? propMatch[1] : `${baseIndent} `;
186
+ const newTag = tag.replace(/\n?[ \t]*\/>$/, `\n${propIndent}registries={${exportName}}\n${baseIndent}/>`);
187
+ let next = source.replace(tag, newTag);
188
+ const importLine = `import { ${exportName} } from '${importPath}';`;
189
+ if (!next.includes(importLine)) {
190
+ next = insertImportAfterLastImport(next, importLine);
191
+ }
192
+ return { changed: true, source: next };
193
+ }
194
+
90
195
  /**
91
196
  * Assess whether we can inject safely. Returns { ok, reason?, strategy, files, rel }.
92
197
  */
@@ -95,6 +200,19 @@ function assessMount(target, framework, appRoot) {
95
200
  return { ok: false, reason: 'could not locate a framework entry file (e.g. src/main.tsx, App.vue, +layout.svelte)' };
96
201
  }
97
202
  const rel = path.relative(appRoot, target.file);
203
+
204
+ // Entry-file bootstrap (Vue / Svelte / SvelteKit). The orb is injected into the
205
+ // app's entry module, not a <template>. For SvelteKit hooks the file may not
206
+ // exist yet — we create it (safe: a brand-new client hook only adds our block).
207
+ const entryKinds = new Set(['vue-entry', 'svelte-entry', 'sveltekit-hooks']);
208
+ if (entryKinds.has(target.kind)) {
209
+ const exists = fs.existsSync(target.file);
210
+ if (exists && isAlreadyMounted(readFile(target.file))) {
211
+ return { ok: false, reason: 'orb already present', already: true, rel };
212
+ }
213
+ return { ok: true, strategy: 'entry-bootstrap', files: [target.file], rel, framework, target, createIfMissing: !exists };
214
+ }
215
+
98
216
  const source = readFile(target.file);
99
217
  if (isAlreadyMounted(source)) {
100
218
  return { ok: false, reason: 'orb already present', already: true, rel };
@@ -105,14 +223,22 @@ function assessMount(target, framework, appRoot) {
105
223
  if (!source.includes('</body>')) {
106
224
  return { ok: false, reason: `${rel} has no </body> — cannot mount the orb wrapper safely` };
107
225
  }
108
- return { ok: true, strategy: 'next-app', files: [target.file], rel, framework, target };
226
+ // The client wrapper we create lives next to the layout. Listing it in
227
+ // `files` means a revert DELETES it (it isn't in the backups) instead of
228
+ // leaving an orphan that imports @fiodos/react.
229
+ const ts = target.ext === '.tsx';
230
+ const wrapperFile = path.join(path.dirname(target.file), `${WRAPPER_BASENAME}${ts ? '.tsx' : '.jsx'}`);
231
+ return { ok: true, strategy: 'next-app', files: [target.file, wrapperFile], rel, framework, target };
109
232
  }
110
233
  case 'next-pages':
111
234
  case 'vite': {
112
235
  if (/createRoot\s*\(/.test(source) && /\.render\s*\(/.test(source)) {
113
236
  return { ok: true, strategy: 'vite-main', files: [target.file], rel, framework, target };
114
237
  }
115
- if (/return\s*\([\s\S]*/.test(source)) {
238
+ // Accept any component that returns JSX — `return (` OR a bare `return <…/>`
239
+ // (the create-next-app default `_app.tsx`). We wrap the returned element in
240
+ // a fragment so the orb sits beside it without breaking adjacent-JSX rules.
241
+ if (/return\s*[<(]/.test(source)) {
116
242
  return { ok: true, strategy: 'react-return', files: [target.file], rel, framework, target };
117
243
  }
118
244
  return { ok: false, reason: `${rel} is not a recognizable React entry (no createRoot().render() or JSX return)` };
@@ -163,8 +289,13 @@ function resolveAngularTemplate(tsFile, tsSource) {
163
289
  }
164
290
 
165
291
  function describePlan(plan) {
166
- const lines = [`Will edit: ${plan.files.map((f) => path.basename(f)).join(', ')}`];
292
+ const action = plan.createIfMissing ? 'Will create' : 'Will edit';
293
+ const lines = [`${action}: ${plan.files.map((f) => path.basename(f)).join(', ')}`];
167
294
  switch (plan.strategy) {
295
+ case 'entry-bootstrap':
296
+ lines.push('Add createFiodosAgent({ apiKey, baseUrl, mount }) after the imports');
297
+ lines.push('Orb self-mounts to document.body (no <template> edit, marked FYODOS:ORB:*)');
298
+ break;
168
299
  case 'next-app':
169
300
  lines.push(`Create ${WRAPPER_BASENAME} client wrapper next to layout`);
170
301
  lines.push(`Import wrapper and mount before </body>`);
@@ -262,6 +393,57 @@ function injectViteMain(plan) {
262
393
  writeFile(target.file, source);
263
394
  }
264
395
 
396
+ // Wrap the component's last `return <jsx>` (with or without parentheses) in a
397
+ // fragment that holds the original element AND the orb. Wrapping ANY single
398
+ // returned expression as `<>{expr}{orb}</>` is always valid JSX, so this is the
399
+ // universal, adjacent-JSX-safe fallback when there is no closing-tag anchor to
400
+ // slot into (e.g. the create-next-app default `return <Component {...pageProps} />`).
401
+ // Returns the new source, or null when no returnable JSX expression is found.
402
+ function wrapLastReturnInFragment(source, orbJsx) {
403
+ const kw = /\breturn\b/g;
404
+ let m;
405
+ let retIdx = -1;
406
+ while ((m = kw.exec(source))) retIdx = m.index;
407
+ if (retIdx === -1) return null;
408
+
409
+ let i = retIdx + 'return'.length;
410
+ while (i < source.length && /\s/.test(source[i])) i += 1;
411
+ if (source[i] !== '(' && source[i] !== '<') return null;
412
+ const exprStart = i;
413
+
414
+ // Scan to the end of this one expression: stop at an unmatched ')' or '}'
415
+ // (the wrapping paren / function body) or a top-level ';'.
416
+ let round = 0;
417
+ let curly = 0;
418
+ let end = source.length;
419
+ for (let j = exprStart; j < source.length; j += 1) {
420
+ const c = source[j];
421
+ if (c === '(') round += 1;
422
+ else if (c === ')') {
423
+ if (round === 0) { end = j; break; }
424
+ round -= 1;
425
+ } else if (c === '{') curly += 1;
426
+ else if (c === '}') {
427
+ if (curly === 0) { end = j; break; }
428
+ curly -= 1;
429
+ } else if (c === ';' && round === 0 && curly === 0) { end = j; break; }
430
+ }
431
+
432
+ let expr = source.slice(exprStart, end).trim();
433
+ if (expr.startsWith('(') && expr.endsWith(')')) expr = expr.slice(1, -1).trim();
434
+ if (!expr) return null;
435
+
436
+ const ind = ' ';
437
+ const orbIndented = orbJsx
438
+ .split('\n')
439
+ .map((l) => (l ? `${ind} ${l}` : l))
440
+ .join('\n');
441
+ const wrapped =
442
+ `(\n${ind}<>\n${ind} ${expr}\n${orbIndented}\n${ind}</>\n${ind.slice(2)})`;
443
+
444
+ return source.slice(0, exprStart) + wrapped + source.slice(end);
445
+ }
446
+
265
447
  function injectReactReturn(plan) {
266
448
  const { target, framework } = plan;
267
449
  const ts = target.ext === '.tsx';
@@ -281,12 +463,11 @@ function injectReactReturn(plan) {
281
463
  return;
282
464
  }
283
465
  }
284
- const ret = source.lastIndexOf('return (');
285
- if (ret === -1) throw new Error('no return ( found');
286
- const close = source.indexOf(');', ret);
287
- if (close === -1) throw new Error('no return close');
288
- source = `${source.slice(0, close)}\n ${orbBlock}\n ${source.slice(close)}`;
289
- writeFile(target.file, source);
466
+ // No closing-tag anchor (e.g. `return <Component {...pageProps} />`): wrap the
467
+ // returned element + orb in a fragment so adjacent JSX is always valid.
468
+ const wrapped = wrapLastReturnInFragment(source, orbBlock);
469
+ if (wrapped == null) throw new Error('no returnable JSX expression found');
470
+ writeFile(target.file, wrapped);
290
471
  }
291
472
 
292
473
  function injectVueSfc(plan) {
@@ -363,6 +544,9 @@ function injectAngularComponent(plan) {
363
544
 
364
545
  function applyMount(plan) {
365
546
  switch (plan.strategy) {
547
+ case 'entry-bootstrap':
548
+ injectEntryBootstrap(plan);
549
+ break;
366
550
  case 'next-app':
367
551
  injectNextApp(plan);
368
552
  break;
@@ -398,18 +582,17 @@ function writeConsentDoc(appRoot, plan) {
398
582
  const dir = path.join(appRoot, 'src', 'fyodos');
399
583
  fs.mkdirSync(dir, { recursive: true });
400
584
  const doc = [
401
- '# Fiodos orb mount (auto-generated)',
585
+ '# Fiodos orb mount',
402
586
  '',
403
- `Date: ${new Date().toISOString()}`,
404
- `Strategy: ${plan.strategy}`,
405
- `Files edited:`,
406
- ...plan.files.map((f) => `- ${path.relative(appRoot, f)}`),
587
+ 'Mounts `<FiodosAgent />` in your app. The orb connects to the Fiodos backend',
588
+ 'with your project API key and fetches its manifest and appearance on its own;',
589
+ 'nothing else to paste.',
407
590
  '',
408
- 'Changes are wrapped in FYODOS:ORB:START / FYODOS:ORB:END markers.',
409
- 'Remove those blocks (and the Fiodos import) to revert manually.',
591
+ `- Strategy: \`${plan.strategy}\``,
592
+ `- Files: ${plan.files.map((f) => `\`${path.relative(appRoot, f)}\``).join(', ')}`,
410
593
  '',
411
- 'Planned edits:',
412
- ...describePlan(plan).map((l) => `- ${l}`),
594
+ 'Edits sit between `FYODOS:ORB:START` / `FYODOS:ORB:END` markers. Remove those',
595
+ 'blocks (and the Fiodos import) to revert.',
413
596
  '',
414
597
  ].join('\n');
415
598
  writeFile(path.join(dir, 'FYODOS_ORB_MOUNT.md'), doc);
@@ -430,6 +613,10 @@ module.exports = {
430
613
  revertFiles,
431
614
  writeConsentDoc,
432
615
  isAlreadyMounted,
616
+ registryRelImport,
617
+ addRegistriesToMountSource,
618
+ addRegistriesToBootstrapSource,
619
+ buildEntryBootstrapBlock,
433
620
  IMPORT_NAME,
434
621
  WRAPPER_BASENAME,
435
622
  };