@fiodos/cli 0.1.23 → 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 +4 -3
- package/src/changeRegistry.js +55 -1
- package/src/index.js +169 -3
- package/src/odata.js +529 -0
- package/src/wireHandlers.js +22 -0
- package/src/wireReactNative.js +52 -0
- package/src/wireSession.js +619 -0
- package/src/wireWeb.js +102 -0
- package/src/wireWebMount.js +87 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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/changeRegistry.js
CHANGED
|
@@ -214,8 +214,31 @@ 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
|
+
|
|
217
240
|
function collectFileEntries(appRoot, ctx) {
|
|
218
|
-
const { orbResult, handlerResult, registryResult, envResult } = ctx;
|
|
241
|
+
const { orbResult, handlerResult, registryResult, sessionResult, envResult } = ctx;
|
|
219
242
|
const created = [];
|
|
220
243
|
const modified = [];
|
|
221
244
|
const docs = [];
|
|
@@ -291,6 +314,36 @@ function collectFileEntries(appRoot, ctx) {
|
|
|
291
314
|
});
|
|
292
315
|
}
|
|
293
316
|
|
|
317
|
+
// Session wiring (auth awareness — automatic)
|
|
318
|
+
if (
|
|
319
|
+
sessionResult &&
|
|
320
|
+
(sessionResult.status === 'applied' || sessionResult.status === 'applied-untested')
|
|
321
|
+
) {
|
|
322
|
+
if (sessionResult.sessionFile) {
|
|
323
|
+
created.push({
|
|
324
|
+
path: normRel(appRoot, sessionResult.sessionFile),
|
|
325
|
+
phase: 'session',
|
|
326
|
+
what: 'Generated session module (`session.generated.*`): isAuthenticated/getUserId for the orb.',
|
|
327
|
+
revert: 'Safe to delete; re-run the installer to regenerate.',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (sessionResult.editedFile) {
|
|
331
|
+
modified.push({
|
|
332
|
+
path: sessionResult.editedFile,
|
|
333
|
+
phase: 'session',
|
|
334
|
+
what: 'Registered the live session in the bridge (reversible FYODOS:SESSION block).',
|
|
335
|
+
revert: 'Remove the FYODOS:SESSION blocks.',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (sessionResult.docPath) {
|
|
339
|
+
docs.push({
|
|
340
|
+
path: normRel(appRoot, sessionResult.docPath),
|
|
341
|
+
phase: 'session',
|
|
342
|
+
what: 'Session wiring summary: strategy, honesty contract, security notes.',
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
294
347
|
// Env (never record values)
|
|
295
348
|
if (envResult?.status === 'written' && envResult.writePlan?.targetRel) {
|
|
296
349
|
modified.push({
|
|
@@ -379,6 +432,7 @@ function buildRunRecord(appRoot, ctx) {
|
|
|
379
432
|
orb: summarizeOrb(ctx.orbResult),
|
|
380
433
|
handlers: summarizeHandlers(ctx.handlerResult, appRoot),
|
|
381
434
|
registries: summarizeRegistries(ctx.registryResult),
|
|
435
|
+
session: summarizeSession(ctx.sessionResult),
|
|
382
436
|
env: summarizeEnv(ctx.envResult),
|
|
383
437
|
},
|
|
384
438
|
files: collectFileEntries(appRoot, ctx),
|
package/src/index.js
CHANGED
|
@@ -94,10 +94,14 @@ 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
|
+
} = require('./wireWeb');
|
|
101
|
+
const { wireReactNativeOrb, reportReactNativeWire, connectRnSession } = require('./wireReactNative');
|
|
99
102
|
const { wireFlutterOrb, writeFlutterEnv, reportFlutterWire } = require('./wireFlutter');
|
|
100
103
|
const { wireHandlers, reportHandlerResult } = require('./wireHandlers');
|
|
104
|
+
const { wireSession, reportSessionResult } = require('./wireSession');
|
|
101
105
|
const { offerWriteEnv, printEnvManualReminder } = require('./writeEnv');
|
|
102
106
|
const { scoreSurface, computeSurface } = require('./scoreSurface');
|
|
103
107
|
const { buildRunRecord, writeChangeRegistry } = require('./changeRegistry');
|
|
@@ -113,6 +117,8 @@ function arg(flag, fallback) {
|
|
|
113
117
|
const VALUE_FLAGS = new Set([
|
|
114
118
|
'--out', '--model', '--max-input-tokens', '--platform', '--api-key', '--api-url',
|
|
115
119
|
'--spec', '--analysis-root',
|
|
120
|
+
// from-odata (closed-software mode)
|
|
121
|
+
'--app-id', '--app-name', '--app-description', '--auth-ref', '--entities',
|
|
116
122
|
]);
|
|
117
123
|
|
|
118
124
|
/**
|
|
@@ -251,6 +257,42 @@ async function withSpinner(label, work) {
|
|
|
251
257
|
}
|
|
252
258
|
|
|
253
259
|
async function main() {
|
|
260
|
+
// ── Closed-software mode: `fiodos from-odata <metadata.xml|url>` ────────────
|
|
261
|
+
// Generates the manifest from a third-party system's OData $metadata (schema,
|
|
262
|
+
// not source code), so the consent gate about code analysis does not apply.
|
|
263
|
+
{
|
|
264
|
+
const pos = positionalArgs();
|
|
265
|
+
if (pos[0] === 'from-odata') {
|
|
266
|
+
const target = pos[1];
|
|
267
|
+
if (!target) {
|
|
268
|
+
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_…]');
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
const { runFromOData } = require('./odata');
|
|
272
|
+
const safeName = path.basename(target).replace(/[^a-z0-9_-]/gi, '_') || 'odata';
|
|
273
|
+
const outDirOData = arg('--out', path.join(os.tmpdir(), 'fiodos', 'analysis', safeName));
|
|
274
|
+
const entitiesArg = arg('--entities', '');
|
|
275
|
+
await runFromOData({
|
|
276
|
+
target,
|
|
277
|
+
outDir: outDirOData,
|
|
278
|
+
opts: {
|
|
279
|
+
appId: arg('--app-id', `odata.${safeName}`),
|
|
280
|
+
appName: arg('--app-name', 'Third-party system'),
|
|
281
|
+
appDescription: arg('--app-description', ''),
|
|
282
|
+
authRef: arg('--auth-ref', 'default'),
|
|
283
|
+
entities: entitiesArg ? entitiesArg.split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
284
|
+
publish: process.argv.includes('--publish'),
|
|
285
|
+
},
|
|
286
|
+
io: {
|
|
287
|
+
logUser,
|
|
288
|
+
logDev,
|
|
289
|
+
publish: (manifest, meta) => publishManifest(manifest, meta),
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
254
296
|
// Consent gate: confirm before doing anything. `yes` continues; `no` (or any
|
|
255
297
|
// other answer) cancels instantly — no analysis is run, the command just ends.
|
|
256
298
|
const proceed = await askProceed();
|
|
@@ -353,11 +395,19 @@ async function main() {
|
|
|
353
395
|
let provenance;
|
|
354
396
|
let evidence = {};
|
|
355
397
|
let wiring = {};
|
|
398
|
+
// The AI's "session" block: WHERE the app's auth/session state lives, so the
|
|
399
|
+
// orb's isAuthenticated/getUserId are wired automatically (no manual step).
|
|
400
|
+
let sessionWiring = null;
|
|
356
401
|
let analysisMeta = { engine: 'auto-manifest-v3-ai-first', model: null, costUSD: 0 };
|
|
357
402
|
// Signed proof returned by the hosted-analysis proxy: it tells the publish
|
|
358
403
|
// step the quota was already consumed at /v1/developer/analyze (no double
|
|
359
404
|
// count). Null in own-key / --no-llm mode.
|
|
360
405
|
let analysisToken = null;
|
|
406
|
+
// Spec-requested content resets declared by the analysis AI: which dashboard
|
|
407
|
+
// context overrides (bubblePrompt/description/examples) the developer's
|
|
408
|
+
// --spec EXPLICITLY asked to regenerate, so the backend clears those (and
|
|
409
|
+
// ONLY those) masking overrides on publish. Null when the spec asked nothing.
|
|
410
|
+
let specResets = null;
|
|
361
411
|
|
|
362
412
|
if (!useLLM) {
|
|
363
413
|
// Static-only fallback: routes with mechanical naming, no actions.
|
|
@@ -410,6 +460,13 @@ async function main() {
|
|
|
410
460
|
const proposed = parsed.manifest || {};
|
|
411
461
|
evidence = parsed.evidence || {};
|
|
412
462
|
wiring = parsed.wiring || {};
|
|
463
|
+
sessionWiring = parsed.session || null;
|
|
464
|
+
// Only meaningful when the developer passed --spec; the backend sanitizes
|
|
465
|
+
// (context fields only, real intents only) before touching any override.
|
|
466
|
+
specResets = (userSpec && parsed.specResets && typeof parsed.specResets === 'object')
|
|
467
|
+
? parsed.specResets
|
|
468
|
+
: null;
|
|
469
|
+
if (specResets) logDev(`[ai] specResets=${JSON.stringify(specResets)}`);
|
|
413
470
|
logDev(`[ai] model=${model} proposed routes=${(proposed.routes || []).length} ` +
|
|
414
471
|
`actions=${(proposed.actions || []).length} in ${(elapsedMs / 1000).toFixed(1)}s`);
|
|
415
472
|
logDev(`[ai] tokens in=${usage.prompt_tokens} out=${usage.completion_tokens} cost=$${costUSD.toFixed(4)}`);
|
|
@@ -427,7 +484,7 @@ async function main() {
|
|
|
427
484
|
logUser('Analysis complete');
|
|
428
485
|
|
|
429
486
|
fs.writeFileSync(path.join(outDir, 'evidence.json'), JSON.stringify({
|
|
430
|
-
evidence, wiring, uncertain: parsed.uncertain || [],
|
|
487
|
+
evidence, wiring, session: sessionWiring, uncertain: parsed.uncertain || [],
|
|
431
488
|
}, null, 2));
|
|
432
489
|
fs.writeFileSync(path.join(outDir, 'usage.json'), JSON.stringify({
|
|
433
490
|
model, elapsedMs, usage,
|
|
@@ -524,6 +581,14 @@ async function main() {
|
|
|
524
581
|
disabledRouteIntents: surfaceScore.disabledRouteIntents || [],
|
|
525
582
|
disabledActionIntents: surfaceScore.disabledActionIntents || [],
|
|
526
583
|
},
|
|
584
|
+
// Present ONLY when the developer's --spec explicitly asked to regenerate
|
|
585
|
+
// previously-edited content (e.g. "rewrite all bubble prompts"): the backend
|
|
586
|
+
// clears the matching dashboard overrides so the fresh values become live.
|
|
587
|
+
...(specResets ? { specResets } : {}),
|
|
588
|
+
// Opt-out of the backend's continuity guard (which keeps intent ids and
|
|
589
|
+
// URL paths stable across re-analyses so dashboard customizations never
|
|
590
|
+
// break). Only for developers who explicitly want a clean slate.
|
|
591
|
+
...(process.argv.includes('--fresh-intents') ? { freshIntents: true } : {}),
|
|
527
592
|
};
|
|
528
593
|
|
|
529
594
|
// Informative, non-blocking summary (the dashboard is where scope is managed).
|
|
@@ -552,6 +617,7 @@ async function main() {
|
|
|
552
617
|
let orbWireResult = null;
|
|
553
618
|
let handlerWireResult = null;
|
|
554
619
|
let registryWireResult = null;
|
|
620
|
+
let sessionWireResult = null;
|
|
555
621
|
const { runPostWireTest } = require('./postWireTest');
|
|
556
622
|
const selfCorrect = !process.argv.includes('--no-self-correct');
|
|
557
623
|
// Corrections follow the same key model as the analysis: hosted (proxy)
|
|
@@ -653,6 +719,52 @@ async function main() {
|
|
|
653
719
|
report(() => reportRegistriesResult(connected, fyodosTermColors(), { quiet: !isVerbose() }));
|
|
654
720
|
}
|
|
655
721
|
|
|
722
|
+
// Third pass — SESSION wiring. Connect the orb to the app's real auth
|
|
723
|
+
// state so the agent KNOWS whether the user is signed in (requiresAuth
|
|
724
|
+
// gate) and keys conversation memory per user. Fully automatic: same AI
|
|
725
|
+
// proposal + mechanical verification + reversible edits + build safety
|
|
726
|
+
// net as handler wiring. NO manual step.
|
|
727
|
+
// · consent: the handler-wiring "yes" covers code edits, so we reuse
|
|
728
|
+
// it. If the developer said NO to handler wiring, we respect that
|
|
729
|
+
// and skip session edits too.
|
|
730
|
+
if (sessionWiring && !process.argv.includes('--no-orb-wire')) {
|
|
731
|
+
const handlerDeclined =
|
|
732
|
+
handlerResult &&
|
|
733
|
+
(handlerResult.status === 'declined' || handlerResult.status === 'declined-noninteractive');
|
|
734
|
+
if (!handlerDeclined) {
|
|
735
|
+
// Only an APPLIED handler wiring proves the developer said "yes" to
|
|
736
|
+
// code edits in every path; anything else makes wireSession ask its
|
|
737
|
+
// own single [yes/no] (or decline on a non-TTY) — never edit silently.
|
|
738
|
+
const handlerConsented =
|
|
739
|
+
handlerResult &&
|
|
740
|
+
(handlerResult.status === 'applied' || handlerResult.status === 'applied-untested');
|
|
741
|
+
const sessionResult = await wireSession(analysisRoot, {
|
|
742
|
+
session: sessionWiring,
|
|
743
|
+
colors: fyodosTermColors(),
|
|
744
|
+
quiet: !isVerbose(),
|
|
745
|
+
assumeYes: assumeYes || Boolean(handlerConsented),
|
|
746
|
+
noWire,
|
|
747
|
+
framework: 'web',
|
|
748
|
+
appName: manifest.appName,
|
|
749
|
+
runTest: !process.argv.includes('--no-wire-test'),
|
|
750
|
+
testRunner: runPostWireTest,
|
|
751
|
+
});
|
|
752
|
+
sessionWireResult = sessionResult;
|
|
753
|
+
report(() => reportSessionResult(sessionResult, fyodosTermColors(), { quiet: !isVerbose() }));
|
|
754
|
+
if (
|
|
755
|
+
orbMounted &&
|
|
756
|
+
(sessionResult.status === 'applied' || sessionResult.status === 'applied-untested')
|
|
757
|
+
) {
|
|
758
|
+
const connectedSession = connectOrbSession(analysisRoot, {
|
|
759
|
+
sessionFileAbs: sessionResult.sessionFile,
|
|
760
|
+
});
|
|
761
|
+
report(() =>
|
|
762
|
+
reportSessionConnectResult(connectedSession, fyodosTermColors(), { quiet: !isVerbose() }),
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
656
768
|
// Finalize the install proof: now that the (slow) action wiring is done,
|
|
657
769
|
// tell the dashboard the terminal status so its "actions connected" tick
|
|
658
770
|
// turns green for real — never before wiring actually finished. Only do
|
|
@@ -720,6 +832,7 @@ async function main() {
|
|
|
720
832
|
orbResult: orbWireResult,
|
|
721
833
|
handlerResult: handlerWireResult,
|
|
722
834
|
registryResult: registryWireResult,
|
|
835
|
+
sessionResult: sessionWireResult,
|
|
723
836
|
envResult: envWriteResult,
|
|
724
837
|
});
|
|
725
838
|
const paths = writeChangeRegistry(analysisRoot, record);
|
|
@@ -739,6 +852,7 @@ async function main() {
|
|
|
739
852
|
const noWire = process.argv.includes('--no-wire');
|
|
740
853
|
let orbWireResult = null;
|
|
741
854
|
let envWriteResult = null;
|
|
855
|
+
let sessionWireResult = null;
|
|
742
856
|
|
|
743
857
|
await withSpinner('Publishing to your project', async ({ setLabel, pause, resume }) => {
|
|
744
858
|
await publishManifest(manifest, publishMeta, analysisToken, { skipSuccessLog: true });
|
|
@@ -771,6 +885,42 @@ async function main() {
|
|
|
771
885
|
logDev(`[orb-wired] mobile install proof not recorded (${proof.status || proof.reason}); dashboard will rely on the runtime ping`);
|
|
772
886
|
}
|
|
773
887
|
}
|
|
888
|
+
|
|
889
|
+
// SESSION wiring (auth awareness) — same automatic pass as web. RN has
|
|
890
|
+
// no cheap full-build check, so the safety net is the mechanical symbol
|
|
891
|
+
// verification + the esbuild parse check on every edited file.
|
|
892
|
+
if (
|
|
893
|
+
sessionWiring &&
|
|
894
|
+
(orbWireResult.status === 'added' || orbWireResult.status === 'already')
|
|
895
|
+
) {
|
|
896
|
+
const sessionResult = await wireSession(analysisRoot, {
|
|
897
|
+
session: sessionWiring,
|
|
898
|
+
colors: fyodosTermColors(),
|
|
899
|
+
quiet: !isVerbose(),
|
|
900
|
+
assumeYes: true, // the orb-mount consent already covered code edits on mobile
|
|
901
|
+
noWire,
|
|
902
|
+
framework: 'mobile',
|
|
903
|
+
appName: manifest.appName,
|
|
904
|
+
runTest: false,
|
|
905
|
+
});
|
|
906
|
+
sessionWireResult = sessionResult;
|
|
907
|
+
pause();
|
|
908
|
+
reportSessionResult(sessionResult, fyodosTermColors(), { quiet: !isVerbose() });
|
|
909
|
+
resume();
|
|
910
|
+
if (sessionResult.status === 'applied' || sessionResult.status === 'applied-untested') {
|
|
911
|
+
const connectedSession = connectRnSession(analysisRoot, {
|
|
912
|
+
sessionFileAbs: sessionResult.sessionFile,
|
|
913
|
+
colors: fyodosTermColors(),
|
|
914
|
+
});
|
|
915
|
+
if (connectedSession.status === 'connected') {
|
|
916
|
+
pause();
|
|
917
|
+
console.error(`${fyodosTermColors().cyan}◉${fyodosTermColors().reset} ${fyodosTermColors().blue}Fiodos${fyodosTermColors().reset} · session connected to the orb mount (${connectedSession.file})`);
|
|
918
|
+
resume();
|
|
919
|
+
} else {
|
|
920
|
+
logDev(`[session] RN mount connection: ${connectedSession.status}${connectedSession.reason ? ` (${connectedSession.reason})` : ''}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
774
924
|
}
|
|
775
925
|
});
|
|
776
926
|
logUser('Published to your project');
|
|
@@ -805,6 +955,7 @@ async function main() {
|
|
|
805
955
|
manifest,
|
|
806
956
|
publishMeta,
|
|
807
957
|
orbResult: orbWireResult,
|
|
958
|
+
sessionResult: sessionWireResult,
|
|
808
959
|
envResult: envWriteResult,
|
|
809
960
|
});
|
|
810
961
|
const paths = writeChangeRegistry(analysisRoot, record);
|
|
@@ -1377,6 +1528,21 @@ async function publishManifest(manifest, meta, analysisToken = null, opts = {})
|
|
|
1377
1528
|
if (!opts.skipSuccessLog) {
|
|
1378
1529
|
logUser('Published to your project');
|
|
1379
1530
|
}
|
|
1531
|
+
if (body.overridesCleared > 0) {
|
|
1532
|
+
logUser(
|
|
1533
|
+
`Your specifications asked to regenerate previously-edited content: ` +
|
|
1534
|
+
`${body.overridesCleared} dashboard override(s) were cleared so the new values are live`,
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
// Continuity guard feedback: the backend kept the previous intent ids /
|
|
1538
|
+
// repaired degraded route paths, so dashboard customizations keep working.
|
|
1539
|
+
if (body.intentsPreserved > 0 || body.pathsRepaired > 0) {
|
|
1540
|
+
logUser(
|
|
1541
|
+
`Continuity with your previous analysis: ${body.intentsPreserved || 0} intent id(s) kept stable` +
|
|
1542
|
+
(body.pathsRepaired ? `, ${body.pathsRepaired} route path(s) repaired` : '') +
|
|
1543
|
+
` — your dashboard customizations stay intact (use --fresh-intents to opt out)`,
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1380
1546
|
logDev(`[publish] OK — ${body.routes} routes, ${body.actions} actions received at ${body.receivedAt}`);
|
|
1381
1547
|
|
|
1382
1548
|
// Self-check: confirm the orb will find this manifest (silent unless --verbose).
|