@fiodos/cli 0.1.23 → 0.1.25

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
@@ -27,6 +27,8 @@ const {
27
27
  registryRelImport,
28
28
  addRegistriesToMountSource,
29
29
  addRegistriesToBootstrapSource,
30
+ addSessionToMountSource,
31
+ addSessionToBootstrapSource,
30
32
  buildEntryBootstrapBlock,
31
33
  IMPORT_NAME,
32
34
  WRAPPER_BASENAME,
@@ -499,6 +501,104 @@ function connectOrbRegistries(appRoot, opts = {}) {
499
501
  }
500
502
  }
501
503
 
504
+ /**
505
+ * Third pass — connect the generated SESSION module to the mounted orb, so the
506
+ * agent KNOWS whether the user is signed in (requiresAuth gate) and keys the
507
+ * conversation memory per user. Mirrors connectOrbRegistries: idempotent,
508
+ * reversible, never overrides a developer-wired isAuthenticated.
509
+ */
510
+ function connectOrbSession(appRoot, opts = {}) {
511
+ const { sessionFileAbs } = opts;
512
+ if (!sessionFileAbs || !fs.existsSync(sessionFileAbs)) return { status: 'no-session-file' };
513
+
514
+ const framework = detectFramework(appRoot);
515
+ const target = detectTarget(appRoot, framework);
516
+ if (!target || !target.file) return { status: 'no-target' };
517
+
518
+ const bootstrapKinds = new Set(['vue-entry', 'svelte-entry', 'sveltekit-hooks']);
519
+ if (bootstrapKinds.has(target.kind)) {
520
+ let source;
521
+ try {
522
+ source = fs.readFileSync(target.file, 'utf8');
523
+ } catch {
524
+ return { status: 'not-mounted' };
525
+ }
526
+ const importPath = registryRelImport(target.file, sessionFileAbs);
527
+ const fileRel = path.relative(appRoot, target.file);
528
+ const backups = backupFiles([target.file]);
529
+ try {
530
+ const res = addSessionToBootstrapSource(source, importPath);
531
+ if (!res.changed) {
532
+ return { status: res.reason === 'already' ? 'already' : res.reason || 'not-mounted', file: fileRel };
533
+ }
534
+ fs.writeFileSync(target.file, res.source);
535
+ return { status: 'connected', file: fileRel };
536
+ } catch (e) {
537
+ revertFiles(backups);
538
+ return { status: 'failed', file: fileRel, error: e };
539
+ }
540
+ }
541
+
542
+ const reactKinds = new Set(['vite', 'next-pages', 'next-app']);
543
+ if (!reactKinds.has(target.kind)) return { status: 'unsupported-framework', framework };
544
+
545
+ let mountFile = target.file;
546
+ if (target.kind === 'next-app') {
547
+ const dir = path.dirname(target.file);
548
+ mountFile =
549
+ [path.join(dir, `${WRAPPER_BASENAME}.tsx`), path.join(dir, `${WRAPPER_BASENAME}.jsx`)].find((f) =>
550
+ fs.existsSync(f),
551
+ ) || null;
552
+ if (!mountFile) return { status: 'not-mounted' };
553
+ }
554
+
555
+ let source;
556
+ try {
557
+ source = fs.readFileSync(mountFile, 'utf8');
558
+ } catch {
559
+ return { status: 'not-mounted' };
560
+ }
561
+ if (!/<FiodosAgent\b/.test(source)) return { status: 'not-mounted' };
562
+
563
+ const importPath = registryRelImport(mountFile, sessionFileAbs);
564
+ const fileRel = path.relative(appRoot, mountFile);
565
+ const backups = backupFiles([mountFile]);
566
+ try {
567
+ const res = addSessionToMountSource(source, importPath);
568
+ if (!res.changed) {
569
+ return { status: res.reason || 'not-mounted', file: fileRel };
570
+ }
571
+ fs.writeFileSync(mountFile, res.source);
572
+ return { status: 'connected', file: fileRel };
573
+ } catch (e) {
574
+ revertFiles(backups);
575
+ return { status: 'failed', file: fileRel, error: e };
576
+ }
577
+ }
578
+
579
+ function reportSessionConnectResult(result, colors = {}, opts = {}) {
580
+ const quiet = opts.quiet !== false;
581
+ if (!result) return;
582
+ const { blue, cyan, dim, reset } = colors;
583
+ const tag = `${cyan || ''}◉${reset || ''} ${blue || ''}Fiodos${reset || ''}`;
584
+ switch (result.status) {
585
+ case 'connected':
586
+ console.error(`${tag} · session connected to the orb mount (${result.file})`);
587
+ break;
588
+ case 'already':
589
+ if (!quiet) console.error(`${tag} · ${dim || ''}session already connected to the mount${reset || ''}`);
590
+ break;
591
+ case 'dev-wired':
592
+ if (!quiet) {
593
+ console.error(`${tag} · ${dim || ''}the mount already has its own isAuthenticated — left as is${reset || ''}`);
594
+ }
595
+ break;
596
+ default:
597
+ if (!quiet) console.error(`${tag} · ${dim || ''}session not connected to the mount (${result.status})${reset || ''}`);
598
+ break;
599
+ }
600
+ }
601
+
502
602
  function reportRegistriesResult(result, colors = {}, opts = {}) {
503
603
  const quiet = opts.quiet !== false;
504
604
  if (!result) return;
@@ -530,6 +630,8 @@ module.exports = {
530
630
  reportWireResult,
531
631
  connectOrbRegistries,
532
632
  reportRegistriesResult,
633
+ connectOrbSession,
634
+ reportSessionConnectResult,
533
635
  detectFramework,
534
636
  detectTarget,
535
637
  };
@@ -83,19 +83,32 @@ function insertBlockAfterLastImport(source, block) {
83
83
  // FYODOS:ORB markers → idempotent (re-runs strip & rewrite) and reversible.
84
84
  const BOOTSTRAP_ALIAS = '__fiodosCreateAgent';
85
85
  const BOOTSTRAP_REGISTRY_EXPORT = 'fyodosGeneratedRegistries';
86
+ // Exports of the generated session module (see wireSession.js). Connected to
87
+ // the mount automatically so auth awareness needs NO manual step.
88
+ const SESSION_IS_AUTH_EXPORT = 'fyodosIsAuthenticated';
89
+ const SESSION_USER_ID_EXPORT = 'fyodosGetUserId';
86
90
 
87
91
  function buildEntryBootstrapBlock(opts = {}) {
88
- const { registryImportPath } = opts;
92
+ const { registryImportPath, sessionImportPath } = opts;
89
93
  const lines = [];
90
94
  lines.push(ORB_SCRIPT_START);
91
95
  lines.push(`import { createFiodosAgent as ${BOOTSTRAP_ALIAS} } from '@fiodos/web-core';`);
92
96
  if (registryImportPath) {
93
97
  lines.push(`import { ${BOOTSTRAP_REGISTRY_EXPORT} } from '${registryImportPath}';`);
94
98
  }
99
+ if (sessionImportPath) {
100
+ lines.push(
101
+ `import { ${SESSION_IS_AUTH_EXPORT}, ${SESSION_USER_ID_EXPORT} } from '${sessionImportPath}';`,
102
+ );
103
+ }
95
104
  lines.push(`${BOOTSTRAP_ALIAS}({`);
96
105
  lines.push(' apiKey: import.meta.env.VITE_FYODOS_API_KEY,');
97
106
  lines.push(' baseUrl: import.meta.env.VITE_FYODOS_API_URL,');
98
107
  if (registryImportPath) lines.push(` registries: ${BOOTSTRAP_REGISTRY_EXPORT},`);
108
+ if (sessionImportPath) {
109
+ lines.push(` isAuthenticated: ${SESSION_IS_AUTH_EXPORT},`);
110
+ lines.push(` getUserId: ${SESSION_USER_ID_EXPORT},`);
111
+ }
99
112
  lines.push(' mount: true,');
100
113
  lines.push('});');
101
114
  lines.push(ORB_SCRIPT_END);
@@ -123,8 +136,35 @@ function addRegistriesToBootstrapSource(source, registryImportPath) {
123
136
  if (/\n\s*registries:\s/.test(source.slice(source.indexOf(ORB_SCRIPT_START)))) {
124
137
  return { changed: false, source, reason: 'already' };
125
138
  }
139
+ // Preserve a session connection a previous pass already added to the block.
140
+ const sessionImportPath = extractBootstrapImportPath(source, SESSION_IS_AUTH_EXPORT);
141
+ const stripped = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
142
+ const block = buildEntryBootstrapBlock({ registryImportPath, sessionImportPath });
143
+ const next = insertBlockAfterLastImport(stripped.trimEnd(), block);
144
+ return { changed: true, source: next.endsWith('\n') ? next : `${next}\n` };
145
+ }
146
+
147
+ // Import path of the module that exports `name` inside our bootstrap block (or null).
148
+ function extractBootstrapImportPath(source, name) {
149
+ const startIdx = source.indexOf(ORB_SCRIPT_START);
150
+ if (startIdx === -1) return null;
151
+ const endIdx = source.indexOf(ORB_SCRIPT_END, startIdx);
152
+ const block = source.slice(startIdx, endIdx === -1 ? undefined : endIdx);
153
+ const m = block.match(new RegExp(`import\\s*\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`));
154
+ return m ? m[1] : null;
155
+ }
156
+
157
+ // Idempotently rewrite an existing entry-bootstrap block to carry the generated
158
+ // SESSION connection (third pass, after session wiring). Preserves the
159
+ // registries the second pass added. Returns { changed, source, reason? }.
160
+ function addSessionToBootstrapSource(source, sessionImportPath) {
161
+ if (!source.includes(ORB_SCRIPT_START)) return { changed: false, source, reason: 'not-mounted' };
162
+ if (/\n\s*isAuthenticated:\s/.test(source.slice(source.indexOf(ORB_SCRIPT_START)))) {
163
+ return { changed: false, source, reason: 'already' };
164
+ }
165
+ const registryImportPath = extractBootstrapImportPath(source, BOOTSTRAP_REGISTRY_EXPORT);
126
166
  const stripped = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
127
- const block = buildEntryBootstrapBlock({ registryImportPath });
167
+ const block = buildEntryBootstrapBlock({ registryImportPath, sessionImportPath });
128
168
  const next = insertBlockAfterLastImport(stripped.trimEnd(), block);
129
169
  return { changed: true, source: next.endsWith('\n') ? next : `${next}\n` };
130
170
  }
@@ -199,6 +239,41 @@ function addRegistriesToMountSource(source, exportName, importPath) {
199
239
  return { changed: true, source: next };
200
240
  }
201
241
 
242
+ // Idempotently connect the generated SESSION module to the mounted
243
+ // <FiodosAgent/>: adds `isAuthenticated={fyodosIsAuthenticated}` and
244
+ // `getUserId={fyodosGetUserId}` + the import. Skips (never overrides) a mount
245
+ // where the developer already wired their own isAuthenticated. Same
246
+ // reversibility model as addRegistriesToMountSource.
247
+ function addSessionToMountSource(source, importPath) {
248
+ const tagRe = /([ \t]*)<FiodosAgent\b[\s\S]*?\/>/;
249
+ const m = source.match(tagRe);
250
+ if (!m) return { changed: false, source, reason: 'not-mounted' };
251
+ const tag = m[0];
252
+ if (new RegExp(`isAuthenticated=\\{${escapeRe(SESSION_IS_AUTH_EXPORT)}\\}`).test(tag)) {
253
+ return { changed: false, source, reason: 'already' };
254
+ }
255
+ if (/\bisAuthenticated=/.test(tag)) {
256
+ return { changed: false, source, reason: 'dev-wired' };
257
+ }
258
+ const baseIndent = m[1] || '';
259
+ const propMatch = tag.match(/\n([ \t]*)(?:apiKey|api-key|apiUrl|baseUrl|registries)/);
260
+ const propIndent = propMatch ? propMatch[1] : `${baseIndent} `;
261
+ const hasUserId = /\bgetUserId=/.test(tag);
262
+ const props =
263
+ `\n${propIndent}isAuthenticated={${SESSION_IS_AUTH_EXPORT}}` +
264
+ (hasUserId ? '' : `\n${propIndent}getUserId={${SESSION_USER_ID_EXPORT}}`);
265
+ const newTag = tag.replace(/\n?[ \t]*\/>$/, `${props}\n${baseIndent}/>`);
266
+ let next = source.replace(tag, newTag);
267
+ const names = hasUserId
268
+ ? SESSION_IS_AUTH_EXPORT
269
+ : `${SESSION_IS_AUTH_EXPORT}, ${SESSION_USER_ID_EXPORT}`;
270
+ const importLine = `import { ${names} } from '${importPath}';`;
271
+ if (!next.includes(importLine)) {
272
+ next = insertImportAfterLastImport(next, importLine);
273
+ }
274
+ return { changed: true, source: next };
275
+ }
276
+
202
277
  /**
203
278
  * Assess whether we can inject safely. Returns { ok, reason?, strategy, files, rel }.
204
279
  */
@@ -601,13 +676,15 @@ function writeConsentDoc(appRoot, plan) {
601
676
  'Edits sit between `FYODOS:ORB:START` / `FYODOS:ORB:END` markers. Remove those',
602
677
  'blocks (and the Fiodos import) to revert.',
603
678
  '',
604
- '## Apps with login (connect auth + screenthe only manual step)',
679
+ '## Apps with login (auth + identitywired AUTOMATICALLY)',
680
+ '',
681
+ 'For apps with login the installer also detects WHERE your session state lives',
682
+ 'and connects it on its own: it generates `src/fyodos/session.generated.*` and',
683
+ 'adds `isAuthenticated={fyodosIsAuthenticated}` / `getUserId={fyodosGetUserId}`',
684
+ 'to the mount (see `src/fyodos/FYODOS_SESSION.md`). Nothing to do by hand.',
605
685
  '',
606
- 'The orb runs with just `apiKey`/`apiUrl`. But only YOUR app knows whether the',
607
- 'user is signed in and which screen they are on, so for apps with login you',
608
- 'connect that in one place. Without it the agent can loop ("do you want to sign',
609
- "in?\") and can't tell it already signed you in. Add these props to",
610
- '`<FiodosAgent/>` (a commented scaffold is already next to the mount):',
686
+ 'ONLY if the installer reported it could not verify your session source (or you',
687
+ 'want to override it), wire the props yourself:',
611
688
  '',
612
689
  '```tsx',
613
690
  '<FiodosAgent',
@@ -653,6 +730,8 @@ module.exports = {
653
730
  registryRelImport,
654
731
  addRegistriesToMountSource,
655
732
  addRegistriesToBootstrapSource,
733
+ addSessionToMountSource,
734
+ addSessionToBootstrapSource,
656
735
  buildEntryBootstrapBlock,
657
736
  IMPORT_NAME,
658
737
  WRAPPER_BASENAME,