@fiodos/cli 0.1.18 → 0.1.19
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 +2 -0
- package/src/changeRegistry.js +534 -0
- package/src/index.js +28 -0
- package/src/verify.js +18 -0
- package/src/wireWeb.js +13 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
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
|
@@ -89,6 +89,7 @@ AppManifest (all fields required unless noted):
|
|
|
89
89
|
"intent": string (snake_case, UNIQUE across routes AND actions),
|
|
90
90
|
"label": string, "route": string (opaque route string for the app's navigation adapter — an expo-router href, a URL path, or "BACK"),
|
|
91
91
|
"description": string (one line: WHAT THIS SCREEN SHOWS and what the user can do there),
|
|
92
|
+
"actionIntents": string[] (OPTIONAL: intents of the actions a user performs FROM this screen — the action handlers actually wired into THIS screen's component/page. Use exact action intent ids from the "actions" array; [] if none. Advisory only, never gates anything),
|
|
92
93
|
"examples": string[] (3-4 natural voice phrases, REQUIRED non-empty)
|
|
93
94
|
}],
|
|
94
95
|
"actions": [{
|
|
@@ -215,6 +216,7 @@ This is the context the agent reads to understand the app, so it can reason inst
|
|
|
215
216
|
- effect: the observable outcome after it runs (state change, navigation, message).
|
|
216
217
|
- relatedIntents: other manifest intents this naturally flows into or from (e.g. add-to-cart → checkout). Use the exact intent ids; [] if none.
|
|
217
218
|
- route description: what the screen displays and what can be done there.
|
|
219
|
+
- route actionIntents: for each screen, the intents of the actions whose handlers are actually wired into THAT screen's component/page (what the user can DO there). Use exact action intent ids; omit or [] when the screen is purely informational/navigational. This is "what you can do here" context — it never restricts the agent.
|
|
218
220
|
- appType/appFlow: the app category and its main end-to-end flow.
|
|
219
221
|
Be precise and concise — quality over volume. Never pad.
|
|
220
222
|
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* changeRegistry — transparent, per-file record of every change the Fiodos
|
|
3
|
+
* installer makes in the client's repo.
|
|
4
|
+
*
|
|
5
|
+
* On each install / re-analysis run we write:
|
|
6
|
+
* · `.fyodos/changes/<runId>.json` — structured snapshot of THIS run (machine-readable)
|
|
7
|
+
* · `.fyodos/changes/index.json` — chronological index of all runs
|
|
8
|
+
* · `src/fyodos/FYODOS_CHANGES.md` — human-readable log (APPENDS; history is never lost)
|
|
9
|
+
*
|
|
10
|
+
* SECURITY: never embed API keys, secrets, or env values — only file paths, variable
|
|
11
|
+
* names, and descriptions of what was touched.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const CLI_VERSION = require('../package.json').version;
|
|
19
|
+
|
|
20
|
+
function normRel(appRoot, absOrRel) {
|
|
21
|
+
const rel = path.isAbsolute(absOrRel) ? path.relative(appRoot, absOrRel) : absOrRel;
|
|
22
|
+
return String(rel || '').replace(/\\/g, '/');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveFyodosDirRel(appRoot) {
|
|
26
|
+
if (fs.existsSync(path.join(appRoot, 'src'))) return 'src/fyodos';
|
|
27
|
+
return 'fyodos';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeRunId() {
|
|
31
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readJsonSafe(file) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function uniquePaths(list) {
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const item of list) {
|
|
46
|
+
const key = item.path;
|
|
47
|
+
if (!key || seen.has(key)) continue;
|
|
48
|
+
seen.add(key);
|
|
49
|
+
out.push(item);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Phase summarizers ────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function summarizeOrb(result) {
|
|
57
|
+
if (!result) return { status: 'skipped', applied: false };
|
|
58
|
+
const base = { status: result.status, mountFile: result.file || null };
|
|
59
|
+
switch (result.status) {
|
|
60
|
+
case 'added':
|
|
61
|
+
return {
|
|
62
|
+
...base,
|
|
63
|
+
applied: true,
|
|
64
|
+
strategy: result.planSummary?.strategy || null,
|
|
65
|
+
steps: result.planSummary?.steps || [],
|
|
66
|
+
files: result.planSummary?.files || [],
|
|
67
|
+
created: result.planSummary?.created || [],
|
|
68
|
+
revert:
|
|
69
|
+
'Remove the blocks between `FYODOS:ORB:START` and `FYODOS:ORB:END` (and the Fiodos import). See `FYODOS_ORB_MOUNT.md`.',
|
|
70
|
+
};
|
|
71
|
+
case 'already':
|
|
72
|
+
return {
|
|
73
|
+
...base,
|
|
74
|
+
applied: false,
|
|
75
|
+
note: 'The orb was already mounted from a previous run; no orb files were changed.',
|
|
76
|
+
};
|
|
77
|
+
case 'declined':
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
applied: false,
|
|
81
|
+
note: 'You declined the automatic orb mount; your source files were not modified.',
|
|
82
|
+
};
|
|
83
|
+
case 'declined-noninteractive':
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
applied: false,
|
|
87
|
+
note: 'Non-interactive terminal: orb mount was skipped. Re-run with --yes to apply.',
|
|
88
|
+
};
|
|
89
|
+
case 'printed':
|
|
90
|
+
case 'reverted':
|
|
91
|
+
case 'failed':
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
applied: false,
|
|
95
|
+
reason: result.reason || result.error || null,
|
|
96
|
+
note: 'Automatic orb mount did not complete; see the CLI output for the manual snippet.',
|
|
97
|
+
};
|
|
98
|
+
case 'skipped':
|
|
99
|
+
return { ...base, applied: false, note: 'Orb wiring was skipped (--no-wire / --no-orb-wire).' };
|
|
100
|
+
default:
|
|
101
|
+
return { ...base, applied: false };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function summarizeHandlers(result, appRoot) {
|
|
106
|
+
if (!result) return { status: 'skipped', applied: false };
|
|
107
|
+
const base = {
|
|
108
|
+
status: result.status,
|
|
109
|
+
autoCount: result.autoCount || 0,
|
|
110
|
+
manualCount: result.manualCount || 0,
|
|
111
|
+
docPath: result.docPath && appRoot ? normRel(appRoot, result.docPath) : null,
|
|
112
|
+
};
|
|
113
|
+
const applied = result.status === 'applied' || result.status === 'applied-untested';
|
|
114
|
+
|
|
115
|
+
if (applied) {
|
|
116
|
+
const bridgeEdits = (result.editedFiles || []).map((f) => ({
|
|
117
|
+
path: normRel('', f),
|
|
118
|
+
what:
|
|
119
|
+
'Added reversible `FYODOS:BRIDGE:START` / `FYODOS:BRIDGE:END` blocks (import + registerFiodosBridge call).',
|
|
120
|
+
intents: (result.plan?.auto || [])
|
|
121
|
+
.filter((e) => e.kind === 'bridge' && e.bridge?.file === f)
|
|
122
|
+
.map((e) => e.intent),
|
|
123
|
+
revert: 'Delete the `FYODOS:BRIDGE:START` … `FYODOS:BRIDGE:END` blocks in this file.',
|
|
124
|
+
}));
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
applied: true,
|
|
128
|
+
registryFile: result.registryFile ? normRel('', result.registryFile) : null,
|
|
129
|
+
bridgeFile: result.bridgeFile ? normRel('', result.bridgeFile) : null,
|
|
130
|
+
editedFiles: bridgeEdits,
|
|
131
|
+
wiredIntents: (result.plan?.auto || []).map((e) => e.intent),
|
|
132
|
+
manualIntents: (result.plan?.manual || result.plan?.review || []).map((e) => e.intent),
|
|
133
|
+
revert:
|
|
134
|
+
'Delete generated files under `src/fyodos/` (handlers.generated.*, bridge.*) and remove `FYODOS:BRIDGE` blocks from your components. See `FYODOS_HANDLERS.md`.',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const docAlways =
|
|
139
|
+
result.docPath &&
|
|
140
|
+
['declined', 'declined-noninteractive', 'manual-only', 'applied', 'applied-untested', 'reverted'].includes(
|
|
141
|
+
result.status,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...base,
|
|
146
|
+
applied: false,
|
|
147
|
+
docWritten: Boolean(docAlways),
|
|
148
|
+
note:
|
|
149
|
+
result.status === 'declined'
|
|
150
|
+
? 'You declined handler wiring; only the documentation file was written.'
|
|
151
|
+
: result.status === 'declined-noninteractive'
|
|
152
|
+
? 'Non-interactive: handler wiring skipped. Documentation was written; re-run with --wire-yes to apply.'
|
|
153
|
+
: result.status === 'manual-only'
|
|
154
|
+
? 'No actions could be wired automatically; see FYODOS_HANDLERS.md for manual steps.'
|
|
155
|
+
: result.status === 'no-actions'
|
|
156
|
+
? 'Manifest has no actions; handler wiring was not needed.'
|
|
157
|
+
: result.status === 'skipped'
|
|
158
|
+
? 'Handler wiring skipped (--no-wire).'
|
|
159
|
+
: result.status === 'reverted'
|
|
160
|
+
? 'Wiring was reverted because the post-install build failed.'
|
|
161
|
+
: null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function summarizeRegistries(result) {
|
|
166
|
+
if (!result) return { status: 'skipped', applied: false };
|
|
167
|
+
const base = { status: result.status, file: result.file || null };
|
|
168
|
+
switch (result.status) {
|
|
169
|
+
case 'connected':
|
|
170
|
+
return {
|
|
171
|
+
...base,
|
|
172
|
+
applied: true,
|
|
173
|
+
what: 'Added `registries={fyodosGeneratedRegistries}` to `<FiodosAgent />` inside the existing `FYODOS:ORB` block.',
|
|
174
|
+
revert: 'Remove the `registries={…}` prop and its import from the orb mount file.',
|
|
175
|
+
};
|
|
176
|
+
case 'already':
|
|
177
|
+
return { ...base, applied: false, note: 'Registries were already connected.' };
|
|
178
|
+
case 'no-registry':
|
|
179
|
+
case 'no-target':
|
|
180
|
+
case 'not-mounted':
|
|
181
|
+
case 'unsupported-framework':
|
|
182
|
+
return { ...base, applied: false, note: 'Registry connection was not applicable for this framework/state.' };
|
|
183
|
+
default:
|
|
184
|
+
return { ...base, applied: false };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function summarizeEnv(result) {
|
|
189
|
+
if (!result) return { status: 'skipped', applied: false };
|
|
190
|
+
const wp = result.writePlan;
|
|
191
|
+
const base = { status: result.status };
|
|
192
|
+
if (result.status === 'written' && wp) {
|
|
193
|
+
return {
|
|
194
|
+
...base,
|
|
195
|
+
applied: true,
|
|
196
|
+
envFile: wp.targetRel || wp.file || null,
|
|
197
|
+
variables: [wp.plan?.keyVar, wp.plan?.urlVar].filter(Boolean),
|
|
198
|
+
gitignoreUpdated: true,
|
|
199
|
+
note: 'API key and URL were written to your local env file (values are NOT recorded here). `.gitignore` was updated if needed.',
|
|
200
|
+
revert: `Remove the Fiodos lines from ${wp.targetRel || 'your env file'}.`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (result.status === 'already aligned') {
|
|
204
|
+
return { ...base, applied: false, note: 'Local env already matched the published project key.' };
|
|
205
|
+
}
|
|
206
|
+
if (result.status === 'declined by user') {
|
|
207
|
+
return { ...base, applied: false, note: 'You declined writing the API key to your local env file.' };
|
|
208
|
+
}
|
|
209
|
+
if (result.status === 'manual') {
|
|
210
|
+
return {
|
|
211
|
+
...base,
|
|
212
|
+
applied: false,
|
|
213
|
+
note: wp?.kind === 'manual-angular' ? 'Angular env must be set manually in environment.ts.' : 'Env write requires manual setup for this framework.',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return { ...base, applied: false };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function collectFileEntries(appRoot, ctx) {
|
|
220
|
+
const { orbResult, handlerResult, registryResult, envResult } = ctx;
|
|
221
|
+
const created = [];
|
|
222
|
+
const modified = [];
|
|
223
|
+
const docs = [];
|
|
224
|
+
|
|
225
|
+
const fyodosDir = resolveFyodosDirRel(appRoot);
|
|
226
|
+
|
|
227
|
+
// Orb
|
|
228
|
+
if (orbResult?.status === 'added' && orbResult.planSummary) {
|
|
229
|
+
for (const f of orbResult.planSummary.created || []) {
|
|
230
|
+
created.push({
|
|
231
|
+
path: f,
|
|
232
|
+
phase: 'orb',
|
|
233
|
+
what: 'Created by the orb mount installer.',
|
|
234
|
+
revert: 'Delete this file if it was created solely for Fiodos (see FYODOS_ORB_MOUNT.md).',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
for (const f of orbResult.planSummary.files || []) {
|
|
238
|
+
if ((orbResult.planSummary.created || []).includes(f)) continue;
|
|
239
|
+
modified.push({
|
|
240
|
+
path: f,
|
|
241
|
+
phase: 'orb',
|
|
242
|
+
what: 'Injected `<FiodosAgent />` (or bootstrap equivalent) between `FYODOS:ORB:START` / `FYODOS:ORB:END` markers.',
|
|
243
|
+
revert: 'Remove the `FYODOS:ORB:START` … `FYODOS:ORB:END` block and the Fiodos import.',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
docs.push({
|
|
247
|
+
path: `${fyodosDir}/FYODOS_ORB_MOUNT.md`,
|
|
248
|
+
phase: 'orb',
|
|
249
|
+
what: 'Human-readable orb mount summary (strategy, files, auth props).',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Handlers — doc is written even when wiring is declined
|
|
254
|
+
if (handlerResult?.docPath) {
|
|
255
|
+
docs.push({
|
|
256
|
+
path: normRel(appRoot, handlerResult.docPath),
|
|
257
|
+
phase: 'handlers',
|
|
258
|
+
what: 'Action wiring plan: auto-wired actions, manual steps, security notes.',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
handlerResult &&
|
|
263
|
+
(handlerResult.status === 'applied' || handlerResult.status === 'applied-untested')
|
|
264
|
+
) {
|
|
265
|
+
if (handlerResult.registryFile) {
|
|
266
|
+
created.push({
|
|
267
|
+
path: normRel(appRoot, handlerResult.registryFile),
|
|
268
|
+
phase: 'handlers',
|
|
269
|
+
what: 'Generated handler registry (`handlers.generated.*`). Regenerated on each apply.',
|
|
270
|
+
revert: 'Safe to delete; re-run the installer to regenerate.',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (handlerResult.bridgeFile) {
|
|
274
|
+
created.push({
|
|
275
|
+
path: normRel(appRoot, handlerResult.bridgeFile),
|
|
276
|
+
phase: 'handlers',
|
|
277
|
+
what: 'Bridge module for component-registered live functions.',
|
|
278
|
+
revert: 'Safe to delete together with handlers.generated.*.',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
for (const edit of summarizeHandlers(handlerResult).editedFiles || []) {
|
|
282
|
+
modified.push({ path: edit.path, phase: 'handlers', what: edit.what, revert: edit.revert, intents: edit.intents });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Registry connection
|
|
287
|
+
if (registryResult?.status === 'connected' && registryResult.file) {
|
|
288
|
+
modified.push({
|
|
289
|
+
path: registryResult.file,
|
|
290
|
+
phase: 'registries',
|
|
291
|
+
what: 'Connected `registries={fyodosGeneratedRegistries}` to the mounted orb.',
|
|
292
|
+
revert: 'Remove the registries prop and its import from the orb mount.',
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Env (never record values)
|
|
297
|
+
if (envResult?.status === 'written' && envResult.writePlan?.targetRel) {
|
|
298
|
+
modified.push({
|
|
299
|
+
path: envResult.writePlan.targetRel,
|
|
300
|
+
phase: 'env',
|
|
301
|
+
what: `Upserted ${envResult.writePlan.plan?.keyVar || 'FYODOS_API_KEY'} (and URL var if applicable). Values not logged here.`,
|
|
302
|
+
revert: 'Remove the Fiodos env lines from this file.',
|
|
303
|
+
});
|
|
304
|
+
modified.push({
|
|
305
|
+
path: '.gitignore',
|
|
306
|
+
phase: 'env',
|
|
307
|
+
what: 'Ensured the env file is git-ignored so secrets are not pushed.',
|
|
308
|
+
revert: 'Optional: remove the Fiodos gitignore comment if you revert everything.',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// This registry itself
|
|
313
|
+
docs.push({
|
|
314
|
+
path: `${fyodosDir}/FYODOS_CHANGES.md`,
|
|
315
|
+
phase: 'registry',
|
|
316
|
+
what: 'This cumulative change log (appended each run).',
|
|
317
|
+
});
|
|
318
|
+
docs.push({
|
|
319
|
+
path: '.fyodos/changes/',
|
|
320
|
+
phase: 'registry',
|
|
321
|
+
what: 'Per-run JSON snapshots and index (machine-readable history).',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
created: uniquePaths(created),
|
|
326
|
+
modified: uniquePaths(modified),
|
|
327
|
+
docs: uniquePaths(docs),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function computeDelta(appRoot, current) {
|
|
332
|
+
const indexPath = path.join(appRoot, '.fyodos', 'changes', 'index.json');
|
|
333
|
+
const index = readJsonSafe(indexPath);
|
|
334
|
+
if (!index?.runs?.length) return null;
|
|
335
|
+
|
|
336
|
+
const prevPath = path.join(appRoot, '.fyodos', 'changes', index.runs[0].jsonFile);
|
|
337
|
+
const prev = readJsonSafe(prevPath);
|
|
338
|
+
if (!prev) return null;
|
|
339
|
+
|
|
340
|
+
const prevIntents = new Set(prev.phases?.handlers?.wiredIntents || []);
|
|
341
|
+
const currIntents = new Set(current.phases?.handlers?.wiredIntents || []);
|
|
342
|
+
const addedIntents = [...currIntents].filter((i) => !prevIntents.has(i));
|
|
343
|
+
const removedIntents = [...prevIntents].filter((i) => !currIntents.has(i));
|
|
344
|
+
|
|
345
|
+
const prevFiles = new Set([
|
|
346
|
+
...(prev.files?.created || []).map((f) => f.path),
|
|
347
|
+
...(prev.files?.modified || []).map((f) => f.path),
|
|
348
|
+
]);
|
|
349
|
+
const currFiles = new Set([
|
|
350
|
+
...(current.files?.created || []).map((f) => f.path),
|
|
351
|
+
...(current.files?.modified || []).map((f) => f.path),
|
|
352
|
+
]);
|
|
353
|
+
const newFiles = [...currFiles].filter((f) => !prevFiles.has(f));
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
previousRunId: prev.runId,
|
|
357
|
+
previousTimestamp: prev.timestamp,
|
|
358
|
+
actionsAdded: addedIntents,
|
|
359
|
+
actionsRemoved: removedIntents,
|
|
360
|
+
newOrModifiedFiles: newFiles,
|
|
361
|
+
orbFirstInstall: prev.phases?.orb?.applied !== true && current.phases?.orb?.applied === true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Build a structured record for one installer run from the phase results.
|
|
367
|
+
*/
|
|
368
|
+
function buildRunRecord(appRoot, ctx) {
|
|
369
|
+
const record = {
|
|
370
|
+
runId: makeRunId(),
|
|
371
|
+
timestamp: new Date().toISOString(),
|
|
372
|
+
cliVersion: CLI_VERSION,
|
|
373
|
+
appName: ctx.appName || path.basename(appRoot),
|
|
374
|
+
manifest: {
|
|
375
|
+
actionCount: (ctx.manifest?.actions || []).length,
|
|
376
|
+
routeCount: (ctx.manifest?.routes || []).length,
|
|
377
|
+
generatedAt: ctx.publishMeta?.generatedAt || null,
|
|
378
|
+
},
|
|
379
|
+
phases: {
|
|
380
|
+
orb: summarizeOrb(ctx.orbResult),
|
|
381
|
+
handlers: summarizeHandlers(ctx.handlerResult, appRoot),
|
|
382
|
+
registries: summarizeRegistries(ctx.registryResult),
|
|
383
|
+
env: summarizeEnv(ctx.envResult),
|
|
384
|
+
},
|
|
385
|
+
files: collectFileEntries(appRoot, ctx),
|
|
386
|
+
};
|
|
387
|
+
record.delta = computeDelta(appRoot, record);
|
|
388
|
+
return record;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function renderHeader() {
|
|
392
|
+
return [
|
|
393
|
+
'# Fiodos — change log',
|
|
394
|
+
'',
|
|
395
|
+
'Every time the Fiodos installer runs (initial install or re-analysis), an entry is **appended** below.',
|
|
396
|
+
'This is your transparency record: what Fiodos created or modified in your repo, file by file.',
|
|
397
|
+
'',
|
|
398
|
+
'- **Revert orb changes:** remove `FYODOS:ORB:START` … `FYODOS:ORB:END` blocks (see `FYODOS_ORB_MOUNT.md`).',
|
|
399
|
+
'- **Revert handler wiring:** delete `handlers.generated.*`, `bridge.*`, and `FYODOS:BRIDGE` blocks (see `FYODOS_HANDLERS.md`).',
|
|
400
|
+
'- **Machine-readable history:** `.fyodos/changes/` (one JSON file per run).',
|
|
401
|
+
'',
|
|
402
|
+
'No API keys or secrets appear in this log.',
|
|
403
|
+
'',
|
|
404
|
+
'---',
|
|
405
|
+
].join('\n');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renderFileList(title, items) {
|
|
409
|
+
if (!items?.length) return [];
|
|
410
|
+
const L = [`### ${title}`, ''];
|
|
411
|
+
for (const f of items) {
|
|
412
|
+
L.push(`- \`${f.path}\`${f.intents?.length ? ` — actions: ${f.intents.map((i) => `\`${i}\``).join(', ')}` : ''}`);
|
|
413
|
+
if (f.what) L.push(` - ${f.what}`);
|
|
414
|
+
if (f.revert) L.push(` - Revert: ${f.revert}`);
|
|
415
|
+
}
|
|
416
|
+
L.push('');
|
|
417
|
+
return L;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function renderRunSection(record) {
|
|
421
|
+
const L = [];
|
|
422
|
+
L.push(`## Run ${record.timestamp}`);
|
|
423
|
+
L.push('');
|
|
424
|
+
L.push(`- CLI: \`@fiodos/cli@${record.cliVersion}\``);
|
|
425
|
+
L.push(`- App: \`${record.appName}\``);
|
|
426
|
+
L.push(`- Manifest: ${record.manifest.actionCount} action(s), ${record.manifest.routeCount} route(s)`);
|
|
427
|
+
L.push(`- Run id: \`${record.runId}\``);
|
|
428
|
+
L.push('');
|
|
429
|
+
|
|
430
|
+
if (record.delta) {
|
|
431
|
+
L.push('### Changes vs previous run');
|
|
432
|
+
L.push('');
|
|
433
|
+
if (record.delta.actionsAdded.length) {
|
|
434
|
+
L.push(`- Actions newly wired: ${record.delta.actionsAdded.map((i) => `\`${i}\``).join(', ')}`);
|
|
435
|
+
}
|
|
436
|
+
if (record.delta.actionsRemoved.length) {
|
|
437
|
+
L.push(`- Actions no longer wired: ${record.delta.actionsRemoved.map((i) => `\`${i}\``).join(', ')}`);
|
|
438
|
+
}
|
|
439
|
+
if (record.delta.newOrModifiedFiles.length) {
|
|
440
|
+
L.push(`- Files touched this run: ${record.delta.newOrModifiedFiles.map((f) => `\`${f}\``).join(', ')}`);
|
|
441
|
+
}
|
|
442
|
+
if (!record.delta.actionsAdded.length && !record.delta.actionsRemoved.length && !record.delta.newOrModifiedFiles.length) {
|
|
443
|
+
L.push('- No file changes compared to the previous run (re-analysis only updated the manifest/backend).');
|
|
444
|
+
}
|
|
445
|
+
L.push('');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const phases = [
|
|
449
|
+
['Orb mount', record.phases.orb],
|
|
450
|
+
['Action handlers', record.phases.handlers],
|
|
451
|
+
['Registry connection', record.phases.registries],
|
|
452
|
+
['Environment', record.phases.env],
|
|
453
|
+
];
|
|
454
|
+
for (const [label, ph] of phases) {
|
|
455
|
+
if (!ph || ph.status === 'skipped') continue;
|
|
456
|
+
L.push(`### ${label}`);
|
|
457
|
+
L.push('');
|
|
458
|
+
L.push(`- Status: \`${ph.status}\`${ph.applied ? ' (applied)' : ''}`);
|
|
459
|
+
if (ph.strategy) L.push(`- Strategy: \`${ph.strategy}\``);
|
|
460
|
+
if (ph.steps?.length) {
|
|
461
|
+
L.push('- Steps:');
|
|
462
|
+
for (const s of ph.steps) L.push(` - ${s}`);
|
|
463
|
+
}
|
|
464
|
+
if (ph.note) L.push(`- ${ph.note}`);
|
|
465
|
+
if (ph.revert) L.push(`- Revert: ${ph.revert}`);
|
|
466
|
+
L.push('');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
L.push(...renderFileList('Files created', record.files.created));
|
|
470
|
+
L.push(...renderFileList('Files modified', record.files.modified));
|
|
471
|
+
L.push(...renderFileList('Documentation generated', record.files.docs));
|
|
472
|
+
|
|
473
|
+
L.push('---');
|
|
474
|
+
return L.join('\n');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function briefSummary(record) {
|
|
478
|
+
const parts = [];
|
|
479
|
+
if (record.phases.orb.applied) parts.push('orb mounted');
|
|
480
|
+
if (record.phases.handlers.applied) parts.push(`${record.phases.handlers.autoCount} handlers wired`);
|
|
481
|
+
if (record.phases.registries.applied) parts.push('registries connected');
|
|
482
|
+
if (record.phases.env.applied) parts.push('env written');
|
|
483
|
+
const touched = record.files.created.length + record.files.modified.length;
|
|
484
|
+
if (!parts.length) parts.push('docs/analysis only');
|
|
485
|
+
return `${parts.join(', ')} · ${touched} file(s) touched`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Persist the run record: JSON snapshot + append to FYODOS_CHANGES.md + update index.
|
|
490
|
+
*/
|
|
491
|
+
function writeChangeRegistry(appRoot, runRecord) {
|
|
492
|
+
const changesDir = path.join(appRoot, '.fyodos', 'changes');
|
|
493
|
+
fs.mkdirSync(changesDir, { recursive: true });
|
|
494
|
+
|
|
495
|
+
const jsonAbs = path.join(changesDir, `${runRecord.runId}.json`);
|
|
496
|
+
fs.writeFileSync(jsonAbs, `${JSON.stringify(runRecord, null, 2)}\n`);
|
|
497
|
+
|
|
498
|
+
const fyodosDir = resolveFyodosDirRel(appRoot);
|
|
499
|
+
const mdAbs = path.join(appRoot, fyodosDir, 'FYODOS_CHANGES.md');
|
|
500
|
+
fs.mkdirSync(path.dirname(mdAbs), { recursive: true });
|
|
501
|
+
|
|
502
|
+
const section = renderRunSection(runRecord);
|
|
503
|
+
if (fs.existsSync(mdAbs)) {
|
|
504
|
+
const existing = fs.readFileSync(mdAbs, 'utf8').trimEnd();
|
|
505
|
+
fs.writeFileSync(mdAbs, `${existing}\n\n${section}\n`);
|
|
506
|
+
} else {
|
|
507
|
+
fs.writeFileSync(mdAbs, `${renderHeader()}\n\n${section}\n`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const indexAbs = path.join(changesDir, 'index.json');
|
|
511
|
+
const index = readJsonSafe(indexAbs) || { runs: [] };
|
|
512
|
+
index.runs.unshift({
|
|
513
|
+
runId: runRecord.runId,
|
|
514
|
+
timestamp: runRecord.timestamp,
|
|
515
|
+
cliVersion: runRecord.cliVersion,
|
|
516
|
+
jsonFile: `${runRecord.runId}.json`,
|
|
517
|
+
summary: briefSummary(runRecord),
|
|
518
|
+
});
|
|
519
|
+
fs.writeFileSync(indexAbs, `${JSON.stringify(index, null, 2)}\n`);
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
mdRel: normRel(appRoot, mdAbs),
|
|
523
|
+
jsonRel: normRel(appRoot, jsonAbs),
|
|
524
|
+
indexRel: normRel(appRoot, indexAbs),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
module.exports = {
|
|
529
|
+
buildRunRecord,
|
|
530
|
+
writeChangeRegistry,
|
|
531
|
+
resolveFyodosDirRel,
|
|
532
|
+
renderRunSection,
|
|
533
|
+
briefSummary,
|
|
534
|
+
};
|
package/src/index.js
CHANGED
|
@@ -97,6 +97,7 @@ const { wireWebOrb, reportWireResult, connectOrbRegistries, reportRegistriesResu
|
|
|
97
97
|
const { wireHandlers, reportHandlerResult } = require('./wireHandlers');
|
|
98
98
|
const { offerWriteEnv, printEnvManualReminder } = require('./writeEnv');
|
|
99
99
|
const { scoreSurface, computeSurface } = require('./scoreSurface');
|
|
100
|
+
const { buildRunRecord, writeChangeRegistry } = require('./changeRegistry');
|
|
100
101
|
|
|
101
102
|
function arg(flag, fallback) {
|
|
102
103
|
const i = process.argv.indexOf(flag);
|
|
@@ -503,6 +504,9 @@ async function main() {
|
|
|
503
504
|
const assumeYes = process.argv.includes('--yes') || process.argv.includes('--wire-yes');
|
|
504
505
|
const noWire = process.argv.includes('--no-wire');
|
|
505
506
|
let envWriteResult = null;
|
|
507
|
+
let orbWireResult = null;
|
|
508
|
+
let handlerWireResult = null;
|
|
509
|
+
let registryWireResult = null;
|
|
506
510
|
const { runPostWireTest } = require('./postWireTest');
|
|
507
511
|
const selfCorrect = !process.argv.includes('--no-self-correct');
|
|
508
512
|
const corrector = selfCorrect
|
|
@@ -549,6 +553,7 @@ async function main() {
|
|
|
549
553
|
noWire,
|
|
550
554
|
runTest: !process.argv.includes('--no-wire-test'),
|
|
551
555
|
});
|
|
556
|
+
orbWireResult = result;
|
|
552
557
|
if (!assumeYes) resume();
|
|
553
558
|
report(() => reportWireResult(result, fyodosTermColors(), { quiet: !isVerbose() }));
|
|
554
559
|
orbMounted = result.status === 'added' || result.status === 'already';
|
|
@@ -576,6 +581,7 @@ async function main() {
|
|
|
576
581
|
setLabel(wiringSpinnerLabel(platform));
|
|
577
582
|
if (!assumeYes) pause();
|
|
578
583
|
const handlerResult = await wireHandlers(analysisRoot, handlerOpts);
|
|
584
|
+
handlerWireResult = handlerResult;
|
|
579
585
|
if (!assumeYes) resume();
|
|
580
586
|
report(() => reportHandlerResult(handlerResult, fyodosTermColors(), { quiet: !isVerbose() }));
|
|
581
587
|
|
|
@@ -593,6 +599,7 @@ async function main() {
|
|
|
593
599
|
const connected = connectOrbRegistries(analysisRoot, {
|
|
594
600
|
registryFileAbs: handlerResult.registryFile,
|
|
595
601
|
});
|
|
602
|
+
registryWireResult = connected;
|
|
596
603
|
report(() => reportRegistriesResult(connected, fyodosTermColors(), { quiet: !isVerbose() }));
|
|
597
604
|
}
|
|
598
605
|
|
|
@@ -650,6 +657,27 @@ async function main() {
|
|
|
650
657
|
logDev(`[write-env] skipped: ${e.message}`);
|
|
651
658
|
}
|
|
652
659
|
}
|
|
660
|
+
|
|
661
|
+
// ── Change registry (transparency): record every file Fiodos touched ───────
|
|
662
|
+
// Appends to FYODOS_CHANGES.md and writes .fyodos/changes/<runId>.json.
|
|
663
|
+
// Runs after orb + handler + env so the record reflects the full pass.
|
|
664
|
+
if (!noWire) {
|
|
665
|
+
try {
|
|
666
|
+
const record = buildRunRecord(analysisRoot, {
|
|
667
|
+
appName: manifest.appName,
|
|
668
|
+
manifest,
|
|
669
|
+
publishMeta,
|
|
670
|
+
orbResult: orbWireResult,
|
|
671
|
+
handlerResult: handlerWireResult,
|
|
672
|
+
registryResult: registryWireResult,
|
|
673
|
+
envResult: envWriteResult,
|
|
674
|
+
});
|
|
675
|
+
const paths = writeChangeRegistry(analysisRoot, record);
|
|
676
|
+
logUser(`Change log: ${paths.mdRel} (history in .fyodos/changes/)`);
|
|
677
|
+
} catch (e) {
|
|
678
|
+
logDev(`[change-registry] could not write: ${e.message}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
653
681
|
} else if (publishing) {
|
|
654
682
|
loadAppEnv(analysisRoot);
|
|
655
683
|
await withSpinner('Publishing to your project', () =>
|
package/src/verify.js
CHANGED
|
@@ -179,6 +179,24 @@ function verifyManifest(manifest, evidence, sentFiles, staticRoutes, opts = {})
|
|
|
179
179
|
return true;
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
// Layer 2 — "what you can do here": keep each route's actionIntents fail-open.
|
|
183
|
+
// Only intents that survived action verification stay (a dangling intent
|
|
184
|
+
// pointing at a dropped/hallucinated action is silently removed, deduped).
|
|
185
|
+
// Empty/invalid → the field is removed entirely (Layer 1 still works).
|
|
186
|
+
const keptActionIntents = new Set(dedupedActions.map((a) => a.intent));
|
|
187
|
+
for (const route of dedupedRoutes) {
|
|
188
|
+
if (!Array.isArray(route.actionIntents)) {
|
|
189
|
+
delete route.actionIntents;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const seenIntent = new Set();
|
|
193
|
+
const cleaned = route.actionIntents.filter(
|
|
194
|
+
(i) => typeof i === 'string' && keptActionIntents.has(i) && !seenIntent.has(i) && seenIntent.add(i),
|
|
195
|
+
);
|
|
196
|
+
if (cleaned.length > 0) route.actionIntents = cleaned;
|
|
197
|
+
else delete route.actionIntents;
|
|
198
|
+
}
|
|
199
|
+
|
|
182
200
|
// Schema coercion the model sometimes needs (same as the old engine).
|
|
183
201
|
for (const action of dedupedActions) {
|
|
184
202
|
for (const spec of Object.values(action.parameters || {})) {
|
package/src/wireWeb.js
CHANGED
|
@@ -32,6 +32,16 @@ const {
|
|
|
32
32
|
WRAPPER_BASENAME,
|
|
33
33
|
} = require('./wireWebMount');
|
|
34
34
|
|
|
35
|
+
function planSummary(appRoot, plan, createdFiles) {
|
|
36
|
+
const rel = (f) => path.relative(appRoot, f).replace(/\\/g, '/');
|
|
37
|
+
return {
|
|
38
|
+
strategy: plan.strategy,
|
|
39
|
+
files: (plan.files || []).map(rel),
|
|
40
|
+
created: (createdFiles || []).map(rel),
|
|
41
|
+
steps: describePlan(plan),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
function readJsonSafe(file) {
|
|
36
46
|
try {
|
|
37
47
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -295,7 +305,7 @@ async function wireWebOrb(appRoot, opts = {}) {
|
|
|
295
305
|
const test = await build();
|
|
296
306
|
if (test.ok || test.stage === 'skipped-no-deps') {
|
|
297
307
|
writeConsentDoc(appRoot, plan);
|
|
298
|
-
return { status: 'added', file: plan.rel, test };
|
|
308
|
+
return { status: 'added', file: plan.rel, test, planSummary: planSummary(appRoot, plan, createdFiles) };
|
|
299
309
|
}
|
|
300
310
|
// Build failed with the orb mounted — was the app building WITHOUT it?
|
|
301
311
|
revertAll();
|
|
@@ -309,11 +319,11 @@ async function wireWebOrb(appRoot, opts = {}) {
|
|
|
309
319
|
applyMount(plan);
|
|
310
320
|
verifyMounted(plan);
|
|
311
321
|
writeConsentDoc(appRoot, plan);
|
|
312
|
-
return { status: 'added', file: plan.rel, test, preexistingFailure: true };
|
|
322
|
+
return { status: 'added', file: plan.rel, test, preexistingFailure: true, planSummary: planSummary(appRoot, plan, createdFiles) };
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
writeConsentDoc(appRoot, plan);
|
|
316
|
-
return { status: 'added', file: plan.rel };
|
|
326
|
+
return { status: 'added', file: plan.rel, planSummary: planSummary(appRoot, plan, createdFiles) };
|
|
317
327
|
} catch (err) {
|
|
318
328
|
revertAll();
|
|
319
329
|
printSnippet(target, framework, colors, appRoot, err.message || String(err));
|