@fiodos/cli 0.1.23 → 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 +4 -3
- package/src/aiAnalyze.js +6 -4
- package/src/changeRegistry.js +109 -1
- package/src/collect.js +16 -2
- package/src/index.js +535 -7
- package/src/odata.js +529 -0
- package/src/shopifyTheme.js +300 -0
- package/src/verify.js +124 -1
- package/src/wireHandlers.js +115 -7
- package/src/wireReactNative.js +52 -0
- package/src/wireScreen.js +562 -0
- package/src/wireSession.js +755 -0
- package/src/wireWeb.js +215 -0
- package/src/wireWebMount.js +171 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
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": {
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"analyze": "node src/index.js"
|
|
17
|
+
"analyze": "node src/index.js",
|
|
18
|
+
"test": "node --test test/*.test.js"
|
|
18
19
|
},
|
|
19
20
|
"engines": {
|
|
20
21
|
"node": ">=18"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@fiodos/core": "^0.1.
|
|
24
|
+
"@fiodos/core": "^0.1.10",
|
|
24
25
|
"esbuild": "^0.28.1",
|
|
25
26
|
"jsdom": "^24.0.0",
|
|
26
27
|
"typescript": "^5.6.0"
|
package/src/aiAnalyze.js
CHANGED
|
@@ -141,7 +141,9 @@ function buildUserPrompt(appMeta, files, omitted, staticRoutes, surface = null,
|
|
|
141
141
|
: '';
|
|
142
142
|
const routeBlock = staticRoutes.length
|
|
143
143
|
? `Statically-scanned routes (existence is verified — use these hrefs):\n${routeHint}\n`
|
|
144
|
-
:
|
|
144
|
+
: opts.platform === 'shopify'
|
|
145
|
+
? 'Shopify storefront: derive routes from the PUBLIC STOREFRONT SNAPSHOT files under _fyodos/storefront/ (universal pages + one route per real collection/page handle) and from the theme templates below.\n'
|
|
146
|
+
: `No static route list (web app): infer routes/views from the router and navigation calls in the code below.\n`;
|
|
145
147
|
|
|
146
148
|
// Detected interactive link elements on the product surface — concrete hrefs
|
|
147
149
|
// the model should turn into LINK actions (verified later against the file).
|
|
@@ -171,7 +173,7 @@ function buildUserPrompt(appMeta, files, omitted, staticRoutes, surface = null,
|
|
|
171
173
|
routeBlock +
|
|
172
174
|
linkBlock +
|
|
173
175
|
omittedNote +
|
|
174
|
-
(includeSchema && LOCAL_PROMPTS ? `\nManifest schema:\n${LOCAL_PROMPTS.manifestSchemaDoc()}\n\n` : '\n') +
|
|
176
|
+
(includeSchema && LOCAL_PROMPTS ? `\nManifest schema:\n${LOCAL_PROMPTS.manifestSchemaDoc(opts.platform === 'shopify')}\n\n` : '\n') +
|
|
175
177
|
`FULL SOURCE CODE (${files.length} files):\n\n${codeBlob}`
|
|
176
178
|
);
|
|
177
179
|
}
|
|
@@ -383,7 +385,7 @@ async function analyzeWithAI({
|
|
|
383
385
|
if (useProxy) {
|
|
384
386
|
// Server prompt mode: the backend builds the system prompt (with the
|
|
385
387
|
// manifest schema embedded), so the user payload carries only the inputs.
|
|
386
|
-
user = buildUserPrompt(appMeta, files, omitted, staticRoutes, surface, spec, { includeSchema: false });
|
|
388
|
+
user = buildUserPrompt(appMeta, files, omitted, staticRoutes, surface, spec, { includeSchema: false, platform });
|
|
387
389
|
({ parsed, usage, serverModel, analysisToken } = await analyzeViaProxy({
|
|
388
390
|
apiKey, apiUrl, model: resolved, user, task: 'analysis', platform, hasUserSpec: Boolean(spec),
|
|
389
391
|
}));
|
|
@@ -391,7 +393,7 @@ async function analyzeWithAI({
|
|
|
391
393
|
const sp = await resolveOwnKeySystemPrompt({
|
|
392
394
|
task: 'analysis', platform, hasUserSpec: Boolean(spec), apiKey, apiUrl,
|
|
393
395
|
});
|
|
394
|
-
user = buildUserPrompt(appMeta, files, omitted, staticRoutes, surface, spec, { includeSchema: sp.schemaInUser });
|
|
396
|
+
user = buildUserPrompt(appMeta, files, omitted, staticRoutes, surface, spec, { includeSchema: sp.schemaInUser, platform });
|
|
395
397
|
({ parsed, usage } = isAnthropicModel(resolved)
|
|
396
398
|
? await completeAnthropic({ model: resolved, system: sp.system, user })
|
|
397
399
|
: await completeOpenAI({ model: resolved, system: sp.system, user }));
|
package/src/changeRegistry.js
CHANGED
|
@@ -214,8 +214,54 @@ function summarizeEnv(result) {
|
|
|
214
214
|
return { ...base, applied: false };
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
function summarizeSession(result) {
|
|
218
|
+
if (!result) return { status: 'skipped', applied: false };
|
|
219
|
+
const base = { status: result.status, strategy: result.strategy || null };
|
|
220
|
+
switch (result.status) {
|
|
221
|
+
case 'applied':
|
|
222
|
+
case 'applied-untested':
|
|
223
|
+
return {
|
|
224
|
+
...base,
|
|
225
|
+
applied: true,
|
|
226
|
+
what: 'Connected the orb to the app\'s real session state (isAuthenticated/getUserId) automatically.',
|
|
227
|
+
revert: 'Delete session.generated.* and remove the FYODOS:SESSION blocks + mount props.',
|
|
228
|
+
};
|
|
229
|
+
case 'review':
|
|
230
|
+
return { ...base, applied: false, note: `Session left for review: ${result.reason || ''}` };
|
|
231
|
+
case 'reverted':
|
|
232
|
+
return { ...base, applied: false, note: `Session wiring reverted: ${result.reason || ''}` };
|
|
233
|
+
case 'no-session':
|
|
234
|
+
return { ...base, applied: false, note: 'No login detected — session wiring not needed.' };
|
|
235
|
+
default:
|
|
236
|
+
return { ...base, applied: false };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function summarizeScreen(result) {
|
|
241
|
+
if (!result) return { status: 'skipped', applied: false };
|
|
242
|
+
const base = { status: result.status };
|
|
243
|
+
switch (result.status) {
|
|
244
|
+
case 'applied':
|
|
245
|
+
case 'applied-untested':
|
|
246
|
+
return {
|
|
247
|
+
...base,
|
|
248
|
+
applied: true,
|
|
249
|
+
what: 'Connected the app\'s ACTIVE screen state to the orb (state-driven screens, no URL change).',
|
|
250
|
+
revert: 'Delete screen.generated.* and remove the FYODOS:SCREEN blocks + mount prop.',
|
|
251
|
+
};
|
|
252
|
+
case 'review':
|
|
253
|
+
return { ...base, applied: false, note: `Screen wiring left for review: ${result.reason || ''}` };
|
|
254
|
+
case 'reverted':
|
|
255
|
+
return { ...base, applied: false, note: `Screen wiring reverted: ${result.reason || ''}` };
|
|
256
|
+
case 'url-screens':
|
|
257
|
+
return { ...base, applied: false, note: 'Screens are URL-driven — no screen wiring needed.' };
|
|
258
|
+
default:
|
|
259
|
+
return { ...base, applied: false };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
217
263
|
function collectFileEntries(appRoot, ctx) {
|
|
218
|
-
const { orbResult, handlerResult, registryResult, envResult } = ctx;
|
|
264
|
+
const { orbResult, handlerResult, registryResult, sessionResult, screenResult, envResult } = ctx;
|
|
219
265
|
const created = [];
|
|
220
266
|
const modified = [];
|
|
221
267
|
const docs = [];
|
|
@@ -291,6 +337,66 @@ function collectFileEntries(appRoot, ctx) {
|
|
|
291
337
|
});
|
|
292
338
|
}
|
|
293
339
|
|
|
340
|
+
// Session wiring (auth awareness — automatic)
|
|
341
|
+
if (
|
|
342
|
+
sessionResult &&
|
|
343
|
+
(sessionResult.status === 'applied' || sessionResult.status === 'applied-untested')
|
|
344
|
+
) {
|
|
345
|
+
if (sessionResult.sessionFile) {
|
|
346
|
+
created.push({
|
|
347
|
+
path: normRel(appRoot, sessionResult.sessionFile),
|
|
348
|
+
phase: 'session',
|
|
349
|
+
what: 'Generated session module (`session.generated.*`): isAuthenticated/getUserId for the orb.',
|
|
350
|
+
revert: 'Safe to delete; re-run the installer to regenerate.',
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (sessionResult.editedFile) {
|
|
354
|
+
modified.push({
|
|
355
|
+
path: sessionResult.editedFile,
|
|
356
|
+
phase: 'session',
|
|
357
|
+
what: 'Registered the live session in the bridge (reversible FYODOS:SESSION block).',
|
|
358
|
+
revert: 'Remove the FYODOS:SESSION blocks.',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (sessionResult.docPath) {
|
|
362
|
+
docs.push({
|
|
363
|
+
path: normRel(appRoot, sessionResult.docPath),
|
|
364
|
+
phase: 'session',
|
|
365
|
+
what: 'Session wiring summary: strategy, honesty contract, security notes.',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Screen-truth wiring (state-driven screens — automatic)
|
|
371
|
+
if (
|
|
372
|
+
screenResult &&
|
|
373
|
+
(screenResult.status === 'applied' || screenResult.status === 'applied-untested')
|
|
374
|
+
) {
|
|
375
|
+
if (screenResult.screenFile) {
|
|
376
|
+
created.push({
|
|
377
|
+
path: normRel(appRoot, screenResult.screenFile),
|
|
378
|
+
phase: 'screen',
|
|
379
|
+
what: 'Generated screen module (`screen.generated.*`): reports the ACTIVE screen to the orb.',
|
|
380
|
+
revert: 'Safe to delete; re-run the installer to regenerate.',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
if (screenResult.editedFile) {
|
|
384
|
+
modified.push({
|
|
385
|
+
path: screenResult.editedFile,
|
|
386
|
+
phase: 'screen',
|
|
387
|
+
what: 'Registered the live active-screen state in the bridge (reversible FYODOS:SCREEN block).',
|
|
388
|
+
revert: 'Remove the FYODOS:SCREEN blocks.',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (screenResult.docPath) {
|
|
392
|
+
docs.push({
|
|
393
|
+
path: normRel(appRoot, screenResult.docPath),
|
|
394
|
+
phase: 'screen',
|
|
395
|
+
what: 'Screen-truth wiring summary: why it was needed, honesty contract.',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
294
400
|
// Env (never record values)
|
|
295
401
|
if (envResult?.status === 'written' && envResult.writePlan?.targetRel) {
|
|
296
402
|
modified.push({
|
|
@@ -379,6 +485,8 @@ function buildRunRecord(appRoot, ctx) {
|
|
|
379
485
|
orb: summarizeOrb(ctx.orbResult),
|
|
380
486
|
handlers: summarizeHandlers(ctx.handlerResult, appRoot),
|
|
381
487
|
registries: summarizeRegistries(ctx.registryResult),
|
|
488
|
+
session: summarizeSession(ctx.sessionResult),
|
|
489
|
+
screen: summarizeScreen(ctx.screenResult),
|
|
382
490
|
env: summarizeEnv(ctx.envResult),
|
|
383
491
|
},
|
|
384
492
|
files: collectFileEntries(appRoot, ctx),
|
package/src/collect.js
CHANGED
|
@@ -47,6 +47,12 @@ const PROFILES = {
|
|
|
47
47
|
exts: ['.kt'],
|
|
48
48
|
skipDirs: ['.git', '.gradle', 'build', '.idea'],
|
|
49
49
|
},
|
|
50
|
+
// Shopify theme repo (GitHub-synced Liquid theme). The "code" is the theme's
|
|
51
|
+
// Liquid templates + JSON templates/settings; heavy static assets are noise.
|
|
52
|
+
shopify: {
|
|
53
|
+
exts: ['.liquid', '.json'],
|
|
54
|
+
skipDirs: ['.git', 'node_modules', 'assets', 'dist', 'build'],
|
|
55
|
+
},
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
const SKIP_FILES = /\.(d\.ts|test\.|spec\.)|babel\.config|metro\.config|tailwind\.config|postcss\.config|jest\.config|eslint/;
|
|
@@ -64,8 +70,16 @@ const CHARS_PER_TOKEN = 3.0;
|
|
|
64
70
|
|
|
65
71
|
/** Lower tier = higher priority when the budget forces choices. Generalized so
|
|
66
72
|
* it ranks screens/state/services first across web, RN AND native naming. */
|
|
67
|
-
function tierFor(rel) {
|
|
73
|
+
function tierFor(rel, platform) {
|
|
68
74
|
const p = rel.replace(/\\/g, '/');
|
|
75
|
+
if (platform === 'shopify') {
|
|
76
|
+
// Shopify theme layout: templates define the store's pages; layout + locales
|
|
77
|
+
// carry structure and language; sections/snippets are secondary UI.
|
|
78
|
+
if (/^(layout|templates)\//.test(p)) return 0;
|
|
79
|
+
if (/^locales\//.test(p)) return 1;
|
|
80
|
+
if (/^sections\//.test(p)) return 2;
|
|
81
|
+
return 3; // snippets/, config/ (settings_data.json can be huge), rest
|
|
82
|
+
}
|
|
69
83
|
if (/^(src\/)?app\//.test(p)) return 0; // expo-router / Next screens
|
|
70
84
|
if (/(^|\/)(screens?|pages?|views?|routes?|activities|fragments)\//i.test(p)) return 0;
|
|
71
85
|
if (/(screen|page|view|activity|fragment)\.(dart|kt|swift)$/i.test(p)) return 0;
|
|
@@ -95,7 +109,7 @@ function collectFiles(appRoot, { maxInputTokens = 150_000, platform = 'mobile' }
|
|
|
95
109
|
if (SKIP_FILES.test(rel)) continue;
|
|
96
110
|
if (isNative && SKIP_NATIVE_FILES.test(rel)) continue;
|
|
97
111
|
const content = fs.readFileSync(full, 'utf8');
|
|
98
|
-
files.push({ rel, content, tier: tierFor(rel), chars: content.length });
|
|
112
|
+
files.push({ rel, content, tier: tierFor(rel, platform), chars: content.length });
|
|
99
113
|
}
|
|
100
114
|
};
|
|
101
115
|
walk(appRoot);
|