@tyroneross/navgator 0.1.0
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/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +486 -0
- package/agents/architecture-advisor.md +109 -0
- package/commands/nav-check.md +64 -0
- package/commands/nav-connections.md +58 -0
- package/commands/nav-diagram.md +106 -0
- package/commands/nav-export.md +71 -0
- package/commands/nav-impact.md +58 -0
- package/commands/nav-scan.md +46 -0
- package/commands/nav-status.md +44 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +627 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config.d.ts +95 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +262 -0
- package/dist/config.js.map +1 -0
- package/dist/diagram.d.ts +36 -0
- package/dist/diagram.d.ts.map +1 -0
- package/dist/diagram.js +333 -0
- package/dist/diagram.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner.d.ts +57 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +282 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scanners/connections/ast-scanner.d.ts +26 -0
- package/dist/scanners/connections/ast-scanner.d.ts.map +1 -0
- package/dist/scanners/connections/ast-scanner.js +430 -0
- package/dist/scanners/connections/ast-scanner.js.map +1 -0
- package/dist/scanners/connections/service-calls.d.ts +14 -0
- package/dist/scanners/connections/service-calls.d.ts.map +1 -0
- package/dist/scanners/connections/service-calls.js +719 -0
- package/dist/scanners/connections/service-calls.js.map +1 -0
- package/dist/scanners/infrastructure/index.d.ts +27 -0
- package/dist/scanners/infrastructure/index.d.ts.map +1 -0
- package/dist/scanners/infrastructure/index.js +233 -0
- package/dist/scanners/infrastructure/index.js.map +1 -0
- package/dist/scanners/packages/npm.d.ts +18 -0
- package/dist/scanners/packages/npm.d.ts.map +1 -0
- package/dist/scanners/packages/npm.js +256 -0
- package/dist/scanners/packages/npm.js.map +1 -0
- package/dist/scanners/packages/pip.d.ts +14 -0
- package/dist/scanners/packages/pip.d.ts.map +1 -0
- package/dist/scanners/packages/pip.js +228 -0
- package/dist/scanners/packages/pip.js.map +1 -0
- package/dist/scanners/prompts/detector.d.ts +119 -0
- package/dist/scanners/prompts/detector.d.ts.map +1 -0
- package/dist/scanners/prompts/detector.js +617 -0
- package/dist/scanners/prompts/detector.js.map +1 -0
- package/dist/scanners/prompts/index.d.ts +51 -0
- package/dist/scanners/prompts/index.d.ts.map +1 -0
- package/dist/scanners/prompts/index.js +340 -0
- package/dist/scanners/prompts/index.js.map +1 -0
- package/dist/scanners/prompts/types.d.ts +127 -0
- package/dist/scanners/prompts/types.d.ts.map +1 -0
- package/dist/scanners/prompts/types.js +37 -0
- package/dist/scanners/prompts/types.js.map +1 -0
- package/dist/setup.d.ts +65 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +261 -0
- package/dist/setup.js.map +1 -0
- package/dist/storage.d.ts +147 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +931 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +55 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-server.d.ts +17 -0
- package/dist/ui-server.d.ts.map +1 -0
- package/dist/ui-server.js +815 -0
- package/dist/ui-server.js.map +1 -0
- package/hooks/hooks.json +57 -0
- package/package.json +80 -0
- package/scripts/ibr-ui-test.mjs +359 -0
- package/scripts/postinstall.cjs +35 -0
- package/skills/architecture-awareness/SKILL.md +141 -0
package/dist/storage.js
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavGator Storage System
|
|
3
|
+
* File-based persistence for components and connections
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
import { getConfig, getComponentsPath, getConnectionsPath, getIndexPath, getGraphPath, getSnapshotsPath, getHashesPath, getSummaryPath, getSummaryFullPath, getFileMapPath, getPromptsPath, ensureStorageDirectories, isValidComponentId, isValidConnectionId, } from './config.js';
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// COMPONENT STORAGE
|
|
11
|
+
// =============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Store a component to disk
|
|
14
|
+
*/
|
|
15
|
+
export async function storeComponent(component, config, projectRoot) {
|
|
16
|
+
const cfg = config || getConfig();
|
|
17
|
+
ensureStorageDirectories(cfg, projectRoot);
|
|
18
|
+
const componentsPath = getComponentsPath(cfg, projectRoot);
|
|
19
|
+
const filePath = path.join(componentsPath, `${component.component_id}.json`);
|
|
20
|
+
await fs.promises.writeFile(filePath, JSON.stringify(component, null, 2), 'utf-8');
|
|
21
|
+
return {
|
|
22
|
+
component_id: component.component_id,
|
|
23
|
+
file_path: filePath,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load a component by ID
|
|
28
|
+
*/
|
|
29
|
+
export async function loadComponent(componentId, config, projectRoot) {
|
|
30
|
+
if (!isValidComponentId(componentId)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const cfg = config || getConfig();
|
|
34
|
+
const componentsPath = getComponentsPath(cfg, projectRoot);
|
|
35
|
+
const filePath = path.join(componentsPath, `${componentId}.json`);
|
|
36
|
+
if (!fs.existsSync(filePath)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load all components (parallelized for efficiency)
|
|
49
|
+
*/
|
|
50
|
+
export async function loadAllComponents(config, projectRoot) {
|
|
51
|
+
const cfg = config || getConfig();
|
|
52
|
+
const componentsPath = getComponentsPath(cfg, projectRoot);
|
|
53
|
+
if (!fs.existsSync(componentsPath)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const files = await fs.promises.readdir(componentsPath);
|
|
57
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
|
58
|
+
// Parallelize reads
|
|
59
|
+
const results = await Promise.all(jsonFiles.map(async (file) => {
|
|
60
|
+
try {
|
|
61
|
+
const filePath = path.join(componentsPath, file);
|
|
62
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
63
|
+
return JSON.parse(content);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
return results.filter((c) => c !== null);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Delete a component by ID
|
|
73
|
+
*/
|
|
74
|
+
export async function deleteComponent(componentId, config, projectRoot) {
|
|
75
|
+
if (!isValidComponentId(componentId)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const cfg = config || getConfig();
|
|
79
|
+
const componentsPath = getComponentsPath(cfg, projectRoot);
|
|
80
|
+
const filePath = path.join(componentsPath, `${componentId}.json`);
|
|
81
|
+
if (!fs.existsSync(filePath)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
await fs.promises.unlink(filePath);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// CONNECTION STORAGE
|
|
89
|
+
// =============================================================================
|
|
90
|
+
/**
|
|
91
|
+
* Store a connection to disk
|
|
92
|
+
*/
|
|
93
|
+
export async function storeConnection(connection, config, projectRoot) {
|
|
94
|
+
const cfg = config || getConfig();
|
|
95
|
+
ensureStorageDirectories(cfg, projectRoot);
|
|
96
|
+
const connectionsPath = getConnectionsPath(cfg, projectRoot);
|
|
97
|
+
const filePath = path.join(connectionsPath, `${connection.connection_id}.json`);
|
|
98
|
+
await fs.promises.writeFile(filePath, JSON.stringify(connection, null, 2), 'utf-8');
|
|
99
|
+
return {
|
|
100
|
+
connection_id: connection.connection_id,
|
|
101
|
+
file_path: filePath,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Load a connection by ID
|
|
106
|
+
*/
|
|
107
|
+
export async function loadConnection(connectionId, config, projectRoot) {
|
|
108
|
+
if (!isValidConnectionId(connectionId)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const cfg = config || getConfig();
|
|
112
|
+
const connectionsPath = getConnectionsPath(cfg, projectRoot);
|
|
113
|
+
const filePath = path.join(connectionsPath, `${connectionId}.json`);
|
|
114
|
+
if (!fs.existsSync(filePath)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
119
|
+
return JSON.parse(content);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Load all connections (parallelized for efficiency)
|
|
127
|
+
*/
|
|
128
|
+
export async function loadAllConnections(config, projectRoot) {
|
|
129
|
+
const cfg = config || getConfig();
|
|
130
|
+
const connectionsPath = getConnectionsPath(cfg, projectRoot);
|
|
131
|
+
if (!fs.existsSync(connectionsPath)) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const files = await fs.promises.readdir(connectionsPath);
|
|
135
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
|
136
|
+
// Parallelize reads
|
|
137
|
+
const results = await Promise.all(jsonFiles.map(async (file) => {
|
|
138
|
+
try {
|
|
139
|
+
const filePath = path.join(connectionsPath, file);
|
|
140
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
141
|
+
return JSON.parse(content);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
return results.filter((c) => c !== null);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Delete a connection by ID
|
|
151
|
+
*/
|
|
152
|
+
export async function deleteConnection(connectionId, config, projectRoot) {
|
|
153
|
+
if (!isValidConnectionId(connectionId)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const cfg = config || getConfig();
|
|
157
|
+
const connectionsPath = getConnectionsPath(cfg, projectRoot);
|
|
158
|
+
const filePath = path.join(connectionsPath, `${connectionId}.json`);
|
|
159
|
+
if (!fs.existsSync(filePath)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
await fs.promises.unlink(filePath);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// INDEX MANAGEMENT
|
|
167
|
+
// =============================================================================
|
|
168
|
+
/**
|
|
169
|
+
* Build and save the index from current components and connections
|
|
170
|
+
*/
|
|
171
|
+
export async function buildIndex(config, projectRoot) {
|
|
172
|
+
const cfg = config || getConfig();
|
|
173
|
+
const components = await loadAllComponents(cfg, projectRoot);
|
|
174
|
+
const connections = await loadAllConnections(cfg, projectRoot);
|
|
175
|
+
const index = {
|
|
176
|
+
version: '1.0.0',
|
|
177
|
+
last_scan: Date.now(),
|
|
178
|
+
project_path: projectRoot || process.cwd(),
|
|
179
|
+
components: {
|
|
180
|
+
by_name: {},
|
|
181
|
+
by_type: {},
|
|
182
|
+
by_layer: {},
|
|
183
|
+
by_status: {},
|
|
184
|
+
},
|
|
185
|
+
connections: {
|
|
186
|
+
by_type: {},
|
|
187
|
+
by_from: {},
|
|
188
|
+
by_to: {},
|
|
189
|
+
},
|
|
190
|
+
stats: {
|
|
191
|
+
total_components: components.length,
|
|
192
|
+
total_connections: connections.length,
|
|
193
|
+
components_by_type: {},
|
|
194
|
+
connections_by_type: {},
|
|
195
|
+
outdated_count: 0,
|
|
196
|
+
vulnerable_count: 0,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
// Index components
|
|
200
|
+
for (const component of components) {
|
|
201
|
+
// By name
|
|
202
|
+
index.components.by_name[component.name] = component.component_id;
|
|
203
|
+
// By type
|
|
204
|
+
if (!index.components.by_type[component.type]) {
|
|
205
|
+
index.components.by_type[component.type] = [];
|
|
206
|
+
}
|
|
207
|
+
index.components.by_type[component.type].push(component.component_id);
|
|
208
|
+
// By layer
|
|
209
|
+
if (!index.components.by_layer[component.role.layer]) {
|
|
210
|
+
index.components.by_layer[component.role.layer] = [];
|
|
211
|
+
}
|
|
212
|
+
index.components.by_layer[component.role.layer].push(component.component_id);
|
|
213
|
+
// By status
|
|
214
|
+
if (!index.components.by_status[component.status]) {
|
|
215
|
+
index.components.by_status[component.status] = [];
|
|
216
|
+
}
|
|
217
|
+
index.components.by_status[component.status].push(component.component_id);
|
|
218
|
+
// Stats
|
|
219
|
+
index.stats.components_by_type[component.type] =
|
|
220
|
+
(index.stats.components_by_type[component.type] || 0) + 1;
|
|
221
|
+
if (component.status === 'outdated')
|
|
222
|
+
index.stats.outdated_count++;
|
|
223
|
+
if (component.status === 'vulnerable')
|
|
224
|
+
index.stats.vulnerable_count++;
|
|
225
|
+
}
|
|
226
|
+
// Index connections
|
|
227
|
+
for (const connection of connections) {
|
|
228
|
+
// By type
|
|
229
|
+
if (!index.connections.by_type[connection.connection_type]) {
|
|
230
|
+
index.connections.by_type[connection.connection_type] = [];
|
|
231
|
+
}
|
|
232
|
+
index.connections.by_type[connection.connection_type].push(connection.connection_id);
|
|
233
|
+
// By from
|
|
234
|
+
if (!index.connections.by_from[connection.from.component_id]) {
|
|
235
|
+
index.connections.by_from[connection.from.component_id] = [];
|
|
236
|
+
}
|
|
237
|
+
index.connections.by_from[connection.from.component_id].push(connection.connection_id);
|
|
238
|
+
// By to
|
|
239
|
+
if (!index.connections.by_to[connection.to.component_id]) {
|
|
240
|
+
index.connections.by_to[connection.to.component_id] = [];
|
|
241
|
+
}
|
|
242
|
+
index.connections.by_to[connection.to.component_id].push(connection.connection_id);
|
|
243
|
+
// Stats
|
|
244
|
+
index.stats.connections_by_type[connection.connection_type] =
|
|
245
|
+
(index.stats.connections_by_type[connection.connection_type] || 0) + 1;
|
|
246
|
+
}
|
|
247
|
+
// Save index
|
|
248
|
+
const indexPath = getIndexPath(cfg, projectRoot);
|
|
249
|
+
await fs.promises.writeFile(indexPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
250
|
+
return index;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Load the index
|
|
254
|
+
*/
|
|
255
|
+
export async function loadIndex(config, projectRoot) {
|
|
256
|
+
const cfg = config || getConfig();
|
|
257
|
+
const indexPath = getIndexPath(cfg, projectRoot);
|
|
258
|
+
if (!fs.existsSync(indexPath)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const content = await fs.promises.readFile(indexPath, 'utf-8');
|
|
263
|
+
return JSON.parse(content);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// =============================================================================
|
|
270
|
+
// GRAPH BUILDING
|
|
271
|
+
// =============================================================================
|
|
272
|
+
/**
|
|
273
|
+
* Build the connection graph
|
|
274
|
+
*/
|
|
275
|
+
export async function buildGraph(config, projectRoot) {
|
|
276
|
+
const cfg = config || getConfig();
|
|
277
|
+
const components = await loadAllComponents(cfg, projectRoot);
|
|
278
|
+
const connections = await loadAllConnections(cfg, projectRoot);
|
|
279
|
+
const nodes = components.map((c) => ({
|
|
280
|
+
id: c.component_id,
|
|
281
|
+
name: c.name,
|
|
282
|
+
type: c.type,
|
|
283
|
+
layer: c.role.layer,
|
|
284
|
+
}));
|
|
285
|
+
const edges = connections.map((c) => ({
|
|
286
|
+
id: c.connection_id,
|
|
287
|
+
source: c.from.component_id,
|
|
288
|
+
target: c.to.component_id,
|
|
289
|
+
type: c.connection_type,
|
|
290
|
+
label: c.description,
|
|
291
|
+
}));
|
|
292
|
+
const graph = {
|
|
293
|
+
nodes,
|
|
294
|
+
edges,
|
|
295
|
+
metadata: {
|
|
296
|
+
generated_at: Date.now(),
|
|
297
|
+
component_count: nodes.length,
|
|
298
|
+
connection_count: edges.length,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
// Save graph
|
|
302
|
+
const graphPath = getGraphPath(cfg, projectRoot);
|
|
303
|
+
await fs.promises.writeFile(graphPath, JSON.stringify(graph, null, 2), 'utf-8');
|
|
304
|
+
return graph;
|
|
305
|
+
}
|
|
306
|
+
// =============================================================================
|
|
307
|
+
// FILE MAP (Tier 2 - O(1) file-to-component lookup)
|
|
308
|
+
// =============================================================================
|
|
309
|
+
/**
|
|
310
|
+
* Build a map of file paths → component IDs for fast lookup in hooks.
|
|
311
|
+
* Sources: component config_files + connection code_reference files + connection locations.
|
|
312
|
+
*/
|
|
313
|
+
export async function buildFileMap(config, projectRoot) {
|
|
314
|
+
const cfg = config || getConfig();
|
|
315
|
+
const root = projectRoot || process.cwd();
|
|
316
|
+
const components = await loadAllComponents(cfg, root);
|
|
317
|
+
const connections = await loadAllConnections(cfg, root);
|
|
318
|
+
const fileMap = {};
|
|
319
|
+
// Index config files from components
|
|
320
|
+
for (const c of components) {
|
|
321
|
+
for (const f of c.source.config_files || []) {
|
|
322
|
+
fileMap[f] = c.component_id;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Index source files from connections (code_reference, from.location, to.location)
|
|
326
|
+
for (const conn of connections) {
|
|
327
|
+
if (conn.code_reference?.file) {
|
|
328
|
+
// Map to the "from" component — this file uses/imports the dependency
|
|
329
|
+
fileMap[conn.code_reference.file] = conn.from.component_id;
|
|
330
|
+
}
|
|
331
|
+
if (conn.from.location?.file) {
|
|
332
|
+
fileMap[conn.from.location.file] = conn.from.component_id;
|
|
333
|
+
}
|
|
334
|
+
if (conn.to.location?.file) {
|
|
335
|
+
fileMap[conn.to.location.file] = conn.to.component_id;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const fileMapPath = getFileMapPath(cfg, root);
|
|
339
|
+
await fs.promises.writeFile(fileMapPath, JSON.stringify(fileMap, null, 2), 'utf-8');
|
|
340
|
+
return fileMap;
|
|
341
|
+
}
|
|
342
|
+
// =============================================================================
|
|
343
|
+
// PROMPT STORAGE (Tier 2 - Full prompt content for on-demand loading)
|
|
344
|
+
// =============================================================================
|
|
345
|
+
/**
|
|
346
|
+
* Save prompt scan results to prompts.json
|
|
347
|
+
*/
|
|
348
|
+
export async function savePromptScan(promptData, config, projectRoot) {
|
|
349
|
+
const cfg = config || getConfig();
|
|
350
|
+
const root = projectRoot || process.cwd();
|
|
351
|
+
ensureStorageDirectories(cfg, root);
|
|
352
|
+
const promptsPath = getPromptsPath(cfg, root);
|
|
353
|
+
await fs.promises.writeFile(promptsPath, JSON.stringify(promptData, null, 2), 'utf-8');
|
|
354
|
+
}
|
|
355
|
+
// =============================================================================
|
|
356
|
+
// SUMMARY GENERATION (Tier 1 - Hot Context for LLMs)
|
|
357
|
+
// =============================================================================
|
|
358
|
+
const AI_PROVIDER_NAMES = new Set([
|
|
359
|
+
'openai', '@anthropic-ai/sdk', '@langchain/core', '@langchain/openai',
|
|
360
|
+
'@langchain/anthropic', '@langchain/groq', 'groq-sdk', 'langsmith',
|
|
361
|
+
'@mistralai/mistralai', 'replicate', '@huggingface/inference',
|
|
362
|
+
'@google/generative-ai', '@vercel/ai', 'ai', 'cohere-ai',
|
|
363
|
+
]);
|
|
364
|
+
/**
|
|
365
|
+
* Build a concise markdown summary with pointers to detail files.
|
|
366
|
+
* This is the "hot context" an LLM reads first on cold start.
|
|
367
|
+
*/
|
|
368
|
+
export async function buildSummary(config, projectRoot, promptScan) {
|
|
369
|
+
const cfg = config || getConfig();
|
|
370
|
+
const root = projectRoot || process.cwd();
|
|
371
|
+
const components = await loadAllComponents(cfg, root);
|
|
372
|
+
const connections = await loadAllConnections(cfg, root);
|
|
373
|
+
const now = new Date().toISOString();
|
|
374
|
+
const aiComponents = components.filter((c) => AI_PROVIDER_NAMES.has(c.name) || c.type === 'llm' || c.type === 'service');
|
|
375
|
+
// Group components by layer
|
|
376
|
+
const byLayer = new Map();
|
|
377
|
+
for (const c of components) {
|
|
378
|
+
const layer = c.role.layer;
|
|
379
|
+
if (!byLayer.has(layer))
|
|
380
|
+
byLayer.set(layer, []);
|
|
381
|
+
byLayer.get(layer).push(c);
|
|
382
|
+
}
|
|
383
|
+
// Build markdown
|
|
384
|
+
const lines = [];
|
|
385
|
+
lines.push('# Architecture Summary');
|
|
386
|
+
lines.push(`> NavGator auto-generated | Scanned: ${now}`);
|
|
387
|
+
lines.push(`> ${components.length} components | ${connections.length} connections | ${aiComponents.length} AI providers`);
|
|
388
|
+
lines.push('');
|
|
389
|
+
// Components by layer
|
|
390
|
+
lines.push('## Components');
|
|
391
|
+
lines.push('');
|
|
392
|
+
const layerOrder = ['frontend', 'backend', 'database', 'queue', 'infra', 'external'];
|
|
393
|
+
for (const layer of layerOrder) {
|
|
394
|
+
const group = byLayer.get(layer);
|
|
395
|
+
if (!group || group.length === 0)
|
|
396
|
+
continue;
|
|
397
|
+
lines.push(`### ${layer.charAt(0).toUpperCase() + layer.slice(1)} (${group.length})`);
|
|
398
|
+
for (const c of group) {
|
|
399
|
+
const ver = c.version ? ` v${c.version}` : '';
|
|
400
|
+
lines.push(`- **${c.name}**${ver} — ${c.role.purpose} \`components/${c.component_id}.json\``);
|
|
401
|
+
}
|
|
402
|
+
lines.push('');
|
|
403
|
+
}
|
|
404
|
+
// AI/LLM routing table
|
|
405
|
+
if (aiComponents.length > 0 || connections.some((c) => c.connection_type === 'service-call')) {
|
|
406
|
+
lines.push('## AI/LLM Routing');
|
|
407
|
+
lines.push('| Provider | File | Line | Purpose | Detail |');
|
|
408
|
+
lines.push('|----------|------|------|---------|--------|');
|
|
409
|
+
const aiConnections = connections.filter((c) => {
|
|
410
|
+
const targetComp = components.find((comp) => comp.component_id === c.to.component_id);
|
|
411
|
+
return targetComp && (AI_PROVIDER_NAMES.has(targetComp.name) || targetComp.type === 'llm' || targetComp.type === 'service');
|
|
412
|
+
});
|
|
413
|
+
for (const conn of aiConnections) {
|
|
414
|
+
const target = components.find((comp) => comp.component_id === conn.to.component_id);
|
|
415
|
+
const file = conn.code_reference?.file || conn.from.location?.file || '—';
|
|
416
|
+
const line = conn.code_reference?.line_start || conn.from.location?.line || '—';
|
|
417
|
+
const purpose = conn.description || target?.role.purpose || '—';
|
|
418
|
+
lines.push(`| ${target?.name || '?'} | ${file} | ${line} | ${purpose} | \`connections/${conn.connection_id}.json\` |`);
|
|
419
|
+
}
|
|
420
|
+
// Also list AI components with no connections yet
|
|
421
|
+
for (const c of aiComponents) {
|
|
422
|
+
const hasConn = connections.some((conn) => conn.to.component_id === c.component_id);
|
|
423
|
+
if (!hasConn) {
|
|
424
|
+
const configFile = c.source.config_files?.[0] || '—';
|
|
425
|
+
lines.push(`| ${c.name} | ${configFile} | — | ${c.role.purpose} | \`components/${c.component_id}.json\` |`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
lines.push('');
|
|
429
|
+
}
|
|
430
|
+
// Top connections (cap at 20)
|
|
431
|
+
if (connections.length > 0) {
|
|
432
|
+
const maxConns = Math.min(connections.length, 20);
|
|
433
|
+
lines.push(`## Connections (${connections.length > 20 ? `top 20 of ${connections.length}` : connections.length})`);
|
|
434
|
+
for (let i = 0; i < maxConns; i++) {
|
|
435
|
+
const conn = connections[i];
|
|
436
|
+
const fromComp = components.find((c) => c.component_id === conn.from.component_id);
|
|
437
|
+
const toComp = components.find((c) => c.component_id === conn.to.component_id);
|
|
438
|
+
const file = conn.code_reference?.file || '';
|
|
439
|
+
const line = conn.code_reference?.line_start ? `:${conn.code_reference.line_start}` : '';
|
|
440
|
+
lines.push(`- ${fromComp?.name || '?'} → ${toComp?.name || '?'} (${conn.connection_type}) ${file}${line}`);
|
|
441
|
+
}
|
|
442
|
+
lines.push('');
|
|
443
|
+
}
|
|
444
|
+
// Delta — compare with previous summary
|
|
445
|
+
const summaryPath = getSummaryPath(cfg, root);
|
|
446
|
+
if (fs.existsSync(summaryPath)) {
|
|
447
|
+
try {
|
|
448
|
+
const prev = await fs.promises.readFile(summaryPath, 'utf-8');
|
|
449
|
+
const prevNames = new Set();
|
|
450
|
+
for (const match of prev.matchAll(/^- \*\*(.+?)\*\*/gm)) {
|
|
451
|
+
prevNames.add(match[1]);
|
|
452
|
+
}
|
|
453
|
+
const currentNames = new Set(components.map((c) => c.name));
|
|
454
|
+
const added = components.filter((c) => !prevNames.has(c.name));
|
|
455
|
+
const removed = [...prevNames].filter((n) => !currentNames.has(n));
|
|
456
|
+
if (added.length > 0 || removed.length > 0) {
|
|
457
|
+
lines.push('## Changes Since Last Scan');
|
|
458
|
+
for (const c of added) {
|
|
459
|
+
lines.push(`- Added: \`${c.name}\` (${c.role.layer})`);
|
|
460
|
+
}
|
|
461
|
+
for (const name of removed) {
|
|
462
|
+
lines.push(`- Removed: \`${name}\``);
|
|
463
|
+
}
|
|
464
|
+
lines.push('');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// First scan or parse error — skip delta
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Prompts section (pointers only — full content in prompts.json)
|
|
472
|
+
if (promptScan && promptScan.prompts.length > 0) {
|
|
473
|
+
lines.push(`## Prompts (${promptScan.prompts.length}) — full content: \`prompts.json\``);
|
|
474
|
+
lines.push('| Name | File | Line | Provider | Category |');
|
|
475
|
+
lines.push('|------|------|------|----------|----------|');
|
|
476
|
+
const maxPrompts = Math.min(promptScan.prompts.length, 20);
|
|
477
|
+
for (let i = 0; i < maxPrompts; i++) {
|
|
478
|
+
const p = promptScan.prompts[i];
|
|
479
|
+
const provider = p.provider?.provider || '—';
|
|
480
|
+
const model = p.provider?.model ? ` (${p.provider.model})` : '';
|
|
481
|
+
const cat = p.category || '—';
|
|
482
|
+
lines.push(`| ${p.name} | ${p.location.file} | ${p.location.lineStart} | ${provider}${model} | ${cat} |`);
|
|
483
|
+
}
|
|
484
|
+
if (promptScan.prompts.length > 20) {
|
|
485
|
+
lines.push(`| ... | | | ${promptScan.prompts.length - 20} more in prompts.json | |`);
|
|
486
|
+
}
|
|
487
|
+
lines.push('');
|
|
488
|
+
}
|
|
489
|
+
// Detail pointers
|
|
490
|
+
lines.push('## Detail Pointers');
|
|
491
|
+
lines.push(`- Full index: \`index.json\``);
|
|
492
|
+
lines.push(`- Connection graph: \`graph.json\``);
|
|
493
|
+
lines.push(`- File map: \`file_map.json\``);
|
|
494
|
+
if (promptScan && promptScan.prompts.length > 0) {
|
|
495
|
+
lines.push(`- Prompts: \`prompts.json\` (${promptScan.prompts.length} prompts, full content)`);
|
|
496
|
+
}
|
|
497
|
+
lines.push(`- All components: \`components/\` (${components.length} files)`);
|
|
498
|
+
lines.push(`- All connections: \`connections/\` (${connections.length} files)`);
|
|
499
|
+
lines.push('');
|
|
500
|
+
const fullContent = lines.join('\n');
|
|
501
|
+
const lineCount = lines.length;
|
|
502
|
+
const COMPRESSION_THRESHOLD = 150;
|
|
503
|
+
if (lineCount > COMPRESSION_THRESHOLD) {
|
|
504
|
+
// Write full version to SUMMARY_FULL.md
|
|
505
|
+
const fullPath = getSummaryFullPath(cfg, root);
|
|
506
|
+
await fs.promises.writeFile(fullPath, fullContent, 'utf-8');
|
|
507
|
+
// Build compressed version: top 10 per layer, AI routing, top 10 connections
|
|
508
|
+
const compressed = [];
|
|
509
|
+
compressed.push('# Architecture Summary (Compressed)');
|
|
510
|
+
compressed.push('');
|
|
511
|
+
compressed.push('> **This is a compressed summary.** Full version: `SUMMARY_FULL.md`');
|
|
512
|
+
compressed.push('');
|
|
513
|
+
compressed.push(`> NavGator auto-generated | Scanned: ${now}`);
|
|
514
|
+
compressed.push(`> ${components.length} components | ${connections.length} connections | ${aiComponents.length} AI providers`);
|
|
515
|
+
compressed.push('');
|
|
516
|
+
// Components (top 10 per layer)
|
|
517
|
+
const hasLayerContent = layerOrder.some((l) => (byLayer.get(l)?.length || 0) > 0);
|
|
518
|
+
if (hasLayerContent) {
|
|
519
|
+
compressed.push('## Components (top 10 per layer)');
|
|
520
|
+
compressed.push('');
|
|
521
|
+
for (const layer of layerOrder) {
|
|
522
|
+
const group = byLayer.get(layer);
|
|
523
|
+
if (!group || group.length === 0)
|
|
524
|
+
continue;
|
|
525
|
+
compressed.push(`### ${layer.charAt(0).toUpperCase() + layer.slice(1)} (${group.length})`);
|
|
526
|
+
const top = group.slice(0, 10);
|
|
527
|
+
for (const c of top) {
|
|
528
|
+
const ver = c.version ? ` v${c.version}` : '';
|
|
529
|
+
compressed.push(`- **${c.name}**${ver} — ${c.role.purpose} \`components/${c.component_id}.json\``);
|
|
530
|
+
}
|
|
531
|
+
if (group.length > 10) {
|
|
532
|
+
compressed.push(`- ... and ${group.length - 10} more (see SUMMARY_FULL.md)`);
|
|
533
|
+
}
|
|
534
|
+
compressed.push('');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// AI/LLM routing table (preserved in compressed version)
|
|
538
|
+
if (aiComponents.length > 0 || connections.some((c) => c.connection_type === 'service-call')) {
|
|
539
|
+
compressed.push('## AI/LLM Routing');
|
|
540
|
+
compressed.push('| Provider | File | Line | Purpose | Detail |');
|
|
541
|
+
compressed.push('|----------|------|------|---------|--------|');
|
|
542
|
+
const aiConnections = connections.filter((c) => {
|
|
543
|
+
const targetComp = components.find((comp) => comp.component_id === c.to.component_id);
|
|
544
|
+
return targetComp && (AI_PROVIDER_NAMES.has(targetComp.name) || targetComp.type === 'llm' || targetComp.type === 'service');
|
|
545
|
+
});
|
|
546
|
+
const maxAiConns = Math.min(aiConnections.length, 10);
|
|
547
|
+
for (let i = 0; i < maxAiConns; i++) {
|
|
548
|
+
const conn = aiConnections[i];
|
|
549
|
+
const target = components.find((comp) => comp.component_id === conn.to.component_id);
|
|
550
|
+
const file = conn.code_reference?.file || conn.from.location?.file || '—';
|
|
551
|
+
const line = conn.code_reference?.line_start || conn.from.location?.line || '—';
|
|
552
|
+
const purpose = conn.description || target?.role.purpose || '—';
|
|
553
|
+
compressed.push(`| ${target?.name || '?'} | ${file} | ${line} | ${purpose} | \`connections/${conn.connection_id}.json\` |`);
|
|
554
|
+
}
|
|
555
|
+
if (aiConnections.length > 10) {
|
|
556
|
+
compressed.push(`| ... | | | ${aiConnections.length - 10} more (see SUMMARY_FULL.md) | |`);
|
|
557
|
+
}
|
|
558
|
+
// AI components with no connections
|
|
559
|
+
for (const c of aiComponents) {
|
|
560
|
+
const hasConn = connections.some((conn) => conn.to.component_id === c.component_id);
|
|
561
|
+
if (!hasConn) {
|
|
562
|
+
const configFile = c.source.config_files?.[0] || '—';
|
|
563
|
+
compressed.push(`| ${c.name} | ${configFile} | — | ${c.role.purpose} | \`components/${c.component_id}.json\` |`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
compressed.push('');
|
|
567
|
+
}
|
|
568
|
+
// Connections (top 10)
|
|
569
|
+
if (connections.length > 0) {
|
|
570
|
+
const maxConns = Math.min(connections.length, 10);
|
|
571
|
+
compressed.push(`## Connections (top 10 of ${connections.length})`);
|
|
572
|
+
for (let i = 0; i < maxConns; i++) {
|
|
573
|
+
const conn = connections[i];
|
|
574
|
+
const fromComp = components.find((c) => c.component_id === conn.from.component_id);
|
|
575
|
+
const toComp = components.find((c) => c.component_id === conn.to.component_id);
|
|
576
|
+
const file = conn.code_reference?.file || '';
|
|
577
|
+
const line = conn.code_reference?.line_start ? `:${conn.code_reference.line_start}` : '';
|
|
578
|
+
compressed.push(`- ${fromComp?.name || '?'} → ${toComp?.name || '?'} (${conn.connection_type}) ${file}${line}`);
|
|
579
|
+
}
|
|
580
|
+
compressed.push('');
|
|
581
|
+
}
|
|
582
|
+
// Add prompts pointer if available
|
|
583
|
+
if (promptScan && promptScan.prompts.length > 0) {
|
|
584
|
+
compressed.push(`## Prompts (${promptScan.prompts.length}) — full content: \`prompts.json\``);
|
|
585
|
+
compressed.push('| Name | File | Provider |');
|
|
586
|
+
compressed.push('|------|------|----------|');
|
|
587
|
+
const maxP = Math.min(promptScan.prompts.length, 10);
|
|
588
|
+
for (let i = 0; i < maxP; i++) {
|
|
589
|
+
const p = promptScan.prompts[i];
|
|
590
|
+
const name = p.name || '?';
|
|
591
|
+
const file = p.location?.file ? `${p.location.file}:${p.location.lineStart}` : '?';
|
|
592
|
+
const provider = p.provider?.provider || 'unknown';
|
|
593
|
+
compressed.push(`| ${name} | ${file} | ${provider} |`);
|
|
594
|
+
}
|
|
595
|
+
if (promptScan.prompts.length > 10) {
|
|
596
|
+
compressed.push(`| ... | +${promptScan.prompts.length - 10} more in prompts.json | |`);
|
|
597
|
+
}
|
|
598
|
+
compressed.push('');
|
|
599
|
+
}
|
|
600
|
+
compressed.push('## Detail Pointers');
|
|
601
|
+
compressed.push('- **Full summary**: `SUMMARY_FULL.md`');
|
|
602
|
+
compressed.push(`- Full index: \`index.json\``);
|
|
603
|
+
compressed.push(`- Connection graph: \`graph.json\``);
|
|
604
|
+
compressed.push(`- File map: \`file_map.json\``);
|
|
605
|
+
compressed.push(`- Prompts: \`prompts.json\` (${promptScan?.prompts?.length || 0} prompts, full content)`);
|
|
606
|
+
compressed.push(`- All components: \`components/\` (${components.length} files)`);
|
|
607
|
+
compressed.push(`- All connections: \`connections/\` (${connections.length} files)`);
|
|
608
|
+
compressed.push('');
|
|
609
|
+
const compressedContent = compressed.join('\n');
|
|
610
|
+
await fs.promises.writeFile(summaryPath, compressedContent, 'utf-8');
|
|
611
|
+
return compressedContent;
|
|
612
|
+
}
|
|
613
|
+
const content = lines.join('\n');
|
|
614
|
+
await fs.promises.writeFile(summaryPath, content, 'utf-8');
|
|
615
|
+
return content;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Load the graph
|
|
619
|
+
*/
|
|
620
|
+
export async function loadGraph(config, projectRoot) {
|
|
621
|
+
const cfg = config || getConfig();
|
|
622
|
+
const graphPath = getGraphPath(cfg, projectRoot);
|
|
623
|
+
if (!fs.existsSync(graphPath)) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
const content = await fs.promises.readFile(graphPath, 'utf-8');
|
|
628
|
+
return JSON.parse(content);
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// =============================================================================
|
|
635
|
+
// SNAPSHOTS
|
|
636
|
+
// =============================================================================
|
|
637
|
+
/**
|
|
638
|
+
* Create a snapshot of current architecture
|
|
639
|
+
*/
|
|
640
|
+
export async function createSnapshot(reason, config, projectRoot) {
|
|
641
|
+
const cfg = config || getConfig();
|
|
642
|
+
ensureStorageDirectories(cfg, projectRoot);
|
|
643
|
+
const components = await loadAllComponents(cfg, projectRoot);
|
|
644
|
+
const connections = await loadAllConnections(cfg, projectRoot);
|
|
645
|
+
const timestamp = Date.now();
|
|
646
|
+
const snapshotId = `SNAP_${new Date(timestamp).toISOString().replace(/[-:T.Z]/g, '').slice(0, 14)}`;
|
|
647
|
+
const snapshot = {
|
|
648
|
+
snapshot_id: snapshotId,
|
|
649
|
+
timestamp,
|
|
650
|
+
reason,
|
|
651
|
+
components: components.map((c) => ({
|
|
652
|
+
component_id: c.component_id,
|
|
653
|
+
name: c.name,
|
|
654
|
+
type: c.type,
|
|
655
|
+
version: c.version,
|
|
656
|
+
status: c.status,
|
|
657
|
+
})),
|
|
658
|
+
connections: connections.map((c) => ({
|
|
659
|
+
connection_id: c.connection_id,
|
|
660
|
+
from: c.from.component_id,
|
|
661
|
+
to: c.to.component_id,
|
|
662
|
+
type: c.connection_type,
|
|
663
|
+
})),
|
|
664
|
+
stats: {
|
|
665
|
+
total_components: components.length,
|
|
666
|
+
total_connections: connections.length,
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
const snapshotsPath = getSnapshotsPath(cfg, projectRoot);
|
|
670
|
+
const filePath = path.join(snapshotsPath, `${snapshotId}.json`);
|
|
671
|
+
await fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
672
|
+
return {
|
|
673
|
+
snapshot_id: snapshotId,
|
|
674
|
+
file_path: filePath,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
// =============================================================================
|
|
678
|
+
// BULK OPERATIONS
|
|
679
|
+
// =============================================================================
|
|
680
|
+
/**
|
|
681
|
+
* Store multiple components at once (parallelized for efficiency)
|
|
682
|
+
*/
|
|
683
|
+
export async function storeComponents(components, config, projectRoot) {
|
|
684
|
+
const cfg = config || getConfig();
|
|
685
|
+
ensureStorageDirectories(cfg, projectRoot);
|
|
686
|
+
const componentsPath = getComponentsPath(cfg, projectRoot);
|
|
687
|
+
// Parallelize writes in batches to avoid overwhelming the filesystem
|
|
688
|
+
const batchSize = 50;
|
|
689
|
+
for (let i = 0; i < components.length; i += batchSize) {
|
|
690
|
+
const batch = components.slice(i, i + batchSize);
|
|
691
|
+
await Promise.all(batch.map(async (component) => {
|
|
692
|
+
const filePath = path.join(componentsPath, `${component.component_id}.json`);
|
|
693
|
+
await fs.promises.writeFile(filePath, JSON.stringify(component, null, 2), 'utf-8');
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Store multiple connections at once (parallelized for efficiency)
|
|
699
|
+
*/
|
|
700
|
+
export async function storeConnections(connections, config, projectRoot) {
|
|
701
|
+
const cfg = config || getConfig();
|
|
702
|
+
ensureStorageDirectories(cfg, projectRoot);
|
|
703
|
+
const connectionsPath = getConnectionsPath(cfg, projectRoot);
|
|
704
|
+
// Parallelize writes in batches to avoid overwhelming the filesystem
|
|
705
|
+
const batchSize = 50;
|
|
706
|
+
for (let i = 0; i < connections.length; i += batchSize) {
|
|
707
|
+
const batch = connections.slice(i, i + batchSize);
|
|
708
|
+
await Promise.all(batch.map(async (connection) => {
|
|
709
|
+
const filePath = path.join(connectionsPath, `${connection.connection_id}.json`);
|
|
710
|
+
await fs.promises.writeFile(filePath, JSON.stringify(connection, null, 2), 'utf-8');
|
|
711
|
+
}));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Clear all stored data (parallelized for efficiency)
|
|
716
|
+
*/
|
|
717
|
+
export async function clearStorage(config, projectRoot) {
|
|
718
|
+
const cfg = config || getConfig();
|
|
719
|
+
const componentsPath = getComponentsPath(cfg, projectRoot);
|
|
720
|
+
const connectionsPath = getConnectionsPath(cfg, projectRoot);
|
|
721
|
+
const indexPath = getIndexPath(cfg, projectRoot);
|
|
722
|
+
const graphPath = getGraphPath(cfg, projectRoot);
|
|
723
|
+
const deletePromises = [];
|
|
724
|
+
// Delete all component files
|
|
725
|
+
if (fs.existsSync(componentsPath)) {
|
|
726
|
+
const componentFiles = await fs.promises.readdir(componentsPath);
|
|
727
|
+
deletePromises.push(...componentFiles.map((file) => fs.promises.unlink(path.join(componentsPath, file)).catch(() => { })));
|
|
728
|
+
}
|
|
729
|
+
// Delete all connection files
|
|
730
|
+
if (fs.existsSync(connectionsPath)) {
|
|
731
|
+
const connectionFiles = await fs.promises.readdir(connectionsPath);
|
|
732
|
+
deletePromises.push(...connectionFiles.map((file) => fs.promises.unlink(path.join(connectionsPath, file)).catch(() => { })));
|
|
733
|
+
}
|
|
734
|
+
// Delete index and graph
|
|
735
|
+
if (fs.existsSync(indexPath)) {
|
|
736
|
+
deletePromises.push(fs.promises.unlink(indexPath).catch(() => { }));
|
|
737
|
+
}
|
|
738
|
+
if (fs.existsSync(graphPath)) {
|
|
739
|
+
deletePromises.push(fs.promises.unlink(graphPath).catch(() => { }));
|
|
740
|
+
}
|
|
741
|
+
await Promise.all(deletePromises);
|
|
742
|
+
}
|
|
743
|
+
// =============================================================================
|
|
744
|
+
// STATISTICS
|
|
745
|
+
// =============================================================================
|
|
746
|
+
/**
|
|
747
|
+
* Get storage statistics
|
|
748
|
+
*/
|
|
749
|
+
export async function getStorageStats(config, projectRoot) {
|
|
750
|
+
const cfg = config || getConfig();
|
|
751
|
+
const components = await loadAllComponents(cfg, projectRoot);
|
|
752
|
+
const connections = await loadAllConnections(cfg, projectRoot);
|
|
753
|
+
// Calculate disk usage
|
|
754
|
+
const componentsPath = getComponentsPath(cfg, projectRoot);
|
|
755
|
+
const connectionsPath = getConnectionsPath(cfg, projectRoot);
|
|
756
|
+
let diskUsage = 0;
|
|
757
|
+
if (fs.existsSync(componentsPath)) {
|
|
758
|
+
const files = await fs.promises.readdir(componentsPath);
|
|
759
|
+
for (const file of files) {
|
|
760
|
+
const stats = await fs.promises.stat(path.join(componentsPath, file));
|
|
761
|
+
diskUsage += stats.size;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (fs.existsSync(connectionsPath)) {
|
|
765
|
+
const files = await fs.promises.readdir(connectionsPath);
|
|
766
|
+
for (const file of files) {
|
|
767
|
+
const stats = await fs.promises.stat(path.join(connectionsPath, file));
|
|
768
|
+
diskUsage += stats.size;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// Find oldest and newest timestamps
|
|
772
|
+
const allTimestamps = [
|
|
773
|
+
...components.map((c) => c.timestamp),
|
|
774
|
+
...connections.map((c) => c.timestamp),
|
|
775
|
+
];
|
|
776
|
+
return {
|
|
777
|
+
total_components: components.length,
|
|
778
|
+
total_connections: connections.length,
|
|
779
|
+
disk_usage_kb: Math.round(diskUsage / 1024),
|
|
780
|
+
oldest_timestamp: allTimestamps.length > 0 ? Math.min(...allTimestamps) : null,
|
|
781
|
+
newest_timestamp: allTimestamps.length > 0 ? Math.max(...allTimestamps) : null,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
// =============================================================================
|
|
785
|
+
// FILE HASH TRACKING
|
|
786
|
+
// =============================================================================
|
|
787
|
+
/**
|
|
788
|
+
* Compute SHA-256 hash of a file
|
|
789
|
+
*/
|
|
790
|
+
export async function computeFileHash(filePath) {
|
|
791
|
+
const content = await fs.promises.readFile(filePath);
|
|
792
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Compute hashes for multiple files (parallelized in batches for efficiency)
|
|
796
|
+
*/
|
|
797
|
+
export async function computeFileHashes(files, projectRoot) {
|
|
798
|
+
const hashes = {};
|
|
799
|
+
const timestamp = Date.now();
|
|
800
|
+
// Process in batches to avoid too many open file handles
|
|
801
|
+
const batchSize = 100;
|
|
802
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
803
|
+
const batch = files.slice(i, i + batchSize);
|
|
804
|
+
const results = await Promise.all(batch.map(async (file) => {
|
|
805
|
+
const filePath = path.join(projectRoot, file);
|
|
806
|
+
try {
|
|
807
|
+
const stats = await fs.promises.stat(filePath);
|
|
808
|
+
if (!stats.isFile())
|
|
809
|
+
return null;
|
|
810
|
+
const hash = await computeFileHash(filePath);
|
|
811
|
+
return {
|
|
812
|
+
file,
|
|
813
|
+
record: {
|
|
814
|
+
hash,
|
|
815
|
+
lastScanned: timestamp,
|
|
816
|
+
size: stats.size,
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}));
|
|
824
|
+
for (const result of results) {
|
|
825
|
+
if (result) {
|
|
826
|
+
hashes[result.file] = result.record;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return hashes;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Save file hashes to disk
|
|
834
|
+
*/
|
|
835
|
+
export async function saveHashes(hashes, config, projectRoot) {
|
|
836
|
+
const cfg = config || getConfig();
|
|
837
|
+
const root = projectRoot || process.cwd();
|
|
838
|
+
ensureStorageDirectories(cfg, root);
|
|
839
|
+
const navHashes = {
|
|
840
|
+
version: '1.0',
|
|
841
|
+
generatedAt: Date.now(),
|
|
842
|
+
projectPath: root,
|
|
843
|
+
files: hashes,
|
|
844
|
+
};
|
|
845
|
+
const hashesPath = getHashesPath(cfg, root);
|
|
846
|
+
await fs.promises.writeFile(hashesPath, JSON.stringify(navHashes, null, 2), 'utf-8');
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Load file hashes from disk
|
|
850
|
+
*/
|
|
851
|
+
export async function loadHashes(config, projectRoot) {
|
|
852
|
+
const cfg = config || getConfig();
|
|
853
|
+
const hashesPath = getHashesPath(cfg, projectRoot);
|
|
854
|
+
if (!fs.existsSync(hashesPath)) {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
const content = await fs.promises.readFile(hashesPath, 'utf-8');
|
|
859
|
+
return JSON.parse(content);
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Detect which files have changed since last scan
|
|
867
|
+
*/
|
|
868
|
+
export async function detectFileChanges(currentFiles, projectRoot, config) {
|
|
869
|
+
const cfg = config || getConfig();
|
|
870
|
+
const previousHashes = await loadHashes(cfg, projectRoot);
|
|
871
|
+
const result = {
|
|
872
|
+
added: [],
|
|
873
|
+
modified: [],
|
|
874
|
+
removed: [],
|
|
875
|
+
unchanged: [],
|
|
876
|
+
};
|
|
877
|
+
// No previous scan - all files are new
|
|
878
|
+
if (!previousHashes) {
|
|
879
|
+
result.added = [...currentFiles];
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
const previousFiles = new Set(Object.keys(previousHashes.files));
|
|
883
|
+
// Check current files
|
|
884
|
+
for (const file of currentFiles) {
|
|
885
|
+
const filePath = path.join(projectRoot, file);
|
|
886
|
+
if (!previousFiles.has(file)) {
|
|
887
|
+
// New file
|
|
888
|
+
result.added.push(file);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
// File existed before - check if modified
|
|
892
|
+
try {
|
|
893
|
+
const currentHash = await computeFileHash(filePath);
|
|
894
|
+
if (currentHash !== previousHashes.files[file].hash) {
|
|
895
|
+
result.modified.push(file);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
result.unchanged.push(file);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
// Can't read file, treat as modified
|
|
903
|
+
result.modified.push(file);
|
|
904
|
+
}
|
|
905
|
+
previousFiles.delete(file);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// Remaining files in previousFiles were removed
|
|
909
|
+
result.removed = Array.from(previousFiles);
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Get a summary of file changes for display
|
|
914
|
+
*/
|
|
915
|
+
export function formatFileChangeSummary(changes) {
|
|
916
|
+
const parts = [];
|
|
917
|
+
if (changes.added.length > 0) {
|
|
918
|
+
parts.push(`${changes.added.length} added`);
|
|
919
|
+
}
|
|
920
|
+
if (changes.modified.length > 0) {
|
|
921
|
+
parts.push(`${changes.modified.length} modified`);
|
|
922
|
+
}
|
|
923
|
+
if (changes.removed.length > 0) {
|
|
924
|
+
parts.push(`${changes.removed.length} removed`);
|
|
925
|
+
}
|
|
926
|
+
if (parts.length === 0) {
|
|
927
|
+
return 'No files changed';
|
|
928
|
+
}
|
|
929
|
+
return parts.join(', ');
|
|
930
|
+
}
|
|
931
|
+
//# sourceMappingURL=storage.js.map
|