@fiodos/cli 0.1.25 → 0.1.26
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 +6 -4
- package/src/changeRegistry.js +55 -1
- package/src/collect.js +16 -2
- package/src/index.js +367 -5
- package/src/shopifyTheme.js +300 -0
- package/src/verify.js +124 -1
- package/src/wireHandlers.js +93 -7
- package/src/wireScreen.js +562 -0
- package/src/wireSession.js +147 -11
- package/src/wireWeb.js +115 -2
- package/src/wireWebMount.js +98 -14
package/src/wireSession.js
CHANGED
|
@@ -61,14 +61,22 @@ const {
|
|
|
61
61
|
buildBridgeModule,
|
|
62
62
|
insertImportBlock,
|
|
63
63
|
insertRegisterBlock,
|
|
64
|
+
deriveBridgeAnchor,
|
|
64
65
|
} = require('./wireHandlers');
|
|
65
66
|
|
|
66
67
|
const SESSION_BASENAME = 'session.generated';
|
|
67
68
|
const DOC_BASENAME = 'FYODOS_SESSION.md';
|
|
68
69
|
const IS_AUTH_EXPORT = 'fyodosIsAuthenticated';
|
|
69
70
|
const USER_ID_EXPORT = 'fyodosGetUserId';
|
|
71
|
+
const CAPS_EXPORT = 'fyodosGetSessionCapabilities';
|
|
70
72
|
const BRIDGE_IS_AUTH = '__fyodos_is_authenticated';
|
|
71
73
|
const BRIDGE_USER_ID = '__fyodos_user_id';
|
|
74
|
+
const BRIDGE_CAPABILITIES = '__fyodos_capabilities';
|
|
75
|
+
|
|
76
|
+
// Capability flags are short snake_case names ("has_payment_method"). Anything
|
|
77
|
+
// else is dropped: a malformed name could never match the manifest's
|
|
78
|
+
// requiredCapabilities anyway, so wiring it would be dead weight.
|
|
79
|
+
const CAPABILITY_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
72
80
|
|
|
73
81
|
// Session edits carry their OWN markers: wireHandlers' stripFiodosBlocks
|
|
74
82
|
// removes every FYODOS:BRIDGE block in a file, so if the session registration
|
|
@@ -128,11 +136,46 @@ function verifyExpr({ expr, importedNames, extraOk = new Set(), what }) {
|
|
|
128
136
|
return null;
|
|
129
137
|
}
|
|
130
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Verify the AI's fine-grained session capabilities (Eje B audience map:
|
|
141
|
+
* "has_payment_method", "is_admin"…) INDIVIDUALLY: a hallucinated capability
|
|
142
|
+
* is dropped with an honest note while the rest of the session wiring still
|
|
143
|
+
* applies — one bad flag must never cost the whole auth awareness.
|
|
144
|
+
* Returns { caps: [{ name, expr }], notes: [reason...] }.
|
|
145
|
+
*/
|
|
146
|
+
function verifyCapabilities({ rawCapabilities, exprKey, importedNames, extraOk = new Set() }) {
|
|
147
|
+
const caps = [];
|
|
148
|
+
const notes = [];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
for (const c of Array.isArray(rawCapabilities) ? rawCapabilities : []) {
|
|
151
|
+
const name = String((c && c.name) || '').trim();
|
|
152
|
+
const expr = String((c && c[exprKey]) || '').trim();
|
|
153
|
+
if (!CAPABILITY_NAME_RE.test(name)) {
|
|
154
|
+
notes.push(`capability '${name || '(unnamed)'}' dropped: not a short snake_case name`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (seen.has(name)) continue;
|
|
158
|
+
if (!expr) {
|
|
159
|
+
notes.push(`capability '${name}' dropped: no '${exprKey}' expression was provided`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const err = verifyExpr({ expr, importedNames, extraOk, what: `capability '${name}'` });
|
|
163
|
+
if (err) {
|
|
164
|
+
notes.push(`capability '${name}' dropped: ${err}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
seen.add(name);
|
|
168
|
+
caps.push({ name, expr });
|
|
169
|
+
}
|
|
170
|
+
return { caps: caps.slice(0, 8), notes };
|
|
171
|
+
}
|
|
172
|
+
|
|
131
173
|
/**
|
|
132
174
|
* Turn the AI's `session` block into a verified plan, or { reason }.
|
|
133
175
|
* Plan shapes:
|
|
134
|
-
* { strategy:'module', imports, isAuthExpr, userIdExpr|null }
|
|
135
|
-
* { strategy:'bridge', bridge:{ file, anchor, scope, imports, isAuthInvoke, userIdInvoke|null
|
|
176
|
+
* { strategy:'module', imports, isAuthExpr, userIdExpr|null, capabilities, capabilityNotes }
|
|
177
|
+
* { strategy:'bridge', bridge:{ file, anchor, scope, imports, isAuthInvoke, userIdInvoke|null,
|
|
178
|
+
* capabilities }, capabilityNotes }
|
|
136
179
|
*/
|
|
137
180
|
function prepareSessionPlan({ appRoot, session, fyodosDirRel }) {
|
|
138
181
|
if (!session || typeof session !== 'object') return { reason: 'no session block' };
|
|
@@ -153,7 +196,7 @@ function prepareSessionPlan({ appRoot, session, fyodosDirRel }) {
|
|
|
153
196
|
if (session.strategy === 'bridge') {
|
|
154
197
|
const b = session.bridge || {};
|
|
155
198
|
const file = normRel(b.file);
|
|
156
|
-
|
|
199
|
+
let anchor = String(b.anchor || '').trim();
|
|
157
200
|
const isAuthInvoke = String(b.isAuthenticatedInvoke || '').trim();
|
|
158
201
|
const userIdInvoke = String(b.userIdInvoke || '').trim() || null;
|
|
159
202
|
|
|
@@ -161,30 +204,49 @@ function prepareSessionPlan({ appRoot, session, fyodosDirRel }) {
|
|
|
161
204
|
const content = readFileSafe(appRoot, file);
|
|
162
205
|
if (content == null) return { reason: `could not read the component '${file}'` };
|
|
163
206
|
if (!isAuthInvoke) return { reason: `no isAuthenticated expression was provided for '${file}'` };
|
|
164
|
-
if (!anchor) return { reason: `no insertion anchor was provided in '${file}'` };
|
|
165
207
|
// The anchor must be unique in the file WITHOUT counting our own previous
|
|
166
|
-
// insertions (idempotent re-runs).
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
208
|
+
// insertions (idempotent re-runs). When the AI's anchor is missing or
|
|
209
|
+
// ambiguous, derive a deterministic one from the declaration of the symbol
|
|
210
|
+
// the invoke reads (same rescue as handler bridges) instead of giving up —
|
|
211
|
+
// safety is still guaranteed by the compile check + revert below.
|
|
212
|
+
const cleanContent = stripSessionBlocks(content);
|
|
213
|
+
const occ = anchor ? countOccurrences(cleanContent, anchor) : 0;
|
|
214
|
+
if (occ !== 1) {
|
|
215
|
+
const derived = deriveBridgeAnchor(cleanContent, isAuthInvoke);
|
|
216
|
+
if (derived) anchor = derived;
|
|
217
|
+
else if (!anchor || occ === 0) return { reason: `no safe insertion point was found in '${file}'` };
|
|
218
|
+
else return { reason: `the insertion anchor appears ${occ} times in '${file}'; it is ambiguous` };
|
|
219
|
+
}
|
|
170
220
|
|
|
171
221
|
const v = verifyImports({ appRoot, rawImports: b.imports, importBaseDirRel: path.dirname(file) });
|
|
172
222
|
if (v.reason) return v;
|
|
173
223
|
|
|
174
224
|
// Identifiers must be in-file, imported, args-free locals or safe globals.
|
|
175
225
|
const inFileOk = new Set();
|
|
176
|
-
|
|
226
|
+
const capExprs = (Array.isArray(session.capabilities) ? session.capabilities : [])
|
|
227
|
+
.map((c) => String((c && c.invoke) || '').trim())
|
|
228
|
+
.filter(Boolean);
|
|
229
|
+
for (const expr of [isAuthInvoke, userIdInvoke, ...capExprs].filter(Boolean)) {
|
|
177
230
|
for (const id of freeIdentifiers(expr)) {
|
|
178
231
|
if (fileHasSymbol(appRoot, file, id)) inFileOk.add(id);
|
|
179
232
|
}
|
|
233
|
+
}
|
|
234
|
+
for (const expr of [isAuthInvoke, userIdInvoke].filter(Boolean)) {
|
|
180
235
|
const err = verifyExpr({ expr, importedNames: v.importedNames, extraOk: inFileOk, what: 'session' });
|
|
181
236
|
if (err) return { reason: `${err} — in '${file}'` };
|
|
182
237
|
}
|
|
238
|
+
const capsV = verifyCapabilities({
|
|
239
|
+
rawCapabilities: session.capabilities,
|
|
240
|
+
exprKey: 'invoke',
|
|
241
|
+
importedNames: v.importedNames,
|
|
242
|
+
extraOk: inFileOk,
|
|
243
|
+
});
|
|
183
244
|
|
|
184
245
|
return {
|
|
185
246
|
plan: {
|
|
186
247
|
strategy: 'bridge',
|
|
187
248
|
reason: String(session.reason || ''),
|
|
249
|
+
capabilityNotes: capsV.notes,
|
|
188
250
|
bridge: {
|
|
189
251
|
file,
|
|
190
252
|
anchor,
|
|
@@ -192,6 +254,7 @@ function prepareSessionPlan({ appRoot, session, fyodosDirRel }) {
|
|
|
192
254
|
imports: v.imports,
|
|
193
255
|
isAuthInvoke,
|
|
194
256
|
userIdInvoke,
|
|
257
|
+
capabilities: capsV.caps,
|
|
195
258
|
importPathToBridge: relImport(path.dirname(file), path.join(fyodosDirRel, BRIDGE_BASENAME)),
|
|
196
259
|
},
|
|
197
260
|
},
|
|
@@ -209,6 +272,11 @@ function prepareSessionPlan({ appRoot, session, fyodosDirRel }) {
|
|
|
209
272
|
const err = verifyExpr({ expr, importedNames: v.importedNames, what: 'session' });
|
|
210
273
|
if (err) return { reason: err };
|
|
211
274
|
}
|
|
275
|
+
const capsV = verifyCapabilities({
|
|
276
|
+
rawCapabilities: session.capabilities,
|
|
277
|
+
exprKey: 'call',
|
|
278
|
+
importedNames: v.importedNames,
|
|
279
|
+
});
|
|
212
280
|
|
|
213
281
|
return {
|
|
214
282
|
plan: {
|
|
@@ -217,6 +285,8 @@ function prepareSessionPlan({ appRoot, session, fyodosDirRel }) {
|
|
|
217
285
|
imports: v.imports,
|
|
218
286
|
isAuthExpr,
|
|
219
287
|
userIdExpr,
|
|
288
|
+
capabilities: capsV.caps,
|
|
289
|
+
capabilityNotes: capsV.notes,
|
|
220
290
|
},
|
|
221
291
|
};
|
|
222
292
|
}
|
|
@@ -240,6 +310,8 @@ function buildSessionModule(plan, opts = {}) {
|
|
|
240
310
|
const esm = opts.esm !== false;
|
|
241
311
|
const boolRet = ts ? ': boolean | null' : '';
|
|
242
312
|
const strRet = ts ? ': string | null' : '';
|
|
313
|
+
const capsRet = ts ? ': Record<string, boolean> | null' : '';
|
|
314
|
+
const capsDecl = ts ? ': Record<string, boolean>' : '';
|
|
243
315
|
|
|
244
316
|
const L = [];
|
|
245
317
|
L.push(
|
|
@@ -282,6 +354,27 @@ function buildSessionModule(plan, opts = {}) {
|
|
|
282
354
|
L.push(' return null;');
|
|
283
355
|
L.push(' }');
|
|
284
356
|
L.push('}');
|
|
357
|
+
L.push('');
|
|
358
|
+
L.push('/**');
|
|
359
|
+
L.push(' * Fine-grained session capability flags (audience map: has_payment_method,');
|
|
360
|
+
L.push(' * is_admin…). null = UNKNOWN — the orb then filters NOTHING (fail-open).');
|
|
361
|
+
L.push(' * Boolean flags only; never counts, names or any user data.');
|
|
362
|
+
L.push(' */');
|
|
363
|
+
L.push(`export function ${CAPS_EXPORT}()${capsRet} {`);
|
|
364
|
+
L.push(' try {');
|
|
365
|
+
L.push(` const fn = getFiodosBridge()[${JSON.stringify(BRIDGE_CAPABILITIES)}];`);
|
|
366
|
+
L.push(" if (typeof fn !== 'function') return null;");
|
|
367
|
+
L.push(' const raw = fn();');
|
|
368
|
+
L.push(" if (!raw || typeof raw !== 'object') return null;");
|
|
369
|
+
L.push(` const caps${capsDecl} = {};`);
|
|
370
|
+
L.push(' for (const k of Object.keys(raw)) {');
|
|
371
|
+
L.push(" if (typeof raw[k] === 'boolean') caps[k] = raw[k];");
|
|
372
|
+
L.push(' }');
|
|
373
|
+
L.push(' return Object.keys(caps).length > 0 ? caps : null;');
|
|
374
|
+
L.push(' } catch {');
|
|
375
|
+
L.push(' return null;');
|
|
376
|
+
L.push(' }');
|
|
377
|
+
L.push('}');
|
|
285
378
|
} else {
|
|
286
379
|
for (const imp of plan.imports || []) L.push(importLine(imp, esm));
|
|
287
380
|
if ((plan.imports || []).length) L.push('');
|
|
@@ -310,9 +403,33 @@ function buildSessionModule(plan, opts = {}) {
|
|
|
310
403
|
L.push(' return null;');
|
|
311
404
|
}
|
|
312
405
|
L.push('}');
|
|
406
|
+
L.push('');
|
|
407
|
+
L.push('/**');
|
|
408
|
+
L.push(' * Fine-grained session capability flags (audience map: has_payment_method,');
|
|
409
|
+
L.push(' * is_admin…). null = UNKNOWN — the orb then filters NOTHING (fail-open).');
|
|
410
|
+
L.push(' * Boolean flags only; never counts, names or any user data.');
|
|
411
|
+
L.push(' */');
|
|
412
|
+
L.push(`export function ${CAPS_EXPORT}()${capsRet} {`);
|
|
413
|
+
const moduleCaps = Array.isArray(plan.capabilities) ? plan.capabilities : [];
|
|
414
|
+
if (moduleCaps.length > 0) {
|
|
415
|
+
L.push(` const caps${capsDecl} = {};`);
|
|
416
|
+
for (const c of moduleCaps) {
|
|
417
|
+
// Each flag is isolated: one throwing read stays UNKNOWN (omitted)
|
|
418
|
+
// instead of blanking every other capability.
|
|
419
|
+
L.push(' try {');
|
|
420
|
+
L.push(` caps[${JSON.stringify(c.name)}] = Boolean(${c.expr});`);
|
|
421
|
+
L.push(' } catch {');
|
|
422
|
+
L.push(' /* unknown — omitted (fail-open) */');
|
|
423
|
+
L.push(' }');
|
|
424
|
+
}
|
|
425
|
+
L.push(' return Object.keys(caps).length > 0 ? caps : null;');
|
|
426
|
+
} else {
|
|
427
|
+
L.push(' return null;');
|
|
428
|
+
}
|
|
429
|
+
L.push('}');
|
|
313
430
|
}
|
|
314
431
|
L.push('');
|
|
315
|
-
if (!esm) L.push(`module.exports = { ${IS_AUTH_EXPORT}, ${USER_ID_EXPORT} };`);
|
|
432
|
+
if (!esm) L.push(`module.exports = { ${IS_AUTH_EXPORT}, ${USER_ID_EXPORT}, ${CAPS_EXPORT} };`);
|
|
316
433
|
let body = L.join('\n');
|
|
317
434
|
if (!esm) {
|
|
318
435
|
// CommonJS build: `export function` is invalid — rewrite to plain functions.
|
|
@@ -329,6 +446,11 @@ function planSessionComponentEdit(appRoot, plan) {
|
|
|
329
446
|
|
|
330
447
|
const methods = [` ${JSON.stringify(BRIDGE_IS_AUTH)}: () => (${b.isAuthInvoke}),`];
|
|
331
448
|
if (b.userIdInvoke) methods.push(` ${JSON.stringify(BRIDGE_USER_ID)}: () => (${b.userIdInvoke}),`);
|
|
449
|
+
const caps = Array.isArray(b.capabilities) ? b.capabilities : [];
|
|
450
|
+
if (caps.length > 0) {
|
|
451
|
+
const entries = caps.map((c) => `${JSON.stringify(c.name)}: Boolean(${c.expr})`).join(', ');
|
|
452
|
+
methods.push(` ${JSON.stringify(BRIDGE_CAPABILITIES)}: () => ({ ${entries} }),`);
|
|
453
|
+
}
|
|
332
454
|
|
|
333
455
|
const importLines = [`import { registerFiodosBridge } from '${b.importPathToBridge}';`];
|
|
334
456
|
for (const imp of b.imports || []) importLines.push(importLine(imp, true));
|
|
@@ -399,10 +521,22 @@ function buildSessionDoc(plan, ctx = {}) {
|
|
|
399
521
|
L.push('');
|
|
400
522
|
L.push(`- App: \`${appName}\``);
|
|
401
523
|
L.push(`- Strategy: \`${plan.strategy}\`${plan.reason ? ` — ${plan.reason}` : ''}`);
|
|
402
|
-
L.push(`- Creates \`${sessionRel}\` — exports \`${IS_AUTH_EXPORT}()\` and \`${
|
|
524
|
+
L.push(`- Creates \`${sessionRel}\` — exports \`${IS_AUTH_EXPORT}()\`, \`${USER_ID_EXPORT}()\` and \`${CAPS_EXPORT}()\`.`);
|
|
403
525
|
if (editedFile) {
|
|
404
526
|
L.push(`- Edits \`${editedFile}\` (only reversible \`FYODOS:SESSION\` blocks) to register the live session.`);
|
|
405
527
|
}
|
|
528
|
+
const planCaps =
|
|
529
|
+
plan.strategy === 'bridge'
|
|
530
|
+
? (plan.bridge && plan.bridge.capabilities) || []
|
|
531
|
+
: plan.capabilities || [];
|
|
532
|
+
if (planCaps.length > 0) {
|
|
533
|
+
L.push(
|
|
534
|
+
`- Capability flags (audience map): ${planCaps.map((c) => `\`${c.name}\``).join(', ')} — boolean-only, read live from the same session source. Actions whose \`requiredCapabilities\` report false are hidden from that user's orb.`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
for (const note of plan.capabilityNotes || []) {
|
|
538
|
+
L.push(`- Capability left out: ${note}.`);
|
|
539
|
+
}
|
|
406
540
|
L.push(`- Verification: ${verified ? 'build check passed' : verifyNote || 'compile check unavailable — applied with the mechanical symbol verification only'}.`);
|
|
407
541
|
L.push('');
|
|
408
542
|
L.push('## Honesty contract (tri-state)');
|
|
@@ -612,8 +746,10 @@ module.exports = {
|
|
|
612
746
|
SESSION_BASENAME,
|
|
613
747
|
IS_AUTH_EXPORT,
|
|
614
748
|
USER_ID_EXPORT,
|
|
749
|
+
CAPS_EXPORT,
|
|
615
750
|
BRIDGE_IS_AUTH,
|
|
616
751
|
BRIDGE_USER_ID,
|
|
752
|
+
BRIDGE_CAPABILITIES,
|
|
617
753
|
SESSION_EDIT_START,
|
|
618
754
|
SESSION_EDIT_END,
|
|
619
755
|
};
|
package/src/wireWeb.js
CHANGED
|
@@ -29,6 +29,8 @@ const {
|
|
|
29
29
|
addRegistriesToBootstrapSource,
|
|
30
30
|
addSessionToMountSource,
|
|
31
31
|
addSessionToBootstrapSource,
|
|
32
|
+
addScreenToMountSource,
|
|
33
|
+
addScreenToBootstrapSource,
|
|
32
34
|
buildEntryBootstrapBlock,
|
|
33
35
|
IMPORT_NAME,
|
|
34
36
|
WRAPPER_BASENAME,
|
|
@@ -511,6 +513,17 @@ function connectOrbSession(appRoot, opts = {}) {
|
|
|
511
513
|
const { sessionFileAbs } = opts;
|
|
512
514
|
if (!sessionFileAbs || !fs.existsSync(sessionFileAbs)) return { status: 'no-session-file' };
|
|
513
515
|
|
|
516
|
+
// Only connect the capability getter when the generated module actually
|
|
517
|
+
// exports it — referencing a missing export would break the app's build.
|
|
518
|
+
let sessionHasCapabilities = false;
|
|
519
|
+
try {
|
|
520
|
+
sessionHasCapabilities = fs
|
|
521
|
+
.readFileSync(sessionFileAbs, 'utf8')
|
|
522
|
+
.includes('fyodosGetSessionCapabilities');
|
|
523
|
+
} catch {
|
|
524
|
+
sessionHasCapabilities = false;
|
|
525
|
+
}
|
|
526
|
+
|
|
514
527
|
const framework = detectFramework(appRoot);
|
|
515
528
|
const target = detectTarget(appRoot, framework);
|
|
516
529
|
if (!target || !target.file) return { status: 'no-target' };
|
|
@@ -527,7 +540,7 @@ function connectOrbSession(appRoot, opts = {}) {
|
|
|
527
540
|
const fileRel = path.relative(appRoot, target.file);
|
|
528
541
|
const backups = backupFiles([target.file]);
|
|
529
542
|
try {
|
|
530
|
-
const res = addSessionToBootstrapSource(source, importPath);
|
|
543
|
+
const res = addSessionToBootstrapSource(source, importPath, { sessionHasCapabilities });
|
|
531
544
|
if (!res.changed) {
|
|
532
545
|
return { status: res.reason === 'already' ? 'already' : res.reason || 'not-mounted', file: fileRel };
|
|
533
546
|
}
|
|
@@ -564,7 +577,82 @@ function connectOrbSession(appRoot, opts = {}) {
|
|
|
564
577
|
const fileRel = path.relative(appRoot, mountFile);
|
|
565
578
|
const backups = backupFiles([mountFile]);
|
|
566
579
|
try {
|
|
567
|
-
const res = addSessionToMountSource(source, importPath);
|
|
580
|
+
const res = addSessionToMountSource(source, importPath, { sessionHasCapabilities });
|
|
581
|
+
if (!res.changed) {
|
|
582
|
+
return { status: res.reason || 'not-mounted', file: fileRel };
|
|
583
|
+
}
|
|
584
|
+
fs.writeFileSync(mountFile, res.source);
|
|
585
|
+
return { status: 'connected', file: fileRel };
|
|
586
|
+
} catch (e) {
|
|
587
|
+
revertFiles(backups);
|
|
588
|
+
return { status: 'failed', file: fileRel, error: e };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Fourth pass — connect the generated SCREEN-TRUTH module to the mounted orb,
|
|
594
|
+
* so the agent knows WHICH SCREEN the user is on even when screens are client
|
|
595
|
+
* state instead of URLs. Mirrors connectOrbSession: idempotent, reversible,
|
|
596
|
+
* never overrides a developer-wired getCurrentRoute.
|
|
597
|
+
*/
|
|
598
|
+
function connectOrbScreen(appRoot, opts = {}) {
|
|
599
|
+
const { screenFileAbs } = opts;
|
|
600
|
+
if (!screenFileAbs || !fs.existsSync(screenFileAbs)) return { status: 'no-screen-file' };
|
|
601
|
+
|
|
602
|
+
const framework = detectFramework(appRoot);
|
|
603
|
+
const target = detectTarget(appRoot, framework);
|
|
604
|
+
if (!target || !target.file) return { status: 'no-target' };
|
|
605
|
+
|
|
606
|
+
const bootstrapKinds = new Set(['vue-entry', 'svelte-entry', 'sveltekit-hooks']);
|
|
607
|
+
if (bootstrapKinds.has(target.kind)) {
|
|
608
|
+
let source;
|
|
609
|
+
try {
|
|
610
|
+
source = fs.readFileSync(target.file, 'utf8');
|
|
611
|
+
} catch {
|
|
612
|
+
return { status: 'not-mounted' };
|
|
613
|
+
}
|
|
614
|
+
const importPath = registryRelImport(target.file, screenFileAbs);
|
|
615
|
+
const fileRel = path.relative(appRoot, target.file);
|
|
616
|
+
const backups = backupFiles([target.file]);
|
|
617
|
+
try {
|
|
618
|
+
const res = addScreenToBootstrapSource(source, importPath);
|
|
619
|
+
if (!res.changed) {
|
|
620
|
+
return { status: res.reason === 'already' ? 'already' : res.reason || 'not-mounted', file: fileRel };
|
|
621
|
+
}
|
|
622
|
+
fs.writeFileSync(target.file, res.source);
|
|
623
|
+
return { status: 'connected', file: fileRel };
|
|
624
|
+
} catch (e) {
|
|
625
|
+
revertFiles(backups);
|
|
626
|
+
return { status: 'failed', file: fileRel, error: e };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const reactKinds = new Set(['vite', 'next-pages', 'next-app']);
|
|
631
|
+
if (!reactKinds.has(target.kind)) return { status: 'unsupported-framework', framework };
|
|
632
|
+
|
|
633
|
+
let mountFile = target.file;
|
|
634
|
+
if (target.kind === 'next-app') {
|
|
635
|
+
const dir = path.dirname(target.file);
|
|
636
|
+
mountFile =
|
|
637
|
+
[path.join(dir, `${WRAPPER_BASENAME}.tsx`), path.join(dir, `${WRAPPER_BASENAME}.jsx`)].find((f) =>
|
|
638
|
+
fs.existsSync(f),
|
|
639
|
+
) || null;
|
|
640
|
+
if (!mountFile) return { status: 'not-mounted' };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let source;
|
|
644
|
+
try {
|
|
645
|
+
source = fs.readFileSync(mountFile, 'utf8');
|
|
646
|
+
} catch {
|
|
647
|
+
return { status: 'not-mounted' };
|
|
648
|
+
}
|
|
649
|
+
if (!/<FiodosAgent\b/.test(source)) return { status: 'not-mounted' };
|
|
650
|
+
|
|
651
|
+
const importPath = registryRelImport(mountFile, screenFileAbs);
|
|
652
|
+
const fileRel = path.relative(appRoot, mountFile);
|
|
653
|
+
const backups = backupFiles([mountFile]);
|
|
654
|
+
try {
|
|
655
|
+
const res = addScreenToMountSource(source, importPath);
|
|
568
656
|
if (!res.changed) {
|
|
569
657
|
return { status: res.reason || 'not-mounted', file: fileRel };
|
|
570
658
|
}
|
|
@@ -576,6 +664,29 @@ function connectOrbSession(appRoot, opts = {}) {
|
|
|
576
664
|
}
|
|
577
665
|
}
|
|
578
666
|
|
|
667
|
+
function reportScreenConnectResult(result, colors = {}, opts = {}) {
|
|
668
|
+
const quiet = opts.quiet !== false;
|
|
669
|
+
if (!result) return;
|
|
670
|
+
const { blue, cyan, dim, reset } = colors;
|
|
671
|
+
const tag = `${cyan || ''}◉${reset || ''} ${blue || ''}Fiodos${reset || ''}`;
|
|
672
|
+
switch (result.status) {
|
|
673
|
+
case 'connected':
|
|
674
|
+
console.error(`${tag} · screen truth connected to the orb mount (${result.file})`);
|
|
675
|
+
break;
|
|
676
|
+
case 'already':
|
|
677
|
+
if (!quiet) console.error(`${tag} · ${dim || ''}screen truth already connected to the mount${reset || ''}`);
|
|
678
|
+
break;
|
|
679
|
+
case 'dev-wired':
|
|
680
|
+
if (!quiet) {
|
|
681
|
+
console.error(`${tag} · ${dim || ''}the mount already has its own getCurrentRoute — left as is${reset || ''}`);
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
default:
|
|
685
|
+
if (!quiet) console.error(`${tag} · ${dim || ''}screen truth not connected to the mount (${result.status})${reset || ''}`);
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
579
690
|
function reportSessionConnectResult(result, colors = {}, opts = {}) {
|
|
580
691
|
const quiet = opts.quiet !== false;
|
|
581
692
|
if (!result) return;
|
|
@@ -632,6 +743,8 @@ module.exports = {
|
|
|
632
743
|
reportRegistriesResult,
|
|
633
744
|
connectOrbSession,
|
|
634
745
|
reportSessionConnectResult,
|
|
746
|
+
connectOrbScreen,
|
|
747
|
+
reportScreenConnectResult,
|
|
635
748
|
detectFramework,
|
|
636
749
|
detectTarget,
|
|
637
750
|
};
|
package/src/wireWebMount.js
CHANGED
|
@@ -87,9 +87,15 @@ const BOOTSTRAP_REGISTRY_EXPORT = 'fyodosGeneratedRegistries';
|
|
|
87
87
|
// the mount automatically so auth awareness needs NO manual step.
|
|
88
88
|
const SESSION_IS_AUTH_EXPORT = 'fyodosIsAuthenticated';
|
|
89
89
|
const SESSION_USER_ID_EXPORT = 'fyodosGetUserId';
|
|
90
|
+
// Capability flags (audience map) — only connected when the generated session
|
|
91
|
+
// module actually exports it (older generated files do not).
|
|
92
|
+
const SESSION_CAPS_EXPORT = 'fyodosGetSessionCapabilities';
|
|
93
|
+
// Export of the generated screen-truth module (see wireScreen.js). Connected
|
|
94
|
+
// to the mount automatically so state-driven screen awareness needs NO manual step.
|
|
95
|
+
const SCREEN_ROUTE_EXPORT = 'fyodosGetCurrentRoute';
|
|
90
96
|
|
|
91
97
|
function buildEntryBootstrapBlock(opts = {}) {
|
|
92
|
-
const { registryImportPath, sessionImportPath } = opts;
|
|
98
|
+
const { registryImportPath, sessionImportPath, screenImportPath, sessionHasCapabilities } = opts;
|
|
93
99
|
const lines = [];
|
|
94
100
|
lines.push(ORB_SCRIPT_START);
|
|
95
101
|
lines.push(`import { createFiodosAgent as ${BOOTSTRAP_ALIAS} } from '@fiodos/web-core';`);
|
|
@@ -97,9 +103,12 @@ function buildEntryBootstrapBlock(opts = {}) {
|
|
|
97
103
|
lines.push(`import { ${BOOTSTRAP_REGISTRY_EXPORT} } from '${registryImportPath}';`);
|
|
98
104
|
}
|
|
99
105
|
if (sessionImportPath) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
);
|
|
106
|
+
const sessionNames = [SESSION_IS_AUTH_EXPORT, SESSION_USER_ID_EXPORT];
|
|
107
|
+
if (sessionHasCapabilities) sessionNames.push(SESSION_CAPS_EXPORT);
|
|
108
|
+
lines.push(`import { ${sessionNames.join(', ')} } from '${sessionImportPath}';`);
|
|
109
|
+
}
|
|
110
|
+
if (screenImportPath) {
|
|
111
|
+
lines.push(`import { ${SCREEN_ROUTE_EXPORT} } from '${screenImportPath}';`);
|
|
103
112
|
}
|
|
104
113
|
lines.push(`${BOOTSTRAP_ALIAS}({`);
|
|
105
114
|
lines.push(' apiKey: import.meta.env.VITE_FYODOS_API_KEY,');
|
|
@@ -108,7 +117,11 @@ function buildEntryBootstrapBlock(opts = {}) {
|
|
|
108
117
|
if (sessionImportPath) {
|
|
109
118
|
lines.push(` isAuthenticated: ${SESSION_IS_AUTH_EXPORT},`);
|
|
110
119
|
lines.push(` getUserId: ${SESSION_USER_ID_EXPORT},`);
|
|
120
|
+
if (sessionHasCapabilities) {
|
|
121
|
+
lines.push(` getSessionCapabilities: ${SESSION_CAPS_EXPORT},`);
|
|
122
|
+
}
|
|
111
123
|
}
|
|
124
|
+
if (screenImportPath) lines.push(` getCurrentRoute: ${SCREEN_ROUTE_EXPORT},`);
|
|
112
125
|
lines.push(' mount: true,');
|
|
113
126
|
lines.push('});');
|
|
114
127
|
lines.push(ORB_SCRIPT_END);
|
|
@@ -136,10 +149,17 @@ function addRegistriesToBootstrapSource(source, registryImportPath) {
|
|
|
136
149
|
if (/\n\s*registries:\s/.test(source.slice(source.indexOf(ORB_SCRIPT_START)))) {
|
|
137
150
|
return { changed: false, source, reason: 'already' };
|
|
138
151
|
}
|
|
139
|
-
// Preserve
|
|
152
|
+
// Preserve session/screen connections previous passes already added.
|
|
140
153
|
const sessionImportPath = extractBootstrapImportPath(source, SESSION_IS_AUTH_EXPORT);
|
|
154
|
+
const sessionHasCapabilities = Boolean(extractBootstrapImportPath(source, SESSION_CAPS_EXPORT));
|
|
155
|
+
const screenImportPath = extractBootstrapImportPath(source, SCREEN_ROUTE_EXPORT);
|
|
141
156
|
const stripped = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
|
|
142
|
-
const block = buildEntryBootstrapBlock({
|
|
157
|
+
const block = buildEntryBootstrapBlock({
|
|
158
|
+
registryImportPath,
|
|
159
|
+
sessionImportPath,
|
|
160
|
+
screenImportPath,
|
|
161
|
+
sessionHasCapabilities,
|
|
162
|
+
});
|
|
143
163
|
const next = insertBlockAfterLastImport(stripped.trimEnd(), block);
|
|
144
164
|
return { changed: true, source: next.endsWith('\n') ? next : `${next}\n` };
|
|
145
165
|
}
|
|
@@ -157,14 +177,42 @@ function extractBootstrapImportPath(source, name) {
|
|
|
157
177
|
// Idempotently rewrite an existing entry-bootstrap block to carry the generated
|
|
158
178
|
// SESSION connection (third pass, after session wiring). Preserves the
|
|
159
179
|
// registries the second pass added. Returns { changed, source, reason? }.
|
|
160
|
-
function addSessionToBootstrapSource(source, sessionImportPath) {
|
|
180
|
+
function addSessionToBootstrapSource(source, sessionImportPath, opts = {}) {
|
|
161
181
|
if (!source.includes(ORB_SCRIPT_START)) return { changed: false, source, reason: 'not-mounted' };
|
|
162
182
|
if (/\n\s*isAuthenticated:\s/.test(source.slice(source.indexOf(ORB_SCRIPT_START)))) {
|
|
163
183
|
return { changed: false, source, reason: 'already' };
|
|
164
184
|
}
|
|
165
185
|
const registryImportPath = extractBootstrapImportPath(source, BOOTSTRAP_REGISTRY_EXPORT);
|
|
186
|
+
const screenImportPath = extractBootstrapImportPath(source, SCREEN_ROUTE_EXPORT);
|
|
187
|
+
const stripped = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
|
|
188
|
+
const block = buildEntryBootstrapBlock({
|
|
189
|
+
registryImportPath,
|
|
190
|
+
sessionImportPath,
|
|
191
|
+
screenImportPath,
|
|
192
|
+
sessionHasCapabilities: Boolean(opts.sessionHasCapabilities),
|
|
193
|
+
});
|
|
194
|
+
const next = insertBlockAfterLastImport(stripped.trimEnd(), block);
|
|
195
|
+
return { changed: true, source: next.endsWith('\n') ? next : `${next}\n` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Idempotently rewrite an existing entry-bootstrap block to carry the generated
|
|
199
|
+
// SCREEN-TRUTH connection (fourth pass, after screen wiring). Preserves the
|
|
200
|
+
// registries and session earlier passes added. Returns { changed, source, reason? }.
|
|
201
|
+
function addScreenToBootstrapSource(source, screenImportPath) {
|
|
202
|
+
if (!source.includes(ORB_SCRIPT_START)) return { changed: false, source, reason: 'not-mounted' };
|
|
203
|
+
if (/\n\s*getCurrentRoute:\s/.test(source.slice(source.indexOf(ORB_SCRIPT_START)))) {
|
|
204
|
+
return { changed: false, source, reason: 'already' };
|
|
205
|
+
}
|
|
206
|
+
const registryImportPath = extractBootstrapImportPath(source, BOOTSTRAP_REGISTRY_EXPORT);
|
|
207
|
+
const sessionImportPath = extractBootstrapImportPath(source, SESSION_IS_AUTH_EXPORT);
|
|
208
|
+
const sessionHasCapabilities = Boolean(extractBootstrapImportPath(source, SESSION_CAPS_EXPORT));
|
|
166
209
|
const stripped = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
|
|
167
|
-
const block = buildEntryBootstrapBlock({
|
|
210
|
+
const block = buildEntryBootstrapBlock({
|
|
211
|
+
registryImportPath,
|
|
212
|
+
sessionImportPath,
|
|
213
|
+
screenImportPath,
|
|
214
|
+
sessionHasCapabilities,
|
|
215
|
+
});
|
|
168
216
|
const next = insertBlockAfterLastImport(stripped.trimEnd(), block);
|
|
169
217
|
return { changed: true, source: next.endsWith('\n') ? next : `${next}\n` };
|
|
170
218
|
}
|
|
@@ -244,7 +292,8 @@ function addRegistriesToMountSource(source, exportName, importPath) {
|
|
|
244
292
|
// `getUserId={fyodosGetUserId}` + the import. Skips (never overrides) a mount
|
|
245
293
|
// where the developer already wired their own isAuthenticated. Same
|
|
246
294
|
// reversibility model as addRegistriesToMountSource.
|
|
247
|
-
function addSessionToMountSource(source, importPath) {
|
|
295
|
+
function addSessionToMountSource(source, importPath, opts = {}) {
|
|
296
|
+
const withCapabilities = Boolean(opts.sessionHasCapabilities);
|
|
248
297
|
const tagRe = /([ \t]*)<FiodosAgent\b[\s\S]*?\/>/;
|
|
249
298
|
const m = source.match(tagRe);
|
|
250
299
|
if (!m) return { changed: false, source, reason: 'not-mounted' };
|
|
@@ -259,15 +308,47 @@ function addSessionToMountSource(source, importPath) {
|
|
|
259
308
|
const propMatch = tag.match(/\n([ \t]*)(?:apiKey|api-key|apiUrl|baseUrl|registries)/);
|
|
260
309
|
const propIndent = propMatch ? propMatch[1] : `${baseIndent} `;
|
|
261
310
|
const hasUserId = /\bgetUserId=/.test(tag);
|
|
311
|
+
const addCaps = withCapabilities && !/\bgetSessionCapabilities=/.test(tag);
|
|
262
312
|
const props =
|
|
263
313
|
`\n${propIndent}isAuthenticated={${SESSION_IS_AUTH_EXPORT}}` +
|
|
264
|
-
(hasUserId ? '' : `\n${propIndent}getUserId={${SESSION_USER_ID_EXPORT}}`)
|
|
314
|
+
(hasUserId ? '' : `\n${propIndent}getUserId={${SESSION_USER_ID_EXPORT}}`) +
|
|
315
|
+
(addCaps ? `\n${propIndent}getSessionCapabilities={${SESSION_CAPS_EXPORT}}` : '');
|
|
265
316
|
const newTag = tag.replace(/\n?[ \t]*\/>$/, `${props}\n${baseIndent}/>`);
|
|
266
317
|
let next = source.replace(tag, newTag);
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const importLine = `import { ${
|
|
318
|
+
const nameList = [SESSION_IS_AUTH_EXPORT];
|
|
319
|
+
if (!hasUserId) nameList.push(SESSION_USER_ID_EXPORT);
|
|
320
|
+
if (addCaps) nameList.push(SESSION_CAPS_EXPORT);
|
|
321
|
+
const importLine = `import { ${nameList.join(', ')} } from '${importPath}';`;
|
|
322
|
+
if (!next.includes(importLine)) {
|
|
323
|
+
next = insertImportAfterLastImport(next, importLine);
|
|
324
|
+
}
|
|
325
|
+
return { changed: true, source: next };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Idempotently connect the generated SCREEN-TRUTH module to the mounted
|
|
329
|
+
// <FiodosAgent/>: adds `getCurrentRoute={fyodosGetCurrentRoute}` + the import.
|
|
330
|
+
// Skips (never overrides) a mount where the developer already wired their own
|
|
331
|
+
// getCurrentRoute. Same reversibility model as addSessionToMountSource.
|
|
332
|
+
function addScreenToMountSource(source, importPath) {
|
|
333
|
+
const tagRe = /([ \t]*)<FiodosAgent\b[\s\S]*?\/>/;
|
|
334
|
+
const m = source.match(tagRe);
|
|
335
|
+
if (!m) return { changed: false, source, reason: 'not-mounted' };
|
|
336
|
+
const tag = m[0];
|
|
337
|
+
if (new RegExp(`getCurrentRoute=\\{${escapeRe(SCREEN_ROUTE_EXPORT)}\\}`).test(tag)) {
|
|
338
|
+
return { changed: false, source, reason: 'already' };
|
|
339
|
+
}
|
|
340
|
+
if (/\bgetCurrentRoute=/.test(tag)) {
|
|
341
|
+
return { changed: false, source, reason: 'dev-wired' };
|
|
342
|
+
}
|
|
343
|
+
const baseIndent = m[1] || '';
|
|
344
|
+
const propMatch = tag.match(/\n([ \t]*)(?:apiKey|api-key|apiUrl|baseUrl|registries)/);
|
|
345
|
+
const propIndent = propMatch ? propMatch[1] : `${baseIndent} `;
|
|
346
|
+
const newTag = tag.replace(
|
|
347
|
+
/\n?[ \t]*\/>$/,
|
|
348
|
+
`\n${propIndent}getCurrentRoute={${SCREEN_ROUTE_EXPORT}}\n${baseIndent}/>`,
|
|
349
|
+
);
|
|
350
|
+
let next = source.replace(tag, newTag);
|
|
351
|
+
const importLine = `import { ${SCREEN_ROUTE_EXPORT} } from '${importPath}';`;
|
|
271
352
|
if (!next.includes(importLine)) {
|
|
272
353
|
next = insertImportAfterLastImport(next, importLine);
|
|
273
354
|
}
|
|
@@ -732,7 +813,10 @@ module.exports = {
|
|
|
732
813
|
addRegistriesToBootstrapSource,
|
|
733
814
|
addSessionToMountSource,
|
|
734
815
|
addSessionToBootstrapSource,
|
|
816
|
+
addScreenToMountSource,
|
|
817
|
+
addScreenToBootstrapSource,
|
|
735
818
|
buildEntryBootstrapBlock,
|
|
736
819
|
IMPORT_NAME,
|
|
737
820
|
WRAPPER_BASENAME,
|
|
821
|
+
SESSION_CAPS_EXPORT,
|
|
738
822
|
};
|