@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/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": {
|
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
|
@@ -237,8 +237,31 @@ function summarizeSession(result) {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
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
|
+
|
|
240
263
|
function collectFileEntries(appRoot, ctx) {
|
|
241
|
-
const { orbResult, handlerResult, registryResult, sessionResult, envResult } = ctx;
|
|
264
|
+
const { orbResult, handlerResult, registryResult, sessionResult, screenResult, envResult } = ctx;
|
|
242
265
|
const created = [];
|
|
243
266
|
const modified = [];
|
|
244
267
|
const docs = [];
|
|
@@ -344,6 +367,36 @@ function collectFileEntries(appRoot, ctx) {
|
|
|
344
367
|
}
|
|
345
368
|
}
|
|
346
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
|
+
|
|
347
400
|
// Env (never record values)
|
|
348
401
|
if (envResult?.status === 'written' && envResult.writePlan?.targetRel) {
|
|
349
402
|
modified.push({
|
|
@@ -433,6 +486,7 @@ function buildRunRecord(appRoot, ctx) {
|
|
|
433
486
|
handlers: summarizeHandlers(ctx.handlerResult, appRoot),
|
|
434
487
|
registries: summarizeRegistries(ctx.registryResult),
|
|
435
488
|
session: summarizeSession(ctx.sessionResult),
|
|
489
|
+
screen: summarizeScreen(ctx.screenResult),
|
|
436
490
|
env: summarizeEnv(ctx.envResult),
|
|
437
491
|
},
|
|
438
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);
|
package/src/index.js
CHANGED
|
@@ -97,11 +97,16 @@ const { verifyManifest } = require('./verify');
|
|
|
97
97
|
const {
|
|
98
98
|
wireWebOrb, reportWireResult, connectOrbRegistries, reportRegistriesResult,
|
|
99
99
|
connectOrbSession, reportSessionConnectResult,
|
|
100
|
+
connectOrbScreen, reportScreenConnectResult,
|
|
100
101
|
} = require('./wireWeb');
|
|
101
102
|
const { wireReactNativeOrb, reportReactNativeWire, connectRnSession } = require('./wireReactNative');
|
|
103
|
+
const {
|
|
104
|
+
isShopifyThemeRoot, fetchStorefrontSnapshot, wireShopifyTheme, reportShopifyWire,
|
|
105
|
+
} = require('./shopifyTheme');
|
|
102
106
|
const { wireFlutterOrb, writeFlutterEnv, reportFlutterWire } = require('./wireFlutter');
|
|
103
107
|
const { wireHandlers, reportHandlerResult } = require('./wireHandlers');
|
|
104
108
|
const { wireSession, reportSessionResult } = require('./wireSession');
|
|
109
|
+
const { wireScreen, reportScreenResult } = require('./wireScreen');
|
|
105
110
|
const { offerWriteEnv, printEnvManualReminder } = require('./writeEnv');
|
|
106
111
|
const { scoreSurface, computeSurface } = require('./scoreSurface');
|
|
107
112
|
const { buildRunRecord, writeChangeRegistry } = require('./changeRegistry');
|
|
@@ -117,6 +122,8 @@ function arg(flag, fallback) {
|
|
|
117
122
|
const VALUE_FLAGS = new Set([
|
|
118
123
|
'--out', '--model', '--max-input-tokens', '--platform', '--api-key', '--api-url',
|
|
119
124
|
'--spec', '--analysis-root',
|
|
125
|
+
// shopify (theme repo mode): the store's public domain for the catalog snapshot
|
|
126
|
+
'--store',
|
|
120
127
|
// from-odata (closed-software mode)
|
|
121
128
|
'--app-id', '--app-name', '--app-description', '--auth-ref', '--entities',
|
|
122
129
|
]);
|
|
@@ -192,9 +199,11 @@ function askProceed() {
|
|
|
192
199
|
});
|
|
193
200
|
}
|
|
194
201
|
|
|
195
|
-
/** Spinner copy: "website" for web targets, "app" for mobile/native. */
|
|
202
|
+
/** Spinner copy: "website" for web targets, "store" for Shopify, "app" for mobile/native. */
|
|
196
203
|
function surfaceNoun(platform) {
|
|
197
|
-
|
|
204
|
+
if (platform === 'web') return 'website';
|
|
205
|
+
if (platform === 'shopify') return 'store';
|
|
206
|
+
return 'app';
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
function orbMountSpinnerLabel(platform) {
|
|
@@ -293,6 +302,20 @@ async function main() {
|
|
|
293
302
|
}
|
|
294
303
|
}
|
|
295
304
|
|
|
305
|
+
// ── Rewire-only mode: `fiodos rewire <dir>` or `fiodos <dir> --wire-only` ──
|
|
306
|
+
// Re-runs ONLY the handler wiring (verify → correct → retry, same loop as the
|
|
307
|
+
// install) from the saved artifacts of the LAST analysis. No re-analysis, no
|
|
308
|
+
// quota, no manifest publish — the repair path when some action ended up
|
|
309
|
+
// "manual" (Context/dashboard point developers here).
|
|
310
|
+
{
|
|
311
|
+
const pos = positionalArgs();
|
|
312
|
+
if (pos[0] === 'rewire' || process.argv.includes('--wire-only')) {
|
|
313
|
+
const target = pos[0] === 'rewire' ? pos[1] || '.' : pos[0] || '.';
|
|
314
|
+
await runRewireOnly(target);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
296
319
|
// Consent gate: confirm before doing anything. `yes` continues; `no` (or any
|
|
297
320
|
// other answer) cancels instantly — no analysis is run, the command just ends.
|
|
298
321
|
const proceed = await askProceed();
|
|
@@ -329,7 +352,7 @@ async function main() {
|
|
|
329
352
|
fs.mkdirSync(outDir, { recursive: true });
|
|
330
353
|
|
|
331
354
|
const platformArg = (arg('--platform', '') || '').toLowerCase();
|
|
332
|
-
const KNOWN_PLATFORMS = ['web', 'mobile', 'flutter', 'ios', 'android'];
|
|
355
|
+
const KNOWN_PLATFORMS = ['web', 'mobile', 'flutter', 'ios', 'android', 'shopify'];
|
|
333
356
|
const forcedPlatform = KNOWN_PLATFORMS.includes(platformArg) ? platformArg : null;
|
|
334
357
|
|
|
335
358
|
// ── 1. Static route scan (cheap facts; also the route verifier's ground truth)
|
|
@@ -369,6 +392,24 @@ async function main() {
|
|
|
369
392
|
|
|
370
393
|
// ── 2. Collect source files (rule-based, budget-aware) ────────────────────
|
|
371
394
|
const { included, omitted, estimatedTokens } = collectFiles(analysisRoot, { maxInputTokens, platform });
|
|
395
|
+
|
|
396
|
+
// Shopify: the theme alone doesn't say WHAT the store sells. Fetch a PUBLIC
|
|
397
|
+
// storefront snapshot (collections + product sample) from the store's own
|
|
398
|
+
// endpoints and include it as synthetic FILE blocks (_fyodos/storefront/*)
|
|
399
|
+
// so the analysis personalizes routes/bubbles/examples to the REAL catalog
|
|
400
|
+
// — and so evidence verification has concrete files to point at. Fail-open:
|
|
401
|
+
// an unreachable store still gets the universal surface file.
|
|
402
|
+
if (platform === 'shopify') {
|
|
403
|
+
const storeDomain = arg('--store', process.env.FYODOS_SHOPIFY_STORE || '');
|
|
404
|
+
if (!storeDomain) {
|
|
405
|
+
logUser('Tip: add --store your-store.com so the analysis reads your live catalog (collections & products).');
|
|
406
|
+
}
|
|
407
|
+
const snapshot = await withSpinner('Reading your store\u2019s public catalog', () =>
|
|
408
|
+
fetchStorefrontSnapshot(storeDomain, { log: logDev }),
|
|
409
|
+
);
|
|
410
|
+
included.push(...snapshot.files);
|
|
411
|
+
}
|
|
412
|
+
|
|
372
413
|
fs.writeFileSync(path.join(outDir, 'files-sent.json'), JSON.stringify({
|
|
373
414
|
included: included.map((f) => f.rel),
|
|
374
415
|
omitted,
|
|
@@ -398,6 +439,10 @@ async function main() {
|
|
|
398
439
|
// The AI's "session" block: WHERE the app's auth/session state lives, so the
|
|
399
440
|
// orb's isAuthenticated/getUserId are wired automatically (no manual step).
|
|
400
441
|
let sessionWiring = null;
|
|
442
|
+
// The AI's "screen" block: whether screens are URL-driven or CLIENT STATE,
|
|
443
|
+
// and where that state lives — so the orb knows which screen the user is on
|
|
444
|
+
// even when the URL never changes (see wireScreen.js).
|
|
445
|
+
let screenWiring = null;
|
|
401
446
|
let analysisMeta = { engine: 'auto-manifest-v3-ai-first', model: null, costUSD: 0 };
|
|
402
447
|
// Signed proof returned by the hosted-analysis proxy: it tells the publish
|
|
403
448
|
// step the quota was already consumed at /v1/developer/analyze (no double
|
|
@@ -461,6 +506,7 @@ async function main() {
|
|
|
461
506
|
evidence = parsed.evidence || {};
|
|
462
507
|
wiring = parsed.wiring || {};
|
|
463
508
|
sessionWiring = parsed.session || null;
|
|
509
|
+
screenWiring = parsed.screen || null;
|
|
464
510
|
// Only meaningful when the developer passed --spec; the backend sanitizes
|
|
465
511
|
// (context fields only, real intents only) before touching any override.
|
|
466
512
|
specResets = (userSpec && parsed.specResets && typeof parsed.specResets === 'object')
|
|
@@ -475,7 +521,7 @@ async function main() {
|
|
|
475
521
|
// Actions are always verified against real code. Routes are verified only
|
|
476
522
|
// when there is filesystem ground truth (mobile); for web they are kept as
|
|
477
523
|
// AI-proposed and flagged unverified in provenance.
|
|
478
|
-
({ manifest, provenance } = verifyManifest(proposed, evidence, included, routes, { verifyRoutes }));
|
|
524
|
+
({ manifest, provenance } = verifyManifest(proposed, evidence, included, routes, { verifyRoutes, platform }));
|
|
479
525
|
ensureBackRoute(manifest, provenance);
|
|
480
526
|
const droppedActions = provenance.actions.filter((a) => !a.included).length;
|
|
481
527
|
const droppedRoutes = provenance.routes.filter((r) => !r.included).length;
|
|
@@ -484,7 +530,7 @@ async function main() {
|
|
|
484
530
|
logUser('Analysis complete');
|
|
485
531
|
|
|
486
532
|
fs.writeFileSync(path.join(outDir, 'evidence.json'), JSON.stringify({
|
|
487
|
-
evidence, wiring, session: sessionWiring, uncertain: parsed.uncertain || [],
|
|
533
|
+
evidence, wiring, session: sessionWiring, screen: screenWiring, uncertain: parsed.uncertain || [],
|
|
488
534
|
}, null, 2));
|
|
489
535
|
fs.writeFileSync(path.join(outDir, 'usage.json'), JSON.stringify({
|
|
490
536
|
model, elapsedMs, usage,
|
|
@@ -618,6 +664,7 @@ async function main() {
|
|
|
618
664
|
let handlerWireResult = null;
|
|
619
665
|
let registryWireResult = null;
|
|
620
666
|
let sessionWireResult = null;
|
|
667
|
+
let screenWireResult = null;
|
|
621
668
|
const { runPostWireTest } = require('./postWireTest');
|
|
622
669
|
const selfCorrect = !process.argv.includes('--no-self-correct');
|
|
623
670
|
// Corrections follow the same key model as the analysis: hosted (proxy)
|
|
@@ -765,6 +812,46 @@ async function main() {
|
|
|
765
812
|
}
|
|
766
813
|
}
|
|
767
814
|
|
|
815
|
+
// Fourth pass — SCREEN-TRUTH wiring. For apps whose screens are CLIENT
|
|
816
|
+
// STATE (a section switcher) instead of URLs, connect the active-screen
|
|
817
|
+
// state to the orb so it truly knows where the user is: per-screen
|
|
818
|
+
// bubble, "you are already there" detection and screen-scoped context.
|
|
819
|
+
// Same consent/verification/revert model as session wiring.
|
|
820
|
+
if (screenWiring && !process.argv.includes('--no-orb-wire')) {
|
|
821
|
+
const handlerDeclined =
|
|
822
|
+
handlerResult &&
|
|
823
|
+
(handlerResult.status === 'declined' || handlerResult.status === 'declined-noninteractive');
|
|
824
|
+
if (!handlerDeclined) {
|
|
825
|
+
const handlerConsented =
|
|
826
|
+
handlerResult &&
|
|
827
|
+
(handlerResult.status === 'applied' || handlerResult.status === 'applied-untested');
|
|
828
|
+
const screenResult = await wireScreen(analysisRoot, {
|
|
829
|
+
screen: screenWiring,
|
|
830
|
+
colors: fyodosTermColors(),
|
|
831
|
+
quiet: !isVerbose(),
|
|
832
|
+
assumeYes: assumeYes || Boolean(handlerConsented),
|
|
833
|
+
noWire,
|
|
834
|
+
framework: 'web',
|
|
835
|
+
appName: manifest.appName,
|
|
836
|
+
runTest: !process.argv.includes('--no-wire-test'),
|
|
837
|
+
testRunner: runPostWireTest,
|
|
838
|
+
});
|
|
839
|
+
screenWireResult = screenResult;
|
|
840
|
+
report(() => reportScreenResult(screenResult, fyodosTermColors(), { quiet: !isVerbose() }));
|
|
841
|
+
if (
|
|
842
|
+
orbMounted &&
|
|
843
|
+
(screenResult.status === 'applied' || screenResult.status === 'applied-untested')
|
|
844
|
+
) {
|
|
845
|
+
const connectedScreen = connectOrbScreen(analysisRoot, {
|
|
846
|
+
screenFileAbs: screenResult.screenFile,
|
|
847
|
+
});
|
|
848
|
+
report(() =>
|
|
849
|
+
reportScreenConnectResult(connectedScreen, fyodosTermColors(), { quiet: !isVerbose() }),
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
768
855
|
// Finalize the install proof: now that the (slow) action wiring is done,
|
|
769
856
|
// tell the dashboard the terminal status so its "actions connected" tick
|
|
770
857
|
// turns green for real — never before wiring actually finished. Only do
|
|
@@ -779,6 +866,7 @@ async function main() {
|
|
|
779
866
|
actionsStatus: status,
|
|
780
867
|
actionsTotal: total,
|
|
781
868
|
actionsDone: done,
|
|
869
|
+
actions: actionsDetailFromHandlerResult(manifest, handlerResult),
|
|
782
870
|
});
|
|
783
871
|
}
|
|
784
872
|
};
|
|
@@ -833,6 +921,7 @@ async function main() {
|
|
|
833
921
|
handlerResult: handlerWireResult,
|
|
834
922
|
registryResult: registryWireResult,
|
|
835
923
|
sessionResult: sessionWireResult,
|
|
924
|
+
screenResult: screenWireResult,
|
|
836
925
|
envResult: envWriteResult,
|
|
837
926
|
});
|
|
838
927
|
const paths = writeChangeRegistry(analysisRoot, record);
|
|
@@ -841,6 +930,66 @@ async function main() {
|
|
|
841
930
|
logDev(`[change-registry] could not write: ${e.message}`);
|
|
842
931
|
}
|
|
843
932
|
}
|
|
933
|
+
} else if (platform === 'shopify' && publishing) {
|
|
934
|
+
// ── Shopify: publish + embed the orb in layout/theme.liquid ───────────────
|
|
935
|
+
// No code wiring: the manifest's commerce actions carry declarative
|
|
936
|
+
// `execution` blocks that the embed turns into handlers at runtime
|
|
937
|
+
// (buildApiActionRegistries, {baseUrl} → the store's own origin). The one
|
|
938
|
+
// edit is the theme.liquid snippet (idempotent markers); Shopify's GitHub
|
|
939
|
+
// sync deploys it when the developer pushes.
|
|
940
|
+
loadAppEnv(analysisRoot);
|
|
941
|
+
const { apiKey, apiUrl } = resolveApiTarget();
|
|
942
|
+
const noWire = process.argv.includes('--no-wire');
|
|
943
|
+
let orbWireResult = null;
|
|
944
|
+
|
|
945
|
+
await withSpinner('Publishing to your project', async ({ setLabel, pause, resume }) => {
|
|
946
|
+
await publishManifest(manifest, publishMeta, analysisToken, { skipSuccessLog: true });
|
|
947
|
+
if (!noWire && !process.argv.includes('--no-orb-wire')) {
|
|
948
|
+
setLabel(orbMountSpinnerLabel(platform));
|
|
949
|
+
orbWireResult = wireShopifyTheme(analysisRoot, { apiKey, apiUrl, noWire });
|
|
950
|
+
pause();
|
|
951
|
+
reportShopifyWire(orbWireResult, fyodosTermColors(), { quiet: !isVerbose() });
|
|
952
|
+
resume();
|
|
953
|
+
// Install proof only when the snippet is genuinely in the theme. All
|
|
954
|
+
// actions are execution-declared (nothing left to wire), so the
|
|
955
|
+
// terminal status is 'done' immediately — the dashboard tick then
|
|
956
|
+
// waits only for the runtime heartbeat once the theme is pushed.
|
|
957
|
+
if (
|
|
958
|
+
apiKey &&
|
|
959
|
+
(orbWireResult.status === 'added' || orbWireResult.status === 'already')
|
|
960
|
+
) {
|
|
961
|
+
const proof = await postOrbWired({
|
|
962
|
+
apiKey,
|
|
963
|
+
apiUrl,
|
|
964
|
+
file: orbWireResult.file || '',
|
|
965
|
+
platform: 'shopify',
|
|
966
|
+
actionsStatus: 'done',
|
|
967
|
+
});
|
|
968
|
+
if (proof && proof.ok === false && proof.reason !== 'no-api-key') {
|
|
969
|
+
logDev(`[orb-wired] shopify install proof not recorded (${proof.status || proof.reason}); dashboard will rely on the runtime ping`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
logUser('Published to your project');
|
|
975
|
+
if (orbWireResult && (orbWireResult.status === 'added' || orbWireResult.status === 'already')) {
|
|
976
|
+
logUser('Commit & push this theme repo — Shopify\u2019s GitHub sync deploys the orb to your live store.');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (!noWire) {
|
|
980
|
+
try {
|
|
981
|
+
const record = buildRunRecord(analysisRoot, {
|
|
982
|
+
appName: manifest.appName,
|
|
983
|
+
manifest,
|
|
984
|
+
publishMeta,
|
|
985
|
+
orbResult: orbWireResult,
|
|
986
|
+
});
|
|
987
|
+
const paths = writeChangeRegistry(analysisRoot, record);
|
|
988
|
+
logUser(`Change log: ${paths.mdRel} (history in .fyodos/changes/)`);
|
|
989
|
+
} catch (e) {
|
|
990
|
+
logDev(`[change-registry] could not write: ${e.message}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
844
993
|
} else if (platform === 'mobile' && publishing) {
|
|
845
994
|
// ── Mobile (Expo / React Native): mirror the web pipeline, adapted ─────────
|
|
846
995
|
// Same single command that publishes also mounts the orb (via the SDK's
|
|
@@ -1131,6 +1280,9 @@ function detectPlatform(appRoot, appDir) {
|
|
|
1131
1280
|
return found;
|
|
1132
1281
|
};
|
|
1133
1282
|
|
|
1283
|
+
// Shopify theme repos (GitHub-synced Liquid themes) are unmistakable and
|
|
1284
|
+
// have no package.json framework signal — check them first.
|
|
1285
|
+
if (isShopifyThemeRoot(appRoot)) return 'shopify';
|
|
1134
1286
|
if (has('pubspec.yaml')) return 'flutter';
|
|
1135
1287
|
if (has('Package.swift') || hasAnyExt('.xcodeproj') || (has('Sources') && hasAnyExt('.swift'))) return 'ios';
|
|
1136
1288
|
if ((has('settings.gradle.kts') || has('settings.gradle') || has('build.gradle.kts') || has('build.gradle')) && hasAnyExt('.kt')) {
|
|
@@ -1563,6 +1715,7 @@ async function postOrbWired({
|
|
|
1563
1715
|
actionsStatus = 'pending',
|
|
1564
1716
|
actionsTotal = 0,
|
|
1565
1717
|
actionsDone = 0,
|
|
1718
|
+
actions = null,
|
|
1566
1719
|
}) {
|
|
1567
1720
|
if (!apiKey) return { ok: false, reason: 'no-api-key' };
|
|
1568
1721
|
try {
|
|
@@ -1575,6 +1728,11 @@ async function postOrbWired({
|
|
|
1575
1728
|
actionsStatus,
|
|
1576
1729
|
actionsTotal,
|
|
1577
1730
|
actionsDone,
|
|
1731
|
+
// Per-action wiring detail (intent/status/reason). Powers the backend's
|
|
1732
|
+
// "wired or nonexistent" invariant (unwired actions never reach the
|
|
1733
|
+
// agent prompt) and the dashboard/Context wiring diagnosis. Older
|
|
1734
|
+
// backends ignore the extra field.
|
|
1735
|
+
...(Array.isArray(actions) ? { actions } : {}),
|
|
1578
1736
|
}),
|
|
1579
1737
|
});
|
|
1580
1738
|
if (res.ok) {
|
|
@@ -1589,6 +1747,210 @@ async function postOrbWired({
|
|
|
1589
1747
|
}
|
|
1590
1748
|
}
|
|
1591
1749
|
|
|
1750
|
+
/**
|
|
1751
|
+
* Rewire-only mode (`fiodos rewire <dir>` / `--wire-only`): re-run JUST the
|
|
1752
|
+
* handler wiring from the saved artifacts of the last analysis (manifest.json +
|
|
1753
|
+
* evidence.json in the analysis output dir). The full verify→diagnose→correct→
|
|
1754
|
+
* retry loop runs again — including AI corrections — so an action that fell to
|
|
1755
|
+
* "manual" in the install gets fresh attempts WITHOUT a re-analysis (no quota,
|
|
1756
|
+
* no manifest publish, no orb re-mount). Ends by re-posting the per-action
|
|
1757
|
+
* wiring status so the dashboard/Context see the repaired state.
|
|
1758
|
+
*/
|
|
1759
|
+
async function runRewireOnly(target) {
|
|
1760
|
+
const inputRoot = path.resolve(target || '.');
|
|
1761
|
+
const safeName = path.basename(inputRoot).replace(/[^a-z0-9_-]/gi, '_') || 'app';
|
|
1762
|
+
const outDir = arg('--out', path.join(os.tmpdir(), 'fiodos', 'analysis', safeName));
|
|
1763
|
+
|
|
1764
|
+
const manifestPath = path.join(outDir, 'manifest.json');
|
|
1765
|
+
const evidencePath = path.join(outDir, 'evidence.json');
|
|
1766
|
+
if (!fs.existsSync(manifestPath) || !fs.existsSync(evidencePath)) {
|
|
1767
|
+
logUser('No saved analysis found for this project — run a full analysis first:');
|
|
1768
|
+
logUser(` npx fiodos analyze ${target || '.'} --publish`);
|
|
1769
|
+
logUser(`(looked in ${outDir}; pass --out <dir> if you analyzed with a custom output dir)`);
|
|
1770
|
+
process.exitCode = 1;
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
1774
|
+
const ev = JSON.parse(fs.readFileSync(evidencePath, 'utf8'));
|
|
1775
|
+
const evidence = ev.evidence || {};
|
|
1776
|
+
const wiring = ev.wiring || {};
|
|
1777
|
+
|
|
1778
|
+
const scope = resolveAnalysisRoot(inputRoot, 'web');
|
|
1779
|
+
const analysisRoot = scope.root;
|
|
1780
|
+
loadAppEnv(analysisRoot);
|
|
1781
|
+
const { apiKey, apiUrl } = resolveApiTarget();
|
|
1782
|
+
const model = resolveModel(arg('--model', '') || DEFAULT_MODEL);
|
|
1783
|
+
const assumeYes = process.argv.includes('--yes') || process.argv.includes('--wire-yes');
|
|
1784
|
+
const { runPostWireTest } = require('./postWireTest');
|
|
1785
|
+
const { included } = collectFiles(analysisRoot, { maxInputTokens: Number(arg('--max-input-tokens', '150000')), platform: 'web' });
|
|
1786
|
+
|
|
1787
|
+
const selfCorrect = !process.argv.includes('--no-self-correct');
|
|
1788
|
+
const correctorUseProxy = !process.argv.includes('--own-ai-key');
|
|
1789
|
+
const corrector = selfCorrect
|
|
1790
|
+
? async (args) => {
|
|
1791
|
+
const { wiring: fixed } = await correctActionWiring({
|
|
1792
|
+
...args, model, platform: 'web', useProxy: correctorUseProxy, apiKey, apiUrl,
|
|
1793
|
+
});
|
|
1794
|
+
return fixed;
|
|
1795
|
+
}
|
|
1796
|
+
: null;
|
|
1797
|
+
|
|
1798
|
+
logUser(`Rewiring actions from the saved analysis (${manifest.actions.length} action(s), no re-analysis)…`);
|
|
1799
|
+
let handlerResult = null;
|
|
1800
|
+
await withSpinner(wiringSpinnerLabel('web'), async ({ pause, resume }) => {
|
|
1801
|
+
if (!assumeYes) pause();
|
|
1802
|
+
handlerResult = await wireHandlers(analysisRoot, {
|
|
1803
|
+
manifest,
|
|
1804
|
+
evidence,
|
|
1805
|
+
wiring,
|
|
1806
|
+
appName: manifest.appName,
|
|
1807
|
+
colors: fyodosTermColors(),
|
|
1808
|
+
assumeYes,
|
|
1809
|
+
noWire: false,
|
|
1810
|
+
quiet: !isVerbose(),
|
|
1811
|
+
runTest: !process.argv.includes('--no-wire-test'),
|
|
1812
|
+
revertOnFailure: true,
|
|
1813
|
+
testRunner: runPostWireTest,
|
|
1814
|
+
corrector,
|
|
1815
|
+
files: included,
|
|
1816
|
+
maxAttempts: Number(process.env.FYODOS_WIRE_MAX_ATTEMPTS || 3),
|
|
1817
|
+
});
|
|
1818
|
+
if (!assumeYes) resume();
|
|
1819
|
+
pause();
|
|
1820
|
+
reportHandlerResult(handlerResult, fyodosTermColors(), { quiet: !isVerbose() });
|
|
1821
|
+
resume();
|
|
1822
|
+
|
|
1823
|
+
const handlerApplied =
|
|
1824
|
+
handlerResult &&
|
|
1825
|
+
(handlerResult.status === 'applied' || handlerResult.status === 'applied-untested') &&
|
|
1826
|
+
(handlerResult.autoCount || 0) > 0 &&
|
|
1827
|
+
handlerResult.registryFile;
|
|
1828
|
+
if (handlerApplied) {
|
|
1829
|
+
const connected = connectOrbRegistries(analysisRoot, {
|
|
1830
|
+
registryFileAbs: handlerResult.registryFile,
|
|
1831
|
+
});
|
|
1832
|
+
pause();
|
|
1833
|
+
reportRegistriesResult(connected, fyodosTermColors(), { quiet: !isVerbose() });
|
|
1834
|
+
resume();
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
// Session + screen wiring also replay from the saved analysis (same consent
|
|
1839
|
+
// model as the main flow: an APPLIED handler wiring proves the "yes").
|
|
1840
|
+
const handlerConsented =
|
|
1841
|
+
handlerResult &&
|
|
1842
|
+
(handlerResult.status === 'applied' || handlerResult.status === 'applied-untested');
|
|
1843
|
+
if (ev.session) {
|
|
1844
|
+
const sessionResult = await wireSession(analysisRoot, {
|
|
1845
|
+
session: ev.session,
|
|
1846
|
+
colors: fyodosTermColors(),
|
|
1847
|
+
quiet: !isVerbose(),
|
|
1848
|
+
assumeYes: assumeYes || Boolean(handlerConsented),
|
|
1849
|
+
framework: 'web',
|
|
1850
|
+
appName: manifest.appName,
|
|
1851
|
+
runTest: !process.argv.includes('--no-wire-test'),
|
|
1852
|
+
testRunner: runPostWireTest,
|
|
1853
|
+
});
|
|
1854
|
+
reportSessionResult(sessionResult, fyodosTermColors(), { quiet: !isVerbose() });
|
|
1855
|
+
if (sessionResult.status === 'applied' || sessionResult.status === 'applied-untested') {
|
|
1856
|
+
const connectedSession = connectOrbSession(analysisRoot, {
|
|
1857
|
+
sessionFileAbs: sessionResult.sessionFile,
|
|
1858
|
+
});
|
|
1859
|
+
reportSessionConnectResult(connectedSession, fyodosTermColors(), { quiet: !isVerbose() });
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (ev.screen) {
|
|
1863
|
+
const screenResult = await wireScreen(analysisRoot, {
|
|
1864
|
+
screen: ev.screen,
|
|
1865
|
+
colors: fyodosTermColors(),
|
|
1866
|
+
quiet: !isVerbose(),
|
|
1867
|
+
assumeYes: assumeYes || Boolean(handlerConsented),
|
|
1868
|
+
framework: 'web',
|
|
1869
|
+
appName: manifest.appName,
|
|
1870
|
+
runTest: !process.argv.includes('--no-wire-test'),
|
|
1871
|
+
testRunner: runPostWireTest,
|
|
1872
|
+
});
|
|
1873
|
+
reportScreenResult(screenResult, fyodosTermColors(), { quiet: !isVerbose() });
|
|
1874
|
+
if (screenResult.status === 'applied' || screenResult.status === 'applied-untested') {
|
|
1875
|
+
const connectedScreen = connectOrbScreen(analysisRoot, {
|
|
1876
|
+
screenFileAbs: screenResult.screenFile,
|
|
1877
|
+
});
|
|
1878
|
+
reportScreenConnectResult(connectedScreen, fyodosTermColors(), { quiet: !isVerbose() });
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (apiKey && handlerResult) {
|
|
1883
|
+
const { status, total, done } = actionsWiredFromHandlerResult(handlerResult);
|
|
1884
|
+
await postOrbWired({
|
|
1885
|
+
apiKey,
|
|
1886
|
+
apiUrl,
|
|
1887
|
+
file: (handlerResult.registryRel || ''),
|
|
1888
|
+
actionsStatus: status,
|
|
1889
|
+
actionsTotal: total,
|
|
1890
|
+
actionsDone: done,
|
|
1891
|
+
actions: actionsDetailFromHandlerResult(manifest, handlerResult),
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const wired = (handlerResult && handlerResult.autoCount) || 0;
|
|
1895
|
+
const manual = (handlerResult && handlerResult.manualCount) || 0;
|
|
1896
|
+
logUser(`Rewire finished: ${wired} action(s) wired, ${manual} still need attention.`);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* Per-action wiring detail for the backend (POST /v1/developer/orb-wired).
|
|
1901
|
+
* One entry per manifest action:
|
|
1902
|
+
* { intent, handler, status: 'wired'|'manual'|'link', reason?, attempts? }
|
|
1903
|
+
* 'wired' → verified and installed (the action really runs);
|
|
1904
|
+
* 'manual' → could not be wired/verified — the backend EXCLUDES it from the
|
|
1905
|
+
* agent surface ("wired or nonexistent" invariant) and the
|
|
1906
|
+
* dashboard/Context can show and repair it;
|
|
1907
|
+
* 'link' → navigation/link action, needs no handler (always available).
|
|
1908
|
+
* Returns null when wiring did not actually run (declined/skipped): with no
|
|
1909
|
+
* verdict the backend must not filter anything.
|
|
1910
|
+
*/
|
|
1911
|
+
function actionsDetailFromHandlerResult(manifest, handlerResult) {
|
|
1912
|
+
const r = handlerResult || {};
|
|
1913
|
+
const ranStatuses = new Set(['applied', 'applied-untested', 'manual-only', 'reverted', 'failed']);
|
|
1914
|
+
if (!ranStatuses.has(r.status)) return null;
|
|
1915
|
+
const plan = r.plan || {};
|
|
1916
|
+
const autoByIntent = new Map((plan.auto || []).map((e) => [e.intent, e]));
|
|
1917
|
+
const reviewByIntent = new Map((plan.review || plan.manual || []).map((e) => [e.intent, e]));
|
|
1918
|
+
const verifByIntent = new Map((r.verification || []).map((v) => [v.intent, v]));
|
|
1919
|
+
const wholesaleFailed = r.status === 'reverted' || r.status === 'failed';
|
|
1920
|
+
|
|
1921
|
+
const detail = [];
|
|
1922
|
+
for (const a of (manifest && manifest.actions) || []) {
|
|
1923
|
+
const intent = a.intent;
|
|
1924
|
+
if (a.kind === 'link') {
|
|
1925
|
+
detail.push({ intent, handler: '', status: 'link' });
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
const verif = verifByIntent.get(intent);
|
|
1929
|
+
if (!wholesaleFailed && autoByIntent.has(intent)) {
|
|
1930
|
+
detail.push({
|
|
1931
|
+
intent,
|
|
1932
|
+
handler: a.handler || intent,
|
|
1933
|
+
status: 'wired',
|
|
1934
|
+
...(verif && verif.level ? { level: verif.level } : {}),
|
|
1935
|
+
...(verif && verif.attempts ? { attempts: verif.attempts } : {}),
|
|
1936
|
+
});
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
const rev = reviewByIntent.get(intent);
|
|
1940
|
+
const reason = wholesaleFailed
|
|
1941
|
+
? `wiring ${r.status}: the combined install did not pass the build safety net`
|
|
1942
|
+
: String((rev && rev.reason) || (verif && verif.error) || 'could not be wired automatically');
|
|
1943
|
+
detail.push({
|
|
1944
|
+
intent,
|
|
1945
|
+
handler: a.handler || intent,
|
|
1946
|
+
status: 'manual',
|
|
1947
|
+
reason: reason.split('|')[0].replace(/\s+/g, ' ').trim().slice(0, 300),
|
|
1948
|
+
...(verif && verif.attempts ? { attempts: verif.attempts } : {}),
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
return detail;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1592
1954
|
// Map a wireHandlers() result to the terminal action-wiring status the backend
|
|
1593
1955
|
// records, plus how many actions ended up wired. Drives the onboarding's third
|
|
1594
1956
|
// "actions connected" tick so it only goes green once wiring truly finished.
|