@fiodos/cli 0.1.22 → 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/package.json +5 -3
- package/src/aiAnalyze.js +151 -274
- package/src/changeRegistry.js +57 -6
- package/src/index.js +338 -5
- package/src/odata.js +529 -0
- package/src/wireHandlers.js +120 -34
- package/src/wireReactNative.js +52 -0
- package/src/wireSession.js +619 -0
- package/src/wireWeb.js +102 -0
- package/src/wireWebMount.js +91 -8
- package/src/llm.js +0 -144
package/src/wireHandlers.js
CHANGED
|
@@ -49,7 +49,10 @@ const REGISTRY_EXPORT = 'fyodosGeneratedRegistries';
|
|
|
49
49
|
const GENERATED_MARKER = 'GENERATED by Fiodos — handler wiring';
|
|
50
50
|
// Markers wrapping every line Fiodos inserts INTO the user's own files, so the
|
|
51
51
|
// edits are idempotent (re-runs strip+rewrite) and trivially reversible.
|
|
52
|
-
|
|
52
|
+
// The strip regex matches any text after START so blocks written by older CLI
|
|
53
|
+
// versions (which localized the parenthetical) are still cleaned on re-runs.
|
|
54
|
+
const EDIT_START_PREFIX = '// FYODOS:BRIDGE:START';
|
|
55
|
+
const EDIT_START = `${EDIT_START_PREFIX} (generated by Fiodos — safe to remove)`;
|
|
53
56
|
const EDIT_END = '// FYODOS:BRIDGE:END';
|
|
54
57
|
|
|
55
58
|
// Directives that make OUR generated files (handlers.generated, bridge) invisible
|
|
@@ -449,7 +452,7 @@ function buildAiEntry({ appRoot, fyodosDirRel, base, aiW, manifestParams }) {
|
|
|
449
452
|
for (const id of freeIdentifiers(call)) {
|
|
450
453
|
if (JS_KEYWORDS.has(id) || importedNames.has(id) || SAFE_GLOBALS.has(id) || locals.has(id)) continue;
|
|
451
454
|
return {
|
|
452
|
-
reason: `the
|
|
455
|
+
reason: `the proposed call references '${id}', which could not be verified in your code — wire this action manually`,
|
|
453
456
|
};
|
|
454
457
|
}
|
|
455
458
|
|
|
@@ -491,14 +494,14 @@ function buildAiBridgeEntry({ appRoot, fyodosDirRel, base, aiW }) {
|
|
|
491
494
|
if (!anchor) return { reason: `the AI did not provide an insertion anchor in '${file}'` };
|
|
492
495
|
|
|
493
496
|
const occ = countOccurrences(content, anchor);
|
|
494
|
-
if (occ === 0) return { reason: `
|
|
495
|
-
if (occ > 1) return { reason: `the insertion
|
|
497
|
+
if (occ === 0) return { reason: `no safe insertion point was found in '${file}' — wire this action manually` };
|
|
498
|
+
if (occ > 1) return { reason: `the insertion point in '${file}' is ambiguous — wire this action manually` };
|
|
496
499
|
|
|
497
500
|
// requiredSymbols must really exist.
|
|
498
501
|
const required = Array.isArray(aiW.requiredSymbols) ? aiW.requiredSymbols : [];
|
|
499
502
|
for (const r of required) {
|
|
500
503
|
if (!fileHasSymbol(appRoot, r && r.file, r && r.name)) {
|
|
501
|
-
return { reason: `
|
|
504
|
+
return { reason: `symbol '${r && r.name}' was not found in '${normRel(r && r.file)}' — wire this action manually` };
|
|
502
505
|
}
|
|
503
506
|
}
|
|
504
507
|
|
|
@@ -534,7 +537,7 @@ function buildAiBridgeEntry({ appRoot, fyodosDirRel, base, aiW }) {
|
|
|
534
537
|
if (id === 'args' || JS_KEYWORDS.has(id) || SAFE_GLOBALS.has(id) || locals.has(id) || importedNames.has(id)) continue;
|
|
535
538
|
if (fileHasSymbol(appRoot, file, id)) continue;
|
|
536
539
|
return {
|
|
537
|
-
reason: `
|
|
540
|
+
reason: `references '${id}', which is not available in '${file}' — wire this action manually`,
|
|
538
541
|
};
|
|
539
542
|
}
|
|
540
543
|
|
|
@@ -582,9 +585,7 @@ function buildMechanicalEntry({ appRoot, fyodosDirRel, base, manifestParams }) {
|
|
|
582
585
|
const store = exported ? null : findStoreMethod(content, handler);
|
|
583
586
|
if (!exported && !store) {
|
|
584
587
|
return {
|
|
585
|
-
reason:
|
|
586
|
-
`'${handler}' was not found as an exported function or a store method in '${evFile}'. ` +
|
|
587
|
-
'It may live in a component, a class or a hook/context. Not wired blindly.',
|
|
588
|
+
reason: `'${handler}' in '${evFile}' could not be reached safely from a generated module — wire it manually`,
|
|
588
589
|
};
|
|
589
590
|
}
|
|
590
591
|
|
|
@@ -740,36 +741,82 @@ function buildRegistryModule(plan, opts = {}) {
|
|
|
740
741
|
// ActionRegistries so `<FiodosAgent registries={...}/>` type-checks. In
|
|
741
742
|
// particular idempotencyCheckers must match IdempotencyChecker —
|
|
742
743
|
// `(context) => Promise<{ alreadyApplied; message? }>` — not a boolean.
|
|
744
|
+
// Handlers accept the engine's optional (context, intent) trailing args so a
|
|
745
|
+
// handler shared by several manifest actions can dispatch on the intent.
|
|
743
746
|
const typeDefs = ts
|
|
744
747
|
? 'type FiodosActionResult = { success: boolean; data?: Record<string, unknown>; error?: string };\n' +
|
|
745
748
|
'type FiodosRegistries = {\n' +
|
|
746
|
-
' handlers: Record<string, (params: Record<string, any
|
|
749
|
+
' handlers: Record<string, (params: Record<string, any>, context?: Record<string, any>, intent?: string) => Promise<FiodosActionResult>>;\n' +
|
|
747
750
|
' idempotencyCheckers: Record<string, (context: Record<string, any>) => Promise<{ alreadyApplied: boolean; message?: string }>>;\n' +
|
|
748
751
|
'};\n'
|
|
749
752
|
: '';
|
|
750
753
|
const resultType = ts ? ': Promise<FiodosActionResult>' : '';
|
|
751
754
|
const paramsType = ts ? ': Record<string, any>' : '';
|
|
755
|
+
const intentType = ts ? ': string | undefined' : '';
|
|
756
|
+
|
|
757
|
+
// Shared runner: one place normalizes any wired call into an action result,
|
|
758
|
+
// so each handler below stays a one-liner. `unknown` keeps the truthiness
|
|
759
|
+
// test legal even when the wired call returns void (e.g. db.delete(...)).
|
|
760
|
+
const runHelper = ts
|
|
761
|
+
? 'async function __fyodosRun(call: () => unknown): Promise<FiodosActionResult> {\n' +
|
|
762
|
+
' try {\n' +
|
|
763
|
+
' const result: unknown = await Promise.resolve(call());\n' +
|
|
764
|
+
" return { success: true, data: (result && typeof result === 'object') ? result as Record<string, unknown> : { result } };\n" +
|
|
765
|
+
' } catch (err) {\n' +
|
|
766
|
+
' return { success: false, error: err instanceof Error ? err.message : String(err) };\n' +
|
|
767
|
+
' }\n' +
|
|
768
|
+
'}\n'
|
|
769
|
+
: 'async function __fyodosRun(call) {\n' +
|
|
770
|
+
' try {\n' +
|
|
771
|
+
' const result = await Promise.resolve(call());\n' +
|
|
772
|
+
" return { success: true, data: (result && typeof result === 'object') ? result : { result } };\n" +
|
|
773
|
+
' } catch (err) {\n' +
|
|
774
|
+
' return { success: false, error: err instanceof Error ? err.message : String(err) };\n' +
|
|
775
|
+
' }\n' +
|
|
776
|
+
'}\n';
|
|
777
|
+
|
|
778
|
+
const entryTakesParams = (e) =>
|
|
779
|
+
e.usesParams != null ? e.usesParams : /\bparams\b/.test(e.callExpr);
|
|
780
|
+
|
|
781
|
+
// Group auto entries by handler name: several manifest actions may share one
|
|
782
|
+
// handler (e.g. three section jumps calling scrollToSection with different
|
|
783
|
+
// arguments). An object literal keeps only the LAST duplicate key, which
|
|
784
|
+
// would silently break the others — so shared handlers dispatch on the
|
|
785
|
+
// intent the engine passes as the third argument instead.
|
|
786
|
+
const byHandler = new Map();
|
|
787
|
+
for (const e of plan.auto) {
|
|
788
|
+
if (!byHandler.has(e.handler)) byHandler.set(e.handler, []);
|
|
789
|
+
byHandler.get(e.handler).push(e);
|
|
790
|
+
}
|
|
752
791
|
|
|
753
|
-
const handlerBlocks =
|
|
754
|
-
|
|
792
|
+
const handlerBlocks = [];
|
|
793
|
+
for (const [handlerName, group] of byHandler) {
|
|
794
|
+
const distinctCalls = new Set(group.map((e) => e.callExpr));
|
|
795
|
+
if (group.length === 1 || distinctCalls.size === 1) {
|
|
796
|
+
const e = group[0];
|
|
797
|
+
const arg = entryTakesParams(e) ? `params${paramsType}` : `_params${paramsType}`;
|
|
798
|
+
handlerBlocks.push(
|
|
799
|
+
` ${handlerName}: async (${arg})${resultType} => __fyodosRun(() => ${e.callExpr}),`,
|
|
800
|
+
);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const takesParams = group.some(entryTakesParams);
|
|
755
804
|
const arg = takesParams ? `params${paramsType}` : `_params${paramsType}`;
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
`
|
|
762
|
-
`
|
|
763
|
-
|
|
764
|
-
`
|
|
765
|
-
` return { success: false, error: err instanceof Error ? err.message : String(err) };\n` +
|
|
805
|
+
const cases = group
|
|
806
|
+
.slice(1)
|
|
807
|
+
.map((e) => ` case ${JSON.stringify(e.intent)}: return __fyodosRun(() => ${e.callExpr});`)
|
|
808
|
+
.join('\n');
|
|
809
|
+
handlerBlocks.push(
|
|
810
|
+
` ${handlerName}: async (${arg}, _context${ts ? ': Record<string, any> | undefined' : ''}, intent${intentType})${resultType} => {\n` +
|
|
811
|
+
` switch (intent) {\n` +
|
|
812
|
+
`${cases}\n` +
|
|
813
|
+
` default: return __fyodosRun(() => ${group[0].callExpr});\n` +
|
|
766
814
|
` }\n` +
|
|
767
|
-
` }
|
|
815
|
+
` },`,
|
|
768
816
|
);
|
|
769
|
-
}
|
|
817
|
+
}
|
|
770
818
|
|
|
771
|
-
|
|
772
|
-
const handlerText = (ts ? handlerBlocks.join('\n') : handlerBlocks.join('\n').replace(/ as Record<string, unknown>/g, ''));
|
|
819
|
+
const handlerText = handlerBlocks.join('\n');
|
|
773
820
|
|
|
774
821
|
// List each handler that needs manual wiring once. Skip any handler already
|
|
775
822
|
// auto-wired above (the same handler name can back both an auto-wired and a
|
|
@@ -794,6 +841,7 @@ function buildRegistryModule(plan, opts = {}) {
|
|
|
794
841
|
`${header}\n` +
|
|
795
842
|
`${importLines.join('\n')}\n\n` +
|
|
796
843
|
(typeDefs ? `${typeDefs}\n` : '') +
|
|
844
|
+
`${runHelper}\n` +
|
|
797
845
|
`${decl}\n` +
|
|
798
846
|
` handlers: {\n` +
|
|
799
847
|
`${handlerText}\n` +
|
|
@@ -955,7 +1003,7 @@ function confLabel(e) {
|
|
|
955
1003
|
function buildHandlerDoc(plan, ctx = {}) {
|
|
956
1004
|
const {
|
|
957
1005
|
appName = 'your app', framework = 'web', registryRel = '', mountSnippet = '',
|
|
958
|
-
bridgeRel = '', edits = [],
|
|
1006
|
+
bridgeRel = '', edits = [], verification = [],
|
|
959
1007
|
} = ctx;
|
|
960
1008
|
const review = plan.review || plan.manual || [];
|
|
961
1009
|
const bridgeEntries = plan.auto.filter((e) => e.kind === 'bridge');
|
|
@@ -993,6 +1041,17 @@ function buildHandlerDoc(plan, ctx = {}) {
|
|
|
993
1041
|
L.push('```');
|
|
994
1042
|
L.push('');
|
|
995
1043
|
|
|
1044
|
+
if (verification.length) {
|
|
1045
|
+
L.push('## Automatic verification');
|
|
1046
|
+
L.push('');
|
|
1047
|
+
L.push('Every action below was verified in your project before being written; anything that could not be verified was left for manual wiring instead of being installed broken.');
|
|
1048
|
+
L.push('');
|
|
1049
|
+
for (const v of verification) {
|
|
1050
|
+
L.push(`- \`${v.intent}\`: ${v.status === 'ready' ? 'verified and wired' : 'could not be verified — see manual wiring below'}`);
|
|
1051
|
+
}
|
|
1052
|
+
L.push('');
|
|
1053
|
+
}
|
|
1054
|
+
|
|
996
1055
|
if (plan.auto.length) {
|
|
997
1056
|
L.push(`## Wired automatically (${plan.auto.length})`);
|
|
998
1057
|
L.push('');
|
|
@@ -1021,13 +1080,18 @@ function buildHandlerDoc(plan, ctx = {}) {
|
|
|
1021
1080
|
L.push('| Action | Handler | Reason |');
|
|
1022
1081
|
L.push('| --- | --- | --- |');
|
|
1023
1082
|
for (const e of review) {
|
|
1024
|
-
// Keep the reason to a clean one-liner: collapse whitespace
|
|
1025
|
-
// npm/compiler dump after the first
|
|
1026
|
-
|
|
1083
|
+
// Keep the reason to a clean one-liner: collapse whitespace and drop the
|
|
1084
|
+
// npm/compiler dump after the first `|` (no leaked paths). When capping
|
|
1085
|
+
// the length, cut at a word boundary and mark the cut with an ellipsis.
|
|
1086
|
+
let reason = String(e.reason || '')
|
|
1027
1087
|
.split('|')[0]
|
|
1028
1088
|
.replace(/\s+/g, ' ')
|
|
1029
|
-
.trim()
|
|
1030
|
-
|
|
1089
|
+
.trim();
|
|
1090
|
+
if (reason.length > 160) {
|
|
1091
|
+
const cut = reason.slice(0, 160);
|
|
1092
|
+
const atWord = cut.lastIndexOf(' ');
|
|
1093
|
+
reason = `${(atWord > 100 ? cut.slice(0, atWord) : cut).trimEnd()}…`;
|
|
1094
|
+
}
|
|
1031
1095
|
L.push(`| \`${e.intent}\` | \`${e.handler}\` | ${reason} |`);
|
|
1032
1096
|
}
|
|
1033
1097
|
L.push('');
|
|
@@ -1049,7 +1113,7 @@ function buildHandlerDoc(plan, ctx = {}) {
|
|
|
1049
1113
|
|
|
1050
1114
|
/** Remove any previously-inserted Fiodos blocks (idempotence across re-runs). */
|
|
1051
1115
|
function stripFiodosBlocks(content) {
|
|
1052
|
-
const re = new RegExp(`\\n?${esc(
|
|
1116
|
+
const re = new RegExp(`\\n?${esc(EDIT_START_PREFIX)}[^\\n]*[\\s\\S]*?${esc(EDIT_END)}\\n?`, 'g');
|
|
1053
1117
|
return content.replace(re, '\n');
|
|
1054
1118
|
}
|
|
1055
1119
|
|
|
@@ -1436,7 +1500,7 @@ async function wireHandlers(appRoot, opts = {}) {
|
|
|
1436
1500
|
`import { ${REGISTRY_EXPORT} } from '${registryImport}';\n` +
|
|
1437
1501
|
`\n` +
|
|
1438
1502
|
`<FiodosAgent\n` +
|
|
1439
|
-
` apiKey={/*
|
|
1503
|
+
` apiKey={/* your API key */}\n` +
|
|
1440
1504
|
` registries={${REGISTRY_EXPORT}}\n` +
|
|
1441
1505
|
`/>`;
|
|
1442
1506
|
|
|
@@ -1790,4 +1854,26 @@ module.exports = {
|
|
|
1790
1854
|
findExportedFunction,
|
|
1791
1855
|
findStoreMethod,
|
|
1792
1856
|
mapParams,
|
|
1857
|
+
// shared with wireSession (same verification + injection machinery)
|
|
1858
|
+
fileHasSymbol,
|
|
1859
|
+
freeIdentifiers,
|
|
1860
|
+
declaredLocals,
|
|
1861
|
+
detachedInstanceReceiver,
|
|
1862
|
+
countOccurrences,
|
|
1863
|
+
readFileSafe,
|
|
1864
|
+
normRel,
|
|
1865
|
+
relImport,
|
|
1866
|
+
detectTsEsm,
|
|
1867
|
+
resolveFiodosDirRel,
|
|
1868
|
+
askYesNo,
|
|
1869
|
+
JS_KEYWORDS,
|
|
1870
|
+
SAFE_GLOBALS,
|
|
1871
|
+
GENERATED_FILE_DIRECTIVES,
|
|
1872
|
+
INJECT_ESLINT_DISABLE,
|
|
1873
|
+
INJECT_ESLINT_ENABLE,
|
|
1874
|
+
EDIT_START,
|
|
1875
|
+
EDIT_END,
|
|
1876
|
+
BRIDGE_BASENAME,
|
|
1877
|
+
insertImportBlock,
|
|
1878
|
+
insertRegisterBlock,
|
|
1793
1879
|
};
|
package/src/wireReactNative.js
CHANGED
|
@@ -165,6 +165,7 @@ function unwireOrb(source) {
|
|
|
165
165
|
if (/FYODOS:ORB/.test(line)) return true; // `// …` or `{/* … */}` markers
|
|
166
166
|
if (/^import\s+\{?\s*FiodosExpoAgent\b/.test(t)) return true;
|
|
167
167
|
if (/^import\s+fyodosManifest\b/.test(t)) return true;
|
|
168
|
+
if (/^import\s+\{\s*fyodosIsAuthenticated\b/.test(t)) return true; // session pass import
|
|
168
169
|
if (/^<FiodosExpoAgent\b[^]*>$/.test(t)) return true; // our single-line open tag
|
|
169
170
|
if (/^<\/FiodosExpoAgent>$/.test(t)) return true;
|
|
170
171
|
return false;
|
|
@@ -258,6 +259,56 @@ function wireReactNativeOrb(appRoot, { manifest, colors = {}, noWire = false } =
|
|
|
258
259
|
return { status: 'added', file: layout.rel };
|
|
259
260
|
}
|
|
260
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Connect the generated SESSION module to the wired <FiodosExpoAgent> tag:
|
|
264
|
+
* adds `isAuthenticated={fyodosIsAuthenticated}` and `getUserId={fyodosGetUserId}`
|
|
265
|
+
* plus the import. Mirrors the web mount session pass. Idempotent; never
|
|
266
|
+
* overrides a developer-wired isAuthenticated; validated with the same parse
|
|
267
|
+
* safety net as the orb wrap.
|
|
268
|
+
*/
|
|
269
|
+
function connectRnSession(appRoot, { sessionFileAbs, colors = {} } = {}) {
|
|
270
|
+
if (!sessionFileAbs || !fs.existsSync(sessionFileAbs)) return { status: 'no-session-file' };
|
|
271
|
+
const layout = findRootLayout(appRoot);
|
|
272
|
+
if (!layout) return { status: 'not-mounted' };
|
|
273
|
+
|
|
274
|
+
let source;
|
|
275
|
+
try {
|
|
276
|
+
source = fs.readFileSync(layout.abs, 'utf8');
|
|
277
|
+
} catch (e) {
|
|
278
|
+
return { status: 'skipped', reason: `read-failed: ${e.message}` };
|
|
279
|
+
}
|
|
280
|
+
const tagRe = /<FiodosExpoAgent\b[^>]*>/;
|
|
281
|
+
const m = source.match(tagRe);
|
|
282
|
+
if (!m) return { status: 'not-mounted' };
|
|
283
|
+
const tag = m[0];
|
|
284
|
+
if (/isAuthenticated=\{fyodosIsAuthenticated\}/.test(tag)) return { status: 'already', file: layout.rel };
|
|
285
|
+
if (/\bisAuthenticated=/.test(tag)) return { status: 'dev-wired', file: layout.rel };
|
|
286
|
+
|
|
287
|
+
let specifier = path.relative(path.dirname(layout.abs), sessionFileAbs).split(path.sep).join('/');
|
|
288
|
+
specifier = specifier.replace(/\.(tsx?|jsx?)$/, '');
|
|
289
|
+
if (!specifier.startsWith('.')) specifier = `./${specifier}`;
|
|
290
|
+
|
|
291
|
+
const newTag = tag.replace(
|
|
292
|
+
/>$/,
|
|
293
|
+
' isAuthenticated={fyodosIsAuthenticated} getUserId={fyodosGetUserId}>',
|
|
294
|
+
);
|
|
295
|
+
let next = source.replace(tag, newTag);
|
|
296
|
+
const importLine = `import { fyodosIsAuthenticated, fyodosGetUserId } from '${specifier}';`;
|
|
297
|
+
if (!next.includes(importLine)) {
|
|
298
|
+
next = insertImportsAfterLastImport(next, importLine);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!parsesAsTsx(next, layout.rel)) {
|
|
302
|
+
return { status: 'skipped', file: layout.rel, reason: 'session props would not parse — left untouched' };
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
fs.writeFileSync(layout.abs, next);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
return { status: 'skipped', reason: `write-failed: ${e.message}` };
|
|
308
|
+
}
|
|
309
|
+
return { status: 'connected', file: layout.rel };
|
|
310
|
+
}
|
|
311
|
+
|
|
261
312
|
function reportReactNativeWire(result, colors = {}, { quiet = false } = {}) {
|
|
262
313
|
const { blue, cyan, dim, reset } = colors;
|
|
263
314
|
if (!result) return;
|
|
@@ -271,6 +322,7 @@ function reportReactNativeWire(result, colors = {}, { quiet = false } = {}) {
|
|
|
271
322
|
module.exports = {
|
|
272
323
|
wireReactNativeOrb,
|
|
273
324
|
reportReactNativeWire,
|
|
325
|
+
connectRnSession,
|
|
274
326
|
// exported for tests
|
|
275
327
|
buildWiredLayout,
|
|
276
328
|
unwireOrb,
|