@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/src/index.js
CHANGED
|
@@ -94,10 +94,19 @@ const { collectFiles } = require('./collect');
|
|
|
94
94
|
const { resolveAnalysisRoot } = require('./resolveAnalysisRoot');
|
|
95
95
|
const { analyzeWithAI, correctActionWiring, generateGuardianWithAI, DEFAULT_MODEL, resolveModel } = require('./aiAnalyze');
|
|
96
96
|
const { verifyManifest } = require('./verify');
|
|
97
|
-
const {
|
|
98
|
-
|
|
97
|
+
const {
|
|
98
|
+
wireWebOrb, reportWireResult, connectOrbRegistries, reportRegistriesResult,
|
|
99
|
+
connectOrbSession, reportSessionConnectResult,
|
|
100
|
+
connectOrbScreen, reportScreenConnectResult,
|
|
101
|
+
} = require('./wireWeb');
|
|
102
|
+
const { wireReactNativeOrb, reportReactNativeWire, connectRnSession } = require('./wireReactNative');
|
|
103
|
+
const {
|
|
104
|
+
isShopifyThemeRoot, fetchStorefrontSnapshot, wireShopifyTheme, reportShopifyWire,
|
|
105
|
+
} = require('./shopifyTheme');
|
|
99
106
|
const { wireFlutterOrb, writeFlutterEnv, reportFlutterWire } = require('./wireFlutter');
|
|
100
107
|
const { wireHandlers, reportHandlerResult } = require('./wireHandlers');
|
|
108
|
+
const { wireSession, reportSessionResult } = require('./wireSession');
|
|
109
|
+
const { wireScreen, reportScreenResult } = require('./wireScreen');
|
|
101
110
|
const { offerWriteEnv, printEnvManualReminder } = require('./writeEnv');
|
|
102
111
|
const { scoreSurface, computeSurface } = require('./scoreSurface');
|
|
103
112
|
const { buildRunRecord, writeChangeRegistry } = require('./changeRegistry');
|
|
@@ -113,6 +122,10 @@ function arg(flag, fallback) {
|
|
|
113
122
|
const VALUE_FLAGS = new Set([
|
|
114
123
|
'--out', '--model', '--max-input-tokens', '--platform', '--api-key', '--api-url',
|
|
115
124
|
'--spec', '--analysis-root',
|
|
125
|
+
// shopify (theme repo mode): the store's public domain for the catalog snapshot
|
|
126
|
+
'--store',
|
|
127
|
+
// from-odata (closed-software mode)
|
|
128
|
+
'--app-id', '--app-name', '--app-description', '--auth-ref', '--entities',
|
|
116
129
|
]);
|
|
117
130
|
|
|
118
131
|
/**
|
|
@@ -186,9 +199,11 @@ function askProceed() {
|
|
|
186
199
|
});
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
/** Spinner copy: "website" for web targets, "app" for mobile/native. */
|
|
202
|
+
/** Spinner copy: "website" for web targets, "store" for Shopify, "app" for mobile/native. */
|
|
190
203
|
function surfaceNoun(platform) {
|
|
191
|
-
|
|
204
|
+
if (platform === 'web') return 'website';
|
|
205
|
+
if (platform === 'shopify') return 'store';
|
|
206
|
+
return 'app';
|
|
192
207
|
}
|
|
193
208
|
|
|
194
209
|
function orbMountSpinnerLabel(platform) {
|
|
@@ -251,6 +266,56 @@ async function withSpinner(label, work) {
|
|
|
251
266
|
}
|
|
252
267
|
|
|
253
268
|
async function main() {
|
|
269
|
+
// ── Closed-software mode: `fiodos from-odata <metadata.xml|url>` ────────────
|
|
270
|
+
// Generates the manifest from a third-party system's OData $metadata (schema,
|
|
271
|
+
// not source code), so the consent gate about code analysis does not apply.
|
|
272
|
+
{
|
|
273
|
+
const pos = positionalArgs();
|
|
274
|
+
if (pos[0] === 'from-odata') {
|
|
275
|
+
const target = pos[1];
|
|
276
|
+
if (!target) {
|
|
277
|
+
console.error('Usage: fiodos from-odata <metadata.xml | https://…/$metadata> --app-id <id> --app-name <name> [--entities a,b] [--auth-ref name] [--out dir] [--publish --api-key fyd_…]');
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
const { runFromOData } = require('./odata');
|
|
281
|
+
const safeName = path.basename(target).replace(/[^a-z0-9_-]/gi, '_') || 'odata';
|
|
282
|
+
const outDirOData = arg('--out', path.join(os.tmpdir(), 'fiodos', 'analysis', safeName));
|
|
283
|
+
const entitiesArg = arg('--entities', '');
|
|
284
|
+
await runFromOData({
|
|
285
|
+
target,
|
|
286
|
+
outDir: outDirOData,
|
|
287
|
+
opts: {
|
|
288
|
+
appId: arg('--app-id', `odata.${safeName}`),
|
|
289
|
+
appName: arg('--app-name', 'Third-party system'),
|
|
290
|
+
appDescription: arg('--app-description', ''),
|
|
291
|
+
authRef: arg('--auth-ref', 'default'),
|
|
292
|
+
entities: entitiesArg ? entitiesArg.split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
293
|
+
publish: process.argv.includes('--publish'),
|
|
294
|
+
},
|
|
295
|
+
io: {
|
|
296
|
+
logUser,
|
|
297
|
+
logDev,
|
|
298
|
+
publish: (manifest, meta) => publishManifest(manifest, meta),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
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
|
+
|
|
254
319
|
// Consent gate: confirm before doing anything. `yes` continues; `no` (or any
|
|
255
320
|
// other answer) cancels instantly — no analysis is run, the command just ends.
|
|
256
321
|
const proceed = await askProceed();
|
|
@@ -287,7 +352,7 @@ async function main() {
|
|
|
287
352
|
fs.mkdirSync(outDir, { recursive: true });
|
|
288
353
|
|
|
289
354
|
const platformArg = (arg('--platform', '') || '').toLowerCase();
|
|
290
|
-
const KNOWN_PLATFORMS = ['web', 'mobile', 'flutter', 'ios', 'android'];
|
|
355
|
+
const KNOWN_PLATFORMS = ['web', 'mobile', 'flutter', 'ios', 'android', 'shopify'];
|
|
291
356
|
const forcedPlatform = KNOWN_PLATFORMS.includes(platformArg) ? platformArg : null;
|
|
292
357
|
|
|
293
358
|
// ── 1. Static route scan (cheap facts; also the route verifier's ground truth)
|
|
@@ -327,6 +392,24 @@ async function main() {
|
|
|
327
392
|
|
|
328
393
|
// ── 2. Collect source files (rule-based, budget-aware) ────────────────────
|
|
329
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
|
+
|
|
330
413
|
fs.writeFileSync(path.join(outDir, 'files-sent.json'), JSON.stringify({
|
|
331
414
|
included: included.map((f) => f.rel),
|
|
332
415
|
omitted,
|
|
@@ -353,11 +436,23 @@ async function main() {
|
|
|
353
436
|
let provenance;
|
|
354
437
|
let evidence = {};
|
|
355
438
|
let wiring = {};
|
|
439
|
+
// The AI's "session" block: WHERE the app's auth/session state lives, so the
|
|
440
|
+
// orb's isAuthenticated/getUserId are wired automatically (no manual step).
|
|
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;
|
|
356
446
|
let analysisMeta = { engine: 'auto-manifest-v3-ai-first', model: null, costUSD: 0 };
|
|
357
447
|
// Signed proof returned by the hosted-analysis proxy: it tells the publish
|
|
358
448
|
// step the quota was already consumed at /v1/developer/analyze (no double
|
|
359
449
|
// count). Null in own-key / --no-llm mode.
|
|
360
450
|
let analysisToken = null;
|
|
451
|
+
// Spec-requested content resets declared by the analysis AI: which dashboard
|
|
452
|
+
// context overrides (bubblePrompt/description/examples) the developer's
|
|
453
|
+
// --spec EXPLICITLY asked to regenerate, so the backend clears those (and
|
|
454
|
+
// ONLY those) masking overrides on publish. Null when the spec asked nothing.
|
|
455
|
+
let specResets = null;
|
|
361
456
|
|
|
362
457
|
if (!useLLM) {
|
|
363
458
|
// Static-only fallback: routes with mechanical naming, no actions.
|
|
@@ -410,6 +505,14 @@ async function main() {
|
|
|
410
505
|
const proposed = parsed.manifest || {};
|
|
411
506
|
evidence = parsed.evidence || {};
|
|
412
507
|
wiring = parsed.wiring || {};
|
|
508
|
+
sessionWiring = parsed.session || null;
|
|
509
|
+
screenWiring = parsed.screen || null;
|
|
510
|
+
// Only meaningful when the developer passed --spec; the backend sanitizes
|
|
511
|
+
// (context fields only, real intents only) before touching any override.
|
|
512
|
+
specResets = (userSpec && parsed.specResets && typeof parsed.specResets === 'object')
|
|
513
|
+
? parsed.specResets
|
|
514
|
+
: null;
|
|
515
|
+
if (specResets) logDev(`[ai] specResets=${JSON.stringify(specResets)}`);
|
|
413
516
|
logDev(`[ai] model=${model} proposed routes=${(proposed.routes || []).length} ` +
|
|
414
517
|
`actions=${(proposed.actions || []).length} in ${(elapsedMs / 1000).toFixed(1)}s`);
|
|
415
518
|
logDev(`[ai] tokens in=${usage.prompt_tokens} out=${usage.completion_tokens} cost=$${costUSD.toFixed(4)}`);
|
|
@@ -418,7 +521,7 @@ async function main() {
|
|
|
418
521
|
// Actions are always verified against real code. Routes are verified only
|
|
419
522
|
// when there is filesystem ground truth (mobile); for web they are kept as
|
|
420
523
|
// AI-proposed and flagged unverified in provenance.
|
|
421
|
-
({ manifest, provenance } = verifyManifest(proposed, evidence, included, routes, { verifyRoutes }));
|
|
524
|
+
({ manifest, provenance } = verifyManifest(proposed, evidence, included, routes, { verifyRoutes, platform }));
|
|
422
525
|
ensureBackRoute(manifest, provenance);
|
|
423
526
|
const droppedActions = provenance.actions.filter((a) => !a.included).length;
|
|
424
527
|
const droppedRoutes = provenance.routes.filter((r) => !r.included).length;
|
|
@@ -427,7 +530,7 @@ async function main() {
|
|
|
427
530
|
logUser('Analysis complete');
|
|
428
531
|
|
|
429
532
|
fs.writeFileSync(path.join(outDir, 'evidence.json'), JSON.stringify({
|
|
430
|
-
evidence, wiring, uncertain: parsed.uncertain || [],
|
|
533
|
+
evidence, wiring, session: sessionWiring, screen: screenWiring, uncertain: parsed.uncertain || [],
|
|
431
534
|
}, null, 2));
|
|
432
535
|
fs.writeFileSync(path.join(outDir, 'usage.json'), JSON.stringify({
|
|
433
536
|
model, elapsedMs, usage,
|
|
@@ -524,6 +627,14 @@ async function main() {
|
|
|
524
627
|
disabledRouteIntents: surfaceScore.disabledRouteIntents || [],
|
|
525
628
|
disabledActionIntents: surfaceScore.disabledActionIntents || [],
|
|
526
629
|
},
|
|
630
|
+
// Present ONLY when the developer's --spec explicitly asked to regenerate
|
|
631
|
+
// previously-edited content (e.g. "rewrite all bubble prompts"): the backend
|
|
632
|
+
// clears the matching dashboard overrides so the fresh values become live.
|
|
633
|
+
...(specResets ? { specResets } : {}),
|
|
634
|
+
// Opt-out of the backend's continuity guard (which keeps intent ids and
|
|
635
|
+
// URL paths stable across re-analyses so dashboard customizations never
|
|
636
|
+
// break). Only for developers who explicitly want a clean slate.
|
|
637
|
+
...(process.argv.includes('--fresh-intents') ? { freshIntents: true } : {}),
|
|
527
638
|
};
|
|
528
639
|
|
|
529
640
|
// Informative, non-blocking summary (the dashboard is where scope is managed).
|
|
@@ -552,6 +663,8 @@ async function main() {
|
|
|
552
663
|
let orbWireResult = null;
|
|
553
664
|
let handlerWireResult = null;
|
|
554
665
|
let registryWireResult = null;
|
|
666
|
+
let sessionWireResult = null;
|
|
667
|
+
let screenWireResult = null;
|
|
555
668
|
const { runPostWireTest } = require('./postWireTest');
|
|
556
669
|
const selfCorrect = !process.argv.includes('--no-self-correct');
|
|
557
670
|
// Corrections follow the same key model as the analysis: hosted (proxy)
|
|
@@ -653,6 +766,92 @@ async function main() {
|
|
|
653
766
|
report(() => reportRegistriesResult(connected, fyodosTermColors(), { quiet: !isVerbose() }));
|
|
654
767
|
}
|
|
655
768
|
|
|
769
|
+
// Third pass — SESSION wiring. Connect the orb to the app's real auth
|
|
770
|
+
// state so the agent KNOWS whether the user is signed in (requiresAuth
|
|
771
|
+
// gate) and keys conversation memory per user. Fully automatic: same AI
|
|
772
|
+
// proposal + mechanical verification + reversible edits + build safety
|
|
773
|
+
// net as handler wiring. NO manual step.
|
|
774
|
+
// · consent: the handler-wiring "yes" covers code edits, so we reuse
|
|
775
|
+
// it. If the developer said NO to handler wiring, we respect that
|
|
776
|
+
// and skip session edits too.
|
|
777
|
+
if (sessionWiring && !process.argv.includes('--no-orb-wire')) {
|
|
778
|
+
const handlerDeclined =
|
|
779
|
+
handlerResult &&
|
|
780
|
+
(handlerResult.status === 'declined' || handlerResult.status === 'declined-noninteractive');
|
|
781
|
+
if (!handlerDeclined) {
|
|
782
|
+
// Only an APPLIED handler wiring proves the developer said "yes" to
|
|
783
|
+
// code edits in every path; anything else makes wireSession ask its
|
|
784
|
+
// own single [yes/no] (or decline on a non-TTY) — never edit silently.
|
|
785
|
+
const handlerConsented =
|
|
786
|
+
handlerResult &&
|
|
787
|
+
(handlerResult.status === 'applied' || handlerResult.status === 'applied-untested');
|
|
788
|
+
const sessionResult = await wireSession(analysisRoot, {
|
|
789
|
+
session: sessionWiring,
|
|
790
|
+
colors: fyodosTermColors(),
|
|
791
|
+
quiet: !isVerbose(),
|
|
792
|
+
assumeYes: assumeYes || Boolean(handlerConsented),
|
|
793
|
+
noWire,
|
|
794
|
+
framework: 'web',
|
|
795
|
+
appName: manifest.appName,
|
|
796
|
+
runTest: !process.argv.includes('--no-wire-test'),
|
|
797
|
+
testRunner: runPostWireTest,
|
|
798
|
+
});
|
|
799
|
+
sessionWireResult = sessionResult;
|
|
800
|
+
report(() => reportSessionResult(sessionResult, fyodosTermColors(), { quiet: !isVerbose() }));
|
|
801
|
+
if (
|
|
802
|
+
orbMounted &&
|
|
803
|
+
(sessionResult.status === 'applied' || sessionResult.status === 'applied-untested')
|
|
804
|
+
) {
|
|
805
|
+
const connectedSession = connectOrbSession(analysisRoot, {
|
|
806
|
+
sessionFileAbs: sessionResult.sessionFile,
|
|
807
|
+
});
|
|
808
|
+
report(() =>
|
|
809
|
+
reportSessionConnectResult(connectedSession, fyodosTermColors(), { quiet: !isVerbose() }),
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
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
|
+
|
|
656
855
|
// Finalize the install proof: now that the (slow) action wiring is done,
|
|
657
856
|
// tell the dashboard the terminal status so its "actions connected" tick
|
|
658
857
|
// turns green for real — never before wiring actually finished. Only do
|
|
@@ -667,6 +866,7 @@ async function main() {
|
|
|
667
866
|
actionsStatus: status,
|
|
668
867
|
actionsTotal: total,
|
|
669
868
|
actionsDone: done,
|
|
869
|
+
actions: actionsDetailFromHandlerResult(manifest, handlerResult),
|
|
670
870
|
});
|
|
671
871
|
}
|
|
672
872
|
};
|
|
@@ -720,6 +920,8 @@ async function main() {
|
|
|
720
920
|
orbResult: orbWireResult,
|
|
721
921
|
handlerResult: handlerWireResult,
|
|
722
922
|
registryResult: registryWireResult,
|
|
923
|
+
sessionResult: sessionWireResult,
|
|
924
|
+
screenResult: screenWireResult,
|
|
723
925
|
envResult: envWriteResult,
|
|
724
926
|
});
|
|
725
927
|
const paths = writeChangeRegistry(analysisRoot, record);
|
|
@@ -728,6 +930,66 @@ async function main() {
|
|
|
728
930
|
logDev(`[change-registry] could not write: ${e.message}`);
|
|
729
931
|
}
|
|
730
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
|
+
}
|
|
731
993
|
} else if (platform === 'mobile' && publishing) {
|
|
732
994
|
// ── Mobile (Expo / React Native): mirror the web pipeline, adapted ─────────
|
|
733
995
|
// Same single command that publishes also mounts the orb (via the SDK's
|
|
@@ -739,6 +1001,7 @@ async function main() {
|
|
|
739
1001
|
const noWire = process.argv.includes('--no-wire');
|
|
740
1002
|
let orbWireResult = null;
|
|
741
1003
|
let envWriteResult = null;
|
|
1004
|
+
let sessionWireResult = null;
|
|
742
1005
|
|
|
743
1006
|
await withSpinner('Publishing to your project', async ({ setLabel, pause, resume }) => {
|
|
744
1007
|
await publishManifest(manifest, publishMeta, analysisToken, { skipSuccessLog: true });
|
|
@@ -771,6 +1034,42 @@ async function main() {
|
|
|
771
1034
|
logDev(`[orb-wired] mobile install proof not recorded (${proof.status || proof.reason}); dashboard will rely on the runtime ping`);
|
|
772
1035
|
}
|
|
773
1036
|
}
|
|
1037
|
+
|
|
1038
|
+
// SESSION wiring (auth awareness) — same automatic pass as web. RN has
|
|
1039
|
+
// no cheap full-build check, so the safety net is the mechanical symbol
|
|
1040
|
+
// verification + the esbuild parse check on every edited file.
|
|
1041
|
+
if (
|
|
1042
|
+
sessionWiring &&
|
|
1043
|
+
(orbWireResult.status === 'added' || orbWireResult.status === 'already')
|
|
1044
|
+
) {
|
|
1045
|
+
const sessionResult = await wireSession(analysisRoot, {
|
|
1046
|
+
session: sessionWiring,
|
|
1047
|
+
colors: fyodosTermColors(),
|
|
1048
|
+
quiet: !isVerbose(),
|
|
1049
|
+
assumeYes: true, // the orb-mount consent already covered code edits on mobile
|
|
1050
|
+
noWire,
|
|
1051
|
+
framework: 'mobile',
|
|
1052
|
+
appName: manifest.appName,
|
|
1053
|
+
runTest: false,
|
|
1054
|
+
});
|
|
1055
|
+
sessionWireResult = sessionResult;
|
|
1056
|
+
pause();
|
|
1057
|
+
reportSessionResult(sessionResult, fyodosTermColors(), { quiet: !isVerbose() });
|
|
1058
|
+
resume();
|
|
1059
|
+
if (sessionResult.status === 'applied' || sessionResult.status === 'applied-untested') {
|
|
1060
|
+
const connectedSession = connectRnSession(analysisRoot, {
|
|
1061
|
+
sessionFileAbs: sessionResult.sessionFile,
|
|
1062
|
+
colors: fyodosTermColors(),
|
|
1063
|
+
});
|
|
1064
|
+
if (connectedSession.status === 'connected') {
|
|
1065
|
+
pause();
|
|
1066
|
+
console.error(`${fyodosTermColors().cyan}◉${fyodosTermColors().reset} ${fyodosTermColors().blue}Fiodos${fyodosTermColors().reset} · session connected to the orb mount (${connectedSession.file})`);
|
|
1067
|
+
resume();
|
|
1068
|
+
} else {
|
|
1069
|
+
logDev(`[session] RN mount connection: ${connectedSession.status}${connectedSession.reason ? ` (${connectedSession.reason})` : ''}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
774
1073
|
}
|
|
775
1074
|
});
|
|
776
1075
|
logUser('Published to your project');
|
|
@@ -805,6 +1104,7 @@ async function main() {
|
|
|
805
1104
|
manifest,
|
|
806
1105
|
publishMeta,
|
|
807
1106
|
orbResult: orbWireResult,
|
|
1107
|
+
sessionResult: sessionWireResult,
|
|
808
1108
|
envResult: envWriteResult,
|
|
809
1109
|
});
|
|
810
1110
|
const paths = writeChangeRegistry(analysisRoot, record);
|
|
@@ -980,6 +1280,9 @@ function detectPlatform(appRoot, appDir) {
|
|
|
980
1280
|
return found;
|
|
981
1281
|
};
|
|
982
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';
|
|
983
1286
|
if (has('pubspec.yaml')) return 'flutter';
|
|
984
1287
|
if (has('Package.swift') || hasAnyExt('.xcodeproj') || (has('Sources') && hasAnyExt('.swift'))) return 'ios';
|
|
985
1288
|
if ((has('settings.gradle.kts') || has('settings.gradle') || has('build.gradle.kts') || has('build.gradle')) && hasAnyExt('.kt')) {
|
|
@@ -1377,6 +1680,21 @@ async function publishManifest(manifest, meta, analysisToken = null, opts = {})
|
|
|
1377
1680
|
if (!opts.skipSuccessLog) {
|
|
1378
1681
|
logUser('Published to your project');
|
|
1379
1682
|
}
|
|
1683
|
+
if (body.overridesCleared > 0) {
|
|
1684
|
+
logUser(
|
|
1685
|
+
`Your specifications asked to regenerate previously-edited content: ` +
|
|
1686
|
+
`${body.overridesCleared} dashboard override(s) were cleared so the new values are live`,
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
// Continuity guard feedback: the backend kept the previous intent ids /
|
|
1690
|
+
// repaired degraded route paths, so dashboard customizations keep working.
|
|
1691
|
+
if (body.intentsPreserved > 0 || body.pathsRepaired > 0) {
|
|
1692
|
+
logUser(
|
|
1693
|
+
`Continuity with your previous analysis: ${body.intentsPreserved || 0} intent id(s) kept stable` +
|
|
1694
|
+
(body.pathsRepaired ? `, ${body.pathsRepaired} route path(s) repaired` : '') +
|
|
1695
|
+
` — your dashboard customizations stay intact (use --fresh-intents to opt out)`,
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1380
1698
|
logDev(`[publish] OK — ${body.routes} routes, ${body.actions} actions received at ${body.receivedAt}`);
|
|
1381
1699
|
|
|
1382
1700
|
// Self-check: confirm the orb will find this manifest (silent unless --verbose).
|
|
@@ -1397,6 +1715,7 @@ async function postOrbWired({
|
|
|
1397
1715
|
actionsStatus = 'pending',
|
|
1398
1716
|
actionsTotal = 0,
|
|
1399
1717
|
actionsDone = 0,
|
|
1718
|
+
actions = null,
|
|
1400
1719
|
}) {
|
|
1401
1720
|
if (!apiKey) return { ok: false, reason: 'no-api-key' };
|
|
1402
1721
|
try {
|
|
@@ -1409,6 +1728,11 @@ async function postOrbWired({
|
|
|
1409
1728
|
actionsStatus,
|
|
1410
1729
|
actionsTotal,
|
|
1411
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 } : {}),
|
|
1412
1736
|
}),
|
|
1413
1737
|
});
|
|
1414
1738
|
if (res.ok) {
|
|
@@ -1423,6 +1747,210 @@ async function postOrbWired({
|
|
|
1423
1747
|
}
|
|
1424
1748
|
}
|
|
1425
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
|
+
|
|
1426
1954
|
// Map a wireHandlers() result to the terminal action-wiring status the backend
|
|
1427
1955
|
// records, plus how many actions ended up wired. Drives the onboarding's third
|
|
1428
1956
|
// "actions connected" tick so it only goes green once wiring truly finished.
|